diff --git a/proto/vaas/provider/v1/genesis.proto b/proto/vaas/provider/v1/genesis.proto index bec705be..ef77d0d8 100644 --- a/proto/vaas/provider/v1/genesis.proto +++ b/proto/vaas/provider/v1/genesis.proto @@ -195,6 +195,15 @@ message ConsumerState { // re-derive the keeper's pause-expiration queue. google.protobuf.Timestamp pause_expiration_time = 16 [ (gogoproto.stdtime) = true ]; + + // PrevConsumerValsetHash is the CometBFT hash of the validator set the + // provider had computed for this consumer immediately before the currently + // stored one. Client discovery accepts a candidate IBC client only if the + // client's latest consensus state carries the hash of the current set or + // this one (the set still running on the consumer while the latest VSC + // packet is in flight). Absent until the consumer's validator set has + // rotated at least once. + bytes prev_consumer_valset_hash = 17; } // ValsetUpdateIdToHeight defines the genesis information for the mapping diff --git a/testutil/keeper/unit_test_helpers.go b/testutil/keeper/unit_test_helpers.go index d43b9d8c..5daaa0d6 100644 --- a/testutil/keeper/unit_test_helpers.go +++ b/testutil/keeper/unit_test_helpers.go @@ -80,6 +80,14 @@ type MockedKeepers struct { *MockAccountKeeper *MockBankKeeper *MockDistributionKeeper + + // ClientCounterparties backs the mocked ClientV2Keeper's + // GetClientCounterparty: a client id present in the map has a registered + // counterparty, any other id does not. Tests exercising client + // authentication (the provider's discovery content check, the consumer's + // provider-client pin) populate it to mark specific clients routable; + // the empty default preserves the historical "no counterparty" behavior. + ClientCounterparties map[string]clientv2types.CounterpartyInfo } // NewMockedKeepers instantiates a struct with pointers to properly instantiated mocked keepers. @@ -94,8 +102,13 @@ func NewMockedKeepers(ctrl *gomock.Controller) MockedKeepers { MockAccountKeeper: NewMockAccountKeeper(ctrl), MockBankKeeper: NewMockBankKeeper(ctrl), MockDistributionKeeper: NewMockDistributionKeeper(ctrl), + ClientCounterparties: map[string]clientv2types.CounterpartyInfo{}, } - mocks.MockClientV2Keeper.EXPECT().GetClientCounterparty(gomock.Any(), gomock.Any()).Return(clientv2types.CounterpartyInfo{}, false).AnyTimes() + mocks.MockClientV2Keeper.EXPECT().GetClientCounterparty(gomock.Any(), gomock.Any()).DoAndReturn( + func(_ sdk.Context, clientID string) (clientv2types.CounterpartyInfo, bool) { + cp, found := mocks.ClientCounterparties[clientID] + return cp, found + }).AnyTimes() mocks.MockClientV2Keeper.EXPECT().SetClientCounterparty(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() return mocks } diff --git a/x/vaas/consumer/ibc_module_test.go b/x/vaas/consumer/ibc_module_test.go index 50b51346..0efd1dc2 100644 --- a/x/vaas/consumer/ibc_module_test.go +++ b/x/vaas/consumer/ibc_module_test.go @@ -19,15 +19,20 @@ import ( // TestIBCModuleOnRecvPacketStoresDestinationClientAsProviderClient guards // against regressing to storing the packet's SourceClient (the provider's // own client, meaningless to the consumer for outbound sends) as the -// consumer's ProviderClientID. It must store DestinationClient: the -// consumer's own client that received the packet, which ibc-go's RecvPacket -// handler has already verified carries a registered counterparty, and which -// SendEvidencePackets later needs to address packets back to the provider. +// consumer's ProviderClientID when the bootstrap adoption re-pins from the +// genesis client. It must store DestinationClient: the consumer's own client +// that received the packet, which ibc-go's RecvPacket handler has already +// verified carries a registered counterparty, and which SendEvidencePackets +// later needs to address packets back to the provider. func TestIBCModuleOnRecvPacketStoresDestinationClientAsProviderClient(t *testing.T) { consumerKeeper, ctx, ctrl, mocks := testkeeper.GetConsumerKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() testkeeper.StubClientState(mocks, "provider-0") + // The unroutable client pinned at genesis; neither the packet's source + // nor its destination, so storing the wrong one is observable. + consumerKeeper.SetProviderClientID(ctx, "07-tendermint-9") + module := consumer.NewIBCModule(&consumerKeeper) pk, err := cryptocodec.ToCmtProtoPublicKey(ed25519.GenPrivKey().PubKey()) @@ -53,18 +58,18 @@ func TestIBCModuleOnRecvPacketStoresDestinationClientAsProviderClient(t *testing "ProviderClientID must be the consumer's own (destination) client, not the provider's own (source) client") } -// TestIBCModuleOnRecvPacketHealsStaleProviderClient guards against the -// consumer latching onto a genesis-time placeholder client (self-created -// before any relayer-established, counterparty-linked client exists) and -// never correcting it: every accepted VSC packet must resync ProviderClientID -// to whichever client actually delivered it. -func TestIBCModuleOnRecvPacketHealsStaleProviderClient(t *testing.T) { +// TestIBCModuleOnRecvPacketBootstrapReplacesGenesisClient covers the one-time +// bootstrap adoption through the full IBC module callback: the consumer is +// pinned to the genesis-time client (self-created, no registered counterparty, +// unreachable by packet routing), and the first VSC packet delivered over the +// relayer's counterparty-linked client re-pins ProviderClientID to it. +func TestIBCModuleOnRecvPacketBootstrapReplacesGenesisClient(t *testing.T) { consumerKeeper, ctx, ctrl, mocks := testkeeper.GetConsumerKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() testkeeper.StubClientState(mocks, "provider-0") - staleGenesisClientID := "07-tendermint-0" - consumerKeeper.SetProviderClientID(ctx, staleGenesisClientID) + genesisClientID := "07-tendermint-0" + consumerKeeper.SetProviderClientID(ctx, genesisClientID) module := consumer.NewIBCModule(&consumerKeeper) @@ -86,7 +91,7 @@ func TestIBCModuleOnRecvPacketHealsStaleProviderClient(t *testing.T) { clientID, found := consumerKeeper.GetProviderClientID(ctx) require.True(t, found) require.Equal(t, liveClientID, clientID, - "ProviderClientID must heal to the client actually delivering VSC packets, not stay stuck on the stale genesis client") + "ProviderClientID must re-pin to the client actually delivering VSC packets, not stay stuck on the unroutable genesis client") } // TestIBCModuleOnRecvPacketRejectsWrongSourcePort guards against accepting a diff --git a/x/vaas/consumer/keeper/genesis.go b/x/vaas/consumer/keeper/genesis.go index 8a9915c7..03c40762 100644 --- a/x/vaas/consumer/keeper/genesis.go +++ b/x/vaas/consumer/keeper/genesis.go @@ -49,6 +49,12 @@ func (k Keeper) InitGenesis(ctx sdk.Context, state *types.GenesisState) []abci.V panic(err) } + // This genesis client is built from provider-authored state, but it can + // never carry packets (created outside a MsgCreateClient, it has no + // recorded creator, so its IBC v2 counterparty can never be + // registered). The pin moves off it exactly once, to the first client + // that actually delivers a VSC packet, and is permanent from then on; + // see enforcePinnedProviderClient in relay.go for the trust model. k.SetProviderClientID(ctx, cid) k.SetHeightValsetUpdateID(ctx, uint64(ctx.BlockHeight()), uint64(0)) diff --git a/x/vaas/consumer/keeper/ibc_v2_integration_test.go b/x/vaas/consumer/keeper/ibc_v2_integration_test.go index 7d90eb8d..b5f38bd6 100644 --- a/x/vaas/consumer/keeper/ibc_v2_integration_test.go +++ b/x/vaas/consumer/keeper/ibc_v2_integration_test.go @@ -7,6 +7,8 @@ import ( abci "github.com/cometbft/cometbft/abci/types" + clientv2types "github.com/cosmos/ibc-go/v10/modules/core/02-client/v2/types" + cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" @@ -15,7 +17,7 @@ import ( ) // TestIBCV2ConsumerFullVSCFlow tests the complete consumer-side IBC v2 VSC packet flow: -// 1. Consumer receives first VSC packet and establishes provider client +// 1. Consumer receives first VSC packet over the pinned provider client // 2. Consumer accumulates validator updates // 3. Consumer tracks highest valset update ID for out-of-order handling func TestIBCV2ConsumerFullVSCFlow(t *testing.T) { @@ -24,6 +26,7 @@ func TestIBCV2ConsumerFullVSCFlow(t *testing.T) { testkeeper.StubClientState(mocks, "provider-0") providerClientID := "07-tendermint-0" + consumerKeeper.SetProviderClientID(ctx, providerClientID) // Create validator updates pk1, err := cryptocodec.ToCmtProtoPublicKey(ed25519.GenPrivKey().PubKey()) @@ -31,7 +34,7 @@ func TestIBCV2ConsumerFullVSCFlow(t *testing.T) { pk2, err := cryptocodec.ToCmtProtoPublicKey(ed25519.GenPrivKey().PubKey()) require.NoError(t, err) - // Step 1: Receive first VSC packet - should establish provider client + // Step 1: Receive first VSC packet over the pinned client valUpdates1 := []abci.ValidatorUpdate{ {PubKey: pk1, Power: 100}, } @@ -40,7 +43,7 @@ func TestIBCV2ConsumerFullVSCFlow(t *testing.T) { err = consumerKeeper.OnRecvVSCPacketV2(ctx, providerClientID, vscPacket1) require.NoError(t, err) - // Verify provider client was established + // Verify the pin is unchanged clientID, found := consumerKeeper.GetProviderClientID(ctx) require.True(t, found) require.Equal(t, providerClientID, clientID) @@ -84,6 +87,7 @@ func TestIBCV2ConsumerOutOfOrderHandling(t *testing.T) { testkeeper.StubClientState(mocks, "provider-0") providerClientID := "07-tendermint-0" + consumerKeeper.SetProviderClientID(ctx, providerClientID) pk1, err := cryptocodec.ToCmtProtoPublicKey(ed25519.GenPrivKey().PubKey()) require.NoError(t, err) @@ -134,8 +138,8 @@ func TestIBCV2ConsumerOutOfOrderHandling(t *testing.T) { require.Len(t, pendingChanges.ValidatorUpdates, 2) } -// TestIBCV2ConsumerRejectsUnknownProvider tests that packets from an unknown -// provider client are rejected after the provider is established. +// TestIBCV2ConsumerRejectsUnknownProvider tests that packets arriving over a +// client other than the pinned, routable provider client are rejected. func TestIBCV2ConsumerRejectsUnknownProvider(t *testing.T) { consumerKeeper, ctx, ctrl, mocks := testkeeper.GetConsumerKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() @@ -143,32 +147,37 @@ func TestIBCV2ConsumerRejectsUnknownProvider(t *testing.T) { providerClientID := "07-tendermint-0" unknownClientID := "07-tendermint-999" + consumerKeeper.SetProviderClientID(ctx, providerClientID) + mocks.ClientCounterparties[providerClientID] = clientv2types.CounterpartyInfo{ClientId: "07-tendermint-7"} pk, err := cryptocodec.ToCmtProtoPublicKey(ed25519.GenPrivKey().PubKey()) require.NoError(t, err) - // Establish provider with first packet + // Traffic over the pinned client flows normally. valUpdates := []abci.ValidatorUpdate{{PubKey: pk, Power: 100}} vscPacket := types.NewValidatorSetChangePacketData(valUpdates, 1) err = consumerKeeper.OnRecvVSCPacketV2(ctx, providerClientID, vscPacket) require.NoError(t, err) - // Verify provider is established clientID, found := consumerKeeper.GetProviderClientID(ctx) require.True(t, found) require.Equal(t, providerClientID, clientID) - // Try to send packet from different client - should succeed - // (IBC v2 layer handles counterparty validation) + // A packet over any other client is rejected: the pin is routable, so + // there is no legitimate reason for VSC traffic to arrive anywhere else. vscPacket2 := types.NewValidatorSetChangePacketData(valUpdates, 2) err = consumerKeeper.OnRecvVSCPacketV2(ctx, unknownClientID, vscPacket2) - require.NoError(t, err, "packet from different client should succeed (IBC v2 validates counterparties)") + require.Error(t, err, "packet over a non-pinned client must be rejected") - // Highest ID should be updated to 2 + // Highest ID must still be 1, and the pin must not have moved. highestID, _, err := consumerKeeper.GetHighestValsetUpdateID(ctx) require.NoError(t, err) - require.Equal(t, uint64(2), highestID) + require.Equal(t, uint64(1), highestID) + + clientID, found = consumerKeeper.GetProviderClientID(ctx) + require.True(t, found) + require.Equal(t, providerClientID, clientID) } // TestIBCV2ConsumerProviderInfoQuery tests the v2 provider info query. @@ -201,6 +210,7 @@ func TestIBCV2ConsumerDuplicatePacketHandling(t *testing.T) { testkeeper.StubClientState(mocks, "provider-0") providerClientID := "07-tendermint-0" + consumerKeeper.SetProviderClientID(ctx, providerClientID) pk, err := cryptocodec.ToCmtProtoPublicKey(ed25519.GenPrivKey().PubKey()) require.NoError(t, err) diff --git a/x/vaas/consumer/keeper/keeper.go b/x/vaas/consumer/keeper/keeper.go index d0e73eb8..0e0be8bd 100644 --- a/x/vaas/consumer/keeper/keeper.go +++ b/x/vaas/consumer/keeper/keeper.go @@ -231,7 +231,10 @@ func (k Keeper) SetPort(ctx context.Context, portID string) { } // SetProviderClientID sets the clientID for the client to the provider. -// Set in InitGenesis +// Written at InitGenesis (the genesis client on NewChain, the exported pin on +// a restart) and by the one-time bootstrap adoption in +// enforcePinnedProviderClient (relay.go), which also documents why the pin +// never moves after that. func (k Keeper) SetProviderClientID(ctx context.Context, clientID string) { if err := k.ProviderClientID.Set(ctx, clientID); err != nil { panic(fmt.Errorf("failed to set provider client ID: %w", err)) diff --git a/x/vaas/consumer/keeper/relay.go b/x/vaas/consumer/keeper/relay.go index 14685ad7..c3a7aa4e 100644 --- a/x/vaas/consumer/keeper/relay.go +++ b/x/vaas/consumer/keeper/relay.go @@ -26,15 +26,19 @@ func (k Keeper) OnRecvVSCPacketV2(ctx sdk.Context, consumerClientID string, newC } // Authenticate the packet's source before touching any state: a client - // tracking an unexpected chain id is rejected outright, before the - // dedup check below and every state mutation that follows it - // (SetLastVSCRecvTime, the ProviderClientID heal, param staging, valset - // apply). Anyone can permissionlessly create an IBC v2 client, so - // DestinationClient alone does not prove the packet came from the - // provider; pinning the chain id closes that gap. + // tracking an unexpected chain id, or a client other than the pinned + // provider client, is rejected outright -- before the dedup check below + // and every state mutation that follows it (SetLastVSCRecvTime, param + // staging, valset apply). Anyone can permissionlessly create an IBC v2 + // client, so DestinationClient alone does not prove the packet came from + // the provider; the chain-id gate and the client pin close that gap in + // layers. if err := k.authenticateProviderChainID(ctx, consumerClientID); err != nil { return err } + if err := k.enforcePinnedProviderClient(ctx, consumerClientID); err != nil { + return err + } highestID, found, err := k.GetHighestValsetUpdateID(ctx) if err != nil { @@ -56,23 +60,6 @@ func (k Keeper) OnRecvVSCPacketV2(ctx sdk.Context, consumerClientID string, newC k.StageDowntimeParams(ctx, *newChanges.DowntimeParams) } - // The stored client follows whichever client is actually delivering VSC - // packets: the first value ever seen is not authoritative, because at - // genesis it is a placeholder client the consumer creates for itself - // before the relayer establishes the real, counterparty-linked client. - if current, found := k.GetProviderClientID(ctx); !found || current != consumerClientID { - k.SetProviderClientID(ctx, consumerClientID) - k.Logger(ctx).Info("Provider client established", "clientID", consumerClientID) - - ctx.EventManager().EmitEvent( - sdk.NewEvent( - vaastypes.EventTypeChannelEstablished, - sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName), - sdk.NewAttribute("client_id", consumerClientID), - ), - ) - } - k.SetConsumerInDebt(ctx, newChanges.ConsumerInDebt) // Set pending changes: snapshot packets replace the set; diff packets accumulate. @@ -128,21 +115,20 @@ func (k Keeper) OnRecvVSCPacketV2(ctx sdk.Context, consumerClientID string, newC // client and get a relayer to route packets through it, so the fact that // consumerClientID is a registered, counterparty-linked client is not by // itself proof the packets originate from the real provider chain -- it only -// proves *some* chain is on the other end. The first VSC packet ever -// accepted teaches the consumer the provider's chain id from that packet's -// destination client; every packet after that must arrive over a client -// tracking the same chain id, or it is rejected before any state changes -// (see the call site in OnRecvVSCPacketV2). A same-chain-id client -// replacement -- e.g. swapping an expired/frozen client for a fresh one -- -// remains allowed, since the ProviderClientID heal logic in OnRecvVSCPacketV2 -// is unaffected by this gate as long as the chain id matches. +// proves *some* chain is on the other end. The chain id is normally pinned at +// genesis from the provider-authored client state; as a fallback, the first +// VSC packet ever accepted teaches the consumer the provider's chain id from +// that packet's destination client. Every packet after that must arrive over +// a client tracking the same chain id, or it is rejected before any state +// changes (see the call site in OnRecvVSCPacketV2). // -// Residual trust boundary: this only pins the chain-id *string*. A chain -// that reuses the same chain-id (a fork, or a chain deliberately renamed to -// collide) still has to produce a light-client history that convinces the -// consumer's tendermint light client of that chain id; forging that history -// is the job of the misbehaviour/light-client-fraud machinery, not this -// check. +// Residual trust boundary: this only pins the chain-id *string*, so on its +// own it does not distinguish the real provider from a chain that reuses the +// same chain-id (a fork, or a chain deliberately renamed to collide). The +// client pin (see enforcePinnedProviderClient) closes most of that gap by +// refusing to follow inbound traffic to another client at all; what remains +// -- fraudulent history on the one pinned client -- is the job of the +// misbehaviour/light-client-fraud machinery, not this check. func (k Keeper) authenticateProviderChainID(ctx sdk.Context, consumerClientID string) error { clientState, found := k.clientKeeper.GetClientState(ctx, consumerClientID) if !found { @@ -167,3 +153,71 @@ func (k Keeper) authenticateProviderChainID(ctx sdk.Context, consumerClientID st return nil } + +// enforcePinnedProviderClient rejects a VSC packet unless it arrived over the +// consumer's pinned provider client, allowing the pin to move at most once in +// the chain's lifetime: from the unroutable client created at genesis to the +// first client that actually delivers a VSC packet. +// +// The pin's trust model: at NewChain genesis the consumer creates its own +// IBC client of the provider from client and consensus state the provider +// itself authored into the consumer genesis, and pins it (a restart restores +// the exported pin instead). That genesis client is a genuine light client of +// the real provider, but it can never carry packets: ibc-go only lets a +// client's recorded creator register the IBC v2 counterparty that packet +// routing requires, and a client created directly at genesis has no recorded +// creator. Whichever relayer serves the chain therefore creates its own, +// counterparty-linked client of the provider, and the first VSC packet +// delivered over such a client re-pins the consumer to it: ibc-go's +// RecvPacket has already proven the packet against that client's consensus +// state and registered counterparty, and authenticateProviderChainID has +// already checked it tracks the pinned provider chain id. +// +// From that moment the pin is permanent. A pinned client that has a +// registered counterparty is a routable client, so there is no legitimate +// reason for VSC traffic to ever arrive anywhere else: anyone can +// permissionlessly create a client of a look-alike chain reusing the +// provider's chain id and have packets routed over it, so following inbound +// traffic off the pin would let such a chain capture the consumer's validator +// set. If the pinned client dies (expires, is frozen), packet flow halts +// until governance revives it in place via ibc-go's MsgRecoverClient, which +// substitutes fresh client state under the SAME client id -- the pin survives +// recovery unchanged. +func (k Keeper) enforcePinnedProviderClient(ctx sdk.Context, consumerClientID string) error { + pinned, found := k.GetProviderClientID(ctx) + if !found { + // Both genesis paths establish the pin (NewChain creates and pins the + // genesis client; a restart restores the exported pin), so an absent + // pin means a malformed genesis or corrupted state: fail closed. + return errorsmod.Wrapf(types.ErrInvalidProviderClient, + "no provider client pinned; rejecting VSC packet over client %s", consumerClientID) + } + if pinned == consumerClientID { + return nil + } + if _, found := k.clientV2Keeper.GetClientCounterparty(ctx, pinned); found { + return errorsmod.Wrapf(types.ErrInvalidProviderClient, + "VSC packet arrived over client %s, but the provider client is pinned to %s", + consumerClientID, pinned) + } + + // The pinned client has no registered counterparty, so it is the genesis + // client that packet routing can never reach: adopt the delivering client + // as the permanent pin. Counterparties cannot be unregistered, so once a + // routable client is pinned this branch is unreachable. + k.SetProviderClientID(ctx, consumerClientID) + k.Logger(ctx).Info("provider client pinned", + "clientID", consumerClientID, + "genesisClientID", pinned, + ) + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + vaastypes.EventTypeChannelEstablished, + sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName), + sdk.NewAttribute("client_id", consumerClientID), + ), + ) + + return nil +} diff --git a/x/vaas/consumer/keeper/relay_test.go b/x/vaas/consumer/keeper/relay_test.go index b0f60d62..f29a077a 100644 --- a/x/vaas/consumer/keeper/relay_test.go +++ b/x/vaas/consumer/keeper/relay_test.go @@ -13,6 +13,7 @@ import ( abci "github.com/cometbft/cometbft/abci/types" + clientv2types "github.com/cosmos/ibc-go/v10/modules/core/02-client/v2/types" ibctmtypes "github.com/cosmos/ibc-go/v10/modules/light-clients/07-tendermint" "cosmossdk.io/math" @@ -45,6 +46,8 @@ func TestOnRecvVSCPacketV2(t *testing.T) { consumerKeeper, ctx, ctrl, mocks := testkeeper.GetConsumerKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() testkeeper.StubClientState(mocks, "provider-0") + consumerKeeper.SetProviderClientID(ctx, providerClientID) + mocks.ClientCounterparties[providerClientID] = clientv2types.CounterpartyInfo{ClientId: "07-tendermint-7"} pd1 := types.NewValidatorSetChangePacketData(changes1, 1) err = consumerKeeper.OnRecvVSCPacketV2(ctx, providerClientID, pd1) @@ -70,14 +73,16 @@ func TestOnRecvVSCPacketV2(t *testing.T) { require.NoError(t, err) require.Equal(t, uint64(2), highestID) + // The pinned client is routable (it has a registered counterparty), so a + // packet arriving over any other client is rejected outright. differentClientID := "07-tendermint-999" pd3 := types.NewValidatorSetChangePacketData(changes1, 3) err = consumerKeeper.OnRecvVSCPacketV2(ctx, differentClientID, pd3) - require.NoError(t, err, "packet from different client should succeed (IBC v2 validates counterparties)") + require.Error(t, err, "a packet over a non-pinned client must be rejected") highestID, _, err = consumerKeeper.GetHighestValsetUpdateID(ctx) require.NoError(t, err) - require.Equal(t, uint64(3), highestID) + require.Equal(t, uint64(2), highestID, "a rejected packet must not advance the highest vsc id") } func TestOnRecvVSCPacketV2OutOfOrder(t *testing.T) { @@ -91,6 +96,7 @@ func TestOnRecvVSCPacketV2OutOfOrder(t *testing.T) { consumerKeeper, ctx, ctrl, mocks := testkeeper.GetConsumerKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() testkeeper.StubClientState(mocks, "provider-0") + consumerKeeper.SetProviderClientID(ctx, providerClientID) changes5 := []abci.ValidatorUpdate{{PubKey: pk1, Power: 50}} pd5 := types.NewValidatorSetChangePacketData(changes5, 5) @@ -139,6 +145,7 @@ func TestOnRecvVSCPacketV2FirstPacketNotDropped(t *testing.T) { consumerKeeper, ctx, ctrl, mocks := testkeeper.GetConsumerKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() testkeeper.StubClientState(mocks, "provider-0") + consumerKeeper.SetProviderClientID(ctx, providerClientID) _, found, err := consumerKeeper.GetHighestValsetUpdateID(ctx) require.NoError(t, err) @@ -183,6 +190,7 @@ func TestOnRecvVSCPacketV2AccumulatesChanges(t *testing.T) { consumerKeeper, ctx, ctrl, mocks := testkeeper.GetConsumerKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() testkeeper.StubClientState(mocks, "provider-0") + consumerKeeper.SetProviderClientID(ctx, providerClientID) pd1 := types.NewValidatorSetChangePacketData(changes1, 1) err = consumerKeeper.OnRecvVSCPacketV2(ctx, providerClientID, pd1) @@ -216,6 +224,7 @@ func TestOnRecvVSCPacketV2DuplicateUpdates(t *testing.T) { consumerKeeper, ctx, ctrl, mocks := testkeeper.GetConsumerKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() testkeeper.StubClientState(mocks, "provider-0") + consumerKeeper.SetProviderClientID(ctx, providerClientID) cId := testcrypto.NewCryptoIdentityFromIntSeed(43278947) valUpdates := []abci.ValidatorUpdate{ @@ -243,6 +252,7 @@ func TestOnRecvVSCPacketV2DebtStatus(t *testing.T) { consumerKeeper, ctx, ctrl, mocks := testkeeper.GetConsumerKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() testkeeper.StubClientState(mocks, "provider-0") + consumerKeeper.SetProviderClientID(ctx, providerClientID) require.False(t, consumerKeeper.IsConsumerInDebt(ctx)) @@ -329,6 +339,7 @@ func TestOnRecvVSCRecordsRecvTime(t *testing.T) { consumerKeeper, ctx, ctrl, mocks := testkeeper.GetConsumerKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() testkeeper.StubClientState(mocks, "provider-0") + consumerKeeper.SetProviderClientID(ctx, providerClientID) advancedTime := ctx.BlockTime().Add(10 * time.Minute) ctx = ctx.WithBlockTime(advancedTime) @@ -354,6 +365,7 @@ func TestDedupDoesNotResetLastVSCRecvTime(t *testing.T) { k, ctx, ctrl, mocks := testkeeper.GetConsumerKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() testkeeper.StubClientState(mocks, "provider-0") + k.SetProviderClientID(ctx, providerClientID) // Deliver packet at blockTime T1 -- records lastVSCRecvTime = T1. t1 := ctx.BlockTime().Add(5 * time.Minute) @@ -385,6 +397,7 @@ func TestRecvPacketAfterStalenessLiftsStale(t *testing.T) { k, ctx, ctrl, mocks := testkeeper.GetConsumerKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() testkeeper.StubClientState(mocks, "provider-0") + k.SetProviderClientID(ctx, providerClientID) const threshold = 2 * time.Hour k.SetParams(ctx, types.NewConsumerParams( @@ -423,6 +436,7 @@ func TestOnRecvVSCPacketStagesDowntimeParams(t *testing.T) { k, ctx, ctrl, mocks := testkeeper.GetConsumerKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() testkeeper.StubClientState(mocks, "provider-0") + k.SetProviderClientID(ctx, providerClientID) initialParams := types.DefaultConsumerParams() k.SetParams(ctx, initialParams) @@ -713,6 +727,7 @@ func TestOnRecvVSCPacketV2PinsProviderChainIdOnFirstPacket(t *testing.T) { defer ctrl.Finish() clientID := "07-tendermint-0" + k.SetProviderClientID(ctx, clientID) mocks.MockClientKeeper.EXPECT().GetClientState(gomock.Any(), clientID). Return(&ibctmtypes.ClientState{ChainId: "provider-chain"}, true).AnyTimes() @@ -749,7 +764,9 @@ func TestOnRecvVSCPacketV2RejectsDifferentChainId(t *testing.T) { pk1, err := cryptocodec.ToCmtProtoPublicKey(ed25519.GenPrivKey().PubKey()) require.NoError(t, err) - // A legitimate first packet establishes the pin. + // The genesis-established client pin; a legitimate first packet over it + // establishes the chain-id pin. + k.SetProviderClientID(ctx, legitClientID) pd1 := types.NewValidatorSetChangePacketData([]abci.ValidatorUpdate{{PubKey: pk1, Power: 10}}, 1) require.NoError(t, k.OnRecvVSCPacketV2(ctx, legitClientID, pd1)) @@ -800,33 +817,143 @@ func TestOnRecvVSCPacketV2RejectsDifferentChainId(t *testing.T) { require.Equal(t, "provider-chain", pinned, "the pin itself must not change") } -// TestOnRecvVSCPacketV2SameChainIdHealsClient adapts the pre-existing -// heal regression coverage (see TestOnRecvVSCPacketV2 above and -// TestIBCModuleOnRecvPacketHealsStaleProviderClient) to the chain-id gate: -// a client replacement is allowed, and continues to heal ProviderClientID, -// as long as the replacement client tracks the SAME pinned chain id. -func TestOnRecvVSCPacketV2SameChainIdHealsClient(t *testing.T) { +// TestOnRecvVSCPacketV2BootstrapAdoptionThenLatch walks the pin through its +// whole lifecycle: pinned to the unroutable genesis client at first (no +// registered counterparty -- packet routing can never reach it), re-pinned +// exactly once to the first client that actually delivers a VSC packet, and +// permanently latched from then on -- later packets over other clients are +// rejected and the pin does not move again. +func TestOnRecvVSCPacketV2BootstrapAdoptionThenLatch(t *testing.T) { k, ctx, ctrl, mocks := testkeeper.GetConsumerKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() testkeeper.StubClientState(mocks, "provider-chain") - staleClientID := "07-tendermint-0" - freshClientID := "07-tendermint-1" + genesisClientID := "07-tendermint-0" + relayerClientID := "07-tendermint-1" + otherClientID := "07-tendermint-2" + + // NewChain genesis pins the client it created; that client never gets a + // counterparty (mocks.ClientCounterparties deliberately has no entry for it). + k.SetProviderClientID(ctx, genesisClientID) + + // The relayer's client is counterparty-linked, as ibc-go guarantees for + // any client a packet actually arrives on. + mocks.ClientCounterparties[relayerClientID] = clientv2types.CounterpartyInfo{ClientId: "07-tendermint-9"} + mocks.ClientCounterparties[otherClientID] = clientv2types.CounterpartyInfo{ClientId: "07-tendermint-8"} pk1, err := cryptocodec.ToCmtProtoPublicKey(ed25519.GenPrivKey().PubKey()) require.NoError(t, err) + + // First VSC packet arrives over the relayer's client: the pin moves to it. pd1 := types.NewValidatorSetChangePacketData([]abci.ValidatorUpdate{{PubKey: pk1, Power: 10}}, 1) - require.NoError(t, k.OnRecvVSCPacketV2(ctx, staleClientID, pd1)) + require.NoError(t, k.OnRecvVSCPacketV2(ctx, relayerClientID, pd1), + "the first VSC over a routable client must be accepted while the pin is the unroutable genesis client") + clientID, found := k.GetProviderClientID(ctx) + require.True(t, found) + require.Equal(t, relayerClientID, clientID, "the pin must move to the delivering client") + + // From now on the pin is latched: a same-chain-id packet over any other + // client is rejected and moves nothing. pd2 := types.NewValidatorSetChangePacketData([]abci.ValidatorUpdate{{PubKey: pk1, Power: 20}}, 2) - require.NoError(t, k.OnRecvVSCPacketV2(ctx, freshClientID, pd2), - "a same-chain-id client replacement must still be accepted") + err = k.OnRecvVSCPacketV2(ctx, otherClientID, pd2) + require.Error(t, err, "a VSC over a non-pinned client must be rejected once the pin is routable") - clientID, found := k.GetProviderClientID(ctx) + clientID, found = k.GetProviderClientID(ctx) require.True(t, found) - require.Equal(t, freshClientID, clientID, "ProviderClientID must heal to the replacement client") + require.Equal(t, relayerClientID, clientID, "the pin must never move on inbound traffic again") - pinned, found := k.GetProviderChainId(ctx) + highestID, _, err := k.GetHighestValsetUpdateID(ctx) + require.NoError(t, err) + require.Equal(t, uint64(1), highestID, "the rejected packet must not have been processed") + + // Traffic over the pinned client keeps flowing. + require.NoError(t, k.OnRecvVSCPacketV2(ctx, relayerClientID, pd2)) +} + +// TestOnRecvVSCPacketV2MissingPinRejects verifies a VSC packet is rejected +// outright when no provider client is pinned at all: both genesis paths +// establish the pin, so its absence means a malformed genesis or corrupted +// state, and the consumer must fail closed rather than adopt whatever client +// the first packet happens to arrive on. +func TestOnRecvVSCPacketV2MissingPinRejects(t *testing.T) { + k, ctx, ctrl, mocks := testkeeper.GetConsumerKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + testkeeper.StubClientState(mocks, "provider-chain") + + pk1, err := cryptocodec.ToCmtProtoPublicKey(ed25519.GenPrivKey().PubKey()) + require.NoError(t, err) + pd := types.NewValidatorSetChangePacketData([]abci.ValidatorUpdate{{PubKey: pk1, Power: 10}}, 1) + + err = k.OnRecvVSCPacketV2(ctx, "07-tendermint-1", pd) + require.Error(t, err, "a VSC packet with no pin established must be rejected") + require.Contains(t, err.Error(), "no provider client pinned") + + _, found := k.GetProviderClientID(ctx) + require.False(t, found, "a rejected packet must not establish a pin") + _, found2, err := k.GetHighestValsetUpdateID(ctx) + require.NoError(t, err) + require.False(t, found2, "a rejected packet must not be processed") +} + +// TestOnRecvVSCPacketV2RejectsNonPinnedClientBeforeStateChanges verifies the +// pin gate fires before any state mutation: a same-chain-id packet over a +// client other than the (routable) pinned one leaves the valset, the pin, the +// staleness clock, the debt flag, staged params, and the vsc-id watermark all +// exactly as they were. +func TestOnRecvVSCPacketV2RejectsNonPinnedClientBeforeStateChanges(t *testing.T) { + k, ctx, ctrl, mocks := testkeeper.GetConsumerKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + testkeeper.StubClientState(mocks, "provider-chain") + + pinnedClientID := "07-tendermint-1" + rogueClientID := "07-tendermint-666" + k.SetProviderClientID(ctx, pinnedClientID) + mocks.ClientCounterparties[pinnedClientID] = clientv2types.CounterpartyInfo{ClientId: "07-tendermint-9"} + + pk1, err := cryptocodec.ToCmtProtoPublicKey(ed25519.GenPrivKey().PubKey()) + require.NoError(t, err) + + pd1 := types.NewValidatorSetChangePacketData([]abci.ValidatorUpdate{{PubKey: pk1, Power: 10}}, 1) + require.NoError(t, k.OnRecvVSCPacketV2(ctx, pinnedClientID, pd1)) + + prevPending, ok := k.GetPendingChanges(ctx) + require.True(t, ok) + prevRecvTime := k.GetLastVSCRecvTime(ctx) + prevHighestID, _, err := k.GetHighestValsetUpdateID(ctx) + require.NoError(t, err) + + // Advance block time so a mutation to LastVSCRecvTime would be observable. + laterCtx := ctx.WithBlockTime(ctx.BlockTime().Add(time.Hour)) + + staged := types.DowntimeParams{ + SignedBlocksWindow: 12345, + MinSignedPerWindow: math.LegacyMustNewDecFromStr("0.5"), + } + pd2 := types.NewValidatorSetChangePacketData([]abci.ValidatorUpdate{{PubKey: pk1, Power: 999}}, 2) + pd2.DowntimeParams = &staged + pd2.IsSnapshot = true + pd2.ConsumerInDebt = true + + err = k.OnRecvVSCPacketV2(laterCtx, rogueClientID, pd2) + require.Error(t, err, "a packet over a non-pinned client must be rejected") + + pending, ok := k.GetPendingChanges(ctx) + require.True(t, ok) + require.Equal(t, *prevPending, *pending, "valset must be unchanged by a rejected packet") + + clientID, found := k.GetProviderClientID(ctx) require.True(t, found) - require.Equal(t, "provider-chain", pinned, "the pinned chain id must not change on a same-chain-id heal") + require.Equal(t, pinnedClientID, clientID, "the pin must be unchanged by a rejected packet") + + require.Equal(t, prevRecvTime, k.GetLastVSCRecvTime(laterCtx), "LastVSCRecvTime must be unchanged by a rejected packet") + + require.False(t, k.IsConsumerInDebt(ctx), "the in-debt flag must be unchanged by a rejected packet") + + _, err = k.StagedDowntimeParams.Get(ctx) + require.Error(t, err, "nothing should be staged from a rejected packet") + + highestID, _, err := k.GetHighestValsetUpdateID(ctx) + require.NoError(t, err) + require.Equal(t, prevHighestID, highestID, "highest vsc id must not advance for a rejected packet") } diff --git a/x/vaas/provider/keeper/client_discovery_test.go b/x/vaas/provider/keeper/client_discovery_test.go new file mode 100644 index 00000000..c600a72c --- /dev/null +++ b/x/vaas/provider/keeper/client_discovery_test.go @@ -0,0 +1,363 @@ +package keeper_test + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + + tmtypes "github.com/cometbft/cometbft/types" + + clienttypes "github.com/cosmos/ibc-go/v10/modules/core/02-client/types" + clientv2types "github.com/cosmos/ibc-go/v10/modules/core/02-client/v2/types" + ibcexported "github.com/cosmos/ibc-go/v10/modules/core/exported" + ibctmtypes "github.com/cosmos/ibc-go/v10/modules/light-clients/07-tendermint" + + "cosmossdk.io/log" + + sdk "github.com/cosmos/cosmos-sdk/types" + + testcrypto "github.com/allinbits/vaas/testutil/crypto" + testkeeper "github.com/allinbits/vaas/testutil/keeper" + providerkeeper "github.com/allinbits/vaas/x/vaas/provider/keeper" + providertypes "github.com/allinbits/vaas/x/vaas/provider/types" +) + +// discoveryVal pairs a deterministic crypto identity with a voting power, so +// a test can both store a consumer validator set on the keeper and compute +// the CometBFT hash a genuine consumer chain running that set would carry. +type discoveryVal struct { + id *testcrypto.CryptoIdentity + power int64 +} + +func discoveryValSet(seedsToPowers map[int]int64) []discoveryVal { + vals := make([]discoveryVal, 0, len(seedsToPowers)) + for seed, power := range seedsToPowers { + vals = append(vals, discoveryVal{id: testcrypto.NewCryptoIdentityFromIntSeed(seed), power: power}) + } + return vals +} + +func consensusValidators(vals []discoveryVal) []providertypes.ConsensusValidator { + out := make([]providertypes.ConsensusValidator, 0, len(vals)) + for _, v := range vals { + pk := v.id.TMProtoCryptoPublicKey() + providerAddr := v.id.ProviderConsAddress() + out = append(out, providertypes.ConsensusValidator{ + ProviderConsAddr: providerAddr.ToSdkConsAddr().Bytes(), + PublicKey: &pk, + Power: v.power, + }) + } + return out +} + +// cometValSetHash computes the reference hash independently of the production +// helper (straight through tmtypes), so the tests do not merely compare the +// production code with itself. +func cometValSetHash(vals []discoveryVal) []byte { + tmVals := make([]*tmtypes.Validator, 0, len(vals)) + for _, v := range vals { + tmVals = append(tmVals, v.id.TMValidator(v.power)) + } + return tmtypes.NewValidatorSet(tmVals).Hash() +} + +// stubCandidateClient makes the mocked client keepers present clientID as a +// tendermint client of chainID at latestHeight with the given status, a +// registered counterparty, and a latest consensus state carrying +// nextValidatorsHash. +func stubCandidateClient( + mocks testkeeper.MockedKeepers, + clientID, chainID string, + latestHeight clienttypes.Height, + status ibcexported.Status, + nextValidatorsHash []byte, +) { + mocks.MockClientKeeper.EXPECT().GetClientStatus(gomock.Any(), clientID).Return(status).AnyTimes() + mocks.ClientCounterparties[clientID] = clientv2types.CounterpartyInfo{ClientId: "counterparty-of-" + clientID} + mocks.MockClientKeeper.EXPECT().GetClientConsensusState(gomock.Any(), clientID, latestHeight). + DoAndReturn(func(sdk.Context, string, ibcexported.Height) (ibcexported.ConsensusState, bool) { + return &ibctmtypes.ConsensusState{NextValidatorsHash: nextValidatorsHash}, true + }).AnyTimes() +} + +// stubClientIteration makes IterateClientStates yield exactly the given +// clients, in order. +func stubClientIteration(mocks testkeeper.MockedKeepers, clients map[string]*ibctmtypes.ClientState, order []string) { + mocks.MockClientKeeper.EXPECT().IterateClientStates(gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(_ sdk.Context, _ []byte, cb func(string, ibcexported.ClientState) bool) { + for _, clientID := range order { + if cb(clientID, clients[clientID]) { + return + } + } + }).AnyTimes() +} + +// TestDiscoveryAdoptsContentVerifiedClient covers the adoption happy path: a +// candidate client of the right chain id whose latest consensus state carries +// the hash of the validator set the provider currently has stored for the +// consumer is adopted and persisted. +func TestDiscoveryAdoptsContentVerifiedClient(t *testing.T) { + k, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + consumerId := k.FetchAndIncrementConsumerId(ctx) + k.SetConsumerChainId(ctx, consumerId, "consumer-1") + + vals := discoveryValSet(map[int]int64{1: 100, 2: 50}) + require.NoError(t, k.SetConsumerValSet(ctx, consumerId, consensusValidators(vals))) + + clientID := "07-tendermint-3" + height := clienttypes.NewHeight(1, 42) + stubCandidateClient(mocks, clientID, "consumer-1", height, ibcexported.Active, cometValSetHash(vals)) + stubClientIteration(mocks, map[string]*ibctmtypes.ClientState{ + clientID: {ChainId: "consumer-1", LatestHeight: height}, + }, []string{clientID}) + + got := k.DiscoverActiveConsumerClientForTest(ctx, consumerId, "") + require.Equal(t, clientID, got) + + stored, found := k.GetConsumerClientId(ctx, consumerId) + require.True(t, found, "adoption must persist the client id") + require.Equal(t, clientID, stored) +} + +// TestDiscoveryRejectsForgedClient covers the chain-id-collision attack: a +// client that copies the consumer's chain id (and is Active, counterparty- +// linked, and up to date) but tracks a chain run by a different validator set +// must not be adopted, and the look-alike must be logged at warn level. +func TestDiscoveryRejectsForgedClient(t *testing.T) { + k, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + var logBuf bytes.Buffer + ctx = ctx.WithLogger(log.NewLogger(&logBuf)) + + consumerId := k.FetchAndIncrementConsumerId(ctx) + k.SetConsumerChainId(ctx, consumerId, "consumer-1") + + honestVals := discoveryValSet(map[int]int64{1: 100, 2: 50}) + require.NoError(t, k.SetConsumerValSet(ctx, consumerId, consensusValidators(honestVals))) + + // The forged chain is run by the attacker's validators: its (perfectly + // valid) light client carries their hash, not the one the provider sent. + attackerVals := discoveryValSet(map[int]int64{666: 100}) + + clientID := "07-tendermint-9" + height := clienttypes.NewHeight(1, 4242) + stubCandidateClient(mocks, clientID, "consumer-1", height, ibcexported.Active, cometValSetHash(attackerVals)) + stubClientIteration(mocks, map[string]*ibctmtypes.ClientState{ + clientID: {ChainId: "consumer-1", LatestHeight: height}, + }, []string{clientID}) + + got := k.DiscoverActiveConsumerClientForTest(ctx, consumerId, "") + require.Empty(t, got, "a forged client must not be adopted") + + _, found := k.GetConsumerClientId(ctx, consumerId) + require.False(t, found, "a forged client must not be persisted") + + require.Contains(t, logBuf.String(), "look-alike", + "a chain-id match failing content verification must be logged") +} + +// TestDiscoveryOneStepTolerance verifies a candidate is adopted when its +// consensus state carries the hash of the previously stored validator set: +// the consumer keeps running the previous set until the VSC packet carrying +// the newest one is delivered, which it cannot be before a client is adopted. +func TestDiscoveryOneStepTolerance(t *testing.T) { + k, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + consumerId := k.FetchAndIncrementConsumerId(ctx) + k.SetConsumerChainId(ctx, consumerId, "consumer-1") + + launchVals := discoveryValSet(map[int]int64{1: 100, 2: 50}) + rotatedVals := discoveryValSet(map[int]int64{1: 100, 2: 50, 3: 30}) + require.NoError(t, k.SetConsumerValSet(ctx, consumerId, consensusValidators(launchVals))) + require.NoError(t, k.SetConsumerValSet(ctx, consumerId, consensusValidators(rotatedVals))) + + // The consumer chain is still running the launch set: no VSC has ever + // been delivered to it. + clientID := "07-tendermint-3" + height := clienttypes.NewHeight(1, 42) + stubCandidateClient(mocks, clientID, "consumer-1", height, ibcexported.Active, cometValSetHash(launchVals)) + stubClientIteration(mocks, map[string]*ibctmtypes.ClientState{ + clientID: {ChainId: "consumer-1", LatestHeight: height}, + }, []string{clientID}) + + got := k.DiscoverActiveConsumerClientForTest(ctx, consumerId, "") + require.Equal(t, clientID, got, "a client carrying the previous set's hash must be adopted") +} + +// TestDiscoveryLatchHoldsAcrossClientDeath verifies that once a client has +// been adopted it is returned unconditionally: no status check, no +// counterparty check, no re-discovery. The mocks report the adopted client as +// Expired and counterparty-less -- were the latch to re-inspect or re-discover, +// the unstubbed IterateClientStates expectation would fail the test. +func TestDiscoveryLatchHoldsAcrossClientDeath(t *testing.T) { + k, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + consumerId := k.FetchAndIncrementConsumerId(ctx) + k.SetConsumerChainId(ctx, consumerId, "consumer-1") + + adopted := "07-tendermint-0" + k.SetConsumerClientId(ctx, consumerId, adopted) + + // The adopted client is as dead as a client can be: expired and without a + // counterparty (ClientCounterparties is left empty). The latch must not care. + mocks.MockClientKeeper.EXPECT().GetClientStatus(gomock.Any(), adopted).Return(ibcexported.Expired).AnyTimes() + + for range 3 { + got := k.DiscoverActiveConsumerClientForTest(ctx, consumerId, adopted) + require.Equal(t, adopted, got, "the adopted client must be returned unconditionally") + } + + stored, found := k.GetConsumerClientId(ctx, consumerId) + require.True(t, found) + require.Equal(t, adopted, stored, "the stored binding must not move") +} + +// TestDiscoveryAdoptsNothingWhenNoCandidateVerifies verifies discovery fails +// closed: with no verifying candidate nothing is adopted, and the next call +// retries from scratch -- adopting once a candidate verifies. +func TestDiscoveryAdoptsNothingWhenNoCandidateVerifies(t *testing.T) { + k, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + consumerId := k.FetchAndIncrementConsumerId(ctx) + k.SetConsumerChainId(ctx, consumerId, "consumer-1") + + vals := discoveryValSet(map[int]int64{1: 100}) + require.NoError(t, k.SetConsumerValSet(ctx, consumerId, consensusValidators(vals))) + + clientID := "07-tendermint-3" + height := clienttypes.NewHeight(1, 42) + + // The candidate's consensus state carries a foreign hash at first (e.g. + // the relayer is mid-setup and the client is not the consumer's), then + // the genuine one on the next epoch's retry. + currentHash := cometValSetHash(discoveryValSet(map[int]int64{666: 1})) + mocks.MockClientKeeper.EXPECT().GetClientStatus(gomock.Any(), clientID).Return(ibcexported.Active).AnyTimes() + mocks.ClientCounterparties[clientID] = clientv2types.CounterpartyInfo{ClientId: "counterparty"} + mocks.MockClientKeeper.EXPECT().GetClientConsensusState(gomock.Any(), clientID, height). + DoAndReturn(func(sdk.Context, string, ibcexported.Height) (ibcexported.ConsensusState, bool) { + return &ibctmtypes.ConsensusState{NextValidatorsHash: currentHash}, true + }).AnyTimes() + stubClientIteration(mocks, map[string]*ibctmtypes.ClientState{ + clientID: {ChainId: "consumer-1", LatestHeight: height}, + }, []string{clientID}) + + got := k.DiscoverActiveConsumerClientForTest(ctx, consumerId, "") + require.Empty(t, got, "no candidate verifies: nothing must be adopted") + _, found := k.GetConsumerClientId(ctx, consumerId) + require.False(t, found) + + // Next epoch: the candidate now carries the genuine hash and is adopted. + currentHash = cometValSetHash(vals) + got = k.DiscoverActiveConsumerClientForTest(ctx, consumerId, "") + require.Equal(t, clientID, got, "discovery must retry and adopt once a candidate verifies") +} + +// TestDiscoveryPrefersHighestVerifiedHeight verifies the tie-break among +// several verifying candidates: the client with the highest latest height +// (the one a relayer is actively updating) wins. +func TestDiscoveryPrefersHighestVerifiedHeight(t *testing.T) { + k, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + consumerId := k.FetchAndIncrementConsumerId(ctx) + k.SetConsumerChainId(ctx, consumerId, "consumer-1") + + vals := discoveryValSet(map[int]int64{1: 100}) + require.NoError(t, k.SetConsumerValSet(ctx, consumerId, consensusValidators(vals))) + hash := cometValSetHash(vals) + + stale := "07-tendermint-1" + fresh := "07-tendermint-2" + staleHeight := clienttypes.NewHeight(1, 10) + freshHeight := clienttypes.NewHeight(1, 99) + stubCandidateClient(mocks, stale, "consumer-1", staleHeight, ibcexported.Active, hash) + stubCandidateClient(mocks, fresh, "consumer-1", freshHeight, ibcexported.Active, hash) + stubClientIteration(mocks, map[string]*ibctmtypes.ClientState{ + stale: {ChainId: "consumer-1", LatestHeight: staleHeight}, + fresh: {ChainId: "consumer-1", LatestHeight: freshHeight}, + }, []string{stale, fresh}) + + got := k.DiscoverActiveConsumerClientForTest(ctx, consumerId, "") + require.Equal(t, fresh, got) +} + +// TestDiscoverySkipsWhenNoValSetHashAvailable verifies discovery fails closed +// when the provider has nothing to verify candidates against (no stored +// validator set and no retained previous hash): it must not even scan for +// candidates, let alone adopt one. +func TestDiscoverySkipsWhenNoValSetHashAvailable(t *testing.T) { + k, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + consumerId := k.FetchAndIncrementConsumerId(ctx) + k.SetConsumerChainId(ctx, consumerId, "consumer-1") + + // IterateClientStates is deliberately not stubbed: scanning would fail the test. + got := k.DiscoverActiveConsumerClientForTest(ctx, consumerId, "") + require.Empty(t, got) +} + +// TestSetConsumerValSetRotatesPrevValSetHash verifies the previous-set hash +// bookkeeping that backs discovery's one-step tolerance: absent before any +// rotation, and always the hash of the set most recently replaced afterwards. +func TestSetConsumerValSetRotatesPrevValSetHash(t *testing.T) { + k, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + consumerId := k.FetchAndIncrementConsumerId(ctx) + + setA := discoveryValSet(map[int]int64{1: 100}) + setB := discoveryValSet(map[int]int64{1: 100, 2: 50}) + setC := discoveryValSet(map[int]int64{2: 50}) + + // First set ever: there is no previous set, so no hash is retained. + require.NoError(t, k.SetConsumerValSet(ctx, consumerId, consensusValidators(setA))) + _, found := k.GetConsumerPrevValSetHash(ctx, consumerId) + require.False(t, found, "no previous hash may exist before the first rotation") + + // Each rotation retains the hash of the set it replaced. + require.NoError(t, k.SetConsumerValSet(ctx, consumerId, consensusValidators(setB))) + got, found := k.GetConsumerPrevValSetHash(ctx, consumerId) + require.True(t, found) + require.Equal(t, cometValSetHash(setA), got) + + require.NoError(t, k.SetConsumerValSet(ctx, consumerId, consensusValidators(setC))) + got, found = k.GetConsumerPrevValSetHash(ctx, consumerId) + require.True(t, found) + require.Equal(t, cometValSetHash(setB), got) + + // The production hash helper must agree with the independently computed + // CometBFT hash, or the content check would never match a real consumer. + helperHash, err := providerkeeper.ComputeConsumerValSetHash(consensusValidators(setB)) + require.NoError(t, err) + require.Equal(t, cometValSetHash(setB), helperHash) +} + +// TestComputeConsumerValSetHashSkipsZeroPower verifies zero-power entries do +// not contribute to the hash: zero power means "not in the set" throughout +// the protocol, so the consumer's consensus engine never hashes them either. +func TestComputeConsumerValSetHashSkipsZeroPower(t *testing.T) { + active := discoveryVal{id: testcrypto.NewCryptoIdentityFromIntSeed(1), power: 100} + removed := discoveryVal{id: testcrypto.NewCryptoIdentityFromIntSeed(2), power: 0} + + withZero, err := providerkeeper.ComputeConsumerValSetHash(consensusValidators([]discoveryVal{active, removed})) + require.NoError(t, err) + require.Equal(t, cometValSetHash([]discoveryVal{active}), withZero, + "a zero-power entry must not change the hash") + + _, err = providerkeeper.ComputeConsumerValSetHash(consensusValidators([]discoveryVal{ + {id: testcrypto.NewCryptoIdentityFromIntSeed(3), power: -1}, + })) + require.Error(t, err, "a negative power is invalid input, not a removal") +} diff --git a/x/vaas/provider/keeper/consumer_lifecycle.go b/x/vaas/provider/keeper/consumer_lifecycle.go index 59060421..af5bcc74 100644 --- a/x/vaas/provider/keeper/consumer_lifecycle.go +++ b/x/vaas/provider/keeper/consumer_lifecycle.go @@ -622,6 +622,7 @@ func (k Keeper) DeleteConsumerChain(ctx sdk.Context, consumerId uint64) (err err k.DeletePendingVSCPackets(ctx, consumerId) k.DeleteConsumerValSet(ctx, consumerId) + k.DeleteConsumerPrevValSetHash(ctx, consumerId) k.DeleteConsumerRemovalTime(ctx, consumerId) k.DeleteConsumerLastAckTime(ctx, consumerId) diff --git a/x/vaas/provider/keeper/export_test.go b/x/vaas/provider/keeper/export_test.go index 93ec134e..9873add0 100644 --- a/x/vaas/provider/keeper/export_test.go +++ b/x/vaas/provider/keeper/export_test.go @@ -44,3 +44,11 @@ func (k Keeper) WindowEndTimestampForTest(ctx sdk.Context, clientId string, wind func (k Keeper) VerifyDowntimeChallengeHeaderForTest(ctx sdk.Context, clientId string, header *ibctmtypes.Header) error { return k.verifyDowntimeChallengeHeader(ctx, clientId, header) } + +// DiscoverActiveConsumerClientForTest exposes discoverActiveConsumerClient so +// tests can drive client adoption -- the content check, the fail-closed +// no-candidate path, and the post-adoption latch -- without assembling the +// full SendVSCPackets EndBlock flow around it. +func (k Keeper) DiscoverActiveConsumerClientForTest(ctx sdk.Context, consumerId uint64, currentClientID string) string { + return k.discoverActiveConsumerClient(ctx, consumerId, currentClientID) +} diff --git a/x/vaas/provider/keeper/genesis.go b/x/vaas/provider/keeper/genesis.go index 5b73094c..51adbe98 100644 --- a/x/vaas/provider/keeper/genesis.go +++ b/x/vaas/provider/keeper/genesis.go @@ -102,6 +102,11 @@ func (k Keeper) InitGenesis(ctx sdk.Context, genState *types.GenesisState) []abc if len(cs.PendingValsetChanges) > 0 { k.AppendPendingVSCPackets(ctx, consumerId, cs.PendingValsetChanges...) } + if len(cs.PrevConsumerValsetHash) > 0 { + if err := k.SetConsumerPrevValSetHash(ctx, consumerId, cs.PrevConsumerValsetHash); err != nil { + panic(fmt.Errorf("init: set previous valset hash for %d: %w", consumerId, err)) + } + } } // Second pass: derive queue and equivocation-min-height state from the @@ -464,6 +469,13 @@ func (k Keeper) ExportGenesis(ctx sdk.Context) *types.GenesisState { cs.HighestAckedVscId = v } + // Previous-valset hash for client discovery's content check: exported + // so a consumer that has not adopted a client yet keeps its one-step + // verification tolerance across a state-export restart. + if h, found := k.GetConsumerPrevValSetHash(ctx, consumerId); found { + cs.PrevConsumerValsetHash = h + } + consumerStates = append(consumerStates, cs) } diff --git a/x/vaas/provider/keeper/genesis_test.go b/x/vaas/provider/keeper/genesis_test.go index 28b517e0..c886a7a7 100644 --- a/x/vaas/provider/keeper/genesis_test.go +++ b/x/vaas/provider/keeper/genesis_test.go @@ -1,6 +1,7 @@ package keeper_test import ( + "bytes" "fmt" "sort" "testing" @@ -194,18 +195,28 @@ func TestInitGenesisRestoresPerConsumerStateAndDerivedQueues(t *testing.T) { ValsetUpdateId: 1, Params: providertypes.DefaultParams(), ConsumerStates: []providertypes.ConsumerState{ - {ConsumerId: 0, ChainId: "consumer-alpha", Phase: providertypes.CONSUMER_PHASE_REGISTERED, - OwnerAddress: owner, Metadata: &md}, - {ConsumerId: 1, ChainId: "consumer-beta", Phase: providertypes.CONSUMER_PHASE_INITIALIZED, - OwnerAddress: owner, Metadata: &md, InitParams: &ip}, - {ConsumerId: 2, ChainId: "consumer-gamma", Phase: providertypes.CONSUMER_PHASE_LAUNCHED, + { + ConsumerId: 0, ChainId: "consumer-alpha", Phase: providertypes.CONSUMER_PHASE_REGISTERED, + OwnerAddress: owner, Metadata: &md, + }, + { + ConsumerId: 1, ChainId: "consumer-beta", Phase: providertypes.CONSUMER_PHASE_INITIALIZED, + OwnerAddress: owner, Metadata: &md, InitParams: &ip, + }, + { + ConsumerId: 2, ChainId: "consumer-gamma", Phase: providertypes.CONSUMER_PHASE_LAUNCHED, OwnerAddress: owner, Metadata: &md, InitParams: &ip, - ClientId: "07-tendermint-0", ConsumerGenesis: cg}, - {ConsumerId: 3, ChainId: "consumer-delta", Phase: providertypes.CONSUMER_PHASE_STOPPED, + ClientId: "07-tendermint-0", ConsumerGenesis: cg, + }, + { + ConsumerId: 3, ChainId: "consumer-delta", Phase: providertypes.CONSUMER_PHASE_STOPPED, OwnerAddress: owner, Metadata: &md, InitParams: &ip, - ClientId: "07-tendermint-1", ConsumerGenesis: cg, RemovalTime: &removeAt}, - {ConsumerId: 4, ChainId: "consumer-epsilon", Phase: providertypes.CONSUMER_PHASE_DELETED, - OwnerAddress: owner, Metadata: &md, InitParams: &ip}, + ClientId: "07-tendermint-1", ConsumerGenesis: cg, RemovalTime: &removeAt, + }, + { + ConsumerId: 4, ChainId: "consumer-epsilon", Phase: providertypes.CONSUMER_PHASE_DELETED, + OwnerAddress: owner, Metadata: &md, InitParams: &ip, + }, }, } @@ -383,6 +394,12 @@ func TestGenesisRoundTrip(t *testing.T) { pkA.SetConsumerHighestSentVscId(ctxA, keyedConsumerID, 7) pkA.SetConsumerHighestAckedVscId(ctxA, keyedConsumerID, 5) + // Seed the previous-valset hash used by client discovery's one-step + // content check so the round-trip covers it (tmhash-sized, as a real + // CometBFT validator set hash would be). + prevValSetHash := bytes.Repeat([]byte{0xAB}, 32) + require.NoError(t, pkA.SetConsumerPrevValSetHash(ctxA, keyedConsumerID, prevValSetHash)) + // Seed the downtime-detection state (pending slash, previous downtime // params, epoch share records) so the round-trip covers it too. downtimeProviderAddr := providertypes.NewProviderConsAddress([]byte("provider-addr-downtime-x1")) @@ -518,6 +535,12 @@ func TestGenesisRoundTrip(t *testing.T) { require.NotNil(t, zeta.PauseExpirationTime, "PAUSED consumer must carry pause_expiration_time") require.Equal(t, pauseExpiresAt, *zeta.PauseExpirationTime) + // Sanity: the LAUNCHED consumer (gamma) carries the previous-valset hash. + gamma, ok := byChainId["consumer-gamma"] + require.True(t, ok, "consumer-gamma missing from export") + require.Equal(t, prevValSetHash, gamma.PrevConsumerValsetHash, + "previous valset hash missing from export") + // Fresh keeper B. pkB, ctxB, ctrlB, stakingB := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrlB.Finish() @@ -580,6 +603,10 @@ func TestGenesisRoundTrip(t *testing.T) { require.True(t, pkB.IsEpochDowntime(ctxB, keyedConsumerID, downtimeProviderAddr.ToSdkConsAddr()), "EpochDowntime lost across round-trip") + gotPrevHash, foundPrevHash := pkB.GetConsumerPrevValSetHash(ctxB, keyedConsumerID) + require.True(t, foundPrevHash, "previous valset hash lost across round-trip") + require.Equal(t, prevValSetHash, gotPrevHash) + // The PAUSED consumer's pause-expiration queue must be rebuilt from the // per-consumer pause_expiration_time, mirroring the removal-time queue. const zetaConsumerID uint64 = 5 diff --git a/x/vaas/provider/keeper/keeper.go b/x/vaas/provider/keeper/keeper.go index 340324da..de4e5807 100644 --- a/x/vaas/provider/keeper/keeper.go +++ b/x/vaas/provider/keeper/keeper.go @@ -101,6 +101,14 @@ type Keeper struct { ConsumerValidators collections.Map[collections.Pair[uint64, []byte], types.ConsensusValidator] LastProviderConsensusVals collections.Map[[]byte, types.ConsensusValidator] + // ConsumerPrevValSetHash retains, per consumer, the CometBFT hash of the + // validator set that was stored for the consumer immediately before the + // current one (see SetConsumerValSet). Client discovery content-verifies a + // candidate IBC client against the hash of the current set or this one: + // while the latest VSC packet is in flight, the consumer is still running + // the previous set, so its headers still carry this hash. + ConsumerPrevValSetHash collections.Map[uint64, []byte] + // Fee pool collections. // // ConsumerFeePoolShares is keyed (consumer_id, denom, depositor): denom @@ -288,6 +296,7 @@ func NewKeeper( // Validator set collections ConsumerValidators: collections.NewMap(sb, types.ConsumerValidatorPrefix, "consumer_validators", collections.PairKeyCodec(collections.Uint64Key, collections.BytesKey), codec.CollValue[types.ConsensusValidator](cdc)), LastProviderConsensusVals: collections.NewMap(sb, types.LastProviderConsensusVals, "last_provider_consensus_vals", collections.BytesKey, codec.CollValue[types.ConsensusValidator](cdc)), + ConsumerPrevValSetHash: collections.NewMap(sb, types.ConsumerIdToPrevValSetHashPrefix, "consumer_prev_val_set_hash", collections.Uint64Key, collections.BytesValue), } // Fee pool collections @@ -1333,8 +1342,31 @@ func (k Keeper) GetConsumerValSet( return validators, nil } -// SetConsumerValSet resets the current consumer validators with the `nextValidators` +// SetConsumerValSet resets the current consumer validators with the `nextValidators`. +// +// Before the stored set is replaced, its CometBFT hash is retained in +// ConsumerPrevValSetHash: while the VSC packet carrying nextValidators is in +// flight, the consumer chain is still running the outgoing set, so client +// discovery must be able to content-verify a candidate client against either +// set's hash (see discoverActiveConsumerClient). When no set is stored yet -- +// the consumer's first set at launch, or right after a state-export restart +// (the per-validator entries are not part of genesis) -- the retained hash is +// left untouched rather than degraded to the hash of an empty set. func (k Keeper) SetConsumerValSet(ctx context.Context, consumerId uint64, nextValidators []types.ConsensusValidator) error { + prevValidators, err := k.GetConsumerValSet(ctx, consumerId) + if err != nil { + return err + } + if len(prevValidators) > 0 { + prevHash, err := ComputeConsumerValSetHash(prevValidators) + if err != nil { + return fmt.Errorf("hashing outgoing consumer validator set, consumerId(%d): %w", consumerId, err) + } + if err := k.SetConsumerPrevValSetHash(ctx, consumerId, prevHash); err != nil { + return err + } + } + // First delete existing validators k.DeleteConsumerValSet(ctx, consumerId) @@ -1347,6 +1379,30 @@ func (k Keeper) SetConsumerValSet(ctx context.Context, consumerId uint64, nextVa return nil } +// SetConsumerPrevValSetHash stores the CometBFT hash of the consumer's +// previous validator set (see ConsumerPrevValSetHash). +func (k Keeper) SetConsumerPrevValSetHash(ctx context.Context, consumerId uint64, hash []byte) error { + return k.ConsumerPrevValSetHash.Set(ctx, consumerId, hash) +} + +// GetConsumerPrevValSetHash returns the CometBFT hash of the consumer's +// previous validator set, if one has been retained. +func (k Keeper) GetConsumerPrevValSetHash(ctx context.Context, consumerId uint64) ([]byte, bool) { + hash, err := k.ConsumerPrevValSetHash.Get(ctx, consumerId) + if err != nil { + return nil, false + } + return hash, true +} + +// DeleteConsumerPrevValSetHash removes the retained previous-validator-set +// hash for the consumer. +func (k Keeper) DeleteConsumerPrevValSetHash(ctx context.Context, consumerId uint64) { + if err := k.ConsumerPrevValSetHash.Remove(ctx, consumerId); err != nil { + panic(fmt.Errorf("failed to delete previous valset hash for consumer id (%d): %w", consumerId, err)) + } +} + // DeleteConsumerValSet deletes all the stored consumer validators for chain with `consumerId` func (k Keeper) DeleteConsumerValSet(ctx context.Context, consumerId uint64) { iter, err := k.ConsumerValidators.Iterate(ctx, collections.NewPrefixedPairRange[uint64, []byte](consumerId)) diff --git a/x/vaas/provider/keeper/relay.go b/x/vaas/provider/keeper/relay.go index 25e86033..aec580a6 100644 --- a/x/vaas/provider/keeper/relay.go +++ b/x/vaas/provider/keeper/relay.go @@ -1,6 +1,7 @@ package keeper import ( + "bytes" "errors" "fmt" @@ -14,15 +15,22 @@ import ( ibcexported "github.com/cosmos/ibc-go/v10/modules/core/exported" ibctmtypes "github.com/cosmos/ibc-go/v10/modules/light-clients/07-tendermint" - errorsmod "cosmossdk.io/errors" - sdk "github.com/cosmos/cosmos-sdk/types" ) func (k Keeper) OnAcknowledgementPacketV2(ctx sdk.Context, sourceClientID string, ackVscId uint64, ackError string) error { consumerId, found := k.GetClientIdToConsumerId(ctx, sourceClientID) if !found { - return errorsmod.Wrapf(providertypes.ErrInvalidConsumerClient, "recv acknowledgement on unknown client %s", sourceClientID) + // ibc-go only delivers acknowledgements for packets this chain actually + // sent (the stored packet commitment is checked before the callback + // runs), and the provider only ever sends over tracked clients. An + // unknown client here therefore means the consumer was removed after + // the packet went out: a stale-but-honest delivery. Log and succeed -- + // failing would fail the relayer's whole tx over a packet nobody + // tracks anymore. + k.Logger(ctx).Info("recv acknowledgement on unknown client, ignoring", + "clientID", sourceClientID, "error", ackError) + return nil } if ackError != "" { @@ -43,8 +51,12 @@ func (k Keeper) OnAcknowledgementPacketV2(ctx sdk.Context, sourceClientID string func (k Keeper) OnTimeoutPacketV2(ctx sdk.Context, sourceClientID string) error { consumerId, found := k.GetClientIdToConsumerId(ctx, sourceClientID) if !found { - k.Logger(ctx).Error("packet timeout, unknown client:", "clientID", sourceClientID) - return errorsmod.Wrapf(providertypes.ErrInvalidConsumerClient, "timeout on unknown client %s", sourceClientID) + // Same reasoning as OnAcknowledgementPacketV2: a timeout can only be + // proven for a packet this chain sent, so an unknown client means the + // consumer is gone. Log-only, so the relayer's tx is not failed over a + // stale delivery. + k.Logger(ctx).Info("packet timeout on unknown client, ignoring", "clientID", sourceClientID) + return nil } k.Logger(ctx).Info("packet timeout, retrying next epoch; liveness sweep owns removal", "consumerId", consumerId, "clientId", sourceClientID) @@ -141,24 +153,50 @@ func (k Keeper) SendVSCPackets(ctx sdk.Context) error { return nil } -// discoverActiveConsumerClient scans for IBC clients pointing to the consumer chain -// and returns the one with the highest latest height that has a counterparty registered. -// This allows the provider to use a client being actively updated by a relayer. -// The current client is only replaced if it is expired, frozen, or has no counterparty. +// discoverActiveConsumerClient returns the IBC client the provider uses to +// reach the consumer. +// +// Once a client has been adopted for the consumer, it is returned +// unconditionally: the binding never moves again, no matter the client's +// status. An expired or frozen adopted client halts VSC traffic (sends fail +// and stay queued; the liveness sweep eventually removes a consumer that +// never resumes acknowledging) instead of reopening adoption -- anyone can +// permissionlessly create a client for a chain that reuses the consumer's +// chain id, so re-running discovery on client death would hand a look-alike +// chain a standing opportunity to capture the binding. Recovering a dead +// client is governance's job via ibc-go's MsgRecoverClient, which substitutes +// fresh client state under the SAME client id: the binding survives recovery +// unchanged, even though the client's latest height may jump arbitrarily. +// +// While no client was ever adopted, each call scans for a candidate and +// adopts one only if it proves by content to track the chain the provider +// itself launched: the candidate must be an Active tendermint client of the +// consumer's chain id with a registered counterparty, whose latest consensus +// state carries the CometBFT hash of the validator set the provider most +// recently computed for this consumer -- or of the set before that, since the +// consumer keeps running the previous set until the latest VSC packet is +// delivered. The chain id string is trivially copied by an attacker, but +// these hashes are not: producing a header carrying them requires the very +// validators the provider put in charge of the consumer to have signed it. A +// same-chain-id client that fails the content check is logged at warn level +// and skipped. If several candidates verify, the one with the highest latest +// height wins; if none does, nothing is adopted and discovery retries at the +// next epoch boundary (fail closed). func (k Keeper) discoverActiveConsumerClient(ctx sdk.Context, consumerId uint64, currentClientID string) string { if currentClientID != "" { - currentStatus := k.clientKeeper.GetClientStatus(ctx, currentClientID) - if currentStatus == ibcexported.Active { - cp, found := k.clientV2Keeper.GetClientCounterparty(ctx, currentClientID) - if found && cp.ClientId != "" { - return currentClientID - } - } + return currentClientID } chainID, err := k.GetConsumerChainId(ctx, consumerId) if err != nil { - return currentClientID + return "" + } + + expectedHashes := k.expectedConsumerValSetHashes(ctx, consumerId) + if len(expectedHashes) == 0 { + k.Logger(ctx).Error("no consumer validator set hash available to verify candidate clients against, skipping discovery", + "consumerId", consumerId) + return "" } var bestClient string @@ -176,6 +214,14 @@ func (k Keeper) discoverActiveConsumerClient(ctx sdk.Context, consumerId uint64, if !found || cp.ClientId == "" { return false } + if !k.clientCarriesExpectedValSetHash(ctx, clientID, tmCS.LatestHeight, expectedHashes) { + k.Logger(ctx).Warn("client matches the consumer chain id but its consensus state does not carry a validator set this provider sent; ignoring look-alike client", + "consumerId", consumerId, + "chainId", chainID, + "clientId", clientID, + ) + return false + } height := tmCS.LatestHeight.RevisionHeight if height > bestHeight { bestHeight = height @@ -185,15 +231,63 @@ func (k Keeper) discoverActiveConsumerClient(ctx sdk.Context, consumerId uint64, }) if bestClient != "" { - k.Logger(ctx).Info("switching to discovered active client", + k.Logger(ctx).Info("adopting content-verified consumer client", "consumerId", consumerId, - "oldClient", currentClientID, - "newClient", bestClient, + "clientId", bestClient, ) k.SetConsumerClientId(ctx, consumerId, bestClient) return bestClient } - return currentClientID + return "" +} + +// expectedConsumerValSetHashes returns the CometBFT hashes a genuine client +// of the consumer chain may currently carry in its latest consensus state: +// the hash of the validator set most recently computed for the consumer and, +// once at least one rotation has happened, the hash of the set before that +// (still running on the consumer while the latest VSC packet is in flight). +func (k Keeper) expectedConsumerValSetHashes(ctx sdk.Context, consumerId uint64) [][]byte { + var hashes [][]byte + + valSet, err := k.GetConsumerValSet(ctx, consumerId) + if err != nil { + k.Logger(ctx).Error("failed to read consumer validator set", + "consumerId", consumerId, "error", err.Error()) + return nil + } + if len(valSet) > 0 { + currentHash, err := ComputeConsumerValSetHash(valSet) + if err != nil { + k.Logger(ctx).Error("failed to hash consumer validator set", + "consumerId", consumerId, "error", err.Error()) + return nil + } + hashes = append(hashes, currentHash) + } + if prevHash, found := k.GetConsumerPrevValSetHash(ctx, consumerId); found { + hashes = append(hashes, prevHash) + } + return hashes +} + +// clientCarriesExpectedValSetHash reports whether the client's consensus +// state at the given height carries one of the expected validator set hashes +// in its NextValidatorsHash. +func (k Keeper) clientCarriesExpectedValSetHash(ctx sdk.Context, clientID string, height clienttypes.Height, expectedHashes [][]byte) bool { + consState, found := k.clientKeeper.GetClientConsensusState(ctx, clientID, height) + if !found { + return false + } + tmConsState, ok := consState.(*ibctmtypes.ConsensusState) + if !ok { + return false + } + for _, expected := range expectedHashes { + if bytes.Equal(tmConsState.NextValidatorsHash, expected) { + return true + } + } + return false } func (k Keeper) SendVSCPacketsToChain(ctx sdk.Context, consumerId uint64, clientId string) error { diff --git a/x/vaas/provider/keeper/relay_test.go b/x/vaas/provider/keeper/relay_test.go index e8ee2db5..707729bd 100644 --- a/x/vaas/provider/keeper/relay_test.go +++ b/x/vaas/provider/keeper/relay_test.go @@ -49,17 +49,23 @@ func TestOnAcknowledgementPacketV2(t *testing.T) { require.Equal(t, providertypes.CONSUMER_PHASE_LAUNCHED, phase) } -// TestOnAcknowledgementPacketV2UnknownClient tests error handling for unknown clients. -func TestOnAcknowledgementPacketV2UnknownClient(t *testing.T) { +// TestOnAcknowledgementPacketV2UnknownClientIsLogOnly verifies that an +// acknowledgement arriving on a client no consumer tracks -- a stale-but-honest +// delivery for a packet sent before its consumer was removed -- is ignored +// without error, so the relayer's tx does not fail over it. +func TestOnAcknowledgementPacketV2UnknownClientIsLogOnly(t *testing.T) { providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() unknownClientId := "07-tendermint-999" - // Error ack with unknown client should return error err := providerKeeper.OnAcknowledgementPacketV2(ctx, unknownClientId, 0, "some error") - require.Error(t, err) - require.Contains(t, err.Error(), "unknown client") + require.NoError(t, err) + + // A success ack on an unknown client is equally ignored, without + // recording any liveness state for a consumer that does not exist. + err = providerKeeper.OnAcknowledgementPacketV2(ctx, unknownClientId, 7, "") + require.NoError(t, err) } // TestOnTimeoutPacketV2 tests the IBC v2 timeout handler. @@ -88,17 +94,17 @@ func TestOnTimeoutPacketV2(t *testing.T) { require.Equal(t, providertypes.CONSUMER_PHASE_LAUNCHED, phase) } -// TestOnTimeoutPacketV2UnknownClient tests error handling for unknown clients. -func TestOnTimeoutPacketV2UnknownClient(t *testing.T) { +// TestOnTimeoutPacketV2UnknownClientIsLogOnly verifies that a timeout proven +// for a client no consumer tracks is ignored without error, mirroring the +// acknowledgement path: the relayer's tx must not fail over a stale delivery. +func TestOnTimeoutPacketV2UnknownClientIsLogOnly(t *testing.T) { providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) defer ctrl.Finish() unknownClientId := "07-tendermint-999" - // Timeout with unknown client should return error err := providerKeeper.OnTimeoutPacketV2(ctx, unknownClientId) - require.Error(t, err) - require.Contains(t, err.Error(), "unknown client") + require.NoError(t, err) } // TestClientIdToConsumerIdMapping tests the client ID to consumer ID mapping used in IBC v2. diff --git a/x/vaas/provider/keeper/validator_set_update.go b/x/vaas/provider/keeper/validator_set_update.go index 491376d1..8e6465bc 100644 --- a/x/vaas/provider/keeper/validator_set_update.go +++ b/x/vaas/provider/keeper/validator_set_update.go @@ -8,6 +8,8 @@ import ( vaastypes "github.com/allinbits/vaas/x/vaas/types" abci "github.com/cometbft/cometbft/abci/types" + cryptoenc "github.com/cometbft/cometbft/crypto/encoding" + tmtypes "github.com/cometbft/cometbft/types" sdk "github.com/cosmos/cosmos-sdk/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" @@ -170,6 +172,43 @@ func (k Keeper) GetLastBondedValidators(ctx sdk.Context) ([]stakingtypes.Validat return vaastypes.GetLastBondedValidatorsUtil(ctx, k.stakingKeeper, maxVals) } +// ComputeConsumerValSetHash computes the CometBFT hash of a consumer +// validator set exactly as the consumer chain's consensus engine does: the +// stored public keys (the assigned consumer keys, i.e. what the consumer +// actually runs) and powers are assembled into a canonically ordered +// tmtypes.ValidatorSet and hashed. The result is comparable byte-for-byte +// with the NextValidatorsHash the consumer's block headers -- and therefore +// the consensus states of any honest IBC client of the consumer -- carry +// while that set is the consumer's next validator set. +// +// A zero-power entry is skipped rather than hashed: everywhere in the +// protocol (ABCI updates, DiffValidators, the consumer's +// ApplyCCValidatorChanges) zero power means the validator is not in the set, +// so the consumer's consensus engine never includes it in the hash either. +func ComputeConsumerValSetHash(validators []types.ConsensusValidator) ([]byte, error) { + tmValidators := make([]*tmtypes.Validator, 0, len(validators)) + for _, val := range validators { + if val.PublicKey == nil { + return nil, fmt.Errorf("consumer validator %x has no public key", val.ProviderConsAddr) + } + // tmtypes.NewValidatorSet panics on negative powers; a stored consumer + // validator always has non-negative power, so surface a violation as + // an error instead. + if val.Power < 0 { + return nil, fmt.Errorf("consumer validator %x has negative power %d", val.ProviderConsAddr, val.Power) + } + if val.Power == 0 { + continue + } + pubKey, err := cryptoenc.PubKeyFromProto(*val.PublicKey) + if err != nil { + return nil, fmt.Errorf("converting consumer validator %x public key: %w", val.ProviderConsAddr, err) + } + tmValidators = append(tmValidators, tmtypes.NewValidator(pubKey, val.Power)) + } + return tmtypes.NewValidatorSet(tmValidators).Hash(), nil +} + // FullValSetUpdates renders a complete validator set as absolute-power updates. // Used for snapshot VSC packets: the consumer replaces its set with these, // deriving removals against its own current set. diff --git a/x/vaas/provider/types/genesis.go b/x/vaas/provider/types/genesis.go index b400e9ce..c6397121 100644 --- a/x/vaas/provider/types/genesis.go +++ b/x/vaas/provider/types/genesis.go @@ -7,6 +7,8 @@ import ( vaastypes "github.com/allinbits/vaas/x/vaas/types" + "github.com/cometbft/cometbft/crypto/tmhash" + errorsmod "cosmossdk.io/errors" "cosmossdk.io/math" @@ -461,6 +463,10 @@ func (cs ConsumerState) Validate() error { return errors.New("valset update ID cannot be equal to zero") } } + if len(cs.PrevConsumerValsetHash) != 0 && len(cs.PrevConsumerValsetHash) != tmhash.Size { + return fmt.Errorf("previous consumer valset hash must be %d bytes when set, got %d", + tmhash.Size, len(cs.PrevConsumerValsetHash)) + } switch cs.Phase { case CONSUMER_PHASE_REGISTERED: diff --git a/x/vaas/provider/types/genesis.pb.go b/x/vaas/provider/types/genesis.pb.go index 53ae7642..a7113b7f 100644 --- a/x/vaas/provider/types/genesis.pb.go +++ b/x/vaas/provider/types/genesis.pb.go @@ -545,6 +545,14 @@ type ConsumerState struct { // resolved. Set only when phase == PAUSED; absent otherwise. Used to // re-derive the keeper's pause-expiration queue. PauseExpirationTime *time.Time `protobuf:"bytes,16,opt,name=pause_expiration_time,json=pauseExpirationTime,proto3,stdtime" json:"pause_expiration_time,omitempty"` + // PrevConsumerValsetHash is the CometBFT hash of the validator set the + // provider had computed for this consumer immediately before the currently + // stored one. Client discovery accepts a candidate IBC client only if the + // client's latest consensus state carries the hash of the current set or + // this one (the set still running on the consumer while the latest VSC + // packet is in flight). Absent until the consumer's validator set has + // rotated at least once. + PrevConsumerValsetHash []byte `protobuf:"bytes,17,opt,name=prev_consumer_valset_hash,json=prevConsumerValsetHash,proto3" json:"prev_consumer_valset_hash,omitempty"` } func (m *ConsumerState) Reset() { *m = ConsumerState{} } @@ -692,6 +700,13 @@ func (m *ConsumerState) GetPauseExpirationTime() *time.Time { return nil } +func (m *ConsumerState) GetPrevConsumerValsetHash() []byte { + if m != nil { + return m.PrevConsumerValsetHash + } + return nil +} + // ValsetUpdateIdToHeight defines the genesis information for the mapping // of each valset update id to a block height type ValsetUpdateIdToHeight struct { @@ -880,102 +895,103 @@ func init() { func init() { proto.RegisterFile("vaas/provider/v1/genesis.proto", fileDescriptor_c9071b84cde652f9) } var fileDescriptor_c9071b84cde652f9 = []byte{ - // 1508 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xc4, 0x57, 0xcd, 0x6e, 0x1b, 0x47, - 0x12, 0x16, 0xf5, 0x67, 0xa9, 0xf5, 0x47, 0xb5, 0x28, 0x79, 0x2c, 0xdb, 0x94, 0x96, 0x5e, 0x1b, - 0xdc, 0x1f, 0x93, 0xb0, 0x17, 0xeb, 0x05, 0xf6, 0xb0, 0x00, 0x29, 0x4b, 0xbb, 0xc4, 0x22, 0x0e, - 0x31, 0x94, 0xed, 0xc0, 0x3e, 0x4c, 0x9a, 0xd3, 0x25, 0x4e, 0x47, 0xc3, 0xee, 0xc1, 0x74, 0x93, - 0x92, 0x72, 0xc8, 0x39, 0x47, 0x3d, 0x43, 0x5e, 0x20, 0x17, 0x3f, 0x84, 0x91, 0x93, 0xe1, 0x53, - 0x90, 0x83, 0x13, 0xd8, 0x6f, 0x90, 0x27, 0x08, 0xba, 0xa7, 0x67, 0x24, 0xfe, 0x48, 0xb2, 0x81, - 0x00, 0xb9, 0x71, 0xfa, 0xfb, 0xaa, 0xbe, 0xea, 0xea, 0xea, 0xea, 0x22, 0x2a, 0xf6, 0x09, 0x91, - 0xd5, 0x28, 0x16, 0x7d, 0x46, 0x21, 0xae, 0xf6, 0x1f, 0x54, 0x3b, 0xc0, 0x41, 0x32, 0x59, 0x89, - 0x62, 0xa1, 0x04, 0xce, 0x6b, 0xbc, 0x92, 0xe2, 0x95, 0xfe, 0x83, 0xcd, 0x1b, 0xbe, 0x90, 0x5d, - 0x21, 0x3d, 0x83, 0x57, 0x93, 0x8f, 0x84, 0xbc, 0x59, 0xe8, 0x88, 0x8e, 0x48, 0xd6, 0xf5, 0x2f, - 0xbb, 0xba, 0xd5, 0x11, 0xa2, 0x13, 0x42, 0xd5, 0x7c, 0xb5, 0x7b, 0x07, 0x55, 0xc5, 0xba, 0x20, - 0x15, 0xe9, 0x46, 0x96, 0x70, 0xdb, 0xc4, 0xd0, 0x7f, 0x50, 0x95, 0x01, 0x89, 0x81, 0x7a, 0xbe, - 0xe0, 0xb2, 0xd7, 0x85, 0xd8, 0xc2, 0x38, 0x85, 0x8f, 0x58, 0x0c, 0xa9, 0xcf, 0x91, 0xb0, 0xb3, - 0x10, 0x0d, 0xa1, 0xf4, 0xed, 0x12, 0x5a, 0xfc, 0x6f, 0xb2, 0x93, 0x96, 0x22, 0x0a, 0x70, 0x19, - 0xe5, 0xfb, 0x24, 0x94, 0xa0, 0xbc, 0x5e, 0x44, 0x89, 0x02, 0x8f, 0x51, 0x27, 0xb7, 0x9d, 0x2b, - 0x4f, 0xbb, 0xcb, 0xc9, 0xfa, 0x53, 0xb3, 0xdc, 0xa0, 0x38, 0x40, 0x2b, 0x69, 0x04, 0x9e, 0xd4, - 0xb6, 0xd2, 0x99, 0xdc, 0x9e, 0x2a, 0x2f, 0x3c, 0xdc, 0xaa, 0x0c, 0x27, 0xa3, 0xb2, 0x63, 0x89, - 0x46, 0xa3, 0x5e, 0x7c, 0xfd, 0x6e, 0x6b, 0xe2, 0xd7, 0x77, 0x5b, 0x1b, 0x27, 0xa4, 0x1b, 0xfe, - 0xbb, 0x34, 0xe4, 0xa5, 0xe4, 0x2e, 0xfb, 0xe7, 0xe9, 0x12, 0x7f, 0x85, 0x36, 0x87, 0x63, 0xf2, - 0x94, 0xf0, 0x02, 0x60, 0x9d, 0x40, 0x39, 0x53, 0x46, 0xb4, 0x3c, 0x2a, 0xfa, 0x6c, 0x20, 0xde, - 0x7d, 0xf1, 0x3f, 0xc3, 0xaf, 0x4f, 0x6b, 0x75, 0x77, 0xa3, 0x3f, 0x16, 0xc5, 0x8f, 0xd0, 0x6c, - 0x44, 0x62, 0xd2, 0x95, 0xce, 0xf4, 0x76, 0xae, 0xbc, 0xf0, 0xd0, 0x19, 0xf5, 0xdb, 0x34, 0xb8, - 0xf5, 0x63, 0xd9, 0xb8, 0x6b, 0x62, 0x64, 0x94, 0x28, 0x11, 0x67, 0x27, 0xe3, 0x45, 0xbd, 0xf6, - 0x21, 0x9c, 0x48, 0x67, 0xc6, 0xc4, 0xf8, 0x97, 0xb1, 0x31, 0x26, 0x36, 0x69, 0x86, 0x9a, 0xbd, - 0xf6, 0xff, 0xe1, 0xc4, 0x3a, 0x77, 0xfa, 0x63, 0x60, 0xed, 0x10, 0x73, 0x74, 0x33, 0xc3, 0xa4, - 0xd7, 0x3e, 0x39, 0x93, 0x24, 0x94, 0xc6, 0xce, 0xec, 0x95, 0x7a, 0xf5, 0x93, 0xd4, 0x65, 0x8d, - 0xd2, 0x78, 0x44, 0x4f, 0x0e, 0xe2, 0xd8, 0x47, 0xd7, 0x07, 0x14, 0xa4, 0x3e, 0x80, 0x28, 0xee, - 0x71, 0x70, 0xae, 0x19, 0xad, 0x7b, 0x17, 0x1f, 0xba, 0x76, 0x20, 0xf7, 0x45, 0x53, 0xb3, 0xad, - 0x50, 0xc1, 0x1f, 0x83, 0xe1, 0x6f, 0xd0, 0x9f, 0x32, 0x91, 0x03, 0x00, 0xe9, 0x45, 0x10, 0x7b, - 0xed, 0x50, 0xf8, 0x87, 0x9e, 0xe8, 0x43, 0x1c, 0x33, 0x0a, 0xd2, 0x99, 0x33, 0x72, 0x95, 0x8b, - 0xe5, 0xf6, 0x00, 0x64, 0x13, 0xe2, 0xba, 0xb6, 0xfb, 0xdc, 0x9a, 0x59, 0xd9, 0xdb, 0xfe, 0x25, - 0x1c, 0x89, 0x01, 0x39, 0xe7, 0xf5, 0xbd, 0x48, 0x88, 0xd0, 0x33, 0x97, 0x4d, 0x3a, 0xf3, 0x57, - 0xed, 0x72, 0x0f, 0xa0, 0x29, 0x44, 0xd8, 0xd2, 0x74, 0x2b, 0xb7, 0xee, 0x8f, 0xc1, 0x24, 0x3e, - 0x40, 0x4e, 0x04, 0x9c, 0x32, 0xde, 0xf1, 0xa8, 0x38, 0xe2, 0xfa, 0x9a, 0x7b, 0x32, 0x24, 0x32, - 0x00, 0xe9, 0xa0, 0x8b, 0x64, 0x9a, 0x89, 0xc5, 0x63, 0x6b, 0xd0, 0xd2, 0xfc, 0xb4, 0x94, 0xa3, - 0x31, 0x18, 0x48, 0xdc, 0x46, 0x4e, 0x14, 0x43, 0x9f, 0x89, 0x9e, 0x3c, 0x13, 0xb2, 0xc5, 0xbd, - 0x60, 0x8a, 0x7b, 0xcc, 0xa5, 0x69, 0x5a, 0x8b, 0xd4, 0x59, 0x52, 0xec, 0xee, 0x46, 0x34, 0x76, - 0x1d, 0x7f, 0x81, 0xd6, 0x20, 0x12, 0x7e, 0x90, 0xa4, 0xc9, 0x8b, 0xc1, 0x17, 0x31, 0x95, 0xce, - 0xa2, 0xd9, 0x46, 0x69, 0xd4, 0xfd, 0xae, 0x26, 0x9b, 0x3c, 0xb8, 0x86, 0x6a, 0xb7, 0xb0, 0x0a, - 0x43, 0xeb, 0x12, 0xbf, 0x44, 0xeb, 0x8c, 0x1f, 0xc4, 0xc4, 0x57, 0x4c, 0xf0, 0x24, 0x6c, 0x50, - 0x10, 0x4b, 0x67, 0xc9, 0x84, 0x3e, 0x26, 0x45, 0x8d, 0x8c, 0xde, 0xcc, 0xd8, 0x6e, 0x81, 0x8d, - 0x59, 0xc5, 0x2f, 0x51, 0xe1, 0x88, 0xa9, 0x20, 0x80, 0x90, 0x9a, 0x93, 0x4e, 0xe3, 0x5e, 0x36, - 0x71, 0xdf, 0x19, 0xf5, 0xfd, 0xdc, 0xb2, 0xf7, 0x60, 0x30, 0x70, 0x7c, 0x34, 0x0c, 0x48, 0x1c, - 0xa1, 0x1b, 0xc4, 0xf7, 0x21, 0x52, 0x40, 0xcf, 0xf2, 0x7e, 0xc4, 0x38, 0x15, 0x47, 0xd2, 0x59, - 0xb9, 0xa8, 0x7c, 0x6b, 0xd6, 0x24, 0x4d, 0xf0, 0x73, 0x63, 0x30, 0x20, 0x76, 0x9d, 0x8c, 0xe5, - 0x48, 0xfc, 0x25, 0xda, 0x48, 0x4e, 0x21, 0x93, 0x03, 0xae, 0x62, 0x06, 0xd2, 0xc9, 0x1b, 0xb9, - 0x3f, 0x5f, 0x70, 0x10, 0xa9, 0x9f, 0x5d, 0xae, 0xe2, 0xb4, 0xe7, 0x14, 0x60, 0x18, 0x61, 0x20, - 0x31, 0x41, 0x1b, 0x43, 0x5b, 0xf1, 0x0e, 0x42, 0x21, 0x62, 0xe9, 0xac, 0x1a, 0x85, 0xbb, 0xa3, - 0x0a, 0x83, 0x41, 0xee, 0x69, 0x76, 0x2a, 0x41, 0x47, 0x21, 0x59, 0x3a, 0x9d, 0x44, 0xb7, 0x2e, - 0x4b, 0x02, 0xde, 0x42, 0x0b, 0xd9, 0xf5, 0xcc, 0x5e, 0x25, 0x94, 0x2e, 0x35, 0x28, 0xfe, 0x3b, - 0xc2, 0x69, 0x00, 0xa6, 0x1f, 0x26, 0xbd, 0x70, 0x72, 0x3b, 0x57, 0x5e, 0x74, 0xf3, 0x29, 0xa2, - 0xef, 0xab, 0x69, 0x69, 0x7f, 0x45, 0xab, 0x76, 0x27, 0xc0, 0xe9, 0xd9, 0x63, 0x92, 0x2b, 0x4f, - 0xb9, 0x2b, 0x09, 0xb0, 0xcb, 0xa9, 0x7d, 0x15, 0x2a, 0x68, 0xcd, 0x72, 0xa5, 0x22, 0xb1, 0x4a, - 0xd9, 0xd3, 0x86, 0x6d, 0xdd, 0xb4, 0x34, 0x62, 0xf9, 0xbb, 0x68, 0x21, 0x2b, 0x01, 0xa2, 0x9c, - 0x19, 0x53, 0xb2, 0x9b, 0x95, 0xe4, 0x85, 0xaf, 0xa4, 0x2f, 0x7c, 0x65, 0x3f, 0x7d, 0xe1, 0xeb, - 0x73, 0x3a, 0x31, 0xa7, 0x3f, 0x6f, 0xe5, 0x5c, 0x94, 0x1a, 0xd6, 0x54, 0xe9, 0x34, 0x87, 0xd6, - 0xc6, 0xa4, 0xf1, 0x0f, 0xcc, 0x44, 0xc9, 0x47, 0x78, 0xb4, 0x74, 0x7e, 0xe7, 0x80, 0x4a, 0xdf, - 0xe7, 0x50, 0x7e, 0xb8, 0x53, 0x5c, 0xad, 0xf1, 0x2f, 0xe4, 0x50, 0x26, 0x55, 0xcc, 0xda, 0xbd, - 0x24, 0xef, 0x5e, 0x8f, 0xb3, 0x63, 0x8f, 0x13, 0x2e, 0x8c, 0xd2, 0x94, 0xbb, 0x7e, 0x0e, 0xaf, - 0xa9, 0xa7, 0x9c, 0x1d, 0x3f, 0x21, 0x5c, 0xe0, 0x1a, 0x9a, 0x31, 0xed, 0xcb, 0xec, 0x79, 0xbe, - 0xfe, 0x37, 0x7d, 0x16, 0x3f, 0xbd, 0xdb, 0x5a, 0x4f, 0x86, 0x36, 0x49, 0x0f, 0x2b, 0x4c, 0x54, - 0xbb, 0x44, 0x05, 0x95, 0x06, 0x57, 0x6f, 0x5f, 0xdd, 0x47, 0x76, 0x9a, 0x6b, 0x70, 0xe5, 0x26, - 0x96, 0xa5, 0xef, 0xae, 0xa1, 0xa5, 0x81, 0x21, 0xe7, 0xea, 0x70, 0x6f, 0xa0, 0x39, 0x3f, 0x20, - 0x8c, 0x6b, 0x54, 0x87, 0x37, 0xef, 0x5e, 0x33, 0xdf, 0x0d, 0x8a, 0x6f, 0xa2, 0x79, 0x3f, 0x64, - 0xc0, 0x95, 0xc6, 0x4c, 0x50, 0xee, 0x5c, 0xb2, 0xd0, 0xa0, 0xf8, 0x2e, 0x5a, 0x66, 0x9c, 0x29, - 0x46, 0xc2, 0xf3, 0x65, 0x38, 0xed, 0x2e, 0xd9, 0x55, 0x5b, 0x82, 0x4f, 0x50, 0x3e, 0xd3, 0xb7, - 0xb3, 0xaa, 0xad, 0xc3, 0xdb, 0xc9, 0x5d, 0x3d, 0xf7, 0x76, 0x9d, 0x9f, 0x00, 0xed, 0x1d, 0xcd, - 0x66, 0x3b, 0x8b, 0xe9, 0x0e, 0x90, 0xbe, 0x5a, 0x76, 0x18, 0xf3, 0x03, 0xc2, 0x3b, 0x20, 0xed, - 0xb0, 0x71, 0x37, 0xf3, 0x9a, 0xcd, 0x18, 0x2d, 0x50, 0x3b, 0x86, 0xd3, 0x24, 0xfe, 0x21, 0xa8, - 0xc7, 0x44, 0x91, 0xb4, 0x03, 0x58, 0x57, 0xc9, 0x88, 0x96, 0x90, 0xa4, 0x2e, 0x12, 0xf3, 0x0e, - 0x9e, 0xb5, 0x31, 0xe2, 0x1f, 0x9a, 0xf9, 0x62, 0xde, 0xcd, 0x1b, 0x24, 0xad, 0xba, 0x9a, 0x7f, - 0x88, 0xff, 0x89, 0x66, 0xa2, 0x80, 0x48, 0x70, 0xe6, 0xb6, 0x73, 0xe5, 0xe5, 0xcb, 0xa6, 0xce, - 0xa6, 0xa6, 0xb9, 0x09, 0x1b, 0xdf, 0x41, 0x4b, 0xe2, 0x88, 0xdb, 0x31, 0x06, 0xa4, 0x7e, 0xd9, - 0x75, 0x7e, 0x17, 0xcd, 0x62, 0x2d, 0x59, 0xc3, 0xff, 0x41, 0x73, 0x5d, 0x50, 0x84, 0x12, 0x45, - 0x1c, 0x64, 0x92, 0x56, 0xba, 0xd8, 0xfd, 0x67, 0x96, 0xe9, 0x66, 0x36, 0xb8, 0x85, 0x16, 0xf4, - 0x69, 0x0c, 0xbe, 0xb6, 0x0f, 0x2f, 0x76, 0xd1, 0x48, 0x8e, 0x8e, 0x7d, 0x4d, 0x86, 0x9e, 0x2f, - 0xa4, 0xdd, 0xd8, 0xb7, 0x76, 0x07, 0x2d, 0xc6, 0xd0, 0x15, 0x7d, 0x12, 0x7a, 0x3a, 0x07, 0xce, - 0xe2, 0x95, 0x5d, 0x65, 0xda, 0x74, 0x94, 0x05, 0x6b, 0xa5, 0xd7, 0xf1, 0x63, 0xb4, 0x14, 0x12, - 0xa9, 0x74, 0x66, 0x13, 0x2f, 0x4b, 0x1f, 0xeb, 0x45, 0x9b, 0xd5, 0xfc, 0x43, 0xe3, 0xe5, 0x3e, - 0x5a, 0x0b, 0x58, 0x27, 0x00, 0xa9, 0x3c, 0xa9, 0xcb, 0xb4, 0x2f, 0x7d, 0x5d, 0xaa, 0xcb, 0xa6, - 0x10, 0xf3, 0x16, 0x6a, 0x01, 0x57, 0xcf, 0xa4, 0xdf, 0xa0, 0xb8, 0x8a, 0x0a, 0x29, 0x5d, 0x57, - 0x02, 0x4d, 0xf9, 0x2b, 0x86, 0xbf, 0x6a, 0xb1, 0x9a, 0x86, 0x12, 0x83, 0x7d, 0xb4, 0x1e, 0x91, - 0x9e, 0x04, 0x0f, 0x8e, 0x23, 0x16, 0x9b, 0xa4, 0x24, 0xd1, 0xe6, 0x3f, 0x32, 0xda, 0x35, 0x63, - 0xbe, 0x9b, 0x59, 0x6b, 0xbc, 0xf4, 0x02, 0x6d, 0x8c, 0xff, 0x4f, 0xf0, 0x09, 0xff, 0x7a, 0x36, - 0xd0, 0xac, 0xbd, 0x75, 0x93, 0x06, 0xb7, 0x5f, 0xa5, 0x0e, 0xba, 0x75, 0xd9, 0x00, 0x7a, 0x75, - 0x3b, 0xb8, 0x87, 0x66, 0x49, 0x57, 0xf4, 0x78, 0xe2, 0x78, 0xbe, 0xbe, 0x3c, 0xd4, 0x68, 0x2c, - 0x5a, 0xfa, 0x21, 0x87, 0x0a, 0xe3, 0x66, 0xce, 0xab, 0x15, 0x1e, 0xa1, 0x79, 0x0a, 0x91, 0x90, - 0x4c, 0x89, 0xd8, 0x8a, 0x38, 0x6f, 0x5f, 0xdd, 0x2f, 0x58, 0x11, 0x5b, 0xfb, 0x2d, 0x15, 0x33, - 0xde, 0x71, 0xcf, 0xa8, 0xb8, 0x80, 0x66, 0x28, 0x70, 0xd1, 0xb5, 0x9d, 0x28, 0xf9, 0xc0, 0x3b, - 0x68, 0xd6, 0x8e, 0xc6, 0xd3, 0x9f, 0xde, 0x35, 0xad, 0x69, 0xbd, 0xf1, 0xfa, 0x7d, 0x31, 0xf7, - 0xe6, 0x7d, 0x31, 0xf7, 0xcb, 0xfb, 0x62, 0xee, 0xf4, 0x43, 0x71, 0xe2, 0xcd, 0x87, 0xe2, 0xc4, - 0x8f, 0x1f, 0x8a, 0x13, 0x2f, 0xaa, 0x1d, 0xa6, 0x82, 0x5e, 0xbb, 0xe2, 0x8b, 0x6e, 0x95, 0x84, - 0x21, 0xe3, 0x6d, 0xa6, 0x64, 0xd5, 0xfc, 0x9d, 0x3d, 0xae, 0x0e, 0xfe, 0xab, 0x55, 0x27, 0x11, - 0xc8, 0xf6, 0xac, 0xa9, 0x85, 0x7f, 0xfc, 0x16, 0x00, 0x00, 0xff, 0xff, 0x80, 0x94, 0xb2, 0xe3, - 0xaa, 0x0f, 0x00, 0x00, + // 1532 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xc4, 0x57, 0xcb, 0x6e, 0x1b, 0x37, + 0x17, 0xb6, 0x7c, 0x8b, 0x4d, 0xdf, 0x64, 0x5a, 0x76, 0xc6, 0x4e, 0x22, 0xfb, 0x57, 0xfe, 0x04, + 0xfa, 0x2f, 0x91, 0x90, 0x14, 0x4d, 0xd1, 0x2e, 0x0a, 0x48, 0x8e, 0xdd, 0x08, 0x45, 0x53, 0x61, + 0xe4, 0x24, 0x45, 0xb2, 0x98, 0x52, 0x33, 0xc7, 0x1a, 0xd6, 0x12, 0x39, 0x18, 0x52, 0xb2, 0xdd, + 0x45, 0xd7, 0x5d, 0xfa, 0x4d, 0xba, 0xc9, 0x43, 0x04, 0x5d, 0x05, 0x59, 0x15, 0x05, 0x9a, 0x16, + 0xc9, 0x1b, 0xf4, 0x09, 0x0a, 0x5e, 0x66, 0x6c, 0x5d, 0x6c, 0x27, 0x40, 0x81, 0xee, 0x34, 0xfc, + 0xbe, 0x73, 0xbe, 0xc3, 0xc3, 0xc3, 0xc3, 0x23, 0x94, 0xef, 0x11, 0x22, 0xca, 0x51, 0xcc, 0x7b, + 0x34, 0x80, 0xb8, 0xdc, 0xbb, 0x5b, 0x6e, 0x01, 0x03, 0x41, 0x45, 0x29, 0x8a, 0xb9, 0xe4, 0x38, + 0xab, 0xf0, 0x52, 0x82, 0x97, 0x7a, 0x77, 0x37, 0xd6, 0x7d, 0x2e, 0x3a, 0x5c, 0x78, 0x1a, 0x2f, + 0x9b, 0x0f, 0x43, 0xde, 0xc8, 0xb5, 0x78, 0x8b, 0x9b, 0x75, 0xf5, 0xcb, 0xae, 0x6e, 0xb6, 0x38, + 0x6f, 0xb5, 0xa1, 0xac, 0xbf, 0x9a, 0xdd, 0xfd, 0xb2, 0xa4, 0x1d, 0x10, 0x92, 0x74, 0x22, 0x4b, + 0xb8, 0xa1, 0x63, 0xe8, 0xdd, 0x2d, 0x8b, 0x90, 0xc4, 0x10, 0x78, 0x3e, 0x67, 0xa2, 0xdb, 0x81, + 0xd8, 0xc2, 0x38, 0x81, 0x0f, 0x69, 0x0c, 0x89, 0xcf, 0xa1, 0xb0, 0xd3, 0x10, 0x35, 0xa1, 0xf0, + 0xe3, 0x02, 0x9a, 0xff, 0xc2, 0xec, 0xa4, 0x21, 0x89, 0x04, 0x5c, 0x44, 0xd9, 0x1e, 0x69, 0x0b, + 0x90, 0x5e, 0x37, 0x0a, 0x88, 0x04, 0x8f, 0x06, 0x4e, 0x66, 0x2b, 0x53, 0x9c, 0x74, 0x17, 0xcd, + 0xfa, 0x63, 0xbd, 0x5c, 0x0b, 0x70, 0x88, 0x96, 0x92, 0x08, 0x3c, 0xa1, 0x6c, 0x85, 0x33, 0xbe, + 0x35, 0x51, 0x9c, 0xbb, 0xb7, 0x59, 0x1a, 0x4c, 0x46, 0x69, 0xdb, 0x12, 0xb5, 0x46, 0x35, 0xff, + 0xf2, 0xcd, 0xe6, 0xd8, 0x9f, 0x6f, 0x36, 0xd7, 0x8e, 0x49, 0xa7, 0xfd, 0x59, 0x61, 0xc0, 0x4b, + 0xc1, 0x5d, 0xf4, 0xcf, 0xd2, 0x05, 0xfe, 0x0e, 0x6d, 0x0c, 0xc6, 0xe4, 0x49, 0xee, 0x85, 0x40, + 0x5b, 0xa1, 0x74, 0x26, 0xb4, 0x68, 0x71, 0x58, 0xf4, 0x49, 0x5f, 0xbc, 0x7b, 0xfc, 0xa1, 0xe6, + 0x57, 0x27, 0x95, 0xba, 0xbb, 0xd6, 0x1b, 0x89, 0xe2, 0xfb, 0x68, 0x3a, 0x22, 0x31, 0xe9, 0x08, + 0x67, 0x72, 0x2b, 0x53, 0x9c, 0xbb, 0xe7, 0x0c, 0xfb, 0xad, 0x6b, 0xdc, 0xfa, 0xb1, 0x6c, 0xdc, + 0xd1, 0x31, 0xd2, 0x80, 0x48, 0x1e, 0xa7, 0x27, 0xe3, 0x45, 0xdd, 0xe6, 0x01, 0x1c, 0x0b, 0x67, + 0x4a, 0xc7, 0xf8, 0x9f, 0x91, 0x31, 0x1a, 0x9b, 0x24, 0x43, 0xf5, 0x6e, 0xf3, 0x4b, 0x38, 0xb6, + 0xce, 0x9d, 0xde, 0x08, 0x58, 0x39, 0xc4, 0x0c, 0x5d, 0x4b, 0x31, 0xe1, 0x35, 0x8f, 0x4f, 0x25, + 0x49, 0x10, 0xc4, 0xce, 0xf4, 0xa5, 0x7a, 0xd5, 0xe3, 0xc4, 0x65, 0x25, 0x08, 0xe2, 0x21, 0x3d, + 0xd1, 0x8f, 0x63, 0x1f, 0x5d, 0xed, 0x53, 0x10, 0xea, 0x00, 0xa2, 0xb8, 0xcb, 0xc0, 0xb9, 0xa2, + 0xb5, 0x6e, 0x9f, 0x7f, 0xe8, 0xca, 0x81, 0xd8, 0xe3, 0x75, 0xc5, 0xb6, 0x42, 0x39, 0x7f, 0x04, + 0x86, 0x7f, 0x40, 0xff, 0x4a, 0x45, 0xf6, 0x01, 0x84, 0x17, 0x41, 0xec, 0x35, 0xdb, 0xdc, 0x3f, + 0xf0, 0x78, 0x0f, 0xe2, 0x98, 0x06, 0x20, 0x9c, 0x19, 0x2d, 0x57, 0x3a, 0x5f, 0x6e, 0x17, 0x40, + 0xd4, 0x21, 0xae, 0x2a, 0xbb, 0xaf, 0xad, 0x99, 0x95, 0xbd, 0xe1, 0x5f, 0xc0, 0x11, 0x18, 0x90, + 0x73, 0x56, 0xdf, 0x8b, 0x38, 0x6f, 0x7b, 0xfa, 0xb2, 0x09, 0x67, 0xf6, 0xb2, 0x5d, 0xee, 0x02, + 0xd4, 0x39, 0x6f, 0x37, 0x14, 0xdd, 0xca, 0xad, 0xfa, 0x23, 0x30, 0x81, 0xf7, 0x91, 0x13, 0x01, + 0x0b, 0x28, 0x6b, 0x79, 0x01, 0x3f, 0x64, 0xea, 0x9a, 0x7b, 0xa2, 0x4d, 0x44, 0x08, 0xc2, 0x41, + 0xe7, 0xc9, 0xd4, 0x8d, 0xc5, 0x03, 0x6b, 0xd0, 0x50, 0xfc, 0xa4, 0x94, 0xa3, 0x11, 0x18, 0x08, + 0xdc, 0x44, 0x4e, 0x14, 0x43, 0x8f, 0xf2, 0xae, 0x38, 0x15, 0xb2, 0xc5, 0x3d, 0xa7, 0x8b, 0x7b, + 0xc4, 0xa5, 0xa9, 0x5b, 0x8b, 0xc4, 0x99, 0x29, 0x76, 0x77, 0x2d, 0x1a, 0xb9, 0x8e, 0xbf, 0x41, + 0x2b, 0x10, 0x71, 0x3f, 0x34, 0x69, 0xf2, 0x62, 0xf0, 0x79, 0x1c, 0x08, 0x67, 0x5e, 0x6f, 0xa3, + 0x30, 0xec, 0x7e, 0x47, 0x91, 0x75, 0x1e, 0x5c, 0x4d, 0xb5, 0x5b, 0x58, 0x86, 0x81, 0x75, 0x81, + 0x9f, 0xa3, 0x55, 0xca, 0xf6, 0x63, 0xe2, 0x4b, 0xca, 0x99, 0x09, 0x1b, 0x24, 0xc4, 0xc2, 0x59, + 0xd0, 0xa1, 0x8f, 0x48, 0x51, 0x2d, 0xa5, 0xd7, 0x53, 0xb6, 0x9b, 0xa3, 0x23, 0x56, 0xf1, 0x73, + 0x94, 0x3b, 0xa4, 0x32, 0x0c, 0xa1, 0x1d, 0xe8, 0x93, 0x4e, 0xe2, 0x5e, 0xd4, 0x71, 0xdf, 0x1c, + 0xf6, 0xfd, 0xd4, 0xb2, 0x77, 0xa1, 0x3f, 0x70, 0x7c, 0x38, 0x08, 0x08, 0x1c, 0xa1, 0x75, 0xe2, + 0xfb, 0x10, 0x49, 0x08, 0x4e, 0xf3, 0x7e, 0x48, 0x59, 0xc0, 0x0f, 0x85, 0xb3, 0x74, 0x5e, 0xf9, + 0x56, 0xac, 0x49, 0x92, 0xe0, 0xa7, 0xda, 0xa0, 0x4f, 0xec, 0x2a, 0x19, 0xc9, 0x11, 0xf8, 0x5b, + 0xb4, 0x66, 0x4e, 0x21, 0x95, 0x03, 0x26, 0x63, 0x0a, 0xc2, 0xc9, 0x6a, 0xb9, 0x7f, 0x9f, 0x73, + 0x10, 0x89, 0x9f, 0x1d, 0x26, 0xe3, 0xa4, 0xe7, 0xe4, 0x60, 0x10, 0xa1, 0x20, 0x30, 0x41, 0x6b, + 0x03, 0x5b, 0xf1, 0xf6, 0xdb, 0x9c, 0xc7, 0xc2, 0x59, 0xd6, 0x0a, 0xb7, 0x86, 0x15, 0xfa, 0x83, + 0xdc, 0x55, 0xec, 0x44, 0x22, 0x18, 0x86, 0x44, 0xe1, 0x64, 0x1c, 0x5d, 0xbf, 0x28, 0x09, 0x78, + 0x13, 0xcd, 0xa5, 0xd7, 0x33, 0x7d, 0x95, 0x50, 0xb2, 0x54, 0x0b, 0xf0, 0xff, 0x11, 0x4e, 0x02, + 0xd0, 0xfd, 0xd0, 0xf4, 0xc2, 0xf1, 0xad, 0x4c, 0x71, 0xde, 0xcd, 0x26, 0x88, 0xba, 0xaf, 0xba, + 0xa5, 0xfd, 0x17, 0x2d, 0xdb, 0x9d, 0x00, 0x0b, 0x4e, 0x1f, 0x93, 0x4c, 0x71, 0xc2, 0x5d, 0x32, + 0xc0, 0x0e, 0x0b, 0xec, 0xab, 0x50, 0x42, 0x2b, 0x96, 0x2b, 0x24, 0x89, 0x65, 0xc2, 0x9e, 0xd4, + 0x6c, 0xeb, 0xa6, 0xa1, 0x10, 0xcb, 0xdf, 0x41, 0x73, 0x69, 0x09, 0x10, 0xe9, 0x4c, 0xe9, 0x92, + 0xdd, 0x28, 0x99, 0x17, 0xbe, 0x94, 0xbc, 0xf0, 0xa5, 0xbd, 0xe4, 0x85, 0xaf, 0xce, 0xa8, 0xc4, + 0x9c, 0xfc, 0xbe, 0x99, 0x71, 0x51, 0x62, 0x58, 0x91, 0x85, 0x93, 0x0c, 0x5a, 0x19, 0x91, 0xc6, + 0x7f, 0x30, 0x13, 0x05, 0x1f, 0xe1, 0xe1, 0xd2, 0xf9, 0x9b, 0x03, 0x2a, 0xfc, 0x94, 0x41, 0xd9, + 0xc1, 0x4e, 0x71, 0xb9, 0xc6, 0x27, 0xc8, 0x09, 0xa8, 0x90, 0x31, 0x6d, 0x76, 0x4d, 0xde, 0xbd, + 0x2e, 0xa3, 0x47, 0x1e, 0x23, 0x8c, 0x6b, 0xa5, 0x09, 0x77, 0xf5, 0x0c, 0x5e, 0x91, 0x8f, 0x19, + 0x3d, 0x7a, 0x44, 0x18, 0xc7, 0x15, 0x34, 0xa5, 0xdb, 0x97, 0xde, 0xf3, 0x6c, 0xf5, 0x7f, 0xea, + 0x2c, 0x7e, 0x7d, 0xb3, 0xb9, 0x6a, 0x86, 0x36, 0x11, 0x1c, 0x94, 0x28, 0x2f, 0x77, 0x88, 0x0c, + 0x4b, 0x35, 0x26, 0x5f, 0xbf, 0xb8, 0x83, 0xec, 0x34, 0x57, 0x63, 0xd2, 0x35, 0x96, 0x85, 0xdf, + 0xae, 0xa0, 0x85, 0xbe, 0x21, 0xe7, 0xf2, 0x70, 0xd7, 0xd1, 0x8c, 0x1f, 0x12, 0xca, 0x14, 0xaa, + 0xc2, 0x9b, 0x75, 0xaf, 0xe8, 0xef, 0x5a, 0x80, 0xaf, 0xa1, 0x59, 0xbf, 0x4d, 0x81, 0x49, 0x85, + 0xe9, 0xa0, 0xdc, 0x19, 0xb3, 0x50, 0x0b, 0xf0, 0x2d, 0xb4, 0x48, 0x19, 0x95, 0x94, 0xb4, 0xcf, + 0x96, 0xe1, 0xa4, 0xbb, 0x60, 0x57, 0x6d, 0x09, 0x3e, 0x42, 0xd9, 0x54, 0xdf, 0xce, 0xaa, 0xb6, + 0x0e, 0x6f, 0x98, 0xbb, 0x7a, 0xe6, 0xed, 0x3a, 0x3b, 0x01, 0xda, 0x3b, 0x9a, 0xce, 0x76, 0x16, + 0x53, 0x1d, 0x20, 0x79, 0xb5, 0xec, 0x30, 0xe6, 0x87, 0x84, 0xb5, 0x40, 0xd8, 0x61, 0xe3, 0x56, + 0xea, 0x35, 0x9d, 0x31, 0x1a, 0x20, 0xb7, 0x35, 0xa7, 0x4e, 0xfc, 0x03, 0x90, 0x0f, 0x88, 0x24, + 0x49, 0x07, 0xb0, 0xae, 0xcc, 0x88, 0x66, 0x48, 0x42, 0x15, 0x89, 0x7e, 0x07, 0x4f, 0xdb, 0x18, + 0xf1, 0x0f, 0xf4, 0x7c, 0x31, 0xeb, 0x66, 0x35, 0x92, 0x54, 0x5d, 0xc5, 0x3f, 0xc0, 0x1f, 0xa3, + 0xa9, 0x28, 0x24, 0x02, 0x9c, 0x99, 0xad, 0x4c, 0x71, 0xf1, 0xa2, 0xa9, 0xb3, 0xae, 0x68, 0xae, + 0x61, 0xe3, 0x9b, 0x68, 0x81, 0x1f, 0x32, 0x3b, 0xc6, 0x80, 0x50, 0x2f, 0xbb, 0xca, 0xef, 0xbc, + 0x5e, 0xac, 0x98, 0x35, 0xfc, 0x39, 0x9a, 0xe9, 0x80, 0x24, 0x01, 0x91, 0xc4, 0x41, 0x3a, 0x69, + 0x85, 0xf3, 0xdd, 0x7f, 0x65, 0x99, 0x6e, 0x6a, 0x83, 0x1b, 0x68, 0x4e, 0x9d, 0x46, 0xff, 0x6b, + 0x7b, 0xef, 0x7c, 0x17, 0x35, 0x73, 0x74, 0xf4, 0x7b, 0x32, 0xf0, 0x7c, 0x21, 0xe5, 0xc6, 0xbe, + 0xb5, 0xdb, 0x68, 0x3e, 0x86, 0x0e, 0xef, 0x91, 0xb6, 0xa7, 0x72, 0xe0, 0xcc, 0x5f, 0xda, 0x55, + 0x26, 0x75, 0x47, 0x99, 0xb3, 0x56, 0x6a, 0x1d, 0x3f, 0x40, 0x0b, 0x6d, 0x22, 0xa4, 0xca, 0xac, + 0xf1, 0xb2, 0xf0, 0xbe, 0x5e, 0x94, 0x59, 0xc5, 0x3f, 0xd0, 0x5e, 0xee, 0xa0, 0x95, 0x90, 0xb6, + 0x42, 0x10, 0xd2, 0x13, 0xaa, 0x4c, 0x7b, 0xc2, 0x57, 0xa5, 0xba, 0xa8, 0x0b, 0x31, 0x6b, 0xa1, + 0x06, 0x30, 0xf9, 0x44, 0xf8, 0xb5, 0x00, 0x97, 0x51, 0x2e, 0xa1, 0xab, 0x4a, 0x08, 0x12, 0xfe, + 0x92, 0xe6, 0x2f, 0x5b, 0xac, 0xa2, 0x20, 0x63, 0xb0, 0x87, 0x56, 0x23, 0xd2, 0x15, 0xe0, 0xc1, + 0x51, 0x44, 0x63, 0x9d, 0x14, 0x13, 0x6d, 0xf6, 0x3d, 0xa3, 0x5d, 0xd1, 0xe6, 0x3b, 0xa9, 0xb5, + 0x8e, 0xfa, 0x53, 0xb4, 0xae, 0xc6, 0x98, 0xd3, 0x59, 0xd9, 0x16, 0x72, 0x48, 0x44, 0xe8, 0x2c, + 0xeb, 0x5e, 0xa4, 0xe7, 0x9c, 0xe4, 0x54, 0x4c, 0x75, 0x3e, 0x24, 0x22, 0x2c, 0x3c, 0x43, 0x6b, + 0xa3, 0xff, 0x4e, 0x7c, 0xc0, 0x1f, 0xa6, 0x35, 0x34, 0x6d, 0x2f, 0xec, 0xb8, 0xc6, 0xed, 0x57, + 0xa1, 0x85, 0xae, 0x5f, 0x34, 0xbb, 0x5e, 0xde, 0x49, 0x6e, 0xa3, 0x69, 0xd2, 0xe1, 0x5d, 0x66, + 0x1c, 0xcf, 0x56, 0x17, 0x07, 0x7a, 0x94, 0x45, 0x0b, 0x3f, 0x67, 0x50, 0x6e, 0xd4, 0xb8, 0x7a, + 0xb9, 0xc2, 0x7d, 0x34, 0x1b, 0x40, 0xc4, 0x05, 0x95, 0x3c, 0xb6, 0x22, 0xce, 0xeb, 0x17, 0x77, + 0x72, 0x56, 0xc4, 0x5e, 0x9b, 0x86, 0x8c, 0x29, 0x6b, 0xb9, 0xa7, 0x54, 0x9c, 0x43, 0x53, 0x01, + 0x30, 0xde, 0xb1, 0x4d, 0xcc, 0x7c, 0xe0, 0x6d, 0x34, 0x6d, 0xa7, 0xea, 0xc9, 0x0f, 0x6f, 0xb8, + 0xd6, 0xb4, 0x5a, 0x7b, 0xf9, 0x36, 0x9f, 0x79, 0xf5, 0x36, 0x9f, 0xf9, 0xe3, 0x6d, 0x3e, 0x73, + 0xf2, 0x2e, 0x3f, 0xf6, 0xea, 0x5d, 0x7e, 0xec, 0x97, 0x77, 0xf9, 0xb1, 0x67, 0xe5, 0x16, 0x95, + 0x61, 0xb7, 0x59, 0xf2, 0x79, 0xa7, 0x4c, 0xda, 0x6d, 0xca, 0x9a, 0x54, 0x8a, 0xb2, 0xfe, 0x27, + 0x7c, 0x54, 0xee, 0xff, 0x43, 0x2c, 0x8f, 0x23, 0x10, 0xcd, 0x69, 0x5d, 0x46, 0x1f, 0xfd, 0x15, + 0x00, 0x00, 0xff, 0xff, 0x9c, 0xd4, 0xd6, 0x80, 0xe5, 0x0f, 0x00, 0x00, } func (m *GenesisState) Marshal() (dAtA []byte, err error) { @@ -1417,6 +1433,15 @@ func (m *ConsumerState) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if len(m.PrevConsumerValsetHash) > 0 { + i -= len(m.PrevConsumerValsetHash) + copy(dAtA[i:], m.PrevConsumerValsetHash) + i = encodeVarintGenesis(dAtA, i, uint64(len(m.PrevConsumerValsetHash))) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0x8a + } if m.PauseExpirationTime != nil { n5, err5 := github_com_cosmos_gogoproto_types.StdTimeMarshalTo(*m.PauseExpirationTime, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdTime(*m.PauseExpirationTime):]) if err5 != nil { @@ -1929,6 +1954,10 @@ func (m *ConsumerState) Size() (n int) { l = github_com_cosmos_gogoproto_types.SizeOfStdTime(*m.PauseExpirationTime) n += 2 + l + sovGenesis(uint64(l)) } + l = len(m.PrevConsumerValsetHash) + if l > 0 { + n += 2 + l + sovGenesis(uint64(l)) + } return n } @@ -3627,6 +3656,40 @@ func (m *ConsumerState) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex + case 17: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field PrevConsumerValsetHash", wireType) + } + var byteLen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + byteLen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if byteLen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + byteLen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.PrevConsumerValsetHash = append(m.PrevConsumerValsetHash[:0], dAtA[iNdEx:postIndex]...) + if m.PrevConsumerValsetHash == nil { + m.PrevConsumerValsetHash = []byte{} + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipGenesis(dAtA[iNdEx:]) diff --git a/x/vaas/provider/types/keys.go b/x/vaas/provider/types/keys.go index 80676368..29dc069f 100644 --- a/x/vaas/provider/types/keys.go +++ b/x/vaas/provider/types/keys.go @@ -98,6 +98,8 @@ const ( PauseExpirationTimeToConsumerIdsKeyName = "PauseExpirationTimeToConsumerIdsKey" DowntimeWindowFloorsKeyName = "DowntimeWindowFloorsKey" + + ConsumerIdToPrevValSetHashKeyName = "ConsumerIdToPrevValSetHashKey" ) // Collection key prefixes for use with cosmossdk.io/collections @@ -142,5 +144,6 @@ var ( ConsumerIdToPauseExpirationTimePrefix = collections.NewPrefix(37) PauseExpirationTimeToConsumerIdsPrefix = collections.NewPrefix(38) DowntimeWindowFloorsPrefix = collections.NewPrefix(39) + ConsumerIdToPrevValSetHashPrefix = collections.NewPrefix(40) ParametersPrefix = collections.NewPrefix(0xFF) )