Skip to content

Commit 5e0654a

Browse files
committed
test: cover four-attester IBC quorum
1 parent 04d6e4a commit 5e0654a

10 files changed

Lines changed: 469 additions & 273 deletions

File tree

modules/network/keeper/keeper.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -282,8 +282,8 @@ func (k Keeper) CheckQuorum(ctx sdk.Context, votedPower, totalPower uint64) (boo
282282
return false, fmt.Errorf("invalid quorum fraction: %w", err)
283283
}
284284

285-
requiredPower := math.LegacyNewDec(int64(totalPower)).Mul(quorumFrac).TruncateInt().Uint64()
286-
return votedPower >= requiredPower, nil
285+
requiredPower := math.LegacyNewDec(int64(totalPower)).Mul(quorumFrac)
286+
return math.LegacyNewDec(int64(votedPower)).GT(requiredPower), nil
287287
}
288288

289289
// IsSoftConfirmed checks if a block at a given height is soft-confirmed

modules/network/keeper/msg_server_test.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,11 @@ func TestAttestWithMultipleAttestersTracksQuorumAndSignatures(t *testing.T) {
367367
consensusAddress: sdk.ValAddress("attester3").String(),
368368
vote: bytes.Repeat([]byte{0x03}, MinVoteLen),
369369
},
370+
{
371+
authority: sdk.AccAddress("operator4").String(),
372+
consensusAddress: sdk.ValAddress("attester4").String(),
373+
vote: bytes.Repeat([]byte{0x04}, MinVoteLen),
374+
},
370375
}
371376
for _, attester := range attesters {
372377
_, err := server.JoinAttesterSet(ctx, &types.MsgJoinAttesterSet{
@@ -396,15 +401,15 @@ func TestAttestWithMultipleAttestersTracksQuorumAndSignatures(t *testing.T) {
396401

397402
totalPower, err := keeper.GetTotalPower(ctx)
398403
require.NoError(t, err)
399-
require.Equal(t, uint64(3), totalPower)
404+
require.Equal(t, uint64(4), totalPower)
400405

401406
quorumReached, err := keeper.CheckQuorum(ctx, votedPower, totalPower)
402407
require.NoError(t, err)
403-
require.Equal(t, i >= 1, quorumReached)
408+
require.Equal(t, i >= 2, quorumReached)
404409

405410
lastAttestedHeight, err := keeper.GetLastAttestedHeight(ctx)
406411
require.NoError(t, err)
407-
if i == 0 {
412+
if i < 2 {
408413
require.Zero(t, lastAttestedHeight)
409414
} else {
410415
require.Equal(t, height, lastAttestedHeight)

pkg/rpc/core/blocks_test.go

Lines changed: 128 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -345,96 +345,19 @@ func TestCommit_AttesterModeUsesAttesterValidatorSetForHeader(t *testing.T) {
345345
func TestCommit_AttesterModeBuildsMultiAttesterCommitWithAbsentSignatures(t *testing.T) {
346346
require := require.New(t)
347347

348-
sequencerPrivKey := ed25519.GenPrivKey()
349-
sequencerPubKey := sequencerPrivKey.PubKey()
350-
sequencerLibP2PPrivKey, err := crypto.UnmarshalEd25519PrivateKey(sequencerPrivKey.Bytes())
351-
require.NoError(err)
352-
sequencerLibP2PPubKey := sequencerLibP2PPrivKey.GetPublic()
353-
sequencerAddress := sequencerPubKey.Address().Bytes()[:20]
354-
355-
mockSigner := &MockSigner{}
356-
mockSigner.On("GetPublic").Return(sequencerLibP2PPubKey, nil)
357-
mockSigner.On("GetAddress").Return(sequencerAddress, nil)
358-
359-
env = setupTestEnvironment(mockSigner)
360-
env.AttesterMode = true
361-
defer func() {
362-
env.AttesterMode = false
363-
}()
364-
365-
chainID := "test-chain"
366-
blockHeight := uint64(1)
367-
now := time.Now()
368-
369-
sequencerValidatorHash, err := adapter.ValidatorHasherProvider()(sequencerAddress, sequencerLibP2PPubKey)
370-
require.NoError(err)
371-
blockData, rollkitHeader := createTestBlock(blockHeight, chainID, now, sequencerAddress, sequencerValidatorHash, 1)
372-
373-
lastCommit, err := env.Adapter.GetLastCommit(context.Background(), blockHeight)
374-
require.NoError(err)
375-
abciHeader, err := adapter.ToABCIHeader(rollkitHeader, lastCommit)
376-
require.NoError(err)
348+
fixture := setupFourAttesterCommitTest(t, 3)
349+
commitResult := fixture.commitResult
377350

378-
attesterPrivKeysByAddress := make(map[string]ed25519.PrivKey)
379-
attesterValidators := make([]*cmttypes.Validator, 0, 4)
380-
for range 4 {
381-
privKey := ed25519.GenPrivKey()
382-
pubKey := privKey.PubKey().(ed25519.PubKey)
383-
attesterPrivKeysByAddress[string(pubKey.Address())] = privKey
384-
attesterValidators = append(attesterValidators, cmttypes.NewValidator(pubKey, 1))
385-
}
386-
attesterValidatorSet := cmttypes.NewValidatorSet(attesterValidators)
387-
abciHeader.ValidatorsHash = attesterValidatorSet.Hash()
388-
abciHeader.NextValidatorsHash = attesterValidatorSet.Hash()
389-
abciHeader.ProposerAddress = attesterValidatorSet.Proposer.Address
390-
391-
abciBlock, err := adapter.ToABCIBlock(abciHeader, lastCommit, blockData)
392-
require.NoError(err)
393-
blockParts, err := abciBlock.MakePartSet(cmttypes.BlockPartSizeBytes)
394-
require.NoError(err)
395-
blockID := &cmttypes.BlockID{
396-
Hash: abciHeader.Hash(),
397-
PartSetHeader: blockParts.Header(),
398-
}
399-
require.NoError(env.Adapter.Store.SaveBlockID(context.Background(), blockHeight, blockID))
400-
401-
attesterQueries := make([]mockAttesterNetworkQuery, 0, len(attesterValidatorSet.Validators))
402-
for validatorIndex, validator := range attesterValidatorSet.Validators {
403-
privKey := attesterPrivKeysByAddress[string(validator.Address)]
404-
pubKey := privKey.PubKey().(ed25519.PubKey)
405-
query := mockAttesterNetworkQuery{
406-
attesterValAddress: sdk.ValAddress(validator.Address).String(),
407-
attesterPubKey: pubKey,
408-
}
409-
if validatorIndex < 3 {
410-
query.voteBytes = signAttesterVoteWithIndex(
411-
t,
412-
chainID,
413-
blockHeight,
414-
rollkitHeader.Time(),
415-
blockID,
416-
int32(validatorIndex),
417-
privKey,
418-
)
419-
}
420-
attesterQueries = append(attesterQueries, query)
421-
}
422-
mockAttesterNetworkQueriesForSet(t, env.Adapter.App.(*MockApp), attesterQueries)
423-
424-
mockBlock(blockHeight, rollkitHeader, blockData, make([]byte, 64), sequencerLibP2PPubKey, sequencerAddress)
425-
426-
commitResult := callCommitRPC(t, blockHeight)
427-
428-
require.Equal(attesterValidatorSet.Hash(), []byte(commitResult.ValidatorsHash))
429-
require.Equal(attesterValidatorSet.Hash(), []byte(commitResult.NextValidatorsHash))
351+
require.Equal(fixture.attesterValidatorSet.Hash(), []byte(commitResult.ValidatorsHash))
352+
require.Equal(fixture.attesterValidatorSet.Hash(), []byte(commitResult.NextValidatorsHash))
430353
require.Len(commitResult.Commit.Signatures, 4)
431354

432355
commitSignatures := 0
433356
absentSignatures := 0
434357
for i, signature := range commitResult.Commit.Signatures {
435358
if i < 3 {
436359
require.Equal(cmttypes.BlockIDFlagCommit, signature.BlockIDFlag)
437-
require.Equal(attesterValidatorSet.Validators[i].Address.Bytes(), []byte(signature.ValidatorAddress))
360+
require.Equal(fixture.attesterValidatorSet.Validators[i].Address.Bytes(), []byte(signature.ValidatorAddress))
438361
commitSignatures++
439362
continue
440363
}
@@ -443,10 +366,33 @@ func TestCommit_AttesterModeBuildsMultiAttesterCommitWithAbsentSignatures(t *tes
443366
}
444367
require.Equal(3, commitSignatures)
445368
require.Equal(1, absentSignatures)
446-
require.NoError(attesterValidatorSet.VerifyCommitLight(
447-
chainID,
369+
require.NoError(fixture.attesterValidatorSet.VerifyCommitLight(
370+
fixture.chainID,
448371
commitResult.Commit.BlockID,
449-
int64(blockHeight),
372+
int64(fixture.blockHeight),
373+
commitResult.Commit,
374+
))
375+
}
376+
377+
func TestCommit_AttesterModeWithTwoOfFourSignaturesIsNotIBCVerifiable(t *testing.T) {
378+
require := require.New(t)
379+
380+
fixture := setupFourAttesterCommitTest(t, 2)
381+
commitResult := fixture.commitResult
382+
383+
require.Len(commitResult.Commit.Signatures, 4)
384+
for i, signature := range commitResult.Commit.Signatures {
385+
if i < 2 {
386+
require.Equal(cmttypes.BlockIDFlagCommit, signature.BlockIDFlag)
387+
require.Equal(fixture.attesterValidatorSet.Validators[i].Address.Bytes(), []byte(signature.ValidatorAddress))
388+
continue
389+
}
390+
require.Equal(cmttypes.BlockIDFlagAbsent, signature.BlockIDFlag)
391+
}
392+
require.Error(fixture.attesterValidatorSet.VerifyCommitLight(
393+
fixture.chainID,
394+
commitResult.Commit.BlockID,
395+
int64(fixture.blockHeight),
450396
commitResult.Commit,
451397
))
452398
}
@@ -540,6 +486,103 @@ func createTestBlock(height uint64, chainID string, baseTime time.Time, validato
540486
return blockData, rollkitHeader
541487
}
542488

489+
type fourAttesterCommitFixture struct {
490+
chainID string
491+
blockHeight uint64
492+
attesterValidatorSet *cmttypes.ValidatorSet
493+
commitResult *ctypes.ResultCommit
494+
}
495+
496+
func setupFourAttesterCommitTest(t *testing.T, signedAttesters int) fourAttesterCommitFixture {
497+
t.Helper()
498+
require := require.New(t)
499+
500+
sequencerPrivKey := ed25519.GenPrivKey()
501+
sequencerPubKey := sequencerPrivKey.PubKey()
502+
sequencerLibP2PPrivKey, err := crypto.UnmarshalEd25519PrivateKey(sequencerPrivKey.Bytes())
503+
require.NoError(err)
504+
sequencerLibP2PPubKey := sequencerLibP2PPrivKey.GetPublic()
505+
sequencerAddress := sequencerPubKey.Address().Bytes()[:20]
506+
507+
mockSigner := &MockSigner{}
508+
mockSigner.On("GetPublic").Return(sequencerLibP2PPubKey, nil)
509+
mockSigner.On("GetAddress").Return(sequencerAddress, nil)
510+
511+
env = setupTestEnvironment(mockSigner)
512+
env.AttesterMode = true
513+
t.Cleanup(func() {
514+
env.AttesterMode = false
515+
})
516+
517+
chainID := "test-chain"
518+
blockHeight := uint64(1)
519+
now := time.Now()
520+
521+
sequencerValidatorHash, err := adapter.ValidatorHasherProvider()(sequencerAddress, sequencerLibP2PPubKey)
522+
require.NoError(err)
523+
blockData, rollkitHeader := createTestBlock(blockHeight, chainID, now, sequencerAddress, sequencerValidatorHash, 1)
524+
525+
lastCommit, err := env.Adapter.GetLastCommit(context.Background(), blockHeight)
526+
require.NoError(err)
527+
abciHeader, err := adapter.ToABCIHeader(rollkitHeader, lastCommit)
528+
require.NoError(err)
529+
530+
attesterPrivKeysByAddress := make(map[string]ed25519.PrivKey)
531+
attesterValidators := make([]*cmttypes.Validator, 0, 4)
532+
for range 4 {
533+
privKey := ed25519.GenPrivKey()
534+
pubKey := privKey.PubKey().(ed25519.PubKey)
535+
attesterPrivKeysByAddress[string(pubKey.Address())] = privKey
536+
attesterValidators = append(attesterValidators, cmttypes.NewValidator(pubKey, 1))
537+
}
538+
attesterValidatorSet := cmttypes.NewValidatorSet(attesterValidators)
539+
abciHeader.ValidatorsHash = attesterValidatorSet.Hash()
540+
abciHeader.NextValidatorsHash = attesterValidatorSet.Hash()
541+
abciHeader.ProposerAddress = attesterValidatorSet.Proposer.Address
542+
543+
abciBlock, err := adapter.ToABCIBlock(abciHeader, lastCommit, blockData)
544+
require.NoError(err)
545+
blockParts, err := abciBlock.MakePartSet(cmttypes.BlockPartSizeBytes)
546+
require.NoError(err)
547+
blockID := &cmttypes.BlockID{
548+
Hash: abciHeader.Hash(),
549+
PartSetHeader: blockParts.Header(),
550+
}
551+
require.NoError(env.Adapter.Store.SaveBlockID(context.Background(), blockHeight, blockID))
552+
553+
attesterQueries := make([]mockAttesterNetworkQuery, 0, len(attesterValidatorSet.Validators))
554+
for validatorIndex, validator := range attesterValidatorSet.Validators {
555+
privKey := attesterPrivKeysByAddress[string(validator.Address)]
556+
pubKey := privKey.PubKey().(ed25519.PubKey)
557+
query := mockAttesterNetworkQuery{
558+
attesterValAddress: sdk.ValAddress(validator.Address).String(),
559+
attesterPubKey: pubKey,
560+
}
561+
if validatorIndex < signedAttesters {
562+
query.voteBytes = signAttesterVoteWithIndex(
563+
t,
564+
chainID,
565+
blockHeight,
566+
rollkitHeader.Time(),
567+
blockID,
568+
int32(validatorIndex),
569+
privKey,
570+
)
571+
}
572+
attesterQueries = append(attesterQueries, query)
573+
}
574+
mockAttesterNetworkQueriesForSet(t, env.Adapter.App.(*MockApp), attesterQueries)
575+
576+
mockBlock(blockHeight, rollkitHeader, blockData, make([]byte, 64), sequencerLibP2PPubKey, sequencerAddress)
577+
578+
return fourAttesterCommitFixture{
579+
chainID: chainID,
580+
blockHeight: blockHeight,
581+
attesterValidatorSet: attesterValidatorSet,
582+
commitResult: callCommitRPC(t, blockHeight),
583+
}
584+
}
585+
543586
func signAttesterVote(
544587
t *testing.T,
545588
chainID string,

pkg/rpc/core/status.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package core
22

33
import (
4+
"context"
45
"errors"
56
"fmt"
67
"time"
@@ -172,6 +173,10 @@ func isAttesterNode() bool {
172173

173174
// getLastAttestedHeight returns the highest block height attested by this validator
174175
func getLastAttestedHeight(ctx *rpctypes.Context) (uint64, error) {
176+
return getLastAttestedHeightFromContext(ctx.Context())
177+
}
178+
179+
func getLastAttestedHeightFromContext(ctx context.Context) (uint64, error) {
175180
// Create protobuf request for LastAttestedHeight query (O(1) operation)
176181
req := &networktypes.QueryLastAttestedHeightRequest{}
177182

@@ -182,7 +187,7 @@ func getLastAttestedHeight(ctx *rpctypes.Context) (uint64, error) {
182187
}
183188

184189
// Use ABCI Query with the new endpoint
185-
res, err := env.Adapter.App.Query(ctx.Context(), &abci.RequestQuery{
190+
res, err := env.Adapter.App.Query(ctx, &abci.RequestQuery{
186191
Path: "/evabci.network.v1.Query/LastAttestedHeight",
187192
Data: reqBytes,
188193
})
@@ -199,6 +204,9 @@ func getLastAttestedHeight(ctx *rpctypes.Context) (uint64, error) {
199204
if err := response.Unmarshal(res.Value); err != nil {
200205
return 0, fmt.Errorf("failed to unmarshal response: %w", err)
201206
}
207+
if response.Height < 0 {
208+
return 0, fmt.Errorf("last attested height cannot be negative: %d", response.Height)
209+
}
202210
return uint64(response.Height), nil
203211
}
204212

pkg/rpc/core/utils.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,11 @@ func normalizeHeight(ctx context.Context, height *int64) (uint64, error) {
2424
if height == nil || *height < 0 {
2525
// Handle negative heights if they have special meaning
2626
// (e.g., -1 for latest)
27-
heightValue, err = env.Adapter.RollkitStore.Height(ctx)
27+
if env != nil && env.AttesterMode {
28+
heightValue, err = getLastAttestedHeightFromContext(ctx)
29+
} else {
30+
heightValue, err = env.Adapter.RollkitStore.Height(ctx)
31+
}
2832
if err != nil {
2933
return 0, fmt.Errorf("failed to get current height: %w", err)
3034
}

pkg/rpc/core/utils_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,24 @@
11
package core
22

33
import (
4+
"context"
45
"crypto/sha256"
56
"encoding/hex"
67
"fmt"
78
"strings"
89
"testing"
910

11+
abci "github.com/cometbft/cometbft/abci/types"
12+
cmtlog "github.com/cometbft/cometbft/libs/log"
13+
"github.com/cosmos/gogoproto/proto"
1014
"github.com/stretchr/testify/assert"
15+
"github.com/stretchr/testify/mock"
1116
"github.com/stretchr/testify/require"
17+
18+
rollkitmocks "github.com/evstack/ev-node/test/mocks"
19+
20+
networktypes "github.com/evstack/ev-abci/modules/network/types"
21+
"github.com/evstack/ev-abci/pkg/adapter"
1222
)
1323

1424
func TestTruncateNodeID(t *testing.T) {
@@ -58,3 +68,35 @@ func TestTruncateNodeID(t *testing.T) {
5868
assert.Equal(t, exactLengthNodeIDStr, truncatedNodeID, "Truncated Node ID should be the same as input for exact length")
5969
})
6070
}
71+
72+
func TestNormalizeHeightAttesterModeUsesLastAttestedHeight(t *testing.T) {
73+
originalEnv := env
74+
t.Cleanup(func() { env = originalEnv })
75+
76+
mockStore := new(rollkitmocks.MockStore)
77+
mockApp := new(MockApp)
78+
env = &Environment{
79+
Adapter: &adapter.Adapter{
80+
RollkitStore: mockStore,
81+
App: mockApp,
82+
},
83+
AttesterMode: true,
84+
Logger: cmtlog.NewNopLogger(),
85+
}
86+
87+
queryResponse, err := proto.Marshal(&networktypes.QueryLastAttestedHeightResponse{
88+
Height: 14,
89+
})
90+
require.NoError(t, err)
91+
92+
mockApp.On("Query", mock.Anything, mock.MatchedBy(func(req *abci.RequestQuery) bool {
93+
return req.Path == "/evabci.network.v1.Query/LastAttestedHeight"
94+
})).Return(&abci.ResponseQuery{Code: 0, Value: queryResponse}, nil).Once()
95+
96+
height, err := normalizeHeight(context.Background(), nil)
97+
98+
require.NoError(t, err)
99+
require.Equal(t, uint64(14), height)
100+
mockStore.AssertNotCalled(t, "Height", mock.Anything)
101+
mockApp.AssertExpectations(t)
102+
}

0 commit comments

Comments
 (0)