Skip to content

Commit df8059f

Browse files
authored
chore: adding p2p handler tests (#2662)
<!-- Please read and fill out this form before submitting your PR. Please make sure you have reviewed our contributors guide before submitting your first PR. NOTE: PR titles should follow semantic commits: https://www.conventionalcommits.org/en/v1.0.0/ --> ## Overview Followed the structure of existing tests in the other packages to add test coverage for the p2p handler <!-- Please provide an explanation of the PR, including the appropriate context, background, goal, and rationale. If there is an issue with this information, please provide a tl;dr and link the issue. Ex: Closes #<issue number> -->
1 parent 7f87f82 commit df8059f

File tree

3 files changed

+309
-4
lines changed

3 files changed

+309
-4
lines changed

block/internal/syncing/da_retriever_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,14 @@ import (
2020
coreda "github.com/evstack/ev-node/core/da"
2121
"github.com/evstack/ev-node/pkg/config"
2222
"github.com/evstack/ev-node/pkg/genesis"
23+
signerpkg "github.com/evstack/ev-node/pkg/signer"
2324
"github.com/evstack/ev-node/pkg/store"
2425
testmocks "github.com/evstack/ev-node/test/mocks"
2526
"github.com/evstack/ev-node/types"
2627
)
2728

2829
// makeSignedHeaderBytes builds a valid SignedHeader and returns its binary encoding and the object
29-
func makeSignedHeaderBytes(t *testing.T, chainID string, height uint64, proposer []byte, pub crypto.PubKey, signer interface{ Sign([]byte) ([]byte, error) }, appHash []byte) ([]byte, *types.SignedHeader) {
30+
func makeSignedHeaderBytes(t *testing.T, chainID string, height uint64, proposer []byte, pub crypto.PubKey, signer signerpkg.Signer, appHash []byte) ([]byte, *types.SignedHeader) {
3031
hdr := &types.SignedHeader{
3132
Header: types.Header{
3233
BaseHeader: types.BaseHeader{ChainID: chainID, Height: height, Time: uint64(time.Now().Add(time.Duration(height) * time.Second).UnixNano())},
@@ -46,7 +47,7 @@ func makeSignedHeaderBytes(t *testing.T, chainID string, height uint64, proposer
4647
}
4748

4849
// makeSignedDataBytes builds SignedData containing the provided Data and returns its binary encoding
49-
func makeSignedDataBytes(t *testing.T, chainID string, height uint64, proposer []byte, pub crypto.PubKey, signer interface{ Sign([]byte) ([]byte, error) }, txs int) ([]byte, *types.SignedData) {
50+
func makeSignedDataBytes(t *testing.T, chainID string, height uint64, proposer []byte, pub crypto.PubKey, signer signerpkg.Signer, txs int) ([]byte, *types.SignedData) {
5051
d := &types.Data{Metadata: &types.Metadata{ChainID: chainID, Height: height, Time: uint64(time.Now().UnixNano())}}
5152
if txs > 0 {
5253
d.Txs = make(types.Txs, txs)
Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
package syncing
2+
3+
import (
4+
"context"
5+
crand "crypto/rand"
6+
"errors"
7+
"testing"
8+
"time"
9+
10+
ds "github.com/ipfs/go-datastore"
11+
dssync "github.com/ipfs/go-datastore/sync"
12+
"github.com/libp2p/go-libp2p/core/crypto"
13+
"github.com/rs/zerolog"
14+
"github.com/stretchr/testify/require"
15+
16+
"github.com/evstack/ev-node/block/internal/cache"
17+
"github.com/evstack/ev-node/block/internal/common"
18+
"github.com/evstack/ev-node/pkg/config"
19+
"github.com/evstack/ev-node/pkg/genesis"
20+
signerpkg "github.com/evstack/ev-node/pkg/signer"
21+
"github.com/evstack/ev-node/pkg/signer/noop"
22+
"github.com/evstack/ev-node/pkg/store"
23+
extmocks "github.com/evstack/ev-node/test/mocks/external"
24+
"github.com/evstack/ev-node/types"
25+
)
26+
27+
// buildTestSigner returns an address, pubkey and signer suitable for tests
28+
func buildTestSigner(t *testing.T) ([]byte, crypto.PubKey, signerpkg.Signer) {
29+
t.Helper()
30+
priv, _, err := crypto.GenerateEd25519Key(crand.Reader)
31+
require.NoError(t, err, "failed to generate ed25519 key for test signer")
32+
n, err := noop.NewNoopSigner(priv)
33+
require.NoError(t, err, "failed to create noop signer from private key")
34+
a, err := n.GetAddress()
35+
require.NoError(t, err, "failed to derive address from signer")
36+
p, err := n.GetPublic()
37+
require.NoError(t, err, "failed to derive public key from signer")
38+
return a, p, n
39+
}
40+
41+
// p2pMakeSignedHeader creates a minimally valid SignedHeader for P2P tests
42+
func p2pMakeSignedHeader(t *testing.T, chainID string, height uint64, proposer []byte, pub crypto.PubKey, signer signerpkg.Signer) *types.SignedHeader {
43+
t.Helper()
44+
hdr := &types.SignedHeader{
45+
Header: types.Header{
46+
BaseHeader: types.BaseHeader{ChainID: chainID, Height: height, Time: uint64(time.Now().UnixNano())},
47+
ProposerAddress: proposer,
48+
},
49+
Signer: types.Signer{PubKey: pub, Address: proposer},
50+
}
51+
bz, err := types.DefaultAggregatorNodeSignatureBytesProvider(&hdr.Header)
52+
require.NoError(t, err, "failed to get signature bytes for header")
53+
sig, err := signer.Sign(bz)
54+
require.NoError(t, err, "failed to sign header bytes")
55+
hdr.Signature = sig
56+
return hdr
57+
}
58+
59+
// p2pMakeData creates Data with the given number of txs
60+
func p2pMakeData(t *testing.T, chainID string, height uint64, txs int) *types.Data {
61+
t.Helper()
62+
d := &types.Data{Metadata: &types.Metadata{ChainID: chainID, Height: height, Time: uint64(time.Now().UnixNano())}}
63+
if txs > 0 {
64+
d.Txs = make(types.Txs, txs)
65+
for i := 0; i < txs; i++ {
66+
d.Txs[i] = []byte{byte(height), byte(i)}
67+
}
68+
}
69+
return d
70+
}
71+
72+
// P2PTestData aggregates all dependencies used by P2P handler tests.
73+
type P2PTestData struct {
74+
Handler *P2PHandler
75+
HeaderStore *extmocks.MockStore[*types.SignedHeader]
76+
DataStore *extmocks.MockStore[*types.Data]
77+
Cache cache.Manager
78+
Genesis genesis.Genesis
79+
ProposerAddr []byte
80+
ProposerPub crypto.PubKey
81+
Signer signerpkg.Signer
82+
}
83+
84+
// setupP2P constructs a P2PHandler with mocked go-header stores and in-memory cache/store
85+
func setupP2P(t *testing.T) *P2PTestData {
86+
t.Helper()
87+
datastore := dssync.MutexWrap(ds.NewMapDatastore())
88+
stateStore := store.New(datastore)
89+
cacheManager, err := cache.NewManager(config.DefaultConfig, stateStore, zerolog.Nop())
90+
require.NoError(t, err, "failed to create cache manager")
91+
92+
proposerAddr, proposerPub, signer := buildTestSigner(t)
93+
94+
gen := genesis.Genesis{ChainID: "p2p-test", InitialHeight: 1, StartTime: time.Now().Add(-time.Second), ProposerAddress: proposerAddr}
95+
96+
headerStoreMock := extmocks.NewMockStore[*types.SignedHeader](t)
97+
dataStoreMock := extmocks.NewMockStore[*types.Data](t)
98+
99+
handler := NewP2PHandler(headerStoreMock, dataStoreMock, cacheManager, gen, common.DefaultBlockOptions(), zerolog.Nop())
100+
return &P2PTestData{
101+
Handler: handler,
102+
HeaderStore: headerStoreMock,
103+
DataStore: dataStoreMock,
104+
Cache: cacheManager,
105+
Genesis: gen,
106+
ProposerAddr: proposerAddr,
107+
ProposerPub: proposerPub,
108+
Signer: signer,
109+
}
110+
}
111+
112+
func TestP2PHandler_ProcessHeaderRange_HeaderAndDataHappyPath(t *testing.T) {
113+
p2pData := setupP2P(t)
114+
ctx := context.Background()
115+
116+
// Signed header at height 5 with non-empty data
117+
require.Equal(t, string(p2pData.Genesis.ProposerAddress), string(p2pData.ProposerAddr), "test signer must match genesis proposer for P2P validation")
118+
signedHeader := p2pMakeSignedHeader(t, p2pData.Genesis.ChainID, 5, p2pData.ProposerAddr, p2pData.ProposerPub, p2pData.Signer)
119+
blockData := p2pMakeData(t, p2pData.Genesis.ChainID, 5, 1)
120+
signedHeader.DataHash = blockData.DACommitment()
121+
122+
// Re-sign after setting DataHash so signature matches header bytes
123+
bz, err := types.DefaultAggregatorNodeSignatureBytesProvider(&signedHeader.Header)
124+
require.NoError(t, err, "failed to get signature bytes after setting DataHash")
125+
sig, err := p2pData.Signer.Sign(bz)
126+
require.NoError(t, err, "failed to re-sign header after setting DataHash")
127+
signedHeader.Signature = sig
128+
129+
// Sanity: header should validate with data using default sync verifier
130+
require.NoError(t, signedHeader.ValidateBasicWithData(blockData), "header+data must validate before handler processes them")
131+
132+
p2pData.HeaderStore.EXPECT().GetByHeight(ctx, uint64(5)).Return(signedHeader, nil).Once()
133+
p2pData.DataStore.EXPECT().GetByHeight(ctx, uint64(5)).Return(blockData, nil).Once()
134+
135+
events := p2pData.Handler.ProcessHeaderRange(ctx, 5, 5)
136+
require.Len(t, events, 1, "expected one event for the provided header/data height")
137+
require.Equal(t, uint64(5), events[0].Header.Height())
138+
require.NotNil(t, events[0].Data)
139+
require.Equal(t, uint64(5), events[0].Data.Height())
140+
}
141+
142+
func TestP2PHandler_ProcessHeaderRange_MissingData_NonEmptyHash(t *testing.T) {
143+
p2pData := setupP2P(t)
144+
ctx := context.Background()
145+
146+
require.Equal(t, string(p2pData.Genesis.ProposerAddress), string(p2pData.ProposerAddr), "test signer must match genesis proposer for P2P validation")
147+
signedHeader := p2pMakeSignedHeader(t, p2pData.Genesis.ChainID, 7, p2pData.ProposerAddr, p2pData.ProposerPub, p2pData.Signer)
148+
149+
// Non-empty data: set header.DataHash to a commitment; expect data store lookup to fail and event skipped
150+
blockData := p2pMakeData(t, p2pData.Genesis.ChainID, 7, 1)
151+
signedHeader.DataHash = blockData.DACommitment()
152+
153+
p2pData.HeaderStore.EXPECT().GetByHeight(ctx, uint64(7)).Return(signedHeader, nil).Once()
154+
p2pData.DataStore.EXPECT().GetByHeight(ctx, uint64(7)).Return(nil, errors.New("not found")).Once()
155+
156+
events := p2pData.Handler.ProcessHeaderRange(ctx, 7, 7)
157+
require.Len(t, events, 0)
158+
}
159+
160+
func TestP2PHandler_ProcessDataRange_HeaderMissing(t *testing.T) {
161+
p2pData := setupP2P(t)
162+
ctx := context.Background()
163+
164+
blockData := p2pMakeData(t, p2pData.Genesis.ChainID, 9, 1)
165+
p2pData.DataStore.EXPECT().GetByHeight(ctx, uint64(9)).Return(blockData, nil).Once()
166+
p2pData.HeaderStore.EXPECT().GetByHeight(ctx, uint64(9)).Return(nil, errors.New("no header")).Once()
167+
168+
events := p2pData.Handler.ProcessDataRange(ctx, 9, 9)
169+
require.Len(t, events, 0)
170+
}
171+
172+
func TestP2PHandler_ProposerMismatch_Rejected(t *testing.T) {
173+
p2pData := setupP2P(t)
174+
ctx := context.Background()
175+
176+
// Build a header with a different proposer
177+
badAddr, pub, signer := buildTestSigner(t)
178+
require.NotEqual(t, string(p2pData.Genesis.ProposerAddress), string(badAddr), "negative test requires mismatched proposer")
179+
signedHeader := p2pMakeSignedHeader(t, p2pData.Genesis.ChainID, 4, badAddr, pub, signer)
180+
signedHeader.DataHash = common.DataHashForEmptyTxs
181+
182+
p2pData.HeaderStore.EXPECT().GetByHeight(ctx, uint64(4)).Return(signedHeader, nil).Once()
183+
184+
events := p2pData.Handler.ProcessHeaderRange(ctx, 4, 4)
185+
require.Len(t, events, 0)
186+
}
187+
188+
func TestP2PHandler_CreateEmptyDataForHeader_UsesPreviousDataHash(t *testing.T) {
189+
p2pData := setupP2P(t)
190+
ctx := context.Background()
191+
192+
// Prepare a header at height 10
193+
signedHeader := p2pMakeSignedHeader(t, p2pData.Genesis.ChainID, 10, p2pData.ProposerAddr, p2pData.ProposerPub, p2pData.Signer)
194+
signedHeader.DataHash = common.DataHashForEmptyTxs
195+
196+
// Mock previous data at height 9 so handler can propagate its hash
197+
previousData := p2pMakeData(t, p2pData.Genesis.ChainID, 9, 1)
198+
p2pData.DataStore.EXPECT().GetByHeight(ctx, uint64(9)).Return(previousData, nil).Once()
199+
200+
emptyData := p2pData.Handler.createEmptyDataForHeader(ctx, signedHeader)
201+
require.NotNil(t, emptyData, "handler should synthesize empty data when header declares empty data hash")
202+
require.Equal(t, p2pData.Genesis.ChainID, emptyData.ChainID(), "synthesized data should carry header chain ID")
203+
require.Equal(t, uint64(10), emptyData.Height(), "synthesized data should carry header height")
204+
require.Equal(t, signedHeader.BaseHeader.Time, emptyData.Metadata.Time, "synthesized data should carry header time")
205+
require.Equal(t, previousData.Hash(), emptyData.LastDataHash, "synthesized data should propagate previous data hash")
206+
}
207+
208+
func TestP2PHandler_CreateEmptyDataForHeader_NoPreviousData(t *testing.T) {
209+
p2pData := setupP2P(t)
210+
ctx := context.Background()
211+
212+
// Prepare a header at height 2 (previous height exists but will return error)
213+
signedHeader := p2pMakeSignedHeader(t, p2pData.Genesis.ChainID, 2, p2pData.ProposerAddr, p2pData.ProposerPub, p2pData.Signer)
214+
signedHeader.DataHash = common.DataHashForEmptyTxs
215+
216+
// Mock previous data fetch failure
217+
p2pData.DataStore.EXPECT().GetByHeight(ctx, uint64(1)).Return(nil, errors.New("not available")).Once()
218+
219+
emptyData := p2pData.Handler.createEmptyDataForHeader(ctx, signedHeader)
220+
require.NotNil(t, emptyData, "handler should synthesize empty data even when previous data is unavailable")
221+
require.Equal(t, p2pData.Genesis.ChainID, emptyData.ChainID(), "synthesized data should carry header chain ID")
222+
require.Equal(t, uint64(2), emptyData.Height(), "synthesized data should carry header height")
223+
require.Equal(t, signedHeader.BaseHeader.Time, emptyData.Metadata.Time, "synthesized data should carry header time")
224+
// When no previous data is available, LastDataHash should be zero value
225+
require.Equal(t, (types.Hash)(nil), emptyData.LastDataHash, "last data hash should be empty when previous data is not available")
226+
}
227+
228+
func TestP2PHandler_ProcessHeaderRange_MultipleHeightsHappyPath(t *testing.T) {
229+
p2pData := setupP2P(t)
230+
ctx := context.Background()
231+
232+
// Build two consecutive heights with valid headers and data
233+
// Height 5
234+
header5 := p2pMakeSignedHeader(t, p2pData.Genesis.ChainID, 5, p2pData.ProposerAddr, p2pData.ProposerPub, p2pData.Signer)
235+
data5 := p2pMakeData(t, p2pData.Genesis.ChainID, 5, 1)
236+
header5.DataHash = data5.DACommitment()
237+
// Re-sign after setting DataHash to keep signature valid
238+
bz5, err := types.DefaultAggregatorNodeSignatureBytesProvider(&header5.Header)
239+
require.NoError(t, err, "failed to get signature bytes for height 5")
240+
sig5, err := p2pData.Signer.Sign(bz5)
241+
require.NoError(t, err, "failed to sign header for height 5")
242+
header5.Signature = sig5
243+
require.NoError(t, header5.ValidateBasicWithData(data5), "header/data invalid for height 5")
244+
245+
// Height 6
246+
header6 := p2pMakeSignedHeader(t, p2pData.Genesis.ChainID, 6, p2pData.ProposerAddr, p2pData.ProposerPub, p2pData.Signer)
247+
data6 := p2pMakeData(t, p2pData.Genesis.ChainID, 6, 2)
248+
header6.DataHash = data6.DACommitment()
249+
bz6, err := types.DefaultAggregatorNodeSignatureBytesProvider(&header6.Header)
250+
require.NoError(t, err, "failed to get signature bytes for height 6")
251+
sig6, err := p2pData.Signer.Sign(bz6)
252+
require.NoError(t, err, "failed to sign header for height 6")
253+
header6.Signature = sig6
254+
require.NoError(t, header6.ValidateBasicWithData(data6), "header/data invalid for height 6")
255+
256+
// Expectations for both heights
257+
p2pData.HeaderStore.EXPECT().GetByHeight(ctx, uint64(5)).Return(header5, nil).Once()
258+
p2pData.DataStore.EXPECT().GetByHeight(ctx, uint64(5)).Return(data5, nil).Once()
259+
p2pData.HeaderStore.EXPECT().GetByHeight(ctx, uint64(6)).Return(header6, nil).Once()
260+
p2pData.DataStore.EXPECT().GetByHeight(ctx, uint64(6)).Return(data6, nil).Once()
261+
262+
events := p2pData.Handler.ProcessHeaderRange(ctx, 5, 6)
263+
require.Len(t, events, 2, "expected two events for heights 5 and 6")
264+
require.Equal(t, uint64(5), events[0].Header.Height(), "first event should be height 5")
265+
require.Equal(t, uint64(6), events[1].Header.Height(), "second event should be height 6")
266+
require.NotNil(t, events[0].Data, "event for height 5 must include data")
267+
require.NotNil(t, events[1].Data, "event for height 6 must include data")
268+
}
269+
270+
func TestP2PHandler_ProcessDataRange_HeaderValidateHeaderFails(t *testing.T) {
271+
p2pData := setupP2P(t)
272+
ctx := context.Background()
273+
274+
// Data exists at height 3
275+
blockData := p2pMakeData(t, p2pData.Genesis.ChainID, 3, 1)
276+
p2pData.DataStore.EXPECT().GetByHeight(ctx, uint64(3)).Return(blockData, nil).Once()
277+
278+
// Header proposer does not match genesis -> validateHeader should fail
279+
badAddr, pub, signer := buildTestSigner(t)
280+
require.NotEqual(t, string(p2pData.Genesis.ProposerAddress), string(badAddr), "negative test requires mismatched proposer")
281+
badHeader := p2pMakeSignedHeader(t, p2pData.Genesis.ChainID, 3, badAddr, pub, signer)
282+
p2pData.HeaderStore.EXPECT().GetByHeight(ctx, uint64(3)).Return(badHeader, nil).Once()
283+
284+
events := p2pData.Handler.ProcessDataRange(ctx, 3, 3)
285+
require.Len(t, events, 0, "validateHeader failure should drop event")
286+
}
287+
288+
func TestP2PHandler_ProcessDataRange_ValidateBasicWithDataFails(t *testing.T) {
289+
p2pData := setupP2P(t)
290+
ctx := context.Background()
291+
292+
// Data exists at height 4
293+
blockData := p2pMakeData(t, p2pData.Genesis.ChainID, 4, 1)
294+
p2pData.DataStore.EXPECT().GetByHeight(ctx, uint64(4)).Return(blockData, nil).Once()
295+
296+
// Header proposer matches genesis, but signature is empty -> ValidateBasicWithData should fail
297+
header := p2pMakeSignedHeader(t, p2pData.Genesis.ChainID, 4, p2pData.ProposerAddr, p2pData.ProposerPub, p2pData.Signer)
298+
header.Signature = nil // force signature validation failure
299+
p2pData.HeaderStore.EXPECT().GetByHeight(ctx, uint64(4)).Return(header, nil).Once()
300+
301+
events := p2pData.Handler.ProcessDataRange(ctx, 4, 4)
302+
require.Len(t, events, 0, "ValidateBasicWithData failure should drop event")
303+
}

block/internal/syncing/syncer_logic_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,15 @@ import (
1818
"github.com/evstack/ev-node/block/internal/common"
1919
"github.com/evstack/ev-node/pkg/config"
2020
"github.com/evstack/ev-node/pkg/genesis"
21+
signerpkg "github.com/evstack/ev-node/pkg/signer"
2122
"github.com/evstack/ev-node/pkg/signer/noop"
2223
"github.com/evstack/ev-node/pkg/store"
2324
testmocks "github.com/evstack/ev-node/test/mocks"
2425
"github.com/evstack/ev-node/types"
2526
)
2627

2728
// helper to create a signer, pubkey and address for tests
28-
func buildSyncTestSigner(t *testing.T) (addr []byte, pub crypto.PubKey, signer interface{ Sign([]byte) ([]byte, error) }) {
29+
func buildSyncTestSigner(t *testing.T) (addr []byte, pub crypto.PubKey, signer signerpkg.Signer) {
2930
t.Helper()
3031
priv, _, err := crypto.GenerateEd25519Key(crand.Reader)
3132
require.NoError(t, err)
@@ -40,7 +41,7 @@ func buildSyncTestSigner(t *testing.T) (addr []byte, pub crypto.PubKey, signer i
4041

4142
// (no dummies needed; tests use mocks)
4243

43-
func makeSignedHeader(t *testing.T, chainID string, height uint64, proposer []byte, pub crypto.PubKey, signer interface{ Sign([]byte) ([]byte, error) }, appHash []byte) *types.SignedHeader {
44+
func makeSignedHeader(t *testing.T, chainID string, height uint64, proposer []byte, pub crypto.PubKey, signer signerpkg.Signer, appHash []byte) *types.SignedHeader {
4445
hdr := &types.SignedHeader{
4546
Header: types.Header{
4647
BaseHeader: types.BaseHeader{ChainID: chainID, Height: height, Time: uint64(time.Now().Add(time.Duration(height) * time.Second).UnixNano())},

0 commit comments

Comments
 (0)