diff --git a/cmd/cmd.go b/cmd/cmd.go index 0172a68db5..057518313f 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -26,6 +26,7 @@ import ( "strings" "time" + "github.com/onflow/cadence" "github.com/onflow/cadence/activations" "github.com/onflow/cadence/ast" "github.com/onflow/cadence/common" @@ -433,6 +434,17 @@ func (h *StandardLibraryHandler) IsContractBeingAdded(common.AddressLocation) bo return false } +func (h *StandardLibraryHandler) ExportValue( + _ interpreter.Value, + _ *interpreter.Interpreter, + _ interpreter.LocationRange, +) ( + cadence.Value, + error, +) { + return nil, goerrors.New("exporting values is not supported in this environment") +} + func formatLocationRange(locationRange interpreter.LocationRange) string { var builder strings.Builder if locationRange.Location != nil { diff --git a/common/computationkind.go b/common/computationkind.go index 3ec0e44698..4ab4ebb312 100644 --- a/common/computationkind.go +++ b/common/computationkind.go @@ -142,4 +142,6 @@ const ( // RLP ComputationKindSTDLIBRLPDecodeString ComputationKindSTDLIBRLPDecodeList + // CCF + ComputationKindSTDLIBCCFEncode ) diff --git a/runtime/environment.go b/runtime/environment.go index 9aff5b329d..38ccb10285 100644 --- a/runtime/environment.go +++ b/runtime/environment.go @@ -146,6 +146,7 @@ var _ stdlib.BLSPoPVerifier = &interpreterEnvironment{} var _ stdlib.BLSPublicKeyAggregator = &interpreterEnvironment{} var _ stdlib.BLSSignatureAggregator = &interpreterEnvironment{} var _ stdlib.Hasher = &interpreterEnvironment{} +var _ stdlib.Exporter = &interpreterEnvironment{} var _ ArgumentDecoder = &interpreterEnvironment{} var _ common.MemoryGauge = &interpreterEnvironment{} @@ -1426,3 +1427,18 @@ func (e *interpreterEnvironment) newValidateAccountCapabilitiesPublishHandler() return ok, err } } + +func (e *interpreterEnvironment) ExportValue( + value interpreter.Value, + interpreter *interpreter.Interpreter, + locationRange interpreter.LocationRange, +) ( + cadence.Value, + error, +) { + return ExportValue( + value, + interpreter, + locationRange, + ) +} diff --git a/stdlib/builtin.go b/stdlib/builtin.go index 1a6d4e65c0..4195a2474c 100644 --- a/stdlib/builtin.go +++ b/stdlib/builtin.go @@ -36,6 +36,7 @@ type StandardLibraryHandler interface { BLSPublicKeyAggregator BLSSignatureAggregator Hasher + Exporter } func DefaultStandardLibraryValues(handler StandardLibraryHandler) []StandardLibraryValue { @@ -54,6 +55,7 @@ func DefaultStandardLibraryValues(handler StandardLibraryHandler) []StandardLibr NewPublicKeyConstructor(handler), NewBLSContract(nil, handler), NewHashAlgorithmConstructor(handler), + NewCCFContract(nil, 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..cd58b4115c --- /dev/null +++ b/stdlib/ccf.go @@ -0,0 +1,113 @@ +/* + * 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/common" + "github.com/onflow/cadence/encoding/ccf" + "github.com/onflow/cadence/errors" + "github.com/onflow/cadence/interpreter" +) + +type Exporter interface { + ExportValue( + value interpreter.Value, + interpreter *interpreter.Interpreter, + locationRange interpreter.LocationRange, + ) ( + cadence.Value, + error, + ) +} + +type CCFContractHandler interface { + Exporter +} + +// newCCFEncodeFunction creates a new host function that encodes a value using the CCF encoding format. +func newCCFEncodeFunction( + gauge common.MemoryGauge, + handler CCFContractHandler, +) *interpreter.HostFunctionValue { + return interpreter.NewStaticHostFunctionValue( + gauge, + CCFTypeEncodeFunctionType, + func(invocation interpreter.Invocation) interpreter.Value { + inter := invocation.Interpreter + locationRange := invocation.LocationRange + + referenceValue, ok := invocation.Arguments[0].(interpreter.ReferenceValue) + if !ok { + panic(errors.NewUnreachableError()) + } + + referencedValue := referenceValue.ReferencedValue(inter, locationRange, true) + if referencedValue == nil { + return interpreter.Nil + } + + exportedValue, err := handler.ExportValue(*referencedValue, inter, locationRange) + if err != nil { + return interpreter.Nil + } + + encoded, err := ccf.Encode(exportedValue) + if err != nil { + return interpreter.Nil + } + + res := interpreter.ByteSliceToByteArrayValue(inter, encoded) + + return interpreter.NewSomeValueNonCopying(inter, res) + }, + ) +} + +var CCFTypeStaticType = interpreter.ConvertSemaToStaticType(nil, CCFType) + +func NewCCFContract( + gauge common.MemoryGauge, + handler CCFContractHandler, +) StandardLibraryValue { + + ccfContractFields := map[string]interpreter.Value{ + CCFTypeEncodeFunctionName: newCCFEncodeFunction(gauge, handler), + } + + var ccfContractValue = interpreter.NewSimpleCompositeValue( + gauge, + CCFType.ID(), + CCFTypeStaticType, + nil, + ccfContractFields, + nil, + nil, + nil, + ) + + return StandardLibraryValue{ + Name: CCFTypeName, + Type: CCFType, + Value: ccfContractValue, + Kind: common.DeclarationKindContract, + } +} diff --git a/tests/ccf_test.go b/tests/ccf_test.go new file mode 100644 index 0000000000..96ef31e647 --- /dev/null +++ b/tests/ccf_test.go @@ -0,0 +1,106 @@ +/* + * 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 tests + +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/tests/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 := NewTestInterpreterRuntime() + + 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{}, + }, + ) + require.NoError(t, err) + + assert.Equal(t, + cadence.NewOptional( + cadence.ByteSliceToByteArray(test.output), + ), + result, + ) + }) + } + + for _, testCase := range tests { + test(testCase) + } +} diff --git a/values.go b/values.go index d3799c0a88..e12d6a27fb 100644 --- a/values.go +++ b/values.go @@ -1436,6 +1436,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 {