From 1336f69eedcfc0c33583634f866eb4de89f3702e Mon Sep 17 00:00:00 2001 From: Randy Grok Date: Tue, 28 Apr 2026 17:04:20 +0200 Subject: [PATCH 1/2] feat(network): verify attester votes --- modules/network/keeper/attester_ibc_test.go | 309 +++++++++ modules/network/keeper/keeper.go | 65 +- modules/network/keeper/msg_server.go | 128 ++-- modules/network/keeper/msg_server_test.go | 666 ++++++++++++++++++-- modules/network/keeper/testhelpers_test.go | 67 ++ modules/network/types/expected_keepers.go | 7 + server/start.go | 17 + 7 files changed, 1150 insertions(+), 109 deletions(-) create mode 100644 modules/network/keeper/attester_ibc_test.go create mode 100644 modules/network/keeper/testhelpers_test.go diff --git a/modules/network/keeper/attester_ibc_test.go b/modules/network/keeper/attester_ibc_test.go new file mode 100644 index 00000000..5d12267d --- /dev/null +++ b/modules/network/keeper/attester_ibc_test.go @@ -0,0 +1,309 @@ +package keeper_test + +import ( + "bytes" + "context" + "crypto/sha256" + "fmt" + "sort" + "testing" + "time" + + cmted25519 "github.com/cometbft/cometbft/crypto/ed25519" + cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" + cmttypes "github.com/cometbft/cometbft/types" + cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/gogoproto/proto" + "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" +) + +// staticBlockIDProvider returns the same BlockID hash for every height — +// mirrors a fixed sequencer view inside unit tests. +type staticBlockIDProvider struct{ hash []byte } + +func (s staticBlockIDProvider) GetBlockID(_ context.Context, _ uint64) (*cmttypes.BlockID, error) { + return &cmttypes.BlockID{Hash: s.hash}, nil +} + +func TestAttesterCommitVerifiesAsIBCLightClient(t *testing.T) { + chainID := "ibc-test-chain" + const height int64 = 100 + + // 1. Three attesters with fresh keys. + privs := []cmted25519.PrivKey{ + cmted25519.GenPrivKey(), + cmted25519.GenPrivKey(), + cmted25519.GenPrivKey(), + } + attesters := make([]types.AttesterInfo, 0, len(privs)) + for _, p := range privs { + pub := p.PubKey().(cmted25519.PubKey) + sdkPk, err := cryptocodec.FromCmtPubKeyInterface(pub) + require.NoError(t, err) + ai, err := types.NewAttesterInfo(sdk.AccAddress(pub.Address()).String(), sdkPk, 0) + require.NoError(t, err) + attesters = append(attesters, *ai) + } + + // 2. Set up keeper, init genesis with the 3 attesters, advance ctx to height. + k, ctx, _ := newKeeperForGenesis(t) + gs := types.GenesisState{ + Params: types.DefaultParams(), + AttesterInfos: attesters, + } + require.NoError(t, network.InitGenesis(ctx, k, gs)) + ctx = ctx.WithBlockHeader(cmtproto.Header{ChainID: chainID, Height: height}). + WithChainID(chainID) + + // 3. Deterministic BlockID hash (what all attesters sign). + blockIDHash := ibcMakeBlockHash(fmt.Sprintf("height-%d", height)) + k.SetBlockIDProvider(staticBlockIDProvider{hash: blockIDHash}) + + // 4. Each attester signs and submits a real MsgAttest (signature-verified path). + msgServer := keeper.NewMsgServerImpl(k) + for _, p := range privs { + pub := p.PubKey().(cmted25519.PubKey) + consAddr := sdk.ConsAddress(pub.Address()).String() + authority := sdk.AccAddress(pub.Address()).String() + voteBytes := ibcSignVote(t, chainID, height, p, blockIDHash) + _, err := msgServer.Attest(ctx, &types.MsgAttest{ + Authority: authority, + ConsensusAddress: consAddr, + Height: height, + Vote: voteBytes, + }) + require.NoError(t, err, "MsgAttest rejected for consAddr=%s", consAddr) + } + + // 5. Read state and assemble a cmttypes.Commit in ValidatorIndex order. + commit := ibcAssembleCommit(t, k, ctx, height, blockIDHash) + + // 6. Canonical ValidatorSet (NewValidatorSet sorts by address asc, matching genesis). + valSet := ibcBuildValidatorSet(attesters) + blockID := cmttypes.BlockID{Hash: blockIDHash, PartSetHeader: cmttypes.PartSetHeader{}} + + // 7. 07-tendermint verification — the decisive assertion. + require.NoError(t, valSet.VerifyCommitLight(chainID, blockID, height, commit), + "reconstructed commit must pass 07-tendermint light-client verification") + require.Len(t, commit.Signatures, 3, "every set member must appear in commit") + for _, cs := range commit.Signatures { + require.Equal(t, cmttypes.BlockIDFlagCommit, cs.BlockIDFlag) + } +} + +func TestAttesterCommit_BelowQuorum(t *testing.T) { + chainID := "ibc-test-chain" + const height int64 = 200 + + privs := []cmted25519.PrivKey{ + cmted25519.GenPrivKey(), + cmted25519.GenPrivKey(), + cmted25519.GenPrivKey(), + } + attesters := make([]types.AttesterInfo, 0, len(privs)) + for _, p := range privs { + pub := p.PubKey().(cmted25519.PubKey) + sdkPk, err := cryptocodec.FromCmtPubKeyInterface(pub) + require.NoError(t, err) + ai, err := types.NewAttesterInfo(sdk.AccAddress(pub.Address()).String(), sdkPk, 0) + require.NoError(t, err) + attesters = append(attesters, *ai) + } + + k, ctx, _ := newKeeperForGenesis(t) + gs := types.GenesisState{ + Params: types.DefaultParams(), + AttesterInfos: attesters, + } + require.NoError(t, network.InitGenesis(ctx, k, gs)) + ctx = ctx.WithBlockHeader(cmtproto.Header{ChainID: chainID, Height: height}). + WithChainID(chainID) + + blockIDHash := ibcMakeBlockHash(fmt.Sprintf("height-%d", height)) + k.SetBlockIDProvider(staticBlockIDProvider{hash: blockIDHash}) + + // Only 1 of 3 signs — below 2/3 quorum; LastAttestedHeight must not advance. + msgServer := keeper.NewMsgServerImpl(k) + p := privs[0] + pub := p.PubKey().(cmted25519.PubKey) + _, err := msgServer.Attest(ctx, &types.MsgAttest{ + Authority: sdk.AccAddress(pub.Address()).String(), + ConsensusAddress: sdk.ConsAddress(pub.Address()).String(), + Height: height, + Vote: ibcSignVote(t, chainID, height, p, blockIDHash), + }) + require.NoError(t, err) + + lastAttested, err := k.GetLastAttestedHeight(ctx) + require.NoError(t, err) + require.Less(t, lastAttested, height, "LastAttestedHeight should not advance below quorum") +} + +func TestAttesterCommit_ExactTwoThirdsDoesNotAdvanceLastAttestedHeight(t *testing.T) { + chainID := "ibc-test-chain" + const height int64 = 201 + + privs := []cmted25519.PrivKey{ + cmted25519.GenPrivKey(), + cmted25519.GenPrivKey(), + cmted25519.GenPrivKey(), + } + attesters := make([]types.AttesterInfo, 0, len(privs)) + for _, p := range privs { + pub := p.PubKey().(cmted25519.PubKey) + sdkPk, err := cryptocodec.FromCmtPubKeyInterface(pub) + require.NoError(t, err) + ai, err := types.NewAttesterInfo(sdk.AccAddress(pub.Address()).String(), sdkPk, 0) + require.NoError(t, err) + attesters = append(attesters, *ai) + } + + k, ctx, _ := newKeeperForGenesis(t) + gs := types.GenesisState{ + Params: types.DefaultParams(), + AttesterInfos: attesters, + } + require.NoError(t, network.InitGenesis(ctx, k, gs)) + ctx = ctx.WithBlockHeader(cmtproto.Header{ChainID: chainID, Height: height}). + WithChainID(chainID) + + blockIDHash := ibcMakeBlockHash(fmt.Sprintf("height-%d", height)) + k.SetBlockIDProvider(staticBlockIDProvider{hash: blockIDHash}) + + msgServer := keeper.NewMsgServerImpl(k) + for _, p := range privs[:2] { + pub := p.PubKey().(cmted25519.PubKey) + _, err := msgServer.Attest(ctx, &types.MsgAttest{ + Authority: sdk.AccAddress(pub.Address()).String(), + ConsensusAddress: sdk.ConsAddress(pub.Address()).String(), + Height: height, + Vote: ibcSignVote(t, chainID, height, p, blockIDHash), + }) + require.NoError(t, err) + } + + lastAttested, err := k.GetLastAttestedHeight(ctx) + require.NoError(t, err) + require.Less(t, lastAttested, height, "exactly 2/3 voting power must not advance LastAttestedHeight") +} + +// -- helpers -- + +func ibcMakeBlockHash(seed string) []byte { + h := sha256.Sum256([]byte(seed)) + return h[:] +} + +// ibcSignVote builds and signs a precommit vote, returning the marshaled proto bytes. +func ibcSignVote(t *testing.T, chainID string, height int64, priv cmted25519.PrivKey, blockIDHash []byte) []byte { + t.Helper() + pub := priv.PubKey().(cmted25519.PubKey) + v := cmtproto.Vote{ + Type: cmtproto.PrecommitType, + Height: height, + Round: 0, + BlockID: cmtproto.BlockID{Hash: blockIDHash, PartSetHeader: cmtproto.PartSetHeader{}}, + Timestamp: time.Date(2026, 4, 22, 12, 0, 0, 0, time.UTC), + ValidatorAddress: pub.Address(), + ValidatorIndex: 0, + } + sb := cmttypes.VoteSignBytes(chainID, &v) + sig, err := priv.Sign(sb) + require.NoError(t, err) + v.Signature = sig + out, err := proto.Marshal(&v) + require.NoError(t, err) + return out +} + +func ibcBuildValidatorSet(attesters []types.AttesterInfo) *cmttypes.ValidatorSet { + vals := make([]*cmttypes.Validator, 0, len(attesters)) + for _, a := range attesters { + pk, err := a.GetPubKey() + if err != nil { + panic(err) + } + cmtPk, err := cryptocodec.ToCmtPubKeyInterface(pk) + if err != nil { + panic(err) + } + vals = append(vals, cmttypes.NewValidator(cmtPk, 1)) + } + // NewValidatorSet sorts internally by address ascending. + return cmttypes.NewValidatorSet(vals) +} + +// ibcAssembleCommit reads ValidatorIndex + Signatures from keeper state and +// assembles a Commit ordered by ValidatorIndex (mirrors the /commit RPC path). +func ibcAssembleCommit(t *testing.T, k keeper.Keeper, ctx sdk.Context, height int64, blockIDHash []byte) *cmttypes.Commit { + t.Helper() + + type entry struct { + consAddr string + addr []byte + index uint16 + } + + var entries []entry + require.NoError(t, k.ValidatorIndex.Walk(ctx, nil, func(addr string, idx uint16) (bool, error) { + info, err := k.GetAttesterInfo(ctx, addr) + if err != nil { + return false, err + } + pk, err := info.GetPubKey() + if err != nil { + return false, err + } + entries = append(entries, entry{ + consAddr: addr, + addr: pk.Address(), + index: idx, + }) + return false, nil + })) + sort.Slice(entries, func(i, j int) bool { return entries[i].index < entries[j].index }) + + sigs := make([]cmttypes.CommitSig, 0, len(entries)) + for _, e := range entries { + has, err := k.HasSignature(ctx, height, e.consAddr) + require.NoError(t, err) + if !has { + sigs = append(sigs, cmttypes.CommitSig{BlockIDFlag: cmttypes.BlockIDFlagAbsent}) + continue + } + voteBytes, err := k.GetSignature(ctx, height, e.consAddr) + require.NoError(t, err) + var vote cmtproto.Vote + require.NoError(t, proto.Unmarshal(voteBytes, &vote)) + sigs = append(sigs, cmttypes.CommitSig{ + BlockIDFlag: cmttypes.BlockIDFlagCommit, + ValidatorAddress: e.addr, + Timestamp: vote.Timestamp, + Signature: vote.Signature, + }) + } + + // Sanity: validator addresses in commit order must be ascending. + prev := []byte(nil) + for _, s := range sigs { + if s.BlockIDFlag == cmttypes.BlockIDFlagCommit && prev != nil { + require.True(t, bytes.Compare(prev, s.ValidatorAddress) < 0, + "validator addresses not ascending in commit") + } + if s.BlockIDFlag == cmttypes.BlockIDFlagCommit { + prev = s.ValidatorAddress + } + } + + return &cmttypes.Commit{ + Height: height, + Round: 0, + BlockID: cmttypes.BlockID{Hash: blockIDHash, PartSetHeader: cmttypes.PartSetHeader{}}, + Signatures: sigs, + } +} diff --git a/modules/network/keeper/keeper.go b/modules/network/keeper/keeper.go index 206d1163..aef544bb 100644 --- a/modules/network/keeper/keeper.go +++ b/modules/network/keeper/keeper.go @@ -3,6 +3,7 @@ package keeper import ( "errors" "fmt" + "sort" "cosmossdk.io/collections" "cosmossdk.io/core/store" @@ -14,6 +15,12 @@ import ( "github.com/evstack/ev-abci/modules/network/types" ) +// mutableState holds keeper fields that must remain observable across value +// copies of Keeper (e.g. the copy captured by msgServer at wiring time). +type mutableState struct { + blockIDProvider types.BlockIDProvider +} + // Keeper of the network store type Keeper struct { cdc codec.BinaryCodec @@ -22,6 +29,7 @@ type Keeper struct { bankKeeper types.BankKeeper authority string bitmapHelper *BitmapHelper + mut *mutableState // Collections for state management ValidatorIndex collections.Map[string, uint16] @@ -55,6 +63,7 @@ func NewKeeper( bankKeeper: bk, authority: authority, bitmapHelper: NewBitmapHelper(), + mut: &mutableState{}, ValidatorIndex: collections.NewMap(sb, types.ValidatorIndexPrefix, "validator_index", collections.StringKey, collections.Uint16Value), ValidatorPower: collections.NewMap(sb, types.ValidatorPowerPrefix, "validator_power", collections.Uint16Key, collections.Uint64Value), @@ -81,6 +90,22 @@ func (k Keeper) GetAuthority() string { return k.authority } +// SetBlockIDProvider wires the source of canonical BlockID hashes used to pin +// attester votes. Must be called once at app-wiring time (post-depinject). +// The provider is stored on a shared mutableState so value-copies of Keeper +// (notably the one captured by msgServer) observe the update. +func (k Keeper) SetBlockIDProvider(p types.BlockIDProvider) { + k.mut.blockIDProvider = p +} + +// blockIDProvider returns the wired provider, or nil if none has been set. +func (k Keeper) blockIDProvider() types.BlockIDProvider { + if k.mut == nil { + return nil + } + return k.mut.blockIDProvider +} + // Logger returns a module-specific logger func (k Keeper) Logger(ctx sdk.Context) log.Logger { return ctx.Logger().With("module", "network") @@ -231,13 +256,11 @@ func (k Keeper) GetTotalPower(ctx sdk.Context) (uint64, error) { // CheckQuorum checks if the voted power meets quorum func (k Keeper) CheckQuorum(ctx sdk.Context, votedPower, totalPower uint64) (bool, error) { params := k.GetParams(ctx) - quorumFrac, err := math.LegacyNewDecFromStr(params.QuorumFraction) - if err != nil { + if _, err := math.LegacyNewDecFromStr(params.QuorumFraction); err != nil { return false, fmt.Errorf("invalid quorum fraction: %w", err) } - requiredPower := math.LegacyNewDec(int64(totalPower)).Mul(quorumFrac).TruncateInt().Uint64() - return votedPower >= requiredPower, nil + return votedPower*3 > totalPower*2, nil } // IsSoftConfirmed checks if a block at a given height is soft-confirmed @@ -327,28 +350,36 @@ func (k Keeper) GetAllSignaturesForHeight(ctx sdk.Context, height int64) (map[st return signatures, nil // No attestations for this height } - // Get all attesters to map indices to addresses - attesters, err := k.GetAllAttesters(ctx) - if err != nil { - return nil, fmt.Errorf("get all attesters: %w", err) + type indexedAttester struct { + addr string + index uint16 } - // Check each attester to see if they signed - for i, attesterAddr := range attesters { - if i >= len(bitmap)*8 { - break // Don't go beyond bitmap size + 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) + } + sort.Slice(attesters, func(i, j int) bool { + return attesters[i].index < attesters[j].index + }) + + for _, attester := range attesters { + if int(attester.index) >= len(bitmap)*8 { + continue } - // Check if this attester signed (bit is set in bitmap) - if k.bitmapHelper.IsSet(bitmap, i) { - signature, err := k.GetSignature(ctx, height, attesterAddr) + if k.bitmapHelper.IsSet(bitmap, int(attester.index)) { + signature, err := k.GetSignature(ctx, height, attester.addr) if err != nil && !errors.Is(err, collections.ErrNotFound) { k.Logger(ctx).Error("failed to get signature for attester", - "height", height, "attester", attesterAddr, "error", err) + "height", height, "attester", attester.addr, "error", err) continue } if signature != nil { - signatures[attesterAddr] = signature + signatures[attester.addr] = signature } } } diff --git a/modules/network/keeper/msg_server.go b/modules/network/keeper/msg_server.go index 17143579..71f6f2fa 100644 --- a/modules/network/keeper/msg_server.go +++ b/modules/network/keeper/msg_server.go @@ -1,6 +1,7 @@ package keeper import ( + "bytes" "context" "errors" "fmt" @@ -8,17 +9,16 @@ import ( "cosmossdk.io/collections" sdkerr "cosmossdk.io/errors" "cosmossdk.io/math" + cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" + cmttypes "github.com/cometbft/cometbft/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" + "github.com/cosmos/gogoproto/proto" "github.com/evstack/ev-abci/modules/network/types" ) -// MinVoteLen is the minimum vote payload length in bytes. -// 64 is the size of a Ed25519 signature -const MinVoteLen = 64 - type msgServer struct { Keeper } @@ -34,16 +34,12 @@ var _ types.MsgServer = msgServer{} func (k msgServer) Attest(goCtx context.Context, msg *types.MsgAttest) (*types.MsgAttestResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) - if k.GetParams(ctx).SignMode == types.SignMode_SIGN_MODE_CHECKPOINT && - !k.IsCheckpointHeight(ctx, msg.Height) { - return nil, sdkerr.Wrapf(sdkerrors.ErrInvalidRequest, "height %d is not a checkpoint", msg.Height) - } - - if len(msg.Vote) < MinVoteLen { - return nil, sdkerr.Wrapf(sdkerrors.ErrInvalidRequest, "vote payload too short: got %d bytes, minimum %d", len(msg.Vote), MinVoteLen) + if err := k.assertValidValidatorAuthority(ctx, msg.ConsensusAddress, msg.Authority); err != nil { + return nil, err } - if err := k.assertValidValidatorAuthority(ctx, msg.ConsensusAddress, msg.Authority); err != nil { + // Verify the vote: decode, internal checks, signature check. + if _, err := k.verifyVote(ctx, msg.ConsensusAddress, msg.Vote, msg.Height); err != nil { return nil, err } @@ -52,18 +48,12 @@ func (k msgServer) Attest(goCtx context.Context, msg *types.MsgAttest) (*types.M return nil, sdkerr.Wrapf(sdkerrors.ErrNotFound, "validator index not found for %s", msg.ConsensusAddress) } - // Enforce attestation height upper bound to prevent storage exhaustion - // from future-height spam. + // Height bounds currentHeight := ctx.BlockHeight() maxFutureHeight := currentHeight + 1 if msg.Height > maxFutureHeight { 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. params := k.GetParams(ctx) minHeight := int64(1) if params.PruneAfter > 0 && params.EpochLength > 0 { @@ -75,6 +65,7 @@ func (k msgServer) Attest(goCtx context.Context, msg *types.MsgAttest) (*types.M if msg.Height < minHeight { return nil, sdkerr.Wrapf(sdkerrors.ErrInvalidRequest, "attestation height %d is below retention window (min %d)", msg.Height, minHeight) } + bitmap, err := k.GetAttestationBitmap(ctx, msg.Height) if err != nil && !errors.Is(err, collections.ErrNotFound) { return nil, sdkerr.Wrap(err, "get attestation bitmap") @@ -84,51 +75,39 @@ func (k msgServer) Attest(goCtx context.Context, msg *types.MsgAttest) (*types.M if err != nil { return nil, err } - numAttesters := len(attesters) - bitmap = k.bitmapHelper.NewBitmap(numAttesters) + bitmap = k.bitmapHelper.NewBitmap(len(attesters)) } if k.bitmapHelper.IsSet(bitmap, int(index)) { return nil, sdkerr.Wrapf(sdkerrors.ErrInvalidRequest, "consensus address %s already attested for height %d", msg.ConsensusAddress, msg.Height) } - // Set the bit k.bitmapHelper.SetBit(bitmap, int(index)) if err := k.SetAttestationBitmap(ctx, msg.Height, bitmap); err != nil { return nil, sdkerr.Wrap(err, "set attestation bitmap") } - - // Store signature using the consensus address (this is the key fix for IBC) if err := k.SetSignature(ctx, msg.Height, msg.ConsensusAddress, msg.Vote); err != nil { return nil, sdkerr.Wrap(err, "store signature") } - // Check if quorum is reached after this attestation 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") } - quorumReached, err := k.CheckQuorum(ctx, votedPower, totalPower) if err != nil { return nil, sdkerr.Wrap(err, "check quorum") } - - // If quorum is reached, update the last attested height if quorumReached { if err := k.UpdateLastAttestedHeight(ctx, msg.Height); err != nil { return nil, sdkerr.Wrap(err, "update last attested height") } - k.Logger(ctx).Info("block reached quorum and is now soft confirmed", - "height", msg.Height, - "voted_power", votedPower, - "total_power", totalPower) + "height", msg.Height, "voted_power", votedPower, "total_power", totalPower) } epoch := k.GetCurrentEpoch(ctx) @@ -138,15 +117,13 @@ func (k msgServer) Attest(goCtx context.Context, msg *types.MsgAttest) (*types.M if err != nil { return nil, err } - numAttesters := len(attesters) - epochBitmap = k.bitmapHelper.NewBitmap(numAttesters) + epochBitmap = k.bitmapHelper.NewBitmap(len(attesters)) } k.bitmapHelper.SetBit(epochBitmap, int(index)) if err := k.SetEpochBitmap(ctx, epoch, epochBitmap); err != nil { return nil, sdkerr.Wrap(err, "set epoch bitmap") } - // Emit event ctx.EventManager().EmitEvent( sdk.NewEvent( types.TypeMsgAttest, @@ -155,7 +132,6 @@ func (k msgServer) Attest(goCtx context.Context, msg *types.MsgAttest) (*types.M sdk.NewAttribute("height", math.NewInt(msg.Height).String()), ), ) - return &types.MsgAttestResponse{}, nil } @@ -185,6 +161,84 @@ func (k msgServer) assertValidValidatorAuthority(ctx sdk.Context, consensusAddre return nil } +// verifyVote decodes vote bytes, performs internal-consistency checks, and +// verifies the signature against the pubkey registered for consensusAddress. +// Returns the decoded vote on success. +func (k msgServer) verifyVote(ctx sdk.Context, consensusAddress string, voteBytes []byte, msgHeight int64) (*cmtproto.Vote, error) { + var v cmtproto.Vote + if err := proto.Unmarshal(voteBytes, &v); err != nil { + return nil, sdkerr.Wrapf(sdkerrors.ErrInvalidRequest, "unmarshal vote: %v", err) + } + if v.Type != cmtproto.PrecommitType { + return nil, sdkerr.Wrapf(sdkerrors.ErrInvalidRequest, "vote type must be Precommit, got %s", v.Type) + } + if v.Height != msgHeight { + return nil, sdkerr.Wrapf(sdkerrors.ErrInvalidRequest, "vote height %d != msg height %d", v.Height, msgHeight) + } + if v.Round != 0 { + return nil, sdkerr.Wrapf(sdkerrors.ErrInvalidRequest, "vote round must be 0, got %d", v.Round) + } + + info, err := k.AttesterInfo.Get(ctx, consensusAddress) + if err != nil { + if errors.Is(err, collections.ErrNotFound) { + return nil, sdkerr.Wrapf(sdkerrors.ErrNotFound, "consensus address %s not registered", consensusAddress) + } + return nil, sdkerr.Wrap(err, "get attester info") + } + pk, err := info.GetPubKey() + if err != nil { + return nil, sdkerr.Wrap(err, "decode pubkey") + } + if !bytes.Equal(v.ValidatorAddress, pk.Address()) { + return nil, sdkerr.Wrapf(sdkerrors.ErrUnauthorized, + "vote validator address %X does not match registered pubkey address %X", + v.ValidatorAddress, pk.Address()) + } + voteBlockID, err := cmttypes.BlockIDFromProto(&v.BlockID) + if err != nil { + return nil, sdkerr.Wrapf(sdkerrors.ErrInvalidRequest, + "invalid vote BlockID: %v", err) + } + + sig := v.Signature + v.Signature = nil + signBytes := cmttypes.VoteSignBytes(ctx.ChainID(), &v) + if !pk.VerifySignature(signBytes, sig) { + return nil, sdkerr.Wrapf(sdkerrors.ErrUnauthorized, "invalid vote signature") + } + v.Signature = sig + + // Pin the full signed BlockID to the sequencer's real BlockID for this + // height. CometBFT sign bytes include both Hash and PartSetHeader; accepting + // a vote over a different PartSetHeader would later fail VerifyCommitLight. + provider := k.blockIDProvider() + if provider == nil { + return nil, sdkerr.Wrap(sdkerrors.ErrLogic, + "block ID provider not wired; cannot verify vote BlockID") + } + storedID, err := provider.GetBlockID(ctx, uint64(msgHeight)) + if err != nil { + return nil, sdkerr.Wrapf(sdkerrors.ErrInvalidRequest, + "get block ID for height %d: %v", msgHeight, err) + } + if storedID == nil { + return nil, sdkerr.Wrapf(sdkerrors.ErrInvalidRequest, + "block ID for height %d not found", msgHeight) + } + if !storedID.Equals(*voteBlockID) { + return nil, sdkerr.Wrapf(sdkerrors.ErrInvalidRequest, + "vote BlockID %v does not match sequencer BlockID %v at height %d", + voteBlockID, storedID, msgHeight) + } + return &v, nil +} + +// VerifyVoteForTest exposes verifyVote for unit testing. Not for production use. +func (k Keeper) VerifyVoteForTest(ctx sdk.Context, consensusAddress string, voteBytes []byte, msgHeight int64) (*cmtproto.Vote, error) { + return msgServer{Keeper: k}.verifyVote(ctx, consensusAddress, voteBytes, msgHeight) +} + // UpdateParams handles MsgUpdateParams func (k msgServer) UpdateParams(goCtx context.Context, msg *types.MsgUpdateParams) (*types.MsgUpdateParamsResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) diff --git a/modules/network/keeper/msg_server_test.go b/modules/network/keeper/msg_server_test.go index 9b3f99a9..74ae05d7 100644 --- a/modules/network/keeper/msg_server_test.go +++ b/modules/network/keeper/msg_server_test.go @@ -3,16 +3,22 @@ package keeper import ( "bytes" "context" + "errors" + "fmt" "maps" "slices" "strings" "testing" "time" + "cosmossdk.io/collections" "cosmossdk.io/log" "cosmossdk.io/math" storetypes "cosmossdk.io/store/types" + cmted25519 "github.com/cometbft/cometbft/crypto/ed25519" cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" + cmttypes "github.com/cometbft/cometbft/types" + 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" @@ -20,11 +26,26 @@ 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/cosmos/gogoproto/proto" "github.com/stretchr/testify/require" "github.com/evstack/ev-abci/modules/network/types" ) +type failingBlockIDProvider struct { + err error +} + +func (f failingBlockIDProvider) GetBlockID(_ context.Context, _ uint64) (*cmttypes.BlockID, error) { + return nil, f.err +} + +type nilBlockIDProvider struct{} + +func (nilBlockIDProvider) GetBlockID(_ context.Context, _ uint64) (*cmttypes.BlockID, error) { + return nil, nil +} + func TestJoinAttesterSetDisabled(t *testing.T) { sk := NewMockStakingKeeper() server, _, ctx := newTestServer(t, &sk) @@ -53,7 +74,13 @@ func TestLeaveAttesterSetDisabled(t *testing.T) { } func TestAttestVotePayloadValidation(t *testing.T) { - myValAddr := sdk.ValAddress("validator1") + chainID := "test-chain" + priv := cmted25519.GenPrivKey() + pub := priv.PubKey().(cmted25519.PubKey) + blockHash := bytes.Repeat([]byte{0x01}, 32) + + consAddr := sdk.ConsAddress(pub.Address()).String() + authorityAddr := sdk.AccAddress(pub.Address()).String() specs := map[string]struct { vote []byte @@ -67,32 +94,52 @@ func TestAttestVotePayloadValidation(t *testing.T) { vote: nil, expErr: sdkerrors.ErrInvalidRequest, }, - "short vote rejected": { - vote: make([]byte, MinVoteLen-1), + "random bytes rejected": { + vote: bytes.Repeat([]byte{0x01}, 64), expErr: sdkerrors.ErrInvalidRequest, }, - "min-length vote accepted": { - vote: make([]byte, MinVoteLen), - }, - "valid-length vote accepted": { - vote: make([]byte, 96), + "valid signed vote accepted": { + vote: nil, // populated below per-subtest }, } for name, spec := range specs { t.Run(name, func(t *testing.T) { sk := NewMockStakingKeeper() - server, keeper, ctx := newTestServer(t, &sk) + cdc := moduletestutil.MakeTestEncodingConfig().Codec + keys := storetypes.NewKVStoreKeys(types.StoreKey) + logger := log.NewTestLogger(t) + cms := integration.CreateMultiStore(keys, logger) + authority := authtypes.NewModuleAddress("gov") + keeper := NewKeeper(cdc, runtime.NewKVStoreService(keys[types.StoreKey]), &sk, nil, nil, authority.String()) + keeper.SetBlockIDProvider(staticBlockIDProvider{hash: blockHash}) + server := msgServer{Keeper: keeper} + ctx := sdk.NewContext(cms, cmtproto.Header{ + ChainID: chainID, + Time: time.Now().UTC(), + Height: 10, + }, false, logger).WithContext(t.Context()) + require.NoError(t, keeper.SetParams(ctx, types.DefaultParams())) - require.NoError(t, keeper.SetAttesterInfo(ctx, myValAddr.String(), &types.AttesterInfo{Authority: myValAddr.String()})) - require.NoError(t, keeper.SetAttesterSetMember(ctx, myValAddr.String())) - require.NoError(t, keeper.SetValidatorIndex(ctx, myValAddr.String(), 0, 1)) + + sdkPk, err := cryptocodec.FromCmtPubKeyInterface(pub) + require.NoError(t, err) + info, err := types.NewAttesterInfo(authorityAddr, sdkPk, 0) + require.NoError(t, err) + require.NoError(t, keeper.SetAttesterInfo(ctx, consAddr, info)) + require.NoError(t, keeper.SetAttesterSetMember(ctx, consAddr)) + require.NoError(t, keeper.SetValidatorIndex(ctx, consAddr, 0, 1)) + + vote := spec.vote + if name == "valid signed vote accepted" { + vote = signTestVote(t, chainID, 10, priv, blockHash) + } msg := &types.MsgAttest{ - Authority: myValAddr.String(), - ConsensusAddress: myValAddr.String(), + Authority: authorityAddr, + ConsensusAddress: consAddr, Height: 10, - Vote: spec.vote, + Vote: vote, } rsp, err := server.Attest(ctx, msg) @@ -108,55 +155,74 @@ func TestAttestVotePayloadValidation(t *testing.T) { } func TestAttest(t *testing.T) { - ownerAddr := sdk.ValAddress("attester_owner") + chainID := "test-chain" + priv := cmted25519.GenPrivKey() + pub := priv.PubKey().(cmted25519.PubKey) + consAddr := sdk.ConsAddress(pub.Address()).String() + authorityAddr := sdk.AccAddress(pub.Address()).String() otherAddr := sdk.ValAddress("other_sender") + blockHash := bytes.Repeat([]byte{0x01}, 32) type testCase struct { - setup func(t *testing.T, ctx sdk.Context, keeper *Keeper, server msgServer) - msg *types.MsgAttest + setup func(t *testing.T, ctx sdk.Context, keeper *Keeper) + msg func() *types.MsgAttest expErr error } tests := map[string]testCase{ "valid": { - setup: func(t *testing.T, ctx sdk.Context, keeper *Keeper, server msgServer) { + setup: func(t *testing.T, ctx sdk.Context, keeper *Keeper) { t.Helper() require.NoError(t, keeper.SetParams(ctx, types.DefaultParams())) - 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)) + sdkPk, err := cryptocodec.FromCmtPubKeyInterface(pub) + require.NoError(t, err) + info, err := types.NewAttesterInfo(authorityAddr, sdkPk, 0) + require.NoError(t, err) + require.NoError(t, keeper.SetAttesterInfo(ctx, consAddr, info)) + require.NoError(t, keeper.SetAttesterSetMember(ctx, consAddr)) + require.NoError(t, keeper.SetValidatorIndex(ctx, consAddr, 0, 1)) }, - msg: &types.MsgAttest{ - Authority: ownerAddr.String(), - ConsensusAddress: ownerAddr.String(), - Height: 10, - Vote: bytes.Repeat([]byte{0x01}, 64), + msg: func() *types.MsgAttest { + return &types.MsgAttest{ + Authority: authorityAddr, + ConsensusAddress: consAddr, + Height: 10, + Vote: signTestVote(t, chainID, 10, priv, blockHash), + } }, }, "not_in_set": { - setup: func(t *testing.T, ctx sdk.Context, keeper *Keeper, server msgServer) { + setup: func(t *testing.T, ctx sdk.Context, keeper *Keeper) { t.Helper() }, - msg: &types.MsgAttest{ - Authority: ownerAddr.String(), - ConsensusAddress: ownerAddr.String(), - Height: 10, - Vote: bytes.Repeat([]byte{0x01}, 64), + msg: func() *types.MsgAttest { + return &types.MsgAttest{ + Authority: authorityAddr, + ConsensusAddress: consAddr, + Height: 10, + Vote: bytes.Repeat([]byte{0x01}, 64), + } }, expErr: sdkerrors.ErrUnauthorized, }, "wrong_authority": { - setup: func(t *testing.T, ctx sdk.Context, keeper *Keeper, server msgServer) { + setup: func(t *testing.T, ctx sdk.Context, keeper *Keeper) { t.Helper() - 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)) + sdkPk, err := cryptocodec.FromCmtPubKeyInterface(pub) + require.NoError(t, err) + info, err := types.NewAttesterInfo(authorityAddr, sdkPk, 0) + require.NoError(t, err) + require.NoError(t, keeper.SetAttesterInfo(ctx, consAddr, info)) + require.NoError(t, keeper.SetAttesterSetMember(ctx, consAddr)) + require.NoError(t, keeper.SetValidatorIndex(ctx, consAddr, 0, 1)) }, - msg: &types.MsgAttest{ - Authority: otherAddr.String(), - ConsensusAddress: ownerAddr.String(), - Height: 10, - Vote: bytes.Repeat([]byte{0x01}, 64), + msg: func() *types.MsgAttest { + return &types.MsgAttest{ + Authority: otherAddr.String(), + ConsensusAddress: consAddr, + Height: 10, + Vote: bytes.Repeat([]byte{0x01}, 64), + } }, expErr: sdkerrors.ErrUnauthorized, }, @@ -166,9 +232,9 @@ func TestAttest(t *testing.T) { sk := NewMockStakingKeeper() server, keeper, ctx := newTestServer(t, &sk) - spec.setup(t, ctx, &keeper, server) + spec.setup(t, ctx, &keeper) - rsp, err := server.Attest(ctx, spec.msg) + rsp, err := server.Attest(ctx, spec.msg()) if spec.expErr != nil { require.ErrorIs(t, err, spec.expErr) require.Nil(t, rsp) @@ -180,6 +246,80 @@ func TestAttest(t *testing.T) { } } +func TestAttestDuplicateDoesNotOverwriteState(t *testing.T) { + chainID := "test-chain" + priv := cmted25519.GenPrivKey() + pub := priv.PubKey().(cmted25519.PubKey) + consAddr := sdk.ConsAddress(pub.Address()).String() + authorityAddr := sdk.AccAddress(pub.Address()).String() + blockHash := bytes.Repeat([]byte{0x01}, 32) + + sk := NewMockStakingKeeper() + server, keeper, ctx := newTestServer(t, &sk) + keeper.SetBlockIDProvider(staticBlockIDProvider{hash: blockHash}) + require.NoError(t, keeper.SetParams(ctx, types.DefaultParams())) + require.NoError(t, registerTestAttester(ctx, &keeper, authorityAddr, consAddr, pub, 0)) + + firstVote := signTestVoteAt(t, chainID, 10, priv, blockHash, testTimeUTC()) + _, err := server.Attest(ctx, &types.MsgAttest{ + Authority: authorityAddr, + ConsensusAddress: consAddr, + Height: 10, + Vote: firstVote, + }) + require.NoError(t, err) + + secondVote := signTestVoteAt(t, chainID, 10, priv, blockHash, testTimeUTC().Add(time.Second)) + _, err = server.Attest(ctx, &types.MsgAttest{ + Authority: authorityAddr, + ConsensusAddress: consAddr, + Height: 10, + Vote: secondVote, + }) + require.ErrorIs(t, err, sdkerrors.ErrInvalidRequest) + require.Contains(t, err.Error(), "already attested") + + storedVote, err := keeper.GetSignature(ctx, 10, consAddr) + require.NoError(t, err) + require.Equal(t, firstVote, storedVote) + + bitmap, err := keeper.GetAttestationBitmap(ctx, 10) + require.NoError(t, err) + require.Equal(t, 1, keeper.bitmapHelper.PopCount(bitmap)) + require.True(t, keeper.bitmapHelper.IsSet(bitmap, 0)) +} + +func TestAttestRejectedVoteDoesNotWriteBitmapOrSignature(t *testing.T) { + chainID := "test-chain" + priv := cmted25519.GenPrivKey() + pub := priv.PubKey().(cmted25519.PubKey) + consAddr := sdk.ConsAddress(pub.Address()).String() + authorityAddr := sdk.AccAddress(pub.Address()).String() + sequencerHash := bytes.Repeat([]byte{0xaa}, 32) + forgedHash := bytes.Repeat([]byte{0xff}, 32) + + sk := NewMockStakingKeeper() + server, keeper, ctx := newTestServer(t, &sk) + keeper.SetBlockIDProvider(staticBlockIDProvider{hash: sequencerHash}) + require.NoError(t, keeper.SetParams(ctx, types.DefaultParams())) + require.NoError(t, registerTestAttester(ctx, &keeper, authorityAddr, consAddr, pub, 0)) + + _, err := server.Attest(ctx, &types.MsgAttest{ + Authority: authorityAddr, + ConsensusAddress: consAddr, + Height: 10, + Vote: signTestVote(t, chainID, 10, priv, forgedHash), + }) + require.ErrorIs(t, err, sdkerrors.ErrInvalidRequest) + + _, err = keeper.GetAttestationBitmap(ctx, 10) + require.ErrorIs(t, err, collections.ErrNotFound) + + hasSignature, err := keeper.HasSignature(ctx, 10, consAddr) + require.NoError(t, err) + require.False(t, hasSignature) +} + func newTestServer(t *testing.T, sk *MockStakingKeeper) (msgServer, Keeper, sdk.Context) { t.Helper() cdc := moduletestutil.MakeTestEncodingConfig().Codec @@ -188,6 +328,10 @@ func newTestServer(t *testing.T, sk *MockStakingKeeper) (msgServer, Keeper, sdk. cms := integration.CreateMultiStore(keys, logger) authority := authtypes.NewModuleAddress("gov") keeper := NewKeeper(cdc, runtime.NewKVStoreService(keys[types.StoreKey]), sk, nil, nil, authority.String()) + // Default-wire the block ID provider so Attest tests work without extra + // boilerplate. Tests that exercise BlockID-mismatch rejection override + // with their own provider before calling Attest. + keeper.SetBlockIDProvider(staticBlockIDProvider{hash: bytes.Repeat([]byte{0x01}, 32)}) server := msgServer{Keeper: keeper} ctx := sdk.NewContext(cms, cmtproto.Header{ChainID: "test-chain", Time: time.Now().UTC(), Height: 10}, false, logger). WithContext(t.Context()) @@ -195,8 +339,6 @@ func newTestServer(t *testing.T, sk *MockStakingKeeper) (msgServer, Keeper, sdk. } func TestAttestHeightBounds(t *testing.T) { - myValAddr := sdk.ValAddress("validator1") - ownerAddr := sdk.ValAddress("attester_owner") // With DefaultParams: EpochLength=1, PruneAfter=15 // At blockHeight=100: currentEpoch=100, minHeight=(100-7)*1=93 specs := map[string]struct { @@ -243,32 +385,47 @@ func TestAttestHeightBounds(t *testing.T) { } for name, spec := range specs { t.Run(name, func(t *testing.T) { + chainID := "test-chain" + priv := cmted25519.GenPrivKey() + pub := priv.PubKey().(cmted25519.PubKey) + consAddr := sdk.ConsAddress(pub.Address()).String() + authorityAddr := sdk.AccAddress(pub.Address()).String() + sk := NewMockStakingKeeper() cdc := moduletestutil.MakeTestEncodingConfig().Codec keys := storetypes.NewKVStoreKeys(types.StoreKey) logger := log.NewTestLogger(t) cms := integration.CreateMultiStore(keys, logger) authority := authtypes.NewModuleAddress("gov") - keeper := NewKeeper(cdc, runtime.NewKVStoreService(keys[types.StoreKey]), sk, nil, nil, authority.String()) + keeper := NewKeeper(cdc, runtime.NewKVStoreService(keys[types.StoreKey]), &sk, nil, nil, authority.String()) + blockHash := bytes.Repeat([]byte{0x01}, 32) + keeper.SetBlockIDProvider(staticBlockIDProvider{hash: blockHash}) server := msgServer{Keeper: keeper} ctx := sdk.NewContext(cms, cmtproto.Header{ - ChainID: "test-chain", + ChainID: chainID, Time: time.Now().UTC(), Height: spec.blockHeight, }, false, logger).WithContext(t.Context()) require.NoError(t, keeper.SetParams(ctx, types.DefaultParams())) - 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)) + // Register the attester directly via keeper (no MsgJoin) + sdkPk, err := cryptocodec.FromCmtPubKeyInterface(pub) + require.NoError(t, err) + info, err := types.NewAttesterInfo(authorityAddr, sdkPk, 0) + require.NoError(t, err) + require.NoError(t, keeper.SetAttesterInfo(ctx, consAddr, info)) + require.NoError(t, keeper.SetAttesterSetMember(ctx, consAddr)) + require.NoError(t, keeper.SetValidatorIndex(ctx, consAddr, 0, 1)) + + // Build a signed vote for the expected height + voteBytes := signTestVote(t, chainID, spec.attestH, priv, blockHash) - // when msg := &types.MsgAttest{ - Authority: ownerAddr.String(), - ConsensusAddress: myValAddr.String(), + Authority: authorityAddr, + ConsensusAddress: consAddr, Height: spec.attestH, - Vote: make([]byte, MinVoteLen), + Vote: voteBytes, } rsp, err := server.Attest(ctx, msg) if spec.expErr != nil { @@ -282,6 +439,32 @@ func TestAttestHeightBounds(t *testing.T) { } } +func TestGetAllSignaturesForHeightUsesValidatorIndexOrder(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") + + 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)) + + bitmap := keeper.bitmapHelper.NewBitmap(2) + keeper.bitmapHelper.SetBit(bitmap, 0) + require.NoError(t, keeper.SetAttestationBitmap(ctx, height, bitmap)) + require.NoError(t, keeper.SetSignature(ctx, height, indexZeroAddr, signature)) + + signatures, err := keeper.GetAllSignaturesForHeight(ctx, height) + require.NoError(t, err) + require.Equal(t, map[string][]byte{ + indexZeroAddr: signature, + }, signatures) +} + var _ types.StakingKeeper = &MockStakingKeeper{} type MockStakingKeeper struct { @@ -325,3 +508,376 @@ func (m MockStakingKeeper) GetLastValidators(ctx context.Context) (validators [] func (m MockStakingKeeper) GetLastTotalPower(ctx context.Context) (math.Int, error) { return math.NewInt(int64(len(m.activeSet))), nil } + +func TestVerifyVote(t *testing.T) { + chainID := "test-chain" + priv := cmted25519.GenPrivKey() + pub := priv.PubKey().(cmted25519.PubKey) + consAddr := sdk.ConsAddress(pub.Address()).String() + // 32-byte block hash (CanonicalizeBlockID requires 32 bytes or empty) + blockHash := bytes.Repeat([]byte{0xbb}, 32) + + sk := NewMockStakingKeeper() + _, keeper, ctx := newTestServer(t, &sk) + // Override the default provider so the "valid" spec's BlockID.Hash + // matches the sequencer's stored hash. + keeper.SetBlockIDProvider(staticBlockIDProvider{hash: blockHash}) + + sdkPk, err := cryptocodec.FromCmtPubKeyInterface(pub) + require.NoError(t, err) + info, err := types.NewAttesterInfo(sdk.AccAddress(pub.Address()).String(), sdkPk, 0) + require.NoError(t, err) + require.NoError(t, keeper.SetAttesterInfo(ctx, consAddr, info)) + + validBytes := signTestVote(t, chainID, 42, priv, blockHash) + + specs := map[string]struct { + consAddr string + vote []byte + msgH int64 + expErr error + }{ + "valid": { + consAddr: consAddr, + vote: validBytes, + msgH: 42, + }, + "wrong chain id": { + consAddr: consAddr, + vote: signTestVote(t, "other-chain", 42, priv, blockHash), + msgH: 42, + expErr: sdkerrors.ErrUnauthorized, + }, + "wrong height": { + consAddr: consAddr, + vote: validBytes, + msgH: 99, + expErr: sdkerrors.ErrInvalidRequest, + }, + "random 64 bytes": { + consAddr: consAddr, + vote: bytes.Repeat([]byte{0x01}, 64), + msgH: 42, + expErr: sdkerrors.ErrInvalidRequest, // unmarshal may succeed but checks fail + }, + "signed by different key": { + consAddr: consAddr, + vote: signTestVote(t, chainID, 42, cmted25519.GenPrivKey(), blockHash), + msgH: 42, + expErr: sdkerrors.ErrUnauthorized, + }, + "prevote type": { + consAddr: consAddr, + vote: func() []byte { + v := cmtproto.Vote{ + Type: cmtproto.PrevoteType, + Height: 42, + Round: 0, + BlockID: cmtproto.BlockID{Hash: blockHash, PartSetHeader: cmtproto.PartSetHeader{}}, + Timestamp: testTimeUTC(), + ValidatorAddress: pub.Address(), + } + sb := cmttypes.VoteSignBytes(chainID, &v) + sig, _ := priv.Sign(sb) + v.Signature = sig + bz, _ := proto.Marshal(&v) + return bz + }(), + msgH: 42, + expErr: sdkerrors.ErrInvalidRequest, + }, + "non-zero round": { + consAddr: consAddr, + vote: func() []byte { + v := cmtproto.Vote{ + Type: cmtproto.PrecommitType, + Height: 42, + Round: 1, + BlockID: cmtproto.BlockID{Hash: blockHash, PartSetHeader: cmtproto.PartSetHeader{}}, + Timestamp: testTimeUTC(), + ValidatorAddress: pub.Address(), + } + sb := cmttypes.VoteSignBytes(chainID, &v) + sig, _ := priv.Sign(sb) + v.Signature = sig + bz, _ := proto.Marshal(&v) + return bz + }(), + msgH: 42, + expErr: sdkerrors.ErrInvalidRequest, + }, + "unknown consensus address": { + consAddr: sdk.ConsAddress(bytes.Repeat([]byte{0x77}, 20)).String(), + vote: validBytes, + msgH: 42, + expErr: sdkerrors.ErrNotFound, + }, + } + + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + sdkCtx := ctx.WithBlockHeader(cmtproto.Header{ChainID: chainID}) + _, err := keeper.VerifyVoteForTest(sdkCtx, spec.consAddr, spec.vote, spec.msgH) + if spec.expErr != nil { + require.ErrorIs(t, err, spec.expErr) + return + } + require.NoError(t, err) + }) + } +} + +// TestVerifyVote_RejectsMismatchedBlockIDHash is a regression for the +// attester-forged-BlockID vector: the attester produces a self-consistent +// signed vote but over a BlockID.Hash that does not match what the +// sequencer stored for the height. The 07-tendermint light client would +// later reject the reconstructed commit; MsgAttest must fail fast here. +func TestVerifyVote_RejectsMismatchedBlockIDHash(t *testing.T) { + chainID := "test-chain" + priv := cmted25519.GenPrivKey() + pub := priv.PubKey().(cmted25519.PubKey) + consAddr := sdk.ConsAddress(pub.Address()).String() + sequencerHash := bytes.Repeat([]byte{0xaa}, 32) + forgedHash := bytes.Repeat([]byte{0xff}, 32) + + sk := NewMockStakingKeeper() + _, keeper, ctx := newTestServer(t, &sk) + keeper.SetBlockIDProvider(staticBlockIDProvider{hash: sequencerHash}) + + sdkPk, err := cryptocodec.FromCmtPubKeyInterface(pub) + require.NoError(t, err) + info, err := types.NewAttesterInfo(sdk.AccAddress(pub.Address()).String(), sdkPk, 0) + require.NoError(t, err) + require.NoError(t, keeper.SetAttesterInfo(ctx, consAddr, info)) + + sdkCtx := ctx.WithBlockHeader(cmtproto.Header{ChainID: chainID}) + + // Attester signs a well-formed vote but over the forged hash. + forgedVote := signTestVote(t, chainID, 42, priv, forgedHash) + _, err = keeper.VerifyVoteForTest(sdkCtx, consAddr, forgedVote, 42) + require.ErrorIs(t, err, sdkerrors.ErrInvalidRequest) + require.Contains(t, err.Error(), "does not match sequencer BlockID") + + // Control: the same machinery accepts a vote over the real hash. + realVote := signTestVote(t, chainID, 42, priv, sequencerHash) + _, err = keeper.VerifyVoteForTest(sdkCtx, consAddr, realVote, 42) + require.NoError(t, err) +} + +func TestVerifyVote_RejectsMismatchedBlockIDPartSetHeader(t *testing.T) { + chainID := "test-chain" + priv := cmted25519.GenPrivKey() + pub := priv.PubKey().(cmted25519.PubKey) + consAddr := sdk.ConsAddress(pub.Address()).String() + blockHash := bytes.Repeat([]byte{0xaa}, 32) + storedPartSetHash := bytes.Repeat([]byte{0x11}, 32) + forgedPartSetHash := bytes.Repeat([]byte{0x22}, 32) + + sk := NewMockStakingKeeper() + _, keeper, ctx := newTestServer(t, &sk) + keeper.SetBlockIDProvider(staticBlockIDProvider{ + hash: blockHash, + partSetHeader: cmttypes.PartSetHeader{ + Total: 1, + Hash: storedPartSetHash, + }, + }) + + sdkPk, err := cryptocodec.FromCmtPubKeyInterface(pub) + require.NoError(t, err) + info, err := types.NewAttesterInfo(sdk.AccAddress(pub.Address()).String(), sdkPk, 0) + require.NoError(t, err) + require.NoError(t, keeper.SetAttesterInfo(ctx, consAddr, info)) + + sdkCtx := ctx.WithBlockHeader(cmtproto.Header{ChainID: chainID}) + forgedBlockID := cmtproto.BlockID{ + Hash: blockHash, + PartSetHeader: cmtproto.PartSetHeader{ + Total: 1, + Hash: forgedPartSetHash, + }, + } + forgedVote := signTestVoteWithBlockID(t, chainID, 42, priv, forgedBlockID) + + _, err = keeper.VerifyVoteForTest(sdkCtx, consAddr, forgedVote, 42) + require.ErrorIs(t, err, sdkerrors.ErrInvalidRequest) + require.Contains(t, err.Error(), "does not match sequencer BlockID") +} + +func TestVerifyVote_RejectsBlockIDProviderError(t *testing.T) { + chainID := "test-chain" + priv := cmted25519.GenPrivKey() + pub := priv.PubKey().(cmted25519.PubKey) + consAddr := sdk.ConsAddress(pub.Address()).String() + blockHash := bytes.Repeat([]byte{0x01}, 32) + + sk := NewMockStakingKeeper() + _, keeper, ctx := newTestServer(t, &sk) + keeper.SetBlockIDProvider(failingBlockIDProvider{err: errors.New("store unavailable")}) + + sdkPk, err := cryptocodec.FromCmtPubKeyInterface(pub) + require.NoError(t, err) + info, err := types.NewAttesterInfo(sdk.AccAddress(pub.Address()).String(), sdkPk, 0) + require.NoError(t, err) + require.NoError(t, keeper.SetAttesterInfo(ctx, consAddr, info)) + + sdkCtx := ctx.WithBlockHeader(cmtproto.Header{ChainID: chainID}) + _, err = keeper.VerifyVoteForTest(sdkCtx, consAddr, signTestVote(t, chainID, 10, priv, blockHash), 10) + require.ErrorIs(t, err, sdkerrors.ErrInvalidRequest) + require.Contains(t, err.Error(), "get block ID for height 10") + require.Contains(t, err.Error(), "store unavailable") +} + +func TestVerifyVote_RejectsNilProviderBlockID(t *testing.T) { + chainID := "test-chain" + priv := cmted25519.GenPrivKey() + pub := priv.PubKey().(cmted25519.PubKey) + consAddr := sdk.ConsAddress(pub.Address()).String() + blockHash := bytes.Repeat([]byte{0x01}, 32) + + sk := NewMockStakingKeeper() + _, keeper, ctx := newTestServer(t, &sk) + keeper.SetBlockIDProvider(nilBlockIDProvider{}) + + sdkPk, err := cryptocodec.FromCmtPubKeyInterface(pub) + require.NoError(t, err) + info, err := types.NewAttesterInfo(sdk.AccAddress(pub.Address()).String(), sdkPk, 0) + require.NoError(t, err) + require.NoError(t, keeper.SetAttesterInfo(ctx, consAddr, info)) + + sdkCtx := ctx.WithBlockHeader(cmtproto.Header{ChainID: chainID}) + _, err = keeper.VerifyVoteForTest(sdkCtx, consAddr, signTestVote(t, chainID, 10, priv, blockHash), 10) + require.ErrorIs(t, err, sdkerrors.ErrInvalidRequest) + require.Contains(t, err.Error(), "block ID for height 10 not found") +} + +func TestVerifyVote_RejectsMalformedBlockID(t *testing.T) { + chainID := "test-chain" + priv := cmted25519.GenPrivKey() + pub := priv.PubKey().(cmted25519.PubKey) + consAddr := sdk.ConsAddress(pub.Address()).String() + + sk := NewMockStakingKeeper() + _, keeper, ctx := newTestServer(t, &sk) + + sdkPk, err := cryptocodec.FromCmtPubKeyInterface(pub) + require.NoError(t, err) + info, err := types.NewAttesterInfo(sdk.AccAddress(pub.Address()).String(), sdkPk, 0) + require.NoError(t, err) + require.NoError(t, keeper.SetAttesterInfo(ctx, consAddr, info)) + + malformedBlockID := cmtproto.BlockID{ + Hash: bytes.Repeat([]byte{0x01}, 31), + PartSetHeader: cmtproto.PartSetHeader{ + Total: 1, + Hash: bytes.Repeat([]byte{0x02}, 32), + }, + } + vote := cmtproto.Vote{ + Type: cmtproto.PrecommitType, + Height: 10, + Round: 0, + BlockID: malformedBlockID, + Timestamp: testTimeUTC(), + ValidatorAddress: pub.Address(), + ValidatorIndex: 0, + Signature: bytes.Repeat([]byte{0x03}, 64), + } + voteBytes, err := proto.Marshal(&vote) + require.NoError(t, err) + + sdkCtx := ctx.WithBlockHeader(cmtproto.Header{ChainID: chainID}) + _, err = keeper.VerifyVoteForTest(sdkCtx, consAddr, voteBytes, 10) + require.ErrorIs(t, err, sdkerrors.ErrInvalidRequest) + require.Contains(t, err.Error(), "invalid vote BlockID") +} + +// TestVerifyVote_RejectsUnwiredProvider guards against a misconfigured app +// where SetBlockIDProvider is never called — MsgAttest must fail closed +// rather than silently accept every vote. +func TestVerifyVote_RejectsUnwiredProvider(t *testing.T) { + chainID := "test-chain" + priv := cmted25519.GenPrivKey() + pub := priv.PubKey().(cmted25519.PubKey) + consAddr := sdk.ConsAddress(pub.Address()).String() + + sk := NewMockStakingKeeper() + cdc := moduletestutil.MakeTestEncodingConfig().Codec + keys := storetypes.NewKVStoreKeys(types.StoreKey) + logger := log.NewTestLogger(t) + cms := integration.CreateMultiStore(keys, logger) + authority := authtypes.NewModuleAddress("gov") + // Intentionally do NOT call SetBlockIDProvider. + keeper := NewKeeper(cdc, runtime.NewKVStoreService(keys[types.StoreKey]), &sk, nil, nil, authority.String()) + ctx := sdk.NewContext(cms, cmtproto.Header{ChainID: chainID, Time: time.Now().UTC(), Height: 10}, false, logger). + WithContext(t.Context()) + + sdkPk, err := cryptocodec.FromCmtPubKeyInterface(pub) + require.NoError(t, err) + info, err := types.NewAttesterInfo(sdk.AccAddress(pub.Address()).String(), sdkPk, 0) + require.NoError(t, err) + require.NoError(t, keeper.SetAttesterInfo(ctx, consAddr, info)) + + voteBytes := signTestVote(t, chainID, 10, priv, bytes.Repeat([]byte{0x01}, 32)) + sdkCtx := ctx.WithBlockHeader(cmtproto.Header{ChainID: chainID}) + + _, err = keeper.VerifyVoteForTest(sdkCtx, consAddr, voteBytes, 10) + require.Error(t, err) + require.Contains(t, err.Error(), "provider not wired") +} + +func registerTestAttester( + ctx sdk.Context, + keeper *Keeper, + authorityAddr string, + consAddr string, + pub cmted25519.PubKey, + index uint16, +) error { + sdkPk, err := cryptocodec.FromCmtPubKeyInterface(pub) + if err != nil { + return fmt.Errorf("convert consensus pubkey: %w", err) + } + info, err := types.NewAttesterInfo(authorityAddr, sdkPk, 0) + if err != nil { + return fmt.Errorf("create attester info: %w", err) + } + if err := keeper.SetAttesterInfo(ctx, consAddr, info); err != nil { + return fmt.Errorf("set attester info: %w", err) + } + if err := keeper.SetAttesterSetMember(ctx, consAddr); err != nil { + return fmt.Errorf("set attester set member: %w", err) + } + if err := keeper.SetValidatorIndex(ctx, consAddr, index, 1); err != nil { + return fmt.Errorf("set validator index: %w", err) + } + return nil +} + +func signTestVoteAt( + t *testing.T, + chainID string, + height int64, + priv cmted25519.PrivKey, + blockIDHash []byte, + timestamp time.Time, +) []byte { + t.Helper() + pub := priv.PubKey().(cmted25519.PubKey) + v := cmtproto.Vote{ + Type: cmtproto.PrecommitType, + Height: height, + Round: 0, + BlockID: cmtproto.BlockID{Hash: blockIDHash, PartSetHeader: cmtproto.PartSetHeader{}}, + Timestamp: timestamp, + ValidatorAddress: pub.Address(), + ValidatorIndex: 0, + } + sb := cmttypes.VoteSignBytes(chainID, &v) + sig, err := priv.Sign(sb) + require.NoError(t, err) + v.Signature = sig + out, err := proto.Marshal(&v) + require.NoError(t, err) + return out +} diff --git a/modules/network/keeper/testhelpers_test.go b/modules/network/keeper/testhelpers_test.go new file mode 100644 index 00000000..72958926 --- /dev/null +++ b/modules/network/keeper/testhelpers_test.go @@ -0,0 +1,67 @@ +package keeper + +import ( + "bytes" + "context" + "testing" + "time" + + cmted25519 "github.com/cometbft/cometbft/crypto/ed25519" + cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" + cmttypes "github.com/cometbft/cometbft/types" + "github.com/cosmos/gogoproto/proto" + "github.com/stretchr/testify/require" +) + +// staticBlockIDProvider is a test double returning the same BlockID regardless +// of height. Mirrors the sequencer's view of a stored block hash. +type staticBlockIDProvider struct { + hash []byte + partSetHeader cmttypes.PartSetHeader +} + +func (s staticBlockIDProvider) GetBlockID(_ context.Context, _ uint64) (*cmttypes.BlockID, error) { + return &cmttypes.BlockID{Hash: s.hash, PartSetHeader: s.partSetHeader}, nil +} + +// signTestVote builds a cmtproto.Vote for the given height and key and returns +// the protobuf-marshaled bytes with the signature attached. +func signTestVote(t *testing.T, chainID string, height int64, priv cmted25519.PrivKey, blockIDHash []byte) []byte { + t.Helper() + return signTestVoteWithBlockID(t, chainID, height, priv, cmtproto.BlockID{ + Hash: blockIDHash, + PartSetHeader: cmtproto.PartSetHeader{}, + }) +} + +func signTestVoteWithBlockID(t *testing.T, chainID string, height int64, priv cmted25519.PrivKey, blockID cmtproto.BlockID) []byte { + t.Helper() + pub := priv.PubKey().(cmted25519.PubKey) + v := cmtproto.Vote{ + Type: cmtproto.PrecommitType, + Height: height, + Round: 0, + BlockID: blockID, + Timestamp: testTimeUTC(), + ValidatorAddress: pub.Address(), + ValidatorIndex: 0, + } + sb := cmttypes.VoteSignBytes(chainID, &v) + sig, err := priv.Sign(sb) + require.NoError(t, err) + v.Signature = sig + out, err := proto.Marshal(&v) + require.NoError(t, err) + return out +} + +// testTimeUTC returns a fixed deterministic time for vote timestamps. +func testTimeUTC() time.Time { + return time.Date(2026, 4, 22, 12, 0, 0, 0, time.UTC) +} + +func TestSignTestVoteCompiles(t *testing.T) { + priv := cmted25519.GenPrivKey() + bz := signTestVote(t, "chain", 10, priv, bytes.Repeat([]byte{0xab}, 32)) + require.NotEmpty(t, bz) +} diff --git a/modules/network/types/expected_keepers.go b/modules/network/types/expected_keepers.go index e769df9c..cc1a23bf 100644 --- a/modules/network/types/expected_keepers.go +++ b/modules/network/types/expected_keepers.go @@ -4,6 +4,7 @@ import ( "context" "cosmossdk.io/math" + cmttypes "github.com/cometbft/cometbft/types" sdk "github.com/cosmos/cosmos-sdk/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" @@ -32,3 +33,9 @@ type BankKeeper interface { type BlockSource interface { GetBlockData(ctx context.Context, height uint64) (*tyrollkittypes.SignedHeader, *tyrollkittypes.Data, error) } + +// BlockIDProvider returns the canonical CometBFT BlockID for a given rollkit height. +// Used by the network module to pin attester votes to the sequencer's block hash. +type BlockIDProvider interface { + GetBlockID(ctx context.Context, height uint64) (*cmttypes.BlockID, error) +} diff --git a/server/start.go b/server/start.go index 53ef9bf7..194e2fc0 100644 --- a/server/start.go +++ b/server/start.go @@ -54,6 +54,7 @@ import ( "github.com/evstack/ev-node/pkg/store" rollkittypes "github.com/evstack/ev-node/types" + "github.com/evstack/ev-abci/modules/network/types" "github.com/evstack/ev-abci/pkg/adapter" "github.com/evstack/ev-abci/pkg/rpc" "github.com/evstack/ev-abci/pkg/rpc/core" @@ -61,6 +62,14 @@ import ( execstore "github.com/evstack/ev-abci/pkg/store" ) +// networkKeeperBlockIDWirer is the minimal interface an application must +// expose so the ev-abci server can attach the adapter block store to the +// network module's keeper. Applications satisfy it by declaring a method +// that accepts the BlockIDProvider and forwards it to the network keeper. +type networkKeeperBlockIDWirer interface { + SetNetworkKeeperBlockIDProvider(types.BlockIDProvider) +} + const ( flagTraceStore = "trace-store" flagGRPCOnly = "grpc-only" @@ -436,6 +445,14 @@ func setupNodeAndExecutor( opts..., ) + // Give the network module's MsgAttest handler access to the adapter's + // block store so it can pin each vote to the sequencer's real BlockID. + if w, ok := app.(networkKeeperBlockIDWirer); ok { + w.SetNetworkKeeperBlockIDProvider(executor.Store) + } else { + sdkLogger.Warn("app does not implement networkKeeperBlockIDWirer; MsgAttest will reject votes if attester mode is enabled") + } + cmtApp := sdkserver.NewCometABCIWrapper(app) clientCreator := proxy.NewLocalClientCreator(cmtApp) From 8cab24af3c07fbc9984083eb971cf6d3fdc732dd Mon Sep 17 00:00:00 2001 From: Randy Grok Date: Tue, 28 Apr 2026 17:04:54 +0200 Subject: [PATCH 2/2] feat(rpc): reconstruct attester commits deterministically --- modules/network/keeper/grpc_query.go | 28 + modules/network/types/query.pb.go | 777 ++++++++++++++++++-- modules/network/types/query.pb.gw.go | 65 ++ modules/proto/evabci/network/v1/query.proto | 25 + pkg/adapter/providers_test.go | 66 ++ pkg/rpc/core/blocks.go | 124 ++-- pkg/rpc/core/commit_reconstruction_test.go | 295 ++++++++ pkg/rpc/core/consensus.go | 162 +--- pkg/rpc/core/consensus_test.go | 106 +++ 9 files changed, 1402 insertions(+), 246 deletions(-) create mode 100644 pkg/adapter/providers_test.go create mode 100644 pkg/rpc/core/commit_reconstruction_test.go diff --git a/modules/network/keeper/grpc_query.go b/modules/network/keeper/grpc_query.go index 3c9f91e4..763f3896 100644 --- a/modules/network/keeper/grpc_query.go +++ b/modules/network/keeper/grpc_query.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "sort" "cosmossdk.io/collections" sdk "github.com/cosmos/cosmos-sdk/types" @@ -237,6 +238,33 @@ func (q *queryServer) LastAttestedHeight(c context.Context, req *types.QueryLast }, nil } +// AttesterSet queries the full ordered attester set +func (q *queryServer) AttesterSet(goCtx context.Context, req *types.QueryAttesterSetRequest) (*types.QueryAttesterSetResponse, error) { + if req == nil { + return nil, status.Error(codes.InvalidArgument, "invalid request") + } + + 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 { + return nil, err + } + sort.Slice(entries, func(i, j int) bool { return entries[i].Index < entries[j].Index }) + return &types.QueryAttesterSetResponse{Entries: entries}, nil +} + // AttesterInfo queries the attester information including public key func (q *queryServer) AttesterInfo(c context.Context, req *types.QueryAttesterInfoRequest) (*types.QueryAttesterInfoResponse, error) { if req == nil { diff --git a/modules/network/types/query.pb.go b/modules/network/types/query.pb.go index 3b389163..7c788d82 100644 --- a/modules/network/types/query.pb.go +++ b/modules/network/types/query.pb.go @@ -6,6 +6,8 @@ package types import ( context "context" fmt "fmt" + _ "github.com/cosmos/cosmos-proto" + types "github.com/cosmos/cosmos-sdk/codec/types" _ "github.com/cosmos/cosmos-sdk/types/query" _ "github.com/cosmos/gogoproto/gogoproto" grpc1 "github.com/cosmos/gogoproto/grpc" @@ -852,6 +854,129 @@ func (m *QueryAttesterInfoResponse) GetAttesterInfo() *AttesterInfo { return nil } +// QueryAttesterSetRequest is the request type for the Query/AttesterSet RPC method. +type QueryAttesterSetRequest struct { +} + +func (m *QueryAttesterSetRequest) Reset() { *m = QueryAttesterSetRequest{} } +func (m *QueryAttesterSetRequest) String() string { return proto.CompactTextString(m) } +func (*QueryAttesterSetRequest) ProtoMessage() {} +func (*QueryAttesterSetRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_faab6bfc228a74e1, []int{17} +} +func (m *QueryAttesterSetRequest) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *QueryAttesterSetRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_QueryAttesterSetRequest.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *QueryAttesterSetRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_QueryAttesterSetRequest.Merge(m, src) +} +func (m *QueryAttesterSetRequest) XXX_Size() int { + return m.Size() +} +func (m *QueryAttesterSetRequest) XXX_DiscardUnknown() { + xxx_messageInfo_QueryAttesterSetRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_QueryAttesterSetRequest proto.InternalMessageInfo + +// 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"` + ConsensusAddress string `protobuf:"bytes,2,opt,name=consensus_address,json=consensusAddress,proto3" json:"consensus_address,omitempty"` + Index uint32 `protobuf:"varint,3,opt,name=index,proto3" json:"index,omitempty"` + Pubkey *types.Any `protobuf:"bytes,4,opt,name=pubkey,proto3" json:"pubkey,omitempty"` +} + +func (m *AttesterSetEntry) Reset() { *m = AttesterSetEntry{} } +func (m *AttesterSetEntry) String() string { return proto.CompactTextString(m) } +func (*AttesterSetEntry) ProtoMessage() {} +func (*AttesterSetEntry) Descriptor() ([]byte, []int) { + return fileDescriptor_faab6bfc228a74e1, []int{18} +} +func (m *AttesterSetEntry) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *AttesterSetEntry) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_AttesterSetEntry.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *AttesterSetEntry) XXX_Merge(src proto.Message) { + xxx_messageInfo_AttesterSetEntry.Merge(m, src) +} +func (m *AttesterSetEntry) XXX_Size() int { + return m.Size() +} +func (m *AttesterSetEntry) XXX_DiscardUnknown() { + xxx_messageInfo_AttesterSetEntry.DiscardUnknown(m) +} + +var xxx_messageInfo_AttesterSetEntry proto.InternalMessageInfo + +// QueryAttesterSetResponse is the response type for the Query/AttesterSet RPC method. +type QueryAttesterSetResponse struct { + Entries []AttesterSetEntry `protobuf:"bytes,1,rep,name=entries,proto3" json:"entries"` +} + +func (m *QueryAttesterSetResponse) Reset() { *m = QueryAttesterSetResponse{} } +func (m *QueryAttesterSetResponse) String() string { return proto.CompactTextString(m) } +func (*QueryAttesterSetResponse) ProtoMessage() {} +func (*QueryAttesterSetResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_faab6bfc228a74e1, []int{19} +} +func (m *QueryAttesterSetResponse) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *QueryAttesterSetResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_QueryAttesterSetResponse.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *QueryAttesterSetResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_QueryAttesterSetResponse.Merge(m, src) +} +func (m *QueryAttesterSetResponse) XXX_Size() int { + return m.Size() +} +func (m *QueryAttesterSetResponse) XXX_DiscardUnknown() { + xxx_messageInfo_QueryAttesterSetResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_QueryAttesterSetResponse proto.InternalMessageInfo + +func (m *QueryAttesterSetResponse) GetEntries() []AttesterSetEntry { + if m != nil { + return m.Entries + } + return nil +} + func init() { proto.RegisterType((*QueryParamsRequest)(nil), "evabci.network.v1.QueryParamsRequest") proto.RegisterType((*QueryParamsResponse)(nil), "evabci.network.v1.QueryParamsResponse") @@ -870,77 +995,94 @@ func init() { proto.RegisterType((*QueryLastAttestedHeightResponse)(nil), "evabci.network.v1.QueryLastAttestedHeightResponse") proto.RegisterType((*QueryAttesterInfoRequest)(nil), "evabci.network.v1.QueryAttesterInfoRequest") proto.RegisterType((*QueryAttesterInfoResponse)(nil), "evabci.network.v1.QueryAttesterInfoResponse") + proto.RegisterType((*QueryAttesterSetRequest)(nil), "evabci.network.v1.QueryAttesterSetRequest") + proto.RegisterType((*AttesterSetEntry)(nil), "evabci.network.v1.AttesterSetEntry") + proto.RegisterType((*QueryAttesterSetResponse)(nil), "evabci.network.v1.QueryAttesterSetResponse") } func init() { proto.RegisterFile("evabci/network/v1/query.proto", fileDescriptor_faab6bfc228a74e1) } var fileDescriptor_faab6bfc228a74e1 = []byte{ - // 1040 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x56, 0xcf, 0x6f, 0x1b, 0x45, - 0x14, 0xce, 0x36, 0x89, 0x21, 0x2f, 0xa1, 0x6d, 0xa6, 0x21, 0x24, 0x6e, 0xe3, 0x38, 0x0b, 0xb4, - 0x6e, 0x8b, 0x3d, 0x75, 0x50, 0x1b, 0x2a, 0xf5, 0xd2, 0xf0, 0xb3, 0x08, 0xa1, 0xb2, 0x91, 0x7a, - 0xe0, 0x80, 0x35, 0xf6, 0x8e, 0x37, 0xab, 0xda, 0x3b, 0x9b, 0x9d, 0xb1, 0x4b, 0x89, 0x72, 0xe1, - 0x08, 0x17, 0x24, 0xfe, 0x00, 0x04, 0x37, 0x04, 0x7f, 0x02, 0xe2, 0xdc, 0x63, 0x25, 0x2e, 0x9c, - 0x10, 0x4a, 0xf8, 0x43, 0xd0, 0xbe, 0x19, 0xaf, 0xd7, 0xf1, 0xae, 0x9d, 0x9c, 0x92, 0x7d, 0xef, - 0xfb, 0xde, 0xfb, 0xe6, 0xcd, 0xcc, 0x37, 0x86, 0x0d, 0xde, 0x67, 0xcd, 0x96, 0x4f, 0x03, 0xae, - 0x9e, 0x89, 0xe8, 0x29, 0xed, 0xd7, 0xe9, 0x41, 0x8f, 0x47, 0xcf, 0x6b, 0x61, 0x24, 0x94, 0x20, - 0xcb, 0x3a, 0x5d, 0x33, 0xe9, 0x5a, 0xbf, 0x5e, 0x5c, 0xf1, 0x84, 0x27, 0x30, 0x4b, 0xe3, 0xff, - 0x34, 0xb0, 0x78, 0xcd, 0x13, 0xc2, 0xeb, 0x70, 0xca, 0x42, 0x9f, 0xb2, 0x20, 0x10, 0x8a, 0x29, - 0x5f, 0x04, 0xd2, 0x64, 0x6f, 0xb5, 0x84, 0xec, 0x0a, 0x49, 0x9b, 0x4c, 0x72, 0x5d, 0x9f, 0xf6, - 0xeb, 0x4d, 0xae, 0x58, 0x9d, 0x86, 0xcc, 0xf3, 0x03, 0x04, 0x1b, 0x6c, 0x86, 0x22, 0xf5, 0x3c, - 0xe4, 0x83, 0x52, 0xe5, 0xf1, 0x34, 0x53, 0x8a, 0x4b, 0xc5, 0x23, 0x8d, 0xb0, 0x57, 0x80, 0x7c, - 0x11, 0xb7, 0x78, 0xcc, 0x22, 0xd6, 0x95, 0x0e, 0x3f, 0xe8, 0x71, 0xa9, 0xec, 0xcf, 0xe1, 0xca, - 0x48, 0x54, 0x86, 0x22, 0x90, 0x9c, 0xec, 0x40, 0x21, 0xc4, 0xc8, 0x9a, 0x55, 0xb6, 0x2a, 0x8b, - 0xdb, 0xeb, 0xb5, 0xb1, 0x15, 0xd7, 0x34, 0x65, 0x77, 0xee, 0xc5, 0x3f, 0x9b, 0x33, 0x8e, 0x81, - 0xdb, 0x3b, 0xb0, 0x81, 0xf5, 0x1e, 0x62, 0x73, 0x5c, 0xc0, 0xae, 0xaf, 0xba, 0x2c, 0x34, 0x0d, - 0xc9, 0x2a, 0x14, 0xf6, 0xb9, 0xef, 0xed, 0x2b, 0xac, 0x3c, 0xeb, 0x98, 0x2f, 0xfb, 0x2b, 0x28, - 0xe5, 0x11, 0x8d, 0xa6, 0x07, 0x50, 0x68, 0x62, 0xc4, 0x68, 0x7a, 0x2b, 0x43, 0xd3, 0x38, 0xdb, - 0x70, 0xec, 0x2a, 0xbc, 0x8e, 0xf5, 0x3f, 0x0c, 0x45, 0x6b, 0xff, 0x51, 0xd0, 0x16, 0x03, 0x41, - 0x2b, 0x30, 0xcf, 0xe3, 0x18, 0x56, 0x9d, 0x73, 0xf4, 0x87, 0xfd, 0xfd, 0x05, 0x58, 0x3d, 0x8d, - 0x37, 0x3a, 0x32, 0x09, 0x64, 0x0b, 0x96, 0xa4, 0x62, 0x91, 0x6a, 0x98, 0xd5, 0x5d, 0xc0, 0xd5, - 0x2d, 0x62, 0xec, 0x13, 0x0c, 0x91, 0x0d, 0x00, 0x1e, 0xb8, 0x03, 0xc0, 0x2c, 0x02, 0x16, 0x78, - 0xe0, 0x9a, 0x74, 0x1d, 0x56, 0x42, 0x16, 0x29, 0xbf, 0xe5, 0x87, 0xb8, 0x80, 0x86, 0x59, 0xed, - 0x5c, 0xd9, 0xaa, 0x2c, 0x39, 0x57, 0x46, 0x72, 0x7a, 0x71, 0xe4, 0x36, 0x2c, 0xb3, 0x96, 0xf2, - 0xfb, 0xbc, 0xd1, 0x67, 0x1d, 0xdf, 0x65, 0x4a, 0x44, 0x72, 0x6d, 0x1e, 0x65, 0x5d, 0xd6, 0x89, - 0x27, 0x49, 0x9c, 0xdc, 0x87, 0xb5, 0x54, 0x8d, 0xc0, 0x4b, 0x73, 0x0a, 0xc8, 0x79, 0x63, 0x24, - 0x3f, 0xa4, 0xda, 0xf7, 0xa0, 0x88, 0xc3, 0x48, 0x42, 0x8f, 0x02, 0x97, 0x7f, 0x3d, 0x98, 0xe0, - 0x1a, 0xbc, 0xc2, 0x5c, 0x37, 0xe2, 0x52, 0x9f, 0x96, 0x05, 0x67, 0xf0, 0x69, 0x3f, 0x81, 0xab, - 0x99, 0xbc, 0xe4, 0x94, 0xcd, 0xfb, 0x71, 0xc0, 0x6c, 0xe8, 0x56, 0xc6, 0x86, 0x9e, 0x62, 0x6a, - 0xbc, 0xfd, 0x00, 0x6c, 0xac, 0xbb, 0x27, 0xda, 0xea, 0x7d, 0x11, 0xb4, 0xfd, 0xa8, 0x8b, 0x63, - 0xd9, 0x53, 0x4c, 0xf5, 0xe4, 0xb4, 0xa3, 0xf6, 0x87, 0x05, 0x6f, 0x4e, 0xa4, 0x1b, 0x79, 0xb7, - 0x60, 0xd9, 0x97, 0x0d, 0x29, 0xda, 0xaa, 0xd1, 0xd2, 0x28, 0xee, 0x62, 0xa9, 0x57, 0x9d, 0x4b, - 0xbe, 0x4c, 0x91, 0xb9, 0x4b, 0x36, 0x61, 0xb1, 0x2f, 0x14, 0x77, 0x1b, 0xa1, 0x78, 0xc6, 0x23, - 0xdc, 0xfd, 0x39, 0x07, 0x30, 0xf4, 0x38, 0x8e, 0xc4, 0x00, 0x25, 0x14, 0xeb, 0x18, 0xc0, 0xac, - 0x06, 0x60, 0x48, 0x03, 0x6e, 0xc0, 0xa5, 0x83, 0x9e, 0x88, 0x7a, 0xdd, 0x46, 0x3b, 0x8a, 0xf7, - 0x4e, 0x04, 0xb8, 0xf3, 0x0b, 0xce, 0x45, 0x1d, 0xfe, 0xc8, 0x44, 0xed, 0xf7, 0x46, 0x6e, 0x0a, - 0x8f, 0xf6, 0x7c, 0x2f, 0x60, 0xaa, 0x17, 0x71, 0x39, 0xfd, 0x8e, 0x2d, 0x8f, 0x91, 0xe2, 0x33, - 0x94, 0x1c, 0x84, 0xc6, 0xe8, 0x3e, 0x5e, 0x4e, 0x12, 0x0f, 0x75, 0x9c, 0x5c, 0x83, 0x05, 0x39, - 0x60, 0xe2, 0x22, 0x97, 0x9c, 0x61, 0xc0, 0xf6, 0x60, 0x33, 0x57, 0x99, 0x99, 0xe9, 0x07, 0x00, - 0x09, 0x3e, 0x6e, 0x33, 0x3b, 0xf1, 0x22, 0xa7, 0x4a, 0x38, 0x29, 0x9e, 0x5d, 0x36, 0x23, 0xf8, - 0x8c, 0x49, 0x65, 0x90, 0xe6, 0x16, 0x0d, 0x7c, 0xed, 0xbe, 0x91, 0x92, 0x85, 0x30, 0x52, 0xf2, - 0xa6, 0xf4, 0x31, 0xac, 0x8d, 0xac, 0x22, 0x6d, 0x16, 0xe7, 0x19, 0x96, 0xcd, 0x60, 0x3d, 0xa3, - 0x50, 0x32, 0x88, 0xd7, 0x06, 0x06, 0xdd, 0xf0, 0x83, 0xb6, 0x30, 0x77, 0x60, 0x73, 0xc2, 0x2c, - 0x90, 0xbf, 0xc4, 0x52, 0x5f, 0xdb, 0x3f, 0x03, 0xcc, 0x63, 0x0f, 0xf2, 0x0d, 0x14, 0xb4, 0x21, - 0x93, 0xb7, 0x33, 0x4a, 0x8c, 0x3b, 0x7f, 0xf1, 0xfa, 0x34, 0x98, 0x16, 0x6a, 0x6f, 0x7d, 0xfb, - 0xd7, 0x7f, 0x3f, 0x5e, 0xb8, 0x4a, 0xd6, 0xe9, 0xf8, 0x13, 0xa3, 0x4d, 0x9f, 0xfc, 0x6a, 0x0d, - 0x0e, 0x56, 0xda, 0x9c, 0xee, 0xe4, 0x35, 0xc8, 0x7b, 0x1b, 0x8a, 0xf5, 0x73, 0x30, 0x8c, 0x3a, - 0x8a, 0xea, 0x6e, 0x92, 0x1b, 0x34, 0xef, 0x01, 0x44, 0x16, 0x3d, 0xd4, 0x9b, 0x7b, 0x44, 0xbe, - 0xb3, 0x60, 0x21, 0xf1, 0x74, 0x52, 0xc9, 0xeb, 0x78, 0xfa, 0x99, 0x28, 0xde, 0x3c, 0x03, 0xd2, - 0x68, 0xaa, 0xa0, 0x26, 0x9b, 0x94, 0x33, 0x34, 0xe1, 0x63, 0x41, 0x0f, 0xf1, 0xcf, 0x11, 0xf9, - 0xc9, 0x82, 0x8b, 0xa3, 0x0e, 0x47, 0xaa, 0x79, 0x7d, 0x32, 0xbd, 0xb7, 0x58, 0x3b, 0x2b, 0xdc, - 0x68, 0xab, 0xa1, 0xb6, 0x0a, 0xb9, 0x9e, 0xa1, 0x2d, 0x39, 0xc0, 0xf4, 0xd0, 0x1c, 0xed, 0x23, - 0xf2, 0xa7, 0x05, 0xab, 0xd9, 0x36, 0x49, 0xee, 0xe6, 0xb5, 0x9e, 0xe8, 0xca, 0xc5, 0x7b, 0xe7, - 0xa5, 0x19, 0xe5, 0x77, 0x51, 0x39, 0x25, 0xd5, 0x0c, 0xe5, 0xb1, 0x47, 0x57, 0x5b, 0x29, 0xee, - 0x70, 0xbf, 0x7f, 0xb3, 0x80, 0x8c, 0xfb, 0x11, 0x99, 0x72, 0xd4, 0x32, 0x5c, 0xb5, 0xb8, 0x7d, - 0x1e, 0xca, 0x19, 0xc6, 0x3d, 0xf4, 0xb3, 0xa1, 0xda, 0xdf, 0x2d, 0x20, 0xe3, 0x96, 0x95, 0xaf, - 0x36, 0xd7, 0x00, 0xf3, 0xd5, 0xe6, 0x3b, 0xe2, 0xc4, 0xcb, 0xd4, 0x61, 0x52, 0x55, 0x8d, 0xf7, - 0xb8, 0x55, 0xad, 0x97, 0xfc, 0x62, 0xc1, 0x52, 0xda, 0x9d, 0xc8, 0xed, 0x69, 0x33, 0x4a, 0x5f, - 0xa9, 0x77, 0xce, 0x06, 0x36, 0xe2, 0x76, 0x50, 0x5c, 0x9d, 0x50, 0x9a, 0xff, 0x53, 0x97, 0x1e, - 0x8e, 0xb9, 0xf3, 0xd1, 0xee, 0xa7, 0x2f, 0x8e, 0x4b, 0xd6, 0xcb, 0xe3, 0x92, 0xf5, 0xef, 0x71, - 0xc9, 0xfa, 0xe1, 0xa4, 0x34, 0xf3, 0xf2, 0xa4, 0x34, 0xf3, 0xf7, 0x49, 0x69, 0xe6, 0xcb, 0x3b, - 0x9e, 0xaf, 0xf6, 0x7b, 0xcd, 0x5a, 0x4b, 0x74, 0x29, 0xef, 0x4b, 0xc5, 0x5a, 0x4f, 0x29, 0xef, - 0x57, 0xb1, 0x7a, 0x57, 0xb8, 0xbd, 0x0e, 0x97, 0x49, 0x17, 0xfc, 0xb1, 0xdd, 0x2c, 0xe0, 0x6f, - 0xe9, 0x77, 0xff, 0x0f, 0x00, 0x00, 0xff, 0xff, 0x3d, 0xe0, 0x17, 0xeb, 0x20, 0x0c, 0x00, 0x00, + // 1253 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, + 0x46, 0xea, 0x81, 0x03, 0xab, 0xb1, 0x3d, 0x76, 0x56, 0x8d, 0x77, 0xb6, 0x3b, 0xb3, 0x2e, 0x26, + 0xca, 0x85, 0x13, 0x82, 0x03, 0x48, 0xfc, 0x01, 0x54, 0xdc, 0x10, 0xdc, 0xe0, 0x88, 0x38, 0x57, + 0x9c, 0x2a, 0xb8, 0x70, 0x42, 0x28, 0xe1, 0xc0, 0xbf, 0xc0, 0x0d, 0xed, 0xcc, 0xec, 0x7a, 0x1d, + 0xef, 0xda, 0xc9, 0xa9, 0x9d, 0xf7, 0x3e, 0x9f, 0x37, 0x9f, 0x79, 0x33, 0x6f, 0x3f, 0x31, 0x5c, + 0x20, 0x7d, 0xdc, 0x6c, 0x39, 0xa6, 0x4b, 0xf8, 0x33, 0xea, 0x3f, 0x31, 0xfb, 0x0d, 0xf3, 0x69, + 0x40, 0xfc, 0x81, 0xe1, 0xf9, 0x94, 0x53, 0xb4, 0x24, 0xd3, 0x86, 0x4a, 0x1b, 0xfd, 0x46, 0xa5, + 0xd4, 0xa5, 0x5d, 0x2a, 0xb2, 0x66, 0xf8, 0x3f, 0x09, 0xac, 0x9c, 0xef, 0x52, 0xda, 0xdd, 0x26, + 0x26, 0xf6, 0x1c, 0x13, 0xbb, 0x2e, 0xe5, 0x98, 0x3b, 0xd4, 0x65, 0x2a, 0xbb, 0xac, 0xb2, 0x62, + 0xd5, 0x0c, 0x3a, 0x26, 0x76, 0x07, 0x51, 0xaa, 0x45, 0x59, 0x8f, 0x32, 0x5b, 0x56, 0x94, 0x0b, + 0x95, 0xba, 0x2a, 0x57, 0x66, 0x13, 0x33, 0x22, 0x55, 0x99, 0xfd, 0x46, 0x93, 0x70, 0xdc, 0x30, + 0x3d, 0xdc, 0x75, 0x5c, 0xb1, 0x85, 0xc2, 0xa6, 0x9c, 0x83, 0x0f, 0x3c, 0x12, 0x95, 0x5a, 0x19, + 0x4f, 0x63, 0xce, 0x09, 0xe3, 0xc4, 0x97, 0x08, 0xbd, 0x04, 0xe8, 0x83, 0x70, 0x8b, 0x47, 0xd8, + 0xc7, 0x3d, 0x66, 0x91, 0xa7, 0x01, 0x61, 0x5c, 0x7f, 0x1f, 0x4e, 0x8f, 0x44, 0x99, 0x47, 0x5d, + 0x46, 0xd0, 0x2d, 0xc8, 0x7b, 0x22, 0x52, 0xd6, 0x56, 0xb4, 0xb5, 0xe2, 0xfa, 0xb2, 0x31, 0xd6, + 0x27, 0x43, 0x52, 0x36, 0xe6, 0x5e, 0xfc, 0x59, 0xcb, 0x59, 0x0a, 0xae, 0xdf, 0x82, 0x0b, 0xa2, + 0xde, 0x5d, 0xb1, 0xb9, 0x38, 0xc0, 0x86, 0xc3, 0x7b, 0xd8, 0x53, 0x1b, 0xa2, 0x33, 0x90, 0xdf, + 0x22, 0x4e, 0x77, 0x8b, 0x8b, 0xca, 0xb3, 0x96, 0x5a, 0xe9, 0x1f, 0x41, 0x35, 0x8b, 0xa8, 0x34, + 0xdd, 0x81, 0x7c, 0x53, 0x44, 0x94, 0xa6, 0xff, 0xa7, 0x68, 0x1a, 0x67, 0x2b, 0x8e, 0x5e, 0x87, + 0xff, 0x89, 0xfa, 0xf7, 0x3d, 0xda, 0xda, 0x7a, 0xe8, 0x76, 0x68, 0x24, 0xa8, 0x04, 0xf3, 0x24, + 0x8c, 0x89, 0xaa, 0x73, 0x96, 0x5c, 0xe8, 0x5f, 0xcc, 0xc0, 0x99, 0x83, 0x78, 0xa5, 0x23, 0x95, + 0x80, 0x56, 0x61, 0x81, 0x71, 0xec, 0x73, 0x5b, 0x9d, 0x6e, 0x46, 0x9c, 0xae, 0x28, 0x62, 0x6f, + 0x8b, 0x10, 0xba, 0x00, 0x40, 0xdc, 0x76, 0x04, 0x98, 0x15, 0x80, 0x02, 0x71, 0xdb, 0x2a, 0xdd, + 0x80, 0x92, 0x87, 0x7d, 0xee, 0xb4, 0x1c, 0x4f, 0x1c, 0xc0, 0x56, 0xa7, 0x9d, 0x5b, 0xd1, 0xd6, + 0x16, 0xac, 0xd3, 0x23, 0x39, 0x79, 0x38, 0x74, 0x0d, 0x96, 0x70, 0x8b, 0x3b, 0x7d, 0x62, 0xf7, + 0xf1, 0xb6, 0xd3, 0xc6, 0x9c, 0xfa, 0xac, 0x3c, 0x2f, 0x64, 0x2d, 0xca, 0xc4, 0xe3, 0x38, 0x8e, + 0x6e, 0x43, 0x39, 0x51, 0xc3, 0xed, 0x26, 0x39, 0x79, 0xc1, 0x39, 0x3b, 0x92, 0x1f, 0x52, 0xf5, + 0x9b, 0x50, 0x11, 0xcd, 0x88, 0x43, 0x0f, 0xdd, 0x36, 0xf9, 0x38, 0xea, 0x60, 0x19, 0x8e, 0xe1, + 0x76, 0xdb, 0x27, 0x4c, 0xbe, 0x96, 0x82, 0x15, 0x2d, 0xf5, 0xc7, 0x70, 0x2e, 0x95, 0x17, 0xbf, + 0xb2, 0x79, 0x27, 0x0c, 0xa8, 0x0b, 0x5d, 0x4d, 0xb9, 0xd0, 0x03, 0x4c, 0x89, 0xd7, 0xef, 0x80, + 0x2e, 0xea, 0x6e, 0xd2, 0x0e, 0xbf, 0x47, 0xdd, 0x8e, 0xe3, 0xf7, 0x44, 0x5b, 0x36, 0x39, 0xe6, + 0x01, 0x9b, 0xf6, 0xd4, 0x7e, 0xd6, 0xe0, 0xe2, 0x44, 0xba, 0x92, 0x77, 0x15, 0x96, 0x1c, 0x66, + 0x33, 0xda, 0xe1, 0x76, 0x4b, 0xa2, 0x48, 0x5b, 0x94, 0x3a, 0x6e, 0x9d, 0x72, 0x58, 0x82, 0x4c, + 0xda, 0xa8, 0x06, 0xc5, 0x3e, 0xe5, 0xa4, 0x6d, 0x7b, 0xf4, 0x19, 0xf1, 0xc5, 0xed, 0xcf, 0x59, + 0x20, 0x42, 0x8f, 0xc2, 0x48, 0x08, 0xe0, 0x94, 0xe3, 0x6d, 0x05, 0x98, 0x95, 0x00, 0x11, 0x92, + 0x80, 0xcb, 0x70, 0xea, 0x69, 0x40, 0xfd, 0xa0, 0x67, 0x77, 0xfc, 0xf0, 0xee, 0xa8, 0x2b, 0x6e, + 0xbe, 0x60, 0x9d, 0x94, 0xe1, 0x07, 0x2a, 0xaa, 0xbf, 0x3e, 0x32, 0x29, 0xc4, 0xdf, 0x74, 0xba, + 0x2e, 0xe6, 0x81, 0x4f, 0xd8, 0xf4, 0x19, 0x5b, 0x1a, 0x23, 0x85, 0x6f, 0x28, 0x7e, 0x08, 0xf6, + 0xe8, 0x3d, 0x2e, 0xc6, 0x89, 0xbb, 0x32, 0x8e, 0xce, 0x43, 0x81, 0x45, 0x4c, 0x71, 0xc8, 0x05, + 0x6b, 0x18, 0xd0, 0xbb, 0x50, 0xcb, 0x54, 0xa6, 0x7a, 0xfa, 0x26, 0x40, 0x8c, 0x0f, 0xb7, 0x99, + 0x9d, 0x38, 0xc8, 0x89, 0x12, 0x56, 0x82, 0xa7, 0xaf, 0xa8, 0x16, 0xbc, 0x87, 0x19, 0x57, 0x48, + 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, } // Reference imports to suppress errors if they are not otherwise used. @@ -971,6 +1113,8 @@ type QueryClient interface { LastAttestedHeight(ctx context.Context, in *QueryLastAttestedHeightRequest, opts ...grpc.CallOption) (*QueryLastAttestedHeightResponse, error) // AttesterInfo queries the attester information including public key AttesterInfo(ctx context.Context, in *QueryAttesterInfoRequest, opts ...grpc.CallOption) (*QueryAttesterInfoResponse, error) + // AttesterSet queries the full ordered attester set + AttesterSet(ctx context.Context, in *QueryAttesterSetRequest, opts ...grpc.CallOption) (*QueryAttesterSetResponse, error) } type queryClient struct { @@ -1053,6 +1197,15 @@ func (c *queryClient) AttesterInfo(ctx context.Context, in *QueryAttesterInfoReq return out, nil } +func (c *queryClient) AttesterSet(ctx context.Context, in *QueryAttesterSetRequest, opts ...grpc.CallOption) (*QueryAttesterSetResponse, error) { + out := new(QueryAttesterSetResponse) + err := c.cc.Invoke(ctx, "/evabci.network.v1.Query/AttesterSet", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // QueryServer is the server API for Query service. type QueryServer interface { // Params queries the module parameters @@ -1071,6 +1224,8 @@ type QueryServer interface { LastAttestedHeight(context.Context, *QueryLastAttestedHeightRequest) (*QueryLastAttestedHeightResponse, error) // AttesterInfo queries the attester information including public key AttesterInfo(context.Context, *QueryAttesterInfoRequest) (*QueryAttesterInfoResponse, error) + // AttesterSet queries the full ordered attester set + AttesterSet(context.Context, *QueryAttesterSetRequest) (*QueryAttesterSetResponse, error) } // UnimplementedQueryServer can be embedded to have forward compatible implementations. @@ -1101,6 +1256,9 @@ func (*UnimplementedQueryServer) LastAttestedHeight(ctx context.Context, req *Qu func (*UnimplementedQueryServer) AttesterInfo(ctx context.Context, req *QueryAttesterInfoRequest) (*QueryAttesterInfoResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method AttesterInfo not implemented") } +func (*UnimplementedQueryServer) AttesterSet(ctx context.Context, req *QueryAttesterSetRequest) (*QueryAttesterSetResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method AttesterSet not implemented") +} func RegisterQueryServer(s grpc1.Server, srv QueryServer) { s.RegisterService(&_Query_serviceDesc, srv) @@ -1250,6 +1408,24 @@ func _Query_AttesterInfo_Handler(srv interface{}, ctx context.Context, dec func( return interceptor(ctx, in, info, handler) } +func _Query_AttesterSet_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(QueryAttesterSetRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(QueryServer).AttesterSet(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/evabci.network.v1.Query/AttesterSet", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(QueryServer).AttesterSet(ctx, req.(*QueryAttesterSetRequest)) + } + return interceptor(ctx, in, info, handler) +} + var Query_serviceDesc = _Query_serviceDesc var _Query_serviceDesc = grpc.ServiceDesc{ ServiceName: "evabci.network.v1.Query", @@ -1287,6 +1463,10 @@ var _Query_serviceDesc = grpc.ServiceDesc{ MethodName: "AttesterInfo", Handler: _Query_AttesterInfo_Handler, }, + { + MethodName: "AttesterSet", + Handler: _Query_AttesterSet_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "evabci/network/v1/query.proto", @@ -1855,6 +2035,120 @@ func (m *QueryAttesterInfoResponse) MarshalToSizedBuffer(dAtA []byte) (int, erro return len(dAtA) - i, nil } +func (m *QueryAttesterSetRequest) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *QueryAttesterSetRequest) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *QueryAttesterSetRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + return len(dAtA) - i, nil +} + +func (m *AttesterSetEntry) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *AttesterSetEntry) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *AttesterSetEntry) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.Pubkey != nil { + { + size, err := m.Pubkey.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintQuery(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x22 + } + if m.Index != 0 { + i = encodeVarintQuery(dAtA, i, uint64(m.Index)) + i-- + dAtA[i] = 0x18 + } + if len(m.ConsensusAddress) > 0 { + i -= len(m.ConsensusAddress) + copy(dAtA[i:], m.ConsensusAddress) + i = encodeVarintQuery(dAtA, i, uint64(len(m.ConsensusAddress))) + i-- + dAtA[i] = 0x12 + } + if len(m.Authority) > 0 { + i -= len(m.Authority) + copy(dAtA[i:], m.Authority) + i = encodeVarintQuery(dAtA, i, uint64(len(m.Authority))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func (m *QueryAttesterSetResponse) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *QueryAttesterSetResponse) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *QueryAttesterSetResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if len(m.Entries) > 0 { + for iNdEx := len(m.Entries) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.Entries[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintQuery(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0xa + } + } + return len(dAtA) - i, nil +} + func encodeVarintQuery(dAtA []byte, offset int, v uint64) int { offset -= sovQuery(v) base := offset @@ -2102,6 +2396,54 @@ func (m *QueryAttesterInfoResponse) Size() (n int) { return n } +func (m *QueryAttesterSetRequest) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + return n +} + +func (m *AttesterSetEntry) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Authority) + if l > 0 { + n += 1 + l + sovQuery(uint64(l)) + } + l = len(m.ConsensusAddress) + if l > 0 { + n += 1 + l + sovQuery(uint64(l)) + } + if m.Index != 0 { + n += 1 + sovQuery(uint64(m.Index)) + } + if m.Pubkey != nil { + l = m.Pubkey.Size() + n += 1 + l + sovQuery(uint64(l)) + } + return n +} + +func (m *QueryAttesterSetResponse) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if len(m.Entries) > 0 { + for _, e := range m.Entries { + l = e.Size() + n += 1 + l + sovQuery(uint64(l)) + } + } + return n +} + func sovQuery(x uint64) (n int) { return (math_bits.Len64(x|1) + 6) / 7 } @@ -3577,6 +3919,309 @@ func (m *QueryAttesterInfoResponse) Unmarshal(dAtA []byte) error { } return nil } +func (m *QueryAttesterSetRequest) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowQuery + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: QueryAttesterSetRequest: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: QueryAttesterSetRequest: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + default: + iNdEx = preIndex + skippy, err := skipQuery(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthQuery + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *AttesterSetEntry) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowQuery + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: AttesterSetEntry: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: AttesterSetEntry: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Authority", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowQuery + } + 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 ErrInvalidLengthQuery + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthQuery + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Authority = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + 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 ErrIntOverflowQuery + } + 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 ErrInvalidLengthQuery + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthQuery + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.ConsensusAddress = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 3: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Index", wireType) + } + m.Index = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowQuery + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Index |= uint32(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 4: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Pubkey", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowQuery + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthQuery + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthQuery + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.Pubkey == nil { + m.Pubkey = &types.Any{} + } + if err := m.Pubkey.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipQuery(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthQuery + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *QueryAttesterSetResponse) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowQuery + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: QueryAttesterSetResponse: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: QueryAttesterSetResponse: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Entries", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowQuery + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthQuery + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthQuery + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Entries = append(m.Entries, AttesterSetEntry{}) + if err := m.Entries[len(m.Entries)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipQuery(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthQuery + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} func skipQuery(dAtA []byte) (n int, err error) { l := len(dAtA) iNdEx := 0 diff --git a/modules/network/types/query.pb.gw.go b/modules/network/types/query.pb.gw.go index 1dc93cde..91b4d014 100644 --- a/modules/network/types/query.pb.gw.go +++ b/modules/network/types/query.pb.gw.go @@ -393,6 +393,24 @@ func local_request_Query_AttesterInfo_0(ctx context.Context, marshaler runtime.M } +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 + + msg, err := client.AttesterSet(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_Query_AttesterSet_0(ctx context.Context, marshaler runtime.Marshaler, server QueryServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq QueryAttesterSetRequest + var metadata runtime.ServerMetadata + + msg, err := server.AttesterSet(ctx, &protoReq) + return msg, metadata, err + +} + // RegisterQueryHandlerServer registers the http handlers for service Query to "mux". // UnaryRPC :call QueryServer directly. // StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. @@ -583,6 +601,29 @@ func RegisterQueryHandlerServer(ctx context.Context, mux *runtime.ServeMux, serv }) + mux.Handle("GET", pattern_Query_AttesterSet_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_Query_AttesterSet_0(rctx, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_Query_AttesterSet_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + return nil } @@ -784,6 +825,26 @@ func RegisterQueryHandlerClient(ctx context.Context, mux *runtime.ServeMux, clie }) + mux.Handle("GET", pattern_Query_AttesterSet_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_Query_AttesterSet_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_Query_AttesterSet_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + return nil } @@ -803,6 +864,8 @@ var ( pattern_Query_LastAttestedHeight_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"evabci", "network", "v1", "last-attested-height"}, "", runtime.AssumeColonVerbOpt(false))) pattern_Query_AttesterInfo_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 1, 0, 4, 1, 5, 4}, []string{"evabci", "network", "v1", "attester", "validator_address"}, "", runtime.AssumeColonVerbOpt(false))) + + pattern_Query_AttesterSet_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"evabci", "network", "v1", "attester_set"}, "", runtime.AssumeColonVerbOpt(false))) ) var ( @@ -821,4 +884,6 @@ var ( forward_Query_LastAttestedHeight_0 = runtime.ForwardResponseMessage forward_Query_AttesterInfo_0 = runtime.ForwardResponseMessage + + forward_Query_AttesterSet_0 = runtime.ForwardResponseMessage ) diff --git a/modules/proto/evabci/network/v1/query.proto b/modules/proto/evabci/network/v1/query.proto index 366c39bb..ac16858b 100644 --- a/modules/proto/evabci/network/v1/query.proto +++ b/modules/proto/evabci/network/v1/query.proto @@ -6,6 +6,8 @@ option go_package = "github.com/evstack/ev-abci/modules/network/types"; import "gogoproto/gogo.proto"; import "google/api/annotations.proto"; +import "google/protobuf/any.proto"; +import "cosmos_proto/cosmos.proto"; import "cosmos/base/query/v1beta1/pagination.proto"; import "evabci/network/v1/types.proto"; import "evabci/network/v1/attester.proto"; @@ -51,6 +53,11 @@ service Query { rpc AttesterInfo(QueryAttesterInfoRequest) returns (QueryAttesterInfoResponse) { option (google.api.http).get = "/evabci/network/v1/attester/{validator_address}"; } + + // AttesterSet queries the full ordered attester set + rpc AttesterSet(QueryAttesterSetRequest) returns (QueryAttesterSetResponse) { + option (google.api.http).get = "/evabci/network/v1/attester_set"; + } } // QueryParamsRequest is the request type for the Query/Params RPC method. @@ -143,3 +150,21 @@ message QueryAttesterInfoRequest { message QueryAttesterInfoResponse { AttesterInfo attester_info = 1; } + +// QueryAttesterSetRequest is the request type for the Query/AttesterSet RPC method. +message QueryAttesterSetRequest {} + +// AttesterSetEntry is a single entry in the attester set, ordered by index. +message AttesterSetEntry { + option (gogoproto.equal) = false; + option (gogoproto.goproto_getters) = false; + string authority = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; + string consensus_address = 2 [(cosmos_proto.scalar) = "cosmos.AddressString"]; + uint32 index = 3; + google.protobuf.Any pubkey = 4 [(cosmos_proto.accepts_interface) = "cosmos.crypto.PubKey"]; +} + +// QueryAttesterSetResponse is the response type for the Query/AttesterSet RPC method. +message QueryAttesterSetResponse { + repeated AttesterSetEntry entries = 1 [(gogoproto.nullable) = false]; +} diff --git a/pkg/adapter/providers_test.go b/pkg/adapter/providers_test.go new file mode 100644 index 00000000..d9cb9a91 --- /dev/null +++ b/pkg/adapter/providers_test.go @@ -0,0 +1,66 @@ +package adapter_test + +import ( + "bytes" + "context" + "sort" + "testing" + + tmcryptoed25519 "github.com/cometbft/cometbft/crypto/ed25519" + cmtstate "github.com/cometbft/cometbft/state" + cmttypes "github.com/cometbft/cometbft/types" + "github.com/libp2p/go-libp2p/core/crypto" + "github.com/stretchr/testify/require" + + "github.com/evstack/ev-abci/pkg/adapter" +) + +type mockStateStore struct { + state *cmtstate.State +} + +func (m *mockStateStore) LoadState(_ context.Context) (*cmtstate.State, error) { + return m.state, nil +} + +func TestValidatorHasherOrderingMatchesAddressSort(t *testing.T) { + // 3 random ed25519 validators + keys := []tmcryptoed25519.PubKey{ + tmcryptoed25519.GenPrivKey().PubKey().(tmcryptoed25519.PubKey), + tmcryptoed25519.GenPrivKey().PubKey().(tmcryptoed25519.PubKey), + tmcryptoed25519.GenPrivKey().PubKey().(tmcryptoed25519.PubKey), + } + + // Canonical: sort by Address() bytes ascending, convert to libp2p, hash. + canonicalOrder := make([]tmcryptoed25519.PubKey, len(keys)) + copy(canonicalOrder, keys) + sort.Slice(canonicalOrder, func(i, j int) bool { + return bytes.Compare(canonicalOrder[i].Address(), canonicalOrder[j].Address()) < 0 + }) + libp2pCanonical := make([]crypto.PubKey, len(canonicalOrder)) + for i, k := range canonicalOrder { + p, err := crypto.UnmarshalEd25519PublicKey(k.Bytes()) + require.NoError(t, err) + libp2pCanonical[i] = p + } + sequencerAddr := canonicalOrder[0].Address().Bytes() + canonicalHash, err := adapter.ValidatorsHasher(libp2pCanonical, sequencerAddr) + require.NoError(t, err) + + // Build a cmttypes.ValidatorSet via NewValidatorSet (will sort internally). + vals := make([]*cmttypes.Validator, len(keys)) + for i, k := range keys { + vals[i] = cmttypes.NewValidator(k, 1) + } + vs := cmttypes.NewValidatorSet(vals) + + // Wrap in a mock state and call the provider. + st := &cmtstate.State{Validators: vs} + store := &mockStateStore{state: st} + hasher := adapter.ValidatorHasherFromStoreProvider(store) + gotHash, err := hasher(sequencerAddr, nil) + require.NoError(t, err) + + require.Equal(t, []byte(canonicalHash), []byte(gotHash), + "provider hash must match address-sorted canonical hash") +} diff --git a/pkg/rpc/core/blocks.go b/pkg/rpc/core/blocks.go index 73921e5d..d3c04bbb 100644 --- a/pkg/rpc/core/blocks.go +++ b/pkg/rpc/core/blocks.go @@ -15,7 +15,7 @@ import ( ctypes "github.com/cometbft/cometbft/rpc/core/types" rpctypes "github.com/cometbft/cometbft/rpc/jsonrpc/types" cmttypes "github.com/cometbft/cometbft/types" - sdk "github.com/cosmos/cosmos-sdk/types" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" "github.com/cosmos/gogoproto/proto" storepkg "github.com/evstack/ev-node/pkg/store" @@ -334,82 +334,109 @@ func BlockchainInfo(ctx *rpctypes.Context, minHeight, maxHeight int64) (*ctypes. }, nil } -// getCommitForHeight returns commit info for a specific height, -// using attester signatures if in attester mode, otherwise sequencer signatures +// getCommitForHeight returns a deterministic cmttypes.Commit for height. +// In attester mode it builds the commit from the ordered attester set, placing +// BlockIDFlagAbsent for non-signers and refusing to return until 2/3 quorum is met. func getCommitForHeight(ctx context.Context, height uint64) (*cmttypes.Commit, error) { - // Debug: Log attester mode status - env.Logger.Info("getCommitForHeight called", - "height", height, - "AttesterMode", env.AttesterMode) - - // If not in attester mode, use the original sequencer-based commit if !env.AttesterMode { - env.Logger.Info("Using sequencer mode - returning sequencer signatures") return env.Adapter.GetLastCommit(ctx, height+1) } - // In attester mode, try to construct commit from attester signatures blockID, err := env.Adapter.Store.GetBlockID(ctx, height) if err != nil { return nil, fmt.Errorf("get block ID for height %d: %w", height, err) } - // Query attester signatures from the network module - env.Logger.Info("In attester mode - querying attester signatures", "height", height) + entries, err := getAttesterSet(ctx) + if err != nil { + return nil, fmt.Errorf("get attester set: %w", err) + } signatures, err := getAttesterSignatures(ctx, int64(height)) if err != nil { - env.Logger.Error("failed to get attester signatures", - "height", height, "error", err) - return nil, fmt.Errorf("attester mode: failed to get attester signatures for height %d: %w", height, err) + return nil, fmt.Errorf("get attester signatures: %w", err) } - // Build commit with attester signatures - commitSigs := make([]cmttypes.CommitSig, 0, len(signatures)) - for validatorAddr, signature := range signatures { - // Parse the signature bytes (they should be marshaled cmtproto.Vote) - var vote cmtproto.Vote - if err := proto.Unmarshal(signature, &vote); err != nil { - env.Logger.Error("failed to unmarshal attester vote", - "validator", validatorAddr, "error", err) + commitSigs := make([]cmttypes.CommitSig, 0, len(entries)) + signedCount := 0 + for _, e := range entries { + voteBytes, ok := signatures[e.ConsensusAddress] + if !ok { + commitSigs = append(commitSigs, cmttypes.CommitSig{BlockIDFlag: cmttypes.BlockIDFlagAbsent}) continue } - - // Decode bech32 validator address to get 20-byte address - valAddrBytes, err := sdk.ValAddressFromBech32(validatorAddr) - if err != nil { - env.Logger.Error("failed to decode validator address", - "validator", validatorAddr, "error", err) + var vote cmtproto.Vote + if err := proto.Unmarshal(voteBytes, &vote); err != nil { + commitSigs = append(commitSigs, cmttypes.CommitSig{BlockIDFlag: cmttypes.BlockIDFlagAbsent}) continue } - commitSigs = append(commitSigs, cmttypes.CommitSig{ BlockIDFlag: cmttypes.BlockIDFlagCommit, - ValidatorAddress: cmttypes.Address(valAddrBytes), + ValidatorAddress: e.ValidatorAddress, Timestamp: vote.Timestamp, Signature: vote.Signature, }) + signedCount++ } - // If no valid attester signatures, return error instead of fallback - if len(commitSigs) == 0 { - env.Logger.Error("no attester signatures found for block", "height", height) - return nil, fmt.Errorf("attester mode: no attester signatures found for height %d - block not attested", height) + total := len(entries) + if signedCount*3 <= total*2 { + return nil, fmt.Errorf("height %d not yet attested (signed %d of %d)", height, signedCount, total) } return &cmttypes.Commit{ - Height: int64(height), + Height: int64(height), //nolint:gosec Round: 0, BlockID: *blockID, Signatures: commitSigs, }, nil } -// getAttesterSignatures queries the network module to get all attester signatures for a height -func getAttesterSignatures(ctx context.Context, height int64) (map[string][]byte, error) { - // Use the new AttesterSignatures gRPC endpoint - env.Logger.Info("Querying AttesterSignatures endpoint", "height", height) +// attesterSetEntry holds an ordered attester set entry used for commit reconstruction. +type attesterSetEntry struct { + ConsensusAddress string + ValidatorAddress []byte + Pubkey cryptotypes.PubKey +} + +// 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{}) + if err != nil { + return nil, err + } + result, err := env.Adapter.App.Query(ctx, &abci.RequestQuery{ + Path: "/evabci.network.v1.Query/AttesterSet", + Data: req, + }) + if err != nil { + return nil, err + } + if result.Code != 0 { + return nil, fmt.Errorf("query AttesterSet failed: %s", result.Log) + } + var resp networktypes.QueryAttesterSetResponse + if err := proto.Unmarshal(result.Value, &resp); err != nil { + return nil, err + } + sort.Slice(resp.Entries, func(i, j int) bool { return resp.Entries[i].Index < resp.Entries[j].Index }) + + out := make([]attesterSetEntry, 0, len(resp.Entries)) + for _, e := range resp.Entries { + var pk cryptotypes.PubKey + if err := networktypes.ModuleCdc.InterfaceRegistry().UnpackAny(e.Pubkey, &pk); err != nil { + return nil, fmt.Errorf("unpack pubkey for %s: %w", e.ConsensusAddress, err) + } + out = append(out, attesterSetEntry{ + ConsensusAddress: e.ConsensusAddress, + ValidatorAddress: pk.Address(), + Pubkey: pk, + }) + } + return out, nil +} - // Query individual attester signatures using the new endpoint +// getAttesterSignatures queries the network module to get all attester signatures for a height. +func getAttesterSignatures(ctx context.Context, height int64) (map[string][]byte, error) { signaturesReq, err := proto.Marshal(&networktypes.QueryAttesterSignaturesRequest{Height: height}) if err != nil { return nil, fmt.Errorf("marshal attester signatures request: %w", err) @@ -420,12 +447,10 @@ func getAttesterSignatures(ctx context.Context, height int64) (map[string][]byte Data: signaturesReq, }) if err != nil { - env.Logger.Debug("AttesterSignatures query failed", "error", err) return make(map[string][]byte), nil } if result.Code != 0 { - env.Logger.Info("AttesterSignatures not found", "height", height, "code", result.Code) return make(map[string][]byte), nil } @@ -434,13 +459,18 @@ func getAttesterSignatures(ctx context.Context, height int64) (map[string][]byte return nil, fmt.Errorf("unmarshal attester signatures response: %w", err) } - // Convert to map format signatures := make(map[string][]byte) for _, sig := range signaturesResp.Signatures { signatures[sig.ValidatorAddress] = sig.Signature } - env.Logger.Info("Found AttesterSignatures", "height", height, "count", len(signatures)) - return signatures, nil } + +// GetCommitForHeightForTest is exported only for tests in this package. +func GetCommitForHeightForTest(ctx context.Context, e *Environment, height uint64) (*cmttypes.Commit, error) { + previousEnv := env + env = e + defer func() { env = previousEnv }() + return getCommitForHeight(ctx, height) +} diff --git a/pkg/rpc/core/commit_reconstruction_test.go b/pkg/rpc/core/commit_reconstruction_test.go new file mode 100644 index 00000000..1fb341ae --- /dev/null +++ b/pkg/rpc/core/commit_reconstruction_test.go @@ -0,0 +1,295 @@ +package core_test + +import ( + "bytes" + "context" + "sort" + "testing" + "time" + + abci "github.com/cometbft/cometbft/abci/types" + cmted25519 "github.com/cometbft/cometbft/crypto/ed25519" + cmtlog "github.com/cometbft/cometbft/libs/log" + cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" + cmttypes "github.com/cometbft/cometbft/types" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/gogoproto/proto" + ds "github.com/ipfs/go-datastore" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + networktypes "github.com/evstack/ev-abci/modules/network/types" + "github.com/evstack/ev-abci/pkg/adapter" + "github.com/evstack/ev-abci/pkg/rpc/core" + execstore "github.com/evstack/ev-abci/pkg/store" +) + +// mockABCI is a minimal mock for the servertypes.ABCI interface. +type mockABCI struct { + mock.Mock +} + +func (m *mockABCI) Info(req *abci.RequestInfo) (*abci.ResponseInfo, error) { + args := m.Called(req) + r, _ := args.Get(0).(*abci.ResponseInfo) + return r, args.Error(1) +} + +func (m *mockABCI) Query(ctx context.Context, req *abci.RequestQuery) (*abci.ResponseQuery, error) { + args := m.Called(ctx, req) + r, _ := args.Get(0).(*abci.ResponseQuery) + return r, args.Error(1) +} + +func (m *mockABCI) CheckTx(req *abci.RequestCheckTx) (*abci.ResponseCheckTx, error) { + args := m.Called(req) + r, _ := args.Get(0).(*abci.ResponseCheckTx) + return r, args.Error(1) +} + +func (m *mockABCI) InitChain(req *abci.RequestInitChain) (*abci.ResponseInitChain, error) { + args := m.Called(req) + r, _ := args.Get(0).(*abci.ResponseInitChain) + return r, args.Error(1) +} + +func (m *mockABCI) PrepareProposal(req *abci.RequestPrepareProposal) (*abci.ResponsePrepareProposal, error) { + args := m.Called(req) + r, _ := args.Get(0).(*abci.ResponsePrepareProposal) + return r, args.Error(1) +} + +func (m *mockABCI) ProcessProposal(req *abci.RequestProcessProposal) (*abci.ResponseProcessProposal, error) { + args := m.Called(req) + r, _ := args.Get(0).(*abci.ResponseProcessProposal) + return r, args.Error(1) +} + +func (m *mockABCI) FinalizeBlock(req *abci.RequestFinalizeBlock) (*abci.ResponseFinalizeBlock, error) { + args := m.Called(req) + r, _ := args.Get(0).(*abci.ResponseFinalizeBlock) + return r, args.Error(1) +} + +func (m *mockABCI) ExtendVote(ctx context.Context, req *abci.RequestExtendVote) (*abci.ResponseExtendVote, error) { + args := m.Called(ctx, req) + r, _ := args.Get(0).(*abci.ResponseExtendVote) + return r, args.Error(1) +} + +func (m *mockABCI) VerifyVoteExtension(req *abci.RequestVerifyVoteExtension) (*abci.ResponseVerifyVoteExtension, error) { + args := m.Called(req) + r, _ := args.Get(0).(*abci.ResponseVerifyVoteExtension) + return r, args.Error(1) +} + +func (m *mockABCI) Commit() (*abci.ResponseCommit, error) { + args := m.Called() + r, _ := args.Get(0).(*abci.ResponseCommit) + return r, args.Error(1) +} + +func (m *mockABCI) ListSnapshots(req *abci.RequestListSnapshots) (*abci.ResponseListSnapshots, error) { + args := m.Called(req) + r, _ := args.Get(0).(*abci.ResponseListSnapshots) + return r, args.Error(1) +} + +func (m *mockABCI) OfferSnapshot(req *abci.RequestOfferSnapshot) (*abci.ResponseOfferSnapshot, error) { + args := m.Called(req) + r, _ := args.Get(0).(*abci.ResponseOfferSnapshot) + return r, args.Error(1) +} + +func (m *mockABCI) LoadSnapshotChunk(req *abci.RequestLoadSnapshotChunk) (*abci.ResponseLoadSnapshotChunk, error) { + args := m.Called(req) + r, _ := args.Get(0).(*abci.ResponseLoadSnapshotChunk) + return r, args.Error(1) +} + +func (m *mockABCI) ApplySnapshotChunk(req *abci.RequestApplySnapshotChunk) (*abci.ResponseApplySnapshotChunk, error) { + args := m.Called(req) + r, _ := args.Get(0).(*abci.ResponseApplySnapshotChunk) + return r, args.Error(1) +} + +// buildEnv sets up a test Environment and returns it along with the canonical +// ValidatorSet and BlockID used for verification. +func buildEnv(t *testing.T, height uint64, keys []cmted25519.PrivKey, signers []int, chainID string) (*core.Environment, *cmttypes.ValidatorSet, cmttypes.BlockID) { + t.Helper() + + blockIDHash := bytes.Repeat([]byte{0xab}, 32) + + // Build canonical ValidatorSet (NewValidatorSet sorts by address internally). + vals := make([]*cmttypes.Validator, len(keys)) + for i, k := range keys { + vals[i] = cmttypes.NewValidator(k.PubKey(), 1) + } + valSet := cmttypes.NewValidatorSet(vals) + + // Map raw 20-byte address → private key for signing. + privByAddr := map[string]cmted25519.PrivKey{} + for _, priv := range keys { + privByAddr[string(priv.PubKey().Address())] = priv + } + + // Build ordered list following valSet order (already sorted by address). + consAddrs := make([]string, len(valSet.Validators)) + pubkeyAnys := make([]*codectypes.Any, len(valSet.Validators)) + for i, v := range valSet.Validators { + sdkPk, err := cryptocodec.FromCmtPubKeyInterface(v.PubKey) + require.NoError(t, err) + consAddrs[i] = sdk.ConsAddress(v.Address).String() + any, err := codectypes.NewAnyWithValue(sdkPk) + require.NoError(t, err) + pubkeyAnys[i] = any + } + + // Sign for each selected signer index (indices into valSet.Validators). + signatures := map[string][]byte{} + for _, i := range signers { + priv := privByAddr[string(valSet.Validators[i].Address)] + v := cmtproto.Vote{ + Type: cmtproto.PrecommitType, + Height: int64(height), //nolint:gosec + Round: 0, + BlockID: cmtproto.BlockID{Hash: blockIDHash, PartSetHeader: cmtproto.PartSetHeader{}}, + Timestamp: time.Date(2026, 4, 22, 12, 0, 0, 0, time.UTC), + ValidatorAddress: valSet.Validators[i].Address, + ValidatorIndex: int32(i), //nolint:gosec + } + sb := cmttypes.VoteSignBytes(chainID, &v) + sig, err := priv.Sign(sb) + require.NoError(t, err) + v.Signature = sig + bz, err := proto.Marshal(&v) + require.NoError(t, err) + signatures[consAddrs[i]] = bz + } + + // Prepare AttesterSet query response (index == position in sorted valSet). + setEntries := make([]networktypes.AttesterSetEntry, 0, len(valSet.Validators)) + for i := range valSet.Validators { + setEntries = append(setEntries, networktypes.AttesterSetEntry{ + Authority: sdk.AccAddress(valSet.Validators[i].Address).String(), + ConsensusAddress: consAddrs[i], + Index: uint32(i), //nolint:gosec + Pubkey: pubkeyAnys[i], + }) + } + setRespBz, err := proto.Marshal(&networktypes.QueryAttesterSetResponse{Entries: setEntries}) + require.NoError(t, err) + + // Prepare AttesterSignatures query response. + sigList := make([]*networktypes.AttesterSignature, 0, len(signatures)) + for consAddr, sig := range signatures { + sigList = append(sigList, &networktypes.AttesterSignature{ + ValidatorAddress: consAddr, + Signature: sig, + }) + } + sigRespBz, err := proto.Marshal(&networktypes.QueryAttesterSignaturesResponse{Signatures: sigList}) + require.NoError(t, err) + + mApp := new(mockABCI) + mApp.On("Query", mock.Anything, mock.MatchedBy(func(r *abci.RequestQuery) bool { + return r.Path == "/evabci.network.v1.Query/AttesterSet" + })).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" + })).Return(&abci.ResponseQuery{Code: 0, Value: sigRespBz}, nil) + + // Use real store with in-memory backend, save the block ID. + dsStore := ds.NewMapDatastore() + abciExecStore := execstore.NewExecABCIStore(dsStore) + blockID := cmttypes.BlockID{Hash: blockIDHash, PartSetHeader: cmttypes.PartSetHeader{}} + err = abciExecStore.SaveBlockID(context.Background(), height, &blockID) + require.NoError(t, err) + + env := &core.Environment{ + Adapter: &adapter.Adapter{ + App: mApp, + Store: abciExecStore, + }, + AttesterMode: true, + Logger: cmtlog.NewNopLogger(), + } + + return env, valSet, blockID +} + +func TestGetCommitForHeight_QuorumMet_SortedWithAbsent(t *testing.T) { + chainID := "test-chain" + keys := []cmted25519.PrivKey{ + cmted25519.GenPrivKey(), cmted25519.GenPrivKey(), + cmted25519.GenPrivKey(), cmted25519.GenPrivKey(), + } + signers := []int{0, 1, 2} // 3 of 4 — quorum + env, valSet, blockID := buildEnv(t, 100, keys, signers, chainID) + + commit, err := core.GetCommitForHeightForTest(context.Background(), env, 100) + require.NoError(t, err) + require.Equal(t, int64(100), commit.Height) + require.Equal(t, int32(0), commit.Round) + require.Equal(t, blockID, commit.BlockID) + require.Len(t, commit.Signatures, 4) + + // Committed validator addresses must be in ascending order. + var addrs [][]byte + for _, cs := range commit.Signatures { + if cs.BlockIDFlag == cmttypes.BlockIDFlagCommit { + addrs = append(addrs, cs.ValidatorAddress) + } + } + require.True(t, sort.SliceIsSorted(addrs, func(i, j int) bool { + return bytes.Compare(addrs[i], addrs[j]) < 0 + })) + + var commitCnt, absentCnt int + for _, cs := range commit.Signatures { + switch cs.BlockIDFlag { + case cmttypes.BlockIDFlagCommit: + commitCnt++ + case cmttypes.BlockIDFlagAbsent: + absentCnt++ + } + } + require.Equal(t, 3, commitCnt) + require.Equal(t, 1, absentCnt) + + // 07-tendermint light client must accept this commit. + require.NoError(t, valSet.VerifyCommitLight(chainID, blockID, 100, commit)) +} + +func TestGetCommitForHeight_NoQuorum_Error(t *testing.T) { + chainID := "test-chain" + keys := []cmted25519.PrivKey{ + cmted25519.GenPrivKey(), cmted25519.GenPrivKey(), + cmted25519.GenPrivKey(), cmted25519.GenPrivKey(), + } + signers := []int{0, 1} // 2 of 4 — not > 2/3 + env, _, _ := buildEnv(t, 200, keys, signers, chainID) + + _, err := core.GetCommitForHeightForTest(context.Background(), env, 200) + require.Error(t, err) + require.Contains(t, err.Error(), "not yet attested") +} + +func TestGetCommitForHeight_AllSigned(t *testing.T) { + chainID := "test-chain" + keys := []cmted25519.PrivKey{ + cmted25519.GenPrivKey(), cmted25519.GenPrivKey(), + cmted25519.GenPrivKey(), cmted25519.GenPrivKey(), + } + env, valSet, blockID := buildEnv(t, 300, keys, []int{0, 1, 2, 3}, chainID) + + commit, err := core.GetCommitForHeightForTest(context.Background(), env, 300) + require.NoError(t, err) + require.Len(t, commit.Signatures, 4) + for _, cs := range commit.Signatures { + require.Equal(t, cmttypes.BlockIDFlagCommit, cs.BlockIDFlag) + } + require.NoError(t, valSet.VerifyCommitLight(chainID, blockID, 300, commit)) +} diff --git a/pkg/rpc/core/consensus.go b/pkg/rpc/core/consensus.go index ff0ee28a..3f380af0 100644 --- a/pkg/rpc/core/consensus.go +++ b/pkg/rpc/core/consensus.go @@ -1,20 +1,12 @@ package core import ( - "context" "fmt" - abci "github.com/cometbft/cometbft/abci/types" - cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" coretypes "github.com/cometbft/cometbft/rpc/core/types" rpctypes "github.com/cometbft/cometbft/rpc/jsonrpc/types" cmttypes "github.com/cometbft/cometbft/types" "github.com/cosmos/cosmos-sdk/crypto/codec" - cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/gogoproto/proto" - - networktypes "github.com/evstack/ev-abci/modules/network/types" ) // Validators gets the validator set at the given block height. @@ -30,29 +22,18 @@ func Validators(ctx *rpctypes.Context, heightPtr *int64, _, _ *int) (*coretypes. return nil, fmt.Errorf("failed to normalize height: %w", err) } - // In attester mode, return active attesters instead of genesis validator + // 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. if env.AttesterMode { - env.Logger.Info("Validators endpoint in attester mode - returning active attesters", "height", height) - - // Get attester signatures for this height to determine active attesters - signatures, err := getAttesterSignatures(ctx.Context(), int64(height)) + env.Logger.Info("Validators endpoint in attester mode - returning full attester set", "height", height) + entries, err := getAttesterSet(ctx.Context()) if err != nil { - env.Logger.Error("failed to get attester signatures", "height", height, "error", err) - // Fallback to genesis validator if no attester signatures available - return getGenesisValidatorSet(height) - } - - // If no attester signatures for this height, fallback to genesis validator - if len(signatures) == 0 { - env.Logger.Info("no attester signatures found for height, using genesis validator", "height", height) - return getGenesisValidatorSet(height) + return nil, fmt.Errorf("get attester set: %w", err) } - - // Convert attester signatures to validator set - validators, err := buildValidatorSetFromAttesters(signatures, height) + validators, err := buildValidatorSetFromAttesterSet(entries) if err != nil { - env.Logger.Error("failed to build validator set from attesters", "error", err) - return getGenesisValidatorSet(height) + return nil, fmt.Errorf("build validator set from attesters: %w", err) } return &coretypes.ResultValidators{ @@ -95,6 +76,27 @@ func getGenesisValidatorSet(height uint64) (*coretypes.ResultValidators, error) }, nil } +func buildValidatorSetFromAttesterSet(entries []attesterSetEntry) ([]*cmttypes.Validator, error) { + if len(entries) == 0 { + return nil, fmt.Errorf("no attesters found") + } + + validators := make([]*cmttypes.Validator, 0, len(entries)) + for _, e := range entries { + cmtPubKey, err := codec.ToCmtPubKeyInterface(e.Pubkey) + if err != nil { + return nil, fmt.Errorf("convert pubkey for %s: %w", e.ConsensusAddress, err) + } + validators = append(validators, &cmttypes.Validator{ + Address: e.ValidatorAddress, + PubKey: cmtPubKey, + VotingPower: 1, + ProposerPriority: 0, + }) + } + return validators, nil +} + // DumpConsensusState dumps consensus state. // UNSTABLE // More: https://docs.cometbft.com/v0.37/rpc/#/Info/dump_consensus_state @@ -147,109 +149,3 @@ func ConsensusParams(ctx *rpctypes.Context, heightPtr *int64) (*coretypes.Result }, }, nil } - -// buildValidatorSetFromAttesters builds a CometBFT validator set from attester signatures -// using stored attester information including public keys -func buildValidatorSetFromAttesters(signatures map[string][]byte, height uint64) ([]*cmttypes.Validator, error) { - ctx := context.Background() - validators := make([]*cmttypes.Validator, 0, len(signatures)) - - for validatorAddr, signature := range signatures { - // Parse the signature bytes (they should be marshaled cmtproto.Vote) - var vote cmtproto.Vote - if err := proto.Unmarshal(signature, &vote); err != nil { - env.Logger.Error("failed to unmarshal attester vote", - "validator", validatorAddr, "error", err) - continue - } - - // Use the validator address from the vote for the consensus address - consensusAddr := cmttypes.Address(vote.ValidatorAddress) - if len(vote.ValidatorAddress) != 20 { - // Fallback: try to derive from the bech32 address - valAddrBytes, err := sdk.ValAddressFromBech32(validatorAddr) - if err != nil { - env.Logger.Error("failed to decode validator address", - "validator", validatorAddr, "error", err) - continue - } - // Use first 20 bytes as consensus address - consensusAddr = cmttypes.Address(valAddrBytes[:20]) - } - - // Query the network module for attester information via ABCI - attesterInfo, err := getAttesterInfoByAddress(ctx, validatorAddr) - if err != nil { - env.Logger.Error("failed to get attester info", - "validator", validatorAddr, "error", err) - continue - } - - // Unpack the Any type to get the actual public key using network module codec - var actualPubKey cryptotypes.PubKey - if err := networktypes.ModuleCdc.InterfaceRegistry().UnpackAny(attesterInfo.Pubkey, &actualPubKey); err != nil { - env.Logger.Error("failed to unpack public key from Any type", - "validator", validatorAddr, "error", err) - continue - } - - // Convert Cosmos SDK PubKey to CometBFT PubKey using standard codec - cmtPubKey, err := codec.ToCmtPubKeyInterface(actualPubKey) - if err != nil { - env.Logger.Error("failed to convert public key to CometBFT format", - "validator", validatorAddr, "error", err) - continue - } - - env.Logger.Info("creating validator entry for attester", - "validator", validatorAddr, "address", consensusAddr.String()) - - validators = append(validators, &cmttypes.Validator{ - Address: consensusAddr, - PubKey: cmtPubKey, - VotingPower: 1, // Set uniform voting power for attesters - ProposerPriority: 0, // Set to 0 for all attesters - }) - } - - if len(validators) == 0 { - return nil, fmt.Errorf("no valid attester validators found") - } - - env.Logger.Info("Built validator set from attesters", - "count", len(validators), "height", height) - - return validators, nil -} - -// getAttesterInfoByAddress queries the network module for attester information via ABCI -func getAttesterInfoByAddress(ctx context.Context, validatorAddr string) (*networktypes.AttesterInfo, error) { - // Create properly marshaled query request - queryReq := &networktypes.QueryAttesterInfoRequest{ - ValidatorAddress: validatorAddr, - } - - queryReqBytes, err := proto.Marshal(queryReq) - if err != nil { - return nil, fmt.Errorf("marshal query request: %w", err) - } - - result, err := env.Adapter.App.Query(ctx, &abci.RequestQuery{ - Path: "/evabci.network.v1.Query/AttesterInfo", - Data: queryReqBytes, // Properly marshaled protobuf request - }) - if err != nil { - return nil, fmt.Errorf("query attester info: %w", err) - } - - if result.Code != 0 { - return nil, fmt.Errorf("attester info not found: code %d, log: %s", result.Code, result.Log) - } - - var queryResp networktypes.QueryAttesterInfoResponse - if err := proto.Unmarshal(result.Value, &queryResp); err != nil { - return nil, fmt.Errorf("unmarshal attester info response: %w", err) - } - - return queryResp.AttesterInfo, nil -} diff --git a/pkg/rpc/core/consensus_test.go b/pkg/rpc/core/consensus_test.go index aa0a9599..3c61adae 100644 --- a/pkg/rpc/core/consensus_test.go +++ b/pkg/rpc/core/consensus_test.go @@ -6,13 +6,18 @@ import ( "testing" "time" + abci "github.com/cometbft/cometbft/abci/types" "github.com/cometbft/cometbft/crypto/ed25519" cmtlog "github.com/cometbft/cometbft/libs/log" cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" rpctypes "github.com/cometbft/cometbft/rpc/jsonrpc/types" cmtstate "github.com/cometbft/cometbft/state" cmttypes "github.com/cometbft/cometbft/types" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" + sdk "github.com/cosmos/cosmos-sdk/types" genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types" + "github.com/cosmos/gogoproto/proto" ds "github.com/ipfs/go-datastore" testifyassert "github.com/stretchr/testify/assert" testifymock "github.com/stretchr/testify/mock" @@ -20,6 +25,7 @@ import ( rollkitmocks "github.com/evstack/ev-node/test/mocks" + networktypes "github.com/evstack/ev-abci/modules/network/types" "github.com/evstack/ev-abci/pkg/adapter" execstore "github.com/evstack/ev-abci/pkg/store" ) @@ -222,6 +228,106 @@ func TestValidators(t *testing.T) { }) } +func TestValidatorsAttesterModeReturnsFullAttesterSet(t *testing.T) { + chainID := "test-chain" + height := int64(100) + privs := []ed25519.PrivKey{ + ed25519.GenPrivKey(), + ed25519.GenPrivKey(), + ed25519.GenPrivKey(), + ed25519.GenPrivKey(), + } + + setEntries := make([]networktypes.AttesterSetEntry, 0, len(privs)) + attesterInfoByAddr := make(map[string]*networktypes.AttesterInfo) + for i, priv := range privs { + pub := priv.PubKey() + sdkPk, err := cryptocodec.FromCmtPubKeyInterface(pub) + require.NoError(t, err) + any, err := codectypes.NewAnyWithValue(sdkPk) + require.NoError(t, err) + consAddr := sdk.ConsAddress(pub.Address()).String() + setEntries = append(setEntries, networktypes.AttesterSetEntry{ + Authority: sdk.AccAddress(pub.Address()).String(), + ConsensusAddress: consAddr, + Index: uint32(i), //nolint:gosec + Pubkey: any, + }) + attesterInfoByAddr[consAddr] = &networktypes.AttesterInfo{ + Authority: sdk.AccAddress(pub.Address()).String(), + Pubkey: any, + ConsensusAddress: consAddr, + } + } + + setRespBz, err := proto.Marshal(&networktypes.QueryAttesterSetResponse{Entries: setEntries}) + require.NoError(t, err) + + signatures := make([]*networktypes.AttesterSignature, 0, 3) + blockID := cmtproto.BlockID{Hash: make([]byte, 32), PartSetHeader: cmtproto.PartSetHeader{}} + for i, priv := range privs[:3] { + vote := cmtproto.Vote{ + Type: cmtproto.PrecommitType, + Height: height, + Round: 0, + BlockID: blockID, + Timestamp: time.Date(2026, 4, 22, 12, 0, 0, 0, time.UTC), + ValidatorAddress: priv.PubKey().Address(), + ValidatorIndex: int32(i), //nolint:gosec + } + sig, err := priv.Sign(cmttypes.VoteSignBytes(chainID, &vote)) + require.NoError(t, err) + vote.Signature = sig + voteBz, err := proto.Marshal(&vote) + require.NoError(t, err) + signatures = append(signatures, &networktypes.AttesterSignature{ + ValidatorAddress: setEntries[i].ConsensusAddress, + Signature: voteBz, + }) + } + sigRespBz, err := proto.Marshal(&networktypes.QueryAttesterSignaturesResponse{Signatures: signatures}) + require.NoError(t, err) + + mApp := new(MockApp) + mApp.On("Query", testifymock.Anything, testifymock.MatchedBy(func(r *abci.RequestQuery) bool { + return r.Path == "/evabci.network.v1.Query/AttesterSet" + })).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" + })).Return(&abci.ResponseQuery{Code: 0, Value: sigRespBz}, nil).Maybe() + for consAddr, info := range attesterInfoByAddr { + infoRespBz, err := proto.Marshal(&networktypes.QueryAttesterInfoResponse{AttesterInfo: info}) + require.NoError(t, err) + consAddr := consAddr + mApp.On("Query", testifymock.Anything, testifymock.MatchedBy(func(r *abci.RequestQuery) bool { + if r.Path != "/evabci.network.v1.Query/AttesterInfo" { + return false + } + var req networktypes.QueryAttesterInfoRequest + if err := proto.Unmarshal(r.Data, &req); err != nil { + return false + } + return req.ValidatorAddress == consAddr + })).Return(&abci.ResponseQuery{Code: 0, Value: infoRespBz}, nil).Maybe() + } + + env = &Environment{ + Adapter: &adapter.Adapter{App: mApp}, + AttesterMode: true, + Logger: cmtlog.NewNopLogger(), + } + + result, err := Validators(&rpctypes.Context{}, &height, nil, nil) + require.NoError(t, err) + require.Len(t, result.Validators, 4) + require.Equal(t, 4, result.Count) + require.Equal(t, 4, result.Total) + for i, validator := range result.Validators { + require.Equal(t, privs[i].PubKey().Address(), validator.Address) + require.Equal(t, int64(1), validator.VotingPower) + } +} + func TestDumpConsensusState(t *testing.T) { assert := testifyassert.New(t) require := require.New(t)