diff --git a/runtime/storage.go b/runtime/storage.go index 8198e69fa..49aaa92b4 100644 --- a/runtime/storage.go +++ b/runtime/storage.go @@ -69,11 +69,10 @@ type Storage struct { var _ atree.SlabStorage = &Storage{} var _ interpreter.Storage = &Storage{} -func NewStorage( +func NewPersistentSlabStorage( ledger atree.Ledger, memoryGauge common.MemoryGauge, - config StorageConfig, -) *Storage { +) *atree.PersistentSlabStorage { decodeStorable := func( decoder *cbor.StreamDecoder, slabID atree.SlabID, @@ -95,13 +94,22 @@ func NewStorage( } ledgerStorage := atree.NewLedgerBaseStorage(ledger) - persistentSlabStorage := atree.NewPersistentSlabStorage( + + return atree.NewPersistentSlabStorage( ledgerStorage, interpreter.CBOREncMode, interpreter.CBORDecMode, decodeStorable, decodeTypeInfo, ) +} + +func NewStorage( + ledger atree.Ledger, + memoryGauge common.MemoryGauge, + config StorageConfig, +) *Storage { + persistentSlabStorage := NewPersistentSlabStorage(ledger, memoryGauge) accountStorageV1 := NewAccountStorageV1( ledger, diff --git a/runtime/storage_test.go b/runtime/storage_test.go index 4e4dc261b..f0bbaef75 100644 --- a/runtime/storage_test.go +++ b/runtime/storage_test.go @@ -23,6 +23,7 @@ import ( "encoding/hex" "fmt" "math/rand" + "runtime" "slices" "sort" "strconv" @@ -8081,6 +8082,940 @@ func TestDomainRegisterMigrationForLargeAccount(t *testing.T) { checkAccountStorageMapData(t, ledger.StoredValues, ledger.StorageIndices, address, accountValues) } +func TestGetDomainStorageMapRegisterReadsForNewAccount(t *testing.T) { + t.Parallel() + + address := common.MustBytesToAddress([]byte{0x1}) + + testCases := []struct { + name string + storageFormatV2Enabled bool + domain common.StorageDomain + createIfNotExists bool + expectedDomainStorageMapIsNil bool + expectedReadsFor1stGetDomainStorageMapCall []ownerKeyPair + expectedReadsFor2ndGetDomainStorageMapCall []ownerKeyPair + expectedReadsSet map[string]struct{} + }{ + // Test cases with storageFormatV2Enabled = false + { + name: "storageFormatV2Enabled = false, domain storage map does not exist, createIfNotExists = false", + storageFormatV2Enabled: false, + domain: common.StorageDomainPathStorage, + createIfNotExists: false, + expectedDomainStorageMapIsNil: true, + expectedReadsFor1stGetDomainStorageMapCall: []ownerKeyPair{ + // Read domain register + { + owner: address[:], + key: []byte(common.StorageDomainPathStorage.Identifier()), + }, + }, + expectedReadsFor2ndGetDomainStorageMapCall: []ownerKeyPair{ + // Read domain register + { + owner: address[:], + key: []byte(common.StorageDomainPathStorage.Identifier()), + }, + }, + expectedReadsSet: map[string]struct{}{ + concatRegisterAddressAndDomain(address, common.StorageDomainPathStorage): {}, + }, + }, + { + name: "storageFormatV2Enabled = false, domain storage map does not exist, createIfNotExists = true", + storageFormatV2Enabled: false, + domain: common.StorageDomainPathStorage, + createIfNotExists: true, + expectedDomainStorageMapIsNil: false, + expectedReadsFor1stGetDomainStorageMapCall: []ownerKeyPair{ + // Read domain register + { + owner: address[:], + key: []byte(common.StorageDomainPathStorage.Identifier()), + }, + }, + expectedReadsFor2ndGetDomainStorageMapCall: []ownerKeyPair{ + // No register reads from the second GetDomainStorageMap() because + // domain storage map is created and cached in the first GetDomainStorageMap(). + }, + expectedReadsSet: map[string]struct{}{ + concatRegisterAddressAndDomain(address, common.StorageDomainPathStorage): {}, + }, + }, + // Test cases with storageFormatV2Enabled = true + { + name: "storageFormatV2Enabled = true, domain storage map does not exist, createIfNotExists = false", + storageFormatV2Enabled: true, + domain: common.StorageDomainPathStorage, + createIfNotExists: false, + expectedDomainStorageMapIsNil: true, + expectedReadsFor1stGetDomainStorageMapCall: []ownerKeyPair{ + // Check if account is v2 + { + owner: address[:], + key: []byte(AccountStorageKey), + }, + // Read all available domain registers to check if it is a new account + // Read returns no value. + { + owner: address[:], + key: []byte(common.PathDomainStorage.Identifier()), + }, + { + owner: address[:], + key: []byte(common.PathDomainPrivate.Identifier()), + }, + { + owner: address[:], + key: []byte(common.PathDomainPublic.Identifier()), + }, + { + owner: address[:], + key: []byte(common.StorageDomainContract.Identifier()), + }, + { + owner: address[:], + key: []byte(common.StorageDomainInbox.Identifier()), + }, + { + owner: address[:], + key: []byte(common.StorageDomainCapabilityController.Identifier()), + }, + { + owner: address[:], + key: []byte(common.StorageDomainCapabilityControllerTag.Identifier()), + }, + { + owner: address[:], + key: []byte(common.StorageDomainPathCapability.Identifier()), + }, + { + owner: address[:], + key: []byte(common.StorageDomainAccountCapability.Identifier()), + }, + // Try to read account register to create account storage map + { + owner: address[:], + key: []byte(AccountStorageKey), + }, + }, + expectedReadsFor2ndGetDomainStorageMapCall: []ownerKeyPair{ + // Second GetDomainStorageMap() get cached account format v2 (cached during first GetDomainStorageMap()). + + // Try to read account register to create account storage map + { + owner: address[:], + key: []byte(AccountStorageKey), + }, + }, + expectedReadsSet: map[string]struct{}{ + concatRegisterAddressAndKey(address, []byte(AccountStorageKey)): {}, + concatRegisterAddressAndDomain(address, common.StorageDomainPathStorage): {}, + concatRegisterAddressAndDomain(address, common.StorageDomainPathPrivate): {}, + concatRegisterAddressAndDomain(address, common.StorageDomainPathPublic): {}, + concatRegisterAddressAndDomain(address, common.StorageDomainContract): {}, + concatRegisterAddressAndDomain(address, common.StorageDomainInbox): {}, + concatRegisterAddressAndDomain(address, common.StorageDomainCapabilityController): {}, + concatRegisterAddressAndDomain(address, common.StorageDomainCapabilityControllerTag): {}, + concatRegisterAddressAndDomain(address, common.StorageDomainPathCapability): {}, + concatRegisterAddressAndDomain(address, common.StorageDomainAccountCapability): {}, + }, + }, + { + name: "storageFormatV2Enabled = true, domain storage map does not exist, createIfNotExists = true", + storageFormatV2Enabled: true, + domain: common.StorageDomainPathStorage, + createIfNotExists: true, + expectedDomainStorageMapIsNil: false, + expectedReadsFor1stGetDomainStorageMapCall: []ownerKeyPair{ + // Check if account is v2 + { + owner: address[:], + key: []byte(AccountStorageKey), + }, + // Check all domain registers + { + owner: address[:], + key: []byte(common.PathDomainStorage.Identifier()), + }, + { + owner: address[:], + key: []byte(common.PathDomainPrivate.Identifier()), + }, + { + owner: address[:], + key: []byte(common.PathDomainPublic.Identifier()), + }, + { + owner: address[:], + key: []byte(common.StorageDomainContract.Identifier()), + }, + { + owner: address[:], + key: []byte(common.StorageDomainInbox.Identifier()), + }, + { + owner: address[:], + key: []byte(common.StorageDomainCapabilityController.Identifier()), + }, + { + owner: address[:], + key: []byte(common.StorageDomainCapabilityControllerTag.Identifier()), + }, + { + owner: address[:], + key: []byte(common.StorageDomainPathCapability.Identifier()), + }, + { + owner: address[:], + key: []byte(common.StorageDomainAccountCapability.Identifier()), + }, + // Read account register to load account storage map + { + owner: address[:], + key: []byte(AccountStorageKey), + }, + }, + expectedReadsFor2ndGetDomainStorageMapCall: []ownerKeyPair{ + // No register reads from the second GetDomainStorageMap() because + // domain storage map is created and cached in the first GetDomainStorageMap(). + }, + expectedReadsSet: map[string]struct{}{ + concatRegisterAddressAndKey(address, []byte(AccountStorageKey)): {}, + concatRegisterAddressAndDomain(address, common.StorageDomainPathStorage): {}, + concatRegisterAddressAndDomain(address, common.StorageDomainPathPrivate): {}, + concatRegisterAddressAndDomain(address, common.StorageDomainPathPublic): {}, + concatRegisterAddressAndDomain(address, common.StorageDomainContract): {}, + concatRegisterAddressAndDomain(address, common.StorageDomainInbox): {}, + concatRegisterAddressAndDomain(address, common.StorageDomainCapabilityController): {}, + concatRegisterAddressAndDomain(address, common.StorageDomainCapabilityControllerTag): {}, + concatRegisterAddressAndDomain(address, common.StorageDomainPathCapability): {}, + concatRegisterAddressAndDomain(address, common.StorageDomainAccountCapability): {}, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + + var ledgerReads []ownerKeyPair + ledgerReadsSet := make(map[string]struct{}) + + // Create empty storage + ledger := NewTestLedger( + func(owner, key, _ []byte) { + ledgerReads = append( + ledgerReads, + ownerKeyPair{ + owner: owner, + key: key, + }, + ) + ledgerReadsSet[string(owner)+"|"+string(key)] = struct{}{} + }, + nil) + + storage := NewStorage( + ledger, + nil, + StorageConfig{ + StorageFormatV2Enabled: tc.storageFormatV2Enabled, + }, + ) + + inter := NewTestInterpreterWithStorage(t, storage) + + domainStorageMap := storage.GetDomainStorageMap(inter, address, tc.domain, tc.createIfNotExists) + require.Equal(t, tc.expectedDomainStorageMapIsNil, domainStorageMap == nil) + require.Equal(t, tc.expectedReadsFor1stGetDomainStorageMapCall, ledgerReads) + + ledgerReads = ledgerReads[:0] + + // Call GetDomainStorageMap() again to test account status is cached and no register reading is needed. + + domainStorageMap = storage.GetDomainStorageMap(inter, address, tc.domain, tc.createIfNotExists) + require.Equal(t, tc.expectedDomainStorageMapIsNil, domainStorageMap == nil) + require.Equal(t, tc.expectedReadsFor2ndGetDomainStorageMapCall, ledgerReads) + + // Check underlying ledger reads + require.Equal(t, len(ledgerReadsSet), len(tc.expectedReadsSet)) + for k := range ledgerReadsSet { + require.Contains(t, tc.expectedReadsSet, k) + } + }) + } +} + +func TestGetDomainStorageMapRegisterReadsForV1Account(t *testing.T) { + + t.Parallel() + + address := common.MustBytesToAddress([]byte{0x1}) + + type getStorageDataFunc func() (storedValues map[string][]byte, StorageIndices map[string]uint64) + + createV1AccountWithDomain := func( + address common.Address, + domain common.StorageDomain, + ) getStorageDataFunc { + return func() (storedValues map[string][]byte, StorageIndices map[string]uint64) { + ledger := NewTestLedger(nil, nil) + + persistentSlabStorage := NewPersistentSlabStorage(ledger, nil) + + orderedMap, err := atree.NewMap( + persistentSlabStorage, + atree.Address(address), + atree.NewDefaultDigesterBuilder(), + interpreter.EmptyTypeInfo{}, + ) + require.NoError(t, err) + + slabIndex := orderedMap.SlabID().Index() + + for i := range 3 { + + key := interpreter.StringStorageMapKey(strconv.Itoa(i)) + + value := interpreter.NewUnmeteredIntValueFromInt64(int64(i)) + + existingStorable, err := orderedMap.Set( + key.AtreeValueCompare, + key.AtreeValueHashInput, + key.AtreeValue(), + value, + ) + require.NoError(t, err) + require.Nil(t, existingStorable) + } + + // Commit domain storage map + err = persistentSlabStorage.FastCommit(runtime.NumCPU()) + require.NoError(t, err) + + // Create domain register + err = ledger.SetValue(address[:], []byte(domain.Identifier()), slabIndex[:]) + require.NoError(t, err) + + return ledger.StoredValues, ledger.StorageIndices + } + } + + testCases := []struct { + name string + getStorageData getStorageDataFunc + storageFormatV2Enabled bool + domain common.StorageDomain + createIfNotExists bool + expectedDomainStorageMapIsNil bool + expectedReadsFor1stGetDomainStorageMapCall []ownerKeyPair + expectedReadsFor2ndGetDomainStorageMapCall []ownerKeyPair + expectedReadsSet map[string]struct{} + }{ + // Test cases with storageFormatV2Enabled = false + { + name: "storageFormatV2Enabled = false, domain storage map does not exist, createIfNotExists = false", + storageFormatV2Enabled: false, + getStorageData: createV1AccountWithDomain(address, common.StorageDomainPathPublic), + domain: common.StorageDomainPathStorage, + createIfNotExists: false, + expectedDomainStorageMapIsNil: true, + expectedReadsFor1stGetDomainStorageMapCall: []ownerKeyPair{ + // Read domain register + { + owner: address[:], + key: []byte(common.StorageDomainPathStorage.Identifier()), + }, + }, + expectedReadsFor2ndGetDomainStorageMapCall: []ownerKeyPair{ + // Read domain register + { + owner: address[:], + key: []byte(common.StorageDomainPathStorage.Identifier()), + }, + }, + expectedReadsSet: map[string]struct{}{ + concatRegisterAddressAndDomain(address, common.StorageDomainPathStorage): {}, + }, + }, + { + name: "storageFormatV2Enabled = false, domain storage map does not exist, createIfNotExists = true", + storageFormatV2Enabled: false, + getStorageData: createV1AccountWithDomain(address, common.StorageDomainPathPublic), + domain: common.StorageDomainPathStorage, + createIfNotExists: true, + expectedDomainStorageMapIsNil: false, + expectedReadsFor1stGetDomainStorageMapCall: []ownerKeyPair{ + // Read domain register + { + owner: address[:], + key: []byte(common.StorageDomainPathStorage.Identifier()), + }, + }, + expectedReadsFor2ndGetDomainStorageMapCall: []ownerKeyPair{ + // No register reading in second GetDomainStorageMap() because + // domain storage map is created and cached in the first + // GetDomainStorageMap(0). + }, + expectedReadsSet: map[string]struct{}{ + concatRegisterAddressAndDomain(address, common.StorageDomainPathStorage): {}, + }, + }, + { + name: "storageFormatV2Enabled = false, domain storage map exists, createIfNotExists = false", + storageFormatV2Enabled: false, + getStorageData: createV1AccountWithDomain(address, common.StorageDomainPathStorage), + domain: common.StorageDomainPathStorage, + createIfNotExists: false, + expectedDomainStorageMapIsNil: false, + expectedReadsFor1stGetDomainStorageMapCall: []ownerKeyPair{ + // Read domain register + { + owner: address[:], + key: []byte(common.StorageDomainPathStorage.Identifier()), + }, + // Read domain storage map register + { + owner: address[:], + key: []byte{'$', 0, 0, 0, 0, 0, 0, 0, 1}, + }, + }, + expectedReadsFor2ndGetDomainStorageMapCall: []ownerKeyPair{ + // No register reading in second GetDomainStorageMap() because + // domain storage map is loaded and cached in the first + // GetDomainStorageMap(0). + }, + expectedReadsSet: map[string]struct{}{ + concatRegisterAddressAndDomain(address, common.StorageDomainPathStorage): {}, + concatRegisterAddressAndKey(address, []byte{'$', 0, 0, 0, 0, 0, 0, 0, 1}): {}, + }, + }, + { + name: "storageFormatV2Enabled = false, domain storage map exists, createIfNotExists = true", + storageFormatV2Enabled: false, + getStorageData: createV1AccountWithDomain(address, common.StorageDomainPathStorage), + domain: common.StorageDomainPathStorage, + createIfNotExists: true, + expectedDomainStorageMapIsNil: false, + expectedReadsFor1stGetDomainStorageMapCall: []ownerKeyPair{ + // Read domain register + { + owner: address[:], + key: []byte(common.StorageDomainPathStorage.Identifier()), + }, + // Read domain storage map register + { + owner: address[:], + key: []byte{'$', 0, 0, 0, 0, 0, 0, 0, 1}, + }, + }, + expectedReadsFor2ndGetDomainStorageMapCall: []ownerKeyPair{ + // No register reading in second GetDomainStorageMap() because + // domain storage map is loaded and cached in the first + // GetDomainStorageMap(0). + }, + expectedReadsSet: map[string]struct{}{ + concatRegisterAddressAndDomain(address, common.StorageDomainPathStorage): {}, + concatRegisterAddressAndKey(address, []byte{'$', 0, 0, 0, 0, 0, 0, 0, 1}): {}, + }, + }, + // Test cases with storageFormatV2Enabled = true + { + name: "storageFormatV2Enabled = true, domain storage map does not exist, createIfNotExists = false", + storageFormatV2Enabled: true, + getStorageData: createV1AccountWithDomain(address, common.StorageDomainPathPublic), + domain: common.StorageDomainPathStorage, + createIfNotExists: false, + expectedDomainStorageMapIsNil: true, + expectedReadsFor1stGetDomainStorageMapCall: []ownerKeyPair{ + // Check if account is v2 + { + owner: address[:], + key: []byte(AccountStorageKey), + }, + // Check all domain registers until existing domain register is read + { + owner: address[:], + key: []byte(common.PathDomainStorage.Identifier()), + }, + { + owner: address[:], + key: []byte(common.PathDomainPrivate.Identifier()), + }, + { + owner: address[:], + key: []byte(common.PathDomainPublic.Identifier()), + }, + // Read requested domain register + { + owner: address[:], + key: []byte(common.StorageDomainPathStorage.Identifier()), + }, + }, + expectedReadsFor2ndGetDomainStorageMapCall: []ownerKeyPair{ + // Read requested domain register + { + owner: address[:], + key: []byte(common.StorageDomainPathStorage.Identifier()), + }, + }, + expectedReadsSet: map[string]struct{}{ + concatRegisterAddressAndKey(address, []byte(AccountStorageKey)): {}, + concatRegisterAddressAndDomain(address, common.StorageDomainPathStorage): {}, + concatRegisterAddressAndDomain(address, common.StorageDomainPathPrivate): {}, + concatRegisterAddressAndDomain(address, common.StorageDomainPathPublic): {}, + }, + }, + { + name: "storageFormatV2Enabled = true, domain storage map does not exist, createIfNotExists = true", + storageFormatV2Enabled: true, + getStorageData: createV1AccountWithDomain(address, common.StorageDomainPathPublic), + domain: common.StorageDomainPathStorage, + createIfNotExists: true, + expectedDomainStorageMapIsNil: false, + expectedReadsFor1stGetDomainStorageMapCall: []ownerKeyPair{ + // Check if account is v2 + { + owner: address[:], + key: []byte(AccountStorageKey), + }, + // Check all domain registers until existing domain register is read + { + owner: address[:], + key: []byte(common.PathDomainStorage.Identifier()), + }, + { + owner: address[:], + key: []byte(common.PathDomainPrivate.Identifier()), + }, + { + owner: address[:], + key: []byte(common.PathDomainPublic.Identifier()), + }, + // Read requested domain register + { + owner: address[:], + key: []byte(common.StorageDomainPathStorage.Identifier()), + }, + }, + expectedReadsFor2ndGetDomainStorageMapCall: []ownerKeyPair{ + // No register reading from second GetDomainStorageMap() because + // domain storage map is created and cached in the first + // GetDomainStorageMap(). + }, + expectedReadsSet: map[string]struct{}{ + concatRegisterAddressAndKey(address, []byte(AccountStorageKey)): {}, + concatRegisterAddressAndDomain(address, common.StorageDomainPathStorage): {}, + concatRegisterAddressAndDomain(address, common.StorageDomainPathPrivate): {}, + concatRegisterAddressAndDomain(address, common.StorageDomainPathPublic): {}, + }, + }, + { + name: "storageFormatV2Enabled = true, domain storage map exists, createIfNotExists = false", + storageFormatV2Enabled: true, + getStorageData: createV1AccountWithDomain(address, common.StorageDomainPathStorage), + domain: common.StorageDomainPathStorage, + createIfNotExists: false, + expectedDomainStorageMapIsNil: false, + expectedReadsFor1stGetDomainStorageMapCall: []ownerKeyPair{ + // Check if account is v2 + { + owner: address[:], + key: []byte(AccountStorageKey), + }, + // Check domain register + { + owner: address[:], + key: []byte(common.StorageDomainPathStorage.Identifier()), + }, + // Read domain register + { + owner: address[:], + key: []byte(common.StorageDomainPathStorage.Identifier()), + }, + // Read domain storage map register + { + owner: address[:], + key: []byte{'$', 0, 0, 0, 0, 0, 0, 0, 1}, + }, + }, + expectedReadsFor2ndGetDomainStorageMapCall: []ownerKeyPair{ + // No register reading from second GetDomainStorageMap() because + // domain storage map is created and cached in the first + // GetDomainStorageMap(). + }, + expectedReadsSet: map[string]struct{}{ + concatRegisterAddressAndKey(address, []byte(AccountStorageKey)): {}, + concatRegisterAddressAndDomain(address, common.StorageDomainPathStorage): {}, + concatRegisterAddressAndKey(address, []byte{'$', 0, 0, 0, 0, 0, 0, 0, 1}): {}, + }, + }, + { + name: "storageFormatV2Enabled = true, domain storage map exists, createIfNotExists = true", + storageFormatV2Enabled: true, + getStorageData: createV1AccountWithDomain(address, common.StorageDomainPathStorage), + domain: common.StorageDomainPathStorage, + createIfNotExists: true, + expectedDomainStorageMapIsNil: false, + expectedReadsFor1stGetDomainStorageMapCall: []ownerKeyPair{ + // Check if account is v2 + { + owner: address[:], + key: []byte(AccountStorageKey), + }, + // Check given domain register + { + owner: address[:], + key: []byte(common.StorageDomainPathStorage.Identifier()), + }, + // Read given domain register + { + owner: address[:], + key: []byte(common.StorageDomainPathStorage.Identifier()), + }, + // Read domain storage map register + { + owner: address[:], + key: []byte{'$', 0, 0, 0, 0, 0, 0, 0, 1}, + }, + }, + expectedReadsFor2ndGetDomainStorageMapCall: []ownerKeyPair{ + // No register reading from second GetDomainStorageMap() because + // domain storage map is created and cached in the first + // GetDomainStorageMap(). + }, + expectedReadsSet: map[string]struct{}{ + concatRegisterAddressAndKey(address, []byte(AccountStorageKey)): {}, + concatRegisterAddressAndDomain(address, common.StorageDomainPathStorage): {}, + concatRegisterAddressAndKey(address, []byte{'$', 0, 0, 0, 0, 0, 0, 0, 1}): {}, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + + storedValues, storedIndices := tc.getStorageData() + + var ledgerReads []ownerKeyPair + ledgerReadsSet := make(map[string]struct{}) + + ledger := NewTestLedgerWithData( + func(owner, key, _ []byte) { + ledgerReads = append( + ledgerReads, + ownerKeyPair{ + owner: owner, + key: key, + }, + ) + ledgerReadsSet[string(owner)+"|"+string(key)] = struct{}{} + }, + nil, + storedValues, + storedIndices, + ) + + storage := NewStorage( + ledger, + nil, + StorageConfig{ + StorageFormatV2Enabled: tc.storageFormatV2Enabled, + }, + ) + + inter := NewTestInterpreterWithStorage(t, storage) + + domainStorageMap := storage.GetDomainStorageMap(inter, address, tc.domain, tc.createIfNotExists) + require.Equal(t, tc.expectedDomainStorageMapIsNil, domainStorageMap == nil) + require.Equal(t, tc.expectedReadsFor1stGetDomainStorageMapCall, ledgerReads) + + ledgerReads = ledgerReads[:0] + + domainStorageMap = storage.GetDomainStorageMap(inter, address, tc.domain, tc.createIfNotExists) + require.Equal(t, tc.expectedDomainStorageMapIsNil, domainStorageMap == nil) + require.Equal(t, tc.expectedReadsFor2ndGetDomainStorageMapCall, ledgerReads) + + // Check underlying ledger reads + require.Equal(t, len(ledgerReadsSet), len(tc.expectedReadsSet)) + for k := range ledgerReadsSet { + require.Contains(t, tc.expectedReadsSet, k) + } + }) + } +} + +func TestGetDomainStorageMapRegisterReadsForV2Account(t *testing.T) { + t.Parallel() + + address := common.MustBytesToAddress([]byte{0x1}) + + type getStorageDataFunc func() (storedValues map[string][]byte, StorageIndices map[string]uint64) + + createV2AccountWithDomain := func( + address common.Address, + domain common.StorageDomain, + ) getStorageDataFunc { + return func() (storedValues map[string][]byte, StorageIndices map[string]uint64) { + ledger := NewTestLedger(nil, nil) + + persistentSlabStorage := NewPersistentSlabStorage(ledger, nil) + + accountOrderedMap, err := atree.NewMap( + persistentSlabStorage, + atree.Address(address), + atree.NewDefaultDigesterBuilder(), + interpreter.EmptyTypeInfo{}, + ) + require.NoError(t, err) + + slabIndex := accountOrderedMap.SlabID().Index() + + domainOrderedMap, err := atree.NewMap( + persistentSlabStorage, + atree.Address(address), + atree.NewDefaultDigesterBuilder(), + interpreter.EmptyTypeInfo{}, + ) + require.NoError(t, err) + + domainKey := interpreter.Uint64StorageMapKey(domain) + + existingDomain, err := accountOrderedMap.Set( + domainKey.AtreeValueCompare, + domainKey.AtreeValueHashInput, + domainKey.AtreeValue(), + domainOrderedMap, + ) + require.NoError(t, err) + require.Nil(t, existingDomain) + + for i := range 3 { + + key := interpreter.StringStorageMapKey(strconv.Itoa(i)) + + value := interpreter.NewUnmeteredIntValueFromInt64(int64(i)) + + existingStorable, err := domainOrderedMap.Set( + key.AtreeValueCompare, + key.AtreeValueHashInput, + key.AtreeValue(), + value, + ) + require.NoError(t, err) + require.Nil(t, existingStorable) + } + + // Commit domain storage map + err = persistentSlabStorage.FastCommit(runtime.NumCPU()) + require.NoError(t, err) + + // Create account register + err = ledger.SetValue(address[:], []byte(AccountStorageKey), slabIndex[:]) + require.NoError(t, err) + + return ledger.StoredValues, ledger.StorageIndices + } + } + + testCases := []struct { + name string + getStorageData getStorageDataFunc + domain common.StorageDomain + createIfNotExists bool + expectedDomainStorageMapIsNil bool + expectedReadsFor1stGetDomainStorageMapCall []ownerKeyPair + expectedReadsFor2ndGetDomainStorageMapCall []ownerKeyPair + expectedReadsSet map[string]struct{} + }{ + { + name: "domain storage map does not exist, createIfNotExists = false", + getStorageData: createV2AccountWithDomain(address, common.StorageDomainPathPublic), + domain: common.StorageDomainPathStorage, + createIfNotExists: false, + expectedDomainStorageMapIsNil: true, + expectedReadsFor1stGetDomainStorageMapCall: []ownerKeyPair{ + // Check if account is v2 + { + owner: address[:], + key: []byte(AccountStorageKey), + }, + // Read account register + { + owner: address[:], + key: []byte(AccountStorageKey), + }, + // Read account storage map + { + owner: address[:], + key: []byte{'$', 0, 0, 0, 0, 0, 0, 0, 1}, + }, + }, + expectedReadsFor2ndGetDomainStorageMapCall: []ownerKeyPair{ + // No register reading from second GetDomainStorageMap because + // account storage map is loaded and cached from first + // GetDomainStorageMap(). + }, + expectedReadsSet: map[string]struct{}{ + concatRegisterAddressAndKey(address, []byte(AccountStorageKey)): {}, + concatRegisterAddressAndKey(address, []byte{'$', 0, 0, 0, 0, 0, 0, 0, 1}): {}, + }, + }, + { + name: "domain storage map does not exist, createIfNotExists = true", + getStorageData: createV2AccountWithDomain(address, common.StorageDomainPathPublic), + domain: common.StorageDomainPathStorage, + createIfNotExists: true, + expectedDomainStorageMapIsNil: false, + expectedReadsFor1stGetDomainStorageMapCall: []ownerKeyPair{ + // Check if account is v2 + { + owner: address[:], + key: []byte(AccountStorageKey), + }, + // Read account register + { + owner: address[:], + key: []byte(AccountStorageKey), + }, + // Read account storage map + { + owner: address[:], + key: []byte{'$', 0, 0, 0, 0, 0, 0, 0, 1}, + }, + }, + expectedReadsFor2ndGetDomainStorageMapCall: []ownerKeyPair{ + // No register reading from second GetDomainStorageMap() because + // domain storage map is created and cached in the first + // GetDomainStorageMap(). + }, + expectedReadsSet: map[string]struct{}{ + concatRegisterAddressAndKey(address, []byte(AccountStorageKey)): {}, + concatRegisterAddressAndKey(address, []byte{'$', 0, 0, 0, 0, 0, 0, 0, 1}): {}, + }, + }, + { + name: "domain storage map exists, createIfNotExists = false", + getStorageData: createV2AccountWithDomain(address, common.StorageDomainPathStorage), + domain: common.StorageDomainPathStorage, + createIfNotExists: false, + expectedDomainStorageMapIsNil: false, + expectedReadsFor1stGetDomainStorageMapCall: []ownerKeyPair{ + // Check if account is v2 + { + owner: address[:], + key: []byte(AccountStorageKey), + }, + // Read account register + { + owner: address[:], + key: []byte(AccountStorageKey), + }, + // Read account storage map + { + owner: address[:], + key: []byte{'$', 0, 0, 0, 0, 0, 0, 0, 1}, + }, + }, + expectedReadsFor2ndGetDomainStorageMapCall: []ownerKeyPair{ + // No register reading from second GetDomainStorageMap() because + // domain storage map is created and cached in the first + // GetDomainStorageMap(). + }, + expectedReadsSet: map[string]struct{}{ + concatRegisterAddressAndKey(address, []byte(AccountStorageKey)): {}, + concatRegisterAddressAndKey(address, []byte{'$', 0, 0, 0, 0, 0, 0, 0, 1}): {}, + }, + }, + { + name: "domain storage map exists, createIfNotExists = true", + getStorageData: createV2AccountWithDomain(address, common.StorageDomainPathStorage), + domain: common.StorageDomainPathStorage, + createIfNotExists: true, + expectedDomainStorageMapIsNil: false, + expectedReadsFor1stGetDomainStorageMapCall: []ownerKeyPair{ + // Check if account is v2 + { + owner: address[:], + key: []byte(AccountStorageKey), + }, + // Read account register + { + owner: address[:], + key: []byte(AccountStorageKey), + }, + // Read account storage map + { + owner: address[:], + key: []byte{'$', 0, 0, 0, 0, 0, 0, 0, 1}, + }, + }, + expectedReadsFor2ndGetDomainStorageMapCall: []ownerKeyPair{ + // No register reading from second GetDomainStorageMap() because + // domain storage map is created and cached in the first + // GetDomainStorageMap(). + }, + expectedReadsSet: map[string]struct{}{ + concatRegisterAddressAndKey(address, []byte(AccountStorageKey)): {}, + concatRegisterAddressAndKey(address, []byte{'$', 0, 0, 0, 0, 0, 0, 0, 1}): {}, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + + storedValues, storedIndices := tc.getStorageData() + + var ledgerReads []ownerKeyPair + ledgerReadsSet := make(map[string]struct{}) + + ledger := NewTestLedgerWithData( + func(owner, key, _ []byte) { + ledgerReads = append( + ledgerReads, + ownerKeyPair{ + owner: owner, + key: key, + }, + ) + ledgerReadsSet[string(owner)+"|"+string(key)] = struct{}{} + }, + nil, + storedValues, + storedIndices, + ) + + storage := NewStorage( + ledger, + nil, + StorageConfig{ + StorageFormatV2Enabled: true, + }, + ) + + inter := NewTestInterpreterWithStorage(t, storage) + + domainStorageMap := storage.GetDomainStorageMap(inter, address, tc.domain, tc.createIfNotExists) + require.Equal(t, tc.expectedDomainStorageMapIsNil, domainStorageMap == nil) + require.Equal(t, tc.expectedReadsFor1stGetDomainStorageMapCall, ledgerReads) + + ledgerReads = ledgerReads[:0] + + domainStorageMap = storage.GetDomainStorageMap(inter, address, tc.domain, tc.createIfNotExists) + require.Equal(t, tc.expectedDomainStorageMapIsNil, domainStorageMap == nil) + require.Equal(t, tc.expectedReadsFor2ndGetDomainStorageMapCall, ledgerReads) + + // Check underlying ledger reads + require.Equal(t, len(ledgerReadsSet), len(tc.expectedReadsSet)) + for k := range ledgerReadsSet { + require.Contains(t, tc.expectedReadsSet, k) + } + }) + } +} + // createAndWriteAccountStorageMap creates account storage map with given domains and writes random values to domain storage map. func createAndWriteAccountStorageMap( t testing.TB, @@ -8207,3 +9142,17 @@ func checkAccountStorageMapData( require.Equal(tb, 1, len(rootSlabIDs)) require.Contains(tb, rootSlabIDs, accountSlabID) } + +func concatRegisterAddressAndKey( + address common.Address, + key []byte, +) string { + return string(address[:]) + "|" + string(key) +} + +func concatRegisterAddressAndDomain( + address common.Address, + domain common.StorageDomain, +) string { + return string(address[:]) + "|" + domain.Identifier() +}