Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FVM] Handle cadence ParentErrors #5272

Merged
merged 17 commits into from
Jan 29, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion engine/access/rpc/backend/backend_scripts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ var (
expectedResponse = []byte("response_data")

cadenceErr = fvmerrors.NewCodedError(fvmerrors.ErrCodeCadenceRunTimeError, "cadence error")
fvmFailureErr = fvmerrors.NewCodedError(fvmerrors.FailureCodeBlockFinderFailure, "fvm error")
fvmFailureErr = fvmerrors.NewCodedFailure(fvmerrors.FailureCodeBlockFinderFailure, "fvm error")
ctxCancelErr = fvmerrors.NewCodedError(fvmerrors.ErrCodeScriptExecutionCancelledError, "context canceled error")
timeoutErr = fvmerrors.NewCodedError(fvmerrors.ErrCodeScriptExecutionTimedOutError, "timeout error")
)
Expand Down
133 changes: 128 additions & 5 deletions fvm/errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,24 @@ type CodedError interface {
error
}

// CodedFailure is a subtype of CodedError specifically for failures
// This is used to make type checking with `errors.As` and `errors.Is` easier and more accurate
type CodedFailure interface {
CodedError
IsFailure()
}

// WrappedParentError is a wrapper for errors.ParentError to make it conform to the unwrappable
// interface used by the std errors lib. Once these error types are updated, this can be removed.
// See https://github.com/onflow/cadence/issues/3035
peterargue marked this conversation as resolved.
Show resolved Hide resolved
type WrappedParentError struct {
errors.ParentError
}

func (err WrappedParentError) Unwrap() []error {
return err.ParentError.ChildErrors()
}

