Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 12 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,17 @@ and [`app/consumer/app.go`](app/consumer/app.go) for reference.

### Kept from ICS

| Feature | Description |
| ------------------------ | ----------------------------------------------------------------------------------- |
| Consumer Lifecycle | Full lifecycle management (REGISTERED → INITIALIZED → LAUNCHED → STOPPED → DELETED) |
| Key Assignment | Validators can use different consensus keys per consumer chain |
| Slashing Parameters | Uses provider defaults; per-consumer customization removed |
| VSC Packets | Validator set updates sent at epoch boundaries |
| Double Voting Evidence | Handle double voting evidence from consumers |
| Light Client Misbehavior | Detection and logging of misbehavior |
| Consumer Metadata | Name, description, metadata for chain discovery |
| Client/Connection Reuse | Reuse existing IBC client when creating consumer |
| Feature | Description |
| ---------------------------------- | ----------------------------------------------------------------------------------- |
| Consumer Lifecycle | Full lifecycle management (REGISTERED → INITIALIZED → LAUNCHED → STOPPED → DELETED) |
| Key Assignment | Validators can use different consensus keys per consumer chain |
| Per-Consumer Infraction Parameters | Customizable slash/jail parameters per consumer |
| VSC Packets | Validator set updates sent at epoch boundaries |
| Double Voting Evidence | Handle double voting evidence from consumers |
| Downtime Slashing | Consumer detects offline validators, sends slash packet to provider via IBC |
| Light Client Misbehavior | Detection and logging of misbehavior |
| Consumer Metadata | Name, description, metadata for chain discovery |
| Client/Connection Reuse | Reuse existing IBC client when creating consumer |

### Removed from ICS

