Skip to content

Commit

Permalink
Add wasm spec factory (#14458)
Browse files Browse the repository at this point in the history
* Add wasm spec factory

* Fix workflow tests

* Fix and test spec validation for non-YAML specs

* Revert use of logger in validate

* tidy and reorder inputs

* brotli the WASM so it's smaller for the sdk to pass around

* Fix so sum is always based on wasm not encoded value

* Fix typo and lint imports in wasm file spec factory

* Fix sha sum on wasm

* Fix go mod merge conflict :(

* Fix lint and remove local hack for go version...
  • Loading branch information
nolag authored Sep 25, 2024
1 parent c4fa565 commit 02982e3
Show file tree
Hide file tree
Showing 21 changed files with 272 additions and 180 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ race.*
golangci-lint-output.txt
/golangci-lint/
.covdata
core/services/job/testdata/wasm/testmodule.wasm

# DB state
./db/
Expand Down
2 changes: 2 additions & 0 deletions core/scripts/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ require (
github.com/NethermindEth/starknet.go v0.7.1-0.20240401080518-34a506f3cfdb // indirect
github.com/VictoriaMetrics/fastcache v1.12.1 // indirect
github.com/XSAM/otelsql v0.27.0 // indirect
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/armon/go-metrics v0.4.1 // indirect
github.com/avast/retry-go/v4 v4.6.0 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
Expand All @@ -70,6 +71,7 @@ require (
github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect
github.com/btcsuite/btcd/btcutil v1.1.3 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/bytecodealliance/wasmtime-go/v23 v23.0.0 // indirect
github.com/bytedance/sonic v1.10.1 // indirect
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
Expand Down
4 changes: 4 additions & 0 deletions core/scripts/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax
github.com/allegro/bigcache v1.2.1 h1:hg1sY1raCwic3Vnsvje6TT7/pnZba83LeFck5NrFKSc=
github.com/allegro/bigcache v1.2.1/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM=
github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129/go.mod h1:rFgpPQZYZ8vdbc+48xibu8ALc3yeyd64IhHS+PU6Yyg=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/appleboy/gofight/v2 v2.1.2 h1:VOy3jow4vIK8BRQJoC/I9muxyYlJ2yb9ht2hZoS3rf4=
github.com/appleboy/gofight/v2 v2.1.2/go.mod h1:frW+U1QZEdDgixycTj4CygQ48yLTUhplt43+Wczp3rw=
Expand Down Expand Up @@ -178,6 +180,8 @@ github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZ
github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/bytecodealliance/wasmtime-go/v23 v23.0.0 h1:NJvU4S8KEk1GnF6+FvlnzMD/8wXTj/mYJSG6Q4yu3Pw=
github.com/bytecodealliance/wasmtime-go/v23 v23.0.0/go.mod h1:5YIL+Ouiww2zpO7u+iZ1U1G5NvmwQYaXdmCZQGjQM0U=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
github.com/bytedance/sonic v1.10.1 h1:7a1wuFXL1cMy7a3f7/VFcEtriuXQnUBhtoVfOZiaysc=
Expand Down
19 changes: 10 additions & 9 deletions core/services/job/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ import (

commonassets "github.com/smartcontractkit/chainlink-common/pkg/assets"
"github.com/smartcontractkit/chainlink-common/pkg/types"
pkgworkflows "github.com/smartcontractkit/chainlink-common/pkg/workflows"

"github.com/smartcontractkit/chainlink/v2/core/logger"
"github.com/smartcontractkit/chainlink/v2/core/services/relay"

"github.com/smartcontractkit/chainlink/v2/core/bridges"
Expand Down Expand Up @@ -865,7 +865,8 @@ type WorkflowSpecType string

const (
YamlSpec WorkflowSpecType = "yaml"
DefaultSpecType = YamlSpec
WASMFile WorkflowSpecType = "wasm_file"
DefaultSpecType = ""
)

type WorkflowSpec struct {
Expand Down Expand Up @@ -894,12 +895,8 @@ const (

// Validate checks the workflow spec for correctness
func (w *WorkflowSpec) Validate(ctx context.Context) error {
s, err := pkgworkflows.ParseWorkflowSpecYaml(w.Workflow)
s, err := w.SDKSpec(ctx, logger.NullLogger)
if err != nil {
return fmt.Errorf("%w: failed to parse workflow spec %s: %w", ErrInvalidWorkflowYAMLSpec, w.Workflow, err)
}

if _, err = w.SDKSpec(ctx); err != nil {
return err
}

Expand All @@ -913,12 +910,16 @@ func (w *WorkflowSpec) Validate(ctx context.Context) error {
return nil
}

func (w *WorkflowSpec) SDKSpec(ctx context.Context) (sdk.WorkflowSpec, error) {
func (w *WorkflowSpec) SDKSpec(ctx context.Context, lggr logger.Logger) (sdk.WorkflowSpec, error) {
if w.sdkWorkflow != nil {
return *w.sdkWorkflow, nil
}

spec, cid, err := workflowSpecFactory.Spec(ctx, w.Workflow, []byte(w.Config), w.SpecType)
workflowSpecFactory, ok := workflowSpecFactories[w.SpecType]
if !ok {
return sdk.WorkflowSpec{}, fmt.Errorf("unknown spec type %s", w.SpecType)
}
spec, cid, err := workflowSpecFactory.Spec(ctx, lggr, w.Workflow, []byte(w.Config))
if err != nil {
return sdk.WorkflowSpec{}, err
}
Expand Down
37 changes: 30 additions & 7 deletions core/services/job/models_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package job
package job_test

import (
_ "embed"
"encoding/json"
"reflect"
"testing"
"time"
Expand All @@ -11,8 +12,10 @@ import (
"github.com/smartcontractkit/chainlink-common/pkg/codec"
"github.com/smartcontractkit/chainlink-common/pkg/types"
pkgworkflows "github.com/smartcontractkit/chainlink-common/pkg/workflows"
"github.com/smartcontractkit/chainlink-common/pkg/workflows/sdk"

"github.com/smartcontractkit/chainlink/v2/core/internal/testutils"
"github.com/smartcontractkit/chainlink/v2/core/services/job"
"github.com/smartcontractkit/chainlink/v2/core/services/relay"

"github.com/stretchr/testify/assert"
Expand All @@ -27,7 +30,7 @@ func TestOCR2OracleSpec_RelayIdentifier(t *testing.T) {
type fields struct {
Relay string
ChainID string
RelayConfig JSONConfig
RelayConfig job.JSONConfig
}
tests := []struct {
name string
Expand Down Expand Up @@ -71,7 +74,7 @@ func TestOCR2OracleSpec_RelayIdentifier(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

s := &OCR2OracleSpec{
s := &job.OCR2OracleSpec{
Relay: tt.fields.Relay,
ChainID: tt.fields.ChainID,
RelayConfig: tt.fields.RelayConfig,
Expand All @@ -96,7 +99,7 @@ var (
)

func TestOCR2OracleSpec(t *testing.T) {
val := OCR2OracleSpec{
val := job.OCR2OracleSpec{
Relay: relay.NetworkEVM,
PluginType: types.Median,
ContractID: "foo",
Expand Down Expand Up @@ -259,13 +262,13 @@ func TestOCR2OracleSpec(t *testing.T) {
})

t.Run("round-trip", func(t *testing.T) {
var gotVal OCR2OracleSpec
var gotVal job.OCR2OracleSpec
require.NoError(t, toml.Unmarshal([]byte(compact), &gotVal))
gotB, err := toml.Marshal(gotVal)
require.NoError(t, err)
require.Equal(t, compact, string(gotB))
t.Run("pretty", func(t *testing.T) {
var gotVal OCR2OracleSpec
var gotVal job.OCR2OracleSpec
require.NoError(t, toml.Unmarshal([]byte(pretty), &gotVal))
gotB, err := toml.Marshal(gotVal)
require.NoError(t, err)
Expand Down Expand Up @@ -321,7 +324,7 @@ func TestWorkflowSpec_Validate(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
w := &WorkflowSpec{
w := &job.WorkflowSpec{
Workflow: tt.fields.Workflow,
}
err := w.Validate(testutils.Context(t))
Expand All @@ -333,4 +336,24 @@ func TestWorkflowSpec_Validate(t *testing.T) {
}
})
}

t.Run("WASM can validate", func(t *testing.T) {
config, err := json.Marshal(sdk.NewWorkflowParams{
Owner: "owner",
Name: "name",
})
require.NoError(t, err)

w := &job.WorkflowSpec{
Workflow: createTestBinary(t),
SpecType: job.WASMFile,
Config: string(config),
}

err = w.Validate(testutils.Context(t))
require.NoError(t, err)
assert.Equal(t, "owner", w.WorkflowOwner)
assert.Equal(t, "name", w.WorkflowName)
require.NotEmpty(t, w.WorkflowID)
})
}
33 changes: 33 additions & 0 deletions core/services/job/testdata/wasm/test_workflow_spec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//go:build wasip1

package main

import (
"encoding/json"
"log"

"github.com/smartcontractkit/chainlink-common/pkg/workflows/wasm"

"github.com/smartcontractkit/chainlink-common/pkg/capabilities/cli/cmd/testdata/fixtures/capabilities/basictrigger"
"github.com/smartcontractkit/chainlink-common/pkg/workflows/sdk"
)

func BuildWorkflow(config []byte) *sdk.WorkflowSpecFactory {
params := sdk.NewWorkflowParams{}
if err := json.Unmarshal(config, &params); err != nil {
log.Fatal(err)
}

workflow := sdk.NewWorkflowSpecFactory(params)

triggerCfg := basictrigger.TriggerConfig{Name: "trigger", Number: 100}
_ = triggerCfg.New(workflow)

return workflow
}

func main() {
runner := wasm.NewRunner()
workflow := BuildWorkflow(runner.Config())
runner.Run(workflow)
}
90 changes: 90 additions & 0 deletions core/services/job/wasm_file_spec_factory.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package job

import (
"bytes"
"context"
"crypto/sha256"
"errors"
"fmt"
"io"
"os"
"path"
"strings"

"github.com/smartcontractkit/chainlink-common/pkg/workflows/sdk"
"github.com/smartcontractkit/chainlink-common/pkg/workflows/wasm/host"

"github.com/smartcontractkit/chainlink/v2/core/logger"

"github.com/andybalholm/brotli"
)

type WasmFileSpecFactory struct{}

func (w WasmFileSpecFactory) Spec(ctx context.Context, lggr logger.Logger, workflow string, config []byte) (sdk.WorkflowSpec, string, error) {
compressedBinary, sha, err := w.rawSpecAndSha(workflow, config)
if err != nil {
return sdk.WorkflowSpec{}, "", err
}

moduleConfig := &host.ModuleConfig{Logger: lggr}
spec, err := host.GetWorkflowSpec(moduleConfig, compressedBinary, config)
if err != nil {
return sdk.WorkflowSpec{}, "", err
} else if spec == nil {
return sdk.WorkflowSpec{}, "", errors.New("workflow spec not found when running wasm")
}

return *spec, sha, nil
}

// rawSpecAndSha returns the brotli compressed version of the raw wasm file, alongside the sha256 hash of the raw wasm file
func (w WasmFileSpecFactory) rawSpecAndSha(wf string, config []byte) ([]byte, string, error) {
read, err := os.ReadFile(wf)
if err != nil {
return nil, "", err
}

extension := strings.ToLower(path.Ext(wf))
switch extension {
case ".wasm", "":
return w.rawSpecAndShaFromWasm(read, config)
case ".br":
return w.rawSpecAndShaFromBrotli(read, config)
default:
return nil, "", fmt.Errorf("unsupported file type %s", extension)
}
}

func (w WasmFileSpecFactory) rawSpecAndShaFromBrotli(wasm, config []byte) ([]byte, string, error) {
brr := brotli.NewReader(bytes.NewReader(wasm))
rawWasm, err := io.ReadAll(brr)
if err != nil {
return nil, "", err
}

return wasm, w.sha(rawWasm, config), nil
}

func (w WasmFileSpecFactory) rawSpecAndShaFromWasm(wasm, config []byte) ([]byte, string, error) {
var b bytes.Buffer
bwr := brotli.NewWriter(&b)
if _, err := bwr.Write(wasm); err != nil {
return nil, "", err
}

if err := bwr.Close(); err != nil {
return nil, "", err
}

return b.Bytes(), w.sha(wasm, config), nil
}

func (w WasmFileSpecFactory) sha(wasm, config []byte) string {
sum := sha256.New()
sum.Write(wasm)
sum.Write(config)
return fmt.Sprintf("%x", sum.Sum(nil))
}

var _ WorkflowSpecFactory = (*WasmFileSpecFactory)(nil)
56 changes: 56 additions & 0 deletions core/services/job/wasm_file_spec_factory_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package job_test

import (
"crypto/sha256"
"encoding/json"
"fmt"
"os"
"os/exec"
"testing"

"github.com/stretchr/testify/require"

"github.com/smartcontractkit/chainlink-common/pkg/workflows/sdk"
"github.com/smartcontractkit/chainlink-common/pkg/workflows/wasm/host"

"github.com/smartcontractkit/chainlink/v2/core/internal/testutils"
"github.com/smartcontractkit/chainlink/v2/core/logger"
"github.com/smartcontractkit/chainlink/v2/core/services/job"
)

func TestWasmFileSpecFactory(t *testing.T) {
binaryLocation := createTestBinary(t)
config, err := json.Marshal(sdk.NewWorkflowParams{
Owner: "owner",
Name: "name",
})
require.NoError(t, err)

factory := job.WasmFileSpecFactory{}
actual, actualSha, err := factory.Spec(testutils.Context(t), logger.NullLogger, binaryLocation, config)
require.NoError(t, err)

rawBinary, err := os.ReadFile(binaryLocation)
require.NoError(t, err)
expected, err := host.GetWorkflowSpec(&host.ModuleConfig{Logger: logger.NullLogger, IsUncompressed: true}, rawBinary, config)
require.NoError(t, err)

expectedSha := sha256.New()
expectedSha.Write(rawBinary)
expectedSha.Write(config)
require.Equal(t, fmt.Sprintf("%x", expectedSha.Sum(nil)), actualSha)

require.Equal(t, *expected, actual)
}

func createTestBinary(t *testing.T) string {
const testBinaryLocation = "testdata/wasm/testmodule.wasm"

cmd := exec.Command("go", "build", "-o", testBinaryLocation, "github.com/smartcontractkit/chainlink/v2/core/services/job/testdata/wasm")
cmd.Env = append(os.Environ(), "GOOS=wasip1", "GOARCH=wasm")

output, err := cmd.CombinedOutput()
require.NoError(t, err, string(output))

return testBinaryLocation
}
Loading

0 comments on commit 02982e3

Please sign in to comment.