From 76dbe192822c7e9f289c98e33ebb6693a07046a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Fri, 31 May 2024 17:56:57 +0900 Subject: [PATCH] [KS-182] keystone: Refactor write_capability + add ChainWriter (#13259) * evm: Add a stub chainwriter impl * evm: Fix config parameter * evm: Change the chainwriter receiver name * evm: Remove the chain writer interface to reference chainlink-common * evm: Update common dep, and fix signature * go.sum: Run gomodtidy * .changeset: Add a changeset * evm: Pseudo-implement the submit transaction method on chainwriter * evm: Add txm dependency to chainwriter * evm: Use the txm param properly * Update code to use the new interface * nix: use monthly foundry branch that's persistent * capabilities: Add config validation to write_target * capabilities: Pass context into InitializeWrite * minor: Resolve some inapplicable TODOs * capabilities: Refactor write target by extracting commmon bits * Refactor WriteTarget to use ChainWriter * capabilities: Move evm specific code inside the relayer * Chainwriter tests (#13360) * Started relayer evm tests * Evm Relay tests * Added generic tests and additional evm tests for WriteTarget * added changeset * Update real-tools-tap.md * lint * small fix * Update chainlink-common to include the new interface * write_target: Fix tests * Move target capability init inside evm.NewRelayer() * go mod tidy * Address lints * scripts: go mod tidy * integration-tests: go mod tidy * evm: Only initialize write target if config actually present * more tidy, fix last lint * evm: Move send strategy to be config driven * evm: Add some todos back * .changeset: Add a changeset * write_target: Mock out Config before NewRelayer is called * Regenerate mocks with right version * fix evm tests * goimports * fix evm tests --------- Co-authored-by: Nick Corin Co-authored-by: Silas Lenihan <32529249+silaslenihan@users.noreply.github.com> --- .changeset/large-plants-count.md | 5 + .changeset/real-tools-tap.md | 5 + .changeset/tricky-flowers-exist.md | 5 + .../targets/mocks/chain_reader.go | 189 ++++++++++++++++ .../targets/mocks/chain_writer.go | 109 +++++++++ core/capabilities/targets/write_target.go | 158 +++++-------- .../capabilities/targets/write_target_test.go | 203 +++++++++-------- core/internal/cltest/factories.go | 7 +- core/services/chainlink/application.go | 1 - core/services/relay/evm/chain_writer.go | 133 +++++++++++ core/services/relay/evm/evm.go | 19 +- core/services/relay/evm/types/types.go | 21 ++ core/services/relay/evm/write_target.go | 75 ++++++ core/services/relay/evm/write_target_test.go | 213 ++++++++++++++++++ core/services/workflows/delegate.go | 21 +- flake.lock | 7 +- flake.nix | 2 +- 17 files changed, 949 insertions(+), 224 deletions(-) create mode 100644 .changeset/large-plants-count.md create mode 100644 .changeset/real-tools-tap.md create mode 100644 .changeset/tricky-flowers-exist.md create mode 100644 core/capabilities/targets/mocks/chain_reader.go create mode 100644 core/capabilities/targets/mocks/chain_writer.go create mode 100644 core/services/relay/evm/chain_writer.go create mode 100644 core/services/relay/evm/write_target.go create mode 100644 core/services/relay/evm/write_target_test.go diff --git a/.changeset/large-plants-count.md b/.changeset/large-plants-count.md new file mode 100644 index 00000000000..e300a33d3e4 --- /dev/null +++ b/.changeset/large-plants-count.md @@ -0,0 +1,5 @@ +--- +"chainlink": minor +--- + +#internal Added a configuration option to chain writer to set the tx send strategy. diff --git a/.changeset/real-tools-tap.md b/.changeset/real-tools-tap.md new file mode 100644 index 00000000000..37a3cf5e581 --- /dev/null +++ b/.changeset/real-tools-tap.md @@ -0,0 +1,5 @@ +--- +"chainlink": minor +--- + +#internal added tests for Chainwriter diff --git a/.changeset/tricky-flowers-exist.md b/.changeset/tricky-flowers-exist.md new file mode 100644 index 00000000000..0b45b116f54 --- /dev/null +++ b/.changeset/tricky-flowers-exist.md @@ -0,0 +1,5 @@ +--- +"chainlink": minor +--- + +#added A ChainWriter implementation in the EVM relay. diff --git a/core/capabilities/targets/mocks/chain_reader.go b/core/capabilities/targets/mocks/chain_reader.go new file mode 100644 index 00000000000..14748c16bb8 --- /dev/null +++ b/core/capabilities/targets/mocks/chain_reader.go @@ -0,0 +1,189 @@ +// Code generated by mockery v2.42.2. DO NOT EDIT. + +package mocks + +import ( + context "context" + + query "github.com/smartcontractkit/chainlink-common/pkg/types/query" + mock "github.com/stretchr/testify/mock" + + types "github.com/smartcontractkit/chainlink-common/pkg/types" +) + +// ChainReader is an autogenerated mock type for the ChainReader type +type ChainReader struct { + mock.Mock +} + +// Bind provides a mock function with given fields: ctx, bindings +func (_m *ChainReader) Bind(ctx context.Context, bindings []types.BoundContract) error { + ret := _m.Called(ctx, bindings) + + if len(ret) == 0 { + panic("no return value specified for Bind") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, []types.BoundContract) error); ok { + r0 = rf(ctx, bindings) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Close provides a mock function with given fields: +func (_m *ChainReader) Close() error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Close") + } + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// GetLatestValue provides a mock function with given fields: ctx, contractName, method, params, returnVal +func (_m *ChainReader) GetLatestValue(ctx context.Context, contractName string, method string, params interface{}, returnVal interface{}) error { + ret := _m.Called(ctx, contractName, method, params, returnVal) + + if len(ret) == 0 { + panic("no return value specified for GetLatestValue") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, interface{}, interface{}) error); ok { + r0 = rf(ctx, contractName, method, params, returnVal) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// HealthReport provides a mock function with given fields: +func (_m *ChainReader) HealthReport() map[string]error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for HealthReport") + } + + var r0 map[string]error + if rf, ok := ret.Get(0).(func() map[string]error); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]error) + } + } + + return r0 +} + +// Name provides a mock function with given fields: +func (_m *ChainReader) Name() string { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Name") + } + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// QueryKey provides a mock function with given fields: ctx, contractName, filter, limitAndSort, sequenceDataType +func (_m *ChainReader) QueryKey(ctx context.Context, contractName string, filter query.KeyFilter, limitAndSort query.LimitAndSort, sequenceDataType interface{}) ([]types.Sequence, error) { + ret := _m.Called(ctx, contractName, filter, limitAndSort, sequenceDataType) + + if len(ret) == 0 { + panic("no return value specified for QueryKey") + } + + var r0 []types.Sequence + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, query.KeyFilter, query.LimitAndSort, interface{}) ([]types.Sequence, error)); ok { + return rf(ctx, contractName, filter, limitAndSort, sequenceDataType) + } + if rf, ok := ret.Get(0).(func(context.Context, string, query.KeyFilter, query.LimitAndSort, interface{}) []types.Sequence); ok { + r0 = rf(ctx, contractName, filter, limitAndSort, sequenceDataType) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]types.Sequence) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, query.KeyFilter, query.LimitAndSort, interface{}) error); ok { + r1 = rf(ctx, contractName, filter, limitAndSort, sequenceDataType) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Ready provides a mock function with given fields: +func (_m *ChainReader) Ready() error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Ready") + } + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Start provides a mock function with given fields: _a0 +func (_m *ChainReader) Start(_a0 context.Context) error { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for Start") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewChainReader creates a new instance of ChainReader. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewChainReader(t interface { + mock.TestingT + Cleanup(func()) +}) *ChainReader { + mock := &ChainReader{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/core/capabilities/targets/mocks/chain_writer.go b/core/capabilities/targets/mocks/chain_writer.go new file mode 100644 index 00000000000..3d8efae8451 --- /dev/null +++ b/core/capabilities/targets/mocks/chain_writer.go @@ -0,0 +1,109 @@ +// Code generated by mockery v2.42.2. DO NOT EDIT. + +package mocks + +import ( + context "context" + big "math/big" + + mock "github.com/stretchr/testify/mock" + + types "github.com/smartcontractkit/chainlink-common/pkg/types" + + uuid "github.com/google/uuid" +) + +// ChainWriter is an autogenerated mock type for the ChainWriter type +type ChainWriter struct { + mock.Mock +} + +// GetFeeComponents provides a mock function with given fields: ctx +func (_m *ChainWriter) GetFeeComponents(ctx context.Context) (*types.ChainFeeComponents, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for GetFeeComponents") + } + + var r0 *types.ChainFeeComponents + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (*types.ChainFeeComponents, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) *types.ChainFeeComponents); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.ChainFeeComponents) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetTransactionStatus provides a mock function with given fields: ctx, transactionID +func (_m *ChainWriter) GetTransactionStatus(ctx context.Context, transactionID uuid.UUID) (types.TransactionStatus, error) { + ret := _m.Called(ctx, transactionID) + + if len(ret) == 0 { + panic("no return value specified for GetTransactionStatus") + } + + var r0 types.TransactionStatus + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) (types.TransactionStatus, error)); ok { + return rf(ctx, transactionID) + } + if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) types.TransactionStatus); ok { + r0 = rf(ctx, transactionID) + } else { + r0 = ret.Get(0).(types.TransactionStatus) + } + + if rf, ok := ret.Get(1).(func(context.Context, uuid.UUID) error); ok { + r1 = rf(ctx, transactionID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SubmitTransaction provides a mock function with given fields: ctx, contractName, method, args, transactionID, toAddress, meta, value +func (_m *ChainWriter) SubmitTransaction(ctx context.Context, contractName string, method string, args []interface{}, transactionID uuid.UUID, toAddress string, meta *types.TxMeta, value big.Int) error { + ret := _m.Called(ctx, contractName, method, args, transactionID, toAddress, meta, value) + + if len(ret) == 0 { + panic("no return value specified for SubmitTransaction") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, []interface{}, uuid.UUID, string, *types.TxMeta, big.Int) error); ok { + r0 = rf(ctx, contractName, method, args, transactionID, toAddress, meta, value) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewChainWriter creates a new instance of ChainWriter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewChainWriter(t interface { + mock.TestingT + Cleanup(func()) +}) *ChainWriter { + mock := &ChainWriter{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/core/capabilities/targets/write_target.go b/core/capabilities/targets/write_target.go index 23135d11dfe..74f4dac4f27 100644 --- a/core/capabilities/targets/write_target.go +++ b/core/capabilities/targets/write_target.go @@ -3,58 +3,32 @@ package targets import ( "context" "fmt" + "math/big" "github.com/ethereum/go-ethereum/common" - "github.com/mitchellh/mapstructure" - - chainselectors "github.com/smartcontractkit/chain-selectors" + "github.com/google/uuid" "github.com/smartcontractkit/chainlink-common/pkg/capabilities" - "github.com/smartcontractkit/chainlink-common/pkg/types/core" + commontypes "github.com/smartcontractkit/chainlink-common/pkg/types" "github.com/smartcontractkit/chainlink-common/pkg/values" - txmgrcommon "github.com/smartcontractkit/chainlink/v2/common/txmgr" - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/txmgr" - evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" - "github.com/smartcontractkit/chainlink/v2/core/chains/legacyevm" - "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/forwarder" "github.com/smartcontractkit/chainlink/v2/core/logger" ) -var forwardABI = evmtypes.MustGetABI(forwarder.KeystoneForwarderMetaData.ABI) - -func InitializeWrite(registry core.CapabilitiesRegistry, legacyEVMChains legacyevm.LegacyChainContainer, lggr logger.Logger) error { - for _, chain := range legacyEVMChains.Slice() { - capability := NewEvmWrite(chain, lggr) - if err := registry.Add(context.TODO(), capability); err != nil { - return err - } - } - return nil -} - var ( - _ capabilities.ActionCapability = &EvmWrite{} + _ capabilities.ActionCapability = &WriteTarget{} ) -const ( - defaultGasLimit = 200000 - signedReportField = "signed_report" -) +const signedReportField = "signed_report" -type EvmWrite struct { - chain legacyevm.Chain +type WriteTarget struct { + cr commontypes.ContractReader + cw commontypes.ChainWriter + forwarderAddress string capabilities.CapabilityInfo lggr logger.Logger } -func NewEvmWrite(chain legacyevm.Chain, lggr logger.Logger) *EvmWrite { - // generate ID based on chain selector - name := fmt.Sprintf("write_%v", chain.ID()) - chainName, err := chainselectors.NameFromChainId(chain.ID().Uint64()) - if err == nil { - name = fmt.Sprintf("write_%v", chainName) - } - +func NewWriteTarget(lggr logger.Logger, name string, cr commontypes.ContractReader, cw commontypes.ChainWriter, forwarderAddress string) *WriteTarget { info := capabilities.MustNewCapabilityInfo( name, capabilities.CapabilityTypeTarget, @@ -63,37 +37,42 @@ func NewEvmWrite(chain legacyevm.Chain, lggr logger.Logger) *EvmWrite { nil, ) - return &EvmWrite{ - chain, + logger := lggr.Named("WriteTarget") + + return &WriteTarget{ + cr, + cw, + forwarderAddress, info, - lggr.Named("EvmWrite"), + logger, } } type EvmConfig struct { - ChainID uint Address string } -// TODO: enforce required key presence - -func parseConfig(rawConfig *values.Map) (EvmConfig, error) { - var config EvmConfig - configAny, err := rawConfig.Unwrap() - if err != nil { +func parseConfig(rawConfig *values.Map) (config EvmConfig, err error) { + if err := rawConfig.UnwrapTo(&config); err != nil { return config, err } - err = mapstructure.Decode(configAny, &config) - return config, err + if !common.IsHexAddress(config.Address) { + return config, fmt.Errorf("'%v' is not a valid address", config.Address) + } + return config, nil } -func (cap *EvmWrite) Execute(ctx context.Context, request capabilities.CapabilityRequest) (<-chan capabilities.CapabilityResponse, error) { - cap.lggr.Debugw("Execute", "request", request) - // TODO: idempotency - - txm := cap.chain.TxManager() +func success() <-chan capabilities.CapabilityResponse { + callback := make(chan capabilities.CapabilityResponse) + go func() { + callback <- capabilities.CapabilityResponse{} + close(callback) + }() + return callback +} - config := cap.chain.Config().EVM().ChainWriter() +func (cap *WriteTarget) Execute(ctx context.Context, request capabilities.CapabilityRequest) (<-chan capabilities.CapabilityResponse, error) { + cap.lggr.Debugw("Execute", "request", request) reqConfig, err := parseConfig(request.Config) if err != nil { @@ -117,68 +96,47 @@ func (cap *EvmWrite) Execute(ctx context.Context, request capabilities.Capabilit if inputs.Report == nil { // We received any empty report -- this means we should skip transmission. cap.lggr.Debugw("Skipping empty report", "request", request) - callback := make(chan capabilities.CapabilityResponse) - go func() { - // TODO: cast tx.Error to Err (or Value to Value?) - callback <- capabilities.CapabilityResponse{ - Value: nil, - Err: nil, - } - close(callback) - }() - return callback, nil + return success(), nil } cap.lggr.Debugw("WriteTarget non-empty report - attempting to push to txmgr", "request", request, "reportLen", len(inputs.Report), "reportContextLen", len(inputs.Context), "nSignatures", len(inputs.Signatures)) // TODO: validate encoded report is prefixed with workflowID and executionID that match the request meta - // construct forwarder payload - calldata, err := forwardABI.Pack("report", common.HexToAddress(reqConfig.Address), inputs.Report, inputs.Context, inputs.Signatures) - if err != nil { + // Check whether value was already transmitted on chain + queryInputs := struct { + Receiver string + WorkflowExecutionID []byte + }{ + Receiver: reqConfig.Address, + WorkflowExecutionID: []byte(request.Metadata.WorkflowExecutionID), + } + var transmitter common.Address + if err = cap.cr.GetLatestValue(ctx, "forwarder", "getTransmitter", queryInputs, &transmitter); err != nil { return nil, err } - - txMeta := &txmgr.TxMeta{ - // FwdrDestAddress could also be set for better logging but it's used for various purposes around Operator Forwarders - WorkflowExecutionID: &request.Metadata.WorkflowExecutionID, + if transmitter != common.HexToAddress("0x0") { + // report already transmitted, early return + return success(), nil } - strategy := txmgrcommon.NewSendEveryStrategy() - checker := txmgr.TransmitCheckerSpec{ - CheckerType: txmgr.TransmitCheckerTypeSimulate, - } - req := txmgr.TxRequest{ - FromAddress: config.FromAddress().Address(), - ToAddress: config.ForwarderAddress().Address(), - EncodedPayload: calldata, - FeeLimit: uint64(defaultGasLimit), - Meta: txMeta, - Strategy: strategy, - Checker: checker, - // SignalCallback: true, TODO: add code that checks if a workflow id is present, if so, route callback to chainwriter rather than pipeline - } - tx, err := txm.CreateTransaction(ctx, req) + txID, err := uuid.NewUUID() // TODO(archseer): it seems odd that CW expects us to generate an ID, rather than return one if err != nil { return nil, err } - cap.lggr.Debugw("Transaction submitted", "request", request, "transaction", tx) - - callback := make(chan capabilities.CapabilityResponse) - go func() { - // TODO: cast tx.Error to Err (or Value to Value?) - callback <- capabilities.CapabilityResponse{ - Value: nil, - Err: nil, - } - close(callback) - }() - return callback, nil + args := []any{common.HexToAddress(reqConfig.Address), inputs.Report, inputs.Context, inputs.Signatures} + meta := commontypes.TxMeta{WorkflowExecutionID: &request.Metadata.WorkflowExecutionID} + value := big.NewInt(0) + if err := cap.cw.SubmitTransaction(ctx, "forwarder", "report", args, txID, cap.forwarderAddress, &meta, *value); err != nil { + return nil, err + } + cap.lggr.Debugw("Transaction submitted", "request", request, "transaction", txID) + return success(), nil } -func (cap *EvmWrite) RegisterToWorkflow(ctx context.Context, request capabilities.RegisterToWorkflowRequest) error { +func (cap *WriteTarget) RegisterToWorkflow(ctx context.Context, request capabilities.RegisterToWorkflowRequest) error { return nil } -func (cap *EvmWrite) UnregisterFromWorkflow(ctx context.Context, request capabilities.UnregisterFromWorkflowRequest) error { +func (cap *WriteTarget) UnregisterFromWorkflow(ctx context.Context, request capabilities.UnregisterFromWorkflowRequest) error { return nil } diff --git a/core/capabilities/targets/write_target_test.go b/core/capabilities/targets/write_target_test.go index f4cab88a739..6964a9617ea 100644 --- a/core/capabilities/targets/write_target_test.go +++ b/core/capabilities/targets/write_target_test.go @@ -1,57 +1,45 @@ package targets_test import ( - "math/big" + "context" + "errors" "testing" + "github.com/ethereum/go-ethereum/common" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" "github.com/smartcontractkit/chainlink-common/pkg/values" "github.com/smartcontractkit/chainlink/v2/core/capabilities/targets" - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/txmgr" - txmmocks "github.com/smartcontractkit/chainlink/v2/core/chains/evm/txmgr/mocks" - "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" - evmmocks "github.com/smartcontractkit/chainlink/v2/core/chains/legacyevm/mocks" - "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/forwarder" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/targets/mocks" "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" - "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/configtest" - "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/evmtest" "github.com/smartcontractkit/chainlink/v2/core/logger" - "github.com/smartcontractkit/chainlink/v2/core/services/chainlink" - - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" ) -var forwardABI = types.MustGetABI(forwarder.KeystoneForwarderMetaData.ABI) +//go:generate mockery --quiet --name ChainWriter --srcpkg=github.com/smartcontractkit/chainlink-common/pkg/types --output ./mocks/ --case=underscore +//go:generate mockery --quiet --name ChainReader --srcpkg=github.com/smartcontractkit/chainlink-common/pkg/types --output ./mocks/ --case=underscore -func TestEvmWrite(t *testing.T) { - chain := evmmocks.NewChain(t) +func TestWriteTarget(t *testing.T) { + lggr := logger.TestLogger(t) + ctx := context.Background() - txManager := txmmocks.NewMockEvmTxManager(t) - chain.On("ID").Return(big.NewInt(11155111)) - chain.On("TxManager").Return(txManager) - - cfg := configtest.NewGeneralConfig(t, func(c *chainlink.Config, s *chainlink.Secrets) { - a := testutils.NewAddress() - addr, err := types.NewEIP55Address(a.Hex()) - require.NoError(t, err) - c.EVM[0].ChainWriter.FromAddress = &addr + cw := mocks.NewChainWriter(t) + cr := mocks.NewChainReader(t) - forwarderA := testutils.NewAddress() - forwarderAddr, err := types.NewEIP55Address(forwarderA.Hex()) - require.NoError(t, err) - c.EVM[0].ChainWriter.ForwarderAddress = &forwarderAddr - }) - evmcfg := evmtest.NewChainScopedConfig(t, cfg) - chain.On("Config").Return(evmcfg) + forwarderA := testutils.NewAddress() + forwarderAddr := forwarderA.Hex() - capability := targets.NewEvmWrite(chain, logger.TestLogger(t)) - ctx := testutils.Context(t) + writeTarget := targets.NewWriteTarget(lggr, "Test", cr, cw, forwarderAddr) + require.NotNil(t, writeTarget) - config, err := values.NewMap(map[string]any{}) + config, err := values.NewMap(map[string]any{ + "Address": forwarderAddr, + }) require.NoError(t, err) - inputs, err := values.NewMap(map[string]any{ + validInputs, err := values.NewMap(map[string]any{ "signed_report": map[string]any{ "report": []byte{1, 2, 3}, "signatures": [][]byte{}, @@ -59,76 +47,93 @@ func TestEvmWrite(t *testing.T) { }) require.NoError(t, err) - req := capabilities.CapabilityRequest{ - Metadata: capabilities.RequestMetadata{ - WorkflowID: "hello", - }, - Config: config, - Inputs: inputs, - } - - txManager.On("CreateTransaction", mock.Anything, mock.Anything).Return(txmgr.Tx{}, nil).Run(func(args mock.Arguments) { - req := args.Get(1).(txmgr.TxRequest) - payload := make(map[string]any) - method := forwardABI.Methods["report"] - err = method.Inputs.UnpackIntoMap(payload, req.EncodedPayload[4:]) - require.NoError(t, err) - require.Equal(t, []byte{0x1, 0x2, 0x3}, payload["rawReport"]) - require.Equal(t, [][]byte{}, payload["signatures"]) + cr.On("GetLatestValue", mock.Anything, "forwarder", "getTransmitter", mock.Anything, mock.Anything).Return(nil).Run(func(args mock.Arguments) { + transmitter := args.Get(4).(*common.Address) + *transmitter = common.HexToAddress("0x0") + }).Once() + + cw.On("SubmitTransaction", mock.Anything, "forwarder", "report", mock.Anything, mock.Anything, forwarderAddr, mock.Anything, mock.Anything).Return(nil).Once() + + t.Run("succeeds with valid report", func(t *testing.T) { + req := capabilities.CapabilityRequest{ + Metadata: capabilities.RequestMetadata{ + WorkflowID: "test-id", + }, + Config: config, + Inputs: validInputs, + } + + ch, err2 := writeTarget.Execute(ctx, req) + require.NoError(t, err2) + response := <-ch + require.NotNil(t, response) }) - ch, err := capability.Execute(ctx, req) - require.NoError(t, err) - - response := <-ch - require.Nil(t, response.Err) -} - -func TestEvmWrite_EmptyReport(t *testing.T) { - chain := evmmocks.NewChain(t) - - txManager := txmmocks.NewMockEvmTxManager(t) - chain.On("ID").Return(big.NewInt(11155111)) - chain.On("TxManager").Return(txManager) - - cfg := configtest.NewGeneralConfig(t, func(c *chainlink.Config, s *chainlink.Secrets) { - a := testutils.NewAddress() - addr, err := types.NewEIP55Address(a.Hex()) - require.NoError(t, err) - c.EVM[0].ChainWriter.FromAddress = &addr - - forwarderA := testutils.NewAddress() - forwarderAddr, err := types.NewEIP55Address(forwarderA.Hex()) - require.NoError(t, err) - c.EVM[0].ChainWriter.ForwarderAddress = &forwarderAddr + t.Run("succeeds with empty report", func(t *testing.T) { + emptyInputs, err2 := values.NewMap(map[string]any{ + "signed_report": map[string]any{ + "report": nil, + }, + "signatures": [][]byte{}, + }) + + require.NoError(t, err2) + req := capabilities.CapabilityRequest{ + Metadata: capabilities.RequestMetadata{ + WorkflowExecutionID: "test-id", + }, + Config: config, + Inputs: emptyInputs, + } + + ch, err2 := writeTarget.Execute(ctx, req) + require.NoError(t, err2) + response := <-ch + require.Nil(t, response.Value) }) - evmcfg := evmtest.NewChainScopedConfig(t, cfg) - chain.On("Config").Return(evmcfg) - - capability := targets.NewEvmWrite(chain, logger.TestLogger(t)) - ctx := testutils.Context(t) - - config, err := values.NewMap(map[string]any{}) - require.NoError(t, err) - inputs, err := values.NewMap(map[string]any{ - "signed_report": map[string]any{ - "report": nil, - }, + t.Run("fails when ChainReader's GetLatestValue returns error", func(t *testing.T) { + req := capabilities.CapabilityRequest{ + Metadata: capabilities.RequestMetadata{ + WorkflowID: "test-id", + }, + Config: config, + Inputs: validInputs, + } + cr.On("GetLatestValue", mock.Anything, "forwarder", "getTransmitter", mock.Anything, mock.Anything).Return(errors.New("reader error")) + + _, err = writeTarget.Execute(ctx, req) + require.Error(t, err) }) - require.NoError(t, err) - req := capabilities.CapabilityRequest{ - Metadata: capabilities.RequestMetadata{ - WorkflowID: "hello", - }, - Config: config, - Inputs: inputs, - } + t.Run("fails when ChainWriter's SubmitTransaction returns error", func(t *testing.T) { + req := capabilities.CapabilityRequest{ + Metadata: capabilities.RequestMetadata{ + WorkflowID: "test-id", + }, + Config: config, + Inputs: validInputs, + } + cw.On("SubmitTransaction", mock.Anything, "forwarder", "report", mock.Anything, mock.Anything, forwarderAddr, mock.Anything, mock.Anything).Return(errors.New("writer error")) + + _, err = writeTarget.Execute(ctx, req) + require.Error(t, err) + }) - ch, err := capability.Execute(ctx, req) - require.NoError(t, err) + t.Run("fails with invalid config", func(t *testing.T) { + invalidConfig, err := values.NewMap(map[string]any{ + "Address": "invalid-address", + }) + require.NoError(t, err) - response := <-ch - require.Nil(t, response.Err) + req := capabilities.CapabilityRequest{ + Metadata: capabilities.RequestMetadata{ + WorkflowID: "test-id", + }, + Config: invalidConfig, + Inputs: validInputs, + } + _, err = writeTarget.Execute(ctx, req) + require.Error(t, err) + }) } diff --git a/core/internal/cltest/factories.go b/core/internal/cltest/factories.go index cd2fa9d9f63..c488dca94a9 100644 --- a/core/internal/cltest/factories.go +++ b/core/internal/cltest/factories.go @@ -262,14 +262,15 @@ type RandomKey struct { func (r RandomKey) MustInsert(t testing.TB, keystore keystore.Eth) (ethkey.KeyV2, common.Address) { ctx := testutils.Context(t) - if r.chainIDs == nil { - r.chainIDs = []ubig.Big{*ubig.New(&FixtureChainID)} + chainIDs := r.chainIDs + if chainIDs == nil { + chainIDs = []ubig.Big{*ubig.New(&FixtureChainID)} } key := MustGenerateRandomKey(t) keystore.XXXTestingOnlyAdd(ctx, key) - for _, cid := range r.chainIDs { + for _, cid := range chainIDs { require.NoError(t, keystore.Add(ctx, key.Address, cid.ToInt())) require.NoError(t, keystore.Enable(ctx, key.Address, cid.ToInt())) if r.Disabled { diff --git a/core/services/chainlink/application.go b/core/services/chainlink/application.go index f847a032d81..3eeaaa880ed 100644 --- a/core/services/chainlink/application.go +++ b/core/services/chainlink/application.go @@ -395,7 +395,6 @@ func NewApplication(opts ApplicationOpts) (Application, error) { delegates[job.Workflow] = workflows.NewDelegate( globalLogger, opts.CapabilitiesRegistry, - legacyEVMChains, workflowORM, func() *p2ptypes.PeerID { if externalPeerWrapper == nil { diff --git a/core/services/relay/evm/chain_writer.go b/core/services/relay/evm/chain_writer.go new file mode 100644 index 00000000000..3b73164c902 --- /dev/null +++ b/core/services/relay/evm/chain_writer.go @@ -0,0 +1,133 @@ +package evm + +import ( + "context" + "fmt" + "math/big" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/google/uuid" + + commonservices "github.com/smartcontractkit/chainlink-common/pkg/services" + "github.com/smartcontractkit/chainlink/v2/common/txmgr" + txmgrtypes "github.com/smartcontractkit/chainlink/v2/common/txmgr/types" + evmclient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" + evmtxmgr "github.com/smartcontractkit/chainlink/v2/core/chains/evm/txmgr" + evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services" + "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/types" + + commontypes "github.com/smartcontractkit/chainlink-common/pkg/types" +) + +type ChainWriterService interface { + services.ServiceCtx + commontypes.ChainWriter +} + +// Compile-time assertion that chainWriter implements the ChainWriterService interface. +var _ ChainWriterService = (*chainWriter)(nil) + +func NewChainWriterService(logger logger.Logger, client evmclient.Client, txm evmtxmgr.TxManager, config types.ChainWriterConfig) ChainWriterService { + return &chainWriter{logger: logger, client: client, config: config, txm: txm} +} + +type chainWriter struct { + commonservices.StateMachine + + logger logger.Logger + client evmclient.Client + config types.ChainWriterConfig + txm evmtxmgr.TxManager +} + +func (w *chainWriter) SubmitTransaction(ctx context.Context, contract, method string, args []any, transactionID uuid.UUID, toAddress string, meta *commontypes.TxMeta, value big.Int) error { + if !common.IsHexAddress(toAddress) { + return fmt.Errorf("toAddress is not a valid ethereum address: %v", toAddress) + } + + // TODO(nickcorin): Pre-process the contracts when initialising the chain writer. + contractConfig, ok := w.config.Contracts[contract] + if !ok { + return fmt.Errorf("contract config not found: %v", contract) + } + + methodConfig, ok := contractConfig.Configs[method] + if !ok { + return fmt.Errorf("method config not found: %v", method) + } + + forwarderABI := evmtypes.MustGetABI(contractConfig.ContractABI) + + calldata, err := forwarderABI.Pack(methodConfig.ChainSpecificName, args...) + if err != nil { + return fmt.Errorf("pack forwarder abi: %w", err) + } + + var checker evmtxmgr.TransmitCheckerSpec + if methodConfig.Checker != "" { + checker.CheckerType = txmgrtypes.TransmitCheckerType(methodConfig.Checker) + } + + var sendStrategy txmgrtypes.TxStrategy = txmgr.SendEveryStrategy{} + if w.config.SendStrategy != nil { + sendStrategy = w.config.SendStrategy + } + + req := evmtxmgr.TxRequest{ + FromAddress: methodConfig.FromAddress, + ToAddress: common.HexToAddress(toAddress), + EncodedPayload: calldata, + FeeLimit: methodConfig.GasLimit, + Meta: &txmgrtypes.TxMeta[common.Address, common.Hash]{WorkflowExecutionID: meta.WorkflowExecutionID}, + Strategy: sendStrategy, + Checker: checker, + } + + _, err = w.txm.CreateTransaction(ctx, req) + if err != nil { + return fmt.Errorf("failed to create tx: %w", err) + } + + return nil +} + +func (w *chainWriter) GetTransactionStatus(ctx context.Context, transactionID uuid.UUID) (commontypes.TransactionStatus, error) { + return commontypes.Unknown, fmt.Errorf("not implemented") +} + +func (w *chainWriter) GetFeeComponents(ctx context.Context) (*commontypes.ChainFeeComponents, error) { + return nil, fmt.Errorf("not implemented") +} + +func (w *chainWriter) Close() error { + return w.StopOnce(w.Name(), func() error { + _, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + // TODO(nickcorin): Add shutdown steps here. + return nil + }) +} + +func (w *chainWriter) HealthReport() map[string]error { + return map[string]error{ + w.Name(): nil, + } +} + +func (w *chainWriter) Name() string { + return "chain-writer" +} + +func (w *chainWriter) Ready() error { + return nil +} + +func (w *chainWriter) Start(ctx context.Context) error { + return w.StartOnce(w.Name(), func() error { + return nil + }) +} diff --git a/core/services/relay/evm/evm.go b/core/services/relay/evm/evm.go index 3f965931596..39160e2f8ac 100644 --- a/core/services/relay/evm/evm.go +++ b/core/services/relay/evm/evm.go @@ -132,7 +132,7 @@ func NewRelayer(lggr logger.Logger, chain legacyevm.Chain, opts RelayerOpts) (*R mercuryORM := mercury.NewORM(opts.DS) lloORM := llo.NewORM(opts.DS, chain.ID()) cdcFactory := llo.NewChannelDefinitionCacheFactory(lggr, lloORM, chain.LogPoller()) - return &Relayer{ + relayer := &Relayer{ ds: opts.DS, chain: chain, lggr: lggr, @@ -143,7 +143,22 @@ func NewRelayer(lggr logger.Logger, chain legacyevm.Chain, opts RelayerOpts) (*R mercuryORM: mercuryORM, transmitterCfg: opts.TransmitterConfig, capabilitiesRegistry: opts.CapabilitiesRegistry, - }, nil + } + + // Initialize write target capability if configuration is defined + if chain.Config().EVM().ChainWriter().ForwarderAddress() != nil { + ctx := context.Background() + capability, err := NewWriteTarget(ctx, relayer, chain, lggr) + if err != nil { + return nil, fmt.Errorf("failed to initialize write target: %w", err) + } + if err := relayer.capabilitiesRegistry.Add(ctx, capability); err != nil { + return nil, err + } + lggr.Infow("Registered write target", "chain_id", chain.ID()) + } + + return relayer, nil } func (r *Relayer) Name() string { diff --git a/core/services/relay/evm/types/types.go b/core/services/relay/evm/types/types.go index 294f7a029c9..8f4a2db864c 100644 --- a/core/services/relay/evm/types/types.go +++ b/core/services/relay/evm/types/types.go @@ -13,6 +13,8 @@ import ( ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + txmgrtypes "github.com/smartcontractkit/chainlink/v2/common/txmgr/types" + "github.com/smartcontractkit/chainlink-common/pkg/codec" "github.com/smartcontractkit/chainlink-common/pkg/services" "github.com/smartcontractkit/chainlink-common/pkg/types" @@ -20,6 +22,25 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils/big" ) +type ChainWriterConfig struct { + SendStrategy txmgrtypes.TxStrategy + Contracts map[string]ChainWriter +} + +type ChainWriter struct { + ContractABI string `json:"contractABI" toml:"contractABI"` + // key is genericName from config + Configs map[string]*ChainWriterDefinition `json:"configs" toml:"configs"` +} + +type ChainWriterDefinition struct { + // chain specific contract method name or event type. + ChainSpecificName string `json:"chainSpecificName"` + Checker string `json:"checker"` + FromAddress common.Address `json:"fromAddress"` + GasLimit uint64 `json:"gasLimit"` // TODO(archseer): what if this has to be configured per call? +} + type ChainReaderConfig struct { // Contracts key is contract name Contracts map[string]ChainContractReader `json:"contracts" toml:"contracts"` diff --git a/core/services/relay/evm/write_target.go b/core/services/relay/evm/write_target.go new file mode 100644 index 00000000000..29447aacf32 --- /dev/null +++ b/core/services/relay/evm/write_target.go @@ -0,0 +1,75 @@ +package evm + +import ( + "context" + "encoding/json" + "fmt" + + chainselectors "github.com/smartcontractkit/chain-selectors" + + commontypes "github.com/smartcontractkit/chainlink-common/pkg/types" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/targets" + "github.com/smartcontractkit/chainlink/v2/core/chains/legacyevm" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/forwarder" + "github.com/smartcontractkit/chainlink/v2/core/logger" + relayevmtypes "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/types" +) + +func NewWriteTarget(ctx context.Context, relayer *Relayer, chain legacyevm.Chain, lggr logger.Logger) (*targets.WriteTarget, error) { + // generate ID based on chain selector + name := fmt.Sprintf("write_%v", chain.ID()) + chainName, err := chainselectors.NameFromChainId(chain.ID().Uint64()) + if err == nil { + name = fmt.Sprintf("write_%v", chainName) + } + + // EVM-specific init + config := chain.Config().EVM().ChainWriter() + + // Initialize a reader to check whether a value was already transmitted on chain + contractReaderConfigEncoded, err := json.Marshal(relayevmtypes.ChainReaderConfig{ + Contracts: map[string]relayevmtypes.ChainContractReader{ + "forwarder": { + ContractABI: forwarder.KeystoneForwarderABI, + Configs: map[string]*relayevmtypes.ChainReaderDefinition{ + "getTransmitter": { + ChainSpecificName: "getTransmitter", + }, + }, + }, + }, + }) + if err != nil { + return nil, fmt.Errorf("failed to marshal contract reader config %v", err) + } + cr, err := relayer.NewContractReader(contractReaderConfigEncoded) + if err != nil { + return nil, err + } + err = cr.Bind(ctx, []commontypes.BoundContract{{ + Address: config.ForwarderAddress().String(), + Name: "forwarder", + }}) + if err != nil { + return nil, err + } + + chainWriterConfig := relayevmtypes.ChainWriterConfig{ + Contracts: map[string]relayevmtypes.ChainWriter{ + "forwarder": { + ContractABI: forwarder.KeystoneForwarderABI, + Configs: map[string]*relayevmtypes.ChainWriterDefinition{ + "report": { + ChainSpecificName: "report", + Checker: "simulate", + FromAddress: config.FromAddress().Address(), + GasLimit: 200_000, + }, + }, + }, + }, + } + cw := NewChainWriterService(lggr.Named("ChainWriter"), chain.Client(), chain.TxManager(), chainWriterConfig) + + return targets.NewWriteTarget(lggr, name, cr, cw, config.ForwarderAddress().String()), nil +} diff --git a/core/services/relay/evm/write_target_test.go b/core/services/relay/evm/write_target_test.go new file mode 100644 index 00000000000..331a30606cc --- /dev/null +++ b/core/services/relay/evm/write_target_test.go @@ -0,0 +1,213 @@ +package evm_test + +import ( + "errors" + "math/big" + "testing" + + "github.com/smartcontractkit/chainlink-common/pkg/values" + "github.com/smartcontractkit/chainlink/v2/core/internal/cltest" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/pgtest" + "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + evmcapabilities "github.com/smartcontractkit/chainlink/v2/core/capabilities" + evmclimocks "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client/mocks" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/txmgr" + txmmocks "github.com/smartcontractkit/chainlink/v2/core/chains/evm/txmgr/mocks" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" + evmmocks "github.com/smartcontractkit/chainlink/v2/core/chains/legacyevm/mocks" + relayevm "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/keystone/generated/forwarder" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/configtest" + "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/evmtest" + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/chainlink" +) + +var forwardABI = types.MustGetABI(forwarder.KeystoneForwarderMetaData.ABI) + +func TestEvmWrite(t *testing.T) { + chain := evmmocks.NewChain(t) + txManager := txmmocks.NewMockEvmTxManager(t) + evmClient := evmclimocks.NewClient(t) + + // This probably isn't the best way to do this, but couldn't find a simpler way to mock the CallContract response + var mockCall []byte + for i := 0; i < 32; i++ { + mockCall = append(mockCall, byte(0)) + } + evmClient.On("CallContract", mock.Anything, mock.Anything, mock.Anything).Return(mockCall, nil).Maybe() + + chain.On("ID").Return(big.NewInt(11155111)) + chain.On("TxManager").Return(txManager) + chain.On("LogPoller").Return(nil) + chain.On("Client").Return(evmClient) + + cfg := configtest.NewGeneralConfig(t, func(c *chainlink.Config, s *chainlink.Secrets) { + a := testutils.NewAddress() + addr, err2 := types.NewEIP55Address(a.Hex()) + require.NoError(t, err2) + c.EVM[0].ChainWriter.FromAddress = &addr + + forwarderA := testutils.NewAddress() + forwarderAddr, err2 := types.NewEIP55Address(forwarderA.Hex()) + require.NoError(t, err2) + c.EVM[0].ChainWriter.ForwarderAddress = &forwarderAddr + }) + evmCfg := evmtest.NewChainScopedConfig(t, cfg) + + chain.On("Config").Return(evmCfg) + + db := pgtest.NewSqlxDB(t) + keyStore := cltest.NewKeyStore(t, db) + + lggr := logger.TestLogger(t) + relayer, err := relayevm.NewRelayer(lggr, chain, relayevm.RelayerOpts{ + DS: db, + CSAETHKeystore: keyStore, + CapabilitiesRegistry: evmcapabilities.NewRegistry(lggr), + }) + require.NoError(t, err) + + txManager.On("CreateTransaction", mock.Anything, mock.Anything).Return(txmgr.Tx{}, nil).Run(func(args mock.Arguments) { + req := args.Get(1).(txmgr.TxRequest) + payload := make(map[string]any) + method := forwardABI.Methods["report"] + err = method.Inputs.UnpackIntoMap(payload, req.EncodedPayload[4:]) + require.NoError(t, err) + require.Equal(t, []byte{0x1, 0x2, 0x3}, payload["rawReport"]) + require.Equal(t, [][]byte{}, payload["signatures"]) + }).Once() + + t.Run("succeeds with valid report", func(t *testing.T) { + ctx := testutils.Context(t) + capability, err := evm.NewWriteTarget(ctx, relayer, chain, lggr) + require.NoError(t, err) + + config, err := values.NewMap(map[string]any{ + "Address": evmCfg.EVM().ChainWriter().ForwarderAddress().String(), + }) + require.NoError(t, err) + + inputs, err := values.NewMap(map[string]any{ + "signed_report": map[string]any{ + "report": []byte{1, 2, 3}, + "signatures": [][]byte{}, + }, + }) + require.NoError(t, err) + + req := capabilities.CapabilityRequest{ + Metadata: capabilities.RequestMetadata{ + WorkflowID: "test-id", + }, + Config: config, + Inputs: inputs, + } + + ch, err := capability.Execute(ctx, req) + require.NoError(t, err) + + response := <-ch + require.Nil(t, response.Err) + }) + + t.Run("succeeds with empty report", func(t *testing.T) { + ctx := testutils.Context(t) + capability, err := evm.NewWriteTarget(ctx, relayer, chain, logger.TestLogger(t)) + require.NoError(t, err) + + config, err := values.NewMap(map[string]any{ + "Address": evmCfg.EVM().ChainWriter().ForwarderAddress().String(), + }) + require.NoError(t, err) + + inputs, err := values.NewMap(map[string]any{ + "signed_report": map[string]any{ + "report": nil, + }, + }) + require.NoError(t, err) + + req := capabilities.CapabilityRequest{ + Metadata: capabilities.RequestMetadata{ + WorkflowID: "test-id", + }, + Config: config, + Inputs: inputs, + } + + ch, err := capability.Execute(ctx, req) + require.NoError(t, err) + + response := <-ch + require.Nil(t, response.Err) + }) + + t.Run("fails with invalid config", func(t *testing.T) { + ctx := testutils.Context(t) + capability, err := evm.NewWriteTarget(ctx, relayer, chain, logger.TestLogger(t)) + require.NoError(t, err) + + invalidConfig, err := values.NewMap(map[string]any{ + "Address": "invalid-address", + }) + require.NoError(t, err) + + inputs, err := values.NewMap(map[string]any{ + "signed_report": map[string]any{ + "report": nil, + }, + }) + require.NoError(t, err) + + req := capabilities.CapabilityRequest{ + Metadata: capabilities.RequestMetadata{ + WorkflowID: "test-id", + }, + Config: invalidConfig, + Inputs: inputs, + } + + _, err = capability.Execute(ctx, req) + require.Error(t, err) + }) + + t.Run("fails when TXM CreateTransaction returns error", func(t *testing.T) { + ctx := testutils.Context(t) + capability, err := evm.NewWriteTarget(ctx, relayer, chain, logger.TestLogger(t)) + require.NoError(t, err) + + config, err := values.NewMap(map[string]any{ + "Address": evmCfg.EVM().ChainWriter().ForwarderAddress().String(), + }) + require.NoError(t, err) + + inputs, err := values.NewMap(map[string]any{ + "signed_report": map[string]any{ + "report": []byte{1, 2, 3}, + "signatures": [][]byte{}, + }, + }) + require.NoError(t, err) + + req := capabilities.CapabilityRequest{ + Metadata: capabilities.RequestMetadata{ + WorkflowID: "test-id", + }, + Config: config, + Inputs: inputs, + } + + txManager.On("CreateTransaction", mock.Anything, mock.Anything).Return(txmgr.Tx{}, errors.New("TXM error")) + + _, err = capability.Execute(ctx, req) + require.Error(t, err) + }) +} diff --git a/core/services/workflows/delegate.go b/core/services/workflows/delegate.go index 95d2f0ca29d..cafc2274770 100644 --- a/core/services/workflows/delegate.go +++ b/core/services/workflows/delegate.go @@ -10,8 +10,6 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/capabilities" "github.com/smartcontractkit/chainlink-common/pkg/types/core" - "github.com/smartcontractkit/chainlink/v2/core/capabilities/targets" - "github.com/smartcontractkit/chainlink/v2/core/chains/legacyevm" "github.com/smartcontractkit/chainlink/v2/core/logger" "github.com/smartcontractkit/chainlink/v2/core/services/job" p2ptypes "github.com/smartcontractkit/chainlink/v2/core/services/p2p/types" @@ -19,11 +17,10 @@ import ( ) type Delegate struct { - registry core.CapabilitiesRegistry - logger logger.Logger - legacyEVMChains legacyevm.LegacyChainContainer - peerID func() *p2ptypes.PeerID - store store.Store + registry core.CapabilitiesRegistry + logger logger.Logger + peerID func() *p2ptypes.PeerID + store store.Store } var _ job.Delegate = (*Delegate)(nil) @@ -42,12 +39,6 @@ func (d *Delegate) OnDeleteJob(context.Context, job.Job) error { return nil } // ServicesForSpec satisfies the job.Delegate interface. func (d *Delegate) ServicesForSpec(ctx context.Context, spec job.Job) ([]job.ServiceCtx, error) { - // NOTE: we temporarily do registration inside ServicesForSpec, this will be moved out of job specs in the future - err := targets.InitializeWrite(d.registry, d.legacyEVMChains, d.logger) - if err != nil { - d.logger.Errorw("could not initialize writes", err) - } - dinfo, err := initializeDONInfo(d.logger) if err != nil { d.logger.Errorw("could not add initialize don info", err) @@ -106,8 +97,8 @@ func initializeDONInfo(lggr logger.Logger) (*capabilities.DON, error) { }, nil } -func NewDelegate(logger logger.Logger, registry core.CapabilitiesRegistry, legacyEVMChains legacyevm.LegacyChainContainer, store store.Store, peerID func() *p2ptypes.PeerID) *Delegate { - return &Delegate{logger: logger, registry: registry, legacyEVMChains: legacyEVMChains, store: store, peerID: peerID} +func NewDelegate(logger logger.Logger, registry core.CapabilitiesRegistry, store store.Store, peerID func() *p2ptypes.PeerID) *Delegate { + return &Delegate{logger: logger, registry: registry, store: store, peerID: peerID} } func ValidatedWorkflowSpec(tomlString string) (job.Job, error) { diff --git a/flake.lock b/flake.lock index 260506ff892..da3a69cd248 100644 --- a/flake.lock +++ b/flake.lock @@ -26,15 +26,16 @@ "nixpkgs": "nixpkgs" }, "locked": { - "lastModified": 1715073011, - "narHash": "sha256-fwTWvaOgAUrQwaCcGfeRn1D+n0G4ltr+I+FPb05RPeY=", + "lastModified": 1714727549, + "narHash": "sha256-CWXRTxxcgMfQubJugpeg3yVWIfm70MYTtgaKWKgD60U=", "owner": "shazow", "repo": "foundry.nix", - "rev": "5d2761d546b8712e3faaa416bacc6567007d757a", + "rev": "47cf189ec395eda4b3e0623179d1075c8027ca97", "type": "github" }, "original": { "owner": "shazow", + "ref": "monthly", "repo": "foundry.nix", "type": "github" } diff --git a/flake.nix b/flake.nix index 7ae9a5435bd..f65847455b2 100644 --- a/flake.nix +++ b/flake.nix @@ -3,7 +3,7 @@ inputs = { nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; - foundry.url = "github:shazow/foundry.nix"; + foundry.url = "github:shazow/foundry.nix/monthly"; flake-utils.url = "github:numtide/flake-utils"; foundry.inputs.flake-utils.follows = "flake-utils"; };