From 7e926fba5343cb575a27127aad31721e4c72b7bd Mon Sep 17 00:00:00 2001 From: Randy Grok Date: Tue, 28 Apr 2026 17:02:19 +0200 Subject: [PATCH 1/3] feat(network): load fixed attester set from genesis --- modules/network/genesis.go | 110 ++++--- modules/network/keeper/bitmap_test.go | 269 ++++++++++++++++++ modules/network/keeper/genesis_test.go | 131 +++++++++ modules/network/keeper/keeper.go | 8 +- modules/network/keeper/msg_server.go | 80 +----- modules/network/keeper/msg_server_test.go | 192 ++----------- modules/network/types/attester.go | 40 +++ modules/network/types/attester.pb.go | 93 ++++-- modules/network/types/genesis.go | 14 + modules/network/types/genesis.pb.go | 106 +++++-- .../proto/evabci/network/v1/attester.proto | 5 + modules/proto/evabci/network/v1/genesis.proto | 5 + 12 files changed, 717 insertions(+), 336 deletions(-) create mode 100644 modules/network/keeper/bitmap_test.go create mode 100644 modules/network/keeper/genesis_test.go create mode 100644 modules/network/types/attester.go diff --git a/modules/network/genesis.go b/modules/network/genesis.go index 85e0f6da..388b3a03 100644 --- a/modules/network/genesis.go +++ b/modules/network/genesis.go @@ -1,9 +1,12 @@ package network import ( + "bytes" "fmt" + "sort" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/bech32" "github.com/evstack/ev-abci/modules/network/keeper" "github.com/evstack/ev-abci/modules/network/types" @@ -11,38 +14,80 @@ import ( // InitGenesis initializes the network module's state from a provided genesis state. func InitGenesis(ctx sdk.Context, k keeper.Keeper, genState types.GenesisState) error { - // Set module params if err := k.SetParams(ctx, genState.Params); err != nil { return fmt.Errorf("set params: %s", err) } - // Set validator indices - for _, vi := range genState.ValidatorIndices { - if err := k.SetValidatorIndex(ctx, vi.Address, uint16(vi.Index), vi.Power); err != nil { - return err + // Load attesters: validate pubkey/address match, then insert and assign indices. + attesters := make([]types.AttesterInfo, len(genState.AttesterInfos)) + copy(attesters, genState.AttesterInfos) + + for i := range attesters { + info := attesters[i] + pk, err := info.GetPubKey() + if err != nil { + return fmt.Errorf("attester %d: %w", i, err) } - // Also add to attester set - if err := k.SetAttesterSetMember(ctx, vi.Address); err != nil { - return err + // Validate pubkey ↔ consensus_address match at the raw-bytes level so + // the check is independent of bech32 prefix (e.g. "cosmosvalcons" vs + // "celestiavalcons"). Whatever prefix was used in genesis, the 20-byte + // payload must equal the pubkey's Address(). + _, rawAddr, decErr := bech32.DecodeAndConvert(info.ConsensusAddress) + if decErr != nil { + return fmt.Errorf("attester %d: decode consensus_address %q: %w", i, info.ConsensusAddress, decErr) + } + if !bytes.Equal(rawAddr, pk.Address()) { + return fmt.Errorf("attester %d: pubkey address mismatch (derived bytes %x, stated bytes %x)", + i, pk.Address(), rawAddr) + } + // Re-encode consensus_address with the runtime SDK config so the + // stored value matches what ConsAddress().String() produces elsewhere + // in the module at runtime. + derived := sdk.ConsAddress(pk.Address()).String() + info.ConsensusAddress = derived + attesters[i] = info + } + + // Order by pubkey.Address() bytes ascending to match cmttypes.NewValidatorSet. + sort.Slice(attesters, func(i, j int) bool { + pki, _ := attesters[i].GetPubKey() + pkj, _ := attesters[j].GetPubKey() + return bytes.Compare(pki.Address(), pkj.Address()) < 0 + }) + + for idx, info := range attesters { + if err := k.SetAttesterInfo(ctx, info.ConsensusAddress, &info); err != nil { + return fmt.Errorf("set attester info: %w", err) + } + if err := k.SetAttesterSetMember(ctx, info.ConsensusAddress); err != nil { + return fmt.Errorf("set attester set member: %w", err) + } + if err := k.SetValidatorIndex(ctx, info.ConsensusAddress, uint16(idx), 1); err != nil { + return fmt.Errorf("set validator index: %w", err) } } - // Set attestation bitmaps + // Still load historical bitmaps if provided (upgrade/dump scenarios). for _, ab := range genState.AttestationBitmaps { if err := k.SetAttestationBitmap(ctx, ab.Height, ab.Bitmap); err != nil { return err } - // Store full attestation info using collections API if err := k.StoredAttestationInfo.Set(ctx, ab.Height, ab); err != nil { return err } - if ab.SoftConfirmed { if err := setSoftConfirmed(ctx, k, ab.Height); err != nil { return err } } } + + // Legacy: genState.ValidatorIndices is now derived from AttesterInfos and + // ignored. Warn if non-empty so operators notice. + if len(genState.ValidatorIndices) > 0 { + k.Logger(ctx).Error("genesis.validator_indices is deprecated and ignored; use attester_infos") + } + return nil } @@ -51,29 +96,22 @@ func ExportGenesis(ctx sdk.Context, k keeper.Keeper) *types.GenesisState { genesis := types.DefaultGenesisState() genesis.Params = k.GetParams(ctx) - // Export validator indices using collections API - var validatorIndices []types.ValidatorIndex - // Iterate through all validator indices - if err := k.ValidatorIndex.Walk(ctx, nil, func(addr string, index uint16) (bool, error) { - power, err := k.GetValidatorPower(ctx, index) - if err != nil { - return false, fmt.Errorf("get validator power: %w", err) - } - validatorIndices = append(validatorIndices, types.ValidatorIndex{ - Address: addr, - Index: uint32(index), - Power: power, - }) + 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 { panic(err) } - genesis.ValidatorIndices = validatorIndices + sort.Slice(attesters, func(i, j int) bool { + pki, _ := attesters[i].GetPubKey() + pkj, _ := attesters[j].GetPubKey() + return bytes.Compare(pki.Address(), pkj.Address()) < 0 + }) + genesis.AttesterInfos = attesters - // Export attestation bitmaps using collections API var attestationBitmaps []types.AttestationBitmap - // Iterate through all stored attestation info - if err := k.StoredAttestationInfo.Walk(ctx, nil, func(height int64, ab types.AttestationBitmap) (bool, error) { + if err := k.StoredAttestationInfo.Walk(ctx, nil, func(_ int64, ab types.AttestationBitmap) (bool, error) { attestationBitmaps = append(attestationBitmaps, ab) return false, nil }); err != nil { @@ -81,24 +119,18 @@ func ExportGenesis(ctx sdk.Context, k keeper.Keeper) *types.GenesisState { } genesis.AttestationBitmaps = attestationBitmaps + // ValidatorIndices no longer exported: they are derived deterministically + // from AttesterInfos order. + genesis.ValidatorIndices = nil + return genesis } -// Helper function to set soft confirmed status func setSoftConfirmed(ctx sdk.Context, k keeper.Keeper, height int64) error { - // Get the existing attestation bitmap ab, err := k.StoredAttestationInfo.Get(ctx, height) if err != nil { - // If there's no existing attestation bitmap, we can't set it as soft confirmed return err } - - // Set the SoftConfirmed field to true ab.SoftConfirmed = true - - // Update the attestation bitmap in the collection - if err := k.StoredAttestationInfo.Set(ctx, height, ab); err != nil { - return err - } - return nil + return k.StoredAttestationInfo.Set(ctx, height, ab) } diff --git a/modules/network/keeper/bitmap_test.go b/modules/network/keeper/bitmap_test.go new file mode 100644 index 00000000..9515f9a4 --- /dev/null +++ b/modules/network/keeper/bitmap_test.go @@ -0,0 +1,269 @@ +package keeper + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewBitmap(t *testing.T) { + bh := NewBitmapHelper() + + specs := map[string]struct { + n int + expSize int + }{ + "zero validators": {n: 0, expSize: 0}, + "one validator": {n: 1, expSize: 1}, + "seven validators": {n: 7, expSize: 1}, + "eight validators": {n: 8, expSize: 1}, + "nine validators": {n: 9, expSize: 2}, + "sixteen validators": {n: 16, expSize: 2}, + "seventeen validators": {n: 17, expSize: 3}, + "one-hundred validators": {n: 100, expSize: 13}, + "ten-thousand validators": {n: 10_000, expSize: 1250}, + } + + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + bitmap := bh.NewBitmap(spec.n) + require.Len(t, bitmap, spec.expSize) + for _, b := range bitmap { + assert.Equal(t, byte(0), b, "new bitmap must be zero-initialised") + } + }) + } +} + +func TestSetBitAndIsSet(t *testing.T) { + bh := NewBitmapHelper() + + t.Run("set and read within range", func(t *testing.T) { + bitmap := bh.NewBitmap(16) + // Cover low-byte, high-byte, and edges. + for _, idx := range []int{0, 1, 7, 8, 9, 15} { + assert.False(t, bh.IsSet(bitmap, idx), "bit %d should start unset", idx) + bh.SetBit(bitmap, idx) + assert.True(t, bh.IsSet(bitmap, idx), "bit %d should be set", idx) + } + }) + + t.Run("out-of-range index is a no-op", func(t *testing.T) { + bitmap := bh.NewBitmap(8) + + bh.SetBit(bitmap, -1) + bh.SetBit(bitmap, 8) + bh.SetBit(bitmap, 100) + + assert.Equal(t, 0, bh.PopCount(bitmap), "out-of-range SetBit must not flip any bit") + assert.False(t, bh.IsSet(bitmap, -1)) + assert.False(t, bh.IsSet(bitmap, 8)) + assert.False(t, bh.IsSet(bitmap, 100)) + }) + + t.Run("set is idempotent", func(t *testing.T) { + bitmap := bh.NewBitmap(8) + bh.SetBit(bitmap, 3) + bh.SetBit(bitmap, 3) + assert.Equal(t, 1, bh.PopCount(bitmap)) + assert.True(t, bh.IsSet(bitmap, 3)) + }) + + t.Run("setting one bit does not affect neighbours", func(t *testing.T) { + bitmap := bh.NewBitmap(16) + bh.SetBit(bitmap, 4) + for i := 0; i < 16; i++ { + if i == 4 { + continue + } + assert.False(t, bh.IsSet(bitmap, i), "bit %d must remain unset", i) + } + }) +} + +func TestPopCount(t *testing.T) { + bh := NewBitmapHelper() + + specs := map[string]struct { + bitmap []byte + expCount int + }{ + "empty": {bitmap: []byte{}, expCount: 0}, + "nil": {bitmap: nil, expCount: 0}, + "all zeros": {bitmap: []byte{0x00, 0x00}, expCount: 0}, + "all ones one byte": {bitmap: []byte{0xFF}, expCount: 8}, + "all ones two bytes": {bitmap: []byte{0xFF, 0xFF}, expCount: 16}, + "single bit low": {bitmap: []byte{0x01}, expCount: 1}, + "single bit high": {bitmap: []byte{0x80}, expCount: 1}, + "mixed": {bitmap: []byte{0x0F, 0xF0}, expCount: 8}, + } + + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + assert.Equal(t, spec.expCount, bh.PopCount(spec.bitmap)) + }) + } +} + +func TestOR(t *testing.T) { + bh := NewBitmapHelper() + + t.Run("same length", func(t *testing.T) { + dst := []byte{0x0F, 0x00} + src := []byte{0xF0, 0xAA} + bh.OR(dst, src) + assert.Equal(t, []byte{0xFF, 0xAA}, dst) + }) + + t.Run("src shorter than dst leaves extra bytes untouched", func(t *testing.T) { + dst := []byte{0x00, 0x00, 0xAA} + src := []byte{0x0F} + bh.OR(dst, src) + assert.Equal(t, []byte{0x0F, 0x00, 0xAA}, dst) + }) + + t.Run("dst shorter than src ignores extra src bytes", func(t *testing.T) { + dst := []byte{0x00} + src := []byte{0x0F, 0xFF} + bh.OR(dst, src) + assert.Equal(t, []byte{0x0F}, dst) + }) + + t.Run("OR is commutative on overlap", func(t *testing.T) { + a := []byte{0b11001100} + b := []byte{0b10101010} + expected := []byte{0b11101110} + + cp1 := append([]byte(nil), a...) + bh.OR(cp1, b) + cp2 := append([]byte(nil), b...) + bh.OR(cp2, a) + + assert.Equal(t, expected, cp1) + assert.Equal(t, expected, cp2) + }) +} + +func TestAND(t *testing.T) { + bh := NewBitmapHelper() + + t.Run("same length", func(t *testing.T) { + dst := []byte{0xFF, 0xAA} + src := []byte{0x0F, 0xF0} + bh.AND(dst, src) + assert.Equal(t, []byte{0x0F, 0xA0}, dst) + }) + + t.Run("src shorter than dst leaves extra bytes untouched", func(t *testing.T) { + dst := []byte{0xFF, 0xFF, 0xAA} + src := []byte{0x0F} + bh.AND(dst, src) + assert.Equal(t, []byte{0x0F, 0xFF, 0xAA}, dst) + }) + + t.Run("disjoint bits produce zero", func(t *testing.T) { + dst := []byte{0xF0} + src := []byte{0x0F} + bh.AND(dst, src) + assert.Equal(t, []byte{0x00}, dst) + }) +} + +func TestCopy(t *testing.T) { + bh := NewBitmapHelper() + + t.Run("nil input returns nil", func(t *testing.T) { + assert.Nil(t, bh.Copy(nil)) + }) + + t.Run("empty input returns empty non-nil slice", func(t *testing.T) { + cp := bh.Copy([]byte{}) + require.NotNil(t, cp) + assert.Len(t, cp, 0) + }) + + t.Run("copy is independent of original", func(t *testing.T) { + original := []byte{0x0F, 0xF0} + cp := bh.Copy(original) + assert.Equal(t, original, cp) + + cp[0] = 0xAA + assert.Equal(t, byte(0x0F), original[0], "mutating copy must not affect original") + original[1] = 0xBB + assert.Equal(t, byte(0xF0), cp[1], "mutating original must not affect copy") + }) +} + +func TestClear(t *testing.T) { + bh := NewBitmapHelper() + + t.Run("clears non-empty bitmap", func(t *testing.T) { + bitmap := []byte{0xFF, 0xAA, 0x55} + bh.Clear(bitmap) + assert.Equal(t, []byte{0x00, 0x00, 0x00}, bitmap) + }) + + t.Run("clear on empty bitmap is a no-op", func(t *testing.T) { + bitmap := []byte{} + bh.Clear(bitmap) + assert.Equal(t, []byte{}, bitmap) + }) + + t.Run("clear on nil is a no-op", func(t *testing.T) { + var bitmap []byte + bh.Clear(bitmap) + assert.Nil(t, bitmap) + }) +} + +func TestCountInRange(t *testing.T) { + bh := NewBitmapHelper() + + // bitmap of 16 bits with bits 1, 3, 5, 8, 15 set + bitmap := bh.NewBitmap(16) + for _, idx := range []int{1, 3, 5, 8, 15} { + bh.SetBit(bitmap, idx) + } + + specs := map[string]struct { + start, end int + expCount int + }{ + "full range": {start: 0, end: 16, expCount: 5}, + "first byte only": {start: 0, end: 8, expCount: 3}, + "second byte only": {start: 8, end: 16, expCount: 2}, + "empty range": {start: 4, end: 4, expCount: 0}, + "inverted range is empty": {start: 10, end: 4, expCount: 0}, + "end past bitmap length": {start: 0, end: 1000, expCount: 5}, + "start past bitmap length": {start: 200, end: 300, expCount: 0}, + "range excluding boundary bit": {start: 2, end: 5, expCount: 1}, + "range including boundary bit": {start: 1, end: 6, expCount: 3}, + } + + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + assert.Equal(t, spec.expCount, bh.CountInRange(bitmap, spec.start, spec.end)) + }) + } +} + +func TestBitmapRoundTrip(t *testing.T) { + bh := NewBitmapHelper() + + indices := []int{0, 3, 7, 8, 15, 63, 99} + bitmap := bh.NewBitmap(100) + for _, idx := range indices { + bh.SetBit(bitmap, idx) + } + + assert.Equal(t, len(indices), bh.PopCount(bitmap)) + for _, idx := range indices { + assert.True(t, bh.IsSet(bitmap, idx), "bit %d should be set", idx) + } + + cp := bh.Copy(bitmap) + bh.Clear(bitmap) + assert.Equal(t, 0, bh.PopCount(bitmap)) + assert.Equal(t, len(indices), bh.PopCount(cp), "copy must be unaffected by clearing original") +} diff --git a/modules/network/keeper/genesis_test.go b/modules/network/keeper/genesis_test.go new file mode 100644 index 00000000..3acdbd06 --- /dev/null +++ b/modules/network/keeper/genesis_test.go @@ -0,0 +1,131 @@ +package keeper_test + +import ( + "bytes" + "sort" + "testing" + "time" + + "cosmossdk.io/log" + storetypes "cosmossdk.io/store/types" + cmted25519 "github.com/cometbft/cometbft/crypto/ed25519" + cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" + "github.com/cosmos/cosmos-sdk/codec" + cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" + "github.com/cosmos/cosmos-sdk/runtime" + "github.com/cosmos/cosmos-sdk/testutil/integration" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/stretchr/testify/require" + + "github.com/evstack/ev-abci/modules/network" + "github.com/evstack/ev-abci/modules/network/keeper" + "github.com/evstack/ev-abci/modules/network/types" +) + +func newKeeperForGenesis(t *testing.T) (keeper.Keeper, sdk.Context, codec.BinaryCodec) { + t.Helper() + cdc := moduletestutil.MakeTestEncodingConfig().Codec + keys := storetypes.NewKVStoreKeys(types.StoreKey) + logger := log.NewTestLogger(t) + cms := integration.CreateMultiStore(keys, logger) + authority := authtypes.NewModuleAddress("gov") + k := keeper.NewKeeper(cdc, runtime.NewKVStoreService(keys[types.StoreKey]), nil, nil, nil, authority.String()) + ctx := sdk.NewContext(cms, cmtproto.Header{ChainID: "test-chain", Time: time.Now().UTC(), Height: 1}, false, logger). + WithContext(t.Context()) + return k, ctx, cdc +} + +func mustAnyPubKey(t *testing.T, cmtPk cmted25519.PubKey) *types.AttesterInfo { + t.Helper() + sdkPk, err := cryptocodec.FromCmtPubKeyInterface(cmtPk) + require.NoError(t, err) + info, err := types.NewAttesterInfo( + sdk.AccAddress(cmtPk.Address()).String(), + sdkPk, + 0, + ) + require.NoError(t, err) + return info +} + +func TestInitGenesisLoadsAttesters(t *testing.T) { + k, ctx, _ := newKeeperForGenesis(t) + + pk1 := cmted25519.GenPrivKey().PubKey().(cmted25519.PubKey) + pk2 := cmted25519.GenPrivKey().PubKey().(cmted25519.PubKey) + info1 := mustAnyPubKey(t, pk1) + info1.ConsensusAddress = sdk.ConsAddress(pk1.Address()).String() + info2 := mustAnyPubKey(t, pk2) + info2.ConsensusAddress = sdk.ConsAddress(pk2.Address()).String() + + gs := types.GenesisState{ + Params: types.DefaultParams(), + AttesterInfos: []types.AttesterInfo{*info1, *info2}, + } + + require.NoError(t, network.InitGenesis(ctx, k, gs)) + + // Both in AttesterSet + for _, info := range []*types.AttesterInfo{info1, info2} { + has, err := k.AttesterSet.Has(ctx, info.ConsensusAddress) + require.NoError(t, err) + require.True(t, has) + + stored, err := k.GetAttesterInfo(ctx, info.ConsensusAddress) + require.NoError(t, err) + require.Equal(t, info.Authority, stored.Authority) + } + + // Validator indices assigned in ascending pubkey-address order, power=1 + expectedOrder := []cmted25519.PubKey{pk1, pk2} + sort.Slice(expectedOrder, func(i, j int) bool { + return bytes.Compare(expectedOrder[i].Address(), expectedOrder[j].Address()) < 0 + }) + for i, pk := range expectedOrder { + consAddr := sdk.ConsAddress(pk.Address()).String() + idx, found := k.GetValidatorIndex(ctx, consAddr) + require.True(t, found, "consensus address %s missing index", consAddr) + require.Equal(t, uint16(i), idx) + power, err := k.GetValidatorPower(ctx, idx) + require.NoError(t, err) + require.Equal(t, uint64(1), power) + } +} + +func TestInitGenesisRejectsPubkeyAddressMismatch(t *testing.T) { + k, ctx, _ := newKeeperForGenesis(t) + + pk := cmted25519.GenPrivKey().PubKey().(cmted25519.PubKey) + info := mustAnyPubKey(t, pk) + info.ConsensusAddress = sdk.ConsAddress([]byte("not-matching-20-bytes")).String() + + gs := types.GenesisState{ + Params: types.DefaultParams(), + AttesterInfos: []types.AttesterInfo{*info}, + } + + err := network.InitGenesis(ctx, k, gs) + require.Error(t, err) + require.Contains(t, err.Error(), "pubkey address mismatch") +} + +func TestExportGenesisRoundtripsAttesters(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{*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) +} diff --git a/modules/network/keeper/keeper.go b/modules/network/keeper/keeper.go index 60494d03..df3d8c50 100644 --- a/modules/network/keeper/keeper.go +++ b/modules/network/keeper/keeper.go @@ -185,11 +185,6 @@ func (k Keeper) GetAllAttesters(ctx sdk.Context) ([]string, error) { return attesters, nil } -// MaxAttesters is the maximum number of attesters allowed in the set. -// This prevents unbounded growth, EndBlocker stalling, and uint16 index overflow -// in BuildValidatorIndexMap. -const MaxAttesters = 10_000 - // BuildValidatorIndexMap rebuilds the validator index mapping func (k Keeper) BuildValidatorIndexMap(ctx sdk.Context) error { // Get all attesters instead of bonded validators @@ -198,8 +193,7 @@ func (k Keeper) BuildValidatorIndexMap(ctx sdk.Context) error { return err } - // Guard against uint16 overflow — should not happen if MaxAttesters is enforced - // at join time, but defense-in-depth + // Guard against uint16 overflow (defense-in-depth). if len(attesters) > int(^uint16(0)) { return fmt.Errorf("attester count %d exceeds uint16 max %d", len(attesters), ^uint16(0)) } diff --git a/modules/network/keeper/msg_server.go b/modules/network/keeper/msg_server.go index 2bf7034e..17143579 100644 --- a/modules/network/keeper/msg_server.go +++ b/modules/network/keeper/msg_server.go @@ -161,86 +161,14 @@ 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) { - ctx := sdk.UnwrapSDKContext(goCtx) - - // Validate the consensus address format - _, err := sdk.ValAddressFromBech32(msg.ConsensusAddress) - if err != nil { - return nil, sdkerr.Wrapf(sdkerrors.ErrInvalidAddress, "invalid consensus address: %s", err) - } - - // NOTE: Removed bonded validator requirement to allow any address to join attester set - // This allows external attesters that are not part of the validator set - - // Check if already in attester set (use consensus address) - has, err := k.IsInAttesterSet(ctx, msg.ConsensusAddress) - if err != nil { - return nil, sdkerr.Wrapf(err, "in attester set") - } - if has { - return nil, sdkerr.Wrapf(sdkerrors.ErrInvalidRequest, "consensus address already in attester set") - } - - // Enforce maximum attester set size to prevent unbounded growth and uint16 index overflow - attesters, err := k.GetAllAttesters(ctx) - if err != nil { - return nil, sdkerr.Wrap(err, "get all attesters") - } - if len(attesters) >= MaxAttesters { - return nil, sdkerr.Wrapf(sdkerrors.ErrInvalidRequest, "attester set is full: %d/%d", len(attesters), MaxAttesters) - } - - // Store the attester information including pubkey (key by consensus address) - attesterInfo := &types.AttesterInfo{ - Authority: msg.Authority, - Pubkey: msg.Pubkey, - JoinedHeight: ctx.BlockHeight(), - } - - if err := k.SetAttesterInfo(ctx, msg.ConsensusAddress, attesterInfo); err != nil { - return nil, sdkerr.Wrap(err, "set attester info") - } - // TODO (Alex): the valset should be updated at the end of an epoch only - if err := k.SetAttesterSetMember(ctx, msg.ConsensusAddress); err != nil { - return nil, sdkerr.Wrap(err, "set attester set member") - } - - ctx.EventManager().EmitEvent( - sdk.NewEvent( - types.TypeMsgJoinAttesterSet, - sdk.NewAttribute("consensus_address", msg.ConsensusAddress), - sdk.NewAttribute("authority", msg.Authority), - ), - ) - k.Logger(ctx).Info("+++ joined attester set", "consensus_address", msg.ConsensusAddress, "authority", msg.Authority) - return &types.MsgJoinAttesterSetResponse{}, nil + return nil, sdkerr.Wrap(sdkerrors.ErrInvalidRequest, + "attester set changes disabled; the set is fixed at genesis") } // LeaveAttesterSet handles MsgLeaveAttesterSet func (k msgServer) LeaveAttesterSet(goCtx context.Context, msg *types.MsgLeaveAttesterSet) (*types.MsgLeaveAttesterSetResponse, error) { - ctx := sdk.UnwrapSDKContext(goCtx) - - if err := k.assertValidValidatorAuthority(ctx, msg.ConsensusAddress, msg.Authority); err != nil { - return nil, err - } - - if err := k.AttesterInfo.Remove(ctx, msg.ConsensusAddress); err != nil { - return nil, sdkerr.Wrap(err, "remove attester info") - } - // TODO (Alex): the valset should be updated at the end of an epoch only - if err := k.RemoveAttesterSetMember(ctx, msg.ConsensusAddress); err != nil { - return nil, sdkerr.Wrap(err, "remove attester set member") - } - - ctx.EventManager().EmitEvent( - sdk.NewEvent( - types.TypeMsgLeaveAttesterSet, - sdk.NewAttribute("consensus_address", msg.ConsensusAddress), - sdk.NewAttribute("authority", msg.Authority), - ), - ) - - return &types.MsgLeaveAttesterSetResponse{}, nil + return nil, sdkerr.Wrap(sdkerrors.ErrInvalidRequest, + "attester set changes disabled; the set is fixed at genesis") } 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 a4419612..02c648ba 100644 --- a/modules/network/keeper/msg_server_test.go +++ b/modules/network/keeper/msg_server_test.go @@ -26,106 +26,31 @@ import ( "github.com/evstack/ev-abci/modules/network/types" ) -func TestJoinAttesterSet(t *testing.T) { - myValAddr := sdk.ValAddress("validator4") +func TestJoinAttesterSetDisabled(t *testing.T) { + sk := NewMockStakingKeeper() + server, _, ctx := newTestServer(t, &sk) - type testCase struct { - setup func(t *testing.T, ctx sdk.Context, keeper *Keeper, sk *MockStakingKeeper) - msg *types.MsgJoinAttesterSet - expErr error - expSet bool - } - - tests := map[string]testCase{ - "valid": { - setup: func(t *testing.T, ctx sdk.Context, keeper *Keeper, sk *MockStakingKeeper) { - validator := stakingtypes.Validator{ - OperatorAddress: myValAddr.String(), - Status: stakingtypes.Bonded, - } - err := sk.SetValidator(ctx, validator) - require.NoError(t, err, "failed to set validator") - }, - msg: &types.MsgJoinAttesterSet{Authority: myValAddr.String(), ConsensusAddress: myValAddr.String()}, - expSet: true, - }, - "invalid_addr": { - setup: func(t *testing.T, ctx sdk.Context, keeper *Keeper, sk *MockStakingKeeper) {}, - msg: &types.MsgJoinAttesterSet{Authority: "invalidAddr", ConsensusAddress: "invalidAddr"}, - expErr: sdkerrors.ErrInvalidAddress, - }, - "already set": { - setup: func(t *testing.T, ctx sdk.Context, keeper *Keeper, sk *MockStakingKeeper) { - validator := stakingtypes.Validator{ - OperatorAddress: myValAddr.String(), - Status: stakingtypes.Bonded, - } - require.NoError(t, sk.SetValidator(ctx, validator)) - require.NoError(t, keeper.SetAttesterSetMember(ctx, myValAddr.String())) - }, - msg: &types.MsgJoinAttesterSet{Authority: myValAddr.String(), ConsensusAddress: myValAddr.String()}, - expErr: sdkerrors.ErrInvalidRequest, - expSet: true, - }, - } - - for name, spec := range tests { - t.Run(name, func(t *testing.T) { - sk := NewMockStakingKeeper() - server, keeper, ctx := newTestServer(t, &sk) - - spec.setup(t, ctx, &keeper, &sk) - - // when - rsp, err := server.JoinAttesterSet(ctx, spec.msg) - // then - if spec.expErr != nil { - require.ErrorIs(t, err, spec.expErr) - require.Nil(t, rsp) - exists, gotErr := keeper.AttesterSet.Has(ctx, spec.msg.ConsensusAddress) - require.NoError(t, gotErr) - assert.Equal(t, exists, spec.expSet) - return - } - require.NoError(t, err) - require.NotNil(t, rsp) - exists, gotErr := keeper.AttesterSet.Has(ctx, spec.msg.ConsensusAddress) - require.NoError(t, gotErr) - assert.True(t, exists) - - // Verify authority is stored correctly in AttesterInfo - info, infoErr := keeper.GetAttesterInfo(ctx, spec.msg.ConsensusAddress) - require.NoError(t, infoErr) - assert.Equal(t, spec.msg.Authority, info.Authority) - }) + 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) } -func TestJoinAttesterSetMaxCap(t *testing.T) { - // Verify the constant is set to a sane value that is within uint16 range - require.LessOrEqual(t, MaxAttesters, int(^uint16(0)), - "MaxAttesters must fit in uint16 to avoid index overflow in BuildValidatorIndexMap") - - t.Run("join succeeds under cap", func(t *testing.T) { - sk := NewMockStakingKeeper() - server, keeper, ctx := newTestServer(t, &sk) +func TestLeaveAttesterSetDisabled(t *testing.T) { + sk := NewMockStakingKeeper() + server, _, ctx := newTestServer(t, &sk) - // With an empty set, join should succeed - newAddr := sdk.ValAddress("new_attester") - msg := &types.MsgJoinAttesterSet{ - Authority: newAddr.String(), - ConsensusAddress: newAddr.String(), - } - - rsp, err := server.JoinAttesterSet(ctx, msg) - require.NoError(t, err) - require.NotNil(t, rsp) - - // Verify the attester was added - exists, err := keeper.AttesterSet.Has(ctx, newAddr.String()) - require.NoError(t, err) - assert.True(t, exists) - }) + msg := &types.MsgLeaveAttesterSet{ + Authority: sdk.AccAddress([]byte("any-authority-20b")).String(), + ConsensusAddress: sdk.ConsAddress([]byte("any-cons-addr-20-b")).String(), + } + rsp, err := server.LeaveAttesterSet(ctx, msg) + require.ErrorIs(t, err, sdkerrors.ErrInvalidRequest) + require.Nil(t, rsp) } func TestAttestVotePayloadValidation(t *testing.T) { @@ -183,83 +108,6 @@ func TestAttestVotePayloadValidation(t *testing.T) { } } -func TestLeaveAttesterSet(t *testing.T) { - ownerAddr := sdk.ValAddress("owner1") - otherAddr := sdk.ValAddress("other1") - - type testCase struct { - setup func(t *testing.T, ctx sdk.Context, keeper *Keeper, server msgServer) - msg *types.MsgLeaveAttesterSet - expErr error - } - - tests := map[string]testCase{ - "valid": { - setup: func(t *testing.T, ctx sdk.Context, keeper *Keeper, server msgServer) { - t.Helper() - joinMsg := &types.MsgJoinAttesterSet{ - Authority: ownerAddr.String(), - ConsensusAddress: ownerAddr.String(), - } - _, err := server.JoinAttesterSet(ctx, joinMsg) - require.NoError(t, err) - }, - msg: &types.MsgLeaveAttesterSet{ - Authority: ownerAddr.String(), - ConsensusAddress: ownerAddr.String(), - }, - }, - "not_in_set": { - setup: func(t *testing.T, ctx sdk.Context, keeper *Keeper, server msgServer) { - t.Helper() - }, - msg: &types.MsgLeaveAttesterSet{ - Authority: ownerAddr.String(), - ConsensusAddress: ownerAddr.String(), - }, - expErr: sdkerrors.ErrUnauthorized, - }, - "wrong_authority": { - setup: func(t *testing.T, ctx sdk.Context, keeper *Keeper, server msgServer) { - t.Helper() - joinMsg := &types.MsgJoinAttesterSet{ - Authority: ownerAddr.String(), - ConsensusAddress: ownerAddr.String(), - } - _, err := server.JoinAttesterSet(ctx, joinMsg) - require.NoError(t, err) - }, - msg: &types.MsgLeaveAttesterSet{ - Authority: otherAddr.String(), - ConsensusAddress: ownerAddr.String(), - }, - expErr: sdkerrors.ErrUnauthorized, - }, - } - - for name, spec := range tests { - t.Run(name, func(t *testing.T) { - sk := NewMockStakingKeeper() - server, keeper, ctx := newTestServer(t, &sk) - - spec.setup(t, ctx, &keeper, server) - - rsp, err := server.LeaveAttesterSet(ctx, spec.msg) - if spec.expErr != nil { - require.ErrorIs(t, err, spec.expErr) - require.Nil(t, rsp) - return - } - require.NoError(t, err) - require.NotNil(t, rsp) - - // Verify actually removed from attester set - exists, gotErr := keeper.AttesterSet.Has(ctx, spec.msg.ConsensusAddress) - require.NoError(t, gotErr) - assert.False(t, exists) - }) - } -} func TestAttest(t *testing.T) { ownerAddr := sdk.ValAddress("attester_owner") diff --git a/modules/network/types/attester.go b/modules/network/types/attester.go new file mode 100644 index 00000000..ebfd544c --- /dev/null +++ b/modules/network/types/attester.go @@ -0,0 +1,40 @@ +package types + +import ( + "fmt" + + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func NewAttesterInfo(authority string, pk cryptotypes.PubKey, joinedHeight int64) (*AttesterInfo, error) { + any, err := codectypes.NewAnyWithValue(pk) + if err != nil { + return nil, fmt.Errorf("pack pubkey: %w", err) + } + return &AttesterInfo{ + Authority: authority, + Pubkey: any, + JoinedHeight: joinedHeight, + ConsensusAddress: sdk.ConsAddress(pk.Address()).String(), + }, nil +} + +// GetPubKey extracts the cryptotypes.PubKey from the Any field. +func (a AttesterInfo) GetPubKey() (cryptotypes.PubKey, error) { + if a.Pubkey == nil { + return nil, fmt.Errorf("pubkey not set") + } + pk, ok := a.Pubkey.GetCachedValue().(cryptotypes.PubKey) + if ok { + return pk, nil + } + return nil, fmt.Errorf("pubkey cached value not cryptotypes.PubKey") +} + +// UnpackInterfaces ensures GetCachedValue works after unmarshaling. +func (a AttesterInfo) UnpackInterfaces(unpacker codectypes.AnyUnpacker) error { + var pk cryptotypes.PubKey + return unpacker.UnpackAny(a.Pubkey, &pk) +} diff --git a/modules/network/types/attester.pb.go b/modules/network/types/attester.pb.go index 361f5e93..40896217 100644 --- a/modules/network/types/attester.pb.go +++ b/modules/network/types/attester.pb.go @@ -33,6 +33,10 @@ type AttesterInfo struct { Pubkey *types.Any `protobuf:"bytes,2,opt,name=pubkey,proto3" json:"pubkey,omitempty"` // joined_height is the height at which the attester joined JoinedHeight int64 `protobuf:"varint,3,opt,name=joined_height,json=joinedHeight,proto3" json:"joined_height,omitempty"` + // consensus_address is the bech32 cosmosvalcons1... derived from pubkey. + // Redundant with pubkey but persisted so the keeper's collections key + // (consensus address) matches the stored struct. + ConsensusAddress string `protobuf:"bytes,4,opt,name=consensus_address,json=consensusAddress,proto3" json:"consensus_address,omitempty"` } func (m *AttesterInfo) Reset() { *m = AttesterInfo{} } @@ -75,28 +79,30 @@ func init() { func init() { proto.RegisterFile("evabci/network/v1/attester.proto", fileDescriptor_a8fe3a2e81f284b4) } var fileDescriptor_a8fe3a2e81f284b4 = []byte{ - // 327 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x44, 0x90, 0xbb, 0x4e, 0xc3, 0x30, - 0x14, 0x86, 0x63, 0x2a, 0x55, 0x34, 0x94, 0x81, 0xa8, 0x43, 0xda, 0x21, 0x8d, 0x60, 0xe9, 0x52, - 0x9b, 0x82, 0xc4, 0xc0, 0xd6, 0x0e, 0x88, 0xcb, 0x82, 0xca, 0xc6, 0x52, 0xe5, 0x72, 0x9a, 0x84, - 0xb6, 0x39, 0x91, 0xed, 0x04, 0xf9, 0x0d, 0x18, 0x79, 0x84, 0x3e, 0x04, 0x2b, 0x3b, 0x62, 0xaa, - 0x98, 0x18, 0x51, 0xbb, 0xf0, 0x18, 0x88, 0x38, 0xd0, 0xcd, 0xe7, 0xf3, 0xe7, 0xe3, 0x5f, 0xbf, - 0xe9, 0x42, 0xe1, 0xf9, 0x41, 0xc2, 0x52, 0x90, 0x8f, 0xc8, 0x67, 0xac, 0x18, 0x30, 0x4f, 0x4a, - 0x10, 0x12, 0x38, 0xcd, 0x38, 0x4a, 0xb4, 0x0e, 0xb4, 0x41, 0x2b, 0x83, 0x16, 0x83, 0x4e, 0x3b, - 0x42, 0x8c, 0xe6, 0xc0, 0x4a, 0xc1, 0xcf, 0xa7, 0xcc, 0x4b, 0x95, 0xb6, 0x3b, 0xed, 0x00, 0xc5, - 0x02, 0xc5, 0xa4, 0x9c, 0x98, 0x1e, 0xaa, 0xab, 0x56, 0x84, 0x11, 0x6a, 0xfe, 0x7b, 0xd2, 0xf4, - 0xf0, 0x95, 0x98, 0xcd, 0x61, 0xf5, 0xe3, 0x55, 0x3a, 0x45, 0xeb, 0xcc, 0x6c, 0x78, 0xb9, 0x8c, - 0x91, 0x27, 0x52, 0xd9, 0xc4, 0x25, 0xbd, 0xc6, 0xc8, 0xfe, 0x78, 0xe9, 0xb7, 0xaa, 0x5d, 0xc3, - 0x30, 0xe4, 0x20, 0xc4, 0x9d, 0xe4, 0x49, 0x1a, 0x8d, 0xb7, 0xaa, 0x75, 0x61, 0xd6, 0xb3, 0xdc, - 0x9f, 0x81, 0xb2, 0x77, 0x5c, 0xd2, 0xdb, 0x3b, 0x69, 0x51, 0x9d, 0x92, 0xfe, 0xa5, 0xa4, 0xc3, - 0x54, 0x8d, 0xec, 0xf7, 0xed, 0xaa, 0x80, 0xab, 0x4c, 0x22, 0xbd, 0xcd, 0xfd, 0x1b, 0x50, 0xe3, - 0xea, 0xb5, 0x75, 0x64, 0xee, 0x3f, 0x60, 0x92, 0x42, 0x38, 0x89, 0x21, 0x89, 0x62, 0x69, 0xd7, - 0x5c, 0xd2, 0xab, 0x8d, 0x9b, 0x1a, 0x5e, 0x96, 0xec, 0x7c, 0xf7, 0x69, 0xd9, 0x35, 0xbe, 0x97, - 0x5d, 0x63, 0x74, 0xfd, 0xb6, 0x76, 0xc8, 0x6a, 0xed, 0x90, 0xaf, 0xb5, 0x43, 0x9e, 0x37, 0x8e, - 0xb1, 0xda, 0x38, 0xc6, 0xe7, 0xc6, 0x31, 0xee, 0x8f, 0xa3, 0x44, 0xc6, 0xb9, 0x4f, 0x03, 0x5c, - 0x30, 0x28, 0x84, 0xf4, 0x82, 0x19, 0x83, 0xa2, 0x5f, 0xd6, 0xbd, 0xc0, 0x30, 0x9f, 0x83, 0xf8, - 0xaf, 0x5d, 0xaa, 0x0c, 0x84, 0x5f, 0x2f, 0xa3, 0x9e, 0xfe, 0x04, 0x00, 0x00, 0xff, 0xff, 0xcf, - 0x03, 0xda, 0x4b, 0x95, 0x01, 0x00, 0x00, + // 355 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x84, 0x91, 0xbd, 0x4e, 0xe3, 0x40, + 0x10, 0xc7, 0xed, 0xe4, 0x14, 0x5d, 0x7c, 0x39, 0xe9, 0x62, 0xa5, 0x70, 0x52, 0x38, 0xd6, 0xd1, + 0xa4, 0xc9, 0x2e, 0x01, 0x89, 0x82, 0x2e, 0x91, 0x40, 0x7c, 0x34, 0x28, 0x74, 0x34, 0x91, 0x3f, + 0x26, 0xb6, 0x49, 0xb2, 0x63, 0x79, 0xd7, 0x46, 0xdb, 0x53, 0x50, 0xf2, 0x08, 0x79, 0x08, 0x1e, + 0x02, 0x51, 0x45, 0x54, 0x94, 0x28, 0x69, 0x78, 0x0c, 0x84, 0xd7, 0x24, 0x25, 0xdd, 0xce, 0x6f, + 0x7f, 0x33, 0xfb, 0x5f, 0x8d, 0xe1, 0x40, 0xee, 0x7a, 0x7e, 0x4c, 0x19, 0x88, 0x3b, 0x4c, 0x67, + 0x34, 0x1f, 0x50, 0x57, 0x08, 0xe0, 0x02, 0x52, 0x92, 0xa4, 0x28, 0xd0, 0x6c, 0x2a, 0x83, 0x94, + 0x06, 0xc9, 0x07, 0x9d, 0x76, 0x88, 0x18, 0xce, 0x81, 0x16, 0x82, 0x97, 0x4d, 0xa9, 0xcb, 0xa4, + 0xb2, 0x3b, 0x6d, 0x1f, 0xf9, 0x02, 0xf9, 0xa4, 0xa8, 0xa8, 0x2a, 0xca, 0xab, 0x56, 0x88, 0x21, + 0x2a, 0xfe, 0x75, 0x52, 0xf4, 0xff, 0x7d, 0xc5, 0x68, 0x0c, 0xcb, 0x17, 0xcf, 0xd9, 0x14, 0xcd, + 0x23, 0xa3, 0xee, 0x66, 0x22, 0xc2, 0x34, 0x16, 0xd2, 0xd2, 0x1d, 0xbd, 0x57, 0x1f, 0x59, 0xaf, + 0x4f, 0xfd, 0x56, 0x39, 0x6b, 0x18, 0x04, 0x29, 0x70, 0x7e, 0x2d, 0xd2, 0x98, 0x85, 0xe3, 0x9d, + 0x6a, 0x9e, 0x1a, 0xb5, 0x24, 0xf3, 0x66, 0x20, 0xad, 0x8a, 0xa3, 0xf7, 0xfe, 0x1c, 0xb4, 0x88, + 0x4a, 0x49, 0xbe, 0x53, 0x92, 0x21, 0x93, 0x23, 0xeb, 0x65, 0x37, 0xca, 0x4f, 0x65, 0x22, 0x90, + 0x5c, 0x65, 0xde, 0x25, 0xc8, 0x71, 0xd9, 0x6d, 0xee, 0x19, 0x7f, 0x6f, 0x31, 0x66, 0x10, 0x4c, + 0x22, 0x88, 0xc3, 0x48, 0x58, 0x55, 0x47, 0xef, 0x55, 0xc7, 0x0d, 0x05, 0xcf, 0x0a, 0x66, 0x9e, + 0x18, 0x4d, 0x1f, 0x19, 0x07, 0xc6, 0x33, 0x3e, 0x71, 0x55, 0x24, 0xeb, 0xd7, 0x0f, 0x61, 0xff, + 0x6d, 0x5b, 0x4a, 0x7e, 0xfc, 0xfb, 0x61, 0xd9, 0xd5, 0x3e, 0x96, 0x5d, 0x6d, 0x74, 0xf1, 0xbc, + 0xb6, 0xf5, 0xd5, 0xda, 0xd6, 0xdf, 0xd7, 0xb6, 0xfe, 0xb8, 0xb1, 0xb5, 0xd5, 0xc6, 0xd6, 0xde, + 0x36, 0xb6, 0x76, 0xb3, 0x1f, 0xc6, 0x22, 0xca, 0x3c, 0xe2, 0xe3, 0x82, 0x42, 0xce, 0x85, 0xeb, + 0xcf, 0x28, 0xe4, 0xfd, 0x62, 0x6b, 0x0b, 0x0c, 0xb2, 0x39, 0xf0, 0xed, 0xf6, 0x84, 0x4c, 0x80, + 0x7b, 0xb5, 0xe2, 0xc7, 0x87, 0x9f, 0x01, 0x00, 0x00, 0xff, 0xff, 0x29, 0xc3, 0xde, 0xe5, 0xdc, + 0x01, 0x00, 0x00, } func (m *AttesterInfo) Marshal() (dAtA []byte, err error) { @@ -119,6 +125,13 @@ func (m *AttesterInfo) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if len(m.ConsensusAddress) > 0 { + i -= len(m.ConsensusAddress) + copy(dAtA[i:], m.ConsensusAddress) + i = encodeVarintAttester(dAtA, i, uint64(len(m.ConsensusAddress))) + i-- + dAtA[i] = 0x22 + } if m.JoinedHeight != 0 { i = encodeVarintAttester(dAtA, i, uint64(m.JoinedHeight)) i-- @@ -174,6 +187,10 @@ func (m *AttesterInfo) Size() (n int) { if m.JoinedHeight != 0 { n += 1 + sovAttester(uint64(m.JoinedHeight)) } + l = len(m.ConsensusAddress) + if l > 0 { + n += 1 + l + sovAttester(uint64(l)) + } return n } @@ -299,6 +316,38 @@ func (m *AttesterInfo) Unmarshal(dAtA []byte) error { break } } + case 4: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ConsensusAddress", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowAttester + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthAttester + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthAttester + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.ConsensusAddress = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipAttester(dAtA[iNdEx:]) diff --git a/modules/network/types/genesis.go b/modules/network/types/genesis.go index c722e021..3a071e68 100644 --- a/modules/network/types/genesis.go +++ b/modules/network/types/genesis.go @@ -3,8 +3,22 @@ package types import ( "fmt" "math" + + codectypes "github.com/cosmos/cosmos-sdk/codec/types" ) +// UnpackInterfaces ensures AttesterInfo.Pubkey Any values have their cached +// concrete value populated after genesis JSON unmarshaling. Without this, the +// cosmos-sdk codec leaves the Any unresolved and GetPubKey() returns an error. +func (gs GenesisState) UnpackInterfaces(unpacker codectypes.AnyUnpacker) error { + for i := range gs.AttesterInfos { + if err := gs.AttesterInfos[i].UnpackInterfaces(unpacker); err != nil { + return fmt.Errorf("unpack attester %d: %w", i, err) + } + } + return nil +} + // DefaultGenesisState returns the default genesis state func DefaultGenesisState() *GenesisState { return &GenesisState{ diff --git a/modules/network/types/genesis.pb.go b/modules/network/types/genesis.pb.go index 539c86ad..020b5c99 100644 --- a/modules/network/types/genesis.pb.go +++ b/modules/network/types/genesis.pb.go @@ -31,6 +31,9 @@ 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). + AttesterInfos []AttesterInfo `protobuf:"bytes,4,rep,name=attester_infos,json=attesterInfos,proto3" json:"attester_infos"` } func (m *GenesisState) Reset() { *m = GenesisState{} } @@ -87,6 +90,13 @@ func (m *GenesisState) GetAttestationBitmaps() []AttestationBitmap { return nil } +func (m *GenesisState) GetAttesterInfos() []AttesterInfo { + if m != nil { + return m.AttesterInfos + } + return nil +} + func init() { proto.RegisterType((*GenesisState)(nil), "evabci.network.v1.GenesisState") } @@ -94,26 +104,28 @@ func init() { func init() { proto.RegisterFile("evabci/network/v1/genesis.proto", fileDescriptor_58e10cce12d8f51a) } var fileDescriptor_58e10cce12d8f51a = []byte{ - // 298 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0xd0, 0xbf, 0x4e, 0xeb, 0x30, - 0x14, 0xc7, 0xf1, 0xf8, 0xf6, 0xaa, 0x43, 0xca, 0x40, 0x03, 0x43, 0xa9, 0x84, 0x5b, 0x10, 0x43, - 0x17, 0x6c, 0x5a, 0x06, 0x66, 0xb2, 0x20, 0x98, 0x10, 0x20, 0x06, 0x18, 0x2a, 0x27, 0x39, 0x0a, - 0x56, 0x9b, 0x38, 0x8a, 0x4f, 0x0d, 0xbc, 0x05, 0x8f, 0xd5, 0xb1, 0x23, 0x13, 0x42, 0xc9, 0x5b, - 0x30, 0xa1, 0x3a, 0xe1, 0x8f, 0x68, 0xb7, 0x28, 0xe7, 0xab, 0x8f, 0xa5, 0x9f, 0xdb, 0x03, 0x23, - 0x82, 0x50, 0xf2, 0x14, 0xf0, 0x51, 0xe5, 0x13, 0x6e, 0x86, 0x3c, 0x86, 0x14, 0xb4, 0xd4, 0x2c, - 0xcb, 0x15, 0x2a, 0xaf, 0x5d, 0x05, 0xac, 0x0e, 0x98, 0x19, 0x76, 0xb7, 0x63, 0x15, 0x2b, 0x7b, - 0xe5, 0xcb, 0xaf, 0x2a, 0xec, 0xee, 0xae, 0x4a, 0xf8, 0x9c, 0x41, 0xed, 0xec, 0x7f, 0x10, 0x77, - 0xe3, 0xac, 0x92, 0xaf, 0x51, 0x20, 0x78, 0x27, 0x6e, 0x33, 0x13, 0xb9, 0x48, 0x74, 0x87, 0xf4, - 0xc9, 0xa0, 0x35, 0xda, 0x61, 0x2b, 0x2f, 0xb1, 0x4b, 0x1b, 0xf8, 0xff, 0xe7, 0x6f, 0x3d, 0xe7, - 0xaa, 0xce, 0xbd, 0x1b, 0xb7, 0x6d, 0xc4, 0x54, 0x46, 0x02, 0x55, 0x3e, 0x96, 0x69, 0x24, 0x43, - 0xd0, 0x9d, 0x7f, 0xfd, 0xc6, 0xa0, 0x35, 0xda, 0x5b, 0x63, 0xdc, 0x7e, 0xb5, 0xe7, 0x69, 0x04, - 0x4f, 0xb5, 0xb5, 0x69, 0x7e, 0xfd, 0x5d, 0x02, 0xde, 0xbd, 0xbb, 0x25, 0x10, 0x41, 0xa3, 0x40, - 0xa9, 0xd2, 0x71, 0x20, 0x31, 0x11, 0x99, 0xee, 0x34, 0xac, 0x7b, 0xb0, 0xc6, 0x3d, 0xfd, 0xa9, - 0x7d, 0x1b, 0xd7, 0xb4, 0x27, 0xfe, 0x1e, 0xb4, 0x7f, 0x31, 0x2f, 0x28, 0x59, 0x14, 0x94, 0xbc, - 0x17, 0x94, 0xbc, 0x94, 0xd4, 0x59, 0x94, 0xd4, 0x79, 0x2d, 0xa9, 0x73, 0x77, 0x14, 0x4b, 0x7c, - 0x98, 0x05, 0x2c, 0x54, 0x09, 0x07, 0xa3, 0x51, 0x84, 0x13, 0x0e, 0xe6, 0xd0, 0x2e, 0x99, 0xa8, - 0x68, 0x36, 0x05, 0xfd, 0xbd, 0xa8, 0x9d, 0x33, 0x68, 0xda, 0x3d, 0x8f, 0x3f, 0x03, 0x00, 0x00, - 0xff, 0xff, 0x66, 0xac, 0x9a, 0x06, 0xba, 0x01, 0x00, 0x00, + // 335 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x74, 0x91, 0xb1, 0x4f, 0xfa, 0x40, + 0x14, 0xc7, 0x5b, 0x20, 0x0c, 0xe5, 0xf7, 0x33, 0x52, 0x1d, 0x2a, 0x89, 0x05, 0x8d, 0x03, 0x8b, + 0xad, 0xe0, 0xe0, 0x2c, 0x8b, 0xc1, 0x38, 0x18, 0x35, 0x0e, 0x3a, 0x90, 0x6b, 0xfb, 0xa8, 0x17, + 0xe8, 0x5d, 0xd3, 0x7b, 0x9c, 0xfa, 0x5f, 0xf8, 0x67, 0x31, 0x32, 0x3a, 0x19, 0x03, 0xff, 0x88, + 0xe1, 0x7a, 0x28, 0xb1, 0xb8, 0x5d, 0xde, 0xf7, 0x93, 0xcf, 0xbd, 0xbc, 0xaf, 0xd5, 0x04, 0x49, + 0x82, 0x90, 0xfa, 0x0c, 0xf0, 0x99, 0x67, 0x23, 0x5f, 0x76, 0xfc, 0x18, 0x18, 0x08, 0x2a, 0xbc, + 0x34, 0xe3, 0xc8, 0xed, 0x7a, 0x0e, 0x78, 0x1a, 0xf0, 0x64, 0xa7, 0xb1, 0x1b, 0xf3, 0x98, 0xab, + 0xd4, 0x5f, 0xbe, 0x72, 0xb0, 0xb1, 0x5f, 0x34, 0xe1, 0x6b, 0x0a, 0xda, 0xd3, 0x68, 0x15, 0x63, + 0x82, 0x08, 0x02, 0x21, 0xcb, 0x89, 0xc3, 0x69, 0xc9, 0xfa, 0x77, 0x91, 0xff, 0x7d, 0x8b, 0x04, + 0xc1, 0x3e, 0xb3, 0xaa, 0x29, 0xc9, 0x48, 0x22, 0x1c, 0xb3, 0x65, 0xb6, 0x6b, 0xdd, 0x3d, 0xaf, + 0xb0, 0x8b, 0x77, 0xad, 0x80, 0x5e, 0x65, 0xfa, 0xd1, 0x34, 0x6e, 0x34, 0x6e, 0xdf, 0x59, 0x75, + 0x49, 0xc6, 0x34, 0x22, 0xc8, 0xb3, 0x01, 0x65, 0x11, 0x0d, 0x41, 0x38, 0xa5, 0x56, 0xb9, 0x5d, + 0xeb, 0x1e, 0x6c, 0x70, 0xdc, 0xaf, 0xd8, 0x3e, 0x8b, 0xe0, 0x45, 0xbb, 0xb6, 0xe5, 0xda, 0x74, + 0x29, 0xb0, 0x1f, 0xad, 0x9d, 0x7c, 0x63, 0x82, 0x94, 0xb3, 0x41, 0x40, 0x31, 0x21, 0xa9, 0x70, + 0xca, 0xca, 0x7b, 0xb4, 0xc1, 0x7b, 0xfe, 0x43, 0xf7, 0x14, 0xac, 0xd5, 0x36, 0xf9, 0x1d, 0x08, + 0xfb, 0xca, 0xda, 0x5a, 0x9d, 0x63, 0x40, 0xd9, 0x90, 0x0b, 0xa7, 0xa2, 0xbc, 0xcd, 0x3f, 0xbd, + 0x90, 0xf5, 0xd9, 0x90, 0x6b, 0xe5, 0x7f, 0xb2, 0x36, 0x13, 0xbd, 0xcb, 0xe9, 0xdc, 0x35, 0x67, + 0x73, 0xd7, 0xfc, 0x9c, 0xbb, 0xe6, 0xdb, 0xc2, 0x35, 0x66, 0x0b, 0xd7, 0x78, 0x5f, 0xb8, 0xc6, + 0xc3, 0x49, 0x4c, 0xf1, 0x69, 0x12, 0x78, 0x21, 0x4f, 0x7c, 0x90, 0x02, 0x49, 0x38, 0xf2, 0x41, + 0x1e, 0xab, 0x6a, 0x12, 0x1e, 0x4d, 0xc6, 0x20, 0xbe, 0x2b, 0x52, 0xf5, 0x05, 0x55, 0xd5, 0xce, + 0xe9, 0x57, 0x00, 0x00, 0x00, 0xff, 0xff, 0x34, 0x07, 0x1b, 0x4a, 0x2a, 0x02, 0x00, 0x00, } func (m *GenesisState) Marshal() (dAtA []byte, err error) { @@ -136,6 +148,20 @@ func (m *GenesisState) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if len(m.AttesterInfos) > 0 { + for iNdEx := len(m.AttesterInfos) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.AttesterInfos[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenesis(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x22 + } + } if len(m.AttestationBitmaps) > 0 { for iNdEx := len(m.AttestationBitmaps) - 1; iNdEx >= 0; iNdEx-- { { @@ -208,6 +234,12 @@ func (m *GenesisState) Size() (n int) { n += 1 + l + sovGenesis(uint64(l)) } } + if len(m.AttesterInfos) > 0 { + for _, e := range m.AttesterInfos { + l = e.Size() + n += 1 + l + sovGenesis(uint64(l)) + } + } return n } @@ -347,6 +379,40 @@ func (m *GenesisState) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex + case 4: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field AttesterInfos", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.AttesterInfos = append(m.AttesterInfos, AttesterInfo{}) + if err := m.AttesterInfos[len(m.AttesterInfos)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipGenesis(dAtA[iNdEx:]) diff --git a/modules/proto/evabci/network/v1/attester.proto b/modules/proto/evabci/network/v1/attester.proto index e54e3a5d..e7d4f9d7 100644 --- a/modules/proto/evabci/network/v1/attester.proto +++ b/modules/proto/evabci/network/v1/attester.proto @@ -21,4 +21,9 @@ message AttesterInfo { // joined_height is the height at which the attester joined int64 joined_height = 3; + + // consensus_address is the bech32 cosmosvalcons1... derived from pubkey. + // Redundant with pubkey but persisted so the keeper's collections key + // (consensus address) matches the stored struct. + string consensus_address = 4 [(cosmos_proto.scalar) = "cosmos.AddressString"]; } \ No newline at end of file diff --git a/modules/proto/evabci/network/v1/genesis.proto b/modules/proto/evabci/network/v1/genesis.proto index 473c514a..8098c25b 100644 --- a/modules/proto/evabci/network/v1/genesis.proto +++ b/modules/proto/evabci/network/v1/genesis.proto @@ -6,6 +6,7 @@ option go_package = "github.com/evstack/ev-abci/modules/network/types"; import "gogoproto/gogo.proto"; import "evabci/network/v1/types.proto"; +import "evabci/network/v1/attester.proto"; // GenesisState defines the network module's genesis state. message GenesisState { @@ -17,4 +18,8 @@ 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). + repeated AttesterInfo attester_infos = 4 [(gogoproto.nullable) = false]; } From 64064acd3e508163f20e63118e6ccf2d2e8e2f85 Mon Sep 17 00:00:00 2001 From: Randy Grok Date: Tue, 28 Apr 2026 17:03:24 +0200 Subject: [PATCH 2/3] refactor(network): keep attester set fixed after genesis --- modules/network/keeper/abci.go | 9 +++-- modules/network/keeper/keeper.go | 40 ----------------------- modules/network/keeper/msg_server_test.go | 36 +++++++------------- 3 files changed, 15 insertions(+), 70 deletions(-) diff --git a/modules/network/keeper/abci.go b/modules/network/keeper/abci.go index f212f5b7..81dcc108 100644 --- a/modules/network/keeper/abci.go +++ b/modules/network/keeper/abci.go @@ -112,14 +112,13 @@ func (k Keeper) processEpochEnd(ctx sdk.Context, epoch uint64) error { } } - // todo (Alex): find a way to prune only bitmaps that are not used anymore + // Validator indices are established at genesis and never mutate at runtime + // (MsgJoin/MsgLeave are disabled). Nothing to rebuild here. + + // todo: find a way to prune only bitmaps that are not used anymore // if err := k.PruneOldBitmaps(ctx, epoch); err != nil { // return fmt.Errorf("pruning old data at epoch %d: %w", epoch, err) // } - - if err := k.BuildValidatorIndexMap(ctx); err != nil { - return fmt.Errorf("rebuilding validator index map at epoch %d: %w", epoch, err) - } return nil } diff --git a/modules/network/keeper/keeper.go b/modules/network/keeper/keeper.go index df3d8c50..206d1163 100644 --- a/modules/network/keeper/keeper.go +++ b/modules/network/keeper/keeper.go @@ -185,46 +185,6 @@ func (k Keeper) GetAllAttesters(ctx sdk.Context) ([]string, error) { return attesters, nil } -// BuildValidatorIndexMap rebuilds the validator index mapping -func (k Keeper) BuildValidatorIndexMap(ctx sdk.Context) error { - // Get all attesters instead of bonded validators - attesters, err := k.GetAllAttesters(ctx) - if err != nil { - return err - } - - // Guard against uint16 overflow (defense-in-depth). - if len(attesters) > int(^uint16(0)) { - return fmt.Errorf("attester count %d exceeds uint16 max %d", len(attesters), ^uint16(0)) - } - - // Clear existing indices and powers - // The `nil` range clears all entries in the collection. - if err := k.ValidatorIndex.Clear(ctx, nil); err != nil { - k.Logger(ctx).Error("failed to clear validator index", "error", err) - return err - } - if err := k.ValidatorPower.Clear(ctx, nil); err != nil { - k.Logger(ctx).Error("failed to clear validator power", "error", err) - return err - } - - // Build new indices for all attesters with voting power of 1 - index := uint16(0) - for _, attesterAddr := range attesters { - power := uint64(1) // Assign voting power of 1 to all attesters - if err := k.SetValidatorIndex(ctx, attesterAddr, index, power); err != nil { - // Consider how to handle partial failures; potentially log and continue or return error. - k.Logger(ctx).Error("failed to set validator index during build", "attester", attesterAddr, "error", err) - return err - } - k.Logger(ctx).Debug("assigned index to attester", "attester", attesterAddr, "index", index, "power", power) - index++ - } - k.Logger(ctx).Info("rebuilt validator index map for attesters", "count", len(attesters)) - return nil -} - // GetCurrentEpoch returns the current epoch number func (k Keeper) GetCurrentEpoch(ctx sdk.Context) uint64 { params := k.GetParams(ctx) diff --git a/modules/network/keeper/msg_server_test.go b/modules/network/keeper/msg_server_test.go index 02c648ba..9b3f99a9 100644 --- a/modules/network/keeper/msg_server_test.go +++ b/modules/network/keeper/msg_server_test.go @@ -20,7 +20,6 @@ import ( moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/evstack/ev-abci/modules/network/types" @@ -85,9 +84,9 @@ func TestAttestVotePayloadValidation(t *testing.T) { sk := NewMockStakingKeeper() server, keeper, ctx := newTestServer(t, &sk) require.NoError(t, keeper.SetParams(ctx, types.DefaultParams())) - require.NoError(t, keeper.SetAttesterSetMember(ctx, myValAddr.String())) require.NoError(t, keeper.SetAttesterInfo(ctx, myValAddr.String(), &types.AttesterInfo{Authority: myValAddr.String()})) - require.NoError(t, keeper.BuildValidatorIndexMap(ctx)) + require.NoError(t, keeper.SetAttesterSetMember(ctx, myValAddr.String())) + require.NoError(t, keeper.SetValidatorIndex(ctx, myValAddr.String(), 0, 1)) msg := &types.MsgAttest{ Authority: myValAddr.String(), @@ -108,7 +107,6 @@ func TestAttestVotePayloadValidation(t *testing.T) { } } - func TestAttest(t *testing.T) { ownerAddr := sdk.ValAddress("attester_owner") otherAddr := sdk.ValAddress("other_sender") @@ -124,13 +122,9 @@ func TestAttest(t *testing.T) { setup: func(t *testing.T, ctx sdk.Context, keeper *Keeper, server msgServer) { t.Helper() require.NoError(t, keeper.SetParams(ctx, types.DefaultParams())) - joinMsg := &types.MsgJoinAttesterSet{ - Authority: ownerAddr.String(), - ConsensusAddress: ownerAddr.String(), - } - _, err := server.JoinAttesterSet(ctx, joinMsg) - require.NoError(t, err) - require.NoError(t, keeper.BuildValidatorIndexMap(ctx)) + require.NoError(t, keeper.SetAttesterInfo(ctx, ownerAddr.String(), &types.AttesterInfo{Authority: ownerAddr.String()})) + require.NoError(t, keeper.SetAttesterSetMember(ctx, ownerAddr.String())) + require.NoError(t, keeper.SetValidatorIndex(ctx, ownerAddr.String(), 0, 1)) }, msg: &types.MsgAttest{ Authority: ownerAddr.String(), @@ -154,13 +148,9 @@ func TestAttest(t *testing.T) { "wrong_authority": { setup: func(t *testing.T, ctx sdk.Context, keeper *Keeper, server msgServer) { t.Helper() - joinMsg := &types.MsgJoinAttesterSet{ - Authority: ownerAddr.String(), - ConsensusAddress: ownerAddr.String(), - } - _, err := server.JoinAttesterSet(ctx, joinMsg) - require.NoError(t, err) - require.NoError(t, keeper.BuildValidatorIndexMap(ctx)) + require.NoError(t, keeper.SetAttesterInfo(ctx, ownerAddr.String(), &types.AttesterInfo{Authority: ownerAddr.String()})) + require.NoError(t, keeper.SetAttesterSetMember(ctx, ownerAddr.String())) + require.NoError(t, keeper.SetValidatorIndex(ctx, ownerAddr.String(), 0, 1)) }, msg: &types.MsgAttest{ Authority: otherAddr.String(), @@ -269,13 +259,9 @@ func TestAttestHeightBounds(t *testing.T) { require.NoError(t, keeper.SetParams(ctx, types.DefaultParams())) - joinMsg := &types.MsgJoinAttesterSet{ - Authority: ownerAddr.String(), - ConsensusAddress: myValAddr.String(), - } - _, err := server.JoinAttesterSet(ctx, joinMsg) - require.NoError(t, err) - require.NoError(t, keeper.BuildValidatorIndexMap(ctx)) + require.NoError(t, keeper.SetAttesterInfo(ctx, myValAddr.String(), &types.AttesterInfo{Authority: ownerAddr.String()})) + require.NoError(t, keeper.SetAttesterSetMember(ctx, myValAddr.String())) + require.NoError(t, keeper.SetValidatorIndex(ctx, myValAddr.String(), 0, 1)) // when msg := &types.MsgAttest{ From dd9e721d8cd5df0aa315a839bafef784c19b8b98 Mon Sep 17 00:00:00 2001 From: Randy Grok Date: Thu, 30 Apr 2026 14:30:30 +0200 Subject: [PATCH 3/3] refactor(network): remove inactive pruning code --- modules/network/keeper/abci.go | 5 ---- modules/network/keeper/keeper.go | 37 ---------------------------- modules/network/keeper/msg_server.go | 6 ++--- 3 files changed, 2 insertions(+), 46 deletions(-) diff --git a/modules/network/keeper/abci.go b/modules/network/keeper/abci.go index 81dcc108..80d59555 100644 --- a/modules/network/keeper/abci.go +++ b/modules/network/keeper/abci.go @@ -114,11 +114,6 @@ func (k Keeper) processEpochEnd(ctx sdk.Context, epoch uint64) error { // Validator indices are established at genesis and never mutate at runtime // (MsgJoin/MsgLeave are disabled). Nothing to rebuild here. - - // todo: find a way to prune only bitmaps that are not used anymore - // if err := k.PruneOldBitmaps(ctx, epoch); err != nil { - // return fmt.Errorf("pruning old data at epoch %d: %w", epoch, err) - // } return nil } diff --git a/modules/network/keeper/keeper.go b/modules/network/keeper/keeper.go index 206d1163..544d5d0f 100644 --- a/modules/network/keeper/keeper.go +++ b/modules/network/keeper/keeper.go @@ -262,43 +262,6 @@ func (k Keeper) IsSoftConfirmed(ctx sdk.Context, height int64) (bool, error) { return k.CheckQuorum(ctx, votedPower, totalPower) } -// PruneOldBitmaps removes bitmaps older than PruneAfter epochs -func (k Keeper) PruneOldBitmaps(ctx sdk.Context, currentEpoch uint64) error { - params := k.GetParams(ctx) - if params.PruneAfter == 0 { // Avoid pruning if PruneAfter is zero or not set - return nil - } - if currentEpoch <= params.PruneAfter { - return nil - } - - pruneBeforeEpoch := currentEpoch - params.PruneAfter - pruneHeight := int64(pruneBeforeEpoch * params.EpochLength) // Assuming EpochLength defines blocks per epoch - - // Prune attestation bitmaps (raw bitmaps) - attestationRange := new(collections.Range[int64]).StartInclusive(0).EndExclusive(pruneHeight) - if err := k.AttestationBitmap.Clear(ctx, attestationRange); err != nil { - return fmt.Errorf("clearing attestation bitmaps before height %d: %w", pruneHeight, err) - } - // Prune stored attestation info (full AttestationBitmap objects) - storedAttestationInfoRange := new(collections.Range[int64]).StartInclusive(0).EndExclusive(pruneHeight) - if err := k.StoredAttestationInfo.Clear(ctx, storedAttestationInfoRange); err != nil { - return fmt.Errorf("clearing stored attestation info before height %d: %w", pruneHeight, err) - } - - // Prune epoch bitmaps - epochRange := new(collections.Range[uint64]).StartInclusive(0).EndExclusive(pruneBeforeEpoch) - if err := k.EpochBitmap.Clear(ctx, epochRange); err != nil { - return fmt.Errorf("clearing epoch bitmaps before epoch %d: %w", pruneBeforeEpoch, err) - } - - // TODO: Consider pruning signatures associated with pruned heights. - // This would involve iterating k.Signatures and removing entries where height < pruneHeight. - - k.Logger(ctx).Info("Pruned old bitmaps and attestation info", "prunedBeforeEpoch", pruneBeforeEpoch, "prunedBeforeHeight", pruneHeight) - return nil -} - // SetSignature stores the vote signature for a given height and validator func (k Keeper) SetSignature(ctx sdk.Context, height int64, validatorAddr string, signature []byte) error { return k.Signatures.Set(ctx, collections.Join(height, validatorAddr), signature) diff --git a/modules/network/keeper/msg_server.go b/modules/network/keeper/msg_server.go index 17143579..a5b20ca3 100644 --- a/modules/network/keeper/msg_server.go +++ b/modules/network/keeper/msg_server.go @@ -60,10 +60,8 @@ func (k msgServer) Attest(goCtx context.Context, msg *types.MsgAttest) (*types.M return nil, sdkerr.Wrapf(sdkerrors.ErrInvalidRequest, "attestation height %d exceeds max allowed height %d", msg.Height, maxFutureHeight) } - // Enforce attestation height lower bound: reject heights that fall below - // the PruneAfter retention window. Attesting pruned/about-to-be-pruned - // heights wastes storage and serves no purpose. This uses the same epoch - // calculation as PruneOldBitmaps so the two stay aligned. + // Enforce attestation height lower bound so validators cannot submit + // attestations for heights outside the configured attestation window. params := k.GetParams(ctx) minHeight := int64(1) if params.PruneAfter > 0 && params.EpochLength > 0 {