diff --git a/cmd/util/ledger/migrations/cadence.go b/cmd/util/ledger/migrations/cadence.go index 8b795b1082d..d78538b8d9a 100644 --- a/cmd/util/ledger/migrations/cadence.go +++ b/cmd/util/ledger/migrations/cadence.go @@ -41,7 +41,10 @@ func NewInterfaceTypeConversionRules(chainID flow.ChainID) StaticTypeMigrationRu } } -func NewCompositeTypeConversionRules(chainID flow.ChainID) StaticTypeMigrationRules { +func NewCompositeTypeConversionRules( + chainID flow.ChainID, + legacyTypeRequirements *LegacyTypeRequirements, +) StaticTypeMigrationRules { systemContracts := systemcontracts.SystemContractsForChain(chainID) oldFungibleTokenVaultCompositeType, newFungibleTokenVaultType := @@ -51,21 +54,42 @@ func NewCompositeTypeConversionRules(chainID flow.ChainID) StaticTypeMigrationRu oldNonFungibleTokenCollectionCompositeType, newNonFungibleTokenCollectionType := nonFungibleTokenCompositeToInterfaceRule(systemContracts, "Collection") - return StaticTypeMigrationRules{ + rules := StaticTypeMigrationRules{ oldFungibleTokenVaultCompositeType.ID(): newFungibleTokenVaultType, oldNonFungibleTokenNFTCompositeType.ID(): newNonFungibleTokenNFTType, oldNonFungibleTokenCollectionCompositeType.ID(): newNonFungibleTokenCollectionType, } + + for _, typeRequirement := range legacyTypeRequirements.typeRequirements { + oldType, newType := compositeToInterfaceRule( + typeRequirement.Address, + typeRequirement.ContractName, + typeRequirement.TypeName, + ) + + rules[oldType.ID()] = newType + } + + return rules } func NewCadence1InterfaceStaticTypeConverter(chainID flow.ChainID) statictypes.InterfaceTypeConverterFunc { - rules := NewInterfaceTypeConversionRules(chainID) - return NewStaticTypeMigration[*interpreter.InterfaceStaticType](rules) + return NewStaticTypeMigration[*interpreter.InterfaceStaticType]( + func() StaticTypeMigrationRules { + return NewInterfaceTypeConversionRules(chainID) + }, + ) } -func NewCadence1CompositeStaticTypeConverter(chainID flow.ChainID) statictypes.CompositeTypeConverterFunc { - rules := NewCompositeTypeConversionRules(chainID) - return NewStaticTypeMigration[*interpreter.CompositeStaticType](rules) +func NewCadence1CompositeStaticTypeConverter( + chainID flow.ChainID, + legacyTypeRequirements *LegacyTypeRequirements, +) statictypes.CompositeTypeConverterFunc { + return NewStaticTypeMigration[*interpreter.CompositeStaticType]( + func() StaticTypeMigrationRules { + return NewCompositeTypeConversionRules(chainID, legacyTypeRequirements) + }, + ) } func nonFungibleTokenCompositeToInterfaceRule( @@ -77,11 +101,26 @@ func nonFungibleTokenCompositeToInterfaceRule( ) { contract := systemContracts.NonFungibleToken - qualifiedIdentifier := fmt.Sprintf("%s.%s", contract.Name, identifier) + return compositeToInterfaceRule( + common.Address(contract.Address), + contract.Name, + identifier, + ) +} + +func compositeToInterfaceRule( + address common.Address, + contractName string, + typeName string, +) ( + *interpreter.CompositeStaticType, + *interpreter.IntersectionStaticType, +) { + qualifiedIdentifier := fmt.Sprintf("%s.%s", contractName, typeName) location := common.AddressLocation{ - Address: common.Address(contract.Address), - Name: contract.Name, + Address: address, + Name: contractName, } nftTypeID := location.TypeID(nil, qualifiedIdentifier) @@ -388,6 +427,7 @@ func NewCadence1ValueMigrations( log zerolog.Logger, rwf reporters.ReportWriterFactory, importantLocations map[common.AddressLocation]struct{}, + legacyTypeRequirements *LegacyTypeRequirements, opts Options, ) (migs []NamedMigration) { @@ -444,7 +484,7 @@ func NewCadence1ValueMigrations( rwf, errorMessageHandler, programs, - NewCadence1CompositeStaticTypeConverter(opts.ChainID), + NewCadence1CompositeStaticTypeConverter(opts.ChainID, legacyTypeRequirements), NewCadence1InterfaceStaticTypeConverter(opts.ChainID), storageDomainCapabilities, opts, @@ -535,10 +575,11 @@ const stagedContractUpdateMigrationName = "staged-contracts-update-migration" func NewCadence1ContractsMigrations( log zerolog.Logger, rwf reporters.ReportWriterFactory, + importantLocations map[common.AddressLocation]struct{}, + legacyTypeRequirements *LegacyTypeRequirements, opts Options, ) ( migs []NamedMigration, - importantLocations map[common.AddressLocation]struct{}, ) { stagedContractsMigrationOptions := StagedContractsMigrationOptions{ @@ -552,10 +593,10 @@ func NewCadence1ContractsMigrations( Burner: opts.BurnerContractChange, } - var systemContractsMigration *StagedContractsMigration - systemContractsMigration, importantLocations = NewSystemContractsMigration( + systemContractsMigration := NewSystemContractsMigration( log, rwf, + importantLocations, systemContractsMigrationOptions, ) @@ -564,6 +605,7 @@ func NewCadence1ContractsMigrations( "staged-contracts-migration", log, rwf, + legacyTypeRequirements, stagedContractsMigrationOptions, ).WithContractUpdateValidation(). WithStagedContractUpdates(opts.StagedContracts) @@ -621,7 +663,7 @@ func NewCadence1ContractsMigrations( }, ) - return migs, importantLocations + return migs } var testnetAccountsWithBrokenSlabReferences = func() map[common.Address]struct{} { @@ -748,9 +790,29 @@ func NewCadence1Migrations( } } - cadence1ContractsMigrations, importantLocations := NewCadence1ContractsMigrations( + importantLocations := make(map[common.AddressLocation]struct{}) + legacyTypeRequirements := &LegacyTypeRequirements{} + + cadenceTypeRequirementsExtractor := NewTypeRequirementsExtractingMigration( + log, + rwf, + importantLocations, + legacyTypeRequirements, + ) + + migs = append( + migs, + NamedMigration{ + Name: "extract-type-requirements", + Migrate: cadenceTypeRequirementsExtractor, + }, + ) + + cadence1ContractsMigrations := NewCadence1ContractsMigrations( log, rwf, + importantLocations, + legacyTypeRequirements, opts, ) @@ -765,6 +827,7 @@ func NewCadence1Migrations( log, rwf, importantLocations, + legacyTypeRequirements, opts, )..., ) diff --git a/cmd/util/ledger/migrations/cadence_values_migration_test.go b/cmd/util/ledger/migrations/cadence_values_migration_test.go index 4d30d26eb39..dcdb2ec6868 100644 --- a/cmd/util/ledger/migrations/cadence_values_migration_test.go +++ b/cmd/util/ledger/migrations/cadence_values_migration_test.go @@ -2692,6 +2692,246 @@ func TestStorageCapConsInferredBorrowTypeEntry_MarshalJSON(t *testing.T) { ) } +func TestTypeRequirementRemovalMigration(t *testing.T) { + t.Parallel() + + rwf := &testReportWriterFactory{} + + logWriter := &writer{} + logger := zerolog.New(logWriter).Level(zerolog.ErrorLevel) + + const nWorker = 2 + + const chainID = flow.Testnet + + payloads, err := newBootstrapPayloads(chainID) + require.NoError(t, err) + + registersByAccount, err := registers.NewByAccountFromPayloads(payloads) + require.NoError(t, err) + + storedAddress := common.Address(chainID.Chain().ServiceAddress()) + tiblesAddress := mustHexToAddress("e93c412c964bdf40") + + // Store a contract in `addressC`. + + migrationRuntime, err := NewInterpreterMigrationRuntime( + registersByAccount, + chainID, + InterpreterMigrationRuntimeConfig{}, + ) + require.NoError(t, err) + + storage := migrationRuntime.Storage + storageDomain := common.PathDomainStorage.Identifier() + + storageMap := storage.GetStorageMap( + storedAddress, + storageDomain, + true, + ) + + contractName := "TiblesProducer" + + // Store a value with the actual `TiblesProducer.Minter` type + storageMap.WriteValue( + migrationRuntime.Interpreter, + interpreter.StringStorageMapKey("a"), + interpreter.NewTypeValue( + nil, + interpreter.NewCompositeStaticTypeComputeTypeID( + nil, + common.AddressLocation{ + Name: contractName, + Address: tiblesAddress, + }, + "TiblesProducer.Minter", + ), + ), + ) + + // Store a value with a random `TiblesProducer.Minter` type (different address) + storageMap.WriteValue( + migrationRuntime.Interpreter, + interpreter.StringStorageMapKey("b"), + interpreter.NewTypeValue( + nil, + interpreter.NewCompositeStaticTypeComputeTypeID( + nil, + common.AddressLocation{ + Name: contractName, + Address: storedAddress, + }, + "TiblesProducer.Minter", + ), + ), + ) + + // Commit + + err = storage.NondeterministicCommit(migrationRuntime.Interpreter, false) + require.NoError(t, err) + + // finalize the transaction + result, err := migrationRuntime.TransactionState.FinalizeMainTransaction() + require.NoError(t, err) + + // Merge the changes into the registers + + expectedAddresses := map[flow.Address]struct{}{ + flow.Address(storedAddress): {}, + } + + err = registers.ApplyChanges( + registersByAccount, + result.WriteSet, + expectedAddresses, + logger, + ) + require.NoError(t, err) + + // Set contract code + + oldCode := ` + pub contract interface TiblesProducer { + + pub struct ContentLocation {} + pub struct interface IContentLocation {} + + pub resource interface IContent { + access(contract) let contentIdsToPaths: {String: TiblesProducer.ContentLocation} + pub fun getMetadata(contentId: String): {String: AnyStruct}? + } + + pub resource interface IProducer { + access(contract) let minters: @{String: Minter} + } + + pub resource Producer: IContent, IProducer { + access(contract) let minters: @{String: Minter} + } + + pub resource interface IMinter { + pub let id: String + pub var lastMintNumber: UInt32 + pub let contentCapability: Capability + pub fun mintNext() + } + + pub resource Minter: IMinter { + pub let id: String + pub var lastMintNumber: UInt32 + pub let contentCapability: Capability + pub fun mintNext() + } + } + ` + + err = registersByAccount.Set( + string(tiblesAddress[:]), + flow.ContractKey(contractName), + []byte(oldCode), + ) + require.NoError(t, err) + + encodedContractNames, err := environment.EncodeContractNames([]string{contractName}) + require.NoError(t, err) + + err = registersByAccount.Set( + string(tiblesAddress[:]), + flow.ContractNamesKey, + encodedContractNames, + ) + require.NoError(t, err) + + // Migrate + + // TODO: EVM contract is not deployed in snapshot yet, so can't update it + const evmContractChange = EVMContractChangeNone + + const burnerContractChange = BurnerContractChangeUpdate + + migrations := NewCadence1Migrations( + logger, + t.TempDir(), + rwf, + Options{ + NWorker: nWorker, + ChainID: chainID, + EVMContractChange: evmContractChange, + BurnerContractChange: burnerContractChange, + VerboseErrorOutput: true, + }, + ) + + for _, migration := range migrations { + err = migration.Migrate(registersByAccount) + require.NoError( + t, + err, + "migration `%s` failed, logs: %v", + migration.Name, + logWriter.logs, + ) + } + + // Check reporters + + reporter := rwf.reportWriters[typeRequirementExtractingReporterName] + require.NotNil(t, reporter) + require.Len(t, reporter.entries, 3) + + assert.Equal( + t, + []any{ + typeRequirementRemovalEntry{ + TypeRequirement{ + Address: tiblesAddress, + ContractName: contractName, + TypeName: "ContentLocation", + }, + }, + typeRequirementRemovalEntry{ + TypeRequirement{ + Address: tiblesAddress, + ContractName: contractName, + TypeName: "Producer", + }, + }, + typeRequirementRemovalEntry{ + TypeRequirement{ + Address: tiblesAddress, + ContractName: contractName, + TypeName: "Minter", + }, + }, + }, + reporter.entries, + ) + + // Check account + + _, err = runScript( + chainID, + registersByAccount, + fmt.Sprintf( + //language=Cadence + ` + access(all) + fun main() { + let storage = getAuthAccount(%s).storage + assert(storage.copy(from: /storage/a)!.identifier == "{A.%s.TiblesProducer.Minter}") + assert(storage.copy(from: /storage/b)!.identifier == "A.%s.TiblesProducer.Minter") + } + `, + storedAddress.HexWithPrefix(), + tiblesAddress.Hex(), + storedAddress.Hex(), + ), + ) + require.NoError(t, err) +} + func runScript(chainID flow.ChainID, registersByAccount *registers.ByAccount, script string) (cadence.Value, error) { options := computation.DefaultFVMOptions(chainID, false, false) options = append(options, diff --git a/cmd/util/ledger/migrations/change_contract_code_migration.go b/cmd/util/ledger/migrations/change_contract_code_migration.go index a809228f467..6deefd278fd 100644 --- a/cmd/util/ledger/migrations/change_contract_code_migration.go +++ b/cmd/util/ledger/migrations/change_contract_code_migration.go @@ -286,25 +286,24 @@ type SystemContractsMigrationOptions struct { func NewSystemContractsMigration( log zerolog.Logger, rwf reporters.ReportWriterFactory, + locations map[common.AddressLocation]struct{}, options SystemContractsMigrationOptions, ) ( migration *StagedContractsMigration, - locations map[common.AddressLocation]struct{}, ) { migration = NewStagedContractsMigration( "SystemContractsMigration", "system-contracts-migration", log, rwf, + &LegacyTypeRequirements{}, // This is empty for system contracts options.StagedContractsMigrationOptions, ) - locations = make(map[common.AddressLocation]struct{}) - for _, change := range SystemContractChanges(options.ChainID, options) { migration.registerContractChange(change) locations[change.AddressLocation()] = struct{}{} } - return migration, locations + return migration } diff --git a/cmd/util/ledger/migrations/change_contract_code_migration_test.go b/cmd/util/ledger/migrations/change_contract_code_migration_test.go index 0e89df23f0a..e9bf4c7d0bd 100644 --- a/cmd/util/ledger/migrations/change_contract_code_migration_test.go +++ b/cmd/util/ledger/migrations/change_contract_code_migration_test.go @@ -81,7 +81,14 @@ func TestChangeContractCodeMigration(t *testing.T) { ChainID: flow.Emulator, VerboseErrorOutput: true, } - migration := NewStagedContractsMigration("test", "test", log, rwf, options) + migration := NewStagedContractsMigration( + "test", + "test", + log, + rwf, + &LegacyTypeRequirements{}, + options, + ) registersByAccount := registers.NewByAccount() @@ -113,7 +120,14 @@ func TestChangeContractCodeMigration(t *testing.T) { ChainID: flow.Emulator, VerboseErrorOutput: true, } - migration := NewStagedContractsMigration("test", "test", log, rwf, options) + migration := NewStagedContractsMigration( + "test", + "test", + log, + rwf, + &LegacyTypeRequirements{}, + options, + ) registersByAccount, err := registersForStagedContracts( StagedContract{ @@ -165,7 +179,14 @@ func TestChangeContractCodeMigration(t *testing.T) { ChainID: flow.Emulator, VerboseErrorOutput: true, } - migration := NewStagedContractsMigration("test", "test", log, rwf, options). + migration := NewStagedContractsMigration( + "test", + "test", + log, + rwf, + &LegacyTypeRequirements{}, + options, + ). WithStagedContractUpdates([]StagedContract{ { Address: common.Address(address1), @@ -226,7 +247,14 @@ func TestChangeContractCodeMigration(t *testing.T) { ChainID: flow.Emulator, VerboseErrorOutput: true, } - migration := NewStagedContractsMigration("test", "test", log, rwf, options). + migration := NewStagedContractsMigration( + "test", + "test", + log, + rwf, + &LegacyTypeRequirements{}, + options, + ). WithStagedContractUpdates([]StagedContract{ { Address: common.Address(address1), @@ -298,7 +326,14 @@ func TestChangeContractCodeMigration(t *testing.T) { ChainID: flow.Emulator, VerboseErrorOutput: true, } - migration := NewStagedContractsMigration("test", "test", log, rwf, options). + migration := NewStagedContractsMigration( + "test", + "test", + log, + rwf, + &LegacyTypeRequirements{}, + options, + ). WithStagedContractUpdates([]StagedContract{ { Address: common.Address(address1), @@ -377,7 +412,14 @@ func TestChangeContractCodeMigration(t *testing.T) { ChainID: flow.Emulator, VerboseErrorOutput: true, } - migration := NewStagedContractsMigration("test", "test", log, rwf, options). + migration := NewStagedContractsMigration( + "test", + "test", + log, + rwf, + &LegacyTypeRequirements{}, + options, + ). WithStagedContractUpdates([]StagedContract{ { Address: common.Address(address1), @@ -452,7 +494,14 @@ func TestChangeContractCodeMigration(t *testing.T) { ChainID: flow.Emulator, VerboseErrorOutput: true, } - migration := NewStagedContractsMigration("test", "test", log, rwf, options). + migration := NewStagedContractsMigration( + "test", + "test", + log, + rwf, + &LegacyTypeRequirements{}, + options, + ). WithStagedContractUpdates([]StagedContract{ { Address: common.Address(address1), @@ -514,7 +563,14 @@ func TestChangeContractCodeMigration(t *testing.T) { ChainID: flow.Emulator, VerboseErrorOutput: true, } - migration := NewStagedContractsMigration("test", "test", log, rwf, options). + migration := NewStagedContractsMigration( + "test", + "test", + log, + rwf, + &LegacyTypeRequirements{}, + options, + ). WithStagedContractUpdates([]StagedContract{ { Address: common.Address(address2), diff --git a/cmd/util/ledger/migrations/contract_checking_migration.go b/cmd/util/ledger/migrations/contract_checking_migration.go index 5f9b9fe524b..5140cc4a424 100644 --- a/cmd/util/ledger/migrations/contract_checking_migration.go +++ b/cmd/util/ledger/migrations/contract_checking_migration.go @@ -51,70 +51,15 @@ func NewContractCheckingMigration( return fmt.Errorf("failed to create interpreter migration runtime: %w", err) } - // Gather all contracts - - log.Info().Msg("Gathering contracts ...") - - contractsForPrettyPrinting := make(map[common.Location][]byte, contractCountEstimate) - - contracts := make([]AddressContract, 0, contractCountEstimate) - - err = registersByAccount.ForEachAccount(func(accountRegisters *registers.AccountRegisters) error { - owner := accountRegisters.Owner() - - encodedContractNames, err := accountRegisters.Get(owner, flow.ContractNamesKey) - if err != nil { - return err - } - - contractNames, err := environment.DecodeContractNames(encodedContractNames) - if err != nil { - return err - } - - for _, contractName := range contractNames { - - contractKey := flow.ContractKey(contractName) - - code, err := accountRegisters.Get(owner, contractKey) - if err != nil { - return err - } - - if len(bytes.TrimSpace(code)) == 0 { - continue - } - - address := common.Address([]byte(owner)) - location := common.AddressLocation{ - Address: address, - Name: contractName, - } - - contracts = append( - contracts, - AddressContract{ - location: location, - code: code, - }, - ) - - contractsForPrettyPrinting[location] = code - } - - return nil - }) + contracts, err := gatherContractsFromRegisters(registersByAccount, log) if err != nil { - return fmt.Errorf("failed to get contracts of accounts: %w", err) + return err } - sort.Slice(contracts, func(i, j int) bool { - a := contracts[i] - b := contracts[j] - return a.location.ID() < b.location.ID() - }) - - log.Info().Msgf("Gathered all contracts (%d)", len(contracts)) + contractsForPrettyPrinting := make(map[common.Location][]byte, len(contracts)) + for _, contract := range contracts { + contractsForPrettyPrinting[contract.location] = contract.code + } // Check all contracts @@ -135,6 +80,68 @@ func NewContractCheckingMigration( } } +func gatherContractsFromRegisters(registersByAccount *registers.ByAccount, log zerolog.Logger) ([]AddressContract, error) { + log.Info().Msg("Gathering contracts ...") + + contracts := make([]AddressContract, 0, contractCountEstimate) + + err := registersByAccount.ForEachAccount(func(accountRegisters *registers.AccountRegisters) error { + owner := accountRegisters.Owner() + + encodedContractNames, err := accountRegisters.Get(owner, flow.ContractNamesKey) + if err != nil { + return err + } + + contractNames, err := environment.DecodeContractNames(encodedContractNames) + if err != nil { + return err + } + + for _, contractName := range contractNames { + + contractKey := flow.ContractKey(contractName) + + code, err := accountRegisters.Get(owner, contractKey) + if err != nil { + return err + } + + if len(bytes.TrimSpace(code)) == 0 { + continue + } + + address := common.Address([]byte(owner)) + location := common.AddressLocation{ + Address: address, + Name: contractName, + } + + contracts = append( + contracts, + AddressContract{ + location: location, + code: code, + }, + ) + } + + return nil + }) + if err != nil { + return nil, fmt.Errorf("failed to get contracts of accounts: %w", err) + } + + sort.Slice(contracts, func(i, j int) bool { + a := contracts[i] + b := contracts[j] + return a.location.ID() < b.location.ID() + }) + + log.Info().Msgf("Gathered all contracts (%d)", len(contracts)) + return contracts, nil +} + func checkContract( contract AddressContract, log zerolog.Logger, diff --git a/cmd/util/ledger/migrations/staged_contracts_migration.go b/cmd/util/ledger/migrations/staged_contracts_migration.go index 72987e7adf3..54725538726 100644 --- a/cmd/util/ledger/migrations/staged_contracts_migration.go +++ b/cmd/util/ledger/migrations/staged_contracts_migration.go @@ -42,6 +42,7 @@ type StagedContractsMigration struct { contractNamesProvider stdlib.AccountContractNamesProvider reporter reporters.ReportWriter verboseErrorOutput bool + legacyTypeRequirements *LegacyTypeRequirements } type StagedContract struct { @@ -73,22 +74,23 @@ func NewStagedContractsMigration( reporterName string, log zerolog.Logger, rwf reporters.ReportWriterFactory, + legacyTypeRequirements *LegacyTypeRequirements, options StagedContractsMigrationOptions, ) *StagedContractsMigration { return &StagedContractsMigration{ - name: name, - log: log, - chainID: options.ChainID, - stagedContracts: map[common.Address]map[string]Contract{}, - contractsByLocation: map[common.Location][]byte{}, - reporter: rwf.ReportWriter(reporterName), - verboseErrorOutput: options.VerboseErrorOutput, + name: name, + log: log, + chainID: options.ChainID, + legacyTypeRequirements: legacyTypeRequirements, + stagedContracts: map[common.Address]map[string]Contract{}, + contractsByLocation: map[common.Location][]byte{}, + reporter: rwf.ReportWriter(reporterName), + verboseErrorOutput: options.VerboseErrorOutput, } } func (m *StagedContractsMigration) WithContractUpdateValidation() *StagedContractsMigration { m.enableUpdateValidation = true - m.userDefinedTypeChangeCheckFunc = NewUserDefinedTypeChangeCheckerFunc(m.chainID) return m } @@ -179,6 +181,12 @@ func (m *StagedContractsMigration) InitMigration( m.contractAdditionHandler = mr.ContractAdditionHandler m.contractNamesProvider = mr.ContractNamesProvider + // `legacyTypeRequirements` are populated by a previous migration. + // So it should only be used once the previous migrations are complete. + if m.enableUpdateValidation { + m.userDefinedTypeChangeCheckFunc = NewUserDefinedTypeChangeCheckerFunc(m.chainID, m.legacyTypeRequirements) + } + return nil } @@ -646,11 +654,12 @@ func StagedContractsFromCSV(path string) ([]StagedContract, error) { func NewUserDefinedTypeChangeCheckerFunc( chainID flow.ChainID, + legacyTypeRequirements *LegacyTypeRequirements, ) func(oldTypeID common.TypeID, newTypeID common.TypeID) (checked, valid bool) { typeChangeRules := map[common.TypeID]common.TypeID{} - compositeTypeRules := NewCompositeTypeConversionRules(chainID) + compositeTypeRules := NewCompositeTypeConversionRules(chainID, legacyTypeRequirements) for typeID, newStaticType := range compositeTypeRules { typeChangeRules[typeID] = newStaticType.ID() } diff --git a/cmd/util/ledger/migrations/staged_contracts_migration_test.go b/cmd/util/ledger/migrations/staged_contracts_migration_test.go index bb253248893..972177e634f 100644 --- a/cmd/util/ledger/migrations/staged_contracts_migration_test.go +++ b/cmd/util/ledger/migrations/staged_contracts_migration_test.go @@ -96,7 +96,14 @@ func TestStagedContractsMigration(t *testing.T) { VerboseErrorOutput: true, } - migration := NewStagedContractsMigration("test", "test", log, rwf, options). + migration := NewStagedContractsMigration( + "test", + "test", + log, + rwf, + &LegacyTypeRequirements{}, + options, + ). WithStagedContractUpdates(stagedContracts) registersByAccount, err := registersForStagedContracts( @@ -161,7 +168,14 @@ func TestStagedContractsMigration(t *testing.T) { VerboseErrorOutput: true, } - migration := NewStagedContractsMigration("test", "test", log, rwf, options). + migration := NewStagedContractsMigration( + "test", + "test", + log, + rwf, + &LegacyTypeRequirements{}, + options, + ). WithContractUpdateValidation() migration.WithStagedContractUpdates(stagedContracts) @@ -228,7 +242,14 @@ func TestStagedContractsMigration(t *testing.T) { VerboseErrorOutput: true, } - migration := NewStagedContractsMigration("test", "test", log, rwf, options). + migration := NewStagedContractsMigration( + "test", + "test", + log, + rwf, + &LegacyTypeRequirements{}, + options, + ). WithContractUpdateValidation(). WithStagedContractUpdates(stagedContracts) @@ -309,7 +330,14 @@ func TestStagedContractsMigration(t *testing.T) { } const reporterName = "test" - migration := NewStagedContractsMigration("test", reporterName, log, rwf, options). + migration := NewStagedContractsMigration( + "test", + reporterName, + log, + rwf, + &LegacyTypeRequirements{}, + options, + ). WithContractUpdateValidation(). WithStagedContractUpdates(stagedContracts) @@ -405,7 +433,14 @@ func TestStagedContractsMigration(t *testing.T) { } const reporterName = "test" - migration := NewStagedContractsMigration("test", reporterName, log, rwf, options). + migration := NewStagedContractsMigration( + "test", + reporterName, + log, + rwf, + &LegacyTypeRequirements{}, + options, + ). WithStagedContractUpdates(stagedContracts) registersByAccount, err := registersForStagedContracts( @@ -514,7 +549,14 @@ func TestStagedContractsMigration(t *testing.T) { VerboseErrorOutput: true, } - migration := NewStagedContractsMigration("test", "test", log, rwf, options). + migration := NewStagedContractsMigration( + "test", + "test", + log, + rwf, + &LegacyTypeRequirements{}, + options, + ). WithStagedContractUpdates(stagedContracts) registersByAccount, err := registersForStagedContracts( @@ -581,7 +623,14 @@ func TestStagedContractsMigration(t *testing.T) { VerboseErrorOutput: true, } - migration := NewStagedContractsMigration("test", "test", log, rwf, options). + migration := NewStagedContractsMigration( + "test", + "test", + log, + rwf, + &LegacyTypeRequirements{}, + options, + ). WithStagedContractUpdates(stagedContracts) // NOTE: no payloads @@ -736,7 +785,14 @@ func TestStagedContractsMigration(t *testing.T) { VerboseErrorOutput: true, } - migration := NewStagedContractsMigration("test", "test", log, rwf, options) + migration := NewStagedContractsMigration( + "test", + "test", + log, + rwf, + &LegacyTypeRequirements{}, + options, + ) accountOwner := string(accountAddress[:]) @@ -846,7 +902,14 @@ func TestStagedContractsWithImports(t *testing.T) { VerboseErrorOutput: true, } - migration := NewStagedContractsMigration("test", "test", log, rwf, options). + migration := NewStagedContractsMigration( + "test", + "test", + log, + rwf, + &LegacyTypeRequirements{}, + options, + ). WithStagedContractUpdates(stagedContracts) registersByAccount, err := registersForStagedContracts( @@ -945,7 +1008,14 @@ func TestStagedContractsWithImports(t *testing.T) { VerboseErrorOutput: true, } - migration := NewStagedContractsMigration("test", "test", log, rwf, options). + migration := NewStagedContractsMigration( + "test", + "test", + log, + rwf, + &LegacyTypeRequirements{}, + options, + ). WithContractUpdateValidation(). WithStagedContractUpdates(stagedContracts) @@ -1060,7 +1130,14 @@ func TestStagedContractsWithImports(t *testing.T) { VerboseErrorOutput: true, } - migration := NewStagedContractsMigration("test", "test", log, rwf, options). + migration := NewStagedContractsMigration( + "test", + "test", + log, + rwf, + &LegacyTypeRequirements{}, + options, + ). WithContractUpdateValidation(). WithStagedContractUpdates(stagedContracts) @@ -1180,7 +1257,14 @@ func TestStagedContractsWithImports(t *testing.T) { VerboseErrorOutput: true, } - migration := NewStagedContractsMigration("test", "test", log, rwf, options). + migration := NewStagedContractsMigration( + "test", + "test", + log, + rwf, + &LegacyTypeRequirements{}, + options, + ). WithContractUpdateValidation(). WithStagedContractUpdates(stagedContracts) @@ -1431,7 +1515,14 @@ func TestStagedContractsWithUpdateValidator(t *testing.T) { VerboseErrorOutput: true, } - migration := NewStagedContractsMigration("test", "test", log, rwf, options). + migration := NewStagedContractsMigration( + "test", + "test", + log, + rwf, + &LegacyTypeRequirements{}, + options, + ). WithContractUpdateValidation(). WithStagedContractUpdates(stagedContracts) @@ -1548,7 +1639,14 @@ func TestStagedContractsWithUpdateValidator(t *testing.T) { VerboseErrorOutput: true, } - migration := NewStagedContractsMigration("test", "test", log, rwf, options). + migration := NewStagedContractsMigration( + "test", + "test", + log, + rwf, + &LegacyTypeRequirements{}, + options, + ). WithContractUpdateValidation(). WithStagedContractUpdates(stagedContracts) @@ -1654,7 +1752,14 @@ func TestStagedContractsWithUpdateValidator(t *testing.T) { VerboseErrorOutput: true, } - migration := NewStagedContractsMigration("test", "test", log, rwf, options). + migration := NewStagedContractsMigration( + "test", + "test", + log, + rwf, + &LegacyTypeRequirements{}, + options, + ). WithContractUpdateValidation(). WithStagedContractUpdates(stagedContracts) @@ -1786,7 +1891,14 @@ func TestStagedContractConformanceChanges(t *testing.T) { VerboseErrorOutput: true, } - migration := NewStagedContractsMigration("test", "test", log, rwf, options). + migration := NewStagedContractsMigration( + "test", + "test", + log, + rwf, + &LegacyTypeRequirements{}, + options, + ). WithContractUpdateValidation(). WithStagedContractUpdates(stagedContracts) @@ -1919,7 +2031,14 @@ func TestStagedContractConformanceChanges(t *testing.T) { VerboseErrorOutput: true, } - migration := NewStagedContractsMigration("test", "test", log, rwf, options). + migration := NewStagedContractsMigration( + "test", + "test", + log, + rwf, + &LegacyTypeRequirements{}, + options, + ). WithContractUpdateValidation(). WithStagedContractUpdates(stagedContracts) @@ -2189,7 +2308,14 @@ func TestStagedContractsUpdateValidationErrors(t *testing.T) { VerboseErrorOutput: true, } - migration := NewStagedContractsMigration("test", "test", log, rwf, options). + migration := NewStagedContractsMigration( + "test", + "test", + log, + rwf, + &LegacyTypeRequirements{}, + options, + ). WithContractUpdateValidation(). WithStagedContractUpdates(stagedContracts) @@ -2305,7 +2431,14 @@ func TestStagedContractsUpdateValidationErrors(t *testing.T) { VerboseErrorOutput: true, } - migration := NewStagedContractsMigration("test", "test", log, rwf, options). + migration := NewStagedContractsMigration( + "test", + "test", + log, + rwf, + &LegacyTypeRequirements{}, + options, + ). WithContractUpdateValidation(). WithStagedContractUpdates(stagedContracts) @@ -2407,3 +2540,387 @@ func TestContractUpdateFailureEntry_MarshalJSON(t *testing.T) { string(actual), ) } + +func TestTypeRequirementRemoval(t *testing.T) { + t.Parallel() + + const chainID = flow.Testnet + + t.Run("TiblesProducer contract", func(t *testing.T) { + t.Parallel() + + tiblesAddress := mustHexToAddress("e93c412c964bdf40") + + oldCode := ` + pub contract interface TiblesProducer { + + pub struct ContentLocation {} + pub struct interface IContentLocation {} + + pub resource interface IContent { + access(contract) let contentIdsToPaths: {String: TiblesProducer.ContentLocation} + pub fun getMetadata(contentId: String): {String: AnyStruct}? + } + + pub resource interface IProducer { + access(contract) let minters: @{String: Minter} + } + + pub resource Producer: IContent, IProducer { + access(contract) let minters: @{String: Minter} + } + + pub resource interface IMinter { + pub let id: String + pub var lastMintNumber: UInt32 + pub let contentCapability: Capability + pub fun mintNext() + } + + pub resource Minter: IMinter { + pub let id: String + pub var lastMintNumber: UInt32 + pub let contentCapability: Capability + pub fun mintNext() + } + } + ` + + newCode := ` + access(all) contract interface TiblesProducer { + + access(all) struct interface ContentLocation {} + access(all) struct interface IContentLocation {} + + access(all) resource interface IContent { + access(contract) let contentIdsToPaths: {String: {TiblesProducer.ContentLocation}} + access(all) fun getMetadata(contentId: String): {String: AnyStruct}? + } + + access(all) resource interface IProducer { + access(contract) let minters: @{String: {Minter}} + } + + access(all) resource interface Producer: IContent, IProducer { + access(contract) let minters: @{String: {Minter}} + } + + access(all) resource interface IMinter { + access(all) let id: String + access(all) var lastMintNumber: UInt32 + access(all) let contentCapability: Capability + access(all) fun mintNext() + } + + access(all) resource interface Minter: IMinter { + access(all) let id: String + access(all) var lastMintNumber: UInt32 + access(all) let contentCapability: Capability + access(all) fun mintNext() + } + } + ` + + contractName := "TiblesProducer" + + stagedContracts := []StagedContract{ + { + Address: tiblesAddress, + Contract: Contract{ + Name: contractName, + Code: []byte(newCode), + }, + }, + } + + logWriter := &logWriter{} + log := zerolog.New(logWriter) + + rwf := &testReportWriterFactory{} + + options := StagedContractsMigrationOptions{ + ChainID: chainID, + VerboseErrorOutput: true, + } + + registersByAccount, err := registersForStagedContracts( + StagedContract{ + Address: tiblesAddress, + Contract: Contract{ + Name: contractName, + Code: []byte(oldCode), + }, + }, + ) + require.NoError(t, err) + + encodedContractNames, err := environment.EncodeContractNames([]string{contractName}) + require.NoError(t, err) + + err = registersByAccount.Set( + string(tiblesAddress[:]), + flow.ContractNamesKey, + encodedContractNames, + ) + require.NoError(t, err) + + // Run type-requirement extractor + + legacyTypeRequirements := &LegacyTypeRequirements{} + + cadenceTypeRequirementsExtractor := NewTypeRequirementsExtractingMigration( + log, + rwf, + nil, + legacyTypeRequirements, + ) + err = cadenceTypeRequirementsExtractor(registersByAccount) + require.NoError(t, err) + + require.Equal( + t, + []TypeRequirement{ + { + Address: tiblesAddress, + ContractName: contractName, + TypeName: "ContentLocation", + }, + { + Address: tiblesAddress, + ContractName: contractName, + TypeName: "Producer", + }, + { + Address: tiblesAddress, + ContractName: contractName, + TypeName: "Minter", + }, + }, + legacyTypeRequirements.typeRequirements, + ) + + // Run staged contract migration + + migration := NewStagedContractsMigration( + "test", + "test", + log, + rwf, + legacyTypeRequirements, + options, + ). + WithStagedContractUpdates(stagedContracts). + WithContractUpdateValidation() + + err = migration.InitMigration(log, registersByAccount, 1) + require.NoError(t, err) + + owner := string(tiblesAddress[:]) + accountRegisters := registersByAccount.AccountRegisters(owner) + + err = migration.MigrateAccount( + context.Background(), + tiblesAddress, + accountRegisters, + ) + require.NoError(t, err) + + err = migration.Close() + require.NoError(t, err) + + require.Empty(t, logWriter.logs) + + require.Equal(t, 2, accountRegisters.Count()) + assert.Equal(t, newCode, contractCode(t, registersByAccount, owner, contractName)) + }) + + t.Run("random contract", func(t *testing.T) { + t.Parallel() + + addressGenerator := chainID.Chain().NewAddressGenerator() + randomAddress, err := addressGenerator.NextAddress() + require.NoError(t, err) + + oldCode := ` + pub contract interface Foo { + + pub struct ContentLocation {} + pub struct interface IContentLocation {} + + pub resource interface IContent { + access(contract) let contentIdsToPaths: {String: Foo.ContentLocation} + pub fun getMetadata(contentId: String): {String: AnyStruct}? + } + + pub resource interface IProducer { + access(contract) let minters: @{String: Minter} + } + + pub resource Producer: IContent, IProducer { + access(contract) let minters: @{String: Minter} + } + + pub resource interface IMinter { + pub let id: String + pub var lastMintNumber: UInt32 + pub let contentCapability: Capability + pub fun mintNext() + } + + pub resource Minter: IMinter { + pub let id: String + pub var lastMintNumber: UInt32 + pub let contentCapability: Capability + pub fun mintNext() + } + } + ` + + newCode := ` + access(all) contract interface Foo { + + access(all) struct interface ContentLocation {} + access(all) struct interface IContentLocation {} + + access(all) resource interface IContent { + access(contract) let contentIdsToPaths: {String: {Foo.ContentLocation}} + access(all) fun getMetadata(contentId: String): {String: AnyStruct}? + } + + access(all) resource interface IProducer { + access(contract) let minters: @{String: {Minter}} + } + + access(all) resource interface Producer: IContent, IProducer { + access(contract) let minters: @{String: {Minter}} + } + + access(all) resource interface IMinter { + access(all) let id: String + access(all) var lastMintNumber: UInt32 + access(all) let contentCapability: Capability + access(all) fun mintNext() + } + + access(all) resource interface Minter: IMinter { + access(all) let id: String + access(all) var lastMintNumber: UInt32 + access(all) let contentCapability: Capability + access(all) fun mintNext() + } + } + ` + + fooContractName := "Foo" + + stagedContracts := []StagedContract{ + { + Address: common.Address(randomAddress), + Contract: Contract{ + Name: fooContractName, + Code: []byte(newCode), + }, + }, + } + + logWriter := &logWriter{} + log := zerolog.New(logWriter) + + rwf := &testReportWriterFactory{} + + options := StagedContractsMigrationOptions{ + ChainID: chainID, + VerboseErrorOutput: true, + } + + registersByAccount, err := registersForStagedContracts( + StagedContract{ + Address: common.Address(randomAddress), + Contract: Contract{ + Name: fooContractName, + Code: []byte(oldCode), + }, + }, + ) + require.NoError(t, err) + + encodedContractNames, err := environment.EncodeContractNames([]string{fooContractName}) + require.NoError(t, err) + + err = registersByAccount.Set( + string(randomAddress[:]), + flow.ContractNamesKey, + encodedContractNames, + ) + require.NoError(t, err) + + // Run type-requirement extractor + + legacyTypeRequirements := &LegacyTypeRequirements{} + + cadenceTypeRequirementsExtractor := NewTypeRequirementsExtractingMigration( + log, + rwf, + nil, + legacyTypeRequirements, + ) + err = cadenceTypeRequirementsExtractor(registersByAccount) + require.NoError(t, err) + + require.Equal( + t, + []TypeRequirement{ + { + Address: common.Address(randomAddress), + ContractName: fooContractName, + TypeName: "ContentLocation", + }, + { + Address: common.Address(randomAddress), + ContractName: fooContractName, + TypeName: "Producer", + }, + { + Address: common.Address(randomAddress), + ContractName: fooContractName, + TypeName: "Minter", + }, + }, + legacyTypeRequirements.typeRequirements, + ) + + // Run staged contract migration + + migration := NewStagedContractsMigration( + "test", + "test", + log, + rwf, + legacyTypeRequirements, + options, + ). + WithStagedContractUpdates(stagedContracts). + WithContractUpdateValidation() + + err = migration.InitMigration(log, registersByAccount, 1) + require.NoError(t, err) + + owner := string(randomAddress[:]) + accountRegisters := registersByAccount.AccountRegisters(owner) + + err = migration.MigrateAccount( + context.Background(), + common.Address(randomAddress), + accountRegisters, + ) + require.NoError(t, err) + + err = migration.Close() + require.NoError(t, err) + + require.Empty(t, logWriter.logs) + + require.Equal(t, 2, accountRegisters.Count()) + assert.Equal(t, newCode, contractCode(t, registersByAccount, owner, fooContractName)) + }) +} diff --git a/cmd/util/ledger/migrations/static_type_migration.go b/cmd/util/ledger/migrations/static_type_migration.go index 11c7e5c95a8..c34164cd755 100644 --- a/cmd/util/ledger/migrations/static_type_migration.go +++ b/cmd/util/ledger/migrations/static_type_migration.go @@ -7,19 +7,29 @@ import ( type StaticTypeMigrationRules map[common.TypeID]interpreter.StaticType +// NewStaticTypeMigration returns a type converter function. +// Accepts a `rulesGetter` which return +// This is because this constructor is called at the time of constructing the migrations (e.g: Cadence value migration), +// but the rules can only be finalized after running previous TypeRequirementsExtractingMigration migration. +// i.e: the LegacyTypeRequirements list used by NewCompositeTypeConversionRules is lazily populated. +// So we need to delay the construction of the rules, until after the execution of previous migration. func NewStaticTypeMigration[T interpreter.StaticType]( - rules StaticTypeMigrationRules, + rulesGetter func() StaticTypeMigrationRules, ) func(staticType T) interpreter.StaticType { - // Returning `nil` form the callback indicates the type wasn't converted. + var rules StaticTypeMigrationRules - if rules == nil { - return func(original T) interpreter.StaticType { + return func(original T) interpreter.StaticType { + // Initialize only once + if rules == nil { + rules = rulesGetter() + } + + // Returning `nil` form the callback indicates the type wasn't converted. + if rules == nil { return nil } - } - return func(original T) interpreter.StaticType { if replacement, ok := rules[original.ID()]; ok { return replacement } diff --git a/cmd/util/ledger/migrations/type_requirements_extractor.go b/cmd/util/ledger/migrations/type_requirements_extractor.go new file mode 100644 index 00000000000..d4965e8eb87 --- /dev/null +++ b/cmd/util/ledger/migrations/type_requirements_extractor.go @@ -0,0 +1,135 @@ +package migrations + +import ( + "encoding/json" + + "github.com/rs/zerolog" + + "github.com/onflow/cadence/runtime/common" + "github.com/onflow/cadence/runtime/old_parser" + + "github.com/onflow/flow-go/cmd/util/ledger/reporters" + "github.com/onflow/flow-go/cmd/util/ledger/util/registers" +) + +const typeRequirementExtractingReporterName = "type-requirements-extracting" + +type LegacyTypeRequirements struct { + typeRequirements []TypeRequirement +} + +type TypeRequirement struct { + Address common.Address + ContractName string + TypeName string +} + +func NewTypeRequirementsExtractingMigration( + log zerolog.Logger, + rwf reporters.ReportWriterFactory, + importantLocations map[common.AddressLocation]struct{}, + legacyTypeRequirements *LegacyTypeRequirements, +) RegistersMigration { + return func(registersByAccount *registers.ByAccount) error { + + reporter := rwf.ReportWriter(typeRequirementExtractingReporterName) + defer reporter.Close() + + // Gather all contracts + + contracts, err := gatherContractsFromRegisters(registersByAccount, log) + if err != nil { + return err + } + + // Extract type requirements from all contracts + + for _, contract := range contracts { + if _, isSystemContract := importantLocations[contract.location]; isSystemContract { + // System contracts have their own type-changing rules. + // So do not add them here. + continue + } + + extractTypeRequirements( + contract, + log, + reporter, + legacyTypeRequirements, + ) + } + + return nil + } +} + +func extractTypeRequirements( + contract AddressContract, + log zerolog.Logger, + reporter reporters.ReportWriter, + legacyTypeRequirements *LegacyTypeRequirements, +) { + + // must be parsed with the old parser. + program, err := old_parser.ParseProgram( + nil, + contract.code, + old_parser.Config{}, + ) + + if err != nil { + // If the old contract cannot be parsed, then ignore + return + } + + contractInterface := program.SoleContractInterfaceDeclaration() + if contractInterface == nil { + // Type requirements can only reside inside contract interfaces. + // Ignore all other programs. + return + } + + for _, composites := range contractInterface.DeclarationMembers().Composites() { + typeRequirement := TypeRequirement{ + Address: contract.location.Address, + ContractName: contractInterface.Identifier.Identifier, + TypeName: composites.Identifier.Identifier, + } + + legacyTypeRequirements.typeRequirements = append(legacyTypeRequirements.typeRequirements, typeRequirement) + + reporter.Write(typeRequirementRemovalEntry{ + TypeRequirement: typeRequirement, + }) + } + + log.Info().Msgf("Collected %d type-requirements", len(legacyTypeRequirements.typeRequirements)) +} + +// cadenceValueMigrationFailureEntry + +type typeRequirementRemovalEntry struct { + TypeRequirement +} + +var _ valueMigrationReportEntry = typeRequirementRemovalEntry{} + +func (e typeRequirementRemovalEntry) accountAddress() common.Address { + return e.Address +} + +var _ json.Marshaler = typeRequirementRemovalEntry{} + +func (e typeRequirementRemovalEntry) MarshalJSON() ([]byte, error) { + return json.Marshal(struct { + Kind string `json:"kind"` + AccountAddress string `json:"account_address"` + ContractName string `json:"contract_name"` + TypeName string `json:"type_name"` + }{ + Kind: "cadence-type-requirement-remove", + AccountAddress: e.Address.HexWithPrefix(), + ContractName: e.ContractName, + TypeName: e.TypeName, + }) +}