github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/storage/pebble/bootstrap_test.go (about) 1 package pebble 2 3 import ( 4 "context" 5 "encoding/binary" 6 "fmt" 7 "io" 8 "os" 9 "path" 10 "testing" 11 12 "github.com/cockroachdb/pebble" 13 "github.com/rs/zerolog" 14 "github.com/stretchr/testify/require" 15 16 "github.com/onflow/flow-go/ledger" 17 "github.com/onflow/flow-go/ledger/common/convert" 18 "github.com/onflow/flow-go/ledger/common/testutils" 19 "github.com/onflow/flow-go/ledger/complete/mtrie/trie" 20 "github.com/onflow/flow-go/ledger/complete/wal" 21 "github.com/onflow/flow-go/model/flow" 22 "github.com/onflow/flow-go/utils/unittest" 23 ) 24 25 const defaultRegisterValue = byte('v') 26 27 func TestRegisterBootstrap_NewBootstrap(t *testing.T) { 28 t.Parallel() 29 unittest.RunWithTempDir(t, func(dir string) { 30 rootHeight := uint64(1) 31 rootHash := ledger.RootHash(unittest.StateCommitmentFixture()) 32 log := zerolog.New(io.Discard) 33 p, err := OpenRegisterPebbleDB(dir) 34 require.NoError(t, err) 35 // set heights 36 require.NoError(t, initHeights(p, rootHeight)) 37 // errors if FirstHeight or LastHeight are populated 38 _, err = NewRegisterBootstrap(p, dir, rootHeight, rootHash, log) 39 require.ErrorIs(t, err, ErrAlreadyBootstrapped) 40 }) 41 } 42 43 func TestRegisterBootstrap_IndexCheckpointFile_Happy(t *testing.T) { 44 t.Parallel() 45 log := zerolog.New(io.Discard) 46 rootHeight := uint64(10000) 47 unittest.RunWithTempDir(t, func(dir string) { 48 tries, registerIDs := simpleTrieWithValidRegisterIDs(t) 49 rootHash := tries[0].RootHash() 50 fileName := "simple-checkpoint" 51 require.NoErrorf(t, wal.StoreCheckpointV6Concurrently(tries, dir, fileName, log), "fail to store checkpoint") 52 checkpointFile := path.Join(dir, fileName) 53 pb, dbDir := createPebbleForTest(t) 54 55 bootstrap, err := NewRegisterBootstrap(pb, checkpointFile, rootHeight, rootHash, log) 56 require.NoError(t, err) 57 err = bootstrap.IndexCheckpointFile(context.Background(), workerCount) 58 require.NoError(t, err) 59 60 // create registers instance and check values 61 reg, err := NewRegisters(pb) 62 require.NoError(t, err) 63 64 require.Equal(t, reg.LatestHeight(), rootHeight) 65 require.Equal(t, reg.FirstHeight(), rootHeight) 66 67 for _, register := range registerIDs { 68 val, err := reg.Get(*register, rootHeight) 69 require.NoError(t, err) 70 require.Equal(t, val, []byte{defaultRegisterValue}) 71 } 72 73 require.NoError(t, pb.Close()) 74 require.NoError(t, os.RemoveAll(dbDir)) 75 }) 76 } 77 78 func TestRegisterBootstrap_IndexCheckpointFile_Empty(t *testing.T) { 79 t.Parallel() 80 log := zerolog.New(io.Discard) 81 rootHeight := uint64(10000) 82 unittest.RunWithTempDir(t, func(dir string) { 83 tries := []*trie.MTrie{trie.NewEmptyMTrie()} 84 rootHash := tries[0].RootHash() 85 fileName := "empty-checkpoint" 86 require.NoErrorf(t, wal.StoreCheckpointV6Concurrently(tries, dir, fileName, log), "fail to store checkpoint") 87 checkpointFile := path.Join(dir, fileName) 88 pb, dbDir := createPebbleForTest(t) 89 90 bootstrap, err := NewRegisterBootstrap(pb, checkpointFile, rootHeight, rootHash, log) 91 require.NoError(t, err) 92 err = bootstrap.IndexCheckpointFile(context.Background(), workerCount) 93 require.NoError(t, err) 94 95 // create registers instance and check values 96 reg, err := NewRegisters(pb) 97 require.NoError(t, err) 98 99 require.Equal(t, reg.LatestHeight(), rootHeight) 100 require.Equal(t, reg.FirstHeight(), rootHeight) 101 102 require.NoError(t, pb.Close()) 103 require.NoError(t, os.RemoveAll(dbDir)) 104 }) 105 } 106 107 func TestRegisterBootstrap_IndexCheckpointFile_FormatIssue(t *testing.T) { 108 t.Parallel() 109 pa1 := testutils.PathByUint8(0) 110 pa2 := testutils.PathByUint8(1) 111 rootHeight := uint64(666) 112 pl1 := testutils.LightPayload8('A', 'A') 113 pl2 := testutils.LightPayload('B', 'B') 114 paths := []ledger.Path{pa1, pa2} 115 payloads := []ledger.Payload{*pl1, *pl2} 116 emptyTrie := trie.NewEmptyMTrie() 117 trieWithInvalidEntry, _, err := trie.NewTrieWithUpdatedRegisters(emptyTrie, paths, payloads, true) 118 require.NoError(t, err) 119 rootHash := trieWithInvalidEntry.RootHash() 120 log := zerolog.New(io.Discard) 121 122 unittest.RunWithTempDir(t, func(dir string) { 123 fileName := "invalid-checkpoint" 124 require.NoErrorf(t, wal.StoreCheckpointV6Concurrently([]*trie.MTrie{trieWithInvalidEntry}, dir, fileName, log), 125 "fail to store checkpoint") 126 checkpointFile := path.Join(dir, fileName) 127 pb, dbDir := createPebbleForTest(t) 128 129 bootstrap, err := NewRegisterBootstrap(pb, checkpointFile, rootHeight, rootHash, log) 130 require.NoError(t, err) 131 err = bootstrap.IndexCheckpointFile(context.Background(), workerCount) 132 require.ErrorContains(t, err, "unexpected ledger key format") 133 require.NoError(t, pb.Close()) 134 require.NoError(t, os.RemoveAll(dbDir)) 135 }) 136 137 } 138 139 func TestRegisterBootstrap_IndexCheckpointFile_CorruptedCheckpointFile(t *testing.T) { 140 t.Parallel() 141 rootHeight := uint64(666) 142 log := zerolog.New(io.Discard) 143 unittest.RunWithTempDir(t, func(dir string) { 144 tries, _ := largeTrieWithValidRegisterIDs(t) 145 rootHash := tries[0].RootHash() 146 checkpointFileName := "large-checkpoint-incomplete" 147 require.NoErrorf(t, wal.StoreCheckpointV6Concurrently(tries, dir, checkpointFileName, log), "fail to store checkpoint") 148 // delete 2nd part of the file (2nd subtrie) 149 fileToDelete := path.Join(dir, fmt.Sprintf("%v.%03d", checkpointFileName, 2)) 150 err := os.RemoveAll(fileToDelete) 151 require.NoError(t, err) 152 pb, dbDir := createPebbleForTest(t) 153 bootstrap, err := NewRegisterBootstrap(pb, checkpointFileName, rootHeight, rootHash, log) 154 require.NoError(t, err) 155 err = bootstrap.IndexCheckpointFile(context.Background(), workerCount) 156 require.ErrorIs(t, err, os.ErrNotExist) 157 require.NoError(t, os.RemoveAll(dbDir)) 158 }) 159 } 160 161 func TestRegisterBootstrap_IndexCheckpointFile_MultipleBatch(t *testing.T) { 162 t.Parallel() 163 log := zerolog.New(io.Discard) 164 rootHeight := uint64(10000) 165 unittest.RunWithTempDir(t, func(dir string) { 166 tries, registerIDs := largeTrieWithValidRegisterIDs(t) 167 rootHash := tries[0].RootHash() 168 fileName := "large-checkpoint" 169 require.NoErrorf(t, wal.StoreCheckpointV6Concurrently(tries, dir, fileName, log), "fail to store checkpoint") 170 checkpointFile := path.Join(dir, fileName) 171 pb, dbDir := createPebbleForTest(t) 172 bootstrap, err := NewRegisterBootstrap(pb, checkpointFile, rootHeight, rootHash, log) 173 require.NoError(t, err) 174 err = bootstrap.IndexCheckpointFile(context.Background(), workerCount) 175 require.NoError(t, err) 176 177 // create registers instance and check values 178 reg, err := NewRegisters(pb) 179 require.NoError(t, err) 180 181 require.Equal(t, reg.LatestHeight(), rootHeight) 182 require.Equal(t, reg.FirstHeight(), rootHeight) 183 184 for _, register := range registerIDs { 185 val, err := reg.Get(*register, rootHeight) 186 require.NoError(t, err) 187 require.Equal(t, val, []byte{defaultRegisterValue}) 188 } 189 190 require.NoError(t, pb.Close()) 191 require.NoError(t, os.RemoveAll(dbDir)) 192 }) 193 194 } 195 196 func simpleTrieWithValidRegisterIDs(t *testing.T) ([]*trie.MTrie, []*flow.RegisterID) { 197 return trieWithValidRegisterIDs(t, 2) 198 } 199 200 const workerCount = 10 201 202 func largeTrieWithValidRegisterIDs(t *testing.T) ([]*trie.MTrie, []*flow.RegisterID) { 203 // large enough trie so every worker should have something to index 204 largeTrieSize := 2 * pebbleBootstrapRegisterBatchLen * workerCount 205 return trieWithValidRegisterIDs(t, uint16(largeTrieSize)) 206 } 207 208 func trieWithValidRegisterIDs(t *testing.T, n uint16) ([]*trie.MTrie, []*flow.RegisterID) { 209 emptyTrie := trie.NewEmptyMTrie() 210 resultRegisterIDs := make([]*flow.RegisterID, 0, n) 211 paths := randomRegisterPaths(n) 212 payloads := randomRegisterPayloads(n) 213 for _, payload := range payloads { 214 key, err := payload.Key() 215 require.NoError(t, err) 216 regID, err := convert.LedgerKeyToRegisterID(key) 217 require.NoError(t, err) 218 resultRegisterIDs = append(resultRegisterIDs, ®ID) 219 } 220 populatedTrie, depth, err := trie.NewTrieWithUpdatedRegisters(emptyTrie, paths, payloads, true) 221 // make sure it has at least 1 leaf node 222 require.GreaterOrEqual(t, depth, uint16(1)) 223 require.NoError(t, err) 224 resultTries := []*trie.MTrie{emptyTrie, populatedTrie} 225 return resultTries, resultRegisterIDs 226 } 227 228 func randomRegisterPayloads(n uint16) []ledger.Payload { 229 p := make([]ledger.Payload, 0, n) 230 for i := uint16(0); i < n; i++ { 231 o := make([]byte, 0, 8) 232 o = binary.BigEndian.AppendUint16(o, n) 233 k := ledger.Key{KeyParts: []ledger.KeyPart{ 234 {Type: ledger.KeyPartOwner, Value: o}, 235 {Type: ledger.KeyPartKey, Value: o}, 236 }} 237 // values are always 'v' for ease of testing/checking 238 v := ledger.Value{defaultRegisterValue} 239 pl := ledger.NewPayload(k, v) 240 p = append(p, *pl) 241 } 242 return p 243 } 244 245 func randomRegisterPaths(n uint16) []ledger.Path { 246 p := make([]ledger.Path, 0, n) 247 for i := uint16(0); i < n; i++ { 248 p = append(p, testutils.PathByUint16(i)) 249 } 250 return p 251 } 252 253 func createPebbleForTest(t *testing.T) (*pebble.DB, string) { 254 dbDir := unittest.TempPebblePath(t) 255 pb, err := OpenRegisterPebbleDB(dbDir) 256 require.NoError(t, err) 257 return pb, dbDir 258 }