From c985da48923754c6b3472dd83ddcd2c91fd4da87 Mon Sep 17 00:00:00 2001 From: Faye Amacker <33205765+fxamacker@users.noreply.github.com> Date: Thu, 25 Jul 2024 16:58:03 -0500 Subject: [PATCH] Return user error when CCF encodes attachment field The CCF encoder currently requires field values and types to match. However, composite values that have attachment field don't satisfy this requirement because: - composite field values INCLUDE attachment field values - composite field types EXCLUDE attachment field types Currently, the encoder rejects this mismatch with a panic. This commit makes CCF encoder return a user error rather than panic for the caller to handle when this mismatch is encountered. --- encoding/ccf/ccf_test.go | 66 ++++++++++++++++++++++++++++++++++++++++ encoding/ccf/encode.go | 52 ++++++++++++++++++++++++++++++- 2 files changed, 117 insertions(+), 1 deletion(-) diff --git a/encoding/ccf/ccf_test.go b/encoding/ccf/ccf_test.go index 258cc85c81..f4eeacce09 100644 --- a/encoding/ccf/ccf_test.go +++ b/encoding/ccf/ccf_test.go @@ -35,9 +35,11 @@ import ( "github.com/onflow/cadence/encoding/ccf" "github.com/onflow/cadence/runtime" "github.com/onflow/cadence/runtime/common" + "github.com/onflow/cadence/runtime/errors" "github.com/onflow/cadence/runtime/interpreter" "github.com/onflow/cadence/runtime/sema" "github.com/onflow/cadence/runtime/tests/checker" + "github.com/onflow/cadence/runtime/tests/runtime_utils" "github.com/onflow/cadence/runtime/tests/utils" ) @@ -17108,3 +17110,67 @@ func TestDecodeFunctionTypeBackwardCompatibility(t *testing.T) { testDecode(t, data, val) } + +func TestEncodeEventWithAttachement(t *testing.T) { + script := ` + access(all) struct S { + access(all) let x: Int + init(x: Int) { + self.x = x + } + } + + access(all) attachment A for S { + access(all) let y: Int + init(y: Int) { + self.y = y + } + } + + access(all) event Foo(s: S) + + access(all) + fun main() { + let s = attach A(y: 3) to S(x: 4) + emit Foo(s: s) + } + ` + + v := exportEventFromScript(t, script) + + _, err := ccf.Encode(v) + + var attachmentFieldError ccf.AttachmentFieldNotSupportedEncodingError + require.ErrorAs(t, err, &attachmentFieldError) + require.Implements(t, (*errors.UserError)(nil), attachmentFieldError) +} + +func exportEventFromScript(t *testing.T, script string) cadence.Event { + rt := runtime_utils.NewTestInterpreterRuntime() + + var events []cadence.Event + + inter := &runtime_utils.TestRuntimeInterface{ + OnEmitEvent: func(event cadence.Event) error { + events = append(events, event) + return nil + }, + } + + _, err := rt.ExecuteScript( + runtime.Script{ + Source: []byte(script), + }, + runtime.Context{ + Interface: inter, + Location: common.ScriptLocation{}, + }, + ) + + require.NoError(t, err) + require.Len(t, events, 1) + + event := events[0] + + return event +} diff --git a/encoding/ccf/encode.go b/encoding/ccf/encode.go index 9317773150..f7fa8a8ca5 100644 --- a/encoding/ccf/encode.go +++ b/encoding/ccf/encode.go @@ -176,6 +176,40 @@ func (em *encMode) NewEncoder(w io.Writer) *Encoder { var defaultEncMode = &encMode{} +// AttachmentFieldNotSupportedEncodingError is a user error that is returned +// when encoding composite value (such as Event) that has cadence.Attachment field. +type AttachmentFieldNotSupportedEncodingError struct { + compositeType string + fieldCount int + fieldTypeCount int +} + +func newAttachmentFieldNotSupportedError( + compositeType string, + fieldCount int, + fieldTypeCount int, +) error { + return AttachmentFieldNotSupportedEncodingError{ + compositeType: compositeType, + fieldCount: fieldCount, + fieldTypeCount: fieldTypeCount, + } +} + +func (e AttachmentFieldNotSupportedEncodingError) Error() string { + return fmt.Sprintf( + "encoding attachment field in composite value isn't supported: %s field count %d doesn't match declared field type count %d", + e.compositeType, + e.fieldCount, + e.fieldTypeCount, + ) +} + +func (e AttachmentFieldNotSupportedEncodingError) IsUserError() { +} + +var _ cadenceErrors.UserError = AttachmentFieldNotSupportedEncodingError{} + // Encode returns the CCF-encoded representation of the given value // by using default CCF encoding options. This function returns an // error if the Cadence value cannot be represented in CCF. @@ -219,7 +253,7 @@ func (e *Encoder) Encode(value cadence.Value) (err error) { // Add context to error if there is any. if err != nil { err = fmt.Errorf( - "ccf: failed to encode value (type %T, %q): %s", + "ccf: failed to encode value (type %T, %q): %w", value, value.Type().ID(), err, @@ -1102,6 +1136,22 @@ func (e *Encoder) encodeComposite( staticFieldTypes := getCompositeTypeFields(typ) if len(staticFieldTypes) != len(fields) { + + // The CCF encoder requires field values and types to match. However, + // composite values that have attachment field don't satisfy this requirement because: + // - composite field values INCLUDE attachment field values + // - composite field types EXCLUDE attachment field types + // Given this, CCF encoder will return a user error if composite value has attachment field. + for _, f := range fields { + if _, ok := f.(cadence.Attachment); ok { + panic(newAttachmentFieldNotSupportedError( + typ.ID(), + len(fields), + len(staticFieldTypes), + )) + } + } + panic(cadenceErrors.NewUnexpectedError( "%s field count %d doesn't match declared field type count %d", typ.ID(),