Skip to content

Commit

Permalink
Merge pull request #5272 from onflow/petera/handle-cadence-parent-err…
Browse files Browse the repository at this point in the history
…ors-fvm

[FVM] Handle cadence ParentErrors
  • Loading branch information
peterargue authored Jan 29, 2024
2 parents 7bb57e9 + 8a9af22 commit c665488
Show file tree
Hide file tree
Showing 14 changed files with 574 additions and 113 deletions.
11 changes: 6 additions & 5 deletions engine/access/rpc/backend/backend_scripts.go
Original file line number Diff line number Diff line change
Expand Up @@ -332,13 +332,14 @@ func convertScriptExecutionError(err error, height uint64) error {
return nil
}

var failure fvmerrors.CodedFailure
if fvmerrors.As(err, &failure) {
return rpc.ConvertError(err, "failed to execute script", codes.Internal)
}

// general FVM/ledger errors
var coded fvmerrors.CodedError
if fvmerrors.As(err, &coded) {
// general FVM/ledger errors
if coded.Code().IsFailure() {
return rpc.ConvertError(err, "failed to execute script", codes.Internal)
}

switch coded.Code() {
case fvmerrors.ErrCodeScriptExecutionCancelledError:
return status.Errorf(codes.Canceled, "script execution canceled: %v", err)
Expand Down
8 changes: 4 additions & 4 deletions 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 Expand Up @@ -438,7 +438,7 @@ func (s *BackendScriptsSuite) testExecuteScriptAtLatestBlock(ctx context.Context
} else {
actual, err := backend.ExecuteScriptAtLatestBlock(ctx, s.failingScript, s.arguments)
s.Require().Error(err)
s.Require().Equal(statusCode, status.Code(err))
s.Require().Equal(statusCode, status.Code(err), "error code mismatch: expected %d, got %d: %s", statusCode, status.Code(err), err)
s.Require().Nil(actual)
}
}
Expand All @@ -454,7 +454,7 @@ func (s *BackendScriptsSuite) testExecuteScriptAtBlockID(ctx context.Context, ba
} else {
actual, err := backend.ExecuteScriptAtBlockID(ctx, blockID, s.failingScript, s.arguments)
s.Require().Error(err)
s.Require().Equal(statusCode, status.Code(err))
s.Require().Equal(statusCode, status.Code(err), "error code mismatch: expected %d, got %d: %s", statusCode, status.Code(err), err)
s.Require().Nil(actual)
}
}
Expand All @@ -470,7 +470,7 @@ func (s *BackendScriptsSuite) testExecuteScriptAtBlockHeight(ctx context.Context
} else {
actual, err := backend.ExecuteScriptAtBlockHeight(ctx, height, s.failingScript, s.arguments)
s.Require().Error(err)
s.Require().Equal(statusCode, status.Code(err))
s.Require().Equalf(statusCode, status.Code(err), "error code mismatch: expected %d, got %d: %s", statusCode, status.Code(err), err)
s.Require().Nil(actual)
}
}
39 changes: 19 additions & 20 deletions fvm/errors/codes.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,30 @@ import "fmt"

type ErrorCode uint16

func (ec ErrorCode) IsFailure() bool {
return ec >= FailureCodeUnknownFailure
}

func (ec ErrorCode) String() string {
if ec.IsFailure() {
return fmt.Sprintf("[Failure Code: %d]", ec)
}
return fmt.Sprintf("[Error Code: %d]", ec)
}

type FailureCode uint16

func (fc FailureCode) String() string {
return fmt.Sprintf("[Failure Code: %d]", fc)
}

