diff --git a/.github/workflows/migration_test.yml b/.github/workflows/migration_test.yml index a5d9c478..a22d1922 100644 --- a/.github/workflows/migration_test.yml +++ b/.github/workflows/migration_test.yml @@ -58,7 +58,7 @@ jobs: steps: - uses: actions/checkout@v5 with: - fetch-depth: 0 + fetch-depth: 1 - name: Log in to GHCR uses: docker/login-action@v4 @@ -107,7 +107,7 @@ jobs: steps: - uses: actions/checkout@v5 with: - fetch-depth: 0 + fetch-depth: 1 - name: Log in to GHCR uses: docker/login-action@v4 @@ -152,7 +152,7 @@ jobs: steps: - uses: actions/checkout@v5 with: - fetch-depth: 0 + fetch-depth: 1 - name: Set up Go uses: actions/setup-go@v6 @@ -194,7 +194,7 @@ jobs: steps: - uses: actions/checkout@v5 with: - fetch-depth: 0 + fetch-depth: 1 - name: Set up Go uses: actions/setup-go@v6 diff --git a/modules/network/genesis.go b/modules/network/genesis.go index 85e0f6da..388b3a03 100644 --- a/modules/network/genesis.go +++ b/modules/network/genesis.go @@ -1,9 +1,12 @@ package network import ( + "bytes" "fmt" + "sort" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/bech32" "github.com/evstack/ev-abci/modules/network/keeper" "github.com/evstack/ev-abci/modules/network/types" @@ -11,38 +14,80 @@ import ( // InitGenesis initializes the network module's state from a provided genesis state. func InitGenesis(ctx sdk.Context, k keeper.Keeper, genState types.GenesisState) error { - // Set module params if err := k.SetParams(ctx, genState.Params); err != nil { return fmt.Errorf("set params: %s", err) } - // Set validator indices - for _, vi := range genState.ValidatorIndices { - if err := k.SetValidatorIndex(ctx, vi.Address, uint16(vi.Index), vi.Power); err != nil { - return err + // Load attesters: validate pubkey/address match, then insert and assign indices. + attesters := make([]types.AttesterInfo, len(genState.AttesterInfos)) + copy(attesters, genState.AttesterInfos) + + for i := range attesters { + info := attesters[i] + pk, err := info.GetPubKey() + if err != nil { + return fmt.Errorf("attester %d: %w", i, err) } - // Also add to attester set - if err := k.SetAttesterSetMember(ctx, vi.Address); err != nil { - return err + // Validate pubkey ↔ consensus_address match at the raw-bytes level so + // the check is independent of bech32 prefix (e.g. "cosmosvalcons" vs + // "celestiavalcons"). Whatever prefix was used in genesis, the 20-byte + // payload must equal the pubkey's Address(). + _, rawAddr, decErr := bech32.DecodeAndConvert(info.ConsensusAddress) + if decErr != nil { + return fmt.Errorf("attester %d: decode consensus_address %q: %w", i, info.ConsensusAddress, decErr) + } + if !bytes.Equal(rawAddr, pk.Address()) { + return fmt.Errorf("attester %d: pubkey address mismatch (derived bytes %x, stated bytes %x)", + i, pk.Address(), rawAddr) + } + // Re-encode consensus_address with the runtime SDK config so the + // stored value matches what ConsAddress().String() produces elsewhere + // in the module at runtime. + derived := sdk.ConsAddress(pk.Address()).String() + info.ConsensusAddress = derived + attesters[i] = info + } + + // Order by pubkey.Address() bytes ascending to match cmttypes.NewValidatorSet. + sort.Slice(attesters, func(i, j int) bool { + pki, _ := attesters[i].GetPubKey() + pkj, _ := attesters[j].GetPubKey() + return bytes.Compare(pki.Address(), pkj.Address()) < 0 + }) + + for idx, info := range attesters { + if err := k.SetAttesterInfo(ctx, info.ConsensusAddress, &info); err != nil { + return fmt.Errorf("set attester info: %w", err) + } + if err := k.SetAttesterSetMember(ctx, info.ConsensusAddress); err != nil { + return fmt.Errorf("set attester set member: %w", err) + } + if err := k.SetValidatorIndex(ctx, info.ConsensusAddress, uint16(idx), 1); err != nil { + return fmt.Errorf("set validator index: %w", err) } } - // Set attestation bitmaps + // Still load historical bitmaps if provided (upgrade/dump scenarios). for _, ab := range genState.AttestationBitmaps { if err := k.SetAttestationBitmap(ctx, ab.Height, ab.Bitmap); err != nil { return err } - // Store full attestation info using collections API if err := k.StoredAttestationInfo.Set(ctx, ab.Height, ab); err != nil { return err } - if ab.SoftConfirmed { if err := setSoftConfirmed(ctx, k, ab.Height); err != nil { return err } } } + + // Legacy: genState.ValidatorIndices is now derived from AttesterInfos and + // ignored. Warn if non-empty so operators notice. + if len(genState.ValidatorIndices) > 0 { + k.Logger(ctx).Error("genesis.validator_indices is deprecated and ignored; use attester_infos") + } + return nil } @@ -51,29 +96,22 @@ func ExportGenesis(ctx sdk.Context, k keeper.Keeper) *types.GenesisState { genesis := types.DefaultGenesisState() genesis.Params = k.GetParams(ctx) - // Export validator indices using collections API - var validatorIndices []types.ValidatorIndex - // Iterate through all validator indices - if err := k.ValidatorIndex.Walk(ctx, nil, func(addr string, index uint16) (bool, error) { - power, err := k.GetValidatorPower(ctx, index) - if err != nil { - return false, fmt.Errorf("get validator power: %w", err) - } - validatorIndices = append(validatorIndices, types.ValidatorIndex{ - Address: addr, - Index: uint32(index), - Power: power, - }) + var attesters []types.AttesterInfo + if err := k.AttesterInfo.Walk(ctx, nil, func(_ string, info types.AttesterInfo) (bool, error) { + attesters = append(attesters, info) return false, nil }); err != nil { panic(err) } - genesis.ValidatorIndices = validatorIndices + sort.Slice(attesters, func(i, j int) bool { + pki, _ := attesters[i].GetPubKey() + pkj, _ := attesters[j].GetPubKey() + return bytes.Compare(pki.Address(), pkj.Address()) < 0 + }) + genesis.AttesterInfos = attesters - // Export attestation bitmaps using collections API var attestationBitmaps []types.AttestationBitmap - // Iterate through all stored attestation info - if err := k.StoredAttestationInfo.Walk(ctx, nil, func(height int64, ab types.AttestationBitmap) (bool, error) { + if err := k.StoredAttestationInfo.Walk(ctx, nil, func(_ int64, ab types.AttestationBitmap) (bool, error) { attestationBitmaps = append(attestationBitmaps, ab) return false, nil }); err != nil { @@ -81,24 +119,18 @@ func ExportGenesis(ctx sdk.Context, k keeper.Keeper) *types.GenesisState { } genesis.AttestationBitmaps = attestationBitmaps + // ValidatorIndices no longer exported: they are derived deterministically + // from AttesterInfos order. + genesis.ValidatorIndices = nil + return genesis } -// Helper function to set soft confirmed status func setSoftConfirmed(ctx sdk.Context, k keeper.Keeper, height int64) error { - // Get the existing attestation bitmap ab, err := k.StoredAttestationInfo.Get(ctx, height) if err != nil { - // If there's no existing attestation bitmap, we can't set it as soft confirmed return err } - - // Set the SoftConfirmed field to true ab.SoftConfirmed = true - - // Update the attestation bitmap in the collection - if err := k.StoredAttestationInfo.Set(ctx, height, ab); err != nil { - return err - } - return nil + return k.StoredAttestationInfo.Set(ctx, height, ab) } diff --git a/modules/network/keeper/abci.go b/modules/network/keeper/abci.go index f212f5b7..81dcc108 100644 --- a/modules/network/keeper/abci.go +++ b/modules/network/keeper/abci.go @@ -112,14 +112,13 @@ func (k Keeper) processEpochEnd(ctx sdk.Context, epoch uint64) error { } } - // todo (Alex): find a way to prune only bitmaps that are not used anymore + // Validator indices are established at genesis and never mutate at runtime + // (MsgJoin/MsgLeave are disabled). Nothing to rebuild here. + + // todo: find a way to prune only bitmaps that are not used anymore // if err := k.PruneOldBitmaps(ctx, epoch); err != nil { // return fmt.Errorf("pruning old data at epoch %d: %w", epoch, err) // } - - if err := k.BuildValidatorIndexMap(ctx); err != nil { - return fmt.Errorf("rebuilding validator index map at epoch %d: %w", epoch, err) - } return nil } diff --git a/modules/network/keeper/attester_ibc_test.go b/modules/network/keeper/attester_ibc_test.go new file mode 100644 index 00000000..3437f338 --- /dev/null +++ b/modules/network/keeper/attester_ibc_test.go @@ -0,0 +1,261 @@ +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") +} + +// -- 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/bitmap_test.go b/modules/network/keeper/bitmap_test.go new file mode 100644 index 00000000..9515f9a4 --- /dev/null +++ b/modules/network/keeper/bitmap_test.go @@ -0,0 +1,269 @@ +package keeper + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewBitmap(t *testing.T) { + bh := NewBitmapHelper() + + specs := map[string]struct { + n int + expSize int + }{ + "zero validators": {n: 0, expSize: 0}, + "one validator": {n: 1, expSize: 1}, + "seven validators": {n: 7, expSize: 1}, + "eight validators": {n: 8, expSize: 1}, + "nine validators": {n: 9, expSize: 2}, + "sixteen validators": {n: 16, expSize: 2}, + "seventeen validators": {n: 17, expSize: 3}, + "one-hundred validators": {n: 100, expSize: 13}, + "ten-thousand validators": {n: 10_000, expSize: 1250}, + } + + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + bitmap := bh.NewBitmap(spec.n) + require.Len(t, bitmap, spec.expSize) + for _, b := range bitmap { + assert.Equal(t, byte(0), b, "new bitmap must be zero-initialised") + } + }) + } +} + +func TestSetBitAndIsSet(t *testing.T) { + bh := NewBitmapHelper() + + t.Run("set and read within range", func(t *testing.T) { + bitmap := bh.NewBitmap(16) + // Cover low-byte, high-byte, and edges. + for _, idx := range []int{0, 1, 7, 8, 9, 15} { + assert.False(t, bh.IsSet(bitmap, idx), "bit %d should start unset", idx) + bh.SetBit(bitmap, idx) + assert.True(t, bh.IsSet(bitmap, idx), "bit %d should be set", idx) + } + }) + + t.Run("out-of-range index is a no-op", func(t *testing.T) { + bitmap := bh.NewBitmap(8) + + bh.SetBit(bitmap, -1) + bh.SetBit(bitmap, 8) + bh.SetBit(bitmap, 100) + + assert.Equal(t, 0, bh.PopCount(bitmap), "out-of-range SetBit must not flip any bit") + assert.False(t, bh.IsSet(bitmap, -1)) + assert.False(t, bh.IsSet(bitmap, 8)) + assert.False(t, bh.IsSet(bitmap, 100)) + }) + + t.Run("set is idempotent", func(t *testing.T) { + bitmap := bh.NewBitmap(8) + bh.SetBit(bitmap, 3) + bh.SetBit(bitmap, 3) + assert.Equal(t, 1, bh.PopCount(bitmap)) + assert.True(t, bh.IsSet(bitmap, 3)) + }) + + t.Run("setting one bit does not affect neighbours", func(t *testing.T) { + bitmap := bh.NewBitmap(16) + bh.SetBit(bitmap, 4) + for i := 0; i < 16; i++ { + if i == 4 { + continue + } + assert.False(t, bh.IsSet(bitmap, i), "bit %d must remain unset", i) + } + }) +} + +func TestPopCount(t *testing.T) { + bh := NewBitmapHelper() + + specs := map[string]struct { + bitmap []byte + expCount int + }{ + "empty": {bitmap: []byte{}, expCount: 0}, + "nil": {bitmap: nil, expCount: 0}, + "all zeros": {bitmap: []byte{0x00, 0x00}, expCount: 0}, + "all ones one byte": {bitmap: []byte{0xFF}, expCount: 8}, + "all ones two bytes": {bitmap: []byte{0xFF, 0xFF}, expCount: 16}, + "single bit low": {bitmap: []byte{0x01}, expCount: 1}, + "single bit high": {bitmap: []byte{0x80}, expCount: 1}, + "mixed": {bitmap: []byte{0x0F, 0xF0}, expCount: 8}, + } + + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + assert.Equal(t, spec.expCount, bh.PopCount(spec.bitmap)) + }) + } +} + +func TestOR(t *testing.T) { + bh := NewBitmapHelper() + + t.Run("same length", func(t *testing.T) { + dst := []byte{0x0F, 0x00} + src := []byte{0xF0, 0xAA} + bh.OR(dst, src) + assert.Equal(t, []byte{0xFF, 0xAA}, dst) + }) + + t.Run("src shorter than dst leaves extra bytes untouched", func(t *testing.T) { + dst := []byte{0x00, 0x00, 0xAA} + src := []byte{0x0F} + bh.OR(dst, src) + assert.Equal(t, []byte{0x0F, 0x00, 0xAA}, dst) + }) + + t.Run("dst shorter than src ignores extra src bytes", func(t *testing.T) { + dst := []byte{0x00} + src := []byte{0x0F, 0xFF} + bh.OR(dst, src) + assert.Equal(t, []byte{0x0F}, dst) + }) + + t.Run("OR is commutative on overlap", func(t *testing.T) { + a := []byte{0b11001100} + b := []byte{0b10101010} + expected := []byte{0b11101110} + + cp1 := append([]byte(nil), a...) + bh.OR(cp1, b) + cp2 := append([]byte(nil), b...) + bh.OR(cp2, a) + + assert.Equal(t, expected, cp1) + assert.Equal(t, expected, cp2) + }) +} + +func TestAND(t *testing.T) { + bh := NewBitmapHelper() + + t.Run("same length", func(t *testing.T) { + dst := []byte{0xFF, 0xAA} + src := []byte{0x0F, 0xF0} + bh.AND(dst, src) + assert.Equal(t, []byte{0x0F, 0xA0}, dst) + }) + + t.Run("src shorter than dst leaves extra bytes untouched", func(t *testing.T) { + dst := []byte{0xFF, 0xFF, 0xAA} + src := []byte{0x0F} + bh.AND(dst, src) + assert.Equal(t, []byte{0x0F, 0xFF, 0xAA}, dst) + }) + + t.Run("disjoint bits produce zero", func(t *testing.T) { + dst := []byte{0xF0} + src := []byte{0x0F} + bh.AND(dst, src) + assert.Equal(t, []byte{0x00}, dst) + }) +} + +func TestCopy(t *testing.T) { + bh := NewBitmapHelper() + + t.Run("nil input returns nil", func(t *testing.T) { + assert.Nil(t, bh.Copy(nil)) + }) + + t.Run("empty input returns empty non-nil slice", func(t *testing.T) { + cp := bh.Copy([]byte{}) + require.NotNil(t, cp) + assert.Len(t, cp, 0) + }) + + t.Run("copy is independent of original", func(t *testing.T) { + original := []byte{0x0F, 0xF0} + cp := bh.Copy(original) + assert.Equal(t, original, cp) + + cp[0] = 0xAA + assert.Equal(t, byte(0x0F), original[0], "mutating copy must not affect original") + original[1] = 0xBB + assert.Equal(t, byte(0xF0), cp[1], "mutating original must not affect copy") + }) +} + +func TestClear(t *testing.T) { + bh := NewBitmapHelper() + + t.Run("clears non-empty bitmap", func(t *testing.T) { + bitmap := []byte{0xFF, 0xAA, 0x55} + bh.Clear(bitmap) + assert.Equal(t, []byte{0x00, 0x00, 0x00}, bitmap) + }) + + t.Run("clear on empty bitmap is a no-op", func(t *testing.T) { + bitmap := []byte{} + bh.Clear(bitmap) + assert.Equal(t, []byte{}, bitmap) + }) + + t.Run("clear on nil is a no-op", func(t *testing.T) { + var bitmap []byte + bh.Clear(bitmap) + assert.Nil(t, bitmap) + }) +} + +func TestCountInRange(t *testing.T) { + bh := NewBitmapHelper() + + // bitmap of 16 bits with bits 1, 3, 5, 8, 15 set + bitmap := bh.NewBitmap(16) + for _, idx := range []int{1, 3, 5, 8, 15} { + bh.SetBit(bitmap, idx) + } + + specs := map[string]struct { + start, end int + expCount int + }{ + "full range": {start: 0, end: 16, expCount: 5}, + "first byte only": {start: 0, end: 8, expCount: 3}, + "second byte only": {start: 8, end: 16, expCount: 2}, + "empty range": {start: 4, end: 4, expCount: 0}, + "inverted range is empty": {start: 10, end: 4, expCount: 0}, + "end past bitmap length": {start: 0, end: 1000, expCount: 5}, + "start past bitmap length": {start: 200, end: 300, expCount: 0}, + "range excluding boundary bit": {start: 2, end: 5, expCount: 1}, + "range including boundary bit": {start: 1, end: 6, expCount: 3}, + } + + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + assert.Equal(t, spec.expCount, bh.CountInRange(bitmap, spec.start, spec.end)) + }) + } +} + +func TestBitmapRoundTrip(t *testing.T) { + bh := NewBitmapHelper() + + indices := []int{0, 3, 7, 8, 15, 63, 99} + bitmap := bh.NewBitmap(100) + for _, idx := range indices { + bh.SetBit(bitmap, idx) + } + + assert.Equal(t, len(indices), bh.PopCount(bitmap)) + for _, idx := range indices { + assert.True(t, bh.IsSet(bitmap, idx), "bit %d should be set", idx) + } + + cp := bh.Copy(bitmap) + bh.Clear(bitmap) + assert.Equal(t, 0, bh.PopCount(bitmap)) + assert.Equal(t, len(indices), bh.PopCount(cp), "copy must be unaffected by clearing original") +} diff --git a/modules/network/keeper/genesis_test.go b/modules/network/keeper/genesis_test.go new file mode 100644 index 00000000..3acdbd06 --- /dev/null +++ b/modules/network/keeper/genesis_test.go @@ -0,0 +1,131 @@ +package keeper_test + +import ( + "bytes" + "sort" + "testing" + "time" + + "cosmossdk.io/log" + storetypes "cosmossdk.io/store/types" + cmted25519 "github.com/cometbft/cometbft/crypto/ed25519" + cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" + "github.com/cosmos/cosmos-sdk/codec" + cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" + "github.com/cosmos/cosmos-sdk/runtime" + "github.com/cosmos/cosmos-sdk/testutil/integration" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/stretchr/testify/require" + + "github.com/evstack/ev-abci/modules/network" + "github.com/evstack/ev-abci/modules/network/keeper" + "github.com/evstack/ev-abci/modules/network/types" +) + +func newKeeperForGenesis(t *testing.T) (keeper.Keeper, sdk.Context, codec.BinaryCodec) { + t.Helper() + cdc := moduletestutil.MakeTestEncodingConfig().Codec + keys := storetypes.NewKVStoreKeys(types.StoreKey) + logger := log.NewTestLogger(t) + cms := integration.CreateMultiStore(keys, logger) + authority := authtypes.NewModuleAddress("gov") + k := keeper.NewKeeper(cdc, runtime.NewKVStoreService(keys[types.StoreKey]), nil, nil, nil, authority.String()) + ctx := sdk.NewContext(cms, cmtproto.Header{ChainID: "test-chain", Time: time.Now().UTC(), Height: 1}, false, logger). + WithContext(t.Context()) + return k, ctx, cdc +} + +func mustAnyPubKey(t *testing.T, cmtPk cmted25519.PubKey) *types.AttesterInfo { + t.Helper() + sdkPk, err := cryptocodec.FromCmtPubKeyInterface(cmtPk) + require.NoError(t, err) + info, err := types.NewAttesterInfo( + sdk.AccAddress(cmtPk.Address()).String(), + sdkPk, + 0, + ) + require.NoError(t, err) + return info +} + +func TestInitGenesisLoadsAttesters(t *testing.T) { + k, ctx, _ := newKeeperForGenesis(t) + + pk1 := cmted25519.GenPrivKey().PubKey().(cmted25519.PubKey) + pk2 := cmted25519.GenPrivKey().PubKey().(cmted25519.PubKey) + info1 := mustAnyPubKey(t, pk1) + info1.ConsensusAddress = sdk.ConsAddress(pk1.Address()).String() + info2 := mustAnyPubKey(t, pk2) + info2.ConsensusAddress = sdk.ConsAddress(pk2.Address()).String() + + gs := types.GenesisState{ + Params: types.DefaultParams(), + AttesterInfos: []types.AttesterInfo{*info1, *info2}, + } + + require.NoError(t, network.InitGenesis(ctx, k, gs)) + + // Both in AttesterSet + for _, info := range []*types.AttesterInfo{info1, info2} { + has, err := k.AttesterSet.Has(ctx, info.ConsensusAddress) + require.NoError(t, err) + require.True(t, has) + + stored, err := k.GetAttesterInfo(ctx, info.ConsensusAddress) + require.NoError(t, err) + require.Equal(t, info.Authority, stored.Authority) + } + + // Validator indices assigned in ascending pubkey-address order, power=1 + expectedOrder := []cmted25519.PubKey{pk1, pk2} + sort.Slice(expectedOrder, func(i, j int) bool { + return bytes.Compare(expectedOrder[i].Address(), expectedOrder[j].Address()) < 0 + }) + for i, pk := range expectedOrder { + consAddr := sdk.ConsAddress(pk.Address()).String() + idx, found := k.GetValidatorIndex(ctx, consAddr) + require.True(t, found, "consensus address %s missing index", consAddr) + require.Equal(t, uint16(i), idx) + power, err := k.GetValidatorPower(ctx, idx) + require.NoError(t, err) + require.Equal(t, uint64(1), power) + } +} + +func TestInitGenesisRejectsPubkeyAddressMismatch(t *testing.T) { + k, ctx, _ := newKeeperForGenesis(t) + + pk := cmted25519.GenPrivKey().PubKey().(cmted25519.PubKey) + info := mustAnyPubKey(t, pk) + info.ConsensusAddress = sdk.ConsAddress([]byte("not-matching-20-bytes")).String() + + gs := types.GenesisState{ + Params: types.DefaultParams(), + AttesterInfos: []types.AttesterInfo{*info}, + } + + err := network.InitGenesis(ctx, k, gs) + require.Error(t, err) + require.Contains(t, err.Error(), "pubkey address mismatch") +} + +func TestExportGenesisRoundtripsAttesters(t *testing.T) { + k, ctx, _ := newKeeperForGenesis(t) + + pk := cmted25519.GenPrivKey().PubKey().(cmted25519.PubKey) + info := mustAnyPubKey(t, pk) + info.ConsensusAddress = sdk.ConsAddress(pk.Address()).String() + + gs := types.GenesisState{ + Params: types.DefaultParams(), + AttesterInfos: []types.AttesterInfo{*info}, + } + require.NoError(t, network.InitGenesis(ctx, k, gs)) + + exported := network.ExportGenesis(ctx, k) + require.Len(t, exported.AttesterInfos, 1) + require.Equal(t, info.ConsensusAddress, exported.AttesterInfos[0].ConsensusAddress) + require.Equal(t, info.Authority, exported.AttesterInfos[0].Authority) +} diff --git a/modules/network/keeper/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/keeper/keeper.go b/modules/network/keeper/keeper.go index 60494d03..fdf5d7ee 100644 --- a/modules/network/keeper/keeper.go +++ b/modules/network/keeper/keeper.go @@ -14,6 +14,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 +28,7 @@ type Keeper struct { bankKeeper types.BankKeeper authority string bitmapHelper *BitmapHelper + mut *mutableState // Collections for state management ValidatorIndex collections.Map[string, uint16] @@ -55,6 +62,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 +89,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") @@ -185,52 +209,6 @@ func (k Keeper) GetAllAttesters(ctx sdk.Context) ([]string, error) { return attesters, nil } -// MaxAttesters is the maximum number of attesters allowed in the set. -// This prevents unbounded growth, EndBlocker stalling, and uint16 index overflow -// in BuildValidatorIndexMap. -const MaxAttesters = 10_000 - -// BuildValidatorIndexMap rebuilds the validator index mapping -func (k Keeper) BuildValidatorIndexMap(ctx sdk.Context) error { - // Get all attesters instead of bonded validators - attesters, err := k.GetAllAttesters(ctx) - if err != nil { - return err - } - - // Guard against uint16 overflow — should not happen if MaxAttesters is enforced - // at join time, but defense-in-depth - if len(attesters) > int(^uint16(0)) { - return fmt.Errorf("attester count %d exceeds uint16 max %d", len(attesters), ^uint16(0)) - } - - // Clear existing indices and powers - // The `nil` range clears all entries in the collection. - if err := k.ValidatorIndex.Clear(ctx, nil); err != nil { - k.Logger(ctx).Error("failed to clear validator index", "error", err) - return err - } - if err := k.ValidatorPower.Clear(ctx, nil); err != nil { - k.Logger(ctx).Error("failed to clear validator power", "error", err) - return err - } - - // Build new indices for all attesters with voting power of 1 - index := uint16(0) - for _, attesterAddr := range attesters { - power := uint64(1) // Assign voting power of 1 to all attesters - if err := k.SetValidatorIndex(ctx, attesterAddr, index, power); err != nil { - // Consider how to handle partial failures; potentially log and continue or return error. - k.Logger(ctx).Error("failed to set validator index during build", "attester", attesterAddr, "error", err) - return err - } - k.Logger(ctx).Debug("assigned index to attester", "attester", attesterAddr, "index", index, "power", power) - index++ - } - k.Logger(ctx).Info("rebuilt validator index map for attesters", "count", len(attesters)) - return nil -} - // GetCurrentEpoch returns the current epoch number func (k Keeper) GetCurrentEpoch(ctx sdk.Context) uint64 { params := k.GetParams(ctx) diff --git a/modules/network/keeper/msg_server.go b/modules/network/keeper/msg_server.go index 2bf7034e..9f121609 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,106 +132,103 @@ func (k msgServer) Attest(goCtx context.Context, msg *types.MsgAttest) (*types.M sdk.NewAttribute("height", math.NewInt(msg.Height).String()), ), ) - return &types.MsgAttestResponse{}, nil } // JoinAttesterSet handles MsgJoinAttesterSet func (k msgServer) JoinAttesterSet(goCtx context.Context, msg *types.MsgJoinAttesterSet) (*types.MsgJoinAttesterSetResponse, error) { - ctx := sdk.UnwrapSDKContext(goCtx) - - // Validate the consensus address format - _, err := sdk.ValAddressFromBech32(msg.ConsensusAddress) - if err != nil { - return nil, sdkerr.Wrapf(sdkerrors.ErrInvalidAddress, "invalid consensus address: %s", err) - } + return nil, sdkerr.Wrap(sdkerrors.ErrInvalidRequest, + "attester set changes disabled; the set is fixed at genesis") +} - // NOTE: Removed bonded validator requirement to allow any address to join attester set - // This allows external attesters that are not part of the validator set +// LeaveAttesterSet handles MsgLeaveAttesterSet +func (k msgServer) LeaveAttesterSet(goCtx context.Context, msg *types.MsgLeaveAttesterSet) (*types.MsgLeaveAttesterSetResponse, error) { + return nil, sdkerr.Wrap(sdkerrors.ErrInvalidRequest, + "attester set changes disabled; the set is fixed at genesis") +} - // Check if already in attester set (use consensus address) - has, err := k.IsInAttesterSet(ctx, msg.ConsensusAddress) +func (k msgServer) assertValidValidatorAuthority(ctx sdk.Context, consensusAddress, authority string) error { + v, err := k.AttesterInfo.Get(ctx, consensusAddress) if err != nil { - return nil, sdkerr.Wrapf(err, "in attester set") + if errors.Is(err, collections.ErrNotFound) { + return sdkerr.Wrapf(sdkerrors.ErrUnauthorized, "consensus address %s not in attester set", consensusAddress) + } + return sdkerr.Wrapf(err, "attester set") } - if has { - return nil, sdkerr.Wrapf(sdkerrors.ErrInvalidRequest, "consensus address already in attester set") + if v.Authority != authority { + return sdkerr.Wrapf(sdkerrors.ErrUnauthorized, "address %s", authority) } + return nil +} - // Enforce maximum attester set size to prevent unbounded growth and uint16 index overflow - attesters, err := k.GetAllAttesters(ctx) - if err != nil { - return nil, sdkerr.Wrap(err, "get all attesters") +// 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 len(attesters) >= MaxAttesters { - return nil, sdkerr.Wrapf(sdkerrors.ErrInvalidRequest, "attester set is full: %d/%d", len(attesters), MaxAttesters) + if v.Type != cmtproto.PrecommitType { + return nil, sdkerr.Wrapf(sdkerrors.ErrInvalidRequest, "vote type must be Precommit, got %s", v.Type) } - - // Store the attester information including pubkey (key by consensus address) - attesterInfo := &types.AttesterInfo{ - Authority: msg.Authority, - Pubkey: msg.Pubkey, - JoinedHeight: ctx.BlockHeight(), + if v.Height != msgHeight { + return nil, sdkerr.Wrapf(sdkerrors.ErrInvalidRequest, "vote height %d != msg height %d", v.Height, msgHeight) } - - if err := k.SetAttesterInfo(ctx, msg.ConsensusAddress, attesterInfo); err != nil { - return nil, sdkerr.Wrap(err, "set attester info") - } - // TODO (Alex): the valset should be updated at the end of an epoch only - if err := k.SetAttesterSetMember(ctx, msg.ConsensusAddress); err != nil { - return nil, sdkerr.Wrap(err, "set attester set member") + if v.Round != 0 { + return nil, sdkerr.Wrapf(sdkerrors.ErrInvalidRequest, "vote round must be 0, got %d", v.Round) } - ctx.EventManager().EmitEvent( - sdk.NewEvent( - types.TypeMsgJoinAttesterSet, - sdk.NewAttribute("consensus_address", msg.ConsensusAddress), - sdk.NewAttribute("authority", msg.Authority), - ), - ) - k.Logger(ctx).Info("+++ joined attester set", "consensus_address", msg.ConsensusAddress, "authority", msg.Authority) - return &types.MsgJoinAttesterSetResponse{}, nil -} - -// LeaveAttesterSet handles MsgLeaveAttesterSet -func (k msgServer) LeaveAttesterSet(goCtx context.Context, msg *types.MsgLeaveAttesterSet) (*types.MsgLeaveAttesterSetResponse, error) { - ctx := sdk.UnwrapSDKContext(goCtx) - - if err := k.assertValidValidatorAuthority(ctx, msg.ConsensusAddress, msg.Authority); err != nil { - return nil, err + 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") } - - if err := k.AttesterInfo.Remove(ctx, msg.ConsensusAddress); err != nil { - return nil, sdkerr.Wrap(err, "remove attester info") + pk, err := info.GetPubKey() + if err != nil { + return nil, sdkerr.Wrap(err, "decode pubkey") } - // TODO (Alex): the valset should be updated at the end of an epoch only - if err := k.RemoveAttesterSetMember(ctx, msg.ConsensusAddress); err != nil { - return nil, sdkerr.Wrap(err, "remove attester set member") + 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()) } - ctx.EventManager().EmitEvent( - sdk.NewEvent( - types.TypeMsgLeaveAttesterSet, - sdk.NewAttribute("consensus_address", msg.ConsensusAddress), - sdk.NewAttribute("authority", msg.Authority), - ), - ) - - return &types.MsgLeaveAttesterSetResponse{}, nil -} + 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 -func (k msgServer) assertValidValidatorAuthority(ctx sdk.Context, consensusAddress, authority string) error { - v, err := k.AttesterInfo.Get(ctx, consensusAddress) + // Pin the signed BlockID.Hash to the sequencer's real block hash for this + // height. Without this, an attester could sign over an arbitrary BlockID + // — the signature would self-verify, but the reconstructed commit would + // fail 07-tendermint VerifyCommitLight on IBC counterparties. + 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 { - if errors.Is(err, collections.ErrNotFound) { - return sdkerr.Wrapf(sdkerrors.ErrUnauthorized, "consensus address %s not in attester set", consensusAddress) - } - return sdkerr.Wrapf(err, "attester set") + return nil, sdkerr.Wrapf(sdkerrors.ErrInvalidRequest, + "get block ID for height %d: %v", msgHeight, err) } - if v.Authority != authority { - return sdkerr.Wrapf(sdkerrors.ErrUnauthorized, "address %s", authority) + if !bytes.Equal(v.BlockID.Hash, storedID.Hash) { + return nil, sdkerr.Wrapf(sdkerrors.ErrInvalidRequest, + "vote BlockID hash %X does not match sequencer block hash %X at height %d", + v.BlockID.Hash, storedID.Hash, msgHeight) } - return nil + 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 diff --git a/modules/network/keeper/msg_server_test.go b/modules/network/keeper/msg_server_test.go index a4419612..e124a9a2 100644 --- a/modules/network/keeper/msg_server_test.go +++ b/modules/network/keeper/msg_server_test.go @@ -12,7 +12,10 @@ import ( "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,116 +23,47 @@ import ( moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" - "github.com/stretchr/testify/assert" + "github.com/cosmos/gogoproto/proto" "github.com/stretchr/testify/require" "github.com/evstack/ev-abci/modules/network/types" ) -func TestJoinAttesterSet(t *testing.T) { - myValAddr := sdk.ValAddress("validator4") +func TestJoinAttesterSetDisabled(t *testing.T) { + sk := NewMockStakingKeeper() + server, _, ctx := newTestServer(t, &sk) - type testCase struct { - setup func(t *testing.T, ctx sdk.Context, keeper *Keeper, sk *MockStakingKeeper) - msg *types.MsgJoinAttesterSet - expErr error - expSet bool - } - - tests := map[string]testCase{ - "valid": { - setup: func(t *testing.T, ctx sdk.Context, keeper *Keeper, sk *MockStakingKeeper) { - validator := stakingtypes.Validator{ - OperatorAddress: myValAddr.String(), - Status: stakingtypes.Bonded, - } - err := sk.SetValidator(ctx, validator) - require.NoError(t, err, "failed to set validator") - }, - msg: &types.MsgJoinAttesterSet{Authority: myValAddr.String(), ConsensusAddress: myValAddr.String()}, - expSet: true, - }, - "invalid_addr": { - setup: func(t *testing.T, ctx sdk.Context, keeper *Keeper, sk *MockStakingKeeper) {}, - msg: &types.MsgJoinAttesterSet{Authority: "invalidAddr", ConsensusAddress: "invalidAddr"}, - expErr: sdkerrors.ErrInvalidAddress, - }, - "already set": { - setup: func(t *testing.T, ctx sdk.Context, keeper *Keeper, sk *MockStakingKeeper) { - validator := stakingtypes.Validator{ - OperatorAddress: myValAddr.String(), - Status: stakingtypes.Bonded, - } - require.NoError(t, sk.SetValidator(ctx, validator)) - require.NoError(t, keeper.SetAttesterSetMember(ctx, myValAddr.String())) - }, - msg: &types.MsgJoinAttesterSet{Authority: myValAddr.String(), ConsensusAddress: myValAddr.String()}, - expErr: sdkerrors.ErrInvalidRequest, - expSet: true, - }, - } - - for name, spec := range tests { - t.Run(name, func(t *testing.T) { - sk := NewMockStakingKeeper() - server, keeper, ctx := newTestServer(t, &sk) - - spec.setup(t, ctx, &keeper, &sk) - - // when - rsp, err := server.JoinAttesterSet(ctx, spec.msg) - // then - if spec.expErr != nil { - require.ErrorIs(t, err, spec.expErr) - require.Nil(t, rsp) - exists, gotErr := keeper.AttesterSet.Has(ctx, spec.msg.ConsensusAddress) - require.NoError(t, gotErr) - assert.Equal(t, exists, spec.expSet) - return - } - require.NoError(t, err) - require.NotNil(t, rsp) - exists, gotErr := keeper.AttesterSet.Has(ctx, spec.msg.ConsensusAddress) - require.NoError(t, gotErr) - assert.True(t, exists) - - // Verify authority is stored correctly in AttesterInfo - info, infoErr := keeper.GetAttesterInfo(ctx, spec.msg.ConsensusAddress) - require.NoError(t, infoErr) - assert.Equal(t, spec.msg.Authority, info.Authority) - }) + msg := &types.MsgJoinAttesterSet{ + Authority: sdk.AccAddress([]byte("any-authority-20b")).String(), + ConsensusAddress: sdk.ConsAddress([]byte("any-cons-addr-20-b")).String(), } + rsp, err := server.JoinAttesterSet(ctx, msg) + require.ErrorIs(t, err, sdkerrors.ErrInvalidRequest) + require.Contains(t, err.Error(), "attester set changes disabled") + require.Nil(t, rsp) } -func TestJoinAttesterSetMaxCap(t *testing.T) { - // Verify the constant is set to a sane value that is within uint16 range - require.LessOrEqual(t, MaxAttesters, int(^uint16(0)), - "MaxAttesters must fit in uint16 to avoid index overflow in BuildValidatorIndexMap") +func TestLeaveAttesterSetDisabled(t *testing.T) { + sk := NewMockStakingKeeper() + server, _, ctx := newTestServer(t, &sk) - t.Run("join succeeds under cap", func(t *testing.T) { - sk := NewMockStakingKeeper() - server, keeper, ctx := newTestServer(t, &sk) - - // With an empty set, join should succeed - newAddr := sdk.ValAddress("new_attester") - msg := &types.MsgJoinAttesterSet{ - Authority: newAddr.String(), - ConsensusAddress: newAddr.String(), - } - - rsp, err := server.JoinAttesterSet(ctx, msg) - require.NoError(t, err) - require.NotNil(t, rsp) - - // Verify the attester was added - exists, err := keeper.AttesterSet.Has(ctx, newAddr.String()) - require.NoError(t, err) - assert.True(t, exists) - }) + msg := &types.MsgLeaveAttesterSet{ + Authority: sdk.AccAddress([]byte("any-authority-20b")).String(), + ConsensusAddress: sdk.ConsAddress([]byte("any-cons-addr-20-b")).String(), + } + rsp, err := server.LeaveAttesterSet(ctx, msg) + require.ErrorIs(t, err, sdkerrors.ErrInvalidRequest) + require.Nil(t, rsp) } func TestAttestVotePayloadValidation(t *testing.T) { - 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 @@ -143,32 +77,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.SetAttesterSetMember(ctx, myValAddr.String())) - require.NoError(t, keeper.SetAttesterInfo(ctx, myValAddr.String(), &types.AttesterInfo{Authority: myValAddr.String()})) - require.NoError(t, keeper.BuildValidatorIndexMap(ctx)) + + 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) @@ -183,142 +137,75 @@ func TestAttestVotePayloadValidation(t *testing.T) { } } -func TestLeaveAttesterSet(t *testing.T) { - ownerAddr := sdk.ValAddress("owner1") - otherAddr := sdk.ValAddress("other1") - - type testCase struct { - setup func(t *testing.T, ctx sdk.Context, keeper *Keeper, server msgServer) - msg *types.MsgLeaveAttesterSet - expErr error - } - - tests := map[string]testCase{ - "valid": { - setup: func(t *testing.T, ctx sdk.Context, keeper *Keeper, server msgServer) { - t.Helper() - joinMsg := &types.MsgJoinAttesterSet{ - Authority: ownerAddr.String(), - ConsensusAddress: ownerAddr.String(), - } - _, err := server.JoinAttesterSet(ctx, joinMsg) - require.NoError(t, err) - }, - msg: &types.MsgLeaveAttesterSet{ - Authority: ownerAddr.String(), - ConsensusAddress: ownerAddr.String(), - }, - }, - "not_in_set": { - setup: func(t *testing.T, ctx sdk.Context, keeper *Keeper, server msgServer) { - t.Helper() - }, - msg: &types.MsgLeaveAttesterSet{ - Authority: ownerAddr.String(), - ConsensusAddress: ownerAddr.String(), - }, - expErr: sdkerrors.ErrUnauthorized, - }, - "wrong_authority": { - setup: func(t *testing.T, ctx sdk.Context, keeper *Keeper, server msgServer) { - t.Helper() - joinMsg := &types.MsgJoinAttesterSet{ - Authority: ownerAddr.String(), - ConsensusAddress: ownerAddr.String(), - } - _, err := server.JoinAttesterSet(ctx, joinMsg) - require.NoError(t, err) - }, - msg: &types.MsgLeaveAttesterSet{ - Authority: otherAddr.String(), - ConsensusAddress: ownerAddr.String(), - }, - expErr: sdkerrors.ErrUnauthorized, - }, - } - - for name, spec := range tests { - t.Run(name, func(t *testing.T) { - sk := NewMockStakingKeeper() - server, keeper, ctx := newTestServer(t, &sk) - - spec.setup(t, ctx, &keeper, server) - - rsp, err := server.LeaveAttesterSet(ctx, spec.msg) - if spec.expErr != nil { - require.ErrorIs(t, err, spec.expErr) - require.Nil(t, rsp) - return - } - require.NoError(t, err) - require.NotNil(t, rsp) - - // Verify actually removed from attester set - exists, gotErr := keeper.AttesterSet.Has(ctx, spec.msg.ConsensusAddress) - require.NoError(t, gotErr) - assert.False(t, exists) - }) - } -} - func TestAttest(t *testing.T) { - ownerAddr := sdk.ValAddress("attester_owner") + 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())) - joinMsg := &types.MsgJoinAttesterSet{ - Authority: ownerAddr.String(), - ConsensusAddress: ownerAddr.String(), - } - _, err := server.JoinAttesterSet(ctx, joinMsg) + sdkPk, err := cryptocodec.FromCmtPubKeyInterface(pub) + require.NoError(t, err) + info, err := types.NewAttesterInfo(authorityAddr, sdkPk, 0) require.NoError(t, err) - require.NoError(t, keeper.BuildValidatorIndexMap(ctx)) + 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() - joinMsg := &types.MsgJoinAttesterSet{ - Authority: ownerAddr.String(), - ConsensusAddress: ownerAddr.String(), - } - _, err := server.JoinAttesterSet(ctx, joinMsg) + sdkPk, err := cryptocodec.FromCmtPubKeyInterface(pub) + require.NoError(t, err) + info, err := types.NewAttesterInfo(authorityAddr, sdkPk, 0) require.NoError(t, err) - require.NoError(t, keeper.BuildValidatorIndexMap(ctx)) + 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, }, @@ -328,9 +215,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) @@ -350,6 +237,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()) @@ -357,8 +248,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 { @@ -405,36 +294,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())) - joinMsg := &types.MsgJoinAttesterSet{ - Authority: ownerAddr.String(), - ConsensusAddress: myValAddr.String(), - } - _, err := server.JoinAttesterSet(ctx, joinMsg) + // 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.BuildValidatorIndexMap(ctx)) + 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 { @@ -491,3 +391,192 @@ 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 block hash") + + // 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) +} + +// 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") +} diff --git a/modules/network/keeper/testhelpers_test.go b/modules/network/keeper/testhelpers_test.go new file mode 100644 index 00000000..8069c10d --- /dev/null +++ b/modules/network/keeper/testhelpers_test.go @@ -0,0 +1,58 @@ +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 +} + +func (s staticBlockIDProvider) GetBlockID(_ context.Context, _ uint64) (*cmttypes.BlockID, error) { + return &cmttypes.BlockID{Hash: s.hash}, 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() + pub := priv.PubKey().(cmted25519.PubKey) + v := cmtproto.Vote{ + Type: cmtproto.PrecommitType, + Height: height, + Round: 0, + BlockID: cmtproto.BlockID{Hash: blockIDHash, PartSetHeader: cmtproto.PartSetHeader{}}, + 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/attester.go b/modules/network/types/attester.go new file mode 100644 index 00000000..ebfd544c --- /dev/null +++ b/modules/network/types/attester.go @@ -0,0 +1,40 @@ +package types + +import ( + "fmt" + + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func NewAttesterInfo(authority string, pk cryptotypes.PubKey, joinedHeight int64) (*AttesterInfo, error) { + any, err := codectypes.NewAnyWithValue(pk) + if err != nil { + return nil, fmt.Errorf("pack pubkey: %w", err) + } + return &AttesterInfo{ + Authority: authority, + Pubkey: any, + JoinedHeight: joinedHeight, + ConsensusAddress: sdk.ConsAddress(pk.Address()).String(), + }, nil +} + +// GetPubKey extracts the cryptotypes.PubKey from the Any field. +func (a AttesterInfo) GetPubKey() (cryptotypes.PubKey, error) { + if a.Pubkey == nil { + return nil, fmt.Errorf("pubkey not set") + } + pk, ok := a.Pubkey.GetCachedValue().(cryptotypes.PubKey) + if ok { + return pk, nil + } + return nil, fmt.Errorf("pubkey cached value not cryptotypes.PubKey") +} + +// UnpackInterfaces ensures GetCachedValue works after unmarshaling. +func (a AttesterInfo) UnpackInterfaces(unpacker codectypes.AnyUnpacker) error { + var pk cryptotypes.PubKey + return unpacker.UnpackAny(a.Pubkey, &pk) +} diff --git a/modules/network/types/attester.pb.go b/modules/network/types/attester.pb.go index 361f5e93..40896217 100644 --- a/modules/network/types/attester.pb.go +++ b/modules/network/types/attester.pb.go @@ -33,6 +33,10 @@ type AttesterInfo struct { Pubkey *types.Any `protobuf:"bytes,2,opt,name=pubkey,proto3" json:"pubkey,omitempty"` // joined_height is the height at which the attester joined JoinedHeight int64 `protobuf:"varint,3,opt,name=joined_height,json=joinedHeight,proto3" json:"joined_height,omitempty"` + // consensus_address is the bech32 cosmosvalcons1... derived from pubkey. + // Redundant with pubkey but persisted so the keeper's collections key + // (consensus address) matches the stored struct. + ConsensusAddress string `protobuf:"bytes,4,opt,name=consensus_address,json=consensusAddress,proto3" json:"consensus_address,omitempty"` } func (m *AttesterInfo) Reset() { *m = AttesterInfo{} } @@ -75,28 +79,30 @@ func init() { func init() { proto.RegisterFile("evabci/network/v1/attester.proto", fileDescriptor_a8fe3a2e81f284b4) } var fileDescriptor_a8fe3a2e81f284b4 = []byte{ - // 327 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x44, 0x90, 0xbb, 0x4e, 0xc3, 0x30, - 0x14, 0x86, 0x63, 0x2a, 0x55, 0x34, 0x94, 0x81, 0xa8, 0x43, 0xda, 0x21, 0x8d, 0x60, 0xe9, 0x52, - 0x9b, 0x82, 0xc4, 0xc0, 0xd6, 0x0e, 0x88, 0xcb, 0x82, 0xca, 0xc6, 0x52, 0xe5, 0x72, 0x9a, 0x84, - 0xb6, 0x39, 0x91, 0xed, 0x04, 0xf9, 0x0d, 0x18, 0x79, 0x84, 0x3e, 0x04, 0x2b, 0x3b, 0x62, 0xaa, - 0x98, 0x18, 0x51, 0xbb, 0xf0, 0x18, 0x88, 0x38, 0xd0, 0xcd, 0xe7, 0xf3, 0xe7, 0xe3, 0x5f, 0xbf, - 0xe9, 0x42, 0xe1, 0xf9, 0x41, 0xc2, 0x52, 0x90, 0x8f, 0xc8, 0x67, 0xac, 0x18, 0x30, 0x4f, 0x4a, - 0x10, 0x12, 0x38, 0xcd, 0x38, 0x4a, 0xb4, 0x0e, 0xb4, 0x41, 0x2b, 0x83, 0x16, 0x83, 0x4e, 0x3b, - 0x42, 0x8c, 0xe6, 0xc0, 0x4a, 0xc1, 0xcf, 0xa7, 0xcc, 0x4b, 0x95, 0xb6, 0x3b, 0xed, 0x00, 0xc5, - 0x02, 0xc5, 0xa4, 0x9c, 0x98, 0x1e, 0xaa, 0xab, 0x56, 0x84, 0x11, 0x6a, 0xfe, 0x7b, 0xd2, 0xf4, - 0xf0, 0x95, 0x98, 0xcd, 0x61, 0xf5, 0xe3, 0x55, 0x3a, 0x45, 0xeb, 0xcc, 0x6c, 0x78, 0xb9, 0x8c, - 0x91, 0x27, 0x52, 0xd9, 0xc4, 0x25, 0xbd, 0xc6, 0xc8, 0xfe, 0x78, 0xe9, 0xb7, 0xaa, 0x5d, 0xc3, - 0x30, 0xe4, 0x20, 0xc4, 0x9d, 0xe4, 0x49, 0x1a, 0x8d, 0xb7, 0xaa, 0x75, 0x61, 0xd6, 0xb3, 0xdc, - 0x9f, 0x81, 0xb2, 0x77, 0x5c, 0xd2, 0xdb, 0x3b, 0x69, 0x51, 0x9d, 0x92, 0xfe, 0xa5, 0xa4, 0xc3, - 0x54, 0x8d, 0xec, 0xf7, 0xed, 0xaa, 0x80, 0xab, 0x4c, 0x22, 0xbd, 0xcd, 0xfd, 0x1b, 0x50, 0xe3, - 0xea, 0xb5, 0x75, 0x64, 0xee, 0x3f, 0x60, 0x92, 0x42, 0x38, 0x89, 0x21, 0x89, 0x62, 0x69, 0xd7, - 0x5c, 0xd2, 0xab, 0x8d, 0x9b, 0x1a, 0x5e, 0x96, 0xec, 0x7c, 0xf7, 0x69, 0xd9, 0x35, 0xbe, 0x97, - 0x5d, 0x63, 0x74, 0xfd, 0xb6, 0x76, 0xc8, 0x6a, 0xed, 0x90, 0xaf, 0xb5, 0x43, 0x9e, 0x37, 0x8e, - 0xb1, 0xda, 0x38, 0xc6, 0xe7, 0xc6, 0x31, 0xee, 0x8f, 0xa3, 0x44, 0xc6, 0xb9, 0x4f, 0x03, 0x5c, - 0x30, 0x28, 0x84, 0xf4, 0x82, 0x19, 0x83, 0xa2, 0x5f, 0xd6, 0xbd, 0xc0, 0x30, 0x9f, 0x83, 0xf8, - 0xaf, 0x5d, 0xaa, 0x0c, 0x84, 0x5f, 0x2f, 0xa3, 0x9e, 0xfe, 0x04, 0x00, 0x00, 0xff, 0xff, 0xcf, - 0x03, 0xda, 0x4b, 0x95, 0x01, 0x00, 0x00, + // 355 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x84, 0x91, 0xbd, 0x4e, 0xe3, 0x40, + 0x10, 0xc7, 0xed, 0xe4, 0x14, 0x5d, 0x7c, 0x39, 0xe9, 0x62, 0xa5, 0x70, 0x52, 0x38, 0xd6, 0xd1, + 0xa4, 0xc9, 0x2e, 0x01, 0x89, 0x82, 0x2e, 0x91, 0x40, 0x7c, 0x34, 0x28, 0x74, 0x34, 0x91, 0x3f, + 0x26, 0xb6, 0x49, 0xb2, 0x63, 0x79, 0xd7, 0x46, 0xdb, 0x53, 0x50, 0xf2, 0x08, 0x79, 0x08, 0x1e, + 0x02, 0x51, 0x45, 0x54, 0x94, 0x28, 0x69, 0x78, 0x0c, 0x84, 0xd7, 0x24, 0x25, 0xdd, 0xce, 0x6f, + 0x7f, 0x33, 0xfb, 0x5f, 0x8d, 0xe1, 0x40, 0xee, 0x7a, 0x7e, 0x4c, 0x19, 0x88, 0x3b, 0x4c, 0x67, + 0x34, 0x1f, 0x50, 0x57, 0x08, 0xe0, 0x02, 0x52, 0x92, 0xa4, 0x28, 0xd0, 0x6c, 0x2a, 0x83, 0x94, + 0x06, 0xc9, 0x07, 0x9d, 0x76, 0x88, 0x18, 0xce, 0x81, 0x16, 0x82, 0x97, 0x4d, 0xa9, 0xcb, 0xa4, + 0xb2, 0x3b, 0x6d, 0x1f, 0xf9, 0x02, 0xf9, 0xa4, 0xa8, 0xa8, 0x2a, 0xca, 0xab, 0x56, 0x88, 0x21, + 0x2a, 0xfe, 0x75, 0x52, 0xf4, 0xff, 0x7d, 0xc5, 0x68, 0x0c, 0xcb, 0x17, 0xcf, 0xd9, 0x14, 0xcd, + 0x23, 0xa3, 0xee, 0x66, 0x22, 0xc2, 0x34, 0x16, 0xd2, 0xd2, 0x1d, 0xbd, 0x57, 0x1f, 0x59, 0xaf, + 0x4f, 0xfd, 0x56, 0x39, 0x6b, 0x18, 0x04, 0x29, 0x70, 0x7e, 0x2d, 0xd2, 0x98, 0x85, 0xe3, 0x9d, + 0x6a, 0x9e, 0x1a, 0xb5, 0x24, 0xf3, 0x66, 0x20, 0xad, 0x8a, 0xa3, 0xf7, 0xfe, 0x1c, 0xb4, 0x88, + 0x4a, 0x49, 0xbe, 0x53, 0x92, 0x21, 0x93, 0x23, 0xeb, 0x65, 0x37, 0xca, 0x4f, 0x65, 0x22, 0x90, + 0x5c, 0x65, 0xde, 0x25, 0xc8, 0x71, 0xd9, 0x6d, 0xee, 0x19, 0x7f, 0x6f, 0x31, 0x66, 0x10, 0x4c, + 0x22, 0x88, 0xc3, 0x48, 0x58, 0x55, 0x47, 0xef, 0x55, 0xc7, 0x0d, 0x05, 0xcf, 0x0a, 0x66, 0x9e, + 0x18, 0x4d, 0x1f, 0x19, 0x07, 0xc6, 0x33, 0x3e, 0x71, 0x55, 0x24, 0xeb, 0xd7, 0x0f, 0x61, 0xff, + 0x6d, 0x5b, 0x4a, 0x7e, 0xfc, 0xfb, 0x61, 0xd9, 0xd5, 0x3e, 0x96, 0x5d, 0x6d, 0x74, 0xf1, 0xbc, + 0xb6, 0xf5, 0xd5, 0xda, 0xd6, 0xdf, 0xd7, 0xb6, 0xfe, 0xb8, 0xb1, 0xb5, 0xd5, 0xc6, 0xd6, 0xde, + 0x36, 0xb6, 0x76, 0xb3, 0x1f, 0xc6, 0x22, 0xca, 0x3c, 0xe2, 0xe3, 0x82, 0x42, 0xce, 0x85, 0xeb, + 0xcf, 0x28, 0xe4, 0xfd, 0x62, 0x6b, 0x0b, 0x0c, 0xb2, 0x39, 0xf0, 0xed, 0xf6, 0x84, 0x4c, 0x80, + 0x7b, 0xb5, 0xe2, 0xc7, 0x87, 0x9f, 0x01, 0x00, 0x00, 0xff, 0xff, 0x29, 0xc3, 0xde, 0xe5, 0xdc, + 0x01, 0x00, 0x00, } func (m *AttesterInfo) Marshal() (dAtA []byte, err error) { @@ -119,6 +125,13 @@ func (m *AttesterInfo) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if len(m.ConsensusAddress) > 0 { + i -= len(m.ConsensusAddress) + copy(dAtA[i:], m.ConsensusAddress) + i = encodeVarintAttester(dAtA, i, uint64(len(m.ConsensusAddress))) + i-- + dAtA[i] = 0x22 + } if m.JoinedHeight != 0 { i = encodeVarintAttester(dAtA, i, uint64(m.JoinedHeight)) i-- @@ -174,6 +187,10 @@ func (m *AttesterInfo) Size() (n int) { if m.JoinedHeight != 0 { n += 1 + sovAttester(uint64(m.JoinedHeight)) } + l = len(m.ConsensusAddress) + if l > 0 { + n += 1 + l + sovAttester(uint64(l)) + } return n } @@ -299,6 +316,38 @@ func (m *AttesterInfo) Unmarshal(dAtA []byte) error { break } } + case 4: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ConsensusAddress", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowAttester + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthAttester + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthAttester + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.ConsensusAddress = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipAttester(dAtA[iNdEx:]) diff --git a/modules/network/types/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/modules/network/types/genesis.go b/modules/network/types/genesis.go index c722e021..3a071e68 100644 --- a/modules/network/types/genesis.go +++ b/modules/network/types/genesis.go @@ -3,8 +3,22 @@ package types import ( "fmt" "math" + + codectypes "github.com/cosmos/cosmos-sdk/codec/types" ) +// UnpackInterfaces ensures AttesterInfo.Pubkey Any values have their cached +// concrete value populated after genesis JSON unmarshaling. Without this, the +// cosmos-sdk codec leaves the Any unresolved and GetPubKey() returns an error. +func (gs GenesisState) UnpackInterfaces(unpacker codectypes.AnyUnpacker) error { + for i := range gs.AttesterInfos { + if err := gs.AttesterInfos[i].UnpackInterfaces(unpacker); err != nil { + return fmt.Errorf("unpack attester %d: %w", i, err) + } + } + return nil +} + // DefaultGenesisState returns the default genesis state func DefaultGenesisState() *GenesisState { return &GenesisState{ diff --git a/modules/network/types/genesis.pb.go b/modules/network/types/genesis.pb.go index 539c86ad..020b5c99 100644 --- a/modules/network/types/genesis.pb.go +++ b/modules/network/types/genesis.pb.go @@ -31,6 +31,9 @@ type GenesisState struct { ValidatorIndices []ValidatorIndex `protobuf:"bytes,2,rep,name=validator_indices,json=validatorIndices,proto3" json:"validator_indices"` // attestation_bitmaps contains historical attestation data AttestationBitmaps []AttestationBitmap `protobuf:"bytes,3,rep,name=attestation_bitmaps,json=attestationBitmaps,proto3" json:"attestation_bitmaps"` + // attester_infos is the fixed attester set loaded at genesis. After chain + // start, the set is immutable (MsgJoin/MsgLeave are disabled). + AttesterInfos []AttesterInfo `protobuf:"bytes,4,rep,name=attester_infos,json=attesterInfos,proto3" json:"attester_infos"` } func (m *GenesisState) Reset() { *m = GenesisState{} } @@ -87,6 +90,13 @@ func (m *GenesisState) GetAttestationBitmaps() []AttestationBitmap { return nil } +func (m *GenesisState) GetAttesterInfos() []AttesterInfo { + if m != nil { + return m.AttesterInfos + } + return nil +} + func init() { proto.RegisterType((*GenesisState)(nil), "evabci.network.v1.GenesisState") } @@ -94,26 +104,28 @@ func init() { func init() { proto.RegisterFile("evabci/network/v1/genesis.proto", fileDescriptor_58e10cce12d8f51a) } var fileDescriptor_58e10cce12d8f51a = []byte{ - // 298 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0xd0, 0xbf, 0x4e, 0xeb, 0x30, - 0x14, 0xc7, 0xf1, 0xf8, 0xf6, 0xaa, 0x43, 0xca, 0x40, 0x03, 0x43, 0xa9, 0x84, 0x5b, 0x10, 0x43, - 0x17, 0x6c, 0x5a, 0x06, 0x66, 0xb2, 0x20, 0x98, 0x10, 0x20, 0x06, 0x18, 0x2a, 0x27, 0x39, 0x0a, - 0x56, 0x9b, 0x38, 0x8a, 0x4f, 0x0d, 0xbc, 0x05, 0x8f, 0xd5, 0xb1, 0x23, 0x13, 0x42, 0xc9, 0x5b, - 0x30, 0xa1, 0x3a, 0xe1, 0x8f, 0x68, 0xb7, 0x28, 0xe7, 0xab, 0x8f, 0xa5, 0x9f, 0xdb, 0x03, 0x23, - 0x82, 0x50, 0xf2, 0x14, 0xf0, 0x51, 0xe5, 0x13, 0x6e, 0x86, 0x3c, 0x86, 0x14, 0xb4, 0xd4, 0x2c, - 0xcb, 0x15, 0x2a, 0xaf, 0x5d, 0x05, 0xac, 0x0e, 0x98, 0x19, 0x76, 0xb7, 0x63, 0x15, 0x2b, 0x7b, - 0xe5, 0xcb, 0xaf, 0x2a, 0xec, 0xee, 0xae, 0x4a, 0xf8, 0x9c, 0x41, 0xed, 0xec, 0x7f, 0x10, 0x77, - 0xe3, 0xac, 0x92, 0xaf, 0x51, 0x20, 0x78, 0x27, 0x6e, 0x33, 0x13, 0xb9, 0x48, 0x74, 0x87, 0xf4, - 0xc9, 0xa0, 0x35, 0xda, 0x61, 0x2b, 0x2f, 0xb1, 0x4b, 0x1b, 0xf8, 0xff, 0xe7, 0x6f, 0x3d, 0xe7, - 0xaa, 0xce, 0xbd, 0x1b, 0xb7, 0x6d, 0xc4, 0x54, 0x46, 0x02, 0x55, 0x3e, 0x96, 0x69, 0x24, 0x43, - 0xd0, 0x9d, 0x7f, 0xfd, 0xc6, 0xa0, 0x35, 0xda, 0x5b, 0x63, 0xdc, 0x7e, 0xb5, 0xe7, 0x69, 0x04, - 0x4f, 0xb5, 0xb5, 0x69, 0x7e, 0xfd, 0x5d, 0x02, 0xde, 0xbd, 0xbb, 0x25, 0x10, 0x41, 0xa3, 0x40, - 0xa9, 0xd2, 0x71, 0x20, 0x31, 0x11, 0x99, 0xee, 0x34, 0xac, 0x7b, 0xb0, 0xc6, 0x3d, 0xfd, 0xa9, - 0x7d, 0x1b, 0xd7, 0xb4, 0x27, 0xfe, 0x1e, 0xb4, 0x7f, 0x31, 0x2f, 0x28, 0x59, 0x14, 0x94, 0xbc, - 0x17, 0x94, 0xbc, 0x94, 0xd4, 0x59, 0x94, 0xd4, 0x79, 0x2d, 0xa9, 0x73, 0x77, 0x14, 0x4b, 0x7c, - 0x98, 0x05, 0x2c, 0x54, 0x09, 0x07, 0xa3, 0x51, 0x84, 0x13, 0x0e, 0xe6, 0xd0, 0x2e, 0x99, 0xa8, - 0x68, 0x36, 0x05, 0xfd, 0xbd, 0xa8, 0x9d, 0x33, 0x68, 0xda, 0x3d, 0x8f, 0x3f, 0x03, 0x00, 0x00, - 0xff, 0xff, 0x66, 0xac, 0x9a, 0x06, 0xba, 0x01, 0x00, 0x00, + // 335 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x74, 0x91, 0xb1, 0x4f, 0xfa, 0x40, + 0x14, 0xc7, 0x5b, 0x20, 0x0c, 0xe5, 0xf7, 0x33, 0x52, 0x1d, 0x2a, 0x89, 0x05, 0x8d, 0x03, 0x8b, + 0xad, 0xe0, 0xe0, 0x2c, 0x8b, 0xc1, 0x38, 0x18, 0x35, 0x0e, 0x3a, 0x90, 0x6b, 0xfb, 0xa8, 0x17, + 0xe8, 0x5d, 0xd3, 0x7b, 0x9c, 0xfa, 0x5f, 0xf8, 0x67, 0x31, 0x32, 0x3a, 0x19, 0x03, 0xff, 0x88, + 0xe1, 0x7a, 0x28, 0xb1, 0xb8, 0x5d, 0xde, 0xf7, 0x93, 0xcf, 0xbd, 0xbc, 0xaf, 0xd5, 0x04, 0x49, + 0x82, 0x90, 0xfa, 0x0c, 0xf0, 0x99, 0x67, 0x23, 0x5f, 0x76, 0xfc, 0x18, 0x18, 0x08, 0x2a, 0xbc, + 0x34, 0xe3, 0xc8, 0xed, 0x7a, 0x0e, 0x78, 0x1a, 0xf0, 0x64, 0xa7, 0xb1, 0x1b, 0xf3, 0x98, 0xab, + 0xd4, 0x5f, 0xbe, 0x72, 0xb0, 0xb1, 0x5f, 0x34, 0xe1, 0x6b, 0x0a, 0xda, 0xd3, 0x68, 0x15, 0x63, + 0x82, 0x08, 0x02, 0x21, 0xcb, 0x89, 0xc3, 0x69, 0xc9, 0xfa, 0x77, 0x91, 0xff, 0x7d, 0x8b, 0x04, + 0xc1, 0x3e, 0xb3, 0xaa, 0x29, 0xc9, 0x48, 0x22, 0x1c, 0xb3, 0x65, 0xb6, 0x6b, 0xdd, 0x3d, 0xaf, + 0xb0, 0x8b, 0x77, 0xad, 0x80, 0x5e, 0x65, 0xfa, 0xd1, 0x34, 0x6e, 0x34, 0x6e, 0xdf, 0x59, 0x75, + 0x49, 0xc6, 0x34, 0x22, 0xc8, 0xb3, 0x01, 0x65, 0x11, 0x0d, 0x41, 0x38, 0xa5, 0x56, 0xb9, 0x5d, + 0xeb, 0x1e, 0x6c, 0x70, 0xdc, 0xaf, 0xd8, 0x3e, 0x8b, 0xe0, 0x45, 0xbb, 0xb6, 0xe5, 0xda, 0x74, + 0x29, 0xb0, 0x1f, 0xad, 0x9d, 0x7c, 0x63, 0x82, 0x94, 0xb3, 0x41, 0x40, 0x31, 0x21, 0xa9, 0x70, + 0xca, 0xca, 0x7b, 0xb4, 0xc1, 0x7b, 0xfe, 0x43, 0xf7, 0x14, 0xac, 0xd5, 0x36, 0xf9, 0x1d, 0x08, + 0xfb, 0xca, 0xda, 0x5a, 0x9d, 0x63, 0x40, 0xd9, 0x90, 0x0b, 0xa7, 0xa2, 0xbc, 0xcd, 0x3f, 0xbd, + 0x90, 0xf5, 0xd9, 0x90, 0x6b, 0xe5, 0x7f, 0xb2, 0x36, 0x13, 0xbd, 0xcb, 0xe9, 0xdc, 0x35, 0x67, + 0x73, 0xd7, 0xfc, 0x9c, 0xbb, 0xe6, 0xdb, 0xc2, 0x35, 0x66, 0x0b, 0xd7, 0x78, 0x5f, 0xb8, 0xc6, + 0xc3, 0x49, 0x4c, 0xf1, 0x69, 0x12, 0x78, 0x21, 0x4f, 0x7c, 0x90, 0x02, 0x49, 0x38, 0xf2, 0x41, + 0x1e, 0xab, 0x6a, 0x12, 0x1e, 0x4d, 0xc6, 0x20, 0xbe, 0x2b, 0x52, 0xf5, 0x05, 0x55, 0xd5, 0xce, + 0xe9, 0x57, 0x00, 0x00, 0x00, 0xff, 0xff, 0x34, 0x07, 0x1b, 0x4a, 0x2a, 0x02, 0x00, 0x00, } func (m *GenesisState) Marshal() (dAtA []byte, err error) { @@ -136,6 +148,20 @@ func (m *GenesisState) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if len(m.AttesterInfos) > 0 { + for iNdEx := len(m.AttesterInfos) - 1; iNdEx >= 0; iNdEx-- { + { + size, err := m.AttesterInfos[iNdEx].MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenesis(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x22 + } + } if len(m.AttestationBitmaps) > 0 { for iNdEx := len(m.AttestationBitmaps) - 1; iNdEx >= 0; iNdEx-- { { @@ -208,6 +234,12 @@ func (m *GenesisState) Size() (n int) { n += 1 + l + sovGenesis(uint64(l)) } } + if len(m.AttesterInfos) > 0 { + for _, e := range m.AttesterInfos { + l = e.Size() + n += 1 + l + sovGenesis(uint64(l)) + } + } return n } @@ -347,6 +379,40 @@ func (m *GenesisState) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex + case 4: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field AttesterInfos", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.AttesterInfos = append(m.AttesterInfos, AttesterInfo{}) + if err := m.AttesterInfos[len(m.AttesterInfos)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipGenesis(dAtA[iNdEx:]) diff --git a/modules/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/attester.proto b/modules/proto/evabci/network/v1/attester.proto index e54e3a5d..e7d4f9d7 100644 --- a/modules/proto/evabci/network/v1/attester.proto +++ b/modules/proto/evabci/network/v1/attester.proto @@ -21,4 +21,9 @@ message AttesterInfo { // joined_height is the height at which the attester joined int64 joined_height = 3; + + // consensus_address is the bech32 cosmosvalcons1... derived from pubkey. + // Redundant with pubkey but persisted so the keeper's collections key + // (consensus address) matches the stored struct. + string consensus_address = 4 [(cosmos_proto.scalar) = "cosmos.AddressString"]; } \ No newline at end of file diff --git a/modules/proto/evabci/network/v1/genesis.proto b/modules/proto/evabci/network/v1/genesis.proto index 473c514a..8098c25b 100644 --- a/modules/proto/evabci/network/v1/genesis.proto +++ b/modules/proto/evabci/network/v1/genesis.proto @@ -6,6 +6,7 @@ option go_package = "github.com/evstack/ev-abci/modules/network/types"; import "gogoproto/gogo.proto"; import "evabci/network/v1/types.proto"; +import "evabci/network/v1/attester.proto"; // GenesisState defines the network module's genesis state. message GenesisState { @@ -17,4 +18,8 @@ message GenesisState { // attestation_bitmaps contains historical attestation data repeated AttestationBitmap attestation_bitmaps = 3 [(gogoproto.nullable) = false]; + + // attester_infos is the fixed attester set loaded at genesis. After chain + // start, the set is immutable (MsgJoin/MsgLeave are disabled). + repeated AttesterInfo attester_infos = 4 [(gogoproto.nullable) = false]; } 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/server/attester_cmd.go b/server/attester_cmd.go index 7fcfc227..8f150d1e 100644 --- a/server/attester_cmd.go +++ b/server/attester_cmd.go @@ -1,12 +1,10 @@ package server import ( - "bytes" "context" "encoding/hex" "encoding/json" "fmt" - "io" "net/http" "net/url" "os" @@ -23,7 +21,6 @@ import ( cmttypes "github.com/cometbft/cometbft/types" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" - cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" "github.com/cosmos/cosmos-sdk/crypto/hd" "github.com/cosmos/cosmos-sdk/crypto/keyring" "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" @@ -129,11 +126,6 @@ func NewAttesterCmd() *cobra.Command { cancel() }() - cmd.Println("Joining attester set...") - if err := joinAttesterSet(ctx, config, valAddr, operatorPrivKey, consensusPrivKey, clientCtx); err != nil { - return fmt.Errorf("join attester set: %w", err) - } - cmd.Println("Starting to watch for new blocks...") if err := pullBlocksAndAttest(ctx, config, valAddr, operatorPrivKey, consensusPrivKey, clientCtx); err != nil { return fmt.Errorf("error watching blocks: %w", err) @@ -152,277 +144,60 @@ func NewAttesterCmd() *cobra.Command { return cmd } -func joinAttesterSet( +func assertRegistered( ctx context.Context, - config *AttesterConfig, - valAddr sdk.ValAddress, - operatorPrivKey *secp256k1.PrivKey, consensusPrivKey *pvm.FilePV, clientCtx client.Context, ) error { - sdkPubKey, err := cryptocodec.FromCmtPubKeyInterface(consensusPrivKey.Key.PubKey) - if err != nil { - return fmt.Errorf("convert public key: %w", err) - } - - authorityAddr, err := clientCtx.InterfaceRegistry.SigningContext().AddressCodec().BytesToString(operatorPrivKey.PubKey().Address()) - if err != nil { - return fmt.Errorf("convert authority address: %w", err) - } - - msg, err := networktypes.NewMsgJoinAttesterSet(authorityAddr, valAddr.String(), sdkPubKey) - if err != nil { - return fmt.Errorf("create join attester set msg: %w", err) - } - - txHash, err := broadcastTx(ctx, config, msg, operatorPrivKey, clientCtx) - if err != nil { - return fmt.Errorf("broadcast join attester set tx: %w", err) - } - - if config.Verbose { - fmt.Printf("📝 Transaction submitted with hash: %s\n", txHash) - } - - time.Sleep(500 * time.Millisecond) - - var txResult *sdk.TxResponse - var retries = 10 - for range retries { - txResult, err = authtx.QueryTx(clientCtx, txHash) - if err == nil { - break - } - time.Sleep(500 * time.Millisecond) - } - + consAddr := sdk.ConsAddress(consensusPrivKey.Key.PubKey.Address()).String() + queryClient := networktypes.NewQueryClient(clientCtx) + resp, err := queryClient.AttesterSet(ctx, &networktypes.QueryAttesterSetRequest{}) if err != nil { - return fmt.Errorf("transaction %s not found after %d attempts: %w", txHash, retries, err) + return fmt.Errorf("query attester set: %w", err) } - - if config.Verbose { - fmt.Printf("📊 Transaction Result: Code=%d, Height=%d\n", txResult.Code, txResult.Height) - } - - if txResult.Code != 0 { - fmt.Printf("❌ MsgJoinAttesterSet FAILED with code %d\n", txResult.Code) - fmt.Printf(" Error details: %s\n", txResult.RawLog) - - if txResult.Code == 18 && strings.Contains(txResult.RawLog, "validator already in attester set") { - fmt.Printf("ℹ️ Already in attester set, proceeding...\n") + for _, e := range resp.Entries { + if e.ConsensusAddress == consAddr { return nil } - - switch txResult.Code { - case 4: - fmt.Println(" Error: Unauthorized - The address may not be a valid validator") - case 5: - fmt.Println(" Error: Insufficient funds") - case 11: - fmt.Println(" Error: Out of gas") - case 18: - fmt.Println(" Error: Invalid request") - default: - fmt.Printf(" Error code %d\n", txResult.Code) - } - - return fmt.Errorf("MsgJoinAttesterSet failed with code %d: %s", txResult.Code, txResult.RawLog) } - - fmt.Printf("✅ Successfully joined attester set\n") - time.Sleep(500 * time.Millisecond) - - return nil + return fmt.Errorf("consensus address %s is not in the attester set; must be registered in genesis", consAddr) } func pullBlocksAndAttest( ctx context.Context, config *AttesterConfig, valAddr sdk.ValAddress, - senderKey *secp256k1.PrivKey, - pv *pvm.FilePV, + operatorPrivKey *secp256k1.PrivKey, + consensusPrivKey *pvm.FilePV, clientCtx client.Context, ) error { - parsed, err := url.Parse(config.Node) - if err != nil { - return fmt.Errorf("parse node URL: %w", err) - } - - httpClient := &http.Client{ - Timeout: 10 * time.Second, - } - - resp, err := httpClient.Get(fmt.Sprintf("http://%s/status", parsed.Host)) - if err != nil { - return fmt.Errorf("error querying status: %v", err) - } - - var statusResponse struct { - Result struct { - SyncInfo struct { - LatestBlockHeight string `json:"latest_block_height"` - } `json:"sync_info"` - } `json:"result"` - } - - if err := json.NewDecoder(resp.Body).Decode(&statusResponse); err != nil { - _ = resp.Body.Close() - return fmt.Errorf("error parsing status response: %v", err) + if err := assertRegistered(ctx, consensusPrivKey, clientCtx); err != nil { + return err } - _ = resp.Body.Close() - - currentHeight, err := strconv.ParseInt(statusResponse.Result.SyncInfo.LatestBlockHeight, 10, 64) - if err != nil { - return fmt.Errorf("error parsing current height: %v", err) - } - - fmt.Printf("📊 Current blockchain height: %d\n", currentHeight) - fmt.Printf("📝 Attesting blocks 1-%d...\n", currentHeight) - failedBlocks := make(map[int64]int) - maxRetries := 3 + var nextHeight int64 = 1 + ticker := time.NewTicker(500 * time.Millisecond) + defer ticker.Stop() - for height := int64(1); height <= currentHeight; height++ { + for { select { case <-ctx.Done(): - return nil - default: - if height%10 == 0 || height == 1 || height == currentHeight { - fmt.Printf("📦 Attesting blocks... %d/%d\n", height, currentHeight) - } - - err = submitAttestation(ctx, config, height, valAddr, senderKey, pv, clientCtx) - if err != nil { - fmt.Printf("⚠️ Error attesting block %d: %v\n", height, err) - failedBlocks[height] = 1 - continue - } - - time.Sleep(time.Millisecond * 100) + return ctx.Err() + case <-ticker.C: } - } - - if len(failedBlocks) > 0 { - fmt.Printf("\n🔄 Retrying %d failed blocks...\n", len(failedBlocks)) - for retryRound := 1; retryRound <= maxRetries && len(failedBlocks) > 0; retryRound++ { - fmt.Printf(" Round %d/%d - %d blocks remaining\n", retryRound, maxRetries, len(failedBlocks)) - blocksToRetry := make([]int64, 0, len(failedBlocks)) - for height := range failedBlocks { - blocksToRetry = append(blocksToRetry, height) - } - - for i := 0; i < len(blocksToRetry); i++ { - for j := i + 1; j < len(blocksToRetry); j++ { - if blocksToRetry[i] > blocksToRetry[j] { - blocksToRetry[i], blocksToRetry[j] = blocksToRetry[j], blocksToRetry[i] - } - } - } - - for _, height := range blocksToRetry { - select { - case <-ctx.Done(): - return nil - default: - fmt.Printf(" 🔄 Retrying block %d (attempt %d)...\n", height, failedBlocks[height]+1) - err = submitAttestation(ctx, config, height, valAddr, senderKey, pv, clientCtx) - if err != nil { - failedBlocks[height]++ - if failedBlocks[height] >= maxRetries { - fmt.Printf(" ❌ Block %d failed after %d attempts\n", height, maxRetries) - delete(failedBlocks, height) - } - } else { - fmt.Printf(" ✅ Block %d attested successfully\n", height) - delete(failedBlocks, height) - } - - time.Sleep(time.Millisecond * 300) - } - } - - if len(failedBlocks) > 0 { - time.Sleep(time.Second * 2) - } + currentHeight, err := getLatestHeight(config.Node) + if err != nil { + fmt.Printf("⚠️ status poll failed: %v\n", err) + continue } - - if len(failedBlocks) > 0 { - fmt.Printf("\n❌ Failed to attest %d blocks after all retries\n", len(failedBlocks)) - for height := range failedBlocks { - fmt.Printf(" - Block %d\n", height) + for h := nextHeight; h <= currentHeight; h++ { + if err := submitAttestation(ctx, config, h, valAddr, operatorPrivKey, consensusPrivKey, clientCtx); err != nil { + // duplicate or transient — log and move on + fmt.Printf("attest h=%d: %v\n", h, err) } } - } - - fmt.Printf("✅ Finished historical blocks. Watching for new blocks...\n") - - lastAttested := currentHeight - - for { - select { - case <-ctx.Done(): - return nil - default: - resp, err := httpClient.Get(fmt.Sprintf("http://%s/block", parsed.Host)) - if err != nil { - fmt.Printf("Error querying block: %v\n", err) - time.Sleep(time.Second / 10) - continue - } - - var blockResponse struct { - Result struct { - Block struct { - Header struct { - Height string `json:"height"` - AppHash string `json:"app_hash"` - } `json:"header"` - } `json:"block"` - } `json:"result"` - } - - var buf bytes.Buffer - if err := json.NewDecoder(io.TeeReader(resp.Body, &buf)).Decode(&blockResponse); err != nil { - fmt.Printf("Error parsing response: %v: %s\n", err, buf.String()) - _ = resp.Body.Close() - time.Sleep(time.Second / 10) - continue - } - _ = resp.Body.Close() - - heightStr := blockResponse.Result.Block.Header.Height - if heightStr == "" { - if config.Verbose { - fmt.Println("Height field is empty in response, retrying...") - } - time.Sleep(time.Second / 10) - continue - } - height, err := strconv.ParseInt(heightStr, 10, 64) - if err != nil { - fmt.Printf("Error parsing height: %v\n", err) - time.Sleep(time.Second / 10) - continue - } - - if height > lastAttested { - for missedHeight := lastAttested + 1; missedHeight <= height; missedHeight++ { - fmt.Printf("📦 New block %d - attesting...\n", missedHeight) - - err = submitAttestation(ctx, config, missedHeight, valAddr, senderKey, pv, clientCtx) - if err != nil { - fmt.Printf("⚠️ Error submitting attestation for block %d: %v\n", missedHeight, err) - continue - } - fmt.Printf("✅ Attested block %d\n", missedHeight) - } - - lastAttested = height - } - - time.Sleep(50 * time.Millisecond) - } + nextHeight = currentHeight + 1 } } @@ -652,7 +427,6 @@ func submitAttestation( if err != nil { return fmt.Errorf("getting Evolve header: %w", err) } - blockID, err := getOriginalBlockID(ctx, config.Node, height) if err != nil { return fmt.Errorf("getting original block ID: %w", err) @@ -661,60 +435,78 @@ func submitAttestation( vote := cmtproto.Vote{ Type: cmtproto.PrecommitType, Height: height, - BlockID: blockID, Round: 0, + BlockID: blockID, Timestamp: header.Time(), - ValidatorAddress: pv.Key.PrivKey.PubKey().Address(), + ValidatorAddress: pv.Key.PubKey.Address(), ValidatorIndex: 0, } - signBytes := cmttypes.VoteSignBytes(config.ChainID, &vote) - - signature, err := pv.Key.PrivKey.Sign(signBytes) + sig, err := pv.Key.PrivKey.Sign(signBytes) if err != nil { - return fmt.Errorf("signing payload: %w", err) - } - - validatorAddr := pv.Key.Address - - fmt.Printf("🔍 DEBUG ValidatorAddr used in vote: %X\n", validatorAddr) - fmt.Printf("🔍 DEBUG pv.GetAddress(): %X\n", pv.GetAddress()) - fmt.Printf("🔍 DEBUG pubKey.Address(): %X\n", pv.Key.PubKey.Address()) - - attesterVote := &cmtproto.Vote{ - Type: cmtproto.PrecommitType, - ValidatorAddress: validatorAddr, - Height: height, - Round: 0, - BlockID: cmtproto.BlockID{Hash: header.Hash(), PartSetHeader: cmtproto.PartSetHeader{}}, - Timestamp: header.Time(), - Signature: signature, + return fmt.Errorf("sign vote: %w", err) } - - voteBytes, err := proto.Marshal(attesterVote) + vote.Signature = sig + voteBytes, err := proto.Marshal(&vote) if err != nil { return fmt.Errorf("marshal vote: %w", err) } authorityAddr := sdk.AccAddress(senderKey.PubKey().Address()).String() - msg := networktypes.NewMsgAttest( - authorityAddr, - valAddr.String(), - height, - voteBytes, - ) + consensusAddr := sdk.ConsAddress(pv.Key.PubKey.Address()).String() + msg := networktypes.NewMsgAttest(authorityAddr, consensusAddr, height, voteBytes) txHash, err := broadcastTx(ctx, config, msg, senderKey, clientCtx) if err != nil { return fmt.Errorf("broadcast attest tx: %w", err) } - if config.Verbose { fmt.Printf("Attestation submitted for block %d with hash: %s\n", height, txHash) } return nil } +// getLatestHeight returns the latest raw block height the sequencer has +// produced. It cannot use /status in attester mode because /status reports +// the last-attested height there (which is 0 before any attestation is made, +// causing a deadlock: attester waits for blocks to attest, but /status can't +// advance until attestations land). Instead, it hits /block with no height, +// which the RPC resolves to RollkitStore.Height — the real production height. +func getLatestHeight(nodeURL string) (int64, error) { + parsed, err := url.Parse(nodeURL) + if err != nil { + return 0, fmt.Errorf("parse node URL: %w", err) + } + httpClient := &http.Client{Timeout: 10 * time.Second} + resp, err := httpClient.Get(fmt.Sprintf("http://%s/block", parsed.Host)) + if err != nil { + return 0, fmt.Errorf("query block: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + var blockResp struct { + Result struct { + Block struct { + Header struct { + Height string `json:"height"` + } `json:"header"` + } `json:"block"` + } `json:"result"` + } + if err := json.NewDecoder(resp.Body).Decode(&blockResp); err != nil { + return 0, fmt.Errorf("decode block: %w", err) + } + heightStr := blockResp.Result.Block.Header.Height + if heightStr == "" { + return 0, nil + } + h, err := strconv.ParseInt(heightStr, 10, 64) + if err != nil { + return 0, fmt.Errorf("parse height %q: %w", heightStr, err) + } + return h, nil +} + func getEvolveHeader(node string, height int64) (*evolvetypes.Header, error) { parsed, err := url.Parse(node) if err != nil { 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) diff --git a/tests/integration/docker/Dockerfile.gm b/tests/integration/docker/Dockerfile.gm index 88cfc173..13c023a5 100644 --- a/tests/integration/docker/Dockerfile.gm +++ b/tests/integration/docker/Dockerfile.gm @@ -34,6 +34,7 @@ RUN chmod +x /workspace/patch-app-wiring.sh && \ # Align module versions like in CI RUN go mod edit -replace github.com/evstack/ev-node=github.com/evstack/ev-node@${EVNODE_VERSION} \ && go mod edit -replace github.com/evstack/ev-abci=../ev-abci \ + && go mod edit -replace github.com/bytedance/sonic=github.com/bytedance/sonic@v1.15.0 \ && go mod tidy # Build gmd binary diff --git a/tests/integration/docker/patches/app-wiring/patch-app-wiring.sh b/tests/integration/docker/patches/app-wiring/patch-app-wiring.sh index 3235d06f..1f391e80 100755 --- a/tests/integration/docker/patches/app-wiring/patch-app-wiring.sh +++ b/tests/integration/docker/patches/app-wiring/patch-app-wiring.sh @@ -189,6 +189,18 @@ func (app *App) GetNetworkKeeper() networkkeeper.Keeper { EOF fi +# Add BlockID provider setter (required by ev-abci server wiring) +if ! grep -q "SetNetworkKeeperBlockIDProvider" "$APP_GO"; then + echo "[patch-app-wiring] Adding SetNetworkKeeperBlockIDProvider method" + add_import "$APP_GO" $'\tnetworktypes "github.com/evstack/ev-abci/modules/network/types"' + cat >>"$APP_GO" <<'EOF' + +func (app *App) SetNetworkKeeperBlockIDProvider(p networktypes.BlockIDProvider) { + app.NetworkKeeper.SetBlockIDProvider(p) +} +EOF +fi + echo "[patch-app-wiring] Step 5: Final validation" # Validate critical components diff --git a/tests/integration/gm_gaia_health_test.go b/tests/integration/gm_gaia_health_test.go index 243ab1e5..7054c4a4 100644 --- a/tests/integration/gm_gaia_health_test.go +++ b/tests/integration/gm_gaia_health_test.go @@ -2,8 +2,10 @@ package integration_test import ( "context" + "encoding/base64" "encoding/json" "fmt" + "strings" "testing" "time" @@ -16,6 +18,8 @@ import ( "github.com/celestiaorg/tastora/framework/testutil/sdkacc" "github.com/celestiaorg/tastora/framework/testutil/wait" "github.com/celestiaorg/tastora/framework/types" + cmted25519 "github.com/cometbft/cometbft/crypto/ed25519" + cmttypes "github.com/cometbft/cometbft/types" "github.com/cosmos/cosmos-sdk/crypto/keyring" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module/testutil" @@ -94,6 +98,53 @@ func (s *DockerIntegrationTestSuite) TestAttesterSystem() { require.NoError(s.T(), err) s.T().Log("Attester node started successfully") + // Wait for the attester to attest some blocks and LastAttestedHeight to advance. + s.T().Log("Waiting for attestations to reach quorum...") + var targetHeight int64 = 10 + err = wait.ForCondition(ctx, 2*time.Minute, 2*time.Second, func() (bool, error) { + node := gmChain.GetNodes()[0] + rpcClient, _ := node.GetRPCClient() + if rpcClient == nil { + return false, nil + } + status, statusErr := rpcClient.Status(ctx) + if statusErr != nil { + return false, nil + } + return status.SyncInfo.LatestBlockHeight >= targetHeight, nil + }) + s.Require().NoError(err, "chain did not reach target height %d", targetHeight) + + // Fetch /commit for the target height and assert VerifyCommitLight passes. + { + node := gmChain.GetNodes()[0] + rpcClient, err := node.GetRPCClient() + s.Require().NoError(err) + commitResp, err := rpcClient.Commit(ctx, &targetHeight) + s.Require().NoError(err, "fetch commit at height %d", targetHeight) + + privValJSONBz, err := node.ReadFile(ctx, "config/priv_validator_key.json") + s.Require().NoError(err) + var pv struct { + PubKey struct { + Type string `json:"type"` + Value string `json:"value"` + } `json:"pub_key"` + } + s.Require().NoError(json.Unmarshal(privValJSONBz, &pv)) + pkBytes, err := base64.StdEncoding.DecodeString(pv.PubKey.Value) + s.Require().NoError(err) + cmtPub := cmted25519.PubKey(pkBytes) + valSet := cmttypes.NewValidatorSet([]*cmttypes.Validator{cmttypes.NewValidator(cmtPub, 1)}) + + commit := commitResp.SignedHeader.Commit + s.Require().NoError( + valSet.VerifyCommitLight("gm", commit.BlockID, targetHeight, commit), + "reconstructed commit must pass 07-tendermint light-client verification", + ) + s.T().Logf("commit at height %d passes VerifyCommitLight with %d signatures", targetHeight, len(commit.Signatures)) + } + hermes, err := relayer.NewHermes(ctx, s.dockerClient, s.T().Name(), s.networkID, 0, s.logger) require.NoError(s.T(), err, "failed to create hermes relayer") @@ -256,7 +307,7 @@ func (s *DockerIntegrationTestSuite) getGmChain(ctx context.Context) *cosmos.Cha "--log_level", "*:info", ). WithNode(cosmos.NewChainNodeConfigBuilder(). - WithPostInit(AddSingleSequencer, writePasshraseFile("12345678")). + WithPostInit(AddSingleSequencer, AddGenesisAttester, writePasshraseFile("12345678")). Build()). Build(ctx) require.NoError(s.T(), err) @@ -305,6 +356,69 @@ func AddSingleSequencer(ctx context.Context, node *cosmos.ChainNode) error { return node.WriteFile(ctx, "config/genesis.json", updatedGenesis) } +// AddGenesisAttester populates app_state.network.attester_infos with a single +// attester entry derived from the node's priv_validator_key.json and the +// operator address of the "validator" keyring entry. +func AddGenesisAttester(ctx context.Context, node *cosmos.ChainNode) error { + genesisBz, err := node.ReadFile(ctx, "config/genesis.json") + if err != nil { + return fmt.Errorf("read genesis: %w", err) + } + + pubKey, err := getPubKey(ctx, node) + if err != nil { + return fmt.Errorf("get consensus pubkey: %w", err) + } + + // Consensus address (cosmosvalcons1... derived from ed25519 Address()) + consensusAddress := sdk.ConsAddress(pubKey.Address()).String() + + // Operator address: run `gmd keys show validator -a` inside the node container. + stdout, stderr, err := node.Exec(ctx, []string{ + node.BinaryName, + "keys", "show", "validator", "-a", + "--keyring-backend", "test", + "--home", node.HomeDir(), + }, nil) + if err != nil { + return fmt.Errorf("query validator operator address (stderr=%q): %w", string(stderr), err) + } + authority := strings.TrimSpace(string(stdout)) + if authority == "" { + return fmt.Errorf("empty operator address for validator keyring entry") + } + + attesterInfo := map[string]interface{}{ + "authority": authority, + "pubkey": map[string]interface{}{ + "@type": "/cosmos.crypto.ed25519.PubKey", + "key": base64.StdEncoding.EncodeToString(pubKey.Bytes()), + }, + "joined_height": 0, + "consensus_address": consensusAddress, + } + + var genDoc map[string]interface{} + if err := json.Unmarshal(genesisBz, &genDoc); err != nil { + return fmt.Errorf("parse genesis: %w", err) + } + appState, ok := genDoc["app_state"].(map[string]interface{}) + if !ok { + return fmt.Errorf("genesis has no app_state object") + } + network, ok := appState["network"].(map[string]interface{}) + if !ok { + return fmt.Errorf("genesis has no app_state.network object") + } + network["attester_infos"] = []interface{}{attesterInfo} + + updatedBz, err := json.MarshalIndent(genDoc, "", " ") + if err != nil { + return fmt.Errorf("marshal genesis: %w", err) + } + return node.WriteFile(ctx, "config/genesis.json", updatedBz) +} + // setupIBCConnection establishes a complete IBC connection and channel func setupIBCConnection(t *testing.T, ctx context.Context, chainA, chainB types.Chain, hermes *relayer.Hermes) (ibc.Connection, ibc.Channel) { err := hermes.CreateClients(ctx, chainA, chainB)