diff --git a/modules/network/genesis.go b/modules/network/genesis.go index f581bc9e..4a53562d 100644 --- a/modules/network/genesis.go +++ b/modules/network/genesis.go @@ -21,7 +21,6 @@ func InitGenesis(ctx sdk.Context, k keeper.Keeper, genState types.GenesisState) // Load attesters: validate pubkey/address match, then insert and assign indices. attesters := make([]types.AttesterInfo, len(genState.AttesterInfos)) copy(attesters, genState.AttesterInfos) - seenConsensusAddresses := make(map[string]int, len(attesters)) for i := range attesters { info := attesters[i] @@ -45,11 +44,6 @@ func InitGenesis(ctx sdk.Context, k keeper.Keeper, genState types.GenesisState) // stored value matches what ConsAddress().String() produces elsewhere // in the module at runtime. derived := sdk.ConsAddress(pk.Address()).String() - if previous, ok := seenConsensusAddresses[derived]; ok { - return fmt.Errorf("attester %d: duplicate consensus address %s after normalization (already used by attester %d)", - i, derived, previous) - } - seenConsensusAddresses[derived] = i info.ConsensusAddress = derived attesters[i] = info } @@ -72,6 +66,9 @@ func InitGenesis(ctx sdk.Context, k keeper.Keeper, genState types.GenesisState) return fmt.Errorf("set validator index: %w", err) } } + if err := k.SetAttesterSetSnapshot(ctx, ctx.BlockHeight()); err != nil { + return fmt.Errorf("set attester set snapshot: %w", err) + } // Still load historical bitmaps if provided (upgrade/dump scenarios). for _, ab := range genState.AttestationBitmaps { @@ -102,18 +99,18 @@ func ExportGenesis(ctx sdk.Context, k keeper.Keeper) *types.GenesisState { genesis := types.DefaultGenesisState() genesis.Params = k.GetParams(ctx) - var attesters []types.AttesterInfo - if err := k.AttesterInfo.Walk(ctx, nil, func(_ string, info types.AttesterInfo) (bool, error) { - attesters = append(attesters, info) - return false, nil - }); err != nil { + activeEntries, err := k.CurrentAttesterSetEntries(ctx) + if err != nil { panic(err) } - sort.Slice(attesters, func(i, j int) bool { - pki, _ := attesters[i].GetPubKey() - pkj, _ := attesters[j].GetPubKey() - return bytes.Compare(pki.Address(), pkj.Address()) < 0 - }) + attesters := make([]types.AttesterInfo, 0, len(activeEntries)) + for _, entry := range activeEntries { + info, err := k.GetAttesterInfo(ctx, entry.ConsensusAddress) + if err != nil { + panic(err) + } + attesters = append(attesters, *info) + } genesis.AttesterInfos = attesters var attestationBitmaps []types.AttestationBitmap diff --git a/modules/network/keeper/genesis_test.go b/modules/network/keeper/genesis_test.go index bb84d4bf..d48cfc58 100644 --- a/modules/network/keeper/genesis_test.go +++ b/modules/network/keeper/genesis_test.go @@ -15,7 +15,6 @@ import ( "github.com/cosmos/cosmos-sdk/runtime" "github.com/cosmos/cosmos-sdk/testutil/integration" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/types/bech32" moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" "github.com/stretchr/testify/require" @@ -112,31 +111,52 @@ func TestInitGenesisRejectsPubkeyAddressMismatch(t *testing.T) { require.Contains(t, err.Error(), "pubkey address mismatch") } -func TestInitGenesisRejectsDuplicateConsensusAddressAfterNormalization(t *testing.T) { +func TestExportGenesisRoundtripsAttesters(t *testing.T) { k, ctx, _ := newKeeperForGenesis(t) pk := cmted25519.GenPrivKey().PubKey().(cmted25519.PubKey) - info1 := mustAnyPubKey(t, pk) - info1.Authority = sdk.AccAddress(bytes.Repeat([]byte{0x11}, 20)).String() - info1.ConsensusAddress = sdk.ConsAddress(pk.Address()).String() + info := mustAnyPubKey(t, pk) + info.ConsensusAddress = sdk.ConsAddress(pk.Address()).String() - info2 := mustAnyPubKey(t, pk) - info2.Authority = sdk.AccAddress(bytes.Repeat([]byte{0x22}, 20)).String() - altAddr, err := bech32.ConvertAndEncode("otherprefixvalcons", pk.Address()) - require.NoError(t, err) - info2.ConsensusAddress = altAddr + gs := types.GenesisState{ + Params: types.DefaultParams(), + AttesterInfos: []types.AttesterInfo{*info}, + } + require.NoError(t, network.InitGenesis(ctx, k, gs)) + + exported := network.ExportGenesis(ctx, k) + require.Len(t, exported.AttesterInfos, 1) + require.Equal(t, info.ConsensusAddress, exported.AttesterInfos[0].ConsensusAddress) + require.Equal(t, info.Authority, exported.AttesterInfos[0].Authority) +} + +func TestInitGenesisSnapshotsInitialAttesterSet(t *testing.T) { + k, ctx, _ := newKeeperForGenesis(t) + + pk := cmted25519.GenPrivKey().PubKey().(cmted25519.PubKey) + info := mustAnyPubKey(t, pk) + info.ConsensusAddress = sdk.ConsAddress(pk.Address()).String() gs := types.GenesisState{ Params: types.DefaultParams(), - AttesterInfos: []types.AttesterInfo{*info1, *info2}, + AttesterInfos: []types.AttesterInfo{*info}, } + require.NoError(t, network.InitGenesis(ctx, k, gs)) - err = network.InitGenesis(ctx, k, gs) - require.Error(t, err) - require.Contains(t, err.Error(), "duplicate consensus address") + msgServer := keeper.NewMsgServerImpl(k) + _, err := msgServer.LeaveAttesterSet(ctx, &types.MsgLeaveAttesterSet{ + Authority: info.Authority, + ConsensusAddress: info.ConsensusAddress, + }) + require.NoError(t, err) + + entries, err := k.GetAttesterSetForHeight(ctx, ctx.BlockHeight()) + require.NoError(t, err) + require.Len(t, entries, 1) + require.Equal(t, info.ConsensusAddress, entries[0].ConsensusAddress) } -func TestExportGenesisRoundtripsAttesters(t *testing.T) { +func TestExportGenesisExcludesRemovedAttesters(t *testing.T) { k, ctx, _ := newKeeperForGenesis(t) pk := cmted25519.GenPrivKey().PubKey().(cmted25519.PubKey) @@ -149,8 +169,13 @@ func TestExportGenesisRoundtripsAttesters(t *testing.T) { } require.NoError(t, network.InitGenesis(ctx, k, gs)) + msgServer := keeper.NewMsgServerImpl(k) + _, err := msgServer.LeaveAttesterSet(ctx, &types.MsgLeaveAttesterSet{ + Authority: info.Authority, + ConsensusAddress: info.ConsensusAddress, + }) + require.NoError(t, err) + exported := network.ExportGenesis(ctx, k) - require.Len(t, exported.AttesterInfos, 1) - require.Equal(t, info.ConsensusAddress, exported.AttesterInfos[0].ConsensusAddress) - require.Equal(t, info.Authority, exported.AttesterInfos[0].Authority) + require.Empty(t, exported.AttesterInfos) } diff --git a/modules/network/keeper/grpc_query.go b/modules/network/keeper/grpc_query.go index 763f3896..47aea16a 100644 --- a/modules/network/keeper/grpc_query.go +++ b/modules/network/keeper/grpc_query.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "sort" "cosmossdk.io/collections" sdk "github.com/cosmos/cosmos-sdk/types" @@ -45,6 +44,14 @@ func (q *queryServer) AttestationBitmap(c context.Context, req *types.QueryAttes ctx := sdk.UnwrapSDKContext(c) + stored, err := q.keeper.StoredAttestationInfo.Get(ctx, req.Height) + if err != nil && !errors.Is(err, collections.ErrNotFound) { + return nil, fmt.Errorf("get stored attestation info: %w", err) + } + if err == nil { + return &types.QueryAttestationBitmapResponse{Bitmap: &stored}, nil + } + bitmapBytes, err := q.keeper.GetAttestationBitmap(ctx, req.Height) if err != nil && !errors.Is(err, collections.ErrNotFound) { return nil, fmt.Errorf("get attestation bitmap: %w", err) @@ -164,6 +171,19 @@ func (q *queryServer) SoftConfirmationStatus(c context.Context, req *types.Query } ctx := sdk.UnwrapSDKContext(c) + stored, err := q.keeper.StoredAttestationInfo.Get(ctx, req.Height) + if err != nil && !errors.Is(err, collections.ErrNotFound) { + return nil, fmt.Errorf("get stored attestation info: %w", err) + } + if err == nil { + return &types.QuerySoftConfirmationStatusResponse{ + IsSoftConfirmed: stored.SoftConfirmed, + VotedPower: stored.VotedPower, + TotalPower: stored.TotalPower, + QuorumFraction: q.keeper.GetParams(ctx).QuorumFraction, + }, nil + } + isSoftConfirmed, err := q.keeper.IsSoftConfirmed(ctx, req.Height) if err != nil { return nil, err @@ -245,23 +265,10 @@ func (q *queryServer) AttesterSet(goCtx context.Context, req *types.QueryAtteste } ctx := sdk.UnwrapSDKContext(goCtx) - entries := []types.AttesterSetEntry{} - if err := q.keeper.ValidatorIndex.Walk(ctx, nil, func(addr string, idx uint16) (bool, error) { - info, err := q.keeper.GetAttesterInfo(ctx, addr) - if err != nil { - return false, err - } - entries = append(entries, types.AttesterSetEntry{ - Authority: info.Authority, - ConsensusAddress: addr, - Index: uint32(idx), - Pubkey: info.Pubkey, - }) - return false, nil - }); err != nil { + entries, err := q.keeper.GetAttesterSetForHeight(ctx, req.Height) + if err != nil { return nil, err } - sort.Slice(entries, func(i, j int) bool { return entries[i].Index < entries[j].Index }) return &types.QueryAttesterSetResponse{Entries: entries}, nil } diff --git a/modules/network/keeper/keeper.go b/modules/network/keeper/keeper.go index 6e51a874..69a51efd 100644 --- a/modules/network/keeper/keeper.go +++ b/modules/network/keeper/keeper.go @@ -1,6 +1,7 @@ package keeper import ( + "bytes" "errors" "fmt" "sort" @@ -11,6 +12,7 @@ import ( "cosmossdk.io/math" "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/gogoproto/proto" "github.com/evstack/ev-abci/modules/network/types" ) @@ -38,6 +40,7 @@ type Keeper struct { EpochBitmap collections.Map[uint64, []byte] AttesterSet collections.KeySet[string] AttesterInfo collections.Map[string, types.AttesterInfo] + AttesterSetSnapshot collections.Map[int64, []byte] Signatures collections.Map[collections.Pair[int64, string], []byte] StoredAttestationInfo collections.Map[int64, types.AttestationBitmap] LastAttestedHeight collections.Item[int64] @@ -71,6 +74,7 @@ func NewKeeper( EpochBitmap: collections.NewMap(sb, types.EpochBitmapPrefix, "epoch_bitmap", collections.Uint64Key, collections.BytesValue), AttesterSet: collections.NewKeySet(sb, types.AttesterSetPrefix, "attester_set", collections.StringKey), AttesterInfo: collections.NewMap(sb, types.AttesterInfoPrefix, "attester_info", collections.StringKey, codec.CollValue[types.AttesterInfo](cdc)), + AttesterSetSnapshot: collections.NewMap(sb, types.AttesterSetSnapshotPrefix, "attester_set_snapshot", collections.Int64Key, collections.BytesValue), Signatures: collections.NewMap(sb, types.SignaturePrefix, "signatures", collections.PairKeyCodec(collections.Int64Key, collections.StringKey), collections.BytesValue), StoredAttestationInfo: collections.NewMap(sb, types.StoredAttestationInfoPrefix, "stored_attestation_info", collections.Int64Key, codec.CollValue[types.AttestationBitmap](cdc)), // Initialize new collection LastAttestedHeight: collections.NewItem(sb, types.LastAttestedHeightKey, "last_attested_height", collections.Int64Value), @@ -133,6 +137,20 @@ func (k Keeper) SetValidatorIndex(ctx sdk.Context, addr string, index uint16, po return k.ValidatorPower.Set(ctx, index, power) } +// NextValidatorIndex returns the next unused bitmap index. +func (k Keeper) NextValidatorIndex(ctx sdk.Context) (uint16, error) { + var next uint16 + if err := k.ValidatorIndex.Walk(ctx, nil, func(_ string, index uint16) (bool, error) { + if index >= next { + next = index + 1 + } + return false, nil + }); err != nil { + return 0, err + } + return next, nil +} + // GetValidatorIndex retrieves the validator index func (k Keeper) GetValidatorIndex(ctx sdk.Context, addr string) (uint16, bool) { index, err := k.ValidatorIndex.Get(ctx, addr) @@ -210,6 +228,93 @@ func (k Keeper) GetAllAttesters(ctx sdk.Context) ([]string, error) { return attesters, nil } +// CurrentAttesterSetEntries returns the active attester set in CometBFT +// validator-set order, with contiguous bitmap indices for this set. +func (k Keeper) CurrentAttesterSetEntries(ctx sdk.Context) ([]types.AttesterSetEntry, error) { + attesters, err := k.GetAllAttesters(ctx) + if err != nil { + return nil, err + } + return k.attesterSetEntriesForAddresses(ctx, attesters) +} + +func (k Keeper) attesterSetEntriesForAddresses(ctx sdk.Context, addrs []string) ([]types.AttesterSetEntry, error) { + type sortableEntry struct { + info types.AttesterInfo + addr []byte + } + + entries := make([]sortableEntry, 0, len(addrs)) + for _, addr := range addrs { + info, err := k.GetAttesterInfo(ctx, addr) + if err != nil { + return nil, err + } + pk, err := info.GetPubKey() + if err != nil { + return nil, err + } + entries = append(entries, sortableEntry{info: *info, addr: pk.Address()}) + } + sort.Slice(entries, func(i, j int) bool { + return bytes.Compare(entries[i].addr, entries[j].addr) < 0 + }) + + out := make([]types.AttesterSetEntry, 0, len(entries)) + for idx, entry := range entries { + out = append(out, types.AttesterSetEntry{ + Authority: entry.info.Authority, + ConsensusAddress: entry.info.ConsensusAddress, + Index: uint32(idx), + Pubkey: entry.info.Pubkey, + }) + } + return out, nil +} + +func (k Keeper) SetAttesterSetSnapshot(ctx sdk.Context, height int64) error { + entries, err := k.CurrentAttesterSetEntries(ctx) + if err != nil { + return err + } + bz, err := proto.Marshal(&types.QueryAttesterSetResponse{Entries: entries}) + if err != nil { + return fmt.Errorf("marshal attester set snapshot: %w", err) + } + return k.AttesterSetSnapshot.Set(ctx, height, bz) +} + +func (k Keeper) GetAttesterSetForHeight(ctx sdk.Context, height int64) ([]types.AttesterSetEntry, error) { + if height <= 0 { + return k.CurrentAttesterSetEntries(ctx) + } + + var ( + found bool + snapshotHeight int64 + snapshotBz []byte + ) + if err := k.AttesterSetSnapshot.Walk(ctx, nil, func(h int64, bz []byte) (bool, error) { + if h <= height && (!found || h > snapshotHeight) { + found = true + snapshotHeight = h + snapshotBz = bz + } + return false, nil + }); err != nil { + return nil, err + } + if !found { + return k.CurrentAttesterSetEntries(ctx) + } + + var resp types.QueryAttesterSetResponse + if err := proto.Unmarshal(snapshotBz, &resp); err != nil { + return nil, fmt.Errorf("unmarshal attester set snapshot: %w", err) + } + return resp.Entries, nil +} + // GetCurrentEpoch returns the current epoch number func (k Keeper) GetCurrentEpoch(ctx sdk.Context) uint64 { params := k.GetParams(ctx) @@ -243,6 +348,16 @@ func (k Keeper) CalculateVotedPower(ctx sdk.Context, bitmap []byte) (uint64, err return votedPower, nil } +func (k Keeper) CalculateVotedPowerForAttesterSet(bitmap []byte, entries []types.AttesterSetEntry) uint64 { + var votedPower uint64 + for _, entry := range entries { + if k.bitmapHelper.IsSet(bitmap, int(entry.Index)) { + votedPower++ + } + } + return votedPower +} + // GetTotalPower returns the total attester power (all attesters have power 1) func (k Keeper) GetTotalPower(ctx sdk.Context) (uint64, error) { attesters, err := k.GetAllAttesters(ctx) @@ -266,6 +381,14 @@ func (k Keeper) CheckQuorum(ctx sdk.Context, votedPower, totalPower uint64) (boo // IsSoftConfirmed checks if a block at a given height is soft-confirmed // based on the attestation bitmap and quorum rules. func (k Keeper) IsSoftConfirmed(ctx sdk.Context, height int64) (bool, error) { + stored, err := k.StoredAttestationInfo.Get(ctx, height) + if err != nil && !errors.Is(err, collections.ErrNotFound) { + return false, fmt.Errorf("get stored attestation info: %w", err) + } + if err == nil { + return stored.SoftConfirmed, nil + } + bitmap, err := k.GetAttestationBitmap(ctx, height) if err != nil && !errors.Is(err, collections.ErrNotFound) { return false, fmt.Errorf("get attestation bitmap: %w", err) @@ -372,36 +495,28 @@ func (k Keeper) GetAllSignaturesForHeight(ctx sdk.Context, height int64) (map[st return signatures, nil // No attestations for this height } - type indexedAttester struct { - addr string - index uint16 - } - - var attesters []indexedAttester - if err := k.ValidatorIndex.Walk(ctx, nil, func(addr string, index uint16) (bool, error) { - attesters = append(attesters, indexedAttester{addr: addr, index: index}) - return false, nil - }); err != nil { - return nil, fmt.Errorf("walk validator index: %w", err) + attesters, err := k.GetAttesterSetForHeight(ctx, height) + if err != nil { + return nil, fmt.Errorf("get attester set for height %d: %w", height, err) } sort.Slice(attesters, func(i, j int) bool { - return attesters[i].index < attesters[j].index + return attesters[i].Index < attesters[j].Index }) for _, attester := range attesters { - if int(attester.index) >= len(bitmap)*8 { + if int(attester.Index) >= len(bitmap)*8 { continue } - if k.bitmapHelper.IsSet(bitmap, int(attester.index)) { - signature, err := k.GetSignature(ctx, height, attester.addr) + if k.bitmapHelper.IsSet(bitmap, int(attester.Index)) { + signature, err := k.GetSignature(ctx, height, attester.ConsensusAddress) if err != nil && !errors.Is(err, collections.ErrNotFound) { k.Logger(ctx).Error("failed to get signature for attester", - "height", height, "attester", attester.addr, "error", err) + "height", height, "attester", attester.ConsensusAddress, "error", err) continue } if signature != nil { - signatures[attester.addr] = signature + signatures[attester.ConsensusAddress] = signature } } } diff --git a/modules/network/keeper/msg_server.go b/modules/network/keeper/msg_server.go index efff6524..8a57851a 100644 --- a/modules/network/keeper/msg_server.go +++ b/modules/network/keeper/msg_server.go @@ -11,6 +11,8 @@ import ( "cosmossdk.io/math" cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" cmttypes "github.com/cometbft/cometbft/types" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" @@ -34,6 +36,15 @@ var _ types.MsgServer = msgServer{} func (k msgServer) Attest(goCtx context.Context, msg *types.MsgAttest) (*types.MsgAttestResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) + attesterSet, err := k.GetAttesterSetForHeight(ctx, msg.Height) + if err != nil { + return nil, sdkerr.Wrap(err, "get attester set") + } + index, found := attesterIndex(attesterSet, msg.ConsensusAddress) + if !found { + return nil, sdkerr.Wrapf(sdkerrors.ErrUnauthorized, "consensus address %s not in attester set", msg.ConsensusAddress) + } + if err := k.assertValidValidatorAuthority(ctx, msg.ConsensusAddress, msg.Authority); err != nil { return nil, err } @@ -43,11 +54,6 @@ func (k msgServer) Attest(goCtx context.Context, msg *types.MsgAttest) (*types.M return nil, err } - index, found := k.GetValidatorIndex(ctx, msg.ConsensusAddress) - if !found { - return nil, sdkerr.Wrapf(sdkerrors.ErrNotFound, "validator index not found for %s", msg.ConsensusAddress) - } - // Height bounds currentHeight := ctx.BlockHeight() maxFutureHeight := currentHeight + 1 @@ -68,18 +74,14 @@ func (k msgServer) Attest(goCtx context.Context, msg *types.MsgAttest) (*types.M return nil, sdkerr.Wrap(err, "get attestation bitmap") } if bitmap == nil { - attesters, err := k.GetAllAttesters(ctx) - if err != nil { - return nil, err - } - bitmap = k.bitmapHelper.NewBitmap(len(attesters)) + bitmap = k.bitmapHelper.NewBitmap(len(attesterSet)) } - if k.bitmapHelper.IsSet(bitmap, int(index)) { + if k.bitmapHelper.IsSet(bitmap, index) { return nil, sdkerr.Wrapf(sdkerrors.ErrInvalidRequest, "consensus address %s already attested for height %d", msg.ConsensusAddress, msg.Height) } - k.bitmapHelper.SetBit(bitmap, int(index)) + k.bitmapHelper.SetBit(bitmap, index) if err := k.SetAttestationBitmap(ctx, msg.Height, bitmap); err != nil { return nil, sdkerr.Wrap(err, "set attestation bitmap") } @@ -87,18 +89,21 @@ func (k msgServer) Attest(goCtx context.Context, msg *types.MsgAttest) (*types.M return nil, sdkerr.Wrap(err, "store signature") } - votedPower, err := k.CalculateVotedPower(ctx, bitmap) - if err != nil { - return nil, sdkerr.Wrap(err, "calculate voted power") - } - totalPower, err := k.GetTotalPower(ctx) - if err != nil { - return nil, sdkerr.Wrap(err, "get total power") - } + votedPower := k.CalculateVotedPowerForAttesterSet(bitmap, attesterSet) + totalPower := uint64(len(attesterSet)) quorumReached, err := k.CheckQuorum(ctx, votedPower, totalPower) if err != nil { return nil, sdkerr.Wrap(err, "check quorum") } + if err := k.StoredAttestationInfo.Set(ctx, msg.Height, types.AttestationBitmap{ + Height: msg.Height, + Bitmap: bitmap, + VotedPower: votedPower, + TotalPower: totalPower, + SoftConfirmed: quorumReached, + }); err != nil { + return nil, sdkerr.Wrap(err, "store attestation info") + } if quorumReached { if err := k.UpdateLastAttestedHeight(ctx, msg.Height); err != nil { return nil, sdkerr.Wrap(err, "update last attested height") @@ -110,13 +115,9 @@ func (k msgServer) Attest(goCtx context.Context, msg *types.MsgAttest) (*types.M epoch := k.GetCurrentEpoch(ctx) epochBitmap := k.GetEpochBitmap(ctx, epoch) if epochBitmap == nil { - attesters, err := k.GetAllAttesters(ctx) - if err != nil { - return nil, err - } - epochBitmap = k.bitmapHelper.NewBitmap(len(attesters)) + epochBitmap = k.bitmapHelper.NewBitmap(len(attesterSet)) } - k.bitmapHelper.SetBit(epochBitmap, int(index)) + k.bitmapHelper.SetBit(epochBitmap, index) if err := k.SetEpochBitmap(ctx, epoch, epochBitmap); err != nil { return nil, sdkerr.Wrap(err, "set epoch bitmap") } @@ -134,14 +135,84 @@ func (k msgServer) Attest(goCtx context.Context, msg *types.MsgAttest) (*types.M // JoinAttesterSet handles MsgJoinAttesterSet func (k msgServer) JoinAttesterSet(goCtx context.Context, msg *types.MsgJoinAttesterSet) (*types.MsgJoinAttesterSetResponse, error) { - return nil, sdkerr.Wrap(sdkerrors.ErrInvalidRequest, - "attester set changes disabled; the set is fixed at genesis") + ctx := sdk.UnwrapSDKContext(goCtx) + + pubKey, err := unpackMsgPubKey(msg.Pubkey) + if err != nil { + return nil, sdkerr.Wrap(sdkerrors.ErrInvalidRequest, err.Error()) + } + derivedConsensusAddress := sdk.ConsAddress(pubKey.Address()).String() + if msg.ConsensusAddress != derivedConsensusAddress { + return nil, sdkerr.Wrapf(sdkerrors.ErrInvalidRequest, + "consensus address %s does not match pubkey address %s", + msg.ConsensusAddress, derivedConsensusAddress) + } + if inSet, err := k.IsInAttesterSet(ctx, msg.ConsensusAddress); err != nil { + return nil, sdkerr.Wrap(err, "check attester set") + } else if inSet { + return nil, sdkerr.Wrapf(sdkerrors.ErrInvalidRequest, "validator already in attester set: %s", msg.ConsensusAddress) + } + + info, err := types.NewAttesterInfo(msg.Authority, pubKey, ctx.BlockHeight()) + if err != nil { + return nil, sdkerr.Wrap(sdkerrors.ErrInvalidRequest, err.Error()) + } + if err := k.SetAttesterInfo(ctx, msg.ConsensusAddress, info); err != nil { + return nil, sdkerr.Wrap(err, "set attester info") + } + nextIndex, err := k.NextValidatorIndex(ctx) + if err != nil { + return nil, sdkerr.Wrap(err, "get next validator index") + } + if err := k.SetValidatorIndex(ctx, msg.ConsensusAddress, nextIndex, 1); err != nil { + return nil, sdkerr.Wrap(err, "set validator index") + } + if err := k.SetAttesterSetMember(ctx, msg.ConsensusAddress); err != nil { + return nil, sdkerr.Wrap(err, "set attester set member") + } + if err := k.SetAttesterSetSnapshot(ctx, ctx.BlockHeight()+1); err != nil { + return nil, sdkerr.Wrap(err, "set attester set snapshot") + } + return &types.MsgJoinAttesterSetResponse{}, nil } // LeaveAttesterSet handles MsgLeaveAttesterSet func (k msgServer) LeaveAttesterSet(goCtx context.Context, msg *types.MsgLeaveAttesterSet) (*types.MsgLeaveAttesterSetResponse, error) { - return nil, sdkerr.Wrap(sdkerrors.ErrInvalidRequest, - "attester set changes disabled; the set is fixed at genesis") + ctx := sdk.UnwrapSDKContext(goCtx) + + if err := k.assertValidValidatorAuthority(ctx, msg.ConsensusAddress, msg.Authority); err != nil { + return nil, err + } + if err := k.RemoveAttesterSetMember(ctx, msg.ConsensusAddress); err != nil { + return nil, sdkerr.Wrap(err, "remove attester set member") + } + if err := k.SetAttesterSetSnapshot(ctx, ctx.BlockHeight()+1); err != nil { + return nil, sdkerr.Wrap(err, "set attester set snapshot") + } + return &types.MsgLeaveAttesterSetResponse{}, nil +} + +func unpackMsgPubKey(pubkey *codectypes.Any) (cryptotypes.PubKey, error) { + if pubkey == nil { + return nil, fmt.Errorf("pubkey not set") + } + if pk, ok := pubkey.GetCachedValue().(cryptotypes.PubKey); ok { + return pk, nil + } + var pk cryptotypes.PubKey + if err := types.ModuleCdc.InterfaceRegistry().UnpackAny(pubkey, &pk); err != nil { + return nil, fmt.Errorf("unpack pubkey: %w", err) + } + return pk, nil +} + +func attesterIndex(entries []types.AttesterSetEntry, consensusAddress string) (int, bool) { + for _, entry := range entries { + if entry.ConsensusAddress == consensusAddress { + return int(entry.Index), true + } + } + return 0, false } func (k msgServer) assertValidValidatorAuthority(ctx sdk.Context, consensusAddress, authority string) error { diff --git a/modules/network/keeper/msg_server_test.go b/modules/network/keeper/msg_server_test.go index 178c0517..4e7dcb03 100644 --- a/modules/network/keeper/msg_server_test.go +++ b/modules/network/keeper/msg_server_test.go @@ -46,30 +46,233 @@ func (nilBlockIDProvider) GetBlockID(_ context.Context, _ uint64) (*cmttypes.Blo return nil, nil } -func TestJoinAttesterSetDisabled(t *testing.T) { +func TestJoinAttesterSetAddsAttester(t *testing.T) { sk := NewMockStakingKeeper() - server, _, ctx := newTestServer(t, &sk) + server, keeper, ctx := newTestServer(t, &sk) + require.NoError(t, keeper.SetParams(ctx, types.DefaultParams())) + + priv := cmted25519.GenPrivKey() + pub := priv.PubKey().(cmted25519.PubKey) + sdkPk, err := cryptocodec.FromCmtPubKeyInterface(pub) + require.NoError(t, err) + + authority := sdk.AccAddress(pub.Address()).String() + consAddr := sdk.ConsAddress(pub.Address()).String() + msg, err := types.NewMsgJoinAttesterSet(authority, consAddr, sdkPk) + require.NoError(t, err) - msg := &types.MsgJoinAttesterSet{ - Authority: sdk.AccAddress([]byte("any-authority-20b")).String(), - ConsensusAddress: sdk.ConsAddress([]byte("any-cons-addr-20-b")).String(), - } rsp, err := server.JoinAttesterSet(ctx, msg) - require.ErrorIs(t, err, sdkerrors.ErrInvalidRequest) - require.Contains(t, err.Error(), "attester set changes disabled") - require.Nil(t, rsp) + require.NoError(t, err) + require.NotNil(t, rsp) + + stored, err := keeper.GetAttesterInfo(ctx, consAddr) + require.NoError(t, err) + require.Equal(t, authority, stored.Authority) + require.Equal(t, consAddr, stored.ConsensusAddress) + idx, found := keeper.GetValidatorIndex(ctx, consAddr) + require.True(t, found) + require.Equal(t, uint16(0), idx) } -func TestLeaveAttesterSetDisabled(t *testing.T) { +func TestLeaveAttesterSetRemovesAttester(t *testing.T) { sk := NewMockStakingKeeper() - server, _, ctx := newTestServer(t, &sk) + server, keeper, ctx := newTestServer(t, &sk) + require.NoError(t, keeper.SetParams(ctx, types.DefaultParams())) + + priv := cmted25519.GenPrivKey() + pub := priv.PubKey().(cmted25519.PubKey) + authority := sdk.AccAddress(pub.Address()).String() + consAddr := sdk.ConsAddress(pub.Address()).String() + require.NoError(t, registerTestAttester(ctx, &keeper, authority, consAddr, pub, 0)) msg := &types.MsgLeaveAttesterSet{ - Authority: sdk.AccAddress([]byte("any-authority-20b")).String(), - ConsensusAddress: sdk.ConsAddress([]byte("any-cons-addr-20-b")).String(), + Authority: authority, + ConsensusAddress: consAddr, } rsp, err := server.LeaveAttesterSet(ctx, msg) - require.ErrorIs(t, err, sdkerrors.ErrInvalidRequest) + require.NoError(t, err) + require.NotNil(t, rsp) + + inSet, err := keeper.IsInAttesterSet(ctx, consAddr) + require.NoError(t, err) + require.False(t, inSet) +} + +func TestAttesterSetQueryExcludesRemovedAttesters(t *testing.T) { + sk := NewMockStakingKeeper() + server, keeper, ctx := newTestServer(t, &sk) + require.NoError(t, keeper.SetParams(ctx, types.DefaultParams())) + + priv := cmted25519.GenPrivKey() + pub := priv.PubKey().(cmted25519.PubKey) + authority := sdk.AccAddress(pub.Address()).String() + consAddr := sdk.ConsAddress(pub.Address()).String() + require.NoError(t, registerTestAttester(ctx, &keeper, authority, consAddr, pub, 0)) + + _, err := server.LeaveAttesterSet(ctx, &types.MsgLeaveAttesterSet{ + Authority: authority, + ConsensusAddress: consAddr, + }) + require.NoError(t, err) + + queryServer := NewQueryServer(keeper) + resp, err := queryServer.AttesterSet(ctx, &types.QueryAttesterSetRequest{}) + require.NoError(t, err) + require.Empty(t, resp.Entries) +} + +func TestAttesterSetQueryReturnsSnapshotForRequestedHeight(t *testing.T) { + const height int64 = 10 + + sk := NewMockStakingKeeper() + server, keeper, ctx := newTestServer(t, &sk) + require.NoError(t, keeper.SetParams(ctx, types.DefaultParams())) + + priv := cmted25519.GenPrivKey() + pub := priv.PubKey().(cmted25519.PubKey) + authority := sdk.AccAddress(pub.Address()).String() + consAddr := sdk.ConsAddress(pub.Address()).String() + require.NoError(t, registerTestAttester(ctx, &keeper, authority, consAddr, pub, 0)) + require.NoError(t, keeper.SetAttesterSetSnapshot(ctx, height)) + + _, err := server.LeaveAttesterSet(ctx, &types.MsgLeaveAttesterSet{ + Authority: authority, + ConsensusAddress: consAddr, + }) + require.NoError(t, err) + + queryServer := NewQueryServer(keeper) + current, err := queryServer.AttesterSet(ctx, &types.QueryAttesterSetRequest{}) + require.NoError(t, err) + require.Empty(t, current.Entries) + + historical, err := queryServer.AttesterSet(ctx, &types.QueryAttesterSetRequest{Height: height}) + require.NoError(t, err) + require.Len(t, historical.Entries, 1) + require.Equal(t, consAddr, historical.Entries[0].ConsensusAddress) +} + +func TestLeaveAttesterSetDoesNotChangeHistoricalQuorum(t *testing.T) { + const height int64 = 10 + + chainID := "test-chain" + blockHash := bytes.Repeat([]byte{0x01}, 32) + privs := []cmted25519.PrivKey{ + cmted25519.GenPrivKey(), + cmted25519.GenPrivKey(), + cmted25519.GenPrivKey(), + } + + sk := NewMockStakingKeeper() + server, keeper, ctx := newTestServer(t, &sk) + require.NoError(t, keeper.SetParams(ctx, types.DefaultParams())) + for idx, priv := range privs { + pub := priv.PubKey().(cmted25519.PubKey) + authority := sdk.AccAddress(pub.Address()).String() + consAddr := sdk.ConsAddress(pub.Address()).String() + require.NoError(t, registerTestAttester(ctx, &keeper, authority, consAddr, pub, uint16(idx))) + } + + for _, priv := range privs[:2] { + pub := priv.PubKey().(cmted25519.PubKey) + _, err := server.Attest(ctx, &types.MsgAttest{ + Authority: sdk.AccAddress(pub.Address()).String(), + ConsensusAddress: sdk.ConsAddress(pub.Address()).String(), + Height: height, + Vote: signTestVote(t, chainID, height, priv, blockHash), + }) + require.NoError(t, err) + } + softConfirmed, err := keeper.IsSoftConfirmed(ctx, height) + require.NoError(t, err) + require.False(t, softConfirmed) + + removedPub := privs[2].PubKey().(cmted25519.PubKey) + _, err = server.LeaveAttesterSet(ctx, &types.MsgLeaveAttesterSet{ + Authority: sdk.AccAddress(removedPub.Address()).String(), + ConsensusAddress: sdk.ConsAddress(removedPub.Address()).String(), + }) + require.NoError(t, err) + + softConfirmed, err = keeper.IsSoftConfirmed(ctx, height) + require.NoError(t, err) + require.False(t, softConfirmed) + + queryServer := NewQueryServer(keeper) + status, err := queryServer.SoftConfirmationStatus(ctx, &types.QuerySoftConfirmationStatusRequest{Height: height}) + require.NoError(t, err) + require.Equal(t, uint64(2), status.VotedPower) + require.Equal(t, uint64(3), status.TotalPower) +} + +func TestAttestRejectsRemovedAttester(t *testing.T) { + const height int64 = 10 + + chainID := "test-chain" + priv := cmted25519.GenPrivKey() + pub := priv.PubKey().(cmted25519.PubKey) + authority := sdk.AccAddress(pub.Address()).String() + consAddr := sdk.ConsAddress(pub.Address()).String() + + sk := NewMockStakingKeeper() + server, keeper, ctx := newTestServer(t, &sk) + require.NoError(t, keeper.SetParams(ctx, types.DefaultParams())) + require.NoError(t, registerTestAttester(ctx, &keeper, authority, consAddr, pub, 0)) + + _, err := server.LeaveAttesterSet(ctx, &types.MsgLeaveAttesterSet{ + Authority: authority, + ConsensusAddress: consAddr, + }) + require.NoError(t, err) + + rsp, err := server.Attest(ctx, &types.MsgAttest{ + Authority: authority, + ConsensusAddress: consAddr, + Height: height, + Vote: signTestVote(t, chainID, height, priv, bytes.Repeat([]byte{0x01}, 32)), + }) + require.ErrorIs(t, err, sdkerrors.ErrUnauthorized) + require.Nil(t, rsp) +} + +func TestLeaveAttesterSetKeepsRemovedAttesterEligibleForSnapshotHeight(t *testing.T) { + const height int64 = 10 + + chainID := "test-chain" + blockHash := bytes.Repeat([]byte{0x01}, 32) + priv := cmted25519.GenPrivKey() + pub := priv.PubKey().(cmted25519.PubKey) + authority := sdk.AccAddress(pub.Address()).String() + consAddr := sdk.ConsAddress(pub.Address()).String() + + sk := NewMockStakingKeeper() + server, keeper, ctx := newTestServer(t, &sk) + require.NoError(t, keeper.SetParams(ctx, types.DefaultParams())) + require.NoError(t, registerTestAttester(ctx, &keeper, authority, consAddr, pub, 0)) + require.NoError(t, keeper.SetAttesterSetSnapshot(ctx, height)) + + _, err := server.LeaveAttesterSet(ctx, &types.MsgLeaveAttesterSet{ + Authority: authority, + ConsensusAddress: consAddr, + }) + require.NoError(t, err) + + rsp, err := server.Attest(ctx, &types.MsgAttest{ + Authority: authority, + ConsensusAddress: consAddr, + Height: height, + Vote: signTestVote(t, chainID, height, priv, blockHash), + }) + require.NoError(t, err) + require.NotNil(t, rsp) + + rsp, err = server.Attest(ctx, &types.MsgAttest{ + Authority: authority, + ConsensusAddress: consAddr, + Height: height + 1, + Vote: signTestVote(t, chainID, height+1, priv, blockHash), + }) + require.ErrorIs(t, err, sdkerrors.ErrUnauthorized) require.Nil(t, rsp) } @@ -460,29 +663,54 @@ func TestAttestHeightBounds(t *testing.T) { } } -func TestGetAllSignaturesForHeightUsesValidatorIndexOrder(t *testing.T) { +func TestGetAllSignaturesForHeightUsesCurrentAttesterSetWhenNoSnapshot(t *testing.T) { + sk := NewMockStakingKeeper() + _, keeper, ctx := newTestServer(t, &sk) + + const height int64 = 42 + priv := cmted25519.GenPrivKey() + pub := priv.PubKey().(cmted25519.PubKey) + consAddr := sdk.ConsAddress(pub.Address()).String() + authority := sdk.AccAddress(pub.Address()).String() + signature := []byte("signature-for-current-set") + + require.NoError(t, registerTestAttester(ctx, &keeper, authority, consAddr, pub, 0)) + + bitmap := keeper.bitmapHelper.NewBitmap(1) + keeper.bitmapHelper.SetBit(bitmap, 0) + require.NoError(t, keeper.SetAttestationBitmap(ctx, height, bitmap)) + require.NoError(t, keeper.SetSignature(ctx, height, consAddr, signature)) + + signatures, err := keeper.GetAllSignaturesForHeight(ctx, height) + require.NoError(t, err) + require.Equal(t, map[string][]byte{ + consAddr: signature, + }, signatures) +} + +func TestGetAllSignaturesForHeightUsesSnapshotIndices(t *testing.T) { sk := NewMockStakingKeeper() _, keeper, ctx := newTestServer(t, &sk) const height int64 = 42 - indexZeroAddr := "z-index-zero" - indexOneAddr := "a-index-one" - signature := []byte("signature-for-index-zero") + priv := cmted25519.GenPrivKey() + pub := priv.PubKey().(cmted25519.PubKey) + consAddr := sdk.ConsAddress(pub.Address()).String() + authority := sdk.AccAddress(pub.Address()).String() + signature := []byte("signature-for-snapshot-index-zero") - require.NoError(t, keeper.SetAttesterSetMember(ctx, indexZeroAddr)) - require.NoError(t, keeper.SetAttesterSetMember(ctx, indexOneAddr)) - require.NoError(t, keeper.SetValidatorIndex(ctx, indexZeroAddr, 0, 1)) - require.NoError(t, keeper.SetValidatorIndex(ctx, indexOneAddr, 1, 1)) + require.NoError(t, registerTestAttester(ctx, &keeper, authority, consAddr, pub, 3)) + require.NoError(t, keeper.SetAttesterSetSnapshot(ctx, height)) - bitmap := keeper.bitmapHelper.NewBitmap(2) + bitmap := keeper.bitmapHelper.NewBitmap(1) keeper.bitmapHelper.SetBit(bitmap, 0) require.NoError(t, keeper.SetAttestationBitmap(ctx, height, bitmap)) - require.NoError(t, keeper.SetSignature(ctx, height, indexZeroAddr, signature)) + require.NoError(t, keeper.SetSignature(ctx, height, consAddr, signature)) signatures, err := keeper.GetAllSignaturesForHeight(ctx, height) require.NoError(t, err) require.Equal(t, map[string][]byte{ - indexZeroAddr: signature, + consAddr: signature, }, signatures) } diff --git a/modules/network/types/genesis.pb.go b/modules/network/types/genesis.pb.go index 020b5c99..36c5a4cc 100644 --- a/modules/network/types/genesis.pb.go +++ b/modules/network/types/genesis.pb.go @@ -31,8 +31,8 @@ type GenesisState struct { ValidatorIndices []ValidatorIndex `protobuf:"bytes,2,rep,name=validator_indices,json=validatorIndices,proto3" json:"validator_indices"` // attestation_bitmaps contains historical attestation data AttestationBitmaps []AttestationBitmap `protobuf:"bytes,3,rep,name=attestation_bitmaps,json=attestationBitmaps,proto3" json:"attestation_bitmaps"` - // attester_infos is the fixed attester set loaded at genesis. After chain - // start, the set is immutable (MsgJoin/MsgLeave are disabled). + // attester_infos is the active attester set loaded at genesis. After chain + // start, MsgJoinAttesterSet and MsgLeaveAttesterSet can update the active set. AttesterInfos []AttesterInfo `protobuf:"bytes,4,rep,name=attester_infos,json=attesterInfos,proto3" json:"attester_infos"` } diff --git a/modules/network/types/keys.go b/modules/network/types/keys.go index adca6055..a869918f 100644 --- a/modules/network/types/keys.go +++ b/modules/network/types/keys.go @@ -29,6 +29,7 @@ var ( EpochBitmapPrefix = collections.NewPrefix("epoch_bitmap") AttesterSetPrefix = collections.NewPrefix("attester_set") AttesterInfoPrefix = collections.NewPrefix("attester_info") + AttesterSetSnapshotPrefix = collections.NewPrefix("attester_snapshot") SignaturePrefix = collections.NewPrefix("signature") StoredAttestationInfoPrefix = collections.NewPrefix("stored_attestation_info") LastAttestedHeightKey = collections.NewPrefix("last_attested_height") diff --git a/modules/network/types/query.pb.go b/modules/network/types/query.pb.go index 7c788d82..a6cf4571 100644 --- a/modules/network/types/query.pb.go +++ b/modules/network/types/query.pb.go @@ -856,6 +856,9 @@ func (m *QueryAttesterInfoResponse) GetAttesterInfo() *AttesterInfo { // QueryAttesterSetRequest is the request type for the Query/AttesterSet RPC method. type QueryAttesterSetRequest struct { + // height selects the attester set active for that height. If unset, the + // current attester set is returned. + Height int64 `protobuf:"varint,1,opt,name=height,proto3" json:"height,omitempty"` } func (m *QueryAttesterSetRequest) Reset() { *m = QueryAttesterSetRequest{} } @@ -891,6 +894,13 @@ func (m *QueryAttesterSetRequest) XXX_DiscardUnknown() { var xxx_messageInfo_QueryAttesterSetRequest proto.InternalMessageInfo +func (m *QueryAttesterSetRequest) GetHeight() int64 { + if m != nil { + return m.Height + } + return 0 +} + // AttesterSetEntry is a single entry in the attester set, ordered by index. type AttesterSetEntry struct { Authority string `protobuf:"bytes,1,opt,name=authority,proto3" json:"authority,omitempty"` @@ -1003,7 +1013,7 @@ func init() { func init() { proto.RegisterFile("evabci/network/v1/query.proto", fileDescriptor_faab6bfc228a74e1) } var fileDescriptor_faab6bfc228a74e1 = []byte{ - // 1253 bytes of a gzipped FileDescriptorProto + // 1255 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x57, 0xcf, 0x6f, 0x1b, 0xc5, 0x17, 0xf7, 0xe6, 0x87, 0x5b, 0x3f, 0xa7, 0x6d, 0x32, 0xf5, 0xb7, 0x75, 0xdc, 0xd6, 0x4e, 0xb6, 0x5f, 0xda, 0xb4, 0xc5, 0xbb, 0x75, 0x50, 0x5b, 0x8a, 0x7a, 0x69, 0x4a, 0x0b, 0x05, 0x84, 0xca, @@ -1050,39 +1060,39 @@ var fileDescriptor_faab6bfc228a74e1 = []byte{ 0x35, 0x45, 0xd1, 0x77, 0xed, 0xb6, 0x92, 0x92, 0x86, 0x50, 0x52, 0xb2, 0xba, 0xf4, 0x16, 0x94, 0x47, 0x4e, 0x91, 0xfc, 0x58, 0x1c, 0xa5, 0x59, 0x3a, 0x86, 0xe5, 0x94, 0x42, 0x71, 0x23, 0x4e, 0x44, 0x1f, 0x68, 0xdb, 0x71, 0x3b, 0x54, 0xcd, 0x40, 0x6d, 0x42, 0x2f, 0x04, 0x7f, 0x01, 0x27, - 0x56, 0xfa, 0x32, 0x9c, 0x1d, 0xed, 0x38, 0x89, 0x3b, 0xf0, 0xaf, 0x06, 0x8b, 0x89, 0xf0, 0x7d, - 0x97, 0xfb, 0x03, 0x74, 0x13, 0x0a, 0x38, 0xe0, 0x5b, 0xd4, 0x77, 0xf8, 0x40, 0xea, 0xde, 0x28, - 0xff, 0xf6, 0x53, 0xbd, 0xa4, 0x6c, 0x49, 0x29, 0xdf, 0xe4, 0xbe, 0xe3, 0x76, 0xad, 0x21, 0x14, - 0xdd, 0x87, 0xa5, 0x56, 0x28, 0xdb, 0x65, 0x01, 0x8b, 0xcf, 0x3d, 0x33, 0x85, 0xbf, 0x18, 0x53, - 0xa2, 0xe7, 0x53, 0x8a, 0x06, 0x3e, 0x7c, 0xfe, 0x27, 0xd4, 0x34, 0xa3, 0x07, 0x90, 0xf7, 0x82, - 0xe6, 0x13, 0x32, 0x10, 0x0f, 0xbe, 0xb8, 0x5e, 0x32, 0xa4, 0x9b, 0x1a, 0x91, 0x9b, 0x1a, 0x77, - 0xdd, 0xc1, 0x46, 0xf9, 0xd7, 0xe1, 0x3e, 0x2d, 0x7f, 0xe0, 0x71, 0x6a, 0x3c, 0x0a, 0x9a, 0xef, - 0x92, 0x81, 0xa5, 0xd8, 0x6f, 0x1c, 0xff, 0xec, 0x79, 0x2d, 0xf7, 0xcf, 0xf3, 0x5a, 0x4e, 0xb7, - 0x0f, 0x5c, 0xa1, 0x68, 0x8b, 0x6a, 0xfc, 0x3d, 0x38, 0x46, 0x5c, 0xee, 0x3b, 0xf1, 0xf3, 0xbb, - 0x38, 0xe9, 0xf9, 0xa9, 0xc6, 0x29, 0x97, 0x8b, 0x98, 0xeb, 0x3f, 0x16, 0x61, 0x5e, 0xec, 0x80, - 0x3e, 0x81, 0xbc, 0x34, 0x42, 0xf4, 0x4a, 0x4a, 0x9d, 0x71, 0xc7, 0xad, 0x5c, 0x9a, 0x06, 0x93, - 0x3a, 0xf5, 0xd5, 0x4f, 0x7f, 0xff, 0xfb, 0xeb, 0x99, 0x73, 0x68, 0xd9, 0x1c, 0xb7, 0x76, 0x69, - 0xb6, 0xe8, 0x3b, 0x2d, 0x1a, 0xe8, 0xa4, 0x29, 0x5c, 0xcf, 0xda, 0x20, 0xcb, 0x93, 0x2b, 0x8d, - 0x23, 0x30, 0x94, 0x3a, 0x53, 0xa8, 0xbb, 0x82, 0x2e, 0x9b, 0x59, 0x7f, 0x78, 0x08, 0x96, 0xb9, - 0x23, 0x87, 0x6a, 0x17, 0x7d, 0xae, 0x41, 0x21, 0xf6, 0x52, 0xb4, 0x96, 0xb5, 0xe3, 0x41, 0x7b, - 0xae, 0x5c, 0x39, 0x04, 0x52, 0x69, 0x5a, 0x13, 0x9a, 0x74, 0xb4, 0x92, 0xa2, 0x49, 0x98, 0xb4, - 0xb9, 0x23, 0xfe, 0xd9, 0x45, 0xdf, 0x68, 0x70, 0x72, 0xd4, 0x59, 0x50, 0x3d, 0x6b, 0x9f, 0x54, - 0xcf, 0xab, 0x18, 0x87, 0x85, 0x2b, 0x6d, 0x86, 0xd0, 0xb6, 0x86, 0x2e, 0xa5, 0x68, 0x8b, 0x3f, - 0x1c, 0xe6, 0x8e, 0x1a, 0xad, 0x5d, 0xf4, 0x8b, 0x06, 0x67, 0xd2, 0xed, 0x09, 0xdd, 0xc8, 0xda, - 0x7a, 0xa2, 0x1b, 0x56, 0x6e, 0x1e, 0x95, 0xa6, 0x94, 0xdf, 0x10, 0xca, 0x4d, 0x54, 0x4f, 0x51, - 0x1e, 0x7a, 0x63, 0xbd, 0x95, 0xe0, 0x0e, 0xef, 0xfb, 0x7b, 0x0d, 0xd0, 0xb8, 0x0f, 0xa0, 0x29, - 0x4f, 0x2d, 0xc5, 0xcd, 0x2a, 0xeb, 0x47, 0xa1, 0x1c, 0xa2, 0xdd, 0x43, 0x1f, 0x19, 0xaa, 0xfd, - 0x41, 0x03, 0x34, 0x6e, 0x15, 0xd9, 0x6a, 0x33, 0x8d, 0x27, 0x5b, 0x6d, 0xb6, 0x13, 0x4d, 0x1c, - 0xa6, 0x6d, 0xcc, 0x78, 0x5d, 0x7d, 0xf3, 0xdb, 0x75, 0xa9, 0x17, 0x7d, 0xab, 0xc1, 0x42, 0xd2, - 0x15, 0xd0, 0xb5, 0x69, 0x3d, 0x4a, 0x8e, 0xd4, 0xab, 0x87, 0x03, 0x2b, 0x71, 0xb7, 0x84, 0xb8, - 0x06, 0x32, 0xcd, 0xec, 0x9f, 0x18, 0xe6, 0xce, 0x98, 0x2b, 0xee, 0xa2, 0x2f, 0x35, 0x28, 0x26, - 0xbe, 0xa3, 0xe8, 0xea, 0xd4, 0x7b, 0x8c, 0xcd, 0xab, 0x72, 0xed, 0x50, 0x58, 0xa5, 0xf0, 0xb2, - 0x50, 0xb8, 0x8a, 0x6a, 0x13, 0x14, 0xda, 0x8c, 0xf0, 0x8d, 0x77, 0x5e, 0xec, 0x55, 0xb5, 0x97, - 0x7b, 0x55, 0xed, 0xaf, 0xbd, 0xaa, 0xf6, 0xd5, 0x7e, 0x35, 0xf7, 0x72, 0xbf, 0x9a, 0xfb, 0x63, - 0xbf, 0x9a, 0xfb, 0xf0, 0x7a, 0xd7, 0xe1, 0x5b, 0x41, 0xd3, 0x68, 0xd1, 0x9e, 0x49, 0xfa, 0x8c, - 0xe3, 0xd6, 0x13, 0x93, 0xf4, 0xeb, 0xa2, 0x5a, 0x8f, 0xb6, 0x83, 0x6d, 0xc2, 0xe2, 0xaa, 0xe2, - 0x67, 0x57, 0x33, 0x2f, 0xcc, 0xe9, 0xb5, 0xff, 0x02, 0x00, 0x00, 0xff, 0xff, 0x10, 0xfd, 0x50, - 0xd7, 0x60, 0x0e, 0x00, 0x00, + 0x56, 0x7a, 0x03, 0xce, 0x8e, 0x76, 0x9c, 0xf0, 0x69, 0x8f, 0xe0, 0x5f, 0x0d, 0x16, 0x13, 0xf0, + 0xfb, 0x2e, 0xf7, 0x07, 0xe8, 0x26, 0x14, 0x70, 0xc0, 0xb7, 0xa8, 0xef, 0xf0, 0x81, 0x3c, 0xcf, + 0x46, 0xf9, 0xb7, 0x9f, 0xea, 0x25, 0x65, 0x57, 0xea, 0x44, 0x9b, 0xdc, 0x77, 0xdc, 0xae, 0x35, + 0x84, 0xa2, 0xfb, 0xb0, 0xd4, 0x0a, 0x8f, 0xe3, 0xb2, 0x80, 0xc5, 0xfd, 0x98, 0x99, 0xc2, 0x5f, + 0x8c, 0x29, 0xd1, 0xb3, 0x2a, 0x45, 0x1f, 0x82, 0x70, 0x2c, 0x4e, 0xa8, 0x29, 0x47, 0x0f, 0x20, + 0xef, 0x05, 0xcd, 0x27, 0x64, 0x20, 0x06, 0xa1, 0xb8, 0x5e, 0x32, 0xa4, 0xcb, 0x1a, 0x91, 0xcb, + 0x1a, 0x77, 0xdd, 0xc1, 0x46, 0xf9, 0xd7, 0xe1, 0x3e, 0x2d, 0x7f, 0xe0, 0x71, 0x6a, 0x3c, 0x0a, + 0x9a, 0xef, 0x92, 0x81, 0xa5, 0xd8, 0x6f, 0x1c, 0xff, 0xec, 0x79, 0x2d, 0xf7, 0xcf, 0xf3, 0x5a, + 0x4e, 0xb7, 0x0f, 0x5c, 0xad, 0x68, 0x97, 0xba, 0x90, 0x7b, 0x70, 0x8c, 0xb8, 0xdc, 0x77, 0xe2, + 0x67, 0x79, 0x71, 0xd2, 0xb3, 0x54, 0x8d, 0x53, 0xee, 0x17, 0x31, 0xd7, 0x7f, 0x2c, 0xc2, 0xbc, + 0xd8, 0x01, 0x7d, 0x02, 0x79, 0x69, 0x90, 0xe8, 0x95, 0x94, 0x3a, 0xe3, 0x4e, 0x5c, 0xb9, 0x34, + 0x0d, 0x26, 0x75, 0xea, 0xab, 0x9f, 0xfe, 0xfe, 0xf7, 0xd7, 0x33, 0xe7, 0xd0, 0xb2, 0x39, 0x6e, + 0xf9, 0xd2, 0x84, 0xd1, 0x77, 0x5a, 0x34, 0xe8, 0x49, 0xb3, 0xb8, 0x9e, 0xb5, 0x41, 0x96, 0x57, + 0x57, 0x1a, 0x47, 0x60, 0x28, 0x75, 0xa6, 0x50, 0x77, 0x05, 0x5d, 0x36, 0xb3, 0xfe, 0x20, 0x11, + 0x2c, 0x73, 0x47, 0xbe, 0xc6, 0x5d, 0xf4, 0xb9, 0x06, 0x85, 0xd8, 0x63, 0xd1, 0x5a, 0xd6, 0x8e, + 0x07, 0x6d, 0xbb, 0x72, 0xe5, 0x10, 0x48, 0xa5, 0x69, 0x4d, 0x68, 0xd2, 0xd1, 0x4a, 0x8a, 0x26, + 0x61, 0xde, 0xe6, 0x8e, 0xf8, 0x67, 0x17, 0x7d, 0xa3, 0xc1, 0xc9, 0x51, 0xc7, 0x41, 0xf5, 0xac, + 0x7d, 0x52, 0xbd, 0xb0, 0x62, 0x1c, 0x16, 0xae, 0xb4, 0x19, 0x42, 0xdb, 0x1a, 0xba, 0x94, 0xa2, + 0x2d, 0xfe, 0xa0, 0x98, 0x3b, 0x6a, 0xb4, 0x76, 0xd1, 0x2f, 0x1a, 0x9c, 0x49, 0xb7, 0x2d, 0x74, + 0x23, 0x6b, 0xeb, 0x89, 0x2e, 0x59, 0xb9, 0x79, 0x54, 0x9a, 0x52, 0x7e, 0x43, 0x28, 0x37, 0x51, + 0x3d, 0x45, 0x79, 0xe8, 0x99, 0xf5, 0x56, 0x82, 0x3b, 0xbc, 0xef, 0xef, 0x35, 0x40, 0xe3, 0xfe, + 0x80, 0xa6, 0x3c, 0xb5, 0x14, 0x97, 0xab, 0xac, 0x1f, 0x85, 0x72, 0x88, 0x76, 0x0f, 0xfd, 0x65, + 0xa8, 0xf6, 0x07, 0x0d, 0xd0, 0xb8, 0x85, 0x64, 0xab, 0xcd, 0x34, 0xa4, 0x6c, 0xb5, 0xd9, 0x0e, + 0x35, 0x71, 0x98, 0xb6, 0x31, 0xe3, 0x75, 0xe5, 0x05, 0xed, 0xba, 0xd4, 0x8b, 0xbe, 0xd5, 0x60, + 0x21, 0xe9, 0x16, 0xe8, 0xda, 0xb4, 0x1e, 0x25, 0x47, 0xea, 0xd5, 0xc3, 0x81, 0x95, 0xb8, 0x5b, + 0x42, 0x5c, 0x03, 0x99, 0x66, 0xf6, 0x4f, 0x0f, 0x73, 0x67, 0xcc, 0x2d, 0x77, 0xd1, 0x97, 0x1a, + 0x14, 0x13, 0xdf, 0x51, 0x74, 0x75, 0xea, 0x3d, 0xc6, 0xa6, 0x56, 0xb9, 0x76, 0x28, 0xac, 0x52, + 0x78, 0x59, 0x28, 0x5c, 0x45, 0xb5, 0x09, 0x0a, 0x6d, 0x46, 0xf8, 0xc6, 0x3b, 0x2f, 0xf6, 0xaa, + 0xda, 0xcb, 0xbd, 0xaa, 0xf6, 0xd7, 0x5e, 0x55, 0xfb, 0x6a, 0xbf, 0x9a, 0x7b, 0xb9, 0x5f, 0xcd, + 0xfd, 0xb1, 0x5f, 0xcd, 0x7d, 0x78, 0xbd, 0xeb, 0xf0, 0xad, 0xa0, 0x69, 0xb4, 0x68, 0xcf, 0x24, + 0x7d, 0xc6, 0x71, 0xeb, 0x89, 0x49, 0xfa, 0x75, 0x51, 0xad, 0x47, 0xdb, 0xc1, 0x36, 0x61, 0x71, + 0x55, 0xf1, 0x73, 0xac, 0x99, 0x17, 0xe6, 0xf4, 0xda, 0x7f, 0x01, 0x00, 0x00, 0xff, 0xff, 0xf4, + 0xb2, 0xce, 0x15, 0x78, 0x0e, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -2055,6 +2065,11 @@ func (m *QueryAttesterSetRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) _ = i var l int _ = l + if m.Height != 0 { + i = encodeVarintQuery(dAtA, i, uint64(m.Height)) + i-- + dAtA[i] = 0x8 + } return len(dAtA) - i, nil } @@ -2402,6 +2417,9 @@ func (m *QueryAttesterSetRequest) Size() (n int) { } var l int _ = l + if m.Height != 0 { + n += 1 + sovQuery(uint64(m.Height)) + } return n } @@ -3948,6 +3966,25 @@ func (m *QueryAttesterSetRequest) Unmarshal(dAtA []byte) error { return fmt.Errorf("proto: QueryAttesterSetRequest: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Height", wireType) + } + m.Height = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowQuery + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Height |= int64(b&0x7F) << shift + if b < 0x80 { + break + } + } default: iNdEx = preIndex skippy, err := skipQuery(dAtA[iNdEx:]) diff --git a/modules/network/types/query.pb.gw.go b/modules/network/types/query.pb.gw.go index 91b4d014..ff1c4d89 100644 --- a/modules/network/types/query.pb.gw.go +++ b/modules/network/types/query.pb.gw.go @@ -393,10 +393,21 @@ func local_request_Query_AttesterInfo_0(ctx context.Context, marshaler runtime.M } +var ( + filter_Query_AttesterSet_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} +) + func request_Query_AttesterSet_0(ctx context.Context, marshaler runtime.Marshaler, client QueryClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq QueryAttesterSetRequest var metadata runtime.ServerMetadata + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Query_AttesterSet_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.AttesterSet(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err @@ -406,6 +417,13 @@ func local_request_Query_AttesterSet_0(ctx context.Context, marshaler runtime.Ma var protoReq QueryAttesterSetRequest var metadata runtime.ServerMetadata + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_Query_AttesterSet_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.AttesterSet(ctx, &protoReq) return msg, metadata, err diff --git a/modules/proto/evabci/network/v1/genesis.proto b/modules/proto/evabci/network/v1/genesis.proto index 8098c25b..00914451 100644 --- a/modules/proto/evabci/network/v1/genesis.proto +++ b/modules/proto/evabci/network/v1/genesis.proto @@ -19,7 +19,7 @@ message GenesisState { // attestation_bitmaps contains historical attestation data repeated AttestationBitmap attestation_bitmaps = 3 [(gogoproto.nullable) = false]; - // attester_infos is the fixed attester set loaded at genesis. After chain - // start, the set is immutable (MsgJoin/MsgLeave are disabled). + // attester_infos is the active attester set loaded at genesis. After chain + // start, MsgJoinAttesterSet and MsgLeaveAttesterSet can update the active set. repeated AttesterInfo attester_infos = 4 [(gogoproto.nullable) = false]; } diff --git a/modules/proto/evabci/network/v1/query.proto b/modules/proto/evabci/network/v1/query.proto index ac16858b..bb6fafd0 100644 --- a/modules/proto/evabci/network/v1/query.proto +++ b/modules/proto/evabci/network/v1/query.proto @@ -152,7 +152,11 @@ message QueryAttesterInfoResponse { } // QueryAttesterSetRequest is the request type for the Query/AttesterSet RPC method. -message QueryAttesterSetRequest {} +message QueryAttesterSetRequest { + // height selects the attester set active for that height. If unset, the + // current attester set is returned. + int64 height = 1; +} // AttesterSetEntry is a single entry in the attester set, ordered by index. message AttesterSetEntry { diff --git a/pkg/rpc/core/blocks.go b/pkg/rpc/core/blocks.go index 95da1d81..73accfc6 100644 --- a/pkg/rpc/core/blocks.go +++ b/pkg/rpc/core/blocks.go @@ -347,7 +347,7 @@ func getCommitForHeight(ctx context.Context, height uint64) (*cmttypes.Commit, e return nil, fmt.Errorf("get block ID for height %d: %w", height, err) } - entries, err := getAttesterSet(ctx) + entries, err := getAttesterSet(ctx, int64(height)) if err != nil { return nil, fmt.Errorf("get attester set: %w", err) } @@ -399,8 +399,8 @@ type attesterSetEntry struct { } // getAttesterSet fetches the ordered attester set from the network module via ABCI query. -func getAttesterSet(ctx context.Context) ([]attesterSetEntry, error) { - req, err := proto.Marshal(&networktypes.QueryAttesterSetRequest{}) +func getAttesterSet(ctx context.Context, height int64) ([]attesterSetEntry, error) { + req, err := proto.Marshal(&networktypes.QueryAttesterSetRequest{Height: height}) if err != nil { return nil, err } diff --git a/pkg/rpc/core/commit_reconstruction_test.go b/pkg/rpc/core/commit_reconstruction_test.go index 1fb341ae..2320f86d 100644 --- a/pkg/rpc/core/commit_reconstruction_test.go +++ b/pkg/rpc/core/commit_reconstruction_test.go @@ -195,7 +195,14 @@ func buildEnv(t *testing.T, height uint64, keys []cmted25519.PrivKey, signers [] mApp := new(mockABCI) mApp.On("Query", mock.Anything, mock.MatchedBy(func(r *abci.RequestQuery) bool { - return r.Path == "/evabci.network.v1.Query/AttesterSet" + if r.Path != "/evabci.network.v1.Query/AttesterSet" { + return false + } + var req networktypes.QueryAttesterSetRequest + if err := proto.Unmarshal(r.Data, &req); err != nil { + return false + } + return req.Height == int64(height) })).Return(&abci.ResponseQuery{Code: 0, Value: setRespBz}, nil) mApp.On("Query", mock.Anything, mock.MatchedBy(func(r *abci.RequestQuery) bool { return r.Path == "/evabci.network.v1.Query/AttesterSignatures" diff --git a/pkg/rpc/core/consensus.go b/pkg/rpc/core/consensus.go index 3f380af0..7aad33fa 100644 --- a/pkg/rpc/core/consensus.go +++ b/pkg/rpc/core/consensus.go @@ -22,12 +22,12 @@ func Validators(ctx *rpctypes.Context, heightPtr *int64, _, _ *int) (*coretypes. return nil, fmt.Errorf("failed to normalize height: %w", err) } - // In attester mode, return the full fixed attester set. /commit uses the - // same set and marks missing signatures as absent, so the two endpoints must - // stay aligned for light-client verification. + // In attester mode, return the attester set for the requested height. + // /commit uses the same height-scoped set and marks missing signatures as + // absent, so the two endpoints must stay aligned for light-client verification. if env.AttesterMode { env.Logger.Info("Validators endpoint in attester mode - returning full attester set", "height", height) - entries, err := getAttesterSet(ctx.Context()) + entries, err := getAttesterSet(ctx.Context(), int64(height)) if err != nil { return nil, fmt.Errorf("get attester set: %w", err) } diff --git a/pkg/rpc/core/consensus_test.go b/pkg/rpc/core/consensus_test.go index 3c61adae..3009bd33 100644 --- a/pkg/rpc/core/consensus_test.go +++ b/pkg/rpc/core/consensus_test.go @@ -290,7 +290,14 @@ func TestValidatorsAttesterModeReturnsFullAttesterSet(t *testing.T) { mApp := new(MockApp) mApp.On("Query", testifymock.Anything, testifymock.MatchedBy(func(r *abci.RequestQuery) bool { - return r.Path == "/evabci.network.v1.Query/AttesterSet" + if r.Path != "/evabci.network.v1.Query/AttesterSet" { + return false + } + var req networktypes.QueryAttesterSetRequest + if err := proto.Unmarshal(r.Data, &req); err != nil { + return false + } + return req.Height == height })).Return(&abci.ResponseQuery{Code: 0, Value: setRespBz}, nil) mApp.On("Query", testifymock.Anything, testifymock.MatchedBy(func(r *abci.RequestQuery) bool { return r.Path == "/evabci.network.v1.Query/AttesterSignatures" diff --git a/server/attester_cmd.go b/server/attester_cmd.go index 16de9fbb..0425d9c1 100644 --- a/server/attester_cmd.go +++ b/server/attester_cmd.go @@ -34,6 +34,7 @@ import ( "github.com/cosmos/cosmos-sdk/x/bank" "github.com/cosmos/gogoproto/proto" "github.com/spf13/cobra" + "google.golang.org/grpc" evolvetypes "github.com/evstack/ev-node/types" @@ -148,10 +149,22 @@ func assertRegistered( ctx context.Context, consensusPrivKey *pvm.FilePV, clientCtx client.Context, +) error { + return assertRegisteredAtHeight(ctx, consensusPrivKey, networktypes.NewQueryClient(clientCtx), 0) +} + +type attesterSetQueryClient interface { + AttesterSet(context.Context, *networktypes.QueryAttesterSetRequest, ...grpc.CallOption) (*networktypes.QueryAttesterSetResponse, error) +} + +func assertRegisteredAtHeight( + ctx context.Context, + consensusPrivKey *pvm.FilePV, + queryClient attesterSetQueryClient, + height int64, ) error { consAddr := sdk.ConsAddress(consensusPrivKey.Key.PubKey.Address()).String() - queryClient := networktypes.NewQueryClient(clientCtx) - resp, err := queryClient.AttesterSet(ctx, &networktypes.QueryAttesterSetRequest{}) + resp, err := queryClient.AttesterSet(ctx, &networktypes.QueryAttesterSetRequest{Height: height}) if err != nil { return fmt.Errorf("query attester set: %w", err) } @@ -160,7 +173,10 @@ func assertRegistered( return nil } } - return fmt.Errorf("consensus address %s is not in the attester set; must be registered in genesis", consAddr) + if height == 0 { + return fmt.Errorf("consensus address %s is not in the attester set", consAddr) + } + return fmt.Errorf("consensus address %s is not in the attester set at height %d", consAddr, height) } func pullBlocksAndAttest( @@ -174,6 +190,7 @@ func pullBlocksAndAttest( if err := assertRegistered(ctx, consensusPrivKey, clientCtx); err != nil { return err } + queryClient := networktypes.NewQueryClient(clientCtx) var nextHeight int64 ticker := time.NewTicker(500 * time.Millisecond) @@ -198,6 +215,10 @@ func pullBlocksAndAttest( continue } for h := nextHeight; h <= currentHeight; h++ { + if err := assertRegisteredAtHeight(ctx, consensusPrivKey, queryClient, h); err != nil { + fmt.Printf("skip attest h=%d: %v\n", h, err) + continue + } if err := submitAttestation(ctx, config, h, valAddr, operatorPrivKey, consensusPrivKey, clientCtx); err != nil { // duplicate or transient — log and move on fmt.Printf("attest h=%d: %v\n", h, err) diff --git a/server/attester_cmd_test.go b/server/attester_cmd_test.go index d633f803..871bcae9 100644 --- a/server/attester_cmd_test.go +++ b/server/attester_cmd_test.go @@ -12,9 +12,13 @@ import ( pvm "github.com/cometbft/cometbft/privval" cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" cmttypes "github.com/cometbft/cometbft/types" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/gogoproto/proto" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "google.golang.org/grpc" + + networktypes "github.com/evstack/ev-abci/modules/network/types" ) func TestPrivateKeyFromMnemonic(t *testing.T) { @@ -233,6 +237,61 @@ func TestInitialAttestationHeight(t *testing.T) { require.Equal(t, int64(42), initialAttestationHeight(42)) } +type fakeAttesterSetQueryClient struct { + entriesByHeight map[int64][]networktypes.AttesterSetEntry + requestedHeight []int64 +} + +func (f *fakeAttesterSetQueryClient) AttesterSet( + _ context.Context, + req *networktypes.QueryAttesterSetRequest, + _ ...grpc.CallOption, +) (*networktypes.QueryAttesterSetResponse, error) { + f.requestedHeight = append(f.requestedHeight, req.Height) + return &networktypes.QueryAttesterSetResponse{Entries: f.entriesByHeight[req.Height]}, nil +} + +func TestAssertRegisteredAtHeightQueriesRequestedHeight(t *testing.T) { + privKey := ed25519.GenPrivKey() + pv := &pvm.FilePV{ + Key: pvm.FilePVKey{ + Address: privKey.PubKey().Address(), + PubKey: privKey.PubKey(), + PrivKey: privKey, + }, + } + consAddr := sdk.ConsAddress(privKey.PubKey().Address()).String() + queryClient := &fakeAttesterSetQueryClient{ + entriesByHeight: map[int64][]networktypes.AttesterSetEntry{ + 7: {{ConsensusAddress: consAddr}}, + }, + } + + err := assertRegisteredAtHeight(context.Background(), pv, queryClient, 7) + require.NoError(t, err) + require.Equal(t, []int64{7}, queryClient.requestedHeight) +} + +func TestAssertRegisteredAtHeightRejectsRemovedAttester(t *testing.T) { + privKey := ed25519.GenPrivKey() + pv := &pvm.FilePV{ + Key: pvm.FilePVKey{ + Address: privKey.PubKey().Address(), + PubKey: privKey.PubKey(), + PrivKey: privKey, + }, + } + queryClient := &fakeAttesterSetQueryClient{ + entriesByHeight: map[int64][]networktypes.AttesterSetEntry{ + 11: {}, + }, + } + + err := assertRegisteredAtHeight(context.Background(), pv, queryClient, 11) + require.Error(t, err) + require.Contains(t, err.Error(), "not in the attester set at height 11") +} + func TestGetEvolveHeader(t *testing.T) { t.Run("valid response builds Evolve header", func(t *testing.T) { nodeURL := newAttesterRPCTestServer(t, `{