diff --git a/cmd/cmd.go b/cmd/cmd.go index 1ccd9d3340..3dc73da026 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -25,6 +25,7 @@ import ( "os" "time" + "github.com/onflow/cadence" "github.com/onflow/cadence/activations" "github.com/onflow/cadence/ast" "github.com/onflow/cadence/common" @@ -435,3 +436,13 @@ func (h *StandardLibraryHandler) IsContractBeingAdded(common.AddressLocation) bo // NO-OP return false } + +func (h *StandardLibraryHandler) ExportValue( + _ interpreter.Value, + _ interpreter.ValueExportContext, +) ( + cadence.Value, + error, +) { + return nil, goerrors.New("exporting values is not available in this environment") +} diff --git a/common/computationkind.go b/common/computationkind.go index 41529090ad..e8e451bedc 100644 --- a/common/computationkind.go +++ b/common/computationkind.go @@ -142,4 +142,6 @@ const ( // RLP ComputationKindSTDLIBRLPDecodeString ComputationKindSTDLIBRLPDecodeList + // CCF + ComputationKindSTDLIBCCFEncode ) diff --git a/common/computationkind_string.go b/common/computationkind_string.go index 302fdd11bb..b2487fc7db 100644 --- a/common/computationkind_string.go +++ b/common/computationkind_string.go @@ -27,6 +27,7 @@ func _() { _ = x[ComputationKindSTDLIBRevertibleRandom-1102] _ = x[ComputationKindSTDLIBRLPDecodeString-1108] _ = x[ComputationKindSTDLIBRLPDecodeList-1109] + _ = x[ComputationKindSTDLIBCCFEncode-1110] } const ( @@ -37,7 +38,7 @@ const ( _ComputationKind_name_4 = "CreateDictionaryValueTransferDictionaryValueDestroyDictionaryValue" _ComputationKind_name_5 = "EncodeValue" _ComputationKind_name_6 = "STDLIBPanicSTDLIBAssertSTDLIBRevertibleRandom" - _ComputationKind_name_7 = "STDLIBRLPDecodeStringSTDLIBRLPDecodeList" + _ComputationKind_name_7 = "STDLIBRLPDecodeStringSTDLIBRLPDecodeListSTDLIBCCFEncode" ) var ( @@ -46,7 +47,7 @@ var ( _ComputationKind_index_3 = [...]uint8{0, 16, 34, 51} _ComputationKind_index_4 = [...]uint8{0, 21, 44, 66} _ComputationKind_index_6 = [...]uint8{0, 11, 23, 45} - _ComputationKind_index_7 = [...]uint8{0, 21, 40} + _ComputationKind_index_7 = [...]uint8{0, 21, 40, 55} ) func (i ComputationKind) String() string { @@ -70,7 +71,7 @@ func (i ComputationKind) String() string { case 1100 <= i && i <= 1102: i -= 1100 return _ComputationKind_name_6[_ComputationKind_index_6[i]:_ComputationKind_index_6[i+1]] - case 1108 <= i && i <= 1109: + case 1108 <= i && i <= 1110: i -= 1108 return _ComputationKind_name_7[_ComputationKind_index_7[i]:_ComputationKind_index_7[i+1]] default: diff --git a/encoding/ccf/decode_type.go b/encoding/ccf/decode_type.go index 3fa743c6f9..a6521cdee0 100644 --- a/encoding/ccf/decode_type.go +++ b/encoding/ccf/decode_type.go @@ -113,7 +113,7 @@ func (d *Decoder) decodeSimpleTypeID() (cadence.Type, error) { return nil, err } - ty := typeBySimpleTypeID(SimpleType(simpleTypeID)) + ty := TypeBySimpleTypeID(SimpleType(simpleTypeID)) if ty == nil { return nil, fmt.Errorf("unsupported encoded simple type ID %d", simpleTypeID) } diff --git a/encoding/ccf/encode.go b/encoding/ccf/encode.go index 47e87a328e..18e75119d2 100644 --- a/encoding/ccf/encode.go +++ b/encoding/ccf/encode.go @@ -1429,7 +1429,7 @@ func (e *Encoder) encodeTypeValue(typ cadence.Type, visited ccfTypeIDByCadenceTy visited[cadenceTypeID] = ccfTypeID(len(visited)) } - simpleTypeID, ok := simpleTypeIDByType(typ) + simpleTypeID, ok := SimpleTypeIDByType(typ) if ok { return e.encodeSimpleTypeValue(simpleTypeID) } diff --git a/encoding/ccf/encode_type.go b/encoding/ccf/encode_type.go index 509a6b4dd2..0ca29d7fe2 100644 --- a/encoding/ccf/encode_type.go +++ b/encoding/ccf/encode_type.go @@ -46,7 +46,7 @@ type encodeTypeFn func(typ cadence.Type, tids ccfTypeIDByCadenceType) error // All exported Cadence types need to be supported by this function, // including abstract and interface types. func (e *Encoder) encodeInlineType(typ cadence.Type, tids ccfTypeIDByCadenceType) error { - simpleTypeID, ok := simpleTypeIDByType(typ) + simpleTypeID, ok := SimpleTypeIDByType(typ) if ok { return e.encodeSimpleType(simpleTypeID) } diff --git a/encoding/ccf/simpletype.go b/encoding/ccf/simpletype.go index 48bbba6d84..aace7d000d 100644 --- a/encoding/ccf/simpletype.go +++ b/encoding/ccf/simpletype.go @@ -35,7 +35,7 @@ import ( // When new simple cadence.Type is added, // - ADD new ID to the end of existing IDs, // - ADD new simple cadence.Type and its ID -// to simpleTypeIDByType *and* typeBySimpleTypeID +// to SimpleTypeIDByType *and* TypeBySimpleTypeID type SimpleType uint64 @@ -156,7 +156,7 @@ const ( // Cadence simple type IDs SimpleType_Count ) -// NOTE: cadence.FunctionType isn't included in simpleTypeIDByType +// NOTE: cadence.FunctionType isn't included in SimpleTypeIDByType // because this function is used by both inline-type and type-value. // cadence.FunctionType needs to be handled differently when this // function is used by inline-type and type-value. @@ -278,7 +278,7 @@ func initSimpleTypeIDBiMap() (m *bimap.BiMap[cadence.PrimitiveType, SimpleType]) var simpleTypeIDBiMap *bimap.BiMap[cadence.PrimitiveType, SimpleType] = initSimpleTypeIDBiMap() -func simpleTypeIDByType(typ cadence.Type) (SimpleType, bool) { +func SimpleTypeIDByType(typ cadence.Type) (SimpleType, bool) { switch typ := typ.(type) { case cadence.BytesType: return SimpleTypeBytes, true @@ -289,7 +289,7 @@ func simpleTypeIDByType(typ cadence.Type) (SimpleType, bool) { return 0, false } -func typeBySimpleTypeID(simpleTypeID SimpleType) cadence.Type { +func TypeBySimpleTypeID(simpleTypeID SimpleType) cadence.Type { if simpleTypeID == SimpleTypeBytes { return cadence.TheBytesType } diff --git a/encoding/ccf/simpletype_test.go b/encoding/ccf/simpletype_test.go index 528df7d2c7..5435961171 100644 --- a/encoding/ccf/simpletype_test.go +++ b/encoding/ccf/simpletype_test.go @@ -16,7 +16,7 @@ * limitations under the License. */ -package ccf +package ccf_test import ( "testing" @@ -24,6 +24,7 @@ import ( "github.com/stretchr/testify/require" "github.com/onflow/cadence" + "github.com/onflow/cadence/encoding/ccf" "github.com/onflow/cadence/interpreter" "github.com/onflow/cadence/runtime" "github.com/onflow/cadence/sema" @@ -43,10 +44,10 @@ func TestTypeConversion(t *testing.T) { cadenceType := runtime.ExportType(semaType, map[sema.TypeID]cadence.Type{}) - simpleTypeID, ok := simpleTypeIDByType(cadenceType) + simpleTypeID, ok := ccf.SimpleTypeIDByType(cadenceType) require.True(t, ok) - ty2 := typeBySimpleTypeID(simpleTypeID) + ty2 := ccf.TypeBySimpleTypeID(simpleTypeID) require.Equal(t, cadence.PrimitiveType(ty), ty2) }) } diff --git a/runtime/ccf_test.go b/runtime/ccf_test.go new file mode 100644 index 0000000000..7d6387fceb --- /dev/null +++ b/runtime/ccf_test.go @@ -0,0 +1,107 @@ +/* + * Cadence - The resource-oriented smart contract programming language + * + * Copyright Flow Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package runtime_test + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/onflow/cadence" + "github.com/onflow/cadence/common" + . "github.com/onflow/cadence/runtime" + . "github.com/onflow/cadence/test_utils/runtime_utils" +) + +func TestRuntimeCCFEncodeStruct(t *testing.T) { + + t.Parallel() + + type testCase struct { + name string + value string + output []byte + } + + tests := []testCase{ + { + name: "String", + value: `"test"`, + output: []byte{0xd8, 0x82, 0x82, 0xd8, 0x89, 0x1, 0x64, 0x74, 0x65, 0x73, 0x74}, + }, + { + name: "Bool", + value: `true`, + output: []byte{0xd8, 0x82, 0x82, 0xd8, 0x89, 0x0, 0xf5}, + }, + { + name: "function", + value: `fun (): Int { return 1 }`, + output: []byte{0xd8, 0x82, 0x82, 0xd8, 0x89, 0x18, 0x33, 0x84, 0x80, 0x80, 0xd8, 0xb9, 0x4, 0x0}, + }, + } + + test := func(test testCase) { + t.Run(test.name, func(t *testing.T) { + + t.Parallel() + + runtime := NewTestRuntime() + + runtimeInterface := &TestRuntimeInterface{ + Storage: NewTestLedger(nil, nil), + } + + script := []byte(fmt.Sprintf( + ` + access(all) fun main(): [UInt8]? { + let value = %s + return CCF.encode(&value as &AnyStruct) + } + `, + test.value, + )) + + result, err := runtime.ExecuteScript( + Script{ + Source: script, + }, + Context{ + Interface: runtimeInterface, + Location: common.ScriptLocation{}, + UseVM: *compile, + }, + ) + require.NoError(t, err) + + assert.Equal(t, + cadence.NewOptional( + cadence.ByteSliceToByteArray(test.output), + ), + result, + ) + }) + } + + for _, testCase := range tests { + test(testCase) + } +} diff --git a/runtime/environment.go b/runtime/environment.go index fa0cf5343d..c64e01ef21 100644 --- a/runtime/environment.go +++ b/runtime/environment.go @@ -21,6 +21,7 @@ package runtime import ( "time" + "github.com/onflow/cadence" "github.com/onflow/cadence/activations" "github.com/onflow/cadence/common" @@ -109,6 +110,7 @@ var _ stdlib.BLSPoPVerifier = &InterpreterEnvironment{} var _ stdlib.BLSPublicKeyAggregator = &InterpreterEnvironment{} var _ stdlib.BLSSignatureAggregator = &InterpreterEnvironment{} var _ stdlib.Hasher = &InterpreterEnvironment{} +var _ stdlib.Exporter = &InterpreterEnvironment{} var _ ArgumentDecoder = &InterpreterEnvironment{} func NewInterpreterEnvironment(config Config) *InterpreterEnvironment { @@ -575,3 +577,13 @@ func (e *InterpreterEnvironment) getBaseActivation( func (e *InterpreterEnvironment) ProgramLog(message string) error { return e.Interface.ProgramLog(message) } + +func (*InterpreterEnvironment) ExportValue( + value interpreter.Value, + context interpreter.ValueExportContext, +) ( + cadence.Value, + error, +) { + return ExportValue(value, context) +} diff --git a/runtime/vm_environment.go b/runtime/vm_environment.go index f0d011542e..63162abb0c 100644 --- a/runtime/vm_environment.go +++ b/runtime/vm_environment.go @@ -21,6 +21,7 @@ package runtime import ( "fmt" + "github.com/onflow/cadence" "github.com/onflow/cadence/activations" "github.com/onflow/cadence/bbq" "github.com/onflow/cadence/bbq/commons" @@ -81,6 +82,7 @@ var _ stdlib.BLSPoPVerifier = &vmEnvironment{} var _ stdlib.BLSPublicKeyAggregator = &vmEnvironment{} var _ stdlib.BLSSignatureAggregator = &vmEnvironment{} var _ stdlib.Hasher = &vmEnvironment{} +var _ stdlib.Exporter = &vmEnvironment{} var _ ArgumentDecoder = &vmEnvironment{} func newVMEnvironment(config Config) *vmEnvironment { @@ -639,3 +641,10 @@ func (e *vmEnvironment) vmBuiltinGlobals( } return } + +func (*vmEnvironment) ExportValue( + value interpreter.Value, + context interpreter.ValueExportContext, +) (cadence.Value, error) { + return ExportValue(value, context) +} diff --git a/stdlib/builtin.go b/stdlib/builtin.go index 76d39f9ef5..9f92e20e2c 100644 --- a/stdlib/builtin.go +++ b/stdlib/builtin.go @@ -37,6 +37,7 @@ type StandardLibraryHandler interface { BLSPublicKeyAggregator BLSSignatureAggregator Hasher + Exporter } var DefaultStandardLibraryTypes = []StandardLibraryType{ @@ -50,6 +51,11 @@ var DefaultStandardLibraryTypes = []StandardLibraryType{ Name: RLPTypeName, Kind: common.DeclarationKindContract, }, + { + Type: CCFType, + Name: CCFTypeName, + Kind: common.DeclarationKindContract, + }, } func InterpreterDefaultStandardLibraryValues(handler StandardLibraryHandler) []StandardLibraryValue { @@ -68,6 +74,7 @@ func InterpreterDefaultStandardLibraryValues(handler StandardLibraryHandler) []S NewInterpreterHashAlgorithmConstructor(handler), RLPContract, NewBLSContract(nil, handler), + NewCCFContract(nil, handler), } } @@ -87,6 +94,7 @@ func VMDefaultStandardLibraryValues(handler StandardLibraryHandler) []StandardLi NewVMHashAlgorithmConstructor(handler), RLPContract, NewBLSContract(nil, handler), + NewCCFContract(nil, handler), } } @@ -138,6 +146,8 @@ func VMFunctions(handler StandardLibraryHandler) []VMFunction { NewVMBLSAggregatePublicKeysFunction(handler), NewVMBLSAggregateSignaturesFunction(handler), + NewVMCCFEncodeFunction(handler), + NewVMHashAlgorithmHashFunction(handler), NewVMHashAlgorithmHashWithTagFunction(handler), diff --git a/stdlib/ccf.cdc b/stdlib/ccf.cdc new file mode 100644 index 0000000000..9e0a4b6bfc --- /dev/null +++ b/stdlib/ccf.cdc @@ -0,0 +1,7 @@ +access(all) +contract CCF { + /// Encodes an encodable value to CCF. + /// Returns nil if the value cannot be encoded. + access(all) + view fun encode(_ input: &Any): [UInt8]? +} diff --git a/stdlib/ccf.gen.go b/stdlib/ccf.gen.go new file mode 100644 index 0000000000..a2e00bff85 --- /dev/null +++ b/stdlib/ccf.gen.go @@ -0,0 +1,82 @@ +// Code generated from ccf.cdc. DO NOT EDIT. +/* + * Cadence - The resource-oriented smart contract programming language + * + * Copyright Flow Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package stdlib + +import ( + "github.com/onflow/cadence/ast" + "github.com/onflow/cadence/common" + "github.com/onflow/cadence/sema" +) + +const CCFTypeEncodeFunctionName = "encode" + +var CCFTypeEncodeFunctionType = &sema.FunctionType{ + Purity: sema.FunctionPurityView, + Parameters: []sema.Parameter{ + { + Label: sema.ArgumentLabelNotRequired, + Identifier: "input", + TypeAnnotation: sema.NewTypeAnnotation(&sema.ReferenceType{ + Type: sema.AnyType, + Authorization: sema.UnauthorizedAccess, + }), + }, + }, + ReturnTypeAnnotation: sema.NewTypeAnnotation( + &sema.OptionalType{ + Type: &sema.VariableSizedType{ + Type: sema.UInt8Type, + }, + }, + ), +} + +const CCFTypeEncodeFunctionDocString = ` +Encodes an encodable value to CCF. +Returns nil if the value cannot be encoded. +` + +const CCFTypeName = "CCF" + +var CCFType = func() *sema.CompositeType { + var t = &sema.CompositeType{ + Identifier: CCFTypeName, + Kind: common.CompositeKindContract, + ImportableBuiltin: false, + HasComputedMembers: true, + } + + return t +}() + +func init() { + var members = []*sema.Member{ + sema.NewUnmeteredFunctionMember( + CCFType, + sema.PrimitiveAccess(ast.AccessAll), + CCFTypeEncodeFunctionName, + CCFTypeEncodeFunctionType, + CCFTypeEncodeFunctionDocString, + ), + } + + CCFType.Members = sema.MembersAsMap(members) + CCFType.Fields = sema.MembersFieldNames(members) +} diff --git a/stdlib/ccf.go b/stdlib/ccf.go new file mode 100644 index 0000000000..60d391a090 --- /dev/null +++ b/stdlib/ccf.go @@ -0,0 +1,133 @@ +/* + * Cadence - The resource-oriented smart contract programming language + * + * Copyright Flow Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package stdlib + +//go:generate go run ../sema/gen -p stdlib ccf.cdc ccf.gen.go + +import ( + "github.com/onflow/cadence" + "github.com/onflow/cadence/bbq/vm" + "github.com/onflow/cadence/common" + "github.com/onflow/cadence/encoding/ccf" + "github.com/onflow/cadence/errors" + "github.com/onflow/cadence/interpreter" +) + +type Exporter interface { + ExportValue( + value interpreter.Value, + context interpreter.ValueExportContext, + ) ( + cadence.Value, + error, + ) +} + +type CCFContractHandler interface { + Exporter +} + +func NativeCCFEncodeFunction(handler CCFContractHandler) interpreter.NativeFunction { + return func( + context interpreter.NativeFunctionContext, + _ interpreter.TypeArgumentsIterator, + _ interpreter.Value, + args []interpreter.Value, + ) interpreter.Value { + + referenceValue, ok := args[0].(interpreter.ReferenceValue) + if !ok { + panic(errors.NewUnreachableError()) + } + + referencedValue := referenceValue.ReferencedValue(context, true) + if referencedValue == nil { + return interpreter.Nil + } + + exportedValue, err := handler.ExportValue(*referencedValue, context) + if err != nil { + return interpreter.Nil + } + + encoded, err := ccf.Encode(exportedValue) + if err != nil { + return interpreter.Nil + } + + res := interpreter.ByteSliceToByteArrayValue(context, encoded) + + return interpreter.NewSomeValueNonCopying(context, res) + } +} + +func newInterpreterCCFEncodeFunction( + gauge common.MemoryGauge, + handler CCFContractHandler, +) *interpreter.HostFunctionValue { + // TODO: Should create a bound-host function here, but interpreter is not available at this point. + // However, this is not a problem for now, since underlying contract doesn't get moved. + return interpreter.NewStaticHostFunctionValueFromNativeFunction( + gauge, + BLSTypeAggregatePublicKeysFunctionType, + NativeCCFEncodeFunction(handler), + ) +} + +func NewVMCCFEncodeFunction(handler CCFContractHandler) VMFunction { + return VMFunction{ + BaseType: CCFType, + FunctionValue: vm.NewNativeFunctionValue( + CCFTypeEncodeFunctionName, + CCFTypeEncodeFunctionType, + NativeCCFEncodeFunction(handler), + ), + } +} + +var CCFTypeStaticType = interpreter.ConvertSemaToStaticType(nil, CCFType) + +func NewCCFContract( + gauge common.MemoryGauge, + handler CCFContractHandler, +) StandardLibraryValue { + + ccfContractFields := map[string]interpreter.Value{ + CCFTypeEncodeFunctionName: newInterpreterCCFEncodeFunction(gauge, handler), + } + + var ccfContractValue = interpreter.NewSimpleCompositeValue( + gauge, + CCFType.ID(), + CCFTypeStaticType, + nil, + ccfContractFields, + nil, + nil, + nil, + nil, + ) + + return StandardLibraryValue{ + Name: CCFTypeName, + Type: CCFType, + Value: ccfContractValue, + Kind: common.DeclarationKindContract, + } +} diff --git a/values.go b/values.go index 5f93f4ef2c..ec16f5f0f3 100644 --- a/values.go +++ b/values.go @@ -1590,6 +1590,19 @@ func (v Array) String() string { return format.Array(values) } +var ByteArrayType = NewVariableSizedArrayType(PrimitiveType(interpreter.PrimitiveStaticTypeUInt8)) + +// ByteSliceToByteArray converts a byte slice to a Cadence byte array of type [UInt8]. +func ByteSliceToByteArray(b []byte) Array { + values := make([]Value, len(b)) + + for i, v := range b { + values[i] = UInt8(v) + } + + return NewArray(values).WithType(ByteArrayType) +} + // Dictionary type Dictionary struct {