@@ -2,6 +2,7 @@ package store
22
33import (
44 "context"
5+ "encoding/binary"
56 "errors"
67 "fmt"
78 "testing"
@@ -24,6 +25,7 @@ type mockBatchingDatastore struct {
2425 unmarshalErrorOnCall int // New field: 0 for no unmarshal error, 1 for first Get, 2 for second Get, etc.
2526 getCallCount int // Tracks number of Get calls
2627 getErrors []error // Specific errors for sequential Get calls
28+ getMetadataError error // Specific error for GetMetadata calls
2729}
2830
2931// mockBatch is a mock implementation of ds.Batch for testing error cases.
@@ -41,6 +43,11 @@ func (m *mockBatchingDatastore) Put(ctx context.Context, key ds.Key, value []byt
4143}
4244
4345func (m * mockBatchingDatastore ) Get (ctx context.Context , key ds.Key ) ([]byte , error ) {
46+ // Check for specific metadata error for DA included height key
47+ if m .getMetadataError != nil && key .String () == "/m/d" {
48+ return nil , m .getMetadataError
49+ }
50+
4451 m .getCallCount ++
4552 if len (m .getErrors ) >= m .getCallCount && m .getErrors [m .getCallCount - 1 ] != nil {
4653 return nil , m .getErrors [m .getCallCount - 1 ]
@@ -792,3 +799,283 @@ func TestRollbackHeightError(t *testing.T) {
792799 require .Error (err )
793800 require .Contains (err .Error (), "failed to get current height" )
794801}
802+
803+ // TestRollbackDAIncludedHeightValidation verifies DA included height validation during rollback
804+ func TestRollbackDAIncludedHeightValidation (t * testing.T ) {
805+ t .Parallel ()
806+ require := require .New (t )
807+
808+ // Test case 1: Rollback to height below DA included height should fail
809+ t .Run ("rollback below DA included height fails" , func (t * testing.T ) {
810+ ctx := context .Background ()
811+ store := New (mustNewInMem ())
812+
813+ // Setup: create and save multiple blocks
814+ chainID := "test-rollback-da-fail"
815+ maxHeight := uint64 (10 )
816+
817+ for h := uint64 (1 ); h <= maxHeight ; h ++ {
818+ header , data := types .GetRandomBlock (h , 2 , chainID )
819+ sig := & header .Signature
820+
821+ err := store .SaveBlockData (ctx , header , data , sig )
822+ require .NoError (err )
823+
824+ err = store .SetHeight (ctx , h )
825+ require .NoError (err )
826+
827+ // Create and update state for this height
828+ state := types.State {
829+ ChainID : chainID ,
830+ InitialHeight : 1 ,
831+ LastBlockHeight : h ,
832+ LastBlockTime : header .Time (),
833+ AppHash : header .AppHash ,
834+ }
835+ err = store .UpdateState (ctx , state )
836+ require .NoError (err )
837+ }
838+
839+ // Set DA included height to 8
840+ daIncludedHeight := uint64 (8 )
841+ heightBytes := make ([]byte , 8 )
842+ binary .LittleEndian .PutUint64 (heightBytes , daIncludedHeight )
843+ err := store .SetMetadata (ctx , DAIncludedHeightKey , heightBytes )
844+ require .NoError (err )
845+
846+ // Rollback to height below DA included height should fail
847+ err = store .Rollback (ctx , uint64 (6 ))
848+ require .Error (err )
849+ require .Contains (err .Error (), "DA included height is greater than the rollback height: cannot rollback a finalized height." )
850+ })
851+
852+ // Test case 2: Rollback to height equal to DA included height should succeed
853+ t .Run ("rollback to DA included height succeeds" , func (t * testing.T ) {
854+ ctx := context .Background ()
855+ store := New (mustNewInMem ())
856+
857+ // Setup: create and save multiple blocks
858+ chainID := "test-rollback-da-equal"
859+ maxHeight := uint64 (10 )
860+
861+ for h := uint64 (1 ); h <= maxHeight ; h ++ {
862+ header , data := types .GetRandomBlock (h , 2 , chainID )
863+ sig := & header .Signature
864+
865+ err := store .SaveBlockData (ctx , header , data , sig )
866+ require .NoError (err )
867+
868+ err = store .SetHeight (ctx , h )
869+ require .NoError (err )
870+
871+ // Create and update state for this height
872+ state := types.State {
873+ ChainID : chainID ,
874+ InitialHeight : 1 ,
875+ LastBlockHeight : h ,
876+ LastBlockTime : header .Time (),
877+ AppHash : header .AppHash ,
878+ }
879+ err = store .UpdateState (ctx , state )
880+ require .NoError (err )
881+ }
882+
883+ // Set DA included height to 8
884+ daIncludedHeight := uint64 (8 )
885+ heightBytes := make ([]byte , 8 )
886+ binary .LittleEndian .PutUint64 (heightBytes , daIncludedHeight )
887+ err := store .SetMetadata (ctx , DAIncludedHeightKey , heightBytes )
888+ require .NoError (err )
889+
890+ // Rollback to height equal to DA included height should succeed
891+ err = store .Rollback (ctx , uint64 (8 ))
892+ require .NoError (err )
893+
894+ // Verify height was rolled back to 8
895+ currentHeight , err := store .Height (ctx )
896+ require .NoError (err )
897+ require .Equal (uint64 (8 ), currentHeight )
898+ })
899+
900+ // Test case 3: Rollback to height above DA included height should succeed
901+ t .Run ("rollback above DA included height succeeds" , func (t * testing.T ) {
902+ ctx := context .Background ()
903+ store := New (mustNewInMem ())
904+
905+ // Setup: create and save multiple blocks
906+ chainID := "test-rollback-da-above"
907+ maxHeight := uint64 (10 )
908+
909+ for h := uint64 (1 ); h <= maxHeight ; h ++ {
910+ header , data := types .GetRandomBlock (h , 2 , chainID )
911+ sig := & header .Signature
912+
913+ err := store .SaveBlockData (ctx , header , data , sig )
914+ require .NoError (err )
915+
916+ err = store .SetHeight (ctx , h )
917+ require .NoError (err )
918+
919+ // Create and update state for this height
920+ state := types.State {
921+ ChainID : chainID ,
922+ InitialHeight : 1 ,
923+ LastBlockHeight : h ,
924+ LastBlockTime : header .Time (),
925+ AppHash : header .AppHash ,
926+ }
927+ err = store .UpdateState (ctx , state )
928+ require .NoError (err )
929+ }
930+
931+ // Set DA included height to 8
932+ daIncludedHeight := uint64 (8 )
933+ heightBytes := make ([]byte , 8 )
934+ binary .LittleEndian .PutUint64 (heightBytes , daIncludedHeight )
935+ err := store .SetMetadata (ctx , DAIncludedHeightKey , heightBytes )
936+ require .NoError (err )
937+
938+ // Rollback to height above DA included height should succeed
939+ err = store .Rollback (ctx , uint64 (9 ))
940+ require .NoError (err )
941+
942+ // Verify height was rolled back to 9
943+ currentHeight , err := store .Height (ctx )
944+ require .NoError (err )
945+ require .Equal (uint64 (9 ), currentHeight )
946+ })
947+ }
948+
949+ // TestRollbackDAIncludedHeightNotSet verifies rollback works when DA included height is not set
950+ func TestRollbackDAIncludedHeightNotSet (t * testing.T ) {
951+ t .Parallel ()
952+ require := require .New (t )
953+
954+ ctx := context .Background ()
955+ store := New (mustNewInMem ())
956+
957+ // Setup: create and save multiple blocks
958+ chainID := "test-rollback-da-notset"
959+ maxHeight := uint64 (5 )
960+
961+ for h := uint64 (1 ); h <= maxHeight ; h ++ {
962+ header , data := types .GetRandomBlock (h , 2 , chainID )
963+ sig := & header .Signature
964+
965+ err := store .SaveBlockData (ctx , header , data , sig )
966+ require .NoError (err )
967+
968+ err = store .SetHeight (ctx , h )
969+ require .NoError (err )
970+
971+ // Create and update state for this height
972+ state := types.State {
973+ ChainID : chainID ,
974+ InitialHeight : 1 ,
975+ LastBlockHeight : h ,
976+ LastBlockTime : header .Time (),
977+ AppHash : header .AppHash ,
978+ }
979+ err = store .UpdateState (ctx , state )
980+ require .NoError (err )
981+ }
982+
983+ // Don't set DA included height - it should not exist
984+ // Rollback should succeed since no DA included height is set
985+ err := store .Rollback (ctx , uint64 (3 ))
986+ require .NoError (err )
987+
988+ // Verify height was rolled back to 3
989+ currentHeight , err := store .Height (ctx )
990+ require .NoError (err )
991+ require .Equal (uint64 (3 ), currentHeight )
992+ }
993+
994+ // TestRollbackDAIncludedHeightInvalidLength verifies rollback works with invalid DA included height data
995+ func TestRollbackDAIncludedHeightInvalidLength (t * testing.T ) {
996+ t .Parallel ()
997+ require := require .New (t )
998+
999+ ctx := context .Background ()
1000+ store := New (mustNewInMem ())
1001+
1002+ // Setup: create and save multiple blocks
1003+ chainID := "test-rollback-da-invalid"
1004+ maxHeight := uint64 (5 )
1005+
1006+ for h := uint64 (1 ); h <= maxHeight ; h ++ {
1007+ header , data := types .GetRandomBlock (h , 2 , chainID )
1008+ sig := & header .Signature
1009+
1010+ err := store .SaveBlockData (ctx , header , data , sig )
1011+ require .NoError (err )
1012+
1013+ err = store .SetHeight (ctx , h )
1014+ require .NoError (err )
1015+
1016+ // Create and update state for this height
1017+ state := types.State {
1018+ ChainID : chainID ,
1019+ InitialHeight : 1 ,
1020+ LastBlockHeight : h ,
1021+ LastBlockTime : header .Time (),
1022+ AppHash : header .AppHash ,
1023+ }
1024+ err = store .UpdateState (ctx , state )
1025+ require .NoError (err )
1026+ }
1027+
1028+ // Set DA included height with invalid length (not 8 bytes)
1029+ invalidHeightData := []byte {1 , 2 , 3 , 4 } // only 4 bytes
1030+ err := store .SetMetadata (ctx , DAIncludedHeightKey , invalidHeightData )
1031+ require .NoError (err )
1032+
1033+ // Rollback should succeed since invalid length data is ignored
1034+ err = store .Rollback (ctx , uint64 (3 ))
1035+ require .NoError (err )
1036+
1037+ // Verify height was rolled back to 3
1038+ currentHeight , err := store .Height (ctx )
1039+ require .NoError (err )
1040+ require .Equal (uint64 (3 ), currentHeight )
1041+ }
1042+
1043+ // TestRollbackDAIncludedHeightGetMetadataError verifies rollback handles GetMetadata errors for DA included height
1044+ func TestRollbackDAIncludedHeightGetMetadataError (t * testing.T ) {
1045+ t .Parallel ()
1046+ require := require .New (t )
1047+
1048+ ctx := context .Background ()
1049+ mock := & mockBatchingDatastore {
1050+ Batching : mustNewInMem (),
1051+ }
1052+ store := New (mock )
1053+
1054+ // Setup: create one block to ensure height > rollback target
1055+ header , data := types .GetRandomBlock (uint64 (2 ), 2 , "test-chain" )
1056+ sig := & header .Signature
1057+ err := store .SaveBlockData (ctx , header , data , sig )
1058+ require .NoError (err )
1059+ err = store .SetHeight (ctx , uint64 (2 ))
1060+ require .NoError (err )
1061+
1062+ // Create and update state for this height
1063+ state := types.State {
1064+ ChainID : "test-chain" ,
1065+ InitialHeight : 1 ,
1066+ LastBlockHeight : 2 ,
1067+ LastBlockTime : header .Time (),
1068+ AppHash : header .AppHash ,
1069+ }
1070+ err = store .UpdateState (ctx , state )
1071+ require .NoError (err )
1072+
1073+ // Configure mock to return error when getting DA included height metadata
1074+ mock .getMetadataError = errors .New ("metadata retrieval failed" )
1075+
1076+ // Rollback should fail due to GetMetadata error
1077+ err = store .Rollback (ctx , uint64 (1 ))
1078+ require .Error (err )
1079+ require .Contains (err .Error (), "failed to get DA included height" )
1080+ require .Contains (err .Error (), "metadata retrieval failed" )
1081+ }
0 commit comments