// Is is a utility function to call std error lib `Is` function for instance equality checks.
func Is(err error, target error) bool {
return stdErrors.Is(err, target)
Expand Down Expand Up @@ -88,7 +106,7 @@ func SplitErrorTypes(inp error) (err CodedError, failure CodedError) {
}

if coded.Code().IsFailure() {
return nil, WrapCodedError(
return nil, WrapCodedFailure(
coded.Code(),
inp,
"failure caused by")
Expand All @@ -102,20 +120,72 @@ func SplitErrorTypes(inp error) (err CodedError, failure CodedError) {

// HandleRuntimeError handles runtime errors and separates
// errors generated by runtime from fvm errors (e.g. environment errors)
func HandleRuntimeError(err error) error {
if err == nil {
func HandleRuntimeError(inp error) (err error) {
if inp == nil {
return nil
}

// if is not a runtime error return as vm error
// this should never happen unless a bug in the code
runErr, ok := err.(runtime.Error)
runErr, ok := inp.(runtime.Error)
if !ok {
return NewUnknownFailure(err)
}

// wrap all runtime errors as CadenceRuntimeError
defer func() {
err = WrapCodedError(
ErrCodeCadenceRunTimeError,
err,
"cadence runtime error",
)
}()

// cadence ParentError types contain a list of errors encountered during execution
// these need to be handled carefully because the std errors lib and the logic in this file
// behave subtly different wrt which errors are returned. specifically:
// - std errors methods will recursively unwrap single errors and error lists until they finds
// the first match using a depth-first search.
// - findImportantCodedError will also recursively unwrap single errors and error lists, but it
// will continue to search until it finds the deepest coded error in the first branch containing
// any coded error. This means it will fail to find CodedFailures if there are any CodedErrors
// earlier in the unwrapped error list.
var parentErr errors.ParentError
if As(inp, &parentErr) {
// wrap ParentErrors so they conform to unwrappable interface
wrapped := WrappedParentError{parentErr}

// first search through all of the errors for a coded failure. this ensures that a failure
// is returned if there were any failures anywhere in the error tree.
peterargue marked this conversation as resolved.
Show resolved Hide resolved
var failure CodedFailure
if As(wrapped, &failure) {
err = WrapCodedFailure(
failure.Code(),
inp,
"cadence runtime failure caused by",
)
return
}

// next, do a depth-first search for the first coded error. this ensures that the deepest
// coded error in the chain is returned.
peterargue marked this conversation as resolved.
Show resolved Hide resolved
var coded CodedError
if As(wrapped, &coded) {
// find the deepest coded error
coded, _ = findImportantCodedError(coded)
// we've already check that the error is a CodedError, so ignoring the return value
err = WrapCodedError(
coded.Code(),
inp,
"cadence runtime error caused by",
)
return
}
}

// All other errors are non-fatal Cadence errors.
return NewCadenceRuntimeError(runErr)
err = runErr
return
}

// This returns true if the error or one of its nested errors matches the
Expand Down Expand Up @@ -164,6 +234,8 @@ type codedError struct {
err error
}

var _ CodedError = (*codedError)(nil)

func newError(
code ErrorCode,
rootCause error,
Expand All @@ -180,6 +252,9 @@ func WrapCodedError(
prefixMsgFormat string,
formatArguments ...interface{},
) codedError {
if code.IsFailure() {
panic(fmt.Sprintf("cannot wrap failure (%s) as CodedError", code))
}
peterargue marked this conversation as resolved.
Show resolved Hide resolved
if prefixMsgFormat != "" {
msg := fmt.Sprintf(prefixMsgFormat, formatArguments...)
err = fmt.Errorf("%s: %w", msg, err)
Expand All @@ -192,6 +267,9 @@ func NewCodedError(
format string,
formatArguments ...interface{},
) codedError {
if code.IsFailure() {
panic(fmt.Sprintf("cannot wrap failure (%s) as CodedError", code))
}
return newError(code, fmt.Errorf(format, formatArguments...))
}

Expand All @@ -207,6 +285,51 @@ func (err codedError) Code() ErrorCode {
return err.code
}

// codedFailure is a subtype of CodedError specifically for failures
type codedFailure struct {
CodedError
}

var _ CodedFailure = (*codedFailure)(nil)

func newFailure(
code ErrorCode,
rootCause error,
) codedFailure {
return codedFailure{
CodedError: newError(code, rootCause),
}
}

func WrapCodedFailure(
code ErrorCode,
err error,
prefixMsgFormat string,
formatArguments ...interface{},
) codedFailure {
if !code.IsFailure() {
panic(fmt.Sprintf("cannot wrap non-failure (%s) as CodedFailure", code))
}
if prefixMsgFormat != "" {
msg := fmt.Sprintf(prefixMsgFormat, formatArguments...)
err = fmt.Errorf("%s: %w", msg, err)
}
return newFailure(code, err)
}

func NewCodedFailure(
code ErrorCode,
format string,
formatArguments ...interface{},
) codedFailure {
if !code.IsFailure() {
panic(fmt.Sprintf("cannot wrap non-failure (%s) as CodedFailure", code))
}
return newFailure(code, fmt.Errorf(format, formatArguments...))
}

func (err codedFailure) IsFailure() {}

// NewEventEncodingError construct a new CodedError which indicates
// that encoding event has failed
func NewEventEncodingError(err error) CodedError {
Expand Down
175 changes: 174 additions & 1 deletion fvm/errors/errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import (
"fmt"
"testing"

"github.com/onflow/cadence/runtime"
cadenceErr "github.com/onflow/cadence/runtime/errors"
"github.com/onflow/cadence/runtime/sema"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/onflow/flow-go/model/flow"
Expand Down Expand Up @@ -39,7 +43,7 @@ func TestErrorHandling(t *testing.T) {
e5 := NewInvalidProposalSignatureError(flow.ProposalKey{}, e4)
e6 := fmt.Errorf("wrapped: %w", e5)

expectedErr := WrapCodedError(
expectedErr := WrapCodedFailure(
e3.Code(), // The shallowest failure's error code
e6, // All the error message detail.
"failure caused by")
Expand All @@ -61,3 +65,172 @@ func TestErrorHandling(t *testing.T) {
require.True(t, IsFailure(e1))
})
}

func TestHandleRuntimeError(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice

baseErr := fmt.Errorf("base error")
tests := []struct {
name string
err error
code ErrorCode
}{
{
name: "nil error",
err: nil,
code: 0,
},
{
name: "unknown error",
err: baseErr,
code: FailureCodeUnknownFailure,
},
{
name: "runtime error",
err: runtime.Error{Err: baseErr},
code: ErrCodeCadenceRunTimeError,
},
{
name: "coded error in Unwrappable error",
err: runtime.Error{
Err: cadenceErr.ExternalError{
Recovered: NewScriptExecutionCancelledError(baseErr),
},
},
code: ErrCodeScriptExecutionCancelledError,
},
{
name: "coded error in ParentError error",
err: runtime.Error{
Err: cadenceErr.ExternalError{
Recovered: sema.CheckerError{
Errors: []error{
fmt.Errorf("first error"),
NewScriptExecutionTimedOutError(),
},
},
},
},
code: ErrCodeScriptExecutionTimedOutError,
},
{
name: "first coded error returned",
err: runtime.Error{
Err: cadenceErr.ExternalError{
Recovered: sema.CheckerError{
Errors: []error{
fmt.Errorf("first error"),
NewScriptExecutionTimedOutError(),
NewScriptExecutionCancelledError(baseErr),
},
},
},
},
code: ErrCodeScriptExecutionTimedOutError,
},
{
name: "failure returned",
err: runtime.Error{
Err: cadenceErr.ExternalError{
Recovered: sema.CheckerError{
Errors: []error{
fmt.Errorf("first error"),
NewLedgerFailure(baseErr),
},
},
},
},
code: FailureCodeLedgerFailure,
},
{
name: "error before failure returns failure",
err: runtime.Error{
Err: cadenceErr.ExternalError{
Recovered: sema.CheckerError{
Errors: []error{
fmt.Errorf("first error"),
NewScriptExecutionTimedOutError(),
NewLedgerFailure(baseErr),
},
},
},
},
code: FailureCodeLedgerFailure,
},
{
name: "embedded coded errors return deepest error",
err: runtime.Error{
Err: cadenceErr.ExternalError{
Recovered: sema.CheckerError{
Errors: []error{
fmt.Errorf("first error"),
NewScriptExecutionCancelledError(
NewScriptExecutionTimedOutError(),
),
},
},
},
},
code: ErrCodeScriptExecutionTimedOutError,
},
{
name: "failure with embedded error returns failure",
err: runtime.Error{
Err: cadenceErr.ExternalError{
Recovered: sema.CheckerError{
Errors: []error{
fmt.Errorf("first error"),
NewLedgerFailure(
NewScriptExecutionTimedOutError(),
),
},
},
},
},
code: FailureCodeLedgerFailure,
},
{
name: "coded error with embedded failure returns failure",
err: runtime.Error{
Err: cadenceErr.ExternalError{
Recovered: sema.CheckerError{
Errors: []error{
fmt.Errorf("first error"),
NewScriptExecutionCancelledError(
NewLedgerFailure(baseErr),
),
},
},
},
},
code: FailureCodeLedgerFailure,
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
actual := HandleRuntimeError(tc.err)
if tc.code == 0 {
assert.NoError(t, actual)
return
}

coded, ok := actual.(CodedError)
require.True(t, ok, "error is not a CodedError")

if tc.code == FailureCodeUnknownFailure {
assert.Equalf(t, tc.code, coded.Code(), "error code mismatch: expected %d, got %d", tc.code, coded.Code())
return
}

// split the error to ensure that the wrapped error is available
actualCoded, failureCoded := SplitErrorTypes(coded)

if tc.code.IsFailure() {
assert.NoError(t, actualCoded)
assert.Equalf(t, tc.code, failureCoded.Code(), "error code mismatch: expected %d, got %d", tc.code, failureCoded.Code())
} else {
assert.NoError(t, failureCoded)
assert.Equalf(t, tc.code, actualCoded.Code(), "error code mismatch: expected %d, got %d", tc.code, actualCoded.Code())
}
})
}
}
Loading
Loading