Skip to content

Commit ede5086

Browse files
committed
Prevent rollback below DA included height
Add validation to disallow rollback to a height lower than the DA included height, ensuring finalized heights cannot be rolled back. Add tests for various DA included height scenarios.
1 parent 139779f commit ede5086

File tree

2 files changed

+297
-0
lines changed

2 files changed

+297
-0
lines changed

pkg/store/store.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,16 @@ func (s *DefaultStore) Rollback(ctx context.Context, height uint64) error {
278278
return nil
279279
}
280280

281+
daIncludedHeightBz, err := s.GetMetadata(ctx, DAIncludedHeightKey)
282+
if err != nil && !errors.Is(err, ds.ErrNotFound) {
283+
return fmt.Errorf("failed to get DA included height: %w", err)
284+
} else if len(daIncludedHeightBz) == 8 { // valid height stored, so able to check
285+
daIncludedHeight := binary.LittleEndian.Uint64(daIncludedHeightBz)
286+
if daIncludedHeight > height {
287+
return fmt.Errorf("DA included height is greater than the rollback height: cannot rollback a finalized height.")
288+
}
289+
}
290+
281291
for currentHeight > height {
282292
header, err := s.GetHeader(ctx, currentHeight)
283293
if err != nil {

pkg/store/store_test.go

Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package store
22

33
import (
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

4345
func (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

Comments
 (0)