const (
FailureCodeUnknownFailure ErrorCode = 2000
FailureCodeEncodingFailure ErrorCode = 2001
FailureCodeLedgerFailure ErrorCode = 2002
FailureCodeStateMergeFailure ErrorCode = 2003
FailureCodeBlockFinderFailure ErrorCode = 2004
// Deprecated: No longer used.
FailureCodeHasherFailure ErrorCode = 2005
FailureCodeParseRestrictedModeInvalidAccessFailure ErrorCode = 2006
FailureCodePayerBalanceCheckFailure ErrorCode = 2007
FailureCodeDerivedDataCacheImplementationFailure ErrorCode = 2008
FailureCodeRandomSourceFailure ErrorCode = 2009
// Deprecated: No longer used.
FailureCodeMetaTransactionFailure ErrorCode = 2100
FailureCodeUnknownFailure FailureCode = 2000
FailureCodeEncodingFailure FailureCode = 2001
FailureCodeLedgerFailure FailureCode = 2002
FailureCodeStateMergeFailure FailureCode = 2003
FailureCodeBlockFinderFailure FailureCode = 2004
// Deprecated: No longer used.
FailureCodeHasherFailure FailureCode = 2005
FailureCodeParseRestrictedModeInvalidAccessFailure FailureCode = 2006
FailureCodePayerBalanceCheckFailure FailureCode = 2007
FailureCodeDerivedDataCacheImplementationFailure FailureCode = 2008
FailureCodeRandomSourceFailure FailureCode = 2009
// Deprecated: No longer used.
FailureCodeMetaTransactionFailure FailureCode = 2100
)

const (
Expand Down
195 changes: 158 additions & 37 deletions fvm/errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,29 @@ import (
)

type Unwrappable interface {
error
Unwrap() error
}

type UnwrappableErrors interface {
error
Unwrap() []error
}

type CodedError interface {
Code() ErrorCode

Unwrappable
error
}

type CodedFailure interface {
Code() FailureCode

Unwrappable
error
}

// 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 All @@ -33,15 +46,14 @@ func As(err error, target interface{}) bool {
return stdErrors.As(err, target)
}

