diff --git a/storage.go b/storage.go index b48d715..e9a5792 100644 --- a/storage.go +++ b/storage.go @@ -1040,7 +1040,11 @@ func storeSlab(storage SlabStorage, slab Slab) error { // fix broken references. As of April 2024, only 10 registers in testnet (not mainnet) // were found to have broken references and they seem to have resulted from a bug // that was fixed 2 years ago by https://github.com/onflow/cadence/pull/1565. -func (s *PersistentSlabStorage) FixLoadedBrokenReferences() ([]StorageID, error) { +func (s *PersistentSlabStorage) FixLoadedBrokenReferences(needToFix func(old Value) bool) ( + fixedStorageIDs map[StorageID][]StorageID, // key: root slab ID, value: slab IDs containing broken refs + skippedStorageIDs map[StorageID][]StorageID, // key: root slab ID, value: slab IDs containing broken refs + err error, +) { // parentOf is used to find root slab from non-root slab. // Broken reference can be in non-root slab, and we need StorageID of root slab @@ -1119,10 +1123,10 @@ func (s *PersistentSlabStorage) FixLoadedBrokenReferences() ([]StorageID, error) } if len(brokenStorageIDs) == 0 { - return nil, nil + return nil, nil, nil } - rootSlabStorageIDsWithBrokenData := make(map[StorageID]struct{}) + rootSlabStorageIDsWithBrokenData := make(map[StorageID][]StorageID) var errs []error // Find StorageIDs of root slab for slabs containing broken references. @@ -1132,26 +1136,35 @@ func (s *PersistentSlabStorage) FixLoadedBrokenReferences() ([]StorageID, error) errs = append(errs, fmt.Errorf("failed to get root slab id for slab %s", id)) continue } - rootSlabStorageIDsWithBrokenData[rootID] = struct{}{} + rootSlabStorageIDsWithBrokenData[rootID] = append(rootSlabStorageIDsWithBrokenData[rootID], id) } - for rootSlabID := range rootSlabStorageIDsWithBrokenData { - rootSlab := s.RetrieveIfLoaded(rootSlabID) + for rootStorageID, brokenStorageIDs := range rootSlabStorageIDsWithBrokenData { + rootSlab := s.RetrieveIfLoaded(rootStorageID) if rootSlab == nil { - errs = append(errs, fmt.Errorf("failed to retrieve loaded root slab %s", rootSlabID)) + errs = append(errs, fmt.Errorf("failed to retrieve loaded root slab %s", rootStorageID)) continue } switch rootSlab := rootSlab.(type) { case MapSlab: - if rootSlab.ExtraData() == nil { - errs = append(errs, fmt.Errorf("failed to fix broken references because slab %s isn't root slab", rootSlab.ID())) + value, err := rootSlab.StoredValue(s) + if err != nil { + errs = append(errs, fmt.Errorf("failed to convert slab %s into value", rootSlab.ID())) continue } - err := s.fixBrokenReferencesInMap(rootSlab) - if err != nil { - errs = append(errs, err) + if needToFix(value) { + err := s.fixBrokenReferencesInMap(rootSlab) + if err != nil { + errs = append(errs, err) + continue + } + } else { + if skippedStorageIDs == nil { + skippedStorageIDs = make(map[StorageID][]StorageID) + } + skippedStorageIDs[rootStorageID] = brokenStorageIDs } default: @@ -1160,7 +1173,11 @@ func (s *PersistentSlabStorage) FixLoadedBrokenReferences() ([]StorageID, error) } } - return brokenStorageIDs, errors.Join(errs...) + for id := range skippedStorageIDs { + delete(rootSlabStorageIDsWithBrokenData, id) + } + + return rootSlabStorageIDsWithBrokenData, skippedStorageIDs, errors.Join(errs...) } // fixBrokenReferencesInMap replaces replaces broken map with empty map diff --git a/storage_test.go b/storage_test.go index 30ff5a5..7b39104 100644 --- a/storage_test.go +++ b/storage_test.go @@ -1672,10 +1672,27 @@ func TestFixLoadedBrokenReferences(t *testing.T) { require.True(t, found) } + var fixedRootIDs map[StorageID][]StorageID + var skippedRootIDs map[StorageID][]StorageID + + // Don't fix any broken references + fixedRootIDs, skippedRootIDs, err = storage.FixLoadedBrokenReferences(func(_ Value) bool { + return false + }) + require.NoError(t, err) + require.Equal(t, 0, len(fixedRootIDs)) + require.Equal(t, 0, len(skippedRootIDs)) + + // No data is modified because no fix happened + require.Equal(t, 0, len(storage.deltas)) + // Fix broken reference - fixedIDs, err := storage.FixLoadedBrokenReferences() + fixedRootIDs, skippedRootIDs, err = storage.FixLoadedBrokenReferences(func(_ Value) bool { + return true + }) require.NoError(t, err) - require.Equal(t, 0, len(fixedIDs)) + require.Equal(t, 0, len(fixedRootIDs)) + require.Equal(t, 0, len(skippedRootIDs)) // No data is modified during fixing broken reference require.Equal(t, 0, len(storage.deltas)) @@ -1691,7 +1708,9 @@ func TestFixLoadedBrokenReferences(t *testing.T) { rootID := StorageID{Address: address, Index: StorageIndex{0, 0, 0, 0, 0, 0, 0, 1}} - brokenRefSlabID := rootID + brokenRefs := map[StorageID][]StorageID{ + rootID: []StorageID{rootID}, + } data := map[StorageID][]byte{ rootID: { @@ -1787,11 +1806,36 @@ func TestFixLoadedBrokenReferences(t *testing.T) { _, err := CheckStorageHealth(storage, -1) require.ErrorContains(t, err, "slab (0x0.1) not found: slab not found during slab iteration") - // Fix broken reference - fixedIDs, err := storage.FixLoadedBrokenReferences() + var fixedRootIDs map[StorageID][]StorageID + var skippedRootIDs map[StorageID][]StorageID + + // Don't fix any broken references + fixedRootIDs, skippedRootIDs, err = storage.FixLoadedBrokenReferences(func(_ Value) bool { + return false + }) + require.NoError(t, err) + require.Equal(t, 0, len(fixedRootIDs)) + require.Equal(t, len(brokenRefs), len(skippedRootIDs)) + + for rootID, slabIDsWithBrokenRef := range brokenRefs { + require.ElementsMatch(t, slabIDsWithBrokenRef, skippedRootIDs[rootID]) + } + + // No data is modified because no fix happened + require.Equal(t, 0, len(storage.deltas)) + + // Fix broken references + fixedRootIDs, skippedRootIDs, err = storage.FixLoadedBrokenReferences(func(_ Value) bool { + return true + }) require.NoError(t, err) - require.Equal(t, 1, len(fixedIDs)) - require.Equal(t, brokenRefSlabID, fixedIDs[0]) + require.Equal(t, len(brokenRefs), len(fixedRootIDs)) + + for rootID, slabIDsWithBrokenRef := range brokenRefs { + require.ElementsMatch(t, slabIDsWithBrokenRef, fixedRootIDs[rootID]) + } + + require.Equal(t, 0, len(skippedRootIDs)) require.Equal(t, 1, len(storage.deltas)) // Check health after fixing broken reference @@ -1822,7 +1866,9 @@ func TestFixLoadedBrokenReferences(t *testing.T) { nonRootDataID1 := StorageID{Address: address, Index: StorageIndex{0, 0, 0, 0, 0, 0, 0, 2}} nonRootDataID2 := StorageID{Address: address, Index: StorageIndex{0, 0, 0, 0, 0, 0, 0, 3}} - brokenRefSlabID := nonRootDataID2 + brokenRefs := map[StorageID][]StorageID{ + rootID: []StorageID{nonRootDataID2}, + } // Expected serialized slab data with storage id data := map[StorageID][]byte{ @@ -2008,11 +2054,36 @@ func TestFixLoadedBrokenReferences(t *testing.T) { _, err := CheckStorageHealth(storage, -1) require.ErrorContains(t, err, "slab (0x0.1) not found: slab not found during slab iteration") + var fixedRootIDs map[StorageID][]StorageID + var skippedRootIDs map[StorageID][]StorageID + + // Don't fix any broken references + fixedRootIDs, skippedRootIDs, err = storage.FixLoadedBrokenReferences(func(_ Value) bool { + return false + }) + require.NoError(t, err) + require.Equal(t, 0, len(fixedRootIDs)) + require.Equal(t, len(brokenRefs), len(skippedRootIDs)) + + for rootID, slabIDsWithBrokenRef := range brokenRefs { + require.ElementsMatch(t, slabIDsWithBrokenRef, skippedRootIDs[rootID]) + } + + // No data is modified because no fix happened + require.Equal(t, 0, len(storage.deltas)) + // Fix broken reference - fixedIDs, err := storage.FixLoadedBrokenReferences() + fixedRootIDs, skippedRootIDs, err = storage.FixLoadedBrokenReferences(func(_ Value) bool { + return true + }) require.NoError(t, err) - require.Equal(t, 1, len(fixedIDs)) - require.Equal(t, brokenRefSlabID, fixedIDs[0]) + require.Equal(t, 1, len(fixedRootIDs)) + + for rootID, slabIDsWithBrokenRef := range brokenRefs { + require.ElementsMatch(t, slabIDsWithBrokenRef, fixedRootIDs[rootID]) + } + + require.Equal(t, 0, len(skippedRootIDs)) require.Equal(t, 3, len(storage.deltas)) // Check health after fixing broken reference @@ -2043,7 +2114,9 @@ func TestFixLoadedBrokenReferences(t *testing.T) { nonRootDataID1 := StorageID{Address: address, Index: StorageIndex{0, 0, 0, 0, 0, 0, 0, 2}} nonRootDataID2 := StorageID{Address: address, Index: StorageIndex{0, 0, 0, 0, 0, 0, 0, 3}} - brokenRefSlabID := []StorageID{nonRootDataID1, nonRootDataID2} + brokenRefs := map[StorageID][]StorageID{ + rootID: []StorageID{nonRootDataID1, nonRootDataID2}, + } data := map[StorageID][]byte{ @@ -2228,23 +2301,38 @@ func TestFixLoadedBrokenReferences(t *testing.T) { _, err := CheckStorageHealth(storage, -1) require.ErrorContains(t, err, "slab not found during slab iteration") + var fixedRootIDs map[StorageID][]StorageID + var skippedRootIDs map[StorageID][]StorageID + + // Don't fix any broken references + fixedRootIDs, skippedRootIDs, err = storage.FixLoadedBrokenReferences(func(_ Value) bool { + return false + }) + require.NoError(t, err) + require.Equal(t, 0, len(fixedRootIDs)) + require.Equal(t, len(brokenRefs), len(skippedRootIDs)) + + for rootID, slabIDsWithBrokenRef := range brokenRefs { + require.ElementsMatch(t, slabIDsWithBrokenRef, skippedRootIDs[rootID]) + } + + // No data is modified because no fix happened + require.Equal(t, 0, len(storage.deltas)) + // Fix broken reference - fixedIDs, err := storage.FixLoadedBrokenReferences() + fixedRootIDs, skippedRootIDs, err = storage.FixLoadedBrokenReferences(func(_ Value) bool { + return true + }) require.NoError(t, err) - require.Equal(t, len(brokenRefSlabID), len(fixedIDs)) - require.Equal(t, 3, len(storage.deltas)) + require.Equal(t, len(brokenRefs), len(fixedRootIDs)) - for _, expected := range brokenRefSlabID { - var found bool - for _, actual := range fixedIDs { - if actual == expected { - found = true - break - } - } - require.True(t, found) + for rootID, slabIDsWithBrokenRef := range brokenRefs { + require.ElementsMatch(t, slabIDsWithBrokenRef, fixedRootIDs[rootID]) } + require.Equal(t, 0, len(skippedRootIDs)) + require.Equal(t, 3, len(storage.deltas)) + // Check health after fixing broken reference rootIDs, err := CheckStorageHealth(storage, -1) require.NoError(t, err) @@ -2274,7 +2362,9 @@ func TestFixLoadedBrokenReferences(t *testing.T) { nonRootDataID2 := StorageID{Address: address, Index: StorageIndex{0, 0, 0, 0, 0, 0, 0, 3}} nestedContainerRootID := StorageID{Address: address, Index: StorageIndex{0, 0, 0, 0, 0, 0, 0, 4}} - brokenRefSlabID := []StorageID{nestedContainerRootID} + brokenRefs := map[StorageID][]StorageID{ + nestedContainerRootID: []StorageID{nestedContainerRootID}, + } data := map[StorageID][]byte{ @@ -2503,11 +2593,36 @@ func TestFixLoadedBrokenReferences(t *testing.T) { _, err := CheckStorageHealth(storage, -1) require.ErrorContains(t, err, "slab (0x0.1) not found: slab not found during slab iteration") + var fixedRootIDs map[StorageID][]StorageID + var skippedRootIDs map[StorageID][]StorageID + + // Don't fix any broken references + fixedRootIDs, skippedRootIDs, err = storage.FixLoadedBrokenReferences(func(_ Value) bool { + return false + }) + require.NoError(t, err) + require.Equal(t, 0, len(fixedRootIDs)) + require.Equal(t, len(brokenRefs), len(skippedRootIDs)) + + for rootID, slabIDsWithBrokenRef := range brokenRefs { + require.ElementsMatch(t, slabIDsWithBrokenRef, skippedRootIDs[rootID]) + } + + // No data is modified because no fix happened + require.Equal(t, 0, len(storage.deltas)) + // Fix broken reference - fixedIDs, err := storage.FixLoadedBrokenReferences() + fixedRootIDs, skippedRootIDs, err = storage.FixLoadedBrokenReferences(func(_ Value) bool { + return true + }) require.NoError(t, err) - require.Equal(t, len(brokenRefSlabID), len(fixedIDs)) - require.Equal(t, brokenRefSlabID[0], fixedIDs[0]) + require.Equal(t, len(brokenRefs), len(fixedRootIDs)) + + for rootID, slabIDsWithBrokenRef := range brokenRefs { + require.ElementsMatch(t, slabIDsWithBrokenRef, fixedRootIDs[rootID]) + } + + require.Equal(t, 0, len(skippedRootIDs)) require.Equal(t, 1, len(storage.deltas)) // Check health after fixing broken reference @@ -2532,4 +2647,353 @@ func TestFixLoadedBrokenReferences(t *testing.T) { require.True(t, found) require.Equal(t, fixedData[nestedContainerRootID], savedData) }) + + t.Run("selectively fix maps", func(t *testing.T) { + rootID1 := StorageID{Address: address, Index: StorageIndex{0, 0, 0, 0, 0, 0, 0, 1}} + nonRootDataID1 := StorageID{Address: address, Index: StorageIndex{0, 0, 0, 0, 0, 0, 0, 2}} + nonRootDataID2 := StorageID{Address: address, Index: StorageIndex{0, 0, 0, 0, 0, 0, 0, 3}} // containing broken ref + + rootID2 := StorageID{Address: address, Index: StorageIndex{0, 0, 0, 0, 0, 0, 0, 4}} // containing broken ref + + rootIDs := []StorageID{rootID1, rootID2} + + brokenRefs := map[StorageID][]StorageID{ + rootID1: []StorageID{nonRootDataID2}, + rootID2: []StorageID{rootID2}, + } + + data := map[StorageID][]byte{ + // metadata slab + rootID1: { + // extra data + // version + 0x00, + // flag: root + map meta + 0x89, + // extra data (CBOR encoded array of 3 elements) + 0x83, + // type info: "map" + 0x18, 0x2A, + // count: 8 + 0x08, + // seed + 0x1b, 0x52, 0xa8, 0x78, 0x3, 0x85, 0x2c, 0xaa, 0x49, + + // version + 0x00, + // flag: root + meta + 0x89, + // child header count + 0x00, 0x02, + // child header 1 (storage id, first key, size) + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x02, + // child header 2 + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, + 0x00, 0x00, 0x00, 0xfe, + }, + + // data slab + nonRootDataID1: { + // version + 0x00, + // flag: map data + 0x08, + // next storage id + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, + + // the following encoded data is valid CBOR + + // elements (array of 3 elements) + 0x83, + + // level: 0 + 0x00, + + // hkeys (byte string of length 8 * 4) + 0x5b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, + // hkey: 0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // hkey: 1 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + // hkey: 2 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, + // hkey: 3 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, + + // elements (array of 4 elements) + // each element is encoded as CBOR array of 2 elements (key, value) + 0x9b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, + // element: [aaaaaaaaaaaaaaaaaaaaaa:aaaaaaaaaaaaaaaaaaaaaa] + 0x82, + 0x76, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + 0x76, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, + // element: [bbbbbbbbbbbbbbbbbbbbbb:bbbbbbbbbbbbbbbbbbbbbb] + 0x82, + 0x76, 0x62, 0x62, 0x62, 0x62, 0x62, 0x62, 0x62, 0x62, 0x62, 0x62, 0x62, 0x62, 0x62, 0x62, 0x62, 0x62, 0x62, 0x62, 0x62, 0x62, 0x62, 0x62, + 0x76, 0x62, 0x62, 0x62, 0x62, 0x62, 0x62, 0x62, 0x62, 0x62, 0x62, 0x62, 0x62, 0x62, 0x62, 0x62, 0x62, 0x62, 0x62, 0x62, 0x62, 0x62, 0x62, + // element: [cccccccccccccccccccccc:cccccccccccccccccccccc] + 0x82, + 0x76, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, + 0x76, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, + // element: [dddddddddddddddddddddd:dddddddddddddddddddddd] + 0x82, + 0x76, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, + 0x76, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, 0x64, + }, + + // data slab + nonRootDataID2: { + // version + 0x00, + // flag: has pointer + map data + 0x48, + // next storage id + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + + // the following encoded data is valid CBOR + + // elements (array of 3 elements) + 0x83, + + // level: 0 + 0x00, + + // hkeys (byte string of length 8 * 4) + 0x5b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, + // hkey: 4 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, + // hkey: 5 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, + // hkey: 6 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, + // hkey: 7 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, + + // elements (array of 4 elements) + // each element is encoded as CBOR array of 2 elements (key, value) + 0x9b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, + // element: [eeeeeeeeeeeeeeeeeeeeee:eeeeeeeeeeeeeeeeeeeeee] + 0x82, + 0x76, 0x65, 0x65, 0x65, 0x65, 0x65, 0x65, 0x65, 0x65, 0x65, 0x65, 0x65, 0x65, 0x65, 0x65, 0x65, 0x65, 0x65, 0x65, 0x65, 0x65, 0x65, 0x65, + 0x76, 0x65, 0x65, 0x65, 0x65, 0x65, 0x65, 0x65, 0x65, 0x65, 0x65, 0x65, 0x65, 0x65, 0x65, 0x65, 0x65, 0x65, 0x65, 0x65, 0x65, 0x65, 0x65, + // element: [ffffffffffffffffffffff:ffffffffffffffffffffff] + 0x82, + 0x76, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, + 0x76, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, + // element: [gggggggggggggggggggggg:gggggggggggggggggggggg] + 0x82, + 0x76, 0x67, 0x67, 0x67, 0x67, 0x67, 0x67, 0x67, 0x67, 0x67, 0x67, 0x67, 0x67, 0x67, 0x67, 0x67, 0x67, 0x67, 0x67, 0x67, 0x67, 0x67, 0x67, + 0x76, 0x67, 0x67, 0x67, 0x67, 0x67, 0x67, 0x67, 0x67, 0x67, 0x67, 0x67, 0x67, 0x67, 0x67, 0x67, 0x67, 0x67, 0x67, 0x67, 0x67, 0x67, 0x67, + // element: [hhhhhhhhhhhhhhhhhhhhhh:StorageID(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1)] + 0x82, + 0x76, 0x68, 0x68, 0x68, 0x68, 0x68, 0x68, 0x68, 0x68, 0x68, 0x68, 0x68, 0x68, 0x68, 0x68, 0x68, 0x68, 0x68, 0x68, 0x68, 0x68, 0x68, 0x68, + 0xd8, 0xff, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + }, + + // map data slab + rootID2: { + // extra data + // version + 0x00, + // flag: root + map data + 0x88, + // extra data (CBOR encoded array of 3 elements) + 0x83, + // type info + 0x18, 0x2a, + // count: 1 + 0x01, + // seed + 0x1b, 0x52, 0xa8, 0x78, 0x3, 0x85, 0x2c, 0xaa, 0x4a, + + // version + 0x00, + // flag: root + map data + 0x88, + + // the following encoded data is valid CBOR + + // elements (array of 3 elements) + 0x83, + + // level: 0 + 0x00, + + // hkeys (byte string of length 8 * 1) + 0x5b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, + // hkey: 0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + + // elements (array of 1 elements) + // each element is encoded as CBOR array of 2 elements (key, value) + 0x9b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + // element: [uint64(0):StorageID(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1)] + 0x82, + 0xd8, 0xa4, 0x00, + 0xd8, 0xff, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + }, + } + + fixedData := map[StorageID][]byte{ + rootID1: { + // extra data + // version + 0x00, + // flag: root + map data + 0x88, + // extra data (CBOR encoded array of 3 elements) + 0x83, + // type info + 0x18, 0x2a, + // count: 0 + 0x00, + // seed + 0x1b, 0x52, 0xa8, 0x78, 0x3, 0x85, 0x2c, 0xaa, 0x49, + + // version + 0x00, + // flag: root + map data + 0x88, + + // the following encoded data is valid CBOR + + // elements (array of 3 elements) + 0x83, + + // level: 0 + 0x00, + + // hkeys (byte string of length 8 * 0) + 0x5b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + + // elements (array of 0 elements) + 0x9b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + }, + + rootID2: { + // extra data + // version + 0x00, + // flag: root + map data + 0x88, + // extra data (CBOR encoded array of 3 elements) + 0x83, + // type info + 0x18, 0x2a, + // count: 0 + 0x00, + // seed + 0x1b, 0x52, 0xa8, 0x78, 0x3, 0x85, 0x2c, 0xaa, 0x4a, + + // version + 0x00, + // flag: root + map data + 0x88, + + // the following encoded data is valid CBOR + + // elements (array of 3 elements) + 0x83, + + // level: 0 + 0x00, + + // hkeys (byte string of length 8 * 0) + 0x5b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + + // elements (array of 0 elements) + 0x9b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + }, + } + + storage := newTestPersistentStorageWithData(t, data) + + // Load data in storage + for id := range data { + _, found, err := storage.Retrieve(id) + require.NoError(t, err) + require.True(t, found) + } + + // Check health before fixing broken reference + _, err := CheckStorageHealth(storage, -1) + require.ErrorContains(t, err, "slab not found during slab iteration") + + var fixedRootIDs map[StorageID][]StorageID + var skippedRootIDs map[StorageID][]StorageID + + // Don't fix any broken references + fixedRootIDs, skippedRootIDs, err = storage.FixLoadedBrokenReferences(func(_ Value) bool { + return false + }) + require.NoError(t, err) + require.Equal(t, 0, len(fixedRootIDs)) + require.Equal(t, len(brokenRefs), len(skippedRootIDs)) + + for rootID, slabIDsWithBrokenRef := range brokenRefs { + require.ElementsMatch(t, slabIDsWithBrokenRef, skippedRootIDs[rootID]) + } + + // No data is modified because no fix happened + require.Equal(t, 0, len(storage.deltas)) + + // Only fix one map with broken reference + fixedRootIDs, skippedRootIDs, err = storage.FixLoadedBrokenReferences(func(v Value) bool { + m, ok := v.(*OrderedMap) + require.True(t, ok) + return rootID1 == m.StorageID() + }) + require.NoError(t, err) + require.Equal(t, 1, len(fixedRootIDs)) + require.Equal(t, brokenRefs[rootID1], fixedRootIDs[rootID1]) + require.Equal(t, 1, len(skippedRootIDs)) + require.Equal(t, brokenRefs[rootID2], skippedRootIDs[rootID2]) + require.Equal(t, 3, len(storage.deltas)) + + // Save data in storage + err = storage.FastCommit(runtime.NumCPU()) + require.NoError(t, err) + require.Equal(t, 0, len(storage.deltas)) + + // Check health after only fixing one map with broken reference + _, err = CheckStorageHealth(storage, -1) + require.ErrorContains(t, err, "slab not found during slab iteration") + + // Fix remaining map with broken reference + fixedRootIDs, skippedRootIDs, err = storage.FixLoadedBrokenReferences(func(v Value) bool { + return true + }) + require.NoError(t, err) + require.Equal(t, 1, len(fixedRootIDs)) + require.Equal(t, brokenRefs[rootID2], fixedRootIDs[rootID2]) + require.Equal(t, 0, len(skippedRootIDs)) + require.Equal(t, 1, len(storage.deltas)) + + // Check health after fixing remaining maps with broken reference + returnedRootIDs, err := CheckStorageHealth(storage, -1) + require.NoError(t, err) + require.Equal(t, len(rootIDs), len(returnedRootIDs)) + + // Save data in storage + err = storage.FastCommit(runtime.NumCPU()) + require.NoError(t, err) + require.Equal(t, 0, len(storage.deltas)) + + // Check encoded data + baseStorage := storage.baseStorage.(*InMemBaseStorage) + require.Equal(t, 2, len(baseStorage.segments)) + + savedData, found, err := baseStorage.Retrieve(rootID1) + require.NoError(t, err) + require.True(t, found) + require.Equal(t, fixedData[rootID1], savedData) + + savedData, found, err = baseStorage.Retrieve(rootID2) + require.NoError(t, err) + require.True(t, found) + require.Equal(t, fixedData[rootID2], savedData) + }) }