@@ -214,6 +214,144 @@ func TestSyncer_ValidateBlock_UsesStateNextProposer(t *testing.T) {
214214 require .Contains (t , err .Error (), "unexpected proposer" )
215215}
216216
217+ func TestSyncer_TrySyncNextBlock_ClassifiesExternalValidationFailures (t * testing.T ) {
218+ expectedAddr , expectedPub , expectedSigner := buildSyncTestSigner (t )
219+ wrongAddr , wrongPub , wrongSigner := buildSyncTestSigner (t )
220+
221+ now := time .Now ()
222+ baseState := types.State {
223+ ChainID : "tchain" ,
224+ InitialHeight : 1 ,
225+ LastBlockHeight : 1 ,
226+ LastBlockTime : now ,
227+ LastHeaderHash : []byte ("last-header-hash" ),
228+ AppHash : []byte ("app0" ),
229+ NextProposerAddress : expectedAddr ,
230+ }
231+
232+ makeSyncer := func (tb testing.TB ) * Syncer {
233+ tb .Helper ()
234+ ds := dssync .MutexWrap (datastore .NewMapDatastore ())
235+ st := store .New (ds )
236+ cm , err := cache .NewManager (config .DefaultConfig (), st , zerolog .Nop ())
237+ require .NoError (tb , err )
238+
239+ return & Syncer {
240+ cache : cm ,
241+ logger : zerolog .Nop (),
242+ options : common .DefaultBlockOptions (),
243+ }
244+ }
245+
246+ makeEvent := func (tb testing.TB , chainID string , proposer []byte , pub crypto.PubKey , signer signerpkg.Signer , appHash []byte , data * types.Data ) common.DAHeightEvent {
247+ tb .Helper ()
248+ _ , header := makeSignedHeaderBytes (tb , chainID , 2 , proposer , pub , signer , appHash , data , baseState .LastHeaderHash )
249+ return common.DAHeightEvent {
250+ Header : header ,
251+ Data : data ,
252+ Source : common .SourceDA ,
253+ }
254+ }
255+
256+ tests := map [string ]struct {
257+ event func (testing.TB ) common.DAHeightEvent
258+ wantState bool
259+ wantInvalid bool
260+ }{
261+ "wrong proposer with bad app hash is an invalid external block" : {
262+ event : func (tb testing.TB ) common.DAHeightEvent {
263+ data := makeData (baseState .ChainID , 2 , 1 )
264+ data .Metadata .Time = uint64 (now .Add (time .Second ).UnixNano ())
265+ return makeEvent (tb , baseState .ChainID , wrongAddr , wrongPub , wrongSigner , []byte ("forged-app" ), data )
266+ },
267+ wantInvalid : true ,
268+ },
269+ "wrong chain ID is an invalid external block" : {
270+ event : func (tb testing.TB ) common.DAHeightEvent {
271+ data := makeData ("other-chain" , 2 , 1 )
272+ data .Metadata .Time = uint64 (now .Add (time .Second ).UnixNano ())
273+ return makeEvent (tb , "other-chain" , expectedAddr , expectedPub , expectedSigner , baseState .AppHash , data )
274+ },
275+ wantInvalid : true ,
276+ },
277+ "data hash mismatch is an invalid external block" : {
278+ event : func (tb testing.TB ) common.DAHeightEvent {
279+ headerData := makeData (baseState .ChainID , 2 , 1 )
280+ headerData .Metadata .Time = uint64 (now .Add (time .Second ).UnixNano ())
281+ event := makeEvent (tb , baseState .ChainID , expectedAddr , expectedPub , expectedSigner , baseState .AppHash , headerData )
282+ event .Data = makeData (baseState .ChainID , 2 , 2 )
283+ event .Data .Metadata .Time = headerData .Metadata .Time
284+ return event
285+ },
286+ wantInvalid : true ,
287+ },
288+ "header data metadata mismatch is an invalid external block" : {
289+ event : func (tb testing.TB ) common.DAHeightEvent {
290+ headerData := makeData (baseState .ChainID , 2 , 1 )
291+ headerData .Metadata .Time = uint64 (now .Add (time .Second ).UnixNano ())
292+ event := makeEvent (tb , baseState .ChainID , expectedAddr , expectedPub , expectedSigner , baseState .AppHash , headerData )
293+ event .Data = & types.Data {
294+ Metadata : & types.Metadata {
295+ ChainID : baseState .ChainID ,
296+ Height : 3 ,
297+ Time : headerData .Metadata .Time ,
298+ },
299+ Txs : headerData .Txs ,
300+ }
301+ return event
302+ },
303+ wantInvalid : true ,
304+ },
305+ "expected proposer with bad app hash is invalid state" : {
306+ event : func (tb testing.TB ) common.DAHeightEvent {
307+ data := makeData (baseState .ChainID , 2 , 1 )
308+ data .Metadata .Time = uint64 (now .Add (time .Second ).UnixNano ())
309+ return makeEvent (tb , baseState .ChainID , expectedAddr , expectedPub , expectedSigner , []byte ("wrong-app" ), data )
310+ },
311+ wantState : true ,
312+ },
313+ }
314+
315+ for name , tc := range tests {
316+ t .Run (name , func (t * testing.T ) {
317+ event := tc .event (t )
318+ err := makeSyncer (t ).trySyncNextBlockWithState (t .Context (), & event , baseState )
319+ require .Error (t , err )
320+ assert .Equal (t , tc .wantInvalid , errors .Is (err , errInvalidBlock ), "invalid block classification" )
321+ assert .Equal (t , tc .wantState , errors .Is (err , errInvalidState ), "invalid state classification" )
322+ })
323+ }
324+ }
325+
326+ func TestSyncer_ValidateBlock_RejectsSignerAddressNotDerivedFromPubKey (t * testing.T ) {
327+ expectedAddr , _ , _ := buildSyncTestSigner (t )
328+ _ , attackerPub , attackerSigner := buildSyncTestSigner (t )
329+
330+ now := time .Now ()
331+ state := types.State {
332+ ChainID : "tchain" ,
333+ InitialHeight : 1 ,
334+ LastBlockHeight : 1 ,
335+ LastBlockTime : now ,
336+ LastHeaderHash : []byte ("last-header-hash" ),
337+ AppHash : []byte ("app0" ),
338+ NextProposerAddress : expectedAddr ,
339+ }
340+
341+ data := makeData (state .ChainID , 2 , 1 )
342+ data .Metadata .Time = uint64 (now .Add (time .Second ).UnixNano ())
343+ _ , header := makeSignedHeaderBytes (t , state .ChainID , 2 , expectedAddr , attackerPub , attackerSigner , state .AppHash , data , state .LastHeaderHash )
344+
345+ s := & Syncer {
346+ logger : zerolog .Nop (),
347+ options : common .DefaultBlockOptions (),
348+ }
349+
350+ err := s .ValidateBlock (t .Context (), state , data , header )
351+ require .Error (t , err )
352+ require .Contains (t , err .Error (), "signer address" )
353+ }
354+
217355func TestSyncer_ApplyBlockPersistsExecutionNextProposer (t * testing.T ) {
218356 addr , _ , _ := buildSyncTestSigner (t )
219357 execNext := []byte ("execution-next-proposer" )
0 commit comments