Expand All @@ -42,7 +43,7 @@ and [`app/consumer/app.go`](app/consumer/app.go) for reference.
| Top N / Opt-In Chains | No validator selection per consumer |
| Power Shaping | No caps, allowlists, denylists, priority lists |
| Consumer Reward Distribution | No cross-chain rewards |
| Slash Packet Throttling | Simplified slash handling |
| Slash Packet Throttling | No rate-limiting across consumers |
| Per-Consumer Commission Rates | Validators use same commission as provider |
| IBC v1 Channel Support | IBC v2 only |
| Standalone-to-Consumer Changeover | Not currently supported (future work) |
Expand Down
1 change: 1 addition & 0 deletions app/consumer/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,7 @@ func New(
runtime.NewKVStoreService(keys[ibcconsumertypes.StoreKey]),
app.IBCKeeper.ClientKeeper,
app.IBCKeeper.ClientV2Keeper,
app.IBCKeeper.ChannelKeeperV2,
app.SlashingKeeper,
app.BankKeeper,
app.AccountKeeper,
Expand Down
9 changes: 4 additions & 5 deletions app/provider/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,7 @@ func New(
runtime.NewKVStoreService(keys[providertypes.StoreKey]),
app.IBCKeeper.ClientKeeper,
app.IBCKeeper.ClientV2Keeper,
app.IBCKeeper.ChannelKeeperV2,
app.StakingKeeper,
app.SlashingKeeper,
app.AccountKeeper,
Expand Down Expand Up @@ -422,10 +423,6 @@ func New(
authtypes.NewModuleAddress(govtypes.ModuleName).String(),
)

providerModule := ibcprovider.NewAppModule(
&app.ProviderKeeper,
)

app.TransferKeeper = ibctransferkeeper.NewKeeper(
appCodec,
runtime.NewKVStoreService(keys[ibctransfertypes.StoreKey]),
Expand All @@ -449,7 +446,9 @@ func New(
ibcRouterV2.AddRoute(vaastypes.ProviderAppID, ibcprovider.NewIBCModule(&app.ProviderKeeper))
app.IBCKeeper.SetRouterV2(ibcRouterV2)

app.ProviderKeeper.SetChannelKeeperV2(app.IBCKeeper.ChannelKeeperV2)
providerModule := ibcprovider.NewAppModule(
&app.ProviderKeeper,
)

govRouter := govv1beta1.NewRouter()
govRouter.
Expand Down
1 change: 1 addition & 0 deletions proto/vaas/v1/wire.proto
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import "cosmos/staking/v1beta1/staking.proto";

import "gogoproto/gogo.proto";
import "tendermint/abci/types.proto";
import "cosmos_proto/cosmos.proto";

//
// Note any type defined in this file is used by both the consumer and provider
Expand Down
116 changes: 116 additions & 0 deletions tests/e2e/e2e_downtime_slash_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package e2e

import (
"fmt"
"time"

"cosmossdk.io/math"

stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
)

func (s *IntegrationTestSuite) testDowntimeSlash() {
s.Run("no downtime slash, consumer down", func() {
valoperAddr, tokensBefore := s.getProviderValidatorTokens()
s.Require().False(tokensBefore.IsZero(), "validator should have tokens before downtime test")

jailed := s.isProviderValidatorJailed()
s.Require().False(jailed, "validator should not be jailed before downtime test")

s.T().Log("pausing consumer container to simulate downtime...")
err := s.dkrPool.Client.PauseContainer(s.consumerValRes[0].Container.ID)
s.Require().NoError(err, "failed to pause consumer container")

time.Sleep(10 * time.Second)

s.T().Log("unpausing consumer container...")
err = s.dkrPool.Client.UnpauseContainer(s.consumerValRes[0].Container.ID)
s.Require().NoError(err, "failed to unpause consumer container")

s.T().Log("waiting for provider to process downtime evidence from consumer...")
s.Require().Eventuallyf(func() bool {
tokensAfter, err := s.getProviderValidatorTokensByAddr(valoperAddr)
if err != nil {
return false
}
return tokensAfter.Equal(tokensBefore)
},
3*time.Minute,
5*time.Second,
"validator tokens were incorrectly slashed during whole consumer chain downtime (before: %s, valoper: %s)",
tokensBefore.String(), valoperAddr,
)

s.T().Log("verifying validator was not jailed after downtime evidence...")
jailed = s.isProviderValidatorJailed()
s.Require().False(jailed, "validator should not be jailed after downtime evidence")
})

// TODO: add individual validator downtime slash test once multiple validators
// are supported in e2e. The test should pause a single validator on the
// consumer chain and verify the provider slashes and jails that validator.
}

func (s *IntegrationTestSuite) patchConsumerSlashingParams() {
s.patchGenesisJSON(s.consumer.dataDir+"/config/genesis.json", func(genesis map[string]any) {
appState, ok := genesis["app_state"].(map[string]any)
if !ok {
return
}
slashing, ok := appState["slashing"].(map[string]any)
if !ok {
slashing = make(map[string]any)
}
params, ok := slashing["params"].(map[string]any)
if !ok {
params = make(map[string]any)
}
params["signed_blocks_window"] = "5"
params["min_signed_per_window"] = "0.050000000000000000"
params["slash_fraction_downtime"] = "0.000000000000000000"
params["downtime_jail_duration"] = "60s"
slashing["params"] = params
appState["slashing"] = slashing
})
}

func (s *IntegrationTestSuite) isProviderValidatorJailed() bool {
vals, err := s.queryProviderValidators()
if err != nil {
return false
}
for _, v := range vals {
if v.Jailed {
return true
}
}
return false
}

// getProviderValidatorTokens returns the first bonded validator's operator address and token amount.
func (s *IntegrationTestSuite) getProviderValidatorTokens() (string, math.Int) {
vals, err := s.queryProviderValidators()
if err != nil {
return "", math.ZeroInt()
}
for _, v := range vals {
if v.Status == stakingtypes.Bonded {
return v.OperatorAddress, v.Tokens
}
}
return "", math.ZeroInt()
}

// getProviderValidatorTokensByAddr returns the token amount for a specific validator by operator address.
func (s *IntegrationTestSuite) getProviderValidatorTokensByAddr(valoperAddr string) (math.Int, error) {
vals, err := s.queryProviderValidators()
if err != nil {
return math.ZeroInt(), err
}
for _, v := range vals {
if v.OperatorAddress == valoperAddr {
return v.Tokens, nil
}
}
return math.ZeroInt(), fmt.Errorf("validator %s not found", valoperAddr)
}
19 changes: 11 additions & 8 deletions tests/e2e/e2e_setup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,23 +239,23 @@ func (s *IntegrationTestSuite) initAndStartProvider() {

// Modify genesis on the host: set fast voting period and small blocks_per_epoch
genesisFile := filepath.Join(providerDir, "config", "genesis.json")
s.patchGenesisJSON(genesisFile, func(genesis map[string]interface{}) {
appState := genesis["app_state"].(map[string]interface{})
s.patchGenesisJSON(genesisFile, func(genesis map[string]any) {
appState := genesis["app_state"].(map[string]any)

// Set fast voting period
if gov, ok := appState["gov"].(map[string]interface{}); ok {
if params, ok := gov["params"].(map[string]interface{}); ok {
if gov, ok := appState["gov"].(map[string]any); ok {
if params, ok := gov["params"].(map[string]any); ok {
params["voting_period"] = "15s"
}
}

// Set fast epoch for VSC, and override fees_per_block to use the
// bond denom so the e2e debt-flow test can fund the consumer fee
// pool from existing genesis accounts.
if provider, ok := appState["provider"].(map[string]interface{}); ok {
if params, ok := provider["params"].(map[string]interface{}); ok {
if provider, ok := appState["provider"].(map[string]any); ok {
if params, ok := provider["params"].(map[string]any); ok {
params["blocks_per_epoch"] = "5"
params["fees_per_block"] = map[string]interface{}{
params["fees_per_block"] = map[string]any{
"denom": bondDenom,
"amount": "1000",
}
Expand Down Expand Up @@ -340,7 +340,7 @@ func (s *IntegrationTestSuite) fetchConsumerGenesis() []byte {
var lastErr error

// Retry fetching consumer genesis (it may take a few blocks)
for i := 0; i < 30; i++ {
for range 30 {
stdout, _, err := s.dockerExec(s.providerValRes[0].Container.ID, []string{
providerBinary, "query", "provider", "consumer-genesis", "0",
"--home", providerHomePath,
Expand Down Expand Up @@ -386,6 +386,9 @@ func (s *IntegrationTestSuite) initAndStartConsumer(consumerGenesisJSON []byte)
err = patchConsumerGenesisWithProviderData(genesisFile, consumerGenesisJSON)
s.Require().NoError(err, "failed to patch consumer genesis")

// Patch consumer slashing params for aggressive downtime detection
s.patchConsumerSlashingParams()

// Copy validator keys from provider to consumer
providerDir := s.provider.dataDir
err = copyFile(
Expand Down
2 changes: 1 addition & 1 deletion tests/e2e/e2e_test.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package e2e


func (s *IntegrationTestSuite) TestVAAS() {
s.testProviderBlockProduction()
s.testConsumerBlockProduction()
s.testConsumerOnProvider()
s.testProviderOnConsumer()
s.testValidatorSetSync()
s.testConsumerDebtFlow()
s.testDowntimeSlash()
// Run last: stops the provider container and replaces it with a fresh
// one started from the exported genesis.
s.testGenesisRoundTrip()
Expand Down
2 changes: 1 addition & 1 deletion tests/e2e/e2e_tsrelayer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func (s *IntegrationTestSuite) verifyTSRelayerConnectivity(chainName, rpcURL str
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

for attempt := 0; attempt < 10; attempt++ {
for attempt := range 10 {
exec, err := s.dkrPool.Client.CreateExec(docker.CreateExecOptions{
Context: ctx,
AttachStdout: true,
Expand Down
10 changes: 5 additions & 5 deletions tests/e2e/genesis_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,17 @@ func patchConsumerGenesisWithProviderData(genesisFilePath string, consumerGenesi
return fmt.Errorf("failed to read genesis file: %w", err)
}

var genesis map[string]interface{}
var genesis map[string]any
if err := json.Unmarshal(bz, &genesis); err != nil {
return fmt.Errorf("failed to unmarshal genesis: %w", err)
}

appState, ok := genesis["app_state"].(map[string]interface{})
appState, ok := genesis["app_state"].(map[string]any)
if !ok {
return fmt.Errorf("app_state not found or not a map")
}

var consumerGenesis interface{}
var consumerGenesis any
if err := json.Unmarshal(consumerGenesisJSON, &consumerGenesis); err != nil {
return fmt.Errorf("failed to unmarshal consumer genesis: %w", err)
}
Expand All @@ -43,11 +43,11 @@ func patchConsumerGenesisWithProviderData(genesisFilePath string, consumerGenesi

// patchGenesisJSON reads a genesis.json file, applies a mutation function,
// and writes it back.
func (s *IntegrationTestSuite) patchGenesisJSON(path string, mutate func(map[string]interface{})) {
func (s *IntegrationTestSuite) patchGenesisJSON(path string, mutate func(map[string]any)) {
bz, err := os.ReadFile(path)
s.Require().NoError(err, "failed to read genesis file")

var genesis map[string]interface{}
var genesis map[string]any
s.Require().NoError(json.Unmarshal(bz, &genesis), "failed to unmarshal genesis")

mutate(genesis)
Expand Down
2 changes: 1 addition & 1 deletion tests/e2e/http_util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func httpGet(endpoint string) ([]byte, error) {
func httpGetWithRetry(endpoint string, maxAttempts int) ([]byte, error) {
var lastErr error

for attempt := 0; attempt < maxAttempts; attempt++ {
for range maxAttempts {
resp, err := http.Get(endpoint) //nolint:gosec
if err != nil {
lastErr = err
Expand Down
12 changes: 12 additions & 0 deletions tests/e2e/testdata/create_consumer.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,17 @@
"unbonding_period": 1728000000000000,
"vaas_timeout_period": 2419200000000000,
"historical_entries": 10000
},
"infraction_parameters": {
"double_sign": {
"slash_fraction": "0.05",
"jail_duration": 9223372036854775807,
"tombstone": true
},
"downtime": {
"slash_fraction": "0.0001",
"jail_duration": 600000000000,
"tombstone": false
}
}
}
22 changes: 15 additions & 7 deletions testutil/keeper/unit_test_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
consumerkeeper "github.com/allinbits/vaas/x/vaas/consumer/keeper"
consumertypes "github.com/allinbits/vaas/x/vaas/consumer/types"
providerkeeper "github.com/allinbits/vaas/x/vaas/provider/keeper"
providertypes "github.com/allinbits/vaas/x/vaas/provider/types"
"github.com/allinbits/vaas/x/vaas/types"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
Expand Down Expand Up @@ -71,6 +72,7 @@ func NewInMemKeeperParams(tb testing.TB) InMemKeeperParams {
type MockedKeepers struct {
*MockClientKeeper
*MockClientV2Keeper
*MockChannelV2Keeper
*MockStakingKeeper
*MockSlashingKeeper
*MockAccountKeeper
Expand All @@ -80,12 +82,13 @@ type MockedKeepers struct {
// NewMockedKeepers instantiates a struct with pointers to properly instantiated mocked keepers.
func NewMockedKeepers(ctrl *gomock.Controller) MockedKeepers {
mocks := MockedKeepers{
MockClientKeeper: NewMockClientKeeper(ctrl),
MockClientV2Keeper: NewMockClientV2Keeper(ctrl),
MockStakingKeeper: NewMockStakingKeeper(ctrl),
MockSlashingKeeper: NewMockSlashingKeeper(ctrl),
MockAccountKeeper: NewMockAccountKeeper(ctrl),
MockBankKeeper: NewMockBankKeeper(ctrl),
MockClientKeeper: NewMockClientKeeper(ctrl),
MockClientV2Keeper: NewMockClientV2Keeper(ctrl),
MockChannelV2Keeper: NewMockChannelV2Keeper(ctrl),
MockStakingKeeper: NewMockStakingKeeper(ctrl),
MockSlashingKeeper: NewMockSlashingKeeper(ctrl),
MockAccountKeeper: NewMockAccountKeeper(ctrl),
MockBankKeeper: NewMockBankKeeper(ctrl),
}
mocks.MockClientV2Keeper.EXPECT().GetClientCounterparty(gomock.Any(), gomock.Any()).Return(clientv2types.CounterpartyInfo{}, false).AnyTimes()
mocks.MockClientV2Keeper.EXPECT().SetClientCounterparty(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
Expand All @@ -100,6 +103,7 @@ func NewInMemProviderKeeper(params InMemKeeperParams, mocks MockedKeepers) provi
storeService,
mocks.MockClientKeeper,
mocks.MockClientV2Keeper,
mocks.MockChannelV2Keeper,
mocks.MockStakingKeeper,
mocks.MockSlashingKeeper,
mocks.MockAccountKeeper,
Expand All @@ -121,6 +125,7 @@ func NewInMemConsumerKeeper(params InMemKeeperParams, mocks MockedKeepers) consu
storeService,
mocks.MockClientKeeper,
mocks.MockClientV2Keeper,
mocks.MockChannelV2Keeper,
mocks.MockSlashingKeeper,
mocks.MockBankKeeper,
mocks.MockAccountKeeper,
Expand All @@ -141,7 +146,10 @@ func GetProviderKeeperAndCtx(t *testing.T, params InMemKeeperParams) (
t.Helper()
ctrl := gomock.NewController(t)
mocks := NewMockedKeepers(ctrl)
return NewInMemProviderKeeper(params, mocks), params.Ctx, ctrl, mocks
providerKeeper := NewInMemProviderKeeper(params, mocks)
providerKeeper.SetParams(params.Ctx, providertypes.DefaultParams())

return providerKeeper, params.Ctx, ctrl, mocks
}

// Return an in-memory consumer keeper, context, controller, and mocks, given a test instance and parameters.
Expand Down
Loading
Loading