diff --git a/runtime/runtime.go b/runtime/runtime.go index b920f1ec20..9481a618b7 100644 --- a/runtime/runtime.go +++ b/runtime/runtime.go @@ -182,6 +182,24 @@ func NewInterpreterRuntime(options ...Option) Runtime { return runtime } +func (r *interpreterRuntime) Recover(onError func(error), context Context) { + recovered := recover() + if recovered == nil { + return + } + + var err error + switch recovered := recovered.(type) { + case Error: + // avoid redundant wrapping + err = recovered + case error: + err = newError(recovered, context) + } + + onError(err) +} + func (r *interpreterRuntime) SetCoverageReport(coverageReport *CoverageReport) { r.coverageReport = coverageReport } @@ -194,7 +212,14 @@ func (r *interpreterRuntime) SetAtreeValidationEnabled(enabled bool) { r.atreeValidationEnabled = enabled } -func (r *interpreterRuntime) ExecuteScript(script Script, context Context) (cadence.Value, error) { +func (r *interpreterRuntime) ExecuteScript(script Script, context Context) (val cadence.Value, err error) { + defer r.Recover( + func(internalErr error) { + err = internalErr + }, + context, + ) + context.InitializeCodesAndPrograms() storage := r.newStorage(context.Interface) @@ -431,7 +456,14 @@ func (r *interpreterRuntime) InvokeContractFunction( arguments []interpreter.Value, argumentTypes []sema.Type, context Context, -) (cadence.Value, error) { +) (val cadence.Value, err error) { + defer r.Recover( + func(internalErr error) { + err = internalErr + }, + context, + ) + context.InitializeCodesAndPrograms() storage := r.newStorage(context.Interface) @@ -558,7 +590,14 @@ func (r *interpreterRuntime) convertArgument( return argument } -func (r *interpreterRuntime) ExecuteTransaction(script Script, context Context) error { +func (r *interpreterRuntime) ExecuteTransaction(script Script, context Context) (err error) { + defer r.Recover( + func(internalErr error) { + err = internalErr + }, + context, + ) + context.InitializeCodesAndPrograms() storage := r.newStorage(context.Interface) @@ -847,7 +886,20 @@ func hasValidStaticType(value interpreter.Value) bool { // ParseAndCheckProgram parses the given code and checks it. // Returns a program that can be interpreted (AST + elaboration). // -func (r *interpreterRuntime) ParseAndCheckProgram(code []byte, context Context) (*interpreter.Program, error) { +func (r *interpreterRuntime) ParseAndCheckProgram( + code []byte, + context Context, +) ( + program *interpreter.Program, + err error, +) { + defer r.Recover( + func(internalErr error) { + err = internalErr + }, + context, + ) + context.InitializeCodesAndPrograms() storage := r.newStorage(context.Interface) @@ -862,7 +914,7 @@ func (r *interpreterRuntime) ParseAndCheckProgram(code []byte, context Context) checkerOptions, ) - program, err := r.parseAndCheckProgram( + program, err = r.parseAndCheckProgram( code, context, functions, @@ -2733,7 +2785,21 @@ func (r *interpreterRuntime) executeNonProgram(interpret interpretFunc, context return exportValue(value) } -func (r *interpreterRuntime) ReadStored(address common.Address, path cadence.Path, context Context) (cadence.Value, error) { +func (r *interpreterRuntime) ReadStored( + address common.Address, + path cadence.Path, + context Context, +) ( + val cadence.Value, + err error, +) { + defer r.Recover( + func(internalErr error) { + err = internalErr + }, + context, + ) + return r.executeNonProgram( func(inter *interpreter.Interpreter) (interpreter.Value, error) { key := interpreter.PathToStorageKey(importPathValue(path)) @@ -2744,7 +2810,21 @@ func (r *interpreterRuntime) ReadStored(address common.Address, path cadence.Pat ) } -func (r *interpreterRuntime) ReadLinked(address common.Address, path cadence.Path, context Context) (cadence.Value, error) { +func (r *interpreterRuntime) ReadLinked( + address common.Address, + path cadence.Path, + context Context, +) ( + val cadence.Value, + err error, +) { + defer r.Recover( + func(internalErr error) { + err = internalErr + }, + context, + ) + return r.executeNonProgram( func(inter *interpreter.Interpreter) (interpreter.Value, error) { key, _, err := inter.GetCapabilityFinalTargetStorageKey( diff --git a/runtime/runtime_test.go b/runtime/runtime_test.go index ff0036fa8c..0db8cf97cd 100644 --- a/runtime/runtime_test.go +++ b/runtime/runtime_test.go @@ -3111,7 +3111,7 @@ func TestRuntimeInvokeContractFunction(t *testing.T) { getAccountContractCode: func(_ Address, _ string) (code []byte, err error) { return accountCode, nil }, - updateAccountContractCode: func(addess Address, _ string, code []byte) error { + updateAccountContractCode: func(_ Address, _ string, code []byte) error { accountCode = code return nil }, @@ -3230,27 +3230,28 @@ func TestRuntimeInvokeContractFunction(t *testing.T) { }) t.Run("function with not enough arguments panics", func(tt *testing.T) { - assert.Panics(tt, func() { - _, _ = runtime.InvokeContractFunction( - common.AddressLocation{ - Address: addressValue, - Name: "Test", - }, - "helloMultiArg", - []interpreter.Value{ - interpreter.NewStringValue("number"), - interpreter.NewIntValueFromInt64(42), - }, - []sema.Type{ - sema.StringType, - sema.IntType, - }, - Context{ - Interface: runtimeInterface, - Location: nextTransactionLocation(), - }, - ) - }) + _, err = runtime.InvokeContractFunction( + common.AddressLocation{ + Address: addressValue, + Name: "Test", + }, + "helloMultiArg", + []interpreter.Value{ + interpreter.NewStringValue("number"), + interpreter.NewIntValueFromInt64(42), + }, + []sema.Type{ + sema.StringType, + sema.IntType, + }, + Context{ + Interface: runtimeInterface, + Location: nextTransactionLocation(), + }, + ) + + require.Error(tt, err) + assert.ErrorAs(tt, err, &Error{}) }) t.Run("function with incorrect argument type errors", func(tt *testing.T) { @@ -3375,7 +3376,7 @@ func TestRuntimeContractNestedResource(t *testing.T) { getAccountContractCode: func(_ Address, _ string) (code []byte, err error) { return accountCode, nil }, - updateAccountContractCode: func(addess Address, _ string, code []byte) error { + updateAccountContractCode: func(_ Address, _ string, code []byte) error { accountCode = code return nil }, @@ -5701,7 +5702,7 @@ func TestRuntimeExternalError(t *testing.T) { t.Parallel() - runtime := newTestInterpreterRuntime() + interpreterRuntime := newTestInterpreterRuntime() script := []byte(` transaction { @@ -5724,22 +5725,18 @@ func TestRuntimeExternalError(t *testing.T) { nextTransactionLocation := newTransactionLocationGenerator() - assert.PanicsWithValue(t, - interpreter.ExternalError{ - Recovered: logPanic{}, + err := interpreterRuntime.ExecuteTransaction( + Script{ + Source: script, }, - func() { - _ = runtime.ExecuteTransaction( - Script{ - Source: script, - }, - Context{ - Interface: runtimeInterface, - Location: nextTransactionLocation(), - }, - ) + Context{ + Interface: runtimeInterface, + Location: nextTransactionLocation(), }, ) + + require.Error(t, err) + assert.ErrorAs(t, err, &interpreter.ExternalError{}) } func TestRuntimeDeployCodeCaching(t *testing.T) { @@ -6820,3 +6817,270 @@ func TestRuntimeStackOverflow(t *testing.T) { var callStackLimitExceededErr CallStackLimitExceededError require.ErrorAs(t, err, &callStackLimitExceededErr) } + +func TestRuntimeInternalErrors(t *testing.T) { + + t.Parallel() + + runtime := newTestInterpreterRuntime() + + t.Run("script with go error", func(t *testing.T) { + + t.Parallel() + + script := []byte(` + pub fun main() { + log("hello") + } + `) + + runtimeInterface := &testRuntimeInterface{ + log: func(message string) { + // panic due to go-error in cadence implementation + var val interface{} = message + _ = val.(int) + }, + } + + nextTransactionLocation := newTransactionLocationGenerator() + + _, err := runtime.ExecuteScript( + Script{ + Source: script, + }, + Context{ + Interface: runtimeInterface, + Location: nextTransactionLocation(), + }, + ) + + require.Error(t, err) + require.ErrorAs(t, err, &Error{}) + }) + + t.Run("script with cadence error", func(t *testing.T) { + + t.Parallel() + + script := []byte(` + pub fun main() { + log("hello") + } + `) + + runtimeInterface := &testRuntimeInterface{ + log: func(message string) { + // intentionally panic + panic(fmt.Errorf("panic trying to log %s", message)) + }, + } + + nextTransactionLocation := newTransactionLocationGenerator() + + _, err := runtime.ExecuteScript( + Script{ + Source: script, + }, + Context{ + Interface: runtimeInterface, + Location: nextTransactionLocation(), + }, + ) + + require.Error(t, err) + require.ErrorAs(t, err, &Error{}) + }) + + t.Run("transaction", func(t *testing.T) { + + t.Parallel() + + script := []byte(` + transaction { + prepare() {} + execute { + log("hello") + } + } + `) + + runtimeInterface := &testRuntimeInterface{ + log: func(message string) { + // panic due to Cadence implementation error + var val interface{} = message + _ = val.(int) + }, + } + + nextTransactionLocation := newTransactionLocationGenerator() + + err := runtime.ExecuteTransaction( + Script{ + Source: script, + }, + Context{ + Interface: runtimeInterface, + Location: nextTransactionLocation(), + }, + ) + + require.Error(t, err) + require.ErrorAs(t, err, &Error{}) + }) + + t.Run("contract function", func(t *testing.T) { + + t.Parallel() + + addressValue := Address{ + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, + } + + contract := []byte(` + pub contract Test { + pub fun hello() { + log("Hello World!") + } + } + `) + + var accountCode []byte + + storage := newTestLedger(nil, nil) + + runtimeInterface := &testRuntimeInterface{ + storage: storage, + getSigningAccounts: func() ([]Address, error) { + return []Address{addressValue}, nil + }, + resolveLocation: singleIdentifierLocationResolver(t), + getAccountContractCode: func(_ Address, _ string) (code []byte, err error) { + return accountCode, nil + }, + updateAccountContractCode: func(_ Address, _ string, code []byte) error { + accountCode = code + return nil + }, + emitEvent: func(_ cadence.Event) error { + return nil + }, + log: func(message string) { + // panic due to Cadence implementation error + var val interface{} = message + _ = val.(int) + }, + } + + nextTransactionLocation := newTransactionLocationGenerator() + + deploy := utils.DeploymentTransaction("Test", contract) + err := runtime.ExecuteTransaction( + Script{ + Source: deploy, + }, + Context{ + Interface: runtimeInterface, + Location: nextTransactionLocation(), + }, + ) + require.NoError(t, err) + + assert.NotNil(t, accountCode) + + _, err = runtime.InvokeContractFunction( + common.AddressLocation{ + Address: addressValue, + Name: "Test", + }, + "hello", + nil, + nil, + Context{ + Interface: runtimeInterface, + Location: nextTransactionLocation(), + }, + ) + + require.Error(t, err) + require.ErrorAs(t, err, &Error{}) + }) + + t.Run("parse and check", func(t *testing.T) { + + t.Parallel() + + script := []byte("pub fun test() {}") + runtimeInterface := &testRuntimeInterface{ + setProgram: func(location Location, program *interpreter.Program) error { + panic(errors.New("crash while setting program")) + }, + } + + nextTransactionLocation := newTransactionLocationGenerator() + + _, err := runtime.ParseAndCheckProgram( + script, + Context{ + Interface: runtimeInterface, + Location: nextTransactionLocation(), + }, + ) + + require.Error(t, err) + require.ErrorAs(t, err, &Error{}) + }) + + t.Run("read stored", func(t *testing.T) { + + t.Parallel() + + runtimeInterface := &testRuntimeInterface{ + storage: testLedger{ + getValue: func(owner, key []byte) (value []byte, err error) { + panic(errors.New("crasher")) + }, + }, + } + + _, err := runtime.ReadStored( + common.BytesToAddress([]byte{0x42}), + cadence.Path{ + Domain: "storage", + Identifier: "test", + }, + Context{ + Interface: runtimeInterface, + }, + ) + + require.Error(t, err) + require.ErrorAs(t, err, &Error{}) + }) + + t.Run("read linked", func(t *testing.T) { + + t.Parallel() + + runtimeInterface := &testRuntimeInterface{ + storage: testLedger{ + getValue: func(owner, key []byte) (value []byte, err error) { + panic(errors.New("crasher")) + }, + }, + } + + _, err := runtime.ReadLinked( + common.BytesToAddress([]byte{0x42}), + cadence.Path{ + Domain: "storage", + Identifier: "test", + }, + Context{ + Interface: runtimeInterface, + }, + ) + + require.Error(t, err) + require.ErrorAs(t, err, &Error{}) + }) +}