From c0ab038a9905893e6c309b43f22b3764cc8b0f9a Mon Sep 17 00:00:00 2001 From: Supun Setunga Date: Wed, 10 Jan 2024 19:01:49 -0800 Subject: [PATCH 01/11] Use legacy hashing function for key removal --- migrations/legacy_intersection_type.go | 5 ++ migrations/legacy_string_value.go | 46 ++++++++++++++++++ migrations/migration.go | 21 +++++---- .../string_normalization/migration_test.go | 47 +++++++------------ migrations/type_value/migration_test.go | 14 ++++-- runtime/interpreter/value.go | 4 +- 6 files changed, 93 insertions(+), 44 deletions(-) create mode 100644 migrations/legacy_string_value.go diff --git a/migrations/legacy_intersection_type.go b/migrations/legacy_intersection_type.go index 7f452c81f8..abcfbfc009 100644 --- a/migrations/legacy_intersection_type.go +++ b/migrations/legacy_intersection_type.go @@ -43,6 +43,11 @@ func (t *LegacyIntersectionType) ID() common.TypeID { } var result strings.Builder + + if t.LegacyType != nil { + result.WriteString(string(t.LegacyType.ID())) + } + result.WriteByte('{') // NOTE: no sorting for i, interfaceTypeID := range interfaceTypeIDs { diff --git a/migrations/legacy_string_value.go b/migrations/legacy_string_value.go new file mode 100644 index 0000000000..4d372f5003 --- /dev/null +++ b/migrations/legacy_string_value.go @@ -0,0 +1,46 @@ +/* + * Cadence - The resource-oriented smart contract programming language + * + * Copyright Dapper Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package migrations + +import ( + "github.com/onflow/cadence/runtime/interpreter" +) + +// LegacyStringValue simulates the old string-value +// which uses the un-normalized string for hashing. +type LegacyStringValue struct { + *interpreter.StringValue +} + +var _ interpreter.Value = &LegacyStringValue{} + +func (v *LegacyStringValue) HashInput(_ *interpreter.Interpreter, _ interpreter.LocationRange, scratch []byte) []byte { + // Use the un-normalized `v.RawStr` for generating the hash. + length := 1 + len(v.RawStr) + var buffer []byte + if length <= len(scratch) { + buffer = scratch[:length] + } else { + buffer = make([]byte, length) + } + + buffer[0] = byte(interpreter.HashInputTypeString) + copy(buffer[1:], v.RawStr) + return buffer +} diff --git a/migrations/migration.go b/migrations/migration.go index 90c6efe9a1..fda8b64728 100644 --- a/migrations/migration.go +++ b/migrations/migration.go @@ -327,19 +327,22 @@ func (m *StorageMigration) migrate( return migration.Migrate(addressPath, value, m.interpreter) } -// legacyKey return the same type with the "old" hash/ID generation algo. +// legacyKey return the same type with the "old" hash/ID generation function. func legacyKey(key interpreter.Value) interpreter.Value { - typeValue, isTypeValue := key.(interpreter.TypeValue) - if !isTypeValue { - return key - } + switch key := key.(type) { + case interpreter.TypeValue: + legacyType := legacyType(key.Type) + if legacyType != nil { + return interpreter.NewUnmeteredTypeValue(legacyType) + } - legacyType := legacyType(typeValue.Type) - if legacyType == nil { - return key + case *interpreter.StringValue: + return &LegacyStringValue{ + key, + } } - return interpreter.NewUnmeteredTypeValue(legacyType) + return key } func legacyType(staticType interpreter.StaticType) interpreter.StaticType { diff --git a/migrations/string_normalization/migration_test.go b/migrations/string_normalization/migration_test.go index cc761d72e4..9ed28a9759 100644 --- a/migrations/string_normalization/migration_test.go +++ b/migrations/string_normalization/migration_test.go @@ -59,29 +59,28 @@ func TestStringNormalizingMigration(t *testing.T) { ) require.NoError(t, err) + newLegacyStringValue := func(s string) *interpreter.StringValue { + return &interpreter.StringValue{ + Str: s, + RawStr: s, + } + } + testCases := map[string]testCase{ "normalized_string": { - storedValue: &interpreter.StringValue{ - Str: "Caf\u00E9", - }, + storedValue: newLegacyStringValue("Caf\u00E9"), expectedValue: interpreter.NewUnmeteredStringValue("Caf\u00E9"), }, "un-normalized_string": { - storedValue: &interpreter.StringValue{ - Str: "Cafe\u0301", - }, + storedValue: newLegacyStringValue("Cafe\u0301"), expectedValue: interpreter.NewUnmeteredStringValue("Caf\u00E9"), }, "normalized_character": { - storedValue: &interpreter.StringValue{ - Str: "Caf\u00E9", - }, + storedValue: newLegacyStringValue("Caf\u00E9"), expectedValue: interpreter.NewUnmeteredStringValue("Caf\u00E9"), }, "un-normalized_character": { - storedValue: &interpreter.StringValue{ - Str: "Cafe\u0301", - }, + storedValue: newLegacyStringValue("Cafe\u0301"), expectedValue: interpreter.NewUnmeteredStringValue("Caf\u00E9"), }, "string_array": { @@ -90,9 +89,7 @@ func TestStringNormalizingMigration(t *testing.T) { locationRange, interpreter.NewVariableSizedStaticType(nil, interpreter.PrimitiveStaticTypeAnyStruct), common.ZeroAddress, - &interpreter.StringValue{ - Str: "Cafe\u0301", - }, + newLegacyStringValue("Cafe\u0301"), ), expectedValue: interpreter.NewArrayValue( inter, @@ -112,9 +109,7 @@ func TestStringNormalizingMigration(t *testing.T) { interpreter.PrimitiveStaticTypeString, ), interpreter.NewUnmeteredInt8Value(4), - &interpreter.StringValue{ - Str: "Cafe\u0301", - }, + newLegacyStringValue("Cafe\u0301"), ), expectedValue: interpreter.NewDictionaryValue( inter, @@ -137,9 +132,7 @@ func TestStringNormalizingMigration(t *testing.T) { interpreter.PrimitiveStaticTypeString, interpreter.PrimitiveStaticTypeInt8, ), - &interpreter.StringValue{ - Str: "Cafe\u0301", - }, + newLegacyStringValue("Cafe\u0301"), interpreter.NewUnmeteredInt8Value(4), ), expectedValue: interpreter.NewDictionaryValue( @@ -163,12 +156,8 @@ func TestStringNormalizingMigration(t *testing.T) { interpreter.PrimitiveStaticTypeString, interpreter.PrimitiveStaticTypeString, ), - &interpreter.StringValue{ - Str: "Cafe\u0301", - }, - &interpreter.StringValue{ - Str: "Cafe\u0301", - }, + newLegacyStringValue("Cafe\u0301"), + newLegacyStringValue("Cafe\u0301"), ), expectedValue: interpreter.NewDictionaryValue( inter, @@ -192,9 +181,7 @@ func TestStringNormalizingMigration(t *testing.T) { []interpreter.CompositeField{ interpreter.NewUnmeteredCompositeField( "field", - &interpreter.StringValue{ - Str: "Cafe\u0301", - }, + newLegacyStringValue("Cafe\u0301"), ), }, common.Address{}, diff --git a/migrations/type_value/migration_test.go b/migrations/type_value/migration_test.go index 62c8bc2b0f..1b30a83f14 100644 --- a/migrations/type_value/migration_test.go +++ b/migrations/type_value/migration_test.go @@ -705,8 +705,11 @@ func TestRehashNestedIntersectionType(t *testing.T) { ) dictValue := interpreter.NewDictionaryValue(inter, locationRange, dictionaryStaticType) + intersectionStaticType := newIntersectionStaticTypeWithTwoInterfacesReversed() + intersectionStaticType.LegacyType = interpreter.PrimitiveStaticTypeAnyStruct + intersectionType := &migrations.LegacyIntersectionType{ - IntersectionStaticType: newIntersectionStaticTypeWithTwoInterfacesReversed(), + IntersectionStaticType: intersectionStaticType, } typeValue := interpreter.NewUnmeteredTypeValue( @@ -725,7 +728,7 @@ func TestRehashNestedIntersectionType(t *testing.T) { // NOTE: intentionally in reverse order assert.Equal(t, - common.TypeID("{A.4200000000000000.Foo.Baz,A.4200000000000000.Foo.Bar}"), + common.TypeID("AnyStruct{A.4200000000000000.Foo.Baz,A.4200000000000000.Foo.Bar}"), intersectionType.ID(), ) @@ -837,8 +840,11 @@ func TestRehashNestedIntersectionType(t *testing.T) { ) dictValue := interpreter.NewDictionaryValue(inter, locationRange, dictionaryStaticType) + intersectionStaticType := newIntersectionStaticTypeWithTwoInterfacesReversed() + intersectionStaticType.LegacyType = interpreter.PrimitiveStaticTypeAnyStruct + intersectionType := &migrations.LegacyIntersectionType{ - IntersectionStaticType: newIntersectionStaticTypeWithTwoInterfacesReversed(), + IntersectionStaticType: intersectionStaticType, } typeValue := interpreter.NewUnmeteredTypeValue( @@ -858,7 +864,7 @@ func TestRehashNestedIntersectionType(t *testing.T) { // NOTE: intentionally in reverse order assert.Equal(t, - common.TypeID("{A.4200000000000000.Foo.Baz,A.4200000000000000.Foo.Bar}"), + common.TypeID("AnyStruct{A.4200000000000000.Foo.Baz,A.4200000000000000.Foo.Bar}"), intersectionType.ID(), ) diff --git a/runtime/interpreter/value.go b/runtime/interpreter/value.go index 24c6e0947f..d331ce80ad 100644 --- a/runtime/interpreter/value.go +++ b/runtime/interpreter/value.go @@ -1017,6 +1017,7 @@ type StringValue struct { // that are based on grapheme clusters graphemes *uniseg.Graphemes Str string + RawStr string // length is the cached length of the string, based on grapheme clusters. // a negative value indicates the length has not been initialized, see Length() length int @@ -1024,7 +1025,8 @@ type StringValue struct { func NewUnmeteredStringValue(str string) *StringValue { return &StringValue{ - Str: norm.NFC.String(str), + Str: norm.NFC.String(str), + RawStr: str, // a negative value indicates the length has not been initialized, see Length() length: -1, } From d50557614e757831ee6253000b3948a4af1006b8 Mon Sep 17 00:00:00 2001 From: Supun Setunga Date: Thu, 11 Jan 2024 15:04:11 -0800 Subject: [PATCH 02/11] Use legacy hashing for reference types --- migrations/legacy_reference_type.go | 52 +++++++++++++++++++++++++++++ migrations/migration.go | 10 ++++-- 2 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 migrations/legacy_reference_type.go diff --git a/migrations/legacy_reference_type.go b/migrations/legacy_reference_type.go new file mode 100644 index 0000000000..081f85c0a0 --- /dev/null +++ b/migrations/legacy_reference_type.go @@ -0,0 +1,52 @@ +/* + * Cadence - The resource-oriented smart contract programming language + * + * Copyright Dapper Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package migrations + +import ( + "strings" + + "github.com/onflow/cadence/runtime/common" + "github.com/onflow/cadence/runtime/interpreter" +) + +// LegacyReferenceType simulates the old reference type with the old typeID generation. +type LegacyReferenceType struct { + *interpreter.ReferenceStaticType +} + +var _ interpreter.StaticType = &LegacyReferenceType{} + +func (t *LegacyReferenceType) ID() common.TypeID { + isAuthorized := t.Authorization == interpreter.UnauthorizedAccess + borrowedType := t.ReferencedType + return common.TypeID(formatReferenceType(isAuthorized, string(borrowedType.ID()))) +} + +func formatReferenceType( + authorized bool, + typeString string, +) string { + var builder strings.Builder + if authorized { + builder.WriteString("auth") + } + builder.WriteByte('&') + builder.WriteString(typeString) + return builder.String() +} diff --git a/migrations/migration.go b/migrations/migration.go index fda8b64728..7b4c33d5bc 100644 --- a/migrations/migration.go +++ b/migrations/migration.go @@ -338,7 +338,7 @@ func legacyKey(key interpreter.Value) interpreter.Value { case *interpreter.StringValue: return &LegacyStringValue{ - key, + StringValue: key, } } @@ -390,9 +390,15 @@ func legacyType(staticType interpreter.StaticType) interpreter.StaticType { } case *interpreter.ReferenceStaticType: + referenceType := typ + legacyReferencedType := legacyType(typ.ReferencedType) if legacyReferencedType != nil { - return interpreter.NewReferenceStaticType(nil, typ.Authorization, legacyReferencedType) + referenceType = interpreter.NewReferenceStaticType(nil, typ.Authorization, legacyReferencedType) + } + + return &LegacyReferenceType{ + ReferenceStaticType: referenceType, } } From 11de480804522194552c874896988e533e8c65d6 Mon Sep 17 00:00:00 2001 From: Supun Setunga Date: Thu, 11 Jan 2024 15:51:51 -0800 Subject: [PATCH 03/11] Refactor --- migrations/legacy_string_value.go | 6 +++--- migrations/string_normalization/migration_test.go | 4 ++-- runtime/interpreter/value.go | 10 +++++----- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/migrations/legacy_string_value.go b/migrations/legacy_string_value.go index 4d372f5003..f9ce1e847c 100644 --- a/migrations/legacy_string_value.go +++ b/migrations/legacy_string_value.go @@ -31,8 +31,8 @@ type LegacyStringValue struct { var _ interpreter.Value = &LegacyStringValue{} func (v *LegacyStringValue) HashInput(_ *interpreter.Interpreter, _ interpreter.LocationRange, scratch []byte) []byte { - // Use the un-normalized `v.RawStr` for generating the hash. - length := 1 + len(v.RawStr) + // Use the un-normalized `v.UnnormalizedStr` for generating the hash. + length := 1 + len(v.UnnormalizedStr) var buffer []byte if length <= len(scratch) { buffer = scratch[:length] @@ -41,6 +41,6 @@ func (v *LegacyStringValue) HashInput(_ *interpreter.Interpreter, _ interpreter. } buffer[0] = byte(interpreter.HashInputTypeString) - copy(buffer[1:], v.RawStr) + copy(buffer[1:], v.UnnormalizedStr) return buffer } diff --git a/migrations/string_normalization/migration_test.go b/migrations/string_normalization/migration_test.go index 9ed28a9759..94bef2bb1f 100644 --- a/migrations/string_normalization/migration_test.go +++ b/migrations/string_normalization/migration_test.go @@ -61,8 +61,8 @@ func TestStringNormalizingMigration(t *testing.T) { newLegacyStringValue := func(s string) *interpreter.StringValue { return &interpreter.StringValue{ - Str: s, - RawStr: s, + Str: s, + UnnormalizedStr: s, } } diff --git a/runtime/interpreter/value.go b/runtime/interpreter/value.go index 8a9cd98efc..824f2bcb79 100644 --- a/runtime/interpreter/value.go +++ b/runtime/interpreter/value.go @@ -1015,9 +1015,9 @@ type StringValue struct { // graphemes is a grapheme cluster segmentation iterator, // which is initialized lazily and reused/reset in functions // that are based on grapheme clusters - graphemes *uniseg.Graphemes - Str string - RawStr string + graphemes *uniseg.Graphemes + Str string + UnnormalizedStr string // length is the cached length of the string, based on grapheme clusters. // a negative value indicates the length has not been initialized, see Length() length int @@ -1025,8 +1025,8 @@ type StringValue struct { func NewUnmeteredStringValue(str string) *StringValue { return &StringValue{ - Str: norm.NFC.String(str), - RawStr: str, + Str: norm.NFC.String(str), + UnnormalizedStr: str, // a negative value indicates the length has not been initialized, see Length() length: -1, } From 5ba86f9f22a657c3aa9610599bcc4882af17bcee Mon Sep 17 00:00:00 2001 From: Supun Setunga Date: Fri, 12 Jan 2024 15:22:12 -0800 Subject: [PATCH 04/11] Re-hash account related types during migration --- migrations/account_type/migration_test.go | 231 ++++++++++++++++++++++ migrations/legacy_primitivestatic_type.go | 65 ++++++ migrations/migration.go | 19 ++ 3 files changed, 315 insertions(+) create mode 100644 migrations/legacy_primitivestatic_type.go diff --git a/migrations/account_type/migration_test.go b/migrations/account_type/migration_test.go index 458590cc5d..7d44372610 100644 --- a/migrations/account_type/migration_test.go +++ b/migrations/account_type/migration_test.go @@ -19,6 +19,7 @@ package account_type import ( + "fmt" "testing" "github.com/stretchr/testify/assert" @@ -870,3 +871,233 @@ func TestValuesWithStaticTypeMigration(t *testing.T) { }) } } + +var testAddress = common.Address{0x42} + +func TestRehash(t *testing.T) { + + t.Parallel() + + locationRange := interpreter.EmptyLocationRange + + ledger := NewTestLedger(nil, nil) + + storageMapKey := interpreter.StringStorageMapKey("dict") + newStringValue := func(s string) interpreter.Value { + return interpreter.NewUnmeteredStringValue(s) + } + + newStorageAndInterpreter := func(t *testing.T) (*runtime.Storage, *interpreter.Interpreter) { + storage := runtime.NewStorage(ledger, nil) + inter, err := interpreter.NewInterpreter( + nil, + utils.TestLocation, + &interpreter.Config{ + Storage: storage, + AtreeValueValidationEnabled: false, + AtreeStorageValidationEnabled: true, + }, + ) + require.NoError(t, err) + + return storage, inter + } + + t.Run("prepare", func(t *testing.T) { + + storage, inter := newStorageAndInterpreter(t) + + dictionaryStaticType := interpreter.NewDictionaryStaticType( + nil, + interpreter.PrimitiveStaticTypeMetaType, + interpreter.PrimitiveStaticTypeString, + ) + dictValue := interpreter.NewDictionaryValue(inter, locationRange, dictionaryStaticType) + + accountTypes := []interpreter.PrimitiveStaticType{ + interpreter.PrimitiveStaticTypePublicAccount, //nolint:staticcheck + interpreter.PrimitiveStaticTypeAuthAccount, //nolint:staticcheck + interpreter.PrimitiveStaticTypeAuthAccountCapabilities, //nolint:staticcheck + interpreter.PrimitiveStaticTypePublicAccountCapabilities, //nolint:staticcheck + interpreter.PrimitiveStaticTypeAuthAccountAccountCapabilities, //nolint:staticcheck + interpreter.PrimitiveStaticTypeAuthAccountStorageCapabilities, //nolint:staticcheck + interpreter.PrimitiveStaticTypeAuthAccountContracts, //nolint:staticcheck + interpreter.PrimitiveStaticTypePublicAccountContracts, //nolint:staticcheck + interpreter.PrimitiveStaticTypeAuthAccountKeys, //nolint:staticcheck + interpreter.PrimitiveStaticTypePublicAccountKeys, //nolint:staticcheck + interpreter.PrimitiveStaticTypeAuthAccountInbox, //nolint:staticcheck + interpreter.PrimitiveStaticTypeAccountKey, //nolint:staticcheck + } + + for _, typ := range accountTypes { + typeValue := interpreter.NewUnmeteredTypeValue( + migrations.LegacyPrimitiveStaticType{ + PrimitiveStaticType: typ, + }, + ) + dictValue.Insert( + inter, + locationRange, + typeValue, + newStringValue(typ.String()), + ) + } + + storageMap := storage.GetStorageMap( + testAddress, + common.PathDomainStorage.Identifier(), + true, + ) + + storageMap.SetValue(inter, + storageMapKey, + dictValue.Transfer( + inter, + locationRange, + atree.Address(testAddress), + false, + nil, + nil, + ), + ) + + err := storage.Commit(inter, false) + require.NoError(t, err) + }) + + t.Run("migrate", func(t *testing.T) { + + storage, inter := newStorageAndInterpreter(t) + + migration := migrations.NewStorageMigration(inter, storage) + + reporter := newTestReporter() + + migration.Migrate( + &migrations.AddressSliceIterator{ + Addresses: []common.Address{ + testAddress, + }, + }, + migration.NewValueMigrationsPathMigrator( + reporter, + NewAccountTypeMigration(), + ), + ) + + err := migration.Commit() + require.NoError(t, err) + + require.Equal(t, + map[interpreter.AddressPath]struct{}{ + { + Address: testAddress, + Path: interpreter.PathValue{ + Domain: common.PathDomainStorage, + Identifier: string(storageMapKey), + }, + }: {}, + }, + reporter.migratedPaths, + ) + }) + + t.Run("load", func(t *testing.T) { + + storage, inter := newStorageAndInterpreter(t) + + storageMap := storage.GetStorageMap(testAddress, common.PathDomainStorage.Identifier(), false) + storedValue := storageMap.ReadValue(inter, storageMapKey) + + require.IsType(t, &interpreter.DictionaryValue{}, storedValue) + + dictValue := storedValue.(*interpreter.DictionaryValue) + + var existingKeys []interpreter.Value + dictValue.Iterate(inter, func(key, value interpreter.Value) (resume bool) { + existingKeys = append(existingKeys, key) + // continue iteration + return true + }) + + for _, key := range existingKeys { + actual := dictValue.Remove( + inter, + interpreter.EmptyLocationRange, + key, + ) + + assert.NotNil(t, actual) + + staticType := key.(interpreter.TypeValue).Type + + var possibleExpectedValues []interpreter.Value + var str string + + switch { + case staticType.Equal(unauthorizedAccountReferenceType): + str = "PublicAccount" + case staticType.Equal(authAccountReferenceType): + str = "AuthAccount" + case staticType.Equal(interpreter.PrimitiveStaticTypeAccount_Capabilities): + // For both `AuthAccount.Capabilities` and `PublicAccount.Capabilities`, + // the migrated key is the same (`Account_Capabilities`). + // So the value at the key could be any of the two original values, + // depending on the order of migration. + possibleExpectedValues = []interpreter.Value{ + interpreter.NewUnmeteredSomeValueNonCopying( + interpreter.NewUnmeteredStringValue("AuthAccountCapabilities"), + ), + interpreter.NewUnmeteredSomeValueNonCopying( + interpreter.NewUnmeteredStringValue("PublicAccountCapabilities"), + ), + } + case staticType.Equal(interpreter.PrimitiveStaticTypeAccount_AccountCapabilities): + str = "AuthAccountAccountCapabilities" + case staticType.Equal(interpreter.PrimitiveStaticTypeAccount_StorageCapabilities): + str = "AuthAccountStorageCapabilities" + case staticType.Equal(interpreter.PrimitiveStaticTypeAccount_Contracts): + // For both `AuthAccount.Contracts` and `PublicAccount.Contracts`, + // the migrated key is the same (Account_Contracts). + // So the value at the key could be any of the two original values, + // depending on the order of migration. + possibleExpectedValues = []interpreter.Value{ + interpreter.NewUnmeteredSomeValueNonCopying( + interpreter.NewUnmeteredStringValue("AuthAccountContracts"), + ), + interpreter.NewUnmeteredSomeValueNonCopying( + interpreter.NewUnmeteredStringValue("PublicAccountContracts"), + ), + } + case staticType.Equal(interpreter.PrimitiveStaticTypeAccount_Keys): + // For both `AuthAccount.Keys` and `PublicAccount.Keys`, + // the migrated key is the same (Account_Keys). + // So the value at the key could be any of the two original values, + // depending on the order of migration. + possibleExpectedValues = []interpreter.Value{ + interpreter.NewUnmeteredSomeValueNonCopying( + interpreter.NewUnmeteredStringValue("AuthAccountKeys"), + ), + interpreter.NewUnmeteredSomeValueNonCopying( + interpreter.NewUnmeteredStringValue("PublicAccountKeys"), + ), + } + case staticType.Equal(interpreter.PrimitiveStaticTypeAccount_Inbox): + str = "AuthAccountInbox" + case staticType.Equal(interpreter.AccountKeyStaticType): + str = "AccountKey" + default: + require.Fail(t, fmt.Sprintf("Unexpected type `%s` in dictionary key", staticType.ID())) + } + + if possibleExpectedValues != nil { + assert.Contains(t, possibleExpectedValues, actual) + } else { + expected := interpreter.NewUnmeteredSomeValueNonCopying( + interpreter.NewUnmeteredStringValue(str), + ) + assert.Equal(t, expected, actual) + } + } + }) +} diff --git a/migrations/legacy_primitivestatic_type.go b/migrations/legacy_primitivestatic_type.go new file mode 100644 index 0000000000..c44b26f592 --- /dev/null +++ b/migrations/legacy_primitivestatic_type.go @@ -0,0 +1,65 @@ +/* + * Cadence - The resource-oriented smart contract programming language + * + * Copyright Dapper Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package migrations + +import ( + "github.com/onflow/cadence/runtime/common" + "github.com/onflow/cadence/runtime/errors" + "github.com/onflow/cadence/runtime/interpreter" +) + +// LegacyPrimitiveStaticType simulates the old primitive-static-type +// which uses the old type-ids for hashing. +type LegacyPrimitiveStaticType struct { + interpreter.PrimitiveStaticType +} + +var _ interpreter.StaticType = LegacyPrimitiveStaticType{} + +func (t LegacyPrimitiveStaticType) ID() common.TypeID { + switch t.PrimitiveStaticType { + case interpreter.PrimitiveStaticTypeAuthAccount: //nolint:staticcheck + return "AuthAccount" + case interpreter.PrimitiveStaticTypePublicAccount: //nolint:staticcheck + return "PublicAccount" + case interpreter.PrimitiveStaticTypeAuthAccountCapabilities: //nolint:staticcheck + return "AuthAccount.Capabilities" + case interpreter.PrimitiveStaticTypePublicAccountCapabilities: //nolint:staticcheck + return "PublicAccount.Capabilities" + case interpreter.PrimitiveStaticTypeAuthAccountAccountCapabilities: //nolint:staticcheck + return "AuthAccount.AccountCapabilities" + case interpreter.PrimitiveStaticTypeAuthAccountStorageCapabilities: //nolint:staticcheck + return "AuthAccount.StorageCapabilities" + case interpreter.PrimitiveStaticTypeAuthAccountContracts: //nolint:staticcheck + return "AuthAccount.Contracts" + case interpreter.PrimitiveStaticTypePublicAccountContracts: //nolint:staticcheck + return "PublicAccount.Contracts" + case interpreter.PrimitiveStaticTypeAuthAccountKeys: //nolint:staticcheck + return "AuthAccount.Keys" + case interpreter.PrimitiveStaticTypePublicAccountKeys: //nolint:staticcheck + return "PublicAccount.Keys" + case interpreter.PrimitiveStaticTypeAuthAccountInbox: //nolint:staticcheck + return "AuthAccount.Inbox" + case interpreter.PrimitiveStaticTypeAccountKey: //nolint:staticcheck + return "AccountKey" + default: + panic(errors.NewUnreachableError()) + + } +} diff --git a/migrations/migration.go b/migrations/migration.go index 7b4c33d5bc..73f713078f 100644 --- a/migrations/migration.go +++ b/migrations/migration.go @@ -400,6 +400,25 @@ func legacyType(staticType interpreter.StaticType) interpreter.StaticType { return &LegacyReferenceType{ ReferenceStaticType: referenceType, } + + case interpreter.PrimitiveStaticType: + switch typ { + case interpreter.PrimitiveStaticTypeAuthAccount, //nolint:staticcheck + interpreter.PrimitiveStaticTypePublicAccount, //nolint:staticcheck + interpreter.PrimitiveStaticTypeAuthAccountCapabilities, //nolint:staticcheck + interpreter.PrimitiveStaticTypePublicAccountCapabilities, //nolint:staticcheck + interpreter.PrimitiveStaticTypeAuthAccountAccountCapabilities, //nolint:staticcheck + interpreter.PrimitiveStaticTypeAuthAccountStorageCapabilities, //nolint:staticcheck + interpreter.PrimitiveStaticTypeAuthAccountContracts, //nolint:staticcheck + interpreter.PrimitiveStaticTypePublicAccountContracts, //nolint:staticcheck + interpreter.PrimitiveStaticTypeAuthAccountKeys, //nolint:staticcheck + interpreter.PrimitiveStaticTypePublicAccountKeys, //nolint:staticcheck + interpreter.PrimitiveStaticTypeAuthAccountInbox, //nolint:staticcheck + interpreter.PrimitiveStaticTypeAccountKey: //nolint:staticcheck + return LegacyPrimitiveStaticType{ + PrimitiveStaticType: typ, + } + } } return nil From 2efbb91088d3d74fc4ef4b1a38ed88333909a9d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Fri, 12 Jan 2024 16:56:45 -0800 Subject: [PATCH 05/11] add re-hash test case for string normalization migration --- .../string_normalization/migration_test.go | 137 ++++++++++++++++++ 1 file changed, 137 insertions(+) diff --git a/migrations/string_normalization/migration_test.go b/migrations/string_normalization/migration_test.go index 94bef2bb1f..c334d3750a 100644 --- a/migrations/string_normalization/migration_test.go +++ b/migrations/string_normalization/migration_test.go @@ -21,6 +21,7 @@ package string_normalization import ( "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/onflow/atree" @@ -269,3 +270,139 @@ func TestStringNormalizingMigration(t *testing.T) { }) } } + +// TestRehash stores a dictionary in storage, +// which has a key that is a string value with a non-normalized representation, +// runs the migration, and ensures the dictionary is still usable +func TestRehash(t *testing.T) { + + t.Parallel() + + var testAddress = common.MustBytesToAddress([]byte{0x1}) + + locationRange := interpreter.EmptyLocationRange + + ledger := NewTestLedger(nil, nil) + + storageMapKey := interpreter.StringStorageMapKey("dict") + newTestValue := func() interpreter.Value { + return interpreter.NewUnmeteredIntValueFromInt64(42) + } + + newStorageAndInterpreter := func(t *testing.T) (*runtime.Storage, *interpreter.Interpreter) { + storage := runtime.NewStorage(ledger, nil) + inter, err := interpreter.NewInterpreter( + nil, + utils.TestLocation, + &interpreter.Config{ + Storage: storage, + AtreeValueValidationEnabled: false, + AtreeStorageValidationEnabled: true, + }, + ) + require.NoError(t, err) + + return storage, inter + } + + t.Run("prepare", func(t *testing.T) { + + storage, inter := newStorageAndInterpreter(t) + + dictionaryStaticType := interpreter.NewDictionaryStaticType( + nil, + interpreter.PrimitiveStaticTypeString, + interpreter.PrimitiveStaticTypeInt, + ) + dictValue := interpreter.NewDictionaryValue(inter, locationRange, dictionaryStaticType) + + // NOTE: non-normalized + stringValue := &migrations.LegacyStringValue{ + StringValue: interpreter.NewUnmeteredStringValue("Cafe\u0301"), + } + + dictValue.Insert( + inter, + locationRange, + stringValue, + newTestValue(), + ) + + assert.Equal(t, + []byte("\x01Cafe\xCC\x81"), + stringValue.HashInput(inter, locationRange, nil), + ) + + storageMap := storage.GetStorageMap( + testAddress, + common.PathDomainStorage.Identifier(), + true, + ) + + storageMap.SetValue( + inter, + storageMapKey, + dictValue.Transfer( + inter, + locationRange, + atree.Address(testAddress), + false, + nil, + nil, + ), + ) + + err := storage.Commit(inter, false) + require.NoError(t, err) + }) + + t.Run("migrate", func(t *testing.T) { + + storage, inter := newStorageAndInterpreter(t) + + migration := migrations.NewStorageMigration(inter, storage) + + migration.Migrate( + &migrations.AddressSliceIterator{ + Addresses: []common.Address{ + testAddress, + }, + }, + migration.NewValueMigrationsPathMigrator( + nil, + NewStringNormalizingMigration(), + ), + ) + + err := migration.Commit() + require.NoError(t, err) + }) + + t.Run("load", func(t *testing.T) { + + storage, inter := newStorageAndInterpreter(t) + + storageMap := storage.GetStorageMap(testAddress, common.PathDomainStorage.Identifier(), false) + storedValue := storageMap.ReadValue(inter, storageMapKey) + + require.IsType(t, &interpreter.DictionaryValue{}, storedValue) + + dictValue := storedValue.(*interpreter.DictionaryValue) + + stringValue := interpreter.NewUnmeteredStringValue("Caf\u00E9") + + assert.Equal(t, + []byte("\x01Caf\xC3\xA9"), + stringValue.HashInput(inter, locationRange, nil), + ) + + value, ok := dictValue.Get(inter, locationRange, stringValue) + require.True(t, ok) + + require.IsType(t, interpreter.IntValue{}, value) + require.Equal(t, + newTestValue(), + value.(interpreter.IntValue), + ) + }) +} From 9ac7bb2aa4acbf6294f1bae6623d44d01a423589 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Fri, 12 Jan 2024 17:15:23 -0800 Subject: [PATCH 06/11] fix test --- migrations/legacy_string_value.go | 20 +++++++++++++++++++ .../string_normalization/migration_test.go | 8 +++++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/migrations/legacy_string_value.go b/migrations/legacy_string_value.go index f9ce1e847c..75379ba802 100644 --- a/migrations/legacy_string_value.go +++ b/migrations/legacy_string_value.go @@ -19,6 +19,8 @@ package migrations import ( + "github.com/onflow/atree" + "github.com/onflow/cadence/runtime/interpreter" ) @@ -44,3 +46,21 @@ func (v *LegacyStringValue) HashInput(_ *interpreter.Interpreter, _ interpreter. copy(buffer[1:], v.UnnormalizedStr) return buffer } + +func (v *LegacyStringValue) Transfer( + interpreter *interpreter.Interpreter, + _ interpreter.LocationRange, + _ atree.Address, + remove bool, + storable atree.Storable, + _ map[atree.StorageID]struct{}, +) interpreter.Value { + if remove { + interpreter.RemoveReferencedSlab(storable) + } + return v +} + +func (v *LegacyStringValue) StoredValue(_ atree.SlabStorage) (atree.Value, error) { + return v, nil +} diff --git a/migrations/string_normalization/migration_test.go b/migrations/string_normalization/migration_test.go index c334d3750a..83858ab64b 100644 --- a/migrations/string_normalization/migration_test.go +++ b/migrations/string_normalization/migration_test.go @@ -316,9 +316,11 @@ func TestRehash(t *testing.T) { ) dictValue := interpreter.NewDictionaryValue(inter, locationRange, dictionaryStaticType) - // NOTE: non-normalized - stringValue := &migrations.LegacyStringValue{ - StringValue: interpreter.NewUnmeteredStringValue("Cafe\u0301"), + // NOTE: un-normalized + unnormalizedString := "Cafe\u0301" + stringValue := &interpreter.StringValue{ + Str: unnormalizedString, + UnnormalizedStr: unnormalizedString, } dictValue.Insert( From 097096cbe8ef42f3991ff52c730429e68c6e85a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Fri, 12 Jan 2024 17:31:39 -0800 Subject: [PATCH 07/11] add comment --- migrations/legacy_string_value.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/migrations/legacy_string_value.go b/migrations/legacy_string_value.go index 75379ba802..3b448664c9 100644 --- a/migrations/legacy_string_value.go +++ b/migrations/legacy_string_value.go @@ -32,6 +32,12 @@ type LegacyStringValue struct { var _ interpreter.Value = &LegacyStringValue{} +// Override HashInput to use the un-normalized string for hashing, +// so the removal of the existing key is using this hash input function, +// instead of the one from StringValue. +// +// However, after hashing the equality function should still use the equality function from StringValue. + func (v *LegacyStringValue) HashInput(_ *interpreter.Interpreter, _ interpreter.LocationRange, scratch []byte) []byte { // Use the un-normalized `v.UnnormalizedStr` for generating the hash. length := 1 + len(v.UnnormalizedStr) From f37db89629f3d615205b0797307e9238fc99b4aa Mon Sep 17 00:00:00 2001 From: Supun Setunga Date: Mon, 15 Jan 2024 11:47:58 -0800 Subject: [PATCH 08/11] Add re-hashing for character value --- migrations/legacy_character_value.go | 72 +++++++ migrations/migration.go | 5 + migrations/string_normalization/migration.go | 2 +- .../string_normalization/migration_test.go | 182 +++++++++++++++++- runtime/convertValues.go | 4 +- runtime/interpreter/decode.go | 6 +- runtime/interpreter/encode.go | 2 +- runtime/interpreter/value.go | 48 +++-- runtime/interpreter/value_string.go | 4 +- 9 files changed, 289 insertions(+), 36 deletions(-) create mode 100644 migrations/legacy_character_value.go diff --git a/migrations/legacy_character_value.go b/migrations/legacy_character_value.go new file mode 100644 index 0000000000..ae9284a408 --- /dev/null +++ b/migrations/legacy_character_value.go @@ -0,0 +1,72 @@ +/* + * Cadence - The resource-oriented smart contract programming language + * + * Copyright Dapper Labs, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package migrations + +import ( + "github.com/onflow/atree" + + "github.com/onflow/cadence/runtime/interpreter" +) + +// LegacyCharacterValue simulates the old character-value +// which uses the un-normalized string for hashing. +type LegacyCharacterValue struct { + interpreter.CharacterValue +} + +var _ interpreter.Value = &LegacyCharacterValue{} + +// Override HashInput to use the un-normalized string for hashing, +// so the removal of the existing key is using this hash input function, +// instead of the one from CharacterValue. +// +// However, after hashing the equality function should still use the equality function from CharacterValue. + +func (v *LegacyCharacterValue) HashInput(_ *interpreter.Interpreter, _ interpreter.LocationRange, scratch []byte) []byte { + // Use the un-normalized `v.UnnormalizedStr` for generating the hash. + length := 1 + len(v.UnnormalizedStr) + var buffer []byte + if length <= len(scratch) { + buffer = scratch[:length] + } else { + buffer = make([]byte, length) + } + + buffer[0] = byte(interpreter.HashInputTypeCharacter) + copy(buffer[1:], v.UnnormalizedStr) + return buffer +} + +func (v *LegacyCharacterValue) Transfer( + interpreter *interpreter.Interpreter, + _ interpreter.LocationRange, + _ atree.Address, + remove bool, + storable atree.Storable, + _ map[atree.StorageID]struct{}, +) interpreter.Value { + if remove { + interpreter.RemoveReferencedSlab(storable) + } + return v +} + +func (v *LegacyCharacterValue) StoredValue(_ atree.SlabStorage) (atree.Value, error) { + return v, nil +} diff --git a/migrations/migration.go b/migrations/migration.go index 73f713078f..0f6f166c0d 100644 --- a/migrations/migration.go +++ b/migrations/migration.go @@ -340,6 +340,11 @@ func legacyKey(key interpreter.Value) interpreter.Value { return &LegacyStringValue{ StringValue: key, } + + case interpreter.CharacterValue: + return &LegacyCharacterValue{ + CharacterValue: key, + } } return key diff --git a/migrations/string_normalization/migration.go b/migrations/string_normalization/migration.go index 9d1bd6a25e..a455009395 100644 --- a/migrations/string_normalization/migration.go +++ b/migrations/string_normalization/migration.go @@ -45,7 +45,7 @@ func (StringNormalizingMigration) Migrate( return interpreter.NewUnmeteredStringValue(value.Str), nil case interpreter.CharacterValue: - return interpreter.NewUnmeteredCharacterValue(string(value)), nil + return interpreter.NewUnmeteredCharacterValue(value.Str), nil } return nil, nil diff --git a/migrations/string_normalization/migration_test.go b/migrations/string_normalization/migration_test.go index 83858ab64b..b46621fba8 100644 --- a/migrations/string_normalization/migration_test.go +++ b/migrations/string_normalization/migration_test.go @@ -67,6 +67,13 @@ func TestStringNormalizingMigration(t *testing.T) { } } + newLegacyCharacterValue := func(s string) interpreter.CharacterValue { + return interpreter.CharacterValue{ + Str: s, + UnnormalizedStr: s, + } + } + testCases := map[string]testCase{ "normalized_string": { storedValue: newLegacyStringValue("Caf\u00E9"), @@ -77,12 +84,12 @@ func TestStringNormalizingMigration(t *testing.T) { expectedValue: interpreter.NewUnmeteredStringValue("Caf\u00E9"), }, "normalized_character": { - storedValue: newLegacyStringValue("Caf\u00E9"), - expectedValue: interpreter.NewUnmeteredStringValue("Caf\u00E9"), + storedValue: newLegacyCharacterValue("\u03A9"), + expectedValue: interpreter.NewUnmeteredCharacterValue("\u03A9"), }, "un-normalized_character": { - storedValue: newLegacyStringValue("Cafe\u0301"), - expectedValue: interpreter.NewUnmeteredStringValue("Caf\u00E9"), + storedValue: newLegacyCharacterValue("\u2126"), + expectedValue: interpreter.NewUnmeteredCharacterValue("\u03A9"), }, "string_array": { storedValue: interpreter.NewArrayValue( @@ -202,6 +209,30 @@ func TestStringNormalizingMigration(t *testing.T) { common.Address{}, ), }, + "dictionary_with_un-normalized_character_key": { + storedValue: interpreter.NewDictionaryValue( + inter, + locationRange, + interpreter.NewDictionaryStaticType( + nil, + interpreter.PrimitiveStaticTypeCharacter, + interpreter.PrimitiveStaticTypeInt8, + ), + newLegacyCharacterValue("\u2126"), + interpreter.NewUnmeteredInt8Value(4), + ), + expectedValue: interpreter.NewDictionaryValue( + inter, + locationRange, + interpreter.NewDictionaryStaticType( + nil, + interpreter.PrimitiveStaticTypeCharacter, + interpreter.PrimitiveStaticTypeInt8, + ), + interpreter.NewUnmeteredCharacterValue("\u03A9"), + interpreter.NewUnmeteredInt8Value(4), + ), + }, } // Store values @@ -271,10 +302,10 @@ func TestStringNormalizingMigration(t *testing.T) { } } -// TestRehash stores a dictionary in storage, +// TestStringValueRehash stores a dictionary in storage, // which has a key that is a string value with a non-normalized representation, // runs the migration, and ensures the dictionary is still usable -func TestRehash(t *testing.T) { +func TestStringValueRehash(t *testing.T) { t.Parallel() @@ -408,3 +439,142 @@ func TestRehash(t *testing.T) { ) }) } + +// TestCharacterValueRehash stores a dictionary in storage, +// which has a key that is a character value with a non-normalized representation, +// runs the migration, and ensures the dictionary is still usable +func TestCharacterValueRehash(t *testing.T) { + + t.Parallel() + + var testAddress = common.MustBytesToAddress([]byte{0x1}) + + locationRange := interpreter.EmptyLocationRange + + ledger := NewTestLedger(nil, nil) + + storageMapKey := interpreter.StringStorageMapKey("dict") + newTestValue := func() interpreter.Value { + return interpreter.NewUnmeteredIntValueFromInt64(42) + } + + newStorageAndInterpreter := func(t *testing.T) (*runtime.Storage, *interpreter.Interpreter) { + storage := runtime.NewStorage(ledger, nil) + inter, err := interpreter.NewInterpreter( + nil, + utils.TestLocation, + &interpreter.Config{ + Storage: storage, + AtreeValueValidationEnabled: false, + AtreeStorageValidationEnabled: true, + }, + ) + require.NoError(t, err) + + return storage, inter + } + + t.Run("prepare", func(t *testing.T) { + + storage, inter := newStorageAndInterpreter(t) + + dictionaryStaticType := interpreter.NewDictionaryStaticType( + nil, + interpreter.PrimitiveStaticTypeCharacter, + interpreter.PrimitiveStaticTypeInt, + ) + dictValue := interpreter.NewDictionaryValue(inter, locationRange, dictionaryStaticType) + + // NOTE: un-normalized 'Ω'. + unnormalizedString := "\u2126" + + characterValue := &interpreter.CharacterValue{ + Str: unnormalizedString, + UnnormalizedStr: unnormalizedString, + } + + dictValue.Insert( + inter, + locationRange, + characterValue, + newTestValue(), + ) + + assert.Equal(t, + []byte("\x06\xe2\x84\xa6"), + characterValue.HashInput(inter, locationRange, nil), + ) + + storageMap := storage.GetStorageMap( + testAddress, + common.PathDomainStorage.Identifier(), + true, + ) + + storageMap.SetValue( + inter, + storageMapKey, + dictValue.Transfer( + inter, + locationRange, + atree.Address(testAddress), + false, + nil, + nil, + ), + ) + + err := storage.Commit(inter, false) + require.NoError(t, err) + }) + + t.Run("migrate", func(t *testing.T) { + + storage, inter := newStorageAndInterpreter(t) + + migration := migrations.NewStorageMigration(inter, storage) + + migration.Migrate( + &migrations.AddressSliceIterator{ + Addresses: []common.Address{ + testAddress, + }, + }, + migration.NewValueMigrationsPathMigrator( + nil, + NewStringNormalizingMigration(), + ), + ) + + err := migration.Commit() + require.NoError(t, err) + }) + + t.Run("load", func(t *testing.T) { + + storage, inter := newStorageAndInterpreter(t) + + storageMap := storage.GetStorageMap(testAddress, common.PathDomainStorage.Identifier(), false) + storedValue := storageMap.ReadValue(inter, storageMapKey) + + require.IsType(t, &interpreter.DictionaryValue{}, storedValue) + + dictValue := storedValue.(*interpreter.DictionaryValue) + + characterValue := interpreter.NewUnmeteredCharacterValue("\u03A9") + + assert.Equal(t, + []byte("\x06\xCe\xA9"), + characterValue.HashInput(inter, locationRange, nil), + ) + + value, ok := dictValue.Get(inter, locationRange, characterValue) + require.True(t, ok) + + require.IsType(t, interpreter.IntValue{}, value) + require.Equal(t, + newTestValue(), + value.(interpreter.IntValue), + ) + }) +} diff --git a/runtime/convertValues.go b/runtime/convertValues.go index ae97ac9528..2feb5b9a5b 100644 --- a/runtime/convertValues.go +++ b/runtime/convertValues.go @@ -98,9 +98,9 @@ func exportValueWithInterpreter( case interpreter.CharacterValue: return cadence.NewMeteredCharacter( inter, - common.NewCadenceCharacterMemoryUsage(len(v)), + common.NewCadenceCharacterMemoryUsage(len(v.Str)), func() string { - return string(v) + return v.Str }, ) case *interpreter.ArrayValue: diff --git a/runtime/interpreter/decode.go b/runtime/interpreter/decode.go index d42782b16b..79579a0aa8 100644 --- a/runtime/interpreter/decode.go +++ b/runtime/interpreter/decode.go @@ -357,15 +357,15 @@ func (d StorableDecoder) decodeCharacter() (CharacterValue, error) { v, err := decodeCharacter(d.decoder, d.memoryGauge) if err != nil { if err, ok := err.(*cbor.WrongTypeError); ok { - return "", errors.NewUnexpectedError( + return CharacterValue{}, errors.NewUnexpectedError( "invalid Character encoding: %s", err.ActualType.String(), ) } - return "", err + return CharacterValue{}, err } if !sema.IsValidCharacter(v) { - return "", errors.NewUnexpectedError( + return CharacterValue{}, errors.NewUnexpectedError( "invalid character encoding: %s", v, ) diff --git a/runtime/interpreter/encode.go b/runtime/interpreter/encode.go index 198ecd0e81..1965fc19ab 100644 --- a/runtime/interpreter/encode.go +++ b/runtime/interpreter/encode.go @@ -267,7 +267,7 @@ func (v CharacterValue) Encode(e *atree.Encoder) error { if err != nil { return err } - return e.CBOR.EncodeString(string(v)) + return e.CBOR.EncodeString(v.Str) } // Encode encodes the value as a CBOR string diff --git a/runtime/interpreter/value.go b/runtime/interpreter/value.go index 824f2bcb79..20e8be0a47 100644 --- a/runtime/interpreter/value.go +++ b/runtime/interpreter/value.go @@ -805,10 +805,16 @@ func (BoolValue) ChildStorables() []atree.Storable { // CharacterValue represents a Cadence character, which is a Unicode extended grapheme cluster. // Hence, use a Go string to be able to hold multiple Unicode code points (Go runes). // It should consist of exactly one grapheme cluster -type CharacterValue string +type CharacterValue struct { + Str string + UnnormalizedStr string +} func NewUnmeteredCharacterValue(r string) CharacterValue { - return CharacterValue(norm.NFC.String(r)) + return CharacterValue{ + Str: norm.NFC.String(r), + UnnormalizedStr: r, + } } func NewCharacterValue( @@ -823,12 +829,12 @@ func NewCharacterValue( return NewUnmeteredCharacterValue(character) } -var _ Value = CharacterValue("a") -var _ atree.Storable = CharacterValue("a") -var _ EquatableValue = CharacterValue("a") -var _ ComparableValue = CharacterValue("a") -var _ HashableValue = CharacterValue("a") -var _ MemberAccessibleValue = CharacterValue("a") +var _ Value = CharacterValue{} +var _ atree.Storable = CharacterValue{} +var _ EquatableValue = CharacterValue{} +var _ ComparableValue = CharacterValue{} +var _ HashableValue = CharacterValue{} +var _ MemberAccessibleValue = CharacterValue{} func (CharacterValue) isValue() {} @@ -849,7 +855,7 @@ func (CharacterValue) IsImportable(_ *Interpreter) bool { } func (v CharacterValue) String() string { - return format.String(string(v)) + return format.String(v.Str) } func (v CharacterValue) RecursiveString(_ SeenReferences) string { @@ -857,7 +863,7 @@ func (v CharacterValue) RecursiveString(_ SeenReferences) string { } func (v CharacterValue) MeteredString(memoryGauge common.MemoryGauge, _ SeenReferences) string { - l := format.FormattedStringLength(string(v)) + l := format.FormattedStringLength(v.Str) common.UseMemory(memoryGauge, common.NewRawStringMemoryUsage(l)) return v.String() } @@ -867,7 +873,7 @@ func (v CharacterValue) Equal(_ *Interpreter, _ LocationRange, other Value) bool if !ok { return false } - return v == otherChar + return v.Str == otherChar.Str } func (v CharacterValue) Less(_ *Interpreter, other ComparableValue, _ LocationRange) BoolValue { @@ -875,7 +881,7 @@ func (v CharacterValue) Less(_ *Interpreter, other ComparableValue, _ LocationRa if !ok { panic(errors.NewUnreachableError()) } - return v < otherChar + return v.Str < otherChar.Str } func (v CharacterValue) LessEqual(_ *Interpreter, other ComparableValue, _ LocationRange) BoolValue { @@ -883,7 +889,7 @@ func (v CharacterValue) LessEqual(_ *Interpreter, other ComparableValue, _ Locat if !ok { panic(errors.NewUnreachableError()) } - return v <= otherChar + return v.Str <= otherChar.Str } func (v CharacterValue) Greater(_ *Interpreter, other ComparableValue, _ LocationRange) BoolValue { @@ -891,7 +897,7 @@ func (v CharacterValue) Greater(_ *Interpreter, other ComparableValue, _ Locatio if !ok { panic(errors.NewUnreachableError()) } - return v > otherChar + return v.Str > otherChar.Str } func (v CharacterValue) GreaterEqual(_ *Interpreter, other ComparableValue, _ LocationRange) BoolValue { @@ -899,11 +905,11 @@ func (v CharacterValue) GreaterEqual(_ *Interpreter, other ComparableValue, _ Lo if !ok { panic(errors.NewUnreachableError()) } - return v >= otherChar + return v.Str >= otherChar.Str } func (v CharacterValue) HashInput(_ *Interpreter, _ LocationRange, scratch []byte) []byte { - s := []byte(string(v)) + s := []byte(v.Str) length := 1 + len(s) var buffer []byte if length <= len(scratch) { @@ -960,7 +966,7 @@ func (CharacterValue) DeepRemove(_ *Interpreter) { } func (v CharacterValue) ByteSize() uint32 { - return cborTagSize + getBytesCBORSize([]byte(v)) + return cborTagSize + getBytesCBORSize([]byte(v.Str)) } func (v CharacterValue) StoredValue(_ atree.SlabStorage) (atree.Value, error) { @@ -980,21 +986,21 @@ func (v CharacterValue) GetMember(interpreter *Interpreter, _ LocationRange, nam func(invocation Invocation) Value { interpreter := invocation.Interpreter - memoryUsage := common.NewStringMemoryUsage(len(v)) + memoryUsage := common.NewStringMemoryUsage(len(v.Str)) return NewStringValue( interpreter, memoryUsage, func() string { - return string(v) + return v.Str }, ) }, ) case sema.CharacterTypeUtf8FieldName: - common.UseMemory(interpreter, common.NewBytesMemoryUsage(len(v))) - return ByteSliceToByteArrayValue(interpreter, []byte(v)) + common.UseMemory(interpreter, common.NewBytesMemoryUsage(len(v.Str))) + return ByteSliceToByteArrayValue(interpreter, []byte(v.Str)) } return nil } diff --git a/runtime/interpreter/value_string.go b/runtime/interpreter/value_string.go index 5b2501a945..2b526717d0 100644 --- a/runtime/interpreter/value_string.go +++ b/runtime/interpreter/value_string.go @@ -95,10 +95,10 @@ func stringFunctionFromCharacters(invocation Invocation) Value { common.UseMemory(inter, common.MemoryUsage{ Kind: common.MemoryKindStringValue, - Amount: uint64(len(character)), + Amount: uint64(len(character.Str)), }, ) - builder.WriteString(string(character)) + builder.WriteString(character.Str) return true }) From d17e05f27f5e798750accf645580f88eb8ff4436 Mon Sep 17 00:00:00 2001 From: Supun Setunga Date: Tue, 16 Jan 2024 10:20:28 -0800 Subject: [PATCH 09/11] Remove test that is no longer valid --- migrations/entitlements/migration_test.go | 241 ---------------------- 1 file changed, 241 deletions(-) diff --git a/migrations/entitlements/migration_test.go b/migrations/entitlements/migration_test.go index b1217e4ed9..fb2b6fef52 100644 --- a/migrations/entitlements/migration_test.go +++ b/migrations/entitlements/migration_test.go @@ -1867,244 +1867,3 @@ func TestMigrateDictOfValues(t *testing.T) { ref.Authorization, ) } - -func TestMigrateDictOfWithTypeValueKey(t *testing.T) { - t.Parallel() - - address1 := [8]byte{0, 0, 0, 0, 0, 0, 0, 1} - address2 := [8]byte{0, 0, 0, 0, 0, 0, 0, 2} - - storage := NewTestLedger(nil, nil) - rt := NewTestInterpreterRuntime() - - accountCodes := map[common.Location][]byte{} - interfaces := map[common.Location]*TestRuntimeInterface{} - - runtimeInterface1 := &TestRuntimeInterface{ - Storage: storage, - OnEmitEvent: func(event cadence.Event) error { - return nil - }, - OnGetSigningAccounts: func() ([]runtime.Address, error) { - return []runtime.Address{address1}, nil - }, - OnGetCode: func(location common.Location) (bytes []byte, err error) { - return accountCodes[location], nil - }, - OnResolveLocation: MultipleIdentifierLocationResolver, - OnGetAccountContractCode: func(location common.AddressLocation) (code []byte, err error) { - code = accountCodes[location] - return code, nil - }, - } - runtimeInterface1.OnUpdateAccountContractCode = func(location common.AddressLocation, code []byte) error { - accountCodes[location] = code - interfaces[location] = runtimeInterface1 - return nil - } - - runtimeInterface2 := &TestRuntimeInterface{ - Storage: storage, - OnEmitEvent: func(event cadence.Event) error { - return nil - }, - OnGetCode: func(location common.Location) (bytes []byte, err error) { - return accountCodes[location], nil - }, - OnGetSigningAccounts: func() ([]runtime.Address, error) { - return []runtime.Address{address2}, nil - }, - OnResolveLocation: MultipleIdentifierLocationResolver, - OnGetAccountContractCode: func(location common.AddressLocation) (code []byte, err error) { - code = accountCodes[location] - return code, nil - }, - } - runtimeInterface2.OnUpdateAccountContractCode = func(location common.AddressLocation, code []byte) error { - accountCodes[location] = code - interfaces[location] = runtimeInterface2 - return nil - } - - nextTransactionLocation := NewTransactionLocationGenerator() - - oldContract := []byte(` - access(all) contract C { - access(all) resource R { - access(all) fun foo() {} - } - access(all) fun makeR(): @R { - return <- create R() - } - } - `) - - contract := []byte(` - access(all) contract C { - access(all) entitlement E - access(all) resource R { - access(E) fun foo() {} - } - access(all) fun makeR(): @R { - return <- create R() - } - } - `) - - saveValues := []byte(` - import C from 0x1 - - transaction { - prepare(signer: auth(Storage, Capabilities) &Account) { - let r1 <- C.makeR() - let r2 <- C.makeR() - let rType = ReferenceType(entitlements: [], type: r1.getType())! - signer.storage.save(<-r1, to: /storage/foo) - signer.storage.save(<-r2, to: /storage/bar) - let cap1 = signer.capabilities.storage.issue<&C.R>(/storage/foo) - let cap2 = signer.capabilities.storage.issue<&C.R>(/storage/bar) - let arr = {rType: cap1, Type(): cap2} - signer.storage.save(arr, to: /storage/caps) - } - } - `) - - // Deploy contract to 0x1 - err := rt.ExecuteTransaction( - runtime.Script{ - Source: DeploymentTransaction("C", oldContract), - }, - runtime.Context{ - Interface: runtimeInterface1, - Location: nextTransactionLocation(), - }, - ) - require.NoError(t, err) - - // Execute transaction on 0x2 - err = rt.ExecuteTransaction( - runtime.Script{ - Source: saveValues, - }, - runtime.Context{ - Interface: runtimeInterface2, - Location: nextTransactionLocation(), - }, - ) - require.NoError(t, err) - - // update contract on 0x1 - err = rt.ExecuteTransaction( - runtime.Script{ - Source: UpdateTransaction("C", contract), - }, - runtime.Context{ - Interface: runtimeInterface1, - Location: nextTransactionLocation(), - }, - ) - require.NoError(t, err) - - runtimeStorage := runtime.NewStorage(storage, nil) - - inter, err := interpreter.NewInterpreter( - nil, - utils.TestLocation, - &interpreter.Config{ - Storage: runtimeStorage, - AtreeValueValidationEnabled: false, - AtreeStorageValidationEnabled: false, - ImportLocationHandler: func(inter *interpreter.Interpreter, location common.Location) interpreter.Import { - program, err := rt.ParseAndCheckProgram( - accountCodes[location], - runtime.Context{ - Interface: interfaces[location], - Location: location, - }, - ) - require.NoError(t, err) - - subInterpreter, err := inter.NewSubInterpreter(program, location) - require.NoError(t, err) - - return interpreter.InterpreterImport{ - Interpreter: subInterpreter, - } - }, - }, - ) - require.NoError(t, err) - - storageIdentifier := common.PathDomainStorage.Identifier() - storageMap := runtimeStorage.GetStorageMap(address2, storageIdentifier, false) - require.NotNil(t, storageMap) - require.Greater(t, storageMap.Count(), uint64(0)) - - // Migrate - - migration := migrations.NewStorageMigration(inter, runtimeStorage) - pathMigrator := migration.NewValueMigrationsPathMigrator(nil, NewEntitlementsMigration(inter)) - migration.Migrate( - &migrations.AddressSliceIterator{ - Addresses: []common.Address{ - address1, - address2, - }, - }, - pathMigrator, - ) - - dictValue := storageMap.ReadValue(nil, interpreter.StringStorageMapKey("caps")) - require.IsType(t, &interpreter.DictionaryValue{}, dictValue) - dictionaryValue := dictValue.(*interpreter.DictionaryValue) - - valueType := dictionaryValue.Type.ValueType - require.IsType(t, &interpreter.CapabilityStaticType{}, valueType) - capElementType := valueType.(*interpreter.CapabilityStaticType) - require.IsType(t, &interpreter.ReferenceStaticType{}, capElementType.BorrowType) - ref := capElementType.BorrowType.(*interpreter.ReferenceStaticType) - require.Equal(t, - interpreter.NewEntitlementSetAuthorization( - inter, - func() []common.TypeID { return []common.TypeID{"A.0000000000000001.C.E"} }, - 1, - sema.Conjunction, - ), - ref.Authorization, - ) - - rTypeKey := interpreter.NewTypeValue(nil, ref) - intTypeKey := interpreter.NewTypeValue(nil, interpreter.PrimitiveStaticTypeInt) - - cap1, present := dictionaryValue.Get(inter, interpreter.EmptyLocationRange, rTypeKey) - require.True(t, present) - require.IsType(t, &interpreter.CapabilityValue{}, cap1) - capValue := cap1.(*interpreter.CapabilityValue) - require.IsType(t, &interpreter.ReferenceStaticType{}, capValue.BorrowType) - ref = capValue.BorrowType.(*interpreter.ReferenceStaticType) - require.Equal(t, - interpreter.NewEntitlementSetAuthorization( - inter, - func() []common.TypeID { return []common.TypeID{"A.0000000000000001.C.E"} }, - 1, - sema.Conjunction, - ), - ref.Authorization, - ) - - cap2, present := dictionaryValue.Get(inter, interpreter.EmptyLocationRange, intTypeKey) - require.True(t, present) - require.IsType(t, &interpreter.CapabilityValue{}, cap2) - capValue = cap1.(*interpreter.CapabilityValue) - require.IsType(t, &interpreter.ReferenceStaticType{}, capValue.BorrowType) - ref = capValue.BorrowType.(*interpreter.ReferenceStaticType) - require.Equal(t, - interpreter.NewEntitlementSetAuthorization( - inter, - func() []common.TypeID { return []common.TypeID{"A.0000000000000001.C.E"} }, - 1, - sema.Conjunction, - ), - ref.Authorization, - ) -} From 8d090141be75eb50c06576b67c33b8053a2f4dc5 Mon Sep 17 00:00:00 2001 From: Supun Setunga Date: Tue, 16 Jan 2024 15:14:24 -0800 Subject: [PATCH 10/11] Improve migrations: Handle authorization in reference types --- migrations/entitlements/migration.go | 13 ++++++++++++- migrations/legacy_reference_type.go | 8 ++++++-- runtime/interpreter/decode.go | 12 +++++++++--- runtime/interpreter/statictype.go | 3 ++- 4 files changed, 29 insertions(+), 7 deletions(-) diff --git a/migrations/entitlements/migration.go b/migrations/entitlements/migration.go index 5f893da9e5..8614c89f67 100644 --- a/migrations/entitlements/migration.go +++ b/migrations/entitlements/migration.go @@ -131,6 +131,13 @@ func ConvertValueToEntitlements( referenceValue.Authorization, interpreter.ConvertSemaToStaticType(inter, referenceValue.BorrowedType), ) + case interpreter.LinkValue: //nolint:staticcheck + // Link values are not supposed to reach here. + // But it could, if the type used in the link is not migrated, + // then the link values would be left un-migrated. + // These need to be skipped specifically, otherwise `v.StaticType(inter)` will panic. + return nil + default: staticType = v.StaticType(inter) } @@ -254,10 +261,14 @@ func ConvertValueToEntitlements( return nil } - convertedType, _ := ConvertToEntitledType( + convertedType, converted := ConvertToEntitledType( inter.MustConvertStaticToSemaType(v.Type), ) + if !converted { + return nil + } + // convert the static type of the value entitledStaticType := interpreter.ConvertSemaToStaticType( inter, diff --git a/migrations/legacy_reference_type.go b/migrations/legacy_reference_type.go index 081f85c0a0..ca3ef3bbde 100644 --- a/migrations/legacy_reference_type.go +++ b/migrations/legacy_reference_type.go @@ -33,9 +33,13 @@ type LegacyReferenceType struct { var _ interpreter.StaticType = &LegacyReferenceType{} func (t *LegacyReferenceType) ID() common.TypeID { - isAuthorized := t.Authorization == interpreter.UnauthorizedAccess borrowedType := t.ReferencedType - return common.TypeID(formatReferenceType(isAuthorized, string(borrowedType.ID()))) + return common.TypeID( + formatReferenceType( + t.LegacyIsAuthorized, + string(borrowedType.ID()), + ), + ) } func formatReferenceType( diff --git a/runtime/interpreter/decode.go b/runtime/interpreter/decode.go index 79579a0aa8..cc7e777783 100644 --- a/runtime/interpreter/decode.go +++ b/runtime/interpreter/decode.go @@ -1772,9 +1772,11 @@ func (d TypeDecoder) decodeReferenceStaticType() (StaticType, error) { return nil, err } + var isAuthorized bool + if t == cbor.BoolType { // if we saw a bool here, this is a reference encoded in the old format - _, err := d.decoder.DecodeBool() + isAuthorized, err = d.decoder.DecodeBool() if err != nil { return nil, err } @@ -1804,11 +1806,15 @@ func (d TypeDecoder) decodeReferenceStaticType() (StaticType, error) { ) } - return NewReferenceStaticType( + referenceType := NewReferenceStaticType( d.memoryGauge, authorization, staticType, - ), nil + ) + + referenceType.LegacyIsAuthorized = isAuthorized + + return referenceType, nil } func (d TypeDecoder) decodeDictionaryStaticType() (StaticType, error) { diff --git a/runtime/interpreter/statictype.go b/runtime/interpreter/statictype.go index c993c9b983..ceae5ff3a4 100644 --- a/runtime/interpreter/statictype.go +++ b/runtime/interpreter/statictype.go @@ -759,7 +759,8 @@ func (a EntitlementMapAuthorization) Equal(other Authorization) bool { type ReferenceStaticType struct { Authorization Authorization // ReferencedType is type of the referenced value (the type of the target) - ReferencedType StaticType + ReferencedType StaticType + LegacyIsAuthorized bool } var _ StaticType = &ReferenceStaticType{} From efd96205743b1df5bf4ed6cf6bff56162f03d736 Mon Sep 17 00:00:00 2001 From: Supun Setunga Date: Thu, 18 Jan 2024 09:27:08 -0800 Subject: [PATCH 11/11] Add rehashing test for entitlements --- migrations/entitlements/migration_test.go | 240 ++++++++++++++++++++++ migrations/legacy_reference_type.go | 27 +++ 2 files changed, 267 insertions(+) diff --git a/migrations/entitlements/migration_test.go b/migrations/entitlements/migration_test.go index 27c830cdfe..0b43d010cb 100644 --- a/migrations/entitlements/migration_test.go +++ b/migrations/entitlements/migration_test.go @@ -24,6 +24,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/onflow/atree" + "github.com/onflow/cadence" "github.com/onflow/cadence/migrations" "github.com/onflow/cadence/migrations/account_type" @@ -1958,3 +1960,241 @@ func TestConvertMigratedAccountTypes(t *testing.T) { test(ty) } } + +var testAddress = common.Address{0x42} + +var _ migrations.Reporter = &testReporter{} + +type testReporter struct { + migratedPaths map[interpreter.AddressPath]struct{} +} + +func newTestReporter() *testReporter { + return &testReporter{ + migratedPaths: map[interpreter.AddressPath]struct{}{}, + } +} + +func (t *testReporter) Migrated( + addressPath interpreter.AddressPath, + _ string, +) { + t.migratedPaths[addressPath] = struct{}{} +} + +func (t *testReporter) Error( + _ interpreter.AddressPath, + _ string, + _ error, +) { +} + +func TestRehash(t *testing.T) { + + t.Parallel() + + locationRange := interpreter.EmptyLocationRange + + ledger := NewTestLedger(nil, nil) + + storageMapKey := interpreter.StringStorageMapKey("dict") + newTestValue := func() interpreter.Value { + return interpreter.NewUnmeteredStringValue("test") + } + + const fooBarQualifiedIdentifier = "Foo.Bar" + account := common.Address{0x42} + fooAddressLocation := common.NewAddressLocation(nil, account, "Foo") + + newStorageAndInterpreter := func(t *testing.T) (*runtime.Storage, *interpreter.Interpreter) { + storage := runtime.NewStorage(ledger, nil) + inter, err := interpreter.NewInterpreter( + nil, + utils.TestLocation, + &interpreter.Config{ + Storage: storage, + AtreeValueValidationEnabled: false, + AtreeStorageValidationEnabled: true, + }, + ) + require.NoError(t, err) + + return storage, inter + } + + newCompositeType := func() *interpreter.CompositeStaticType { + return interpreter.NewCompositeStaticType( + nil, + fooAddressLocation, + fooBarQualifiedIdentifier, + common.NewTypeIDFromQualifiedName( + nil, + fooAddressLocation, + fooBarQualifiedIdentifier, + ), + ) + } + + entitlementSetAuthorization := sema.NewEntitlementSetAccess( + []*sema.EntitlementType{ + sema.NewEntitlementType( + nil, + fooAddressLocation, + "E", + ), + }, + sema.Conjunction, + ) + + t.Run("prepare", func(t *testing.T) { + + storage, inter := newStorageAndInterpreter(t) + + dictionaryStaticType := interpreter.NewDictionaryStaticType( + nil, + interpreter.PrimitiveStaticTypeMetaType, + interpreter.PrimitiveStaticTypeString, + ) + dictValue := interpreter.NewDictionaryValue(inter, locationRange, dictionaryStaticType) + + refType := interpreter.NewReferenceStaticType( + nil, + interpreter.UnauthorizedAccess, + newCompositeType(), + ) + refType.LegacyIsAuthorized = true + + legacyRefType := &migrations.LegacyReferenceType{ + ReferenceStaticType: refType, + } + + typeValue := interpreter.NewUnmeteredTypeValue(legacyRefType) + + dictValue.Insert( + inter, + locationRange, + typeValue, + newTestValue(), + ) + + // Note: ID is in the old format + assert.Equal(t, + common.TypeID("auth&A.4200000000000000.Foo.Bar"), + legacyRefType.ID(), + ) + + storageMap := storage.GetStorageMap( + testAddress, + common.PathDomainStorage.Identifier(), + true, + ) + + storageMap.SetValue(inter, + storageMapKey, + dictValue.Transfer( + inter, + locationRange, + atree.Address(testAddress), + false, + nil, + nil, + ), + ) + + err := storage.Commit(inter, false) + require.NoError(t, err) + }) + + t.Run("migrate", func(t *testing.T) { + + storage, inter := newStorageAndInterpreter(t) + + inter.SharedState.Config.CompositeTypeHandler = func(location common.Location, typeID interpreter.TypeID) *sema.CompositeType { + + compositeType := &sema.CompositeType{ + Location: fooAddressLocation, + Identifier: fooBarQualifiedIdentifier, + Kind: common.CompositeKindStructure, + } + + compositeType.Members = sema.MembersAsMap([]*sema.Member{ + sema.NewUnmeteredFunctionMember( + compositeType, + entitlementSetAuthorization, + "sayHello", + &sema.FunctionType{}, + "", + ), + }) + + return compositeType + } + + migration := migrations.NewStorageMigration(inter, storage) + + reporter := newTestReporter() + + migration.Migrate( + &migrations.AddressSliceIterator{ + Addresses: []common.Address{ + testAddress, + }, + }, + migration.NewValueMigrationsPathMigrator( + reporter, + NewEntitlementsMigration(inter), + ), + ) + + err := migration.Commit() + require.NoError(t, err) + + require.Equal(t, + map[interpreter.AddressPath]struct{}{ + { + Address: testAddress, + Path: interpreter.PathValue{ + Domain: common.PathDomainStorage, + Identifier: string(storageMapKey), + }, + }: {}, + }, + reporter.migratedPaths, + ) + }) + + t.Run("load", func(t *testing.T) { + + storage, inter := newStorageAndInterpreter(t) + + storageMap := storage.GetStorageMap(testAddress, common.PathDomainStorage.Identifier(), false) + storedValue := storageMap.ReadValue(inter, storageMapKey) + + require.IsType(t, &interpreter.DictionaryValue{}, storedValue) + + dictValue := storedValue.(*interpreter.DictionaryValue) + + refType := interpreter.NewReferenceStaticType( + nil, + interpreter.ConvertSemaAccessToStaticAuthorization(nil, entitlementSetAuthorization), + newCompositeType(), + ) + + typeValue := interpreter.NewUnmeteredTypeValue(refType) + + // Note: ID is in the new format + assert.Equal(t, + common.TypeID("auth(A.4200000000000000.E)&A.4200000000000000.Foo.Bar"), + refType.ID(), + ) + + value, ok := dictValue.Get(inter, locationRange, typeValue) + require.True(t, ok) + + require.IsType(t, &interpreter.StringValue{}, value) + require.Equal(t, + newTestValue(), + value.(*interpreter.StringValue), + ) + }) +} diff --git a/migrations/legacy_reference_type.go b/migrations/legacy_reference_type.go index ca3ef3bbde..0e786b634c 100644 --- a/migrations/legacy_reference_type.go +++ b/migrations/legacy_reference_type.go @@ -21,6 +21,8 @@ package migrations import ( "strings" + "github.com/fxamacker/cbor/v2" + "github.com/onflow/cadence/runtime/common" "github.com/onflow/cadence/runtime/interpreter" ) @@ -54,3 +56,28 @@ func formatReferenceType( builder.WriteString(typeString) return builder.String() } + +func (t *LegacyReferenceType) Encode(e *cbor.StreamEncoder) error { + // Encode tag number and array head + err := e.EncodeRawBytes([]byte{ + // tag number + 0xd8, interpreter.CBORTagReferenceStaticType, + // array, 2 items follow + 0x82, + }) + if err != nil { + return err + } + + // Encode the `LegacyIsAuthorized` flag instead of the `Authorization`. + // This is how it was done in pre-1.0. + // Decode already supports decoding this flag, for backward compatibility. + // Encode authorized at array index encodedReferenceStaticTypeAuthorizedFieldKey + err = e.EncodeBool(t.LegacyIsAuthorized) + if err != nil { + return err + } + + // Encode type at array index encodedReferenceStaticTypeTypeFieldKey + return t.ReferencedType.Encode(e) +}