// findImportantCodedError recursively unwraps the error to search for important
// coded error:
// findRootCodedError recursively unwraps the error to search for the root (deepest) coded error:
// 1. If err is nil, this returns (nil, false),
// 2. If err has no error code, this returns (nil, true),
// 3. If err has a failure error code, this returns
// (<the shallowest failure coded error>, false),
// 4. If err has a non-failure error code, this returns
// (<the deepest, aka root cause, non-failure coded error>, false)
func findImportantCodedError(err error) (CodedError, bool) {
// 3. If err has an error code, this returns
// (<the deepest, aka root cause, coded error>, false)
//
// Note: This assumes the caller has already checked if the error contains a CodedFailure.
func findRootCodedError(err error) (CodedError, bool) {
if err == nil {
return nil, false
}
Expand All @@ -52,10 +64,6 @@ func findImportantCodedError(err error) (CodedError, bool) {
}

for {
if coded.Code().IsFailure() {
return coded, false
}

var nextCoded CodedError
if !As(coded.Unwrap(), &nextCoded) {
return coded, false
Expand All @@ -68,32 +76,45 @@ func findImportantCodedError(err error) (CodedError, bool) {
// IsFailure returns true if the error is un-coded, or if the error contains
// a failure code.
func IsFailure(err error) bool {
return AsFailure(err) != nil
}

func AsFailure(err error) CodedFailure {
if err == nil {
return false
return nil
}

var failure CodedFailure
if As(err, &failure) {
return failure
}

coded, isUnknown := findImportantCodedError(err)
return isUnknown || coded.Code().IsFailure()
var coded CodedError
if !As(err, &coded) {
return NewUnknownFailure(err)
}

return nil
}

// SplitErrorTypes splits the error into fatal (failures) and non-fatal errors
func SplitErrorTypes(inp error) (err CodedError, failure CodedError) {
func SplitErrorTypes(inp error) (err CodedError, failure CodedFailure) {
if inp == nil {
return nil, nil
}

coded, isUnknown := findImportantCodedError(inp)
if isUnknown {
return nil, NewUnknownFailure(inp)
}

if coded.Code().IsFailure() {
return nil, WrapCodedError(
coded.Code(),
if failure = AsFailure(inp); failure != nil {
return nil, WrapCodedFailure(
failure.Code(),
inp,
"failure caused by")
}

coded, isUnknown := findRootCodedError(inp)
if isUnknown {
return nil, NewUnknownFailure(inp)
}

return WrapCodedError(
coded.Code(),
inp,
Expand All @@ -118,38 +139,86 @@ func HandleRuntimeError(err error) error {
return NewCadenceRuntimeError(runErr)
}

// This returns true if the error or one of its nested errors matches the
// HasErrorCode returns true if the error or one of its nested errors matches the
// specified error code.
func HasErrorCode(err error, code ErrorCode) bool {
return Find(err, code) != nil
}

// This recursively unwraps the error and returns first CodedError that matches
// HasFailureCode returns true if the error or one of its nested errors matches the
// specified failure code.
func HasFailureCode(err error, code FailureCode) bool {
return FindFailure(err, code) != nil
}

// Find recursively unwraps the error and returns the first CodedError that matches
// the specified error code.
func Find(originalErr error, code ErrorCode) CodedError {
if originalErr == nil {
return nil
}

var unwrappable Unwrappable
if !As(originalErr, &unwrappable) {
// Handle non-chained errors
var unwrappedErrs []error
switch err := originalErr.(type) {
case *multierror.Error:
unwrappedErrs = err.WrappedErrors()
case UnwrappableErrors:
unwrappedErrs = err.Unwrap()

// IMPORTANT: this check needs to run after *multierror.Error because multierror does implement
// the Unwrappable interface, however its implementation only visits the base errors in the list,
// and ignores their descendants.
case Unwrappable:
coded, ok := err.(CodedError)
if ok && coded.Code() == code {
return coded
}
return Find(err.Unwrap(), code)
default:
return nil
}

coded, ok := unwrappable.(CodedError)
if ok && coded.Code() == code {
return coded
for _, innerErr := range unwrappedErrs {
coded := Find(innerErr, code)
if coded != nil {
return coded
}
}

// NOTE: we need to special case multierror.Error since As() will only
// inspect the first error within multierror.Error.
errors, ok := unwrappable.(*multierror.Error)
if !ok {
return Find(unwrappable.Unwrap(), code)
return nil
}

// FindFailure recursively unwraps the error and returns the first CodedFailure that matches
// the specified error code.
func FindFailure(originalErr error, code FailureCode) CodedFailure {
if originalErr == nil {
return nil
}

for _, innerErr := range errors.Errors {
coded = Find(innerErr, code)
// Handle non-chained errors
var unwrappedErrs []error
switch err := originalErr.(type) {
case *multierror.Error:
unwrappedErrs = err.WrappedErrors()
case UnwrappableErrors:
unwrappedErrs = err.Unwrap()

// IMPORTANT: this check needs to run after *multierror.Error because multierror does implement
// the Unwrappable interface, however its implementation only visits the base errors in the list,
// and ignores their descendants.
case Unwrappable:
coded, ok := err.(CodedFailure)
if ok && coded.Code() == code {
return coded
}
return FindFailure(err.Unwrap(), code)
default:
return nil
}

for _, innerErr := range unwrappedErrs {
coded := FindFailure(innerErr, code)
if coded != nil {
return coded
}
Expand All @@ -158,6 +227,8 @@ func Find(originalErr error, code ErrorCode) CodedError {
return nil
}

var _ CodedError = (*codedError)(nil)

type codedError struct {
code ErrorCode

Expand Down Expand Up @@ -207,6 +278,56 @@ func (err codedError) Code() ErrorCode {
return err.code
}

var _ CodedFailure = (*codedFailure)(nil)

type codedFailure struct {
code FailureCode
err error
}

func newFailure(
code FailureCode,
rootCause error,
) codedFailure {
return codedFailure{
code: code,
err: rootCause,
}
}

func WrapCodedFailure(
code FailureCode,
err error,
prefixMsgFormat string,
formatArguments ...interface{},
) codedFailure {
if prefixMsgFormat != "" {
msg := fmt.Sprintf(prefixMsgFormat, formatArguments...)
err = fmt.Errorf("%s: %w", msg, err)
}
return newFailure(code, err)
}

func NewCodedFailure(
code FailureCode,
format string,
formatArguments ...interface{},
) codedFailure {
return newFailure(code, fmt.Errorf(format, formatArguments...))
}

func (err codedFailure) Unwrap() error {
return err.err
}

func (err codedFailure) Error() string {
return fmt.Sprintf("%v %v", err.code, err.err)
}

func (err codedFailure) Code() FailureCode {
return err.code
}

// NewEventEncodingError construct a new CodedError which indicates
// that encoding event has failed
func NewEventEncodingError(err error) CodedError {
Expand Down
Loading

0 comments on commit c665488

Please sign in to comment.