From 253409addb312bbdd11d4a87623043fd9b7e3d1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Thu, 22 Aug 2024 11:41:27 -0700 Subject: [PATCH 1/4] recover NFT contract --- .../contract_checking_migration_test.go | 76 +++++++-- fvm/environment/program_recovery.go | 146 ++++++++++++++++-- 2 files changed, 197 insertions(+), 25 deletions(-) diff --git a/cmd/util/ledger/migrations/contract_checking_migration_test.go b/cmd/util/ledger/migrations/contract_checking_migration_test.go index 5f7cb84553a..b168934b477 100644 --- a/cmd/util/ledger/migrations/contract_checking_migration_test.go +++ b/cmd/util/ledger/migrations/contract_checking_migration_test.go @@ -19,12 +19,12 @@ import ( "github.com/onflow/flow-go/model/flow" ) -func oldExampleTokenCode(fungibleTokenAddress flow.Address) string { +func oldExampleFungibleTokenCode(fungibleTokenAddress flow.Address) string { return fmt.Sprintf( ` import FungibleToken from 0x%s - pub contract ExampleToken: FungibleToken { + pub contract ExampleFungibleToken: FungibleToken { pub var totalSupply: UFix64 pub resource Vault: FungibleToken.Provider, FungibleToken.Receiver, FungibleToken.Balance { @@ -68,6 +68,40 @@ func oldExampleTokenCode(fungibleTokenAddress flow.Address) string { ) } +func oldExampleNonFungibleTokenCode(fungibleTokenAddress flow.Address) string { + return fmt.Sprintf( + ` + import NonFungibleToken from 0x%s + + pub contract ExampleNFT: NonFungibleToken { + + /// Total supply of ExampleNFTs in existence + pub var totalSupply: UInt64 + + /// The core resource that represents a Non Fungible Token. + /// New instances will be created using the NFTMinter resource + /// and stored in the Collection resource + /// + pub resource NFT: NonFungibleToken.INFT { + + /// The unique ID that each NFT has + pub let id: UInt64 + + init(id: UInt64) { + self.id = id + } + } + + init() { + // Initialize the total supply + self.totalSupply = 0 + } + } + `, + fungibleTokenAddress.Hex(), + ) +} + func TestContractCheckingMigrationProgramRecovery(t *testing.T) { t.Parallel() @@ -114,15 +148,27 @@ func TestContractCheckingMigrationProgramRecovery(t *testing.T) { systemContracts.FungibleToken, coreContracts.FungibleToken(env), ) + addSystemContract( + systemContracts.NonFungibleToken, + coreContracts.NonFungibleToken(env), + ) - // Use an old version of the ExampleToken contract, + // Use an old version of the ExampleFungibleToken contract, + // and "deploy" it at some arbitrary, high (i.e. non-system) address + exampleAddress, err := chainID.Chain().AddressAtIndex(1000) + require.NoError(t, err) + addContract( + exampleAddress, + "ExampleFungibleToken", + []byte(oldExampleFungibleTokenCode(systemContracts.FungibleToken.Address)), + ) + // Use an old version of the ExampleNonFungibleToken contract, // and "deploy" it at some arbitrary, high (i.e. non-system) address - exampleTokenAddress, err := chainID.Chain().AddressAtIndex(1000) require.NoError(t, err) addContract( - exampleTokenAddress, - "ExampleToken", - []byte(oldExampleTokenCode(systemContracts.FungibleToken.Address)), + exampleAddress, + "ExampleNonFungibleToken", + []byte(oldExampleNonFungibleTokenCode(systemContracts.NonFungibleToken.Address)), ) for address, addressContracts := range contracts { @@ -177,6 +223,11 @@ func TestContractCheckingMigrationProgramRecovery(t *testing.T) { assert.Equal(t, []any{ + contractCheckingSuccess{ + AccountAddress: common.Address(systemContracts.NonFungibleToken.Address), + ContractName: systemcontracts.ContractNameNonFungibleToken, + Code: string(coreContracts.NonFungibleToken(env)), + }, contractCheckingSuccess{ AccountAddress: common.Address(systemContracts.ViewResolver.Address), ContractName: systemcontracts.ContractNameViewResolver, @@ -193,9 +244,14 @@ func TestContractCheckingMigrationProgramRecovery(t *testing.T) { Code: string(coreContracts.FungibleToken(env)), }, contractCheckingSuccess{ - AccountAddress: common.Address(exampleTokenAddress), - ContractName: "ExampleToken", - Code: oldExampleTokenCode(systemContracts.FungibleToken.Address), + AccountAddress: common.Address(exampleAddress), + ContractName: "ExampleFungibleToken", + Code: oldExampleFungibleTokenCode(systemContracts.FungibleToken.Address), + }, + contractCheckingSuccess{ + AccountAddress: common.Address(exampleAddress), + ContractName: "ExampleNonFungibleToken", + Code: oldExampleNonFungibleTokenCode(systemContracts.NonFungibleToken.Address), }, }, reporter.entries, diff --git a/fvm/environment/program_recovery.go b/fvm/environment/program_recovery.go index 9f2485c617d..c6f085b1ae4 100644 --- a/fvm/environment/program_recovery.go +++ b/fvm/environment/program_recovery.go @@ -29,16 +29,23 @@ func RecoverProgram( sc := systemcontracts.SystemContractsForChain(chainID) fungibleTokenAddress := common.Address(sc.FungibleToken.Address) + nonFungibleTokenAddress := common.Address(sc.NonFungibleToken.Address) - if !isFungibleTokenContract(program, fungibleTokenAddress) { - return nil, nil - } + var code string - contractName := addressLocation.Name + switch { + case isFungibleTokenContract(program, fungibleTokenAddress): + code = RecoveredFungibleTokenCode(fungibleTokenAddress, addressLocation.Name) - code := RecoveredFungibleTokenCode(fungibleTokenAddress, contractName) + case isNonFungibleTokenContract(program, nonFungibleTokenAddress): + code = RecoveredNonFungibleTokenCode(nonFungibleTokenAddress, addressLocation.Name) + } + + if code != "" { + return parser.ParseProgram(memoryGauge, []byte(code), parser.Config{}) + } - return parser.ParseProgram(memoryGauge, []byte(code), parser.Config{}) + return nil, nil } func RecoveredFungibleTokenCode(fungibleTokenAddress common.Address, contractName string) string { @@ -59,12 +66,12 @@ func RecoveredFungibleTokenCode(fungibleTokenAddress common.Address, contractNam access(all) view fun getContractViews(resourceType: Type?): [Type] { - panic("getContractViews is not implemented") + panic("getContractViews is not available in recovered program") } access(all) fun resolveContractView(resourceType: Type?, viewType: Type): AnyStruct? { - panic("resolveContractView is not implemented") + panic("resolveContractView is not available in recovered program") } access(all) @@ -79,38 +86,38 @@ func RecoveredFungibleTokenCode(fungibleTokenAddress common.Address, contractNam access(FungibleToken.Withdraw) fun withdraw(amount: UFix64): @{FungibleToken.Vault} { - panic("withdraw is not implemented") + panic("withdraw is not available in recovered program") } access(all) view fun isAvailableToWithdraw(amount: UFix64): Bool { - panic("isAvailableToWithdraw is not implemented") + panic("isAvailableToWithdraw is not available in recovered program") } access(all) fun deposit(from: @{FungibleToken.Vault}) { - panic("deposit is not implemented") + panic("deposit is not available in recovered program") } access(all) fun createEmptyVault(): @{FungibleToken.Vault} { - panic("createEmptyVault is not implemented") + panic("createEmptyVault is not available in recovered program") } access(all) view fun getViews(): [Type] { - panic("getViews is not implemented") + panic("getViews is not available in recovered program") } access(all) fun resolveView(_ view: Type): AnyStruct? { - panic("resolveView is not implemented") + panic("resolveView is not available in recovered program") } } access(all) fun createEmptyVault(vaultType: Type): @{FungibleToken.Vault} { - panic("createEmptyVault is not implemented") + panic("createEmptyVault is not available in recovered program") } } `, @@ -119,6 +126,62 @@ func RecoveredFungibleTokenCode(fungibleTokenAddress common.Address, contractNam ) } +func RecoveredNonFungibleTokenCode(nonFungibleTokenAddress common.Address, contractName string) string { + return fmt.Sprintf( + //language=Cadence + ` + import NonFungibleToken from %s + + access(all) + contract %s: NonFungibleToken { + + access(all) + resource NFT: NonFungibleToken.NFT { + + access(all) + let id: UInt64 + + init(id: UInt64) { + self.id = id + } + + access(all) + view fun getViews(): [Type] { + panic("getViews is not available in recovered program") + } + + access(all) + fun resolveView(_ view: Type): AnyStruct? { + panic("resolveView is not available in recovered program") + } + + access(all) + fun createEmptyCollection(): @{NonFungibleToken.Collection} { + panic("createEmptyCollection is not available in recovered program") + } + } + + access(all) + view fun getContractViews(resourceType: Type?): [Type] { + panic("getContractViews is not available in recovered program") + } + + access(all) + fun resolveContractView(resourceType: Type?, viewType: Type): AnyStruct? { + panic("resolveContractView is not available in recovered program") + } + + access(all) + fun createEmptyCollection(nftType: Type): @{NonFungibleToken.Collection} { + panic("createEmptyCollection is not available in recovered program") + } + } + `, + nonFungibleTokenAddress.HexWithPrefix(), + contractName, + ) +} + func importsAddressLocation(program *ast.Program, address common.Address, name string) bool { importDeclarations := program.ImportDeclarations() @@ -169,6 +232,11 @@ const fungibleTokenTypeTotalSupplyFieldName = "totalSupply" const fungibleTokenVaultTypeIdentifier = "Vault" const fungibleTokenVaultTypeBalanceFieldName = "balance" +const nonFungibleTokenTypeIdentifier = "NonFungibleToken" +const nonFungibleTokenTypeTotalSupplyFieldName = "totalSupply" +const nonFungibleTokenNFTTypeIdentifier = "NFT" +const nonFungibleTokenNFTTypeIDFieldName = "id" + func isFungibleTokenContract(program *ast.Program, fungibleTokenAddress common.Address) bool { // Check if the contract imports the FungibleToken contract @@ -218,6 +286,54 @@ func isFungibleTokenContract(program *ast.Program, fungibleTokenAddress common.A return true } +func isNonFungibleTokenContract(program *ast.Program, nonFungibleTokenAddress common.Address) bool { + + // Check if the contract imports the NonFungibleToken contract + if !importsAddressLocation(program, nonFungibleTokenAddress, nonFungibleTokenTypeIdentifier) { + return false + } + + contractDeclaration := program.SoleContractDeclaration() + if contractDeclaration == nil { + return false + } + + // Check if the contract implements the NonFungibleToken interface + if !declaresConformanceTo(contractDeclaration, nonFungibleTokenTypeIdentifier) { + return false + } + + // Check if the contract has a totalSupply field + totalSupplyFieldDeclaration := getField(contractDeclaration, nonFungibleTokenTypeTotalSupplyFieldName) + if totalSupplyFieldDeclaration == nil { + return false + } + + // Check if the totalSupply field is of type UInt64 + if !isNominalType(totalSupplyFieldDeclaration.TypeAnnotation.Type, sema.UInt64TypeName) { + return false + } + + // Check if the contract has an NFT resource + nftDeclaration := contractDeclaration.Members.CompositesByIdentifier()[nonFungibleTokenNFTTypeIdentifier] + if nftDeclaration == nil { + return false + } + + // Check if the NFT resource has an id field + idFieldDeclaration := getField(nftDeclaration, nonFungibleTokenNFTTypeIDFieldName) + if idFieldDeclaration == nil { + return false + } + + // Check if the id field is of type UInt64 + if !isNominalType(idFieldDeclaration.TypeAnnotation.Type, sema.UInt64TypeName) { + return false + } + + return true +} + func getField(declaration *ast.CompositeDeclaration, name string) *ast.FieldDeclaration { for _, fieldDeclaration := range declaration.Members.Fields() { if fieldDeclaration.Identifier.Identifier == name { From 95abab778e865efb4bb1544333180720ea1e6213 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Thu, 22 Aug 2024 11:57:53 -0700 Subject: [PATCH 2/4] Update fvm/environment/program_recovery.go Co-authored-by: Supun Setunga --- fvm/environment/program_recovery.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fvm/environment/program_recovery.go b/fvm/environment/program_recovery.go index c6f085b1ae4..fdc7987fd8f 100644 --- a/fvm/environment/program_recovery.go +++ b/fvm/environment/program_recovery.go @@ -39,11 +39,11 @@ func RecoverProgram( case isNonFungibleTokenContract(program, nonFungibleTokenAddress): code = RecoveredNonFungibleTokenCode(nonFungibleTokenAddress, addressLocation.Name) + default: + return nil, nil } - if code != "" { - return parser.ParseProgram(memoryGauge, []byte(code), parser.Config{}) - } + return parser.ParseProgram(memoryGauge, []byte(code), parser.Config{}) return nil, nil } From 2844c975a37a747a76d6983e66072b5f36c03824 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Thu, 22 Aug 2024 16:17:24 -0700 Subject: [PATCH 3/4] improve program recovery: return code, panic with better error messages --- .../contract_checking_migration_test.go | 206 +++++++++++++++++- .../ledger/migrations/migrator_runtime.go | 22 ++ .../util/migration_runtime_interface.go | 6 +- fvm/environment/facade_env.go | 3 +- fvm/environment/program_recovery.go | 118 +++++----- 5 files changed, 292 insertions(+), 63 deletions(-) diff --git a/cmd/util/ledger/migrations/contract_checking_migration_test.go b/cmd/util/ledger/migrations/contract_checking_migration_test.go index b168934b477..1a4ffd957f2 100644 --- a/cmd/util/ledger/migrations/contract_checking_migration_test.go +++ b/cmd/util/ledger/migrations/contract_checking_migration_test.go @@ -5,6 +5,7 @@ import ( "sort" "testing" + "github.com/onflow/cadence/runtime" "github.com/onflow/cadence/runtime/common" "github.com/onflow/cadence/runtime/interpreter" coreContracts "github.com/onflow/flow-core-contracts/lib/go/contracts" @@ -14,8 +15,11 @@ import ( "github.com/stretchr/testify/require" "github.com/onflow/flow-go/cmd/util/ledger/util/registers" + "github.com/onflow/flow-go/fvm" "github.com/onflow/flow-go/fvm/environment" + "github.com/onflow/flow-go/fvm/storage/snapshot" "github.com/onflow/flow-go/fvm/systemcontracts" + "github.com/onflow/flow-go/ledger/common/convert" "github.com/onflow/flow-go/model/flow" ) @@ -111,6 +115,7 @@ func TestContractCheckingMigrationProgramRecovery(t *testing.T) { // Set up contracts const chainID = flow.Testnet + chain := chainID.Chain() systemContracts := systemcontracts.SystemContractsForChain(chainID) @@ -153,13 +158,16 @@ func TestContractCheckingMigrationProgramRecovery(t *testing.T) { coreContracts.NonFungibleToken(env), ) + const exampleFungibleTokenContractName = "ExampleFungibleToken" + const exampleNonFungibleTokenContractName = "ExampleNonFungibleToken" + // Use an old version of the ExampleFungibleToken contract, // and "deploy" it at some arbitrary, high (i.e. non-system) address - exampleAddress, err := chainID.Chain().AddressAtIndex(1000) + exampleAddress, err := chain.AddressAtIndex(1000) require.NoError(t, err) addContract( exampleAddress, - "ExampleFungibleToken", + exampleFungibleTokenContractName, []byte(oldExampleFungibleTokenCode(systemContracts.FungibleToken.Address)), ) // Use an old version of the ExampleNonFungibleToken contract, @@ -167,7 +175,7 @@ func TestContractCheckingMigrationProgramRecovery(t *testing.T) { require.NoError(t, err) addContract( exampleAddress, - "ExampleNonFungibleToken", + exampleNonFungibleTokenContractName, []byte(oldExampleNonFungibleTokenCode(systemContracts.NonFungibleToken.Address)), ) @@ -245,15 +253,203 @@ func TestContractCheckingMigrationProgramRecovery(t *testing.T) { }, contractCheckingSuccess{ AccountAddress: common.Address(exampleAddress), - ContractName: "ExampleFungibleToken", + ContractName: exampleFungibleTokenContractName, Code: oldExampleFungibleTokenCode(systemContracts.FungibleToken.Address), }, contractCheckingSuccess{ AccountAddress: common.Address(exampleAddress), - ContractName: "ExampleNonFungibleToken", + ContractName: exampleNonFungibleTokenContractName, Code: oldExampleNonFungibleTokenCode(systemContracts.NonFungibleToken.Address), }, }, reporter.entries, ) + + // Check that the programs are recovered correctly after the migration. + + mr, err := NewInterpreterMigrationRuntime(registersByAccount, chainID, InterpreterMigrationRuntimeConfig{}) + require.NoError(t, err) + + // First, we need to create the example account + + err = mr.Accounts.Create(nil, exampleAddress) + require.NoError(t, err) + + expectedAddresses := map[flow.Address]struct{}{ + exampleAddress: {}, + } + + err = mr.Commit(expectedAddresses, log) + require.NoError(t, err) + + // Next, we need to manually store contract values in the example account, + // simulating the effect of the deploying the original contracts. + // + // We need to do so with a new runtime, + // because the previous runtime's transaction state is finalized. + + mr, err = NewInterpreterMigrationRuntime(registersByAccount, chainID, InterpreterMigrationRuntimeConfig{}) + require.NoError(t, err) + + contractsStorageMap := mr.Storage.GetStorageMap( + common.Address(exampleAddress), + runtime.StorageDomainContract, + true, + ) + + inter := mr.Interpreter + + exampleFungibleTokenContractValue := interpreter.NewCompositeValue( + inter, + interpreter.EmptyLocationRange, + common.NewAddressLocation( + nil, + common.Address(exampleAddress), + exampleFungibleTokenContractName, + ), + exampleFungibleTokenContractName, + common.CompositeKindContract, + []interpreter.CompositeField{ + { + Name: "totalSupply", + Value: interpreter.NewUnmeteredUFix64ValueWithInteger(42, interpreter.EmptyLocationRange), + }, + }, + common.Address(exampleAddress), + ) + + contractsStorageMap.SetValue( + inter, + interpreter.StringStorageMapKey(exampleFungibleTokenContractName), + exampleFungibleTokenContractValue, + ) + + exampleNonFungibleTokenContractValue := interpreter.NewCompositeValue( + inter, + interpreter.EmptyLocationRange, + common.NewAddressLocation( + nil, + common.Address(exampleAddress), + exampleNonFungibleTokenContractName, + ), + exampleNonFungibleTokenContractName, + common.CompositeKindContract, + []interpreter.CompositeField{ + { + Name: "totalSupply", + Value: interpreter.NewUnmeteredUInt64Value(42), + }, + }, + common.Address(exampleAddress), + ) + + contractsStorageMap.SetValue( + inter, + interpreter.StringStorageMapKey(exampleNonFungibleTokenContractName), + exampleNonFungibleTokenContractValue, + ) + + err = mr.Storage.NondeterministicCommit(inter, false) + require.NoError(t, err) + + err = mr.Commit(expectedAddresses, log) + require.NoError(t, err) + + // Setup complete, now we can run the test transactions + + type testCase struct { + name string + code string + check func(t *testing.T, err error) + } + + testCases := []testCase{ + { + name: exampleFungibleTokenContractName, + code: fmt.Sprintf( + ` + import ExampleFungibleToken from %s + + transaction { + execute { + assert(ExampleFungibleToken.totalSupply == 42.0) + destroy ExampleFungibleToken.createEmptyVault( + vaultType: Type<@ExampleFungibleToken.Vault>() + ) + } + } + `, + exampleAddress.HexWithPrefix(), + ), + check: func(t *testing.T, err error) { + require.Error(t, err) + require.ErrorContains(t, err, "Contract ExampleFungibleToken is no longer functional") + require.ErrorContains(t, err, "createEmptyVault is not available in recovered program.") + }, + }, + { + name: exampleNonFungibleTokenContractName, + code: fmt.Sprintf( + ` + import ExampleNonFungibleToken from %s + + transaction { + execute { + destroy ExampleNonFungibleToken.createEmptyCollection( + nftType: Type<@ExampleNonFungibleToken.NFT>() + ) + } + } + `, + exampleAddress.HexWithPrefix(), + ), + check: func(t *testing.T, err error) { + require.Error(t, err) + require.ErrorContains(t, err, "Contract ExampleNonFungibleToken is no longer functional") + require.ErrorContains(t, err, "createEmptyCollection is not available in recovered program.") + }, + }, + } + + storageSnapshot := snapshot.MapStorageSnapshot{} + + newPayloads := registersByAccount.DestructIntoPayloads(1) + + for _, newPayload := range newPayloads { + registerID, registerValue, err := convert.PayloadToRegister(newPayload) + require.NoError(t, err) + + storageSnapshot[registerID] = registerValue + } + + test := func(testCase testCase) { + + t.Run(testCase.name, func(t *testing.T) { + + txBody := flow.NewTransactionBody(). + SetScript([]byte(testCase.code)) + + vm := fvm.NewVirtualMachine() + + ctx := fvm.NewContext( + fvm.WithChain(chain), + fvm.WithAuthorizationChecksEnabled(false), + fvm.WithSequenceNumberCheckAndIncrementEnabled(false), + fvm.WithCadenceLogging(true), + ) + + _, output, err := vm.Run( + ctx, + fvm.Transaction(txBody, 0), + storageSnapshot, + ) + + require.NoError(t, err) + testCase.check(t, output.Err) + }) + } + + for _, testCase := range testCases { + test(testCase) + } } diff --git a/cmd/util/ledger/migrations/migrator_runtime.go b/cmd/util/ledger/migrations/migrator_runtime.go index ba72224d520..72a049539ec 100644 --- a/cmd/util/ledger/migrations/migrator_runtime.go +++ b/cmd/util/ledger/migrations/migrator_runtime.go @@ -8,6 +8,7 @@ import ( "github.com/onflow/cadence/runtime/interpreter" "github.com/onflow/cadence/runtime/stdlib" "github.com/onflow/crypto/hash" + "github.com/rs/zerolog" "github.com/onflow/flow-go/cmd/util/ledger/util" "github.com/onflow/flow-go/cmd/util/ledger/util/registers" @@ -19,12 +20,32 @@ import ( ) type BasicMigrationRuntime struct { + Registers registers.Registers TransactionState state.NestedTransactionPreparer Storage *runtime.Storage AccountsLedger *util.AccountsAtreeLedger Accounts environment.Accounts } +func (r *BasicMigrationRuntime) Commit(expectedAddresses map[flow.Address]struct{}, log zerolog.Logger) error { + + result, err := r.TransactionState.FinalizeMainTransaction() + if err != nil { + return fmt.Errorf("failed to finalize main transaction: %w", err) + } + + err = registers.ApplyChanges( + r.Registers, + result.WriteSet, + expectedAddresses, + log, + ) + if err != nil { + return fmt.Errorf("failed to apply changes: %w", err) + } + return nil +} + type InterpreterMigrationRuntime struct { *BasicMigrationRuntime Interpreter *interpreter.Interpreter @@ -120,6 +141,7 @@ func NewBasicMigrationRuntime(regs registers.Registers) *BasicMigrationRuntime { runtimeStorage := runtime.NewStorage(accountsAtreeLedger, nil) return &BasicMigrationRuntime{ + Registers: regs, TransactionState: transactionState, Storage: runtimeStorage, AccountsLedger: accountsAtreeLedger, diff --git a/cmd/util/ledger/util/migration_runtime_interface.go b/cmd/util/ledger/util/migration_runtime_interface.go index f91c4116096..8a43d7cff3b 100644 --- a/cmd/util/ledger/util/migration_runtime_interface.go +++ b/cmd/util/ledger/util/migration_runtime_interface.go @@ -62,8 +62,6 @@ func NewMigrationRuntimeInterface( } } -var _ runtime.Interface = &MigrationRuntimeInterface{} - func (m *MigrationRuntimeInterface) ResolveLocation( identifiers []runtime.Identifier, location runtime.Location, @@ -175,8 +173,8 @@ func (m *MigrationRuntimeInterface) GetOrLoadProgram( func (m *MigrationRuntimeInterface) RecoverProgram( program *ast.Program, location common.Location, -) (*ast.Program, error) { - return environment.RecoverProgram(nil, m.chainID, program, location) +) ([]byte, error) { + return environment.RecoverProgram(m.chainID, program, location) } type migrationTransactionPreparer struct { diff --git a/fvm/environment/facade_env.go b/fvm/environment/facade_env.go index 88812b4cec3..a9e558c5106 100644 --- a/fvm/environment/facade_env.go +++ b/fvm/environment/facade_env.go @@ -336,9 +336,8 @@ func (*facadeEnvironment) GetInterpreterSharedState() *interpreter.SharedState { return nil } -func (env *facadeEnvironment) RecoverProgram(program *ast.Program, location common.Location) (*ast.Program, error) { +func (env *facadeEnvironment) RecoverProgram(program *ast.Program, location common.Location) ([]byte, error) { return RecoverProgram( - env, env.chain.ChainID(), program, location, diff --git a/fvm/environment/program_recovery.go b/fvm/environment/program_recovery.go index fdc7987fd8f..c8946e9a4ec 100644 --- a/fvm/environment/program_recovery.go +++ b/fvm/environment/program_recovery.go @@ -5,7 +5,6 @@ import ( "github.com/onflow/cadence/runtime/ast" "github.com/onflow/cadence/runtime/common" - "github.com/onflow/cadence/runtime/parser" "github.com/onflow/cadence/runtime/sema" "github.com/onflow/flow-go/fvm/systemcontracts" @@ -13,12 +12,11 @@ import ( ) func RecoverProgram( - memoryGauge common.MemoryGauge, chainID flow.ChainID, program *ast.Program, location common.Location, ) ( - *ast.Program, + []byte, error, ) { addressLocation, ok := location.(common.AddressLocation) @@ -31,31 +29,32 @@ func RecoverProgram( fungibleTokenAddress := common.Address(sc.FungibleToken.Address) nonFungibleTokenAddress := common.Address(sc.NonFungibleToken.Address) - var code string - switch { case isFungibleTokenContract(program, fungibleTokenAddress): - code = RecoveredFungibleTokenCode(fungibleTokenAddress, addressLocation.Name) + return RecoveredFungibleTokenCode(fungibleTokenAddress, addressLocation.Name), nil case isNonFungibleTokenContract(program, nonFungibleTokenAddress): - code = RecoveredNonFungibleTokenCode(nonFungibleTokenAddress, addressLocation.Name) - default: - return nil, nil + return RecoveredNonFungibleTokenCode(nonFungibleTokenAddress, addressLocation.Name), nil } - return parser.ParseProgram(memoryGauge, []byte(code), parser.Config{}) - return nil, nil } -func RecoveredFungibleTokenCode(fungibleTokenAddress common.Address, contractName string) string { - return fmt.Sprintf( +func RecoveredFungibleTokenCode(fungibleTokenAddress common.Address, contractName string) []byte { + return []byte(fmt.Sprintf( //language=Cadence ` - import FungibleToken from %s + import FungibleToken from %[1]s access(all) - contract %s: FungibleToken { + contract %[2]s: FungibleToken { + + access(self) + view fun recoveryPanic(_ functionName: String): Never { + return panic( + "%[3]s ".concat(functionName).concat(" is not available in recovered program.") + ) + } access(all) var totalSupply: UFix64 @@ -66,12 +65,17 @@ func RecoveredFungibleTokenCode(fungibleTokenAddress common.Address, contractNam access(all) view fun getContractViews(resourceType: Type?): [Type] { - panic("getContractViews is not available in recovered program") + %[2]s.recoveryPanic("getContractViews") } access(all) fun resolveContractView(resourceType: Type?, viewType: Type): AnyStruct? { - panic("resolveContractView is not available in recovered program") + %[2]s.recoveryPanic("resolveContractView") + } + + access(all) + fun createEmptyVault(vaultType: Type): @{FungibleToken.Vault} { + %[2]s.recoveryPanic("createEmptyVault") } access(all) @@ -86,54 +90,75 @@ func RecoveredFungibleTokenCode(fungibleTokenAddress common.Address, contractNam access(FungibleToken.Withdraw) fun withdraw(amount: UFix64): @{FungibleToken.Vault} { - panic("withdraw is not available in recovered program") + %[2]s.recoveryPanic("Vault.withdraw") } access(all) view fun isAvailableToWithdraw(amount: UFix64): Bool { - panic("isAvailableToWithdraw is not available in recovered program") + %[2]s.recoveryPanic("Vault.isAvailableToWithdraw") } access(all) fun deposit(from: @{FungibleToken.Vault}) { - panic("deposit is not available in recovered program") + %[2]s.recoveryPanic("Vault.deposit") } access(all) fun createEmptyVault(): @{FungibleToken.Vault} { - panic("createEmptyVault is not available in recovered program") + %[2]s.recoveryPanic("Vault.createEmptyVault") } access(all) view fun getViews(): [Type] { - panic("getViews is not available in recovered program") + %[2]s.recoveryPanic("Vault.getViews") } access(all) fun resolveView(_ view: Type): AnyStruct? { - panic("resolveView is not available in recovered program") + %[2]s.recoveryPanic("Vault.resolveView") } } - - access(all) - fun createEmptyVault(vaultType: Type): @{FungibleToken.Vault} { - panic("createEmptyVault is not available in recovered program") - } } `, fungibleTokenAddress.HexWithPrefix(), contractName, - ) + fmt.Sprintf("Contract %s is no longer functional. "+ + "A version of the contract has been recovered to allow access to the fields declared in the FT standard.", + contractName, + ), + )) } -func RecoveredNonFungibleTokenCode(nonFungibleTokenAddress common.Address, contractName string) string { - return fmt.Sprintf( +func RecoveredNonFungibleTokenCode(nonFungibleTokenAddress common.Address, contractName string) []byte { + return []byte(fmt.Sprintf( //language=Cadence ` - import NonFungibleToken from %s + import NonFungibleToken from %[1]s access(all) - contract %s: NonFungibleToken { + contract %[2]s: NonFungibleToken { + + access(self) + view fun recoveryPanic(_ functionName: String): Never { + return panic( + "%[3]s ".concat(functionName).concat(" is not available in recovered program.") + ) + } + + access(all) + view fun getContractViews(resourceType: Type?): [Type] { + %[2]s.recoveryPanic("getContractViews") + } + + access(all) + fun resolveContractView(resourceType: Type?, viewType: Type): AnyStruct? { + %[2]s.recoveryPanic("resolveContractView") + } + + access(all) + fun createEmptyCollection(nftType: Type): @{NonFungibleToken.Collection} { + %[2]s.recoveryPanic("createEmptyCollection") + } access(all) resource NFT: NonFungibleToken.NFT { @@ -147,39 +172,28 @@ func RecoveredNonFungibleTokenCode(nonFungibleTokenAddress common.Address, contr access(all) view fun getViews(): [Type] { - panic("getViews is not available in recovered program") + %[2]s.recoveryPanic("NFT.getViews") } access(all) fun resolveView(_ view: Type): AnyStruct? { - panic("resolveView is not available in recovered program") + %[2]s.recoveryPanic("NFT.resolveView") } access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} { - panic("createEmptyCollection is not available in recovered program") + %[2]s.recoveryPanic("NFT.createEmptyCollection") } } - - access(all) - view fun getContractViews(resourceType: Type?): [Type] { - panic("getContractViews is not available in recovered program") - } - - access(all) - fun resolveContractView(resourceType: Type?, viewType: Type): AnyStruct? { - panic("resolveContractView is not available in recovered program") - } - - access(all) - fun createEmptyCollection(nftType: Type): @{NonFungibleToken.Collection} { - panic("createEmptyCollection is not available in recovered program") - } } `, nonFungibleTokenAddress.HexWithPrefix(), contractName, - ) + fmt.Sprintf("Contract %s is no longer functional. "+ + "A version of the contract has been recovered to allow access to the fields declared in the NFT standard.", + contractName, + ), + )) } func importsAddressLocation(program *ast.Program, address common.Address, name string) bool { From 1c21cd12d77ec30ef258d9fa276112e7af94af69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Thu, 22 Aug 2024 16:17:47 -0700 Subject: [PATCH 4/4] refactor commit and merge --- .../migrations/account_storage_migration.go | 16 +++------------- cmd/util/ledger/migrations/cadence.go | 16 +++------------- .../migrations/cadence_values_migration.go | 16 +++------------- .../migrations/fix_broken_data_migration.go | 16 +++------------- 4 files changed, 12 insertions(+), 52 deletions(-) diff --git a/cmd/util/ledger/migrations/account_storage_migration.go b/cmd/util/ledger/migrations/account_storage_migration.go index d96bfd5f83b..444819ffac6 100644 --- a/cmd/util/ledger/migrations/account_storage_migration.go +++ b/cmd/util/ledger/migrations/account_storage_migration.go @@ -52,25 +52,15 @@ func NewAccountStorageMigration( log.Err(err).Msg("storage health check failed") } - // Finalize the transaction - result, err := migrationRuntime.TransactionState.FinalizeMainTransaction() - if err != nil { - return fmt.Errorf("failed to finalize main transaction: %w", err) - } + // Commit/finalize the transaction - // Merge the changes into the registers expectedAddresses := map[flow.Address]struct{}{ flow.Address(address): {}, } - err = registers.ApplyChanges( - registersByAccount, - result.WriteSet, - expectedAddresses, - log, - ) + err = migrationRuntime.Commit(expectedAddresses, log) if err != nil { - return fmt.Errorf("failed to apply register changes: %w", err) + return fmt.Errorf("failed to commit: %w", err) } return nil diff --git a/cmd/util/ledger/migrations/cadence.go b/cmd/util/ledger/migrations/cadence.go index d78538b8d9a..e4f39665c7a 100644 --- a/cmd/util/ledger/migrations/cadence.go +++ b/cmd/util/ledger/migrations/cadence.go @@ -392,25 +392,15 @@ func (m *IssueStorageCapConMigration) MigrateAccount( return fmt.Errorf("failed to commit changes: %w", err) } - // finalize the transaction - result, err := migrationRuntime.TransactionState.FinalizeMainTransaction() - if err != nil { - return fmt.Errorf("failed to finalize main transaction: %w", err) - } + // Commit/finalize the transaction - // Merge the changes into the registers expectedAddresses := map[flow.Address]struct{}{ flow.Address(address): {}, } - err = registers.ApplyChanges( - accountRegisters, - result.WriteSet, - expectedAddresses, - m.log, - ) + err = migrationRuntime.Commit(expectedAddresses, m.log) if err != nil { - return fmt.Errorf("failed to apply changes: %w", err) + return fmt.Errorf("failed to commit: %w", err) } return nil diff --git a/cmd/util/ledger/migrations/cadence_values_migration.go b/cmd/util/ledger/migrations/cadence_values_migration.go index 2dad5f49eb6..4bd2c756cde 100644 --- a/cmd/util/ledger/migrations/cadence_values_migration.go +++ b/cmd/util/ledger/migrations/cadence_values_migration.go @@ -180,25 +180,15 @@ func (m *CadenceBaseMigration) MigrateAccount( } } - // finalize the transaction - result, err := migrationRuntime.TransactionState.FinalizeMainTransaction() - if err != nil { - return fmt.Errorf("failed to finalize main transaction: %w", err) - } + // Commit/finalize the transaction - // Merge the changes into the registers expectedAddresses := map[flow.Address]struct{}{ flow.Address(address): {}, } - err = registers.ApplyChanges( - accountRegisters, - result.WriteSet, - expectedAddresses, - m.log, - ) + err = migrationRuntime.Commit(expectedAddresses, m.log) if err != nil { - return fmt.Errorf("failed to apply changes: %w", err) + return fmt.Errorf("failed to commit: %w", err) } if m.diffReporter != nil { diff --git a/cmd/util/ledger/migrations/fix_broken_data_migration.go b/cmd/util/ledger/migrations/fix_broken_data_migration.go index e5b489ffda8..81988dddddd 100644 --- a/cmd/util/ledger/migrations/fix_broken_data_migration.go +++ b/cmd/util/ledger/migrations/fix_broken_data_migration.go @@ -122,25 +122,15 @@ func (m *FixSlabsWithBrokenReferencesMigration) MigrateAccount( return fmt.Errorf("failed to commit storage: %w", err) } - // Finalize the transaction - result, err := migrationRuntime.TransactionState.FinalizeMainTransaction() - if err != nil { - return fmt.Errorf("failed to finalize main transaction: %w", err) - } + // Commit/finalize the transaction - // Merge the changes to the original payloads. expectedAddresses := map[flow.Address]struct{}{ flow.Address(address): {}, } - err = registers.ApplyChanges( - accountRegisters, - result.WriteSet, - expectedAddresses, - m.log, - ) + err = migrationRuntime.Commit(expectedAddresses, m.log) if err != nil { - return fmt.Errorf("failed to apply changes to account registers: %w", err) + return fmt.Errorf("failed to commit: %w", err) } // Log fixed payloads