From 9d4dd9e8e8e714cb4172d1eab3d2e9e3a9782890 Mon Sep 17 00:00:00 2001 From: Daniel Sainati Date: Wed, 24 Jan 2024 13:17:38 -0500 Subject: [PATCH 1/2] use old parser for contract upgrades when legacy upgrade config option is set --- runtime/config.go | 2 + runtime/contract_update_test.go | 115 +++++++++++++++++++ runtime/environment.go | 1 + runtime/interpreter/config.go | 2 + runtime/stdlib/account.go | 34 +++++- runtime/stdlib/contract_update_validation.go | 39 +++++++ 6 files changed, 187 insertions(+), 6 deletions(-) diff --git a/runtime/config.go b/runtime/config.go index 891e9e5032..2915c70e30 100644 --- a/runtime/config.go +++ b/runtime/config.go @@ -37,4 +37,6 @@ type Config struct { CoverageReport *CoverageReport // AttachmentsEnabled specifies if attachments are enabled AttachmentsEnabled bool + // LegacyContractUpgradeEnabled enabled specifies whether to use the old parser when parsing an old contract + LegacyContractUpgradeEnabled bool } diff --git a/runtime/contract_update_test.go b/runtime/contract_update_test.go index 30463e7051..72a505427b 100644 --- a/runtime/contract_update_test.go +++ b/runtime/contract_update_test.go @@ -680,3 +680,118 @@ func TestRuntimeContractRedeploymentInSeparateTransactions(t *testing.T) { ) require.NoError(t, err) } + +func TestRuntimeLegacyContractUpdate(t *testing.T) { + t.Parallel() + + runtime := NewTestInterpreterRuntimeWithConfig(Config{ + AtreeValidationEnabled: true, + LegacyContractUpgradeEnabled: true, + }) + + accountCodes := map[common.Location][]byte{} + signerAccount := common.MustBytesToAddress([]byte{0x1}) + fooLocation := common.AddressLocation{ + Address: signerAccount, + Name: "Foo", + } + var checkGetAndSetProgram, getProgramCalled bool + + programs := map[Location]*interpreter.Program{} + clearPrograms := func() { + for l := range programs { + delete(programs, l) + } + } + + runtimeInterface := &TestRuntimeInterface{ + OnGetCode: func(location Location) (bytes []byte, err error) { + return accountCodes[location], nil + }, + Storage: NewTestLedger(nil, nil), + OnGetSigningAccounts: func() ([]Address, error) { + return []Address{signerAccount}, nil + }, + OnResolveLocation: NewSingleIdentifierLocationResolver(t), + OnGetAccountContractCode: func(location common.AddressLocation) (code []byte, err error) { + return accountCodes[location], nil + }, + OnUpdateAccountContractCode: func(location common.AddressLocation, code []byte) error { + accountCodes[location] = code + return nil + }, + OnEmitEvent: func(event cadence.Event) error { + return nil + }, + OnDecodeArgument: func(b []byte, t cadence.Type) (value cadence.Value, err error) { + return json.Decode(nil, b) + }, + OnGetAndSetProgram: func( + location Location, + load func() (*interpreter.Program, error), + ) ( + program *interpreter.Program, + err error, + ) { + _, isTransactionLocation := location.(common.TransactionLocation) + if checkGetAndSetProgram && !isTransactionLocation { + require.Equal(t, location, fooLocation) + require.False(t, getProgramCalled) + } + + var ok bool + program, ok = programs[location] + if ok { + return + } + + program, err = load() + + // NOTE: important: still set empty program, + // even if error occurred + + programs[location] = program + + return + }, + } + + nextTransactionLocation := NewTransactionLocationGenerator() + + const fooContractV1 = ` + pub contract Foo { + init() {} + pub fun hello() {} + } + ` + + const fooContractV2 = ` + access(all) contract Foo { + init() {} + access(all) fun hello() {} + } + ` + + // Mock the deploy of the old 'Foo' contract + accountCodes[fooLocation] = []byte(fooContractV1) + + // Programs are only valid during the transaction + clearPrograms() + + // Update 'Foo' contract to Cadence 1.0 version + + signerAccount = common.MustBytesToAddress([]byte{0x1}) + err := runtime.ExecuteTransaction( + Script{ + Source: UpdateTransaction("Foo", []byte(fooContractV2)), + }, + Context{ + Interface: runtimeInterface, + Location: nextTransactionLocation(), + }, + ) + require.NoError(t, err) + + // Programs are only valid during the transaction + clearPrograms() +} diff --git a/runtime/environment.go b/runtime/environment.go index 64f9944b62..94422f278c 100644 --- a/runtime/environment.go +++ b/runtime/environment.go @@ -192,6 +192,7 @@ func (e *interpreterEnvironment) newInterpreterConfig() *interpreter.Config { OnInvokedFunctionReturn: e.newOnInvokedFunctionReturnHandler(), CapabilityBorrowHandler: stdlib.BorrowCapabilityController, CapabilityCheckHandler: stdlib.CheckCapabilityController, + LegacyContractUpgradeEnabled: e.config.LegacyContractUpgradeEnabled, } } diff --git a/runtime/interpreter/config.go b/runtime/interpreter/config.go index 4188613384..441efbbee9 100644 --- a/runtime/interpreter/config.go +++ b/runtime/interpreter/config.go @@ -68,4 +68,6 @@ type Config struct { CapabilityCheckHandler CapabilityCheckHandlerFunc // CapabilityBorrowHandler is used to borrow ID capabilities CapabilityBorrowHandler CapabilityBorrowHandlerFunc + // LegacyContractUpgradeEnabled specifies whether to fall back to the old parser when attempting a contract upgrade + LegacyContractUpgradeEnabled bool } diff --git a/runtime/stdlib/account.go b/runtime/stdlib/account.go index 8800c4ffeb..f2a618bb7a 100644 --- a/runtime/stdlib/account.go +++ b/runtime/stdlib/account.go @@ -29,6 +29,7 @@ import ( "github.com/onflow/cadence/runtime/common" "github.com/onflow/cadence/runtime/errors" "github.com/onflow/cadence/runtime/interpreter" + "github.com/onflow/cadence/runtime/old_parser" "github.com/onflow/cadence/runtime/parser" "github.com/onflow/cadence/runtime/sema" ) @@ -1593,16 +1594,37 @@ func changeAccountContracts( }, ) + var legacyContractUpgrade bool + // if we are allowing legacy contract upgrades, fall back to the old parser when the new one fails + if !ignoreUpdatedProgramParserError(err) && invocation.Interpreter.SharedState.Config.LegacyContractUpgradeEnabled { + legacyContractUpgrade = true + oldProgram, err = old_parser.ParseProgram( + invocation.Interpreter.SharedState.Config.MemoryGauge, + oldCode, + old_parser.Config{}, + ) + } + if !ignoreUpdatedProgramParserError(err) { handleContractUpdateError(err) } - validator := NewContractUpdateValidator( - location, - contractName, - oldProgram, - program.Program, - ) + var validator UpdateValidator + if legacyContractUpgrade { + validator = NewLegacyContractUpdateValidator( + location, + contractName, + oldProgram, + program.Program, + ) + } else { + validator = NewContractUpdateValidator( + location, + contractName, + oldProgram, + program.Program, + ) + } err = validator.Validate() handleContractUpdateError(err) } diff --git a/runtime/stdlib/contract_update_validation.go b/runtime/stdlib/contract_update_validation.go index 4c7502a27e..7e36faefe3 100644 --- a/runtime/stdlib/contract_update_validation.go +++ b/runtime/stdlib/contract_update_validation.go @@ -27,6 +27,10 @@ import ( "github.com/onflow/cadence/runtime/errors" ) +type UpdateValidator interface { + Validate() error +} + type ContractUpdateValidator struct { TypeComparator @@ -40,6 +44,7 @@ type ContractUpdateValidator struct { // ContractUpdateValidator should implement ast.TypeEqualityChecker var _ ast.TypeEqualityChecker = &ContractUpdateValidator{} +var _ UpdateValidator = &ContractUpdateValidator{} // NewContractUpdateValidator initializes and returns a validator, without performing any validation. // Invoke the `Validate()` method of the validator returned, to start validating the contract. @@ -608,3 +613,37 @@ func (e *MissingDeclarationError) Error() string { e.Name, ) } + +type LegacyContractUpdateValidator struct { + TypeComparator + + location common.Location + contractName string + oldProgram *ast.Program + newProgram *ast.Program +} + +// NewContractUpdateValidator initializes and returns a validator, without performing any validation. +// Invoke the `Validate()` method of the validator returned, to start validating the contract. +func NewLegacyContractUpdateValidator( + location common.Location, + contractName string, + oldProgram *ast.Program, + newProgram *ast.Program, +) *LegacyContractUpdateValidator { + + return &LegacyContractUpdateValidator{ + location: location, + oldProgram: oldProgram, + newProgram: newProgram, + contractName: contractName, + } +} + +var _ UpdateValidator = &LegacyContractUpdateValidator{} + +// Validate validates the contract update, and returns an error if it is an invalid update. +// TODO: for now this is empty until we determine what validation is necessary for a Cadence 1.0 upgrade +func (validator *LegacyContractUpdateValidator) Validate() error { + return nil +} From f961da190b4aaae3878d76175aa6f041ad7cdcb6 Mon Sep 17 00:00:00 2001 From: Daniel Sainati Date: Wed, 24 Jan 2024 14:41:25 -0500 Subject: [PATCH 2/2] refactor --- runtime/stdlib/account.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/runtime/stdlib/account.go b/runtime/stdlib/account.go index f2a618bb7a..6a0b9b219a 100644 --- a/runtime/stdlib/account.go +++ b/runtime/stdlib/account.go @@ -1586,8 +1586,11 @@ func changeAccountContracts( oldCode, err := handler.GetAccountContractCode(location) handleContractUpdateError(err) + memoryGauge := invocation.Interpreter.SharedState.Config.MemoryGauge + legacyUpgradeEnabled := invocation.Interpreter.SharedState.Config.LegacyContractUpgradeEnabled + oldProgram, err := parser.ParseProgram( - invocation.Interpreter.SharedState.Config.MemoryGauge, + memoryGauge, oldCode, parser.Config{ IgnoreLeadingIdentifierEnabled: true, @@ -1596,10 +1599,10 @@ func changeAccountContracts( var legacyContractUpgrade bool // if we are allowing legacy contract upgrades, fall back to the old parser when the new one fails - if !ignoreUpdatedProgramParserError(err) && invocation.Interpreter.SharedState.Config.LegacyContractUpgradeEnabled { + if !ignoreUpdatedProgramParserError(err) && legacyUpgradeEnabled { legacyContractUpgrade = true oldProgram, err = old_parser.ParseProgram( - invocation.Interpreter.SharedState.Config.MemoryGauge, + memoryGauge, oldCode, old_parser.Config{}, )