Skip to content
Open
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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ and [`app/consumer/app.go`](app/consumer/app.go) for reference.
| 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 |
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
13 changes: 13 additions & 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 Expand Up @@ -39,3 +40,15 @@ message ValidatorSetChangePacketData {
// collects fees successfully and propagates false on the next VSC.
bool consumer_in_debt = 3;
}

// SlashPacketData is sent from a consumer chain to the provider chain
// to report a validator infraction (e.g., downtime) detected on the consumer.
// The provider uses this to slash and jail the validator on the provider.
message SlashPacketData {
// validator consensus address on the consumer chain
bytes validator_addr = 1;
// infraction height on the consumer chain
int64 infraction_height = 2;
// infraction type (DOWNTIME or DOUBLE_SIGN)
cosmos.staking.v1beta1.Infraction infraction = 3;
}
16 changes: 9 additions & 7 deletions tests/e2e/chain_test.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package e2e

const (
providerBinary = "provider"
consumerBinary = "consumer"
providerHomePath = "/home/nonroot/.provider"
consumerHomePath = "/home/nonroot/.consumer"
bondDenom = "uatone"
providerChainID = "provider-e2e"
consumerChainID = "consumer-e2e"
providerBinary = "provider"
consumerBinary = "consumer"
providerHomePath = "/home/nonroot/.provider"
providerVal1HomePath = "/home/nonroot/.provider-val1"
consumerHomePath = "/home/nonroot/.consumer"
consumerVal1HomePath = "/home/nonroot/.consumer-val1"
bondDenom = "uatone"
providerChainID = "provider-e2e"
consumerChainID = "consumer-e2e"
)

// chain represents a Cosmos chain instance (either provider or consumer).
Expand Down
112 changes: 112 additions & 0 deletions tests/e2e/e2e_downtime_slash_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package e2e

import (
"fmt"
"time"

"cosmossdk.io/math"

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

func (s *IntegrationTestSuite) testDowntimeSlash() {
s.Run("downtime slash", 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 val0 container to simulate downtime (val1 keeps chain alive)...")
err := s.dkrPool.Client.PauseContainer(s.consumerValRes[0].Container.ID)
s.Require().NoError(err, "failed to pause consumer val0 container")

time.Sleep(30 * time.Second)

s.T().Log("unpausing consumer val0 container...")
err = s.dkrPool.Client.UnpauseContainer(s.consumerValRes[0].Container.ID)
s.Require().NoError(err, "failed to unpause consumer val0 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.LT(tokensBefore)
},
3*time.Minute,
5*time.Second,
"validator tokens were not slashed on provider after consumer downtime (before: %s, valoper: %s)",
tokensBefore.String(), valoperAddr,
)

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

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)
}
Loading
Loading