@@ -352,6 +352,166 @@ func TestSyncer_ValidateBlock_RejectsSignerAddressNotDerivedFromPubKey(t *testin
352352 require .Contains (t , err .Error (), "signer address" )
353353}
354354
355+ func TestSyncer_ValidateBlock_ClassifiesFault (t * testing.T ) {
356+ expectedAddr , expectedPub , expectedSigner := buildSyncTestSigner (t )
357+ wrongAddr , wrongPub , wrongSigner := buildSyncTestSigner (t )
358+
359+ now := time .Now ()
360+ baseState := types.State {
361+ ChainID : "tchain" ,
362+ InitialHeight : 1 ,
363+ LastBlockHeight : 1 ,
364+ LastBlockTime : now ,
365+ LastHeaderHash : []byte ("last-header-hash" ),
366+ AppHash : []byte ("app0" ),
367+ NextProposerAddress : expectedAddr ,
368+ }
369+
370+ makeHeader := func (tb testing.TB , chainID string , proposer []byte , pub crypto.PubKey , signer signerpkg.Signer , appHash []byte , data * types.Data ) * types.SignedHeader {
371+ tb .Helper ()
372+ _ , header := makeSignedHeaderBytes (tb , chainID , 2 , proposer , pub , signer , appHash , data , baseState .LastHeaderHash )
373+ return header
374+ }
375+
376+ tests := map [string ]struct {
377+ setup func (testing.TB ) (* types.SignedHeader , * types.Data )
378+ wantFault ValidationFault
379+ }{
380+ "wrong proposer -> header fault" : {
381+ setup : func (tb testing.TB ) (* types.SignedHeader , * types.Data ) {
382+ data := makeData (baseState .ChainID , 2 , 1 )
383+ data .Metadata .Time = uint64 (now .Add (time .Second ).UnixNano ())
384+ return makeHeader (tb , baseState .ChainID , wrongAddr , wrongPub , wrongSigner , baseState .AppHash , data ), data
385+ },
386+ wantFault : FaultHeader ,
387+ },
388+ "wrong chain id -> header fault" : {
389+ setup : func (tb testing.TB ) (* types.SignedHeader , * types.Data ) {
390+ data := makeData ("other-chain" , 2 , 1 )
391+ data .Metadata .Time = uint64 (now .Add (time .Second ).UnixNano ())
392+ return makeHeader (tb , "other-chain" , expectedAddr , expectedPub , expectedSigner , baseState .AppHash , data ), data
393+ },
394+ wantFault : FaultHeader ,
395+ },
396+ "data hash mismatch -> data fault" : {
397+ setup : func (tb testing.TB ) (* types.SignedHeader , * types.Data ) {
398+ headerData := makeData (baseState .ChainID , 2 , 1 )
399+ headerData .Metadata .Time = uint64 (now .Add (time .Second ).UnixNano ())
400+ header := makeHeader (tb , baseState .ChainID , expectedAddr , expectedPub , expectedSigner , baseState .AppHash , headerData )
401+ attached := makeData (baseState .ChainID , 2 , 2 )
402+ attached .Metadata .Time = headerData .Metadata .Time
403+ return header , attached
404+ },
405+ wantFault : FaultData ,
406+ },
407+ "header data metadata mismatch -> data fault" : {
408+ setup : func (tb testing.TB ) (* types.SignedHeader , * types.Data ) {
409+ headerData := makeData (baseState .ChainID , 2 , 1 )
410+ headerData .Metadata .Time = uint64 (now .Add (time .Second ).UnixNano ())
411+ header := makeHeader (tb , baseState .ChainID , expectedAddr , expectedPub , expectedSigner , baseState .AppHash , headerData )
412+ attached := & types.Data {
413+ Metadata : & types.Metadata {
414+ ChainID : baseState .ChainID ,
415+ Height : 3 ,
416+ Time : headerData .Metadata .Time ,
417+ },
418+ Txs : headerData .Txs ,
419+ }
420+ return header , attached
421+ },
422+ wantFault : FaultData ,
423+ },
424+ }
425+
426+ for name , tc := range tests {
427+ t .Run (name , func (t * testing.T ) {
428+ header , data := tc .setup (t )
429+ s := & Syncer {logger : zerolog .Nop (), options : common .DefaultBlockOptions ()}
430+ err := s .ValidateBlock (t .Context (), baseState , data , header )
431+ require .Error (t , err )
432+
433+ var vErr * BlockValidationError
434+ require .ErrorAs (t , err , & vErr , "expected *BlockValidationError" )
435+ assert .Equal (t , tc .wantFault , vErr .Fault , "fault classification" )
436+ })
437+ }
438+ }
439+
440+ func TestSyncer_TrySyncNextBlock_SelectiveCacheCleanup (t * testing.T ) {
441+ expectedAddr , expectedPub , expectedSigner := buildSyncTestSigner (t )
442+ wrongAddr , wrongPub , wrongSigner := buildSyncTestSigner (t )
443+
444+ now := time .Now ()
445+ baseState := types.State {
446+ ChainID : "tchain" ,
447+ InitialHeight : 1 ,
448+ LastBlockHeight : 1 ,
449+ LastBlockTime : now ,
450+ LastHeaderHash : []byte ("last-header-hash" ),
451+ AppHash : []byte ("app0" ),
452+ NextProposerAddress : expectedAddr ,
453+ }
454+
455+ makeSyncer := func (tb testing.TB ) (* Syncer , cache.Manager ) {
456+ tb .Helper ()
457+ ds := dssync .MutexWrap (datastore .NewMapDatastore ())
458+ st := store .New (ds )
459+ cm , err := cache .NewManager (config .DefaultConfig (), st , zerolog .Nop ())
460+ require .NoError (tb , err )
461+ return & Syncer {cache : cm , logger : zerolog .Nop (), options : common .DefaultBlockOptions ()}, cm
462+ }
463+
464+ tests := map [string ]struct {
465+ event func (testing.TB ) common.DAHeightEvent
466+ wantHeaderInCache bool
467+ wantDataInCache bool
468+ }{
469+ "header fault keeps data in cache" : {
470+ event : func (tb testing.TB ) common.DAHeightEvent {
471+ data := makeData (baseState .ChainID , 2 , 1 )
472+ data .Metadata .Time = uint64 (now .Add (time .Second ).UnixNano ())
473+ _ , header := makeSignedHeaderBytes (tb , baseState .ChainID , 2 , wrongAddr , wrongPub , wrongSigner , baseState .AppHash , data , baseState .LastHeaderHash )
474+ return common.DAHeightEvent {Header : header , Data : data , Source : common .SourceDA }
475+ },
476+ wantHeaderInCache : false ,
477+ wantDataInCache : true ,
478+ },
479+ "data fault keeps header in cache" : {
480+ event : func (tb testing.TB ) common.DAHeightEvent {
481+ headerData := makeData (baseState .ChainID , 2 , 1 )
482+ headerData .Metadata .Time = uint64 (now .Add (time .Second ).UnixNano ())
483+ _ , header := makeSignedHeaderBytes (tb , baseState .ChainID , 2 , expectedAddr , expectedPub , expectedSigner , baseState .AppHash , headerData , baseState .LastHeaderHash )
484+ attached := makeData (baseState .ChainID , 2 , 2 )
485+ attached .Metadata .Time = headerData .Metadata .Time
486+ return common.DAHeightEvent {Header : header , Data : attached , Source : common .SourceDA }
487+ },
488+ wantHeaderInCache : true ,
489+ wantDataInCache : false ,
490+ },
491+ }
492+
493+ for name , tc := range tests {
494+ t .Run (name , func (t * testing.T ) {
495+ s , cm := makeSyncer (t )
496+ event := tc .event (t )
497+ headerHash := event .Header .Hash ().String ()
498+ dataHash := event .Data .DACommitment ().String ()
499+
500+ // Seed the cache so we can observe what gets removed.
501+ cm .SetHeaderDAIncluded (headerHash , 1 , event .Header .Height ())
502+ cm .SetDataDAIncluded (dataHash , 1 , event .Header .Height ())
503+
504+ err := s .trySyncNextBlockWithState (t .Context (), & event , baseState )
505+ require .Error (t , err )
506+
507+ _ , headerStillIncluded := cm .GetHeaderDAIncludedByHash (headerHash )
508+ _ , dataStillIncluded := cm .GetDataDAIncludedByHash (dataHash )
509+ assert .Equal (t , tc .wantHeaderInCache , headerStillIncluded , "header cache presence" )
510+ assert .Equal (t , tc .wantDataInCache , dataStillIncluded , "data cache presence" )
511+ })
512+ }
513+ }
514+
355515func TestSyncer_ApplyBlockPersistsExecutionNextProposer (t * testing.T ) {
356516 addr , _ , _ := buildSyncTestSigner (t )
357517 execNext := []byte ("execution-next-proposer" )
0 commit comments