diff --git a/.github/workflows/solidity-foundry.yml b/.github/workflows/solidity-foundry.yml index 7c930207fe7..47f9571eea3 100644 --- a/.github/workflows/solidity-foundry.yml +++ b/.github/workflows/solidity-foundry.yml @@ -32,7 +32,7 @@ jobs: strategy: fail-fast: false matrix: - product: [vrf, automation, llo-feeds, l2ep, functions, shared] + product: [vrf, automation, llo-feeds, l2ep, functions, keystone, shared] needs: [changes] name: Foundry Tests ${{ matrix.product }} # See https://github.com/foundry-rs/foundry/issues/3827 diff --git a/.github/workflows/solidity-hardhat.yml b/.github/workflows/solidity-hardhat.yml index da9d7daccbb..f07c9f8fdca 100644 --- a/.github/workflows/solidity-hardhat.yml +++ b/.github/workflows/solidity-hardhat.yml @@ -25,7 +25,7 @@ jobs: with: filters: | src: - - 'contracts/src/!(v0.8/(llo-feeds|ccip)/**)/**/*' + - 'contracts/src/!(v0.8/(llo-feeds|keystone|ccip)/**)/**/*' - 'contracts/test/**/*' - 'contracts/package.json' - 'contracts/pnpm-lock.yaml' diff --git a/common/txmgr/types/tx.go b/common/txmgr/types/tx.go index 27cda86f6eb..3b294adcd07 100644 --- a/common/txmgr/types/tx.go +++ b/common/txmgr/types/tx.go @@ -150,6 +150,9 @@ type TxMeta[ADDR types.Hashable, TX_HASH types.Hashable] struct { ForceFulfilled *bool `json:"ForceFulfilled,omitempty"` ForceFulfillmentAttempt *uint64 `json:"ForceFulfillmentAttempt,omitempty"` + // Used for Keystone Workflows + WorkflowExecutionID *string `json:"WorkflowExecutionID,omitempty"` + // Used only for forwarded txs, tracks the original destination address. // When this is set, it indicates tx is forwarded through To address. FwdrDestAddress *ADDR `json:"ForwarderDestAddress,omitempty"` diff --git a/contracts/GNUmakefile b/contracts/GNUmakefile index f666014c48f..751a47b3be4 100644 --- a/contracts/GNUmakefile +++ b/contracts/GNUmakefile @@ -1,6 +1,6 @@ # ALL_FOUNDRY_PRODUCTS contains a list of all products that have a foundry # profile defined and use the Foundry snapshots. -ALL_FOUNDRY_PRODUCTS = l2ep llo-feeds functions shared +ALL_FOUNDRY_PRODUCTS = l2ep llo-feeds functions keystone shared # To make a snapshot for a specific product, either set the `FOUNDRY_PROFILE` env var # or call the target with `FOUNDRY_PROFILE=product` diff --git a/contracts/foundry.toml b/contracts/foundry.toml index 931f0d12361..13807c71bdf 100644 --- a/contracts/foundry.toml +++ b/contracts/foundry.toml @@ -46,6 +46,12 @@ test = 'src/v0.8/llo-feeds/test' solc_version = '0.8.19' # We cannot turn on deny_warnings = true as that will hide any CI failure +[profile.keystone] +solc_version = '0.8.19' +src = 'src/v0.8/keystone' +test = 'src/v0.8/keystone/test' +optimizer_runs = 10_000 + [profile.shared] optimizer_runs = 1000000 src = 'src/v0.8/shared' diff --git a/contracts/gas-snapshots/keystone.gas-snapshot b/contracts/gas-snapshots/keystone.gas-snapshot new file mode 100644 index 00000000000..be23de1fc62 --- /dev/null +++ b/contracts/gas-snapshots/keystone.gas-snapshot @@ -0,0 +1,2 @@ +KeystoneForwarderTest:test_abi_partial_decoding_works() (gas: 2068) +KeystoneForwarderTest:test_it_works() (gas: 993848) \ No newline at end of file diff --git a/contracts/scripts/native_solc_compile_all b/contracts/scripts/native_solc_compile_all index cf1226a2d5c..f4cec6ce1ee 100755 --- a/contracts/scripts/native_solc_compile_all +++ b/contracts/scripts/native_solc_compile_all @@ -12,7 +12,7 @@ python3 -m pip install --require-hashes -r $SCRIPTPATH/requirements.txt # 6 and 7 are legacy contracts, for each other product we have a native_solc_compile_all_$product script # These scripts can be run individually, or all together with this script. # To add new CL products, simply write a native_solc_compile_all_$product script and add it to the list below. -for product in 6 7 automation events_mock feeds functions llo-feeds logpoller operatorforwarder shared transmission vrf +for product in 6 7 automation events_mock feeds functions keystone llo-feeds logpoller operatorforwarder shared transmission vrf do $SCRIPTPATH/native_solc_compile_all_$product done diff --git a/contracts/scripts/native_solc_compile_all_keystone b/contracts/scripts/native_solc_compile_all_keystone new file mode 100755 index 00000000000..3f4d33d6ecc --- /dev/null +++ b/contracts/scripts/native_solc_compile_all_keystone @@ -0,0 +1,31 @@ +#!/usr/bin/env bash + +set -e + +echo " ┌──────────────────────────────────────────────┐" +echo " │ Compiling Keystone contracts... │" +echo " └──────────────────────────────────────────────┘" + +SOLC_VERSION="0.8.19" +OPTIMIZE_RUNS=1000000 + + +SCRIPTPATH="$( cd "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" +python3 -m pip install --require-hashes -r "$SCRIPTPATH"/requirements.txt +solc-select install $SOLC_VERSION +solc-select use $SOLC_VERSION +export SOLC_VERSION=$SOLC_VERSION + +ROOT="$( cd "$(dirname "$0")" >/dev/null 2>&1 ; cd ../../ && pwd -P )" + +compileContract () { + local contract + contract=$(basename "$1" ".sol") + + solc --overwrite --optimize --optimize-runs $OPTIMIZE_RUNS --metadata-hash none \ + -o "$ROOT"/contracts/solc/v$SOLC_VERSION/"$contract" \ + --abi --bin --allow-paths "$ROOT"/contracts/src/v0.8\ + "$ROOT"/contracts/src/v0.8/"$1" +} + +compileContract keystone/KeystoneForwarder.sol diff --git a/contracts/src/v0.8/keystone/KeystoneForwarder.sol b/contracts/src/v0.8/keystone/KeystoneForwarder.sol new file mode 100644 index 00000000000..2fa3304addc --- /dev/null +++ b/contracts/src/v0.8/keystone/KeystoneForwarder.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {IForwarder} from "./interfaces/IForwarder.sol"; +import {ConfirmedOwner} from "../shared/access/ConfirmedOwner.sol"; +import {TypeAndVersionInterface} from "../interfaces/TypeAndVersionInterface.sol"; +import {Utils} from "./libraries/Utils.sol"; + +// solhint-disable custom-errors, no-unused-vars +contract KeystoneForwarder is IForwarder, ConfirmedOwner, TypeAndVersionInterface { + error ReentrantCall(); + + struct HotVars { + bool reentrancyGuard; // guard against reentrancy + } + + HotVars internal s_hotVars; // Mixture of config and state, commonly accessed + + mapping(bytes32 => address) internal s_reports; + + constructor() ConfirmedOwner(msg.sender) {} + + // send a report to targetAddress + function report( + address targetAddress, + bytes calldata data, + bytes[] calldata signatures + ) external nonReentrant returns (bool) { + require(data.length > 4 + 64, "invalid data length"); + + // data is an encoded call with the selector prefixed: (bytes4 selector, bytes report, ...) + // we are able to partially decode just the first param, since we don't know the rest + bytes memory rawReport = abi.decode(data[4:], (bytes)); + + // TODO: we probably need some type of f value config? + + bytes32 hash = keccak256(rawReport); + + // validate signatures + for (uint256 i = 0; i < signatures.length; i++) { + // TODO: is libocr-style multiple bytes32 arrays more optimal? + (bytes32 r, bytes32 s, uint8 v) = Utils._splitSignature(signatures[i]); + address signer = ecrecover(hash, v, r, s); + // TODO: we need to store oracle cluster similar to aggregator then, to validate valid signer list + } + + (bytes32 workflowId, bytes32 workflowExecutionId) = Utils._splitReport(rawReport); + + // report was already processed + if (s_reports[workflowExecutionId] != address(0)) { + return false; + } + + // solhint-disable-next-line avoid-low-level-calls + (bool success, bytes memory result) = targetAddress.call(data); + + s_reports[workflowExecutionId] = msg.sender; + return true; + } + + // get transmitter of a given report or 0x0 if it wasn't transmitted yet + function getTransmitter(bytes32 workflowExecutionId) external view returns (address) { + return s_reports[workflowExecutionId]; + } + + /// @inheritdoc TypeAndVersionInterface + function typeAndVersion() external pure override returns (string memory) { + return "KeystoneForwarder 1.0.0"; + } + + /** + * @dev replicates Open Zeppelin's ReentrancyGuard but optimized to fit our storage + */ + modifier nonReentrant() { + if (s_hotVars.reentrancyGuard) revert ReentrantCall(); + s_hotVars.reentrancyGuard = true; + _; + s_hotVars.reentrancyGuard = false; + } +} diff --git a/contracts/src/v0.8/keystone/interfaces/IForwarder.sol b/contracts/src/v0.8/keystone/interfaces/IForwarder.sol new file mode 100644 index 00000000000..ce9512c6570 --- /dev/null +++ b/contracts/src/v0.8/keystone/interfaces/IForwarder.sol @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +/// @title IForwarder - forwards keystone reports to a target +interface IForwarder {} diff --git a/contracts/src/v0.8/keystone/libraries/Utils.sol b/contracts/src/v0.8/keystone/libraries/Utils.sol new file mode 100644 index 00000000000..3a11c0792a1 --- /dev/null +++ b/contracts/src/v0.8/keystone/libraries/Utils.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +// solhint-disable custom-errors +library Utils { + // solhint-disable avoid-low-level-calls, chainlink-solidity/explicit-returns + function _splitSignature(bytes memory sig) internal pure returns (bytes32 r, bytes32 s, uint8 v) { + require(sig.length == 65, "invalid signature length"); + + assembly { + /* + First 32 bytes stores the length of the signature + + add(sig, 32) = pointer of sig + 32 + effectively, skips first 32 bytes of signature + + mload(p) loads next 32 bytes starting at the memory address p into memory + */ + + // first 32 bytes, after the length prefix + r := mload(add(sig, 32)) + // second 32 bytes + s := mload(add(sig, 64)) + // final byte (first byte of the next 32 bytes) + v := byte(0, mload(add(sig, 96))) + } + + // implicitly return (r, s, v) + } + + // solhint-disable avoid-low-level-calls, chainlink-solidity/explicit-returns + function _splitReport( + bytes memory rawReport + ) internal pure returns (bytes32 workflowId, bytes32 workflowExecutionId) { + require(rawReport.length > 64, "invalid report length"); + assembly { + // skip first 32 bytes, contains length of the report + workflowId := mload(add(rawReport, 32)) + workflowExecutionId := mload(add(rawReport, 64)) + } + } +} diff --git a/contracts/src/v0.8/keystone/test/KeystoneForwarder.t.sol b/contracts/src/v0.8/keystone/test/KeystoneForwarder.t.sol new file mode 100644 index 00000000000..10d4cb3f9c2 --- /dev/null +++ b/contracts/src/v0.8/keystone/test/KeystoneForwarder.t.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; + +import "../KeystoneForwarder.sol"; +import {Utils} from "../libraries/Utils.sol"; + +contract Receiver { + event MessageReceived(bytes32 indexed workflowId, bytes32 indexed workflowExecutionId, bytes[] mercuryReports); + + constructor() {} + + function foo(bytes calldata rawReport) external { + // decode metadata + (bytes32 workflowId, bytes32 workflowExecutionId) = Utils._splitReport(rawReport); + // parse actual report + bytes[] memory mercuryReports = abi.decode(rawReport[64:], (bytes[])); + emit MessageReceived(workflowId, workflowExecutionId, mercuryReports); + } +} + +contract KeystoneForwarderTest is Test { + function setUp() public virtual {} + + function test_abi_partial_decoding_works() public { + bytes memory report = hex"0102"; + uint256 amount = 1; + bytes memory payload = abi.encode(report, amount); + bytes memory decodedReport = abi.decode(payload, (bytes)); + assertEq(decodedReport, report, "not equal"); + } + + function test_it_works() public { + KeystoneForwarder forwarder = new KeystoneForwarder(); + Receiver receiver = new Receiver(); + + // taken from https://github.com/smartcontractkit/chainlink/blob/2390ec7f3c56de783ef4e15477e99729f188c524/core/services/relay/evm/cap_encoder_test.go#L42-L55 + bytes + memory report = hex"6d795f69640000000000000000000000000000000000000000000000000000006d795f657865637574696f6e5f696400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000301020300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004aabbccdd00000000000000000000000000000000000000000000000000000000"; + bytes memory data = abi.encodeWithSignature("foo(bytes)", report); + bytes[] memory signatures = new bytes[](0); + + vm.expectCall(address(receiver), data); + vm.recordLogs(); + + bool delivered1 = forwarder.report(address(receiver), data, signatures); + assertTrue(delivered1, "report not delivered"); + + Vm.Log[] memory entries = vm.getRecordedLogs(); + assertEq(entries[0].emitter, address(receiver)); + // validate workflow id and workflow execution id + bytes32 workflowId = hex"6d795f6964000000000000000000000000000000000000000000000000000000"; + bytes32 executionId = hex"6d795f657865637574696f6e5f69640000000000000000000000000000000000"; + assertEq(entries[0].topics[1], workflowId); + assertEq(entries[0].topics[2], executionId); + bytes[] memory mercuryReports = abi.decode(entries[0].data, (bytes[])); + assertEq(mercuryReports.length, 2); + assertEq(mercuryReports[0], hex"010203"); + assertEq(mercuryReports[1], hex"aabbccdd"); + + // doesn't deliver the same report more than once + bool delivered2 = forwarder.report(address(receiver), data, signatures); + assertFalse(delivered2, "report redelivered"); + } +} diff --git a/core/capabilities/targets/write_target.go b/core/capabilities/targets/write_target.go new file mode 100644 index 00000000000..c6d34271662 --- /dev/null +++ b/core/capabilities/targets/write_target.go @@ -0,0 +1,239 @@ +package targets + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/mitchellh/mapstructure" + "github.com/pkg/errors" + + chainselectors "github.com/smartcontractkit/chain-selectors" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" + commontypes "github.com/smartcontractkit/chainlink-common/pkg/types" + "github.com/smartcontractkit/chainlink-common/pkg/values" + txmgrcommon "github.com/smartcontractkit/chainlink/v2/common/txmgr" + abiutil "github.com/smartcontractkit/chainlink/v2/core/chains/evm/abi" + "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/services/pipeline" +) + +var forwardABI = evmtypes.MustGetABI(forwarder.KeystoneForwarderMetaData.ABI) + +func InitializeWrite(registry commontypes.CapabilitiesRegistry, legacyEVMChains legacyevm.LegacyChainContainer) error { + for _, chain := range legacyEVMChains.Slice() { + capability := NewEvmWrite(chain) + if err := registry.Add(context.TODO(), capability); err != nil { + return err + } + } + return nil +} + +var ( + _ capabilities.ActionCapability = &EvmWrite{} +) + +type EvmWrite struct { + chain legacyevm.Chain + capabilities.CapabilityInfo +} + +func NewEvmWrite(chain legacyevm.Chain) *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) + } + + info := capabilities.MustNewCapabilityInfo( + name, + capabilities.CapabilityTypeTarget, + "Write target.", + "v1.0.0", + ) + + return &EvmWrite{ + chain, + info, + } +} + +type EvmConfig struct { + ChainID uint + Address string + Params []any + ABI string +} + +// TODO: enforce required key presence + +func parseConfig(rawConfig *values.Map) (EvmConfig, error) { + var config EvmConfig + configAny, err := rawConfig.Unwrap() + if err != nil { + return config, err + } + err = mapstructure.Decode(configAny, &config) + return config, err +} + +func evaluateParams(params []any, inputs map[string]any) ([]any, error) { + vars := pipeline.NewVarsFrom(inputs) + var args []any + for _, param := range params { + switch v := param.(type) { + case string: + val, err := pipeline.VarExpr(v, vars)() + if err == nil { + args = append(args, val) + } else if errors.Is(errors.Cause(err), pipeline.ErrParameterEmpty) { + args = append(args, param) + } else { + return args, err + } + default: + args = append(args, param) + } + } + + return args, nil +} + +func encodePayload(args []any, rawSelector string) ([]byte, error) { + // TODO: do spec parsing as part of parseConfig() + + // Based on https://github.com/ethereum/go-ethereum/blob/f1c27c286ea2d0e110a507e5749e92d0a6144f08/signer/fourbyte/abi.go#L77-L102 + + // NOTE: without having full ABI it's actually impossible to support function overloading + selector, err := abiutil.ParseSignature(rawSelector) + if err != nil { + return nil, err + } + + abidata, err := json.Marshal([]abi.SelectorMarshaling{selector}) + if err != nil { + return nil, err + } + + spec, err := abi.JSON(strings.NewReader(string(abidata))) + if err != nil { + return nil, err + } + + return spec.Pack(selector.Name, args...) + + // NOTE: could avoid JSON encoding/decoding the selector + // var args abi.Arguments + // for _, arg := range selector.Inputs { + // ty, err := abi.NewType(arg.Type, arg.InternalType, arg.Components) + // if err != nil { + // return nil, err + // } + // args = append(args, abi.Argument{Name: arg.Name, Type: ty}) + // } + // // we only care about the name + inputs so we can compute the method ID + // method := abi.NewMethod(selector.Name, selector.Name, abi.Function, "nonpayable", false, false, args, nil) + // + // https://github.com/ethereum/go-ethereum/blob/f1c27c286ea2d0e110a507e5749e92d0a6144f08/accounts/abi/abi.go#L77-L82 + // arguments, err := method.Inputs.Pack(args...) + // if err != nil { + // return nil, err + // } + // // Pack up the method ID too if not a constructor and return + // return append(method.ID, arguments...), nil +} + +func (cap *EvmWrite) Execute(ctx context.Context, callback chan<- capabilities.CapabilityResponse, request capabilities.CapabilityRequest) error { + // TODO: idempotency + + // TODO: extract into ChainWriter? + txm := cap.chain.TxManager() + + config := cap.chain.Config().EVM().ChainWriter() + + reqConfig, err := parseConfig(request.Config) + if err != nil { + return err + } + + inputsAny, err := request.Inputs.Unwrap() + if err != nil { + return err + } + inputs := inputsAny.(map[string]any) + + // evaluate any variables in reqConfig.Params + args, err := evaluateParams(reqConfig.Params, inputs) + if err != nil { + return err + } + + data, err := encodePayload(args, reqConfig.ABI) + if err != nil { + return err + } + + // TODO: validate encoded report is prefixed with workflowID and executionID that match the request meta + + // unlimited gas in the MVP demo + gasLimit := 0 + // No signature validation in the MVP demo + signatures := [][]byte{} + + // construct forwarding payload + calldata, err := forwardABI.Pack("report", common.HexToAddress(reqConfig.Address), data, signatures) + if err != nil { + return 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, + } + strategy := txmgrcommon.NewSendEveryStrategy() + + checker := txmgr.TransmitCheckerSpec{ + CheckerType: txmgr.TransmitCheckerTypeSimulate, + } + req := txmgr.TxRequest{ + FromAddress: config.FromAddress().Address(), + ToAddress: config.ForwarderAddress().Address(), + EncodedPayload: calldata, + FeeLimit: uint32(gasLimit), + 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) + if err != nil { + return err + } + fmt.Printf("Transaction submitted %v", tx.ID) + go func() { + // TODO: cast tx.Error to Err (or Value to Value?) + callback <- capabilities.CapabilityResponse{ + Value: nil, + Err: nil, + } + close(callback) + }() + return nil +} + +func (cap *EvmWrite) RegisterToWorkflow(ctx context.Context, request capabilities.RegisterToWorkflowRequest) error { + return nil +} + +func (cap *EvmWrite) 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 new file mode 100644 index 00000000000..68ca890cc0c --- /dev/null +++ b/core/capabilities/targets/write_target_test.go @@ -0,0 +1,92 @@ +package targets_test + +import ( + "math/big" + "testing" + + "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" + evmtypes "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/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/services/chainlink" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/ethkey" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +var forwardABI = evmtypes.MustGetABI(forwarder.KeystoneForwarderMetaData.ABI) + +func TestEvmWrite(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 := ethkey.NewEIP55Address(a.Hex()) + require.NoError(t, err) + c.EVM[0].ChainWriter.FromAddress = &addr + + forwarderA := testutils.NewAddress() + forwarderAddr, err := ethkey.NewEIP55Address(forwarderA.Hex()) + require.NoError(t, err) + c.EVM[0].ChainWriter.ForwarderAddress = &forwarderAddr + }) + evmcfg := evmtest.NewChainScopedConfig(t, cfg) + chain.On("Config").Return(evmcfg) + + capability := targets.NewEvmWrite(chain) + ctx := testutils.Context(t) + + config, err := values.NewMap(map[string]any{ + "abi": "receive(report bytes)", + "params": []any{"$(report)"}, + }) + require.NoError(t, err) + + inputs, err := values.NewMap(map[string]any{ + "report": []byte{1, 2, 3}, + }) + 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{ + 0xa6, 0x9b, 0x6e, 0xd0, // selector = keccak(signature)[:4] + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x20, // type = bytes + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, // len = 3 + 0x1, 0x2, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // elements [1, 2, 3] zero padded + }, payload["data"]) + + }) + + ch := make(chan capabilities.CapabilityResponse) + + err = capability.Execute(ctx, ch, req) + require.NoError(t, err) + + response := <-ch + require.Nil(t, response.Err) +} diff --git a/core/chains/evm/config/chain_scoped.go b/core/chains/evm/config/chain_scoped.go index fb6df26b1ad..c579da86c8c 100644 --- a/core/chains/evm/config/chain_scoped.go +++ b/core/chains/evm/config/chain_scoped.go @@ -94,6 +94,10 @@ func (e *evmConfig) OCR2() OCR2 { return &ocr2Config{c: e.c.OCR2} } +func (e *evmConfig) ChainWriter() ChainWriter { + return &chainWriterConfig{c: e.c.ChainWriter} +} + func (e *evmConfig) GasEstimator() GasEstimator { return &gasEstimatorConfig{c: e.c.GasEstimator, blockDelay: e.c.RPCBlockQueryDelay, transactionsMaxInFlight: e.c.Transactions.MaxInFlight, k: e.c.KeySpecific} } diff --git a/core/chains/evm/config/chain_scoped_chain_writer.go b/core/chains/evm/config/chain_scoped_chain_writer.go new file mode 100644 index 00000000000..b84731314e1 --- /dev/null +++ b/core/chains/evm/config/chain_scoped_chain_writer.go @@ -0,0 +1,18 @@ +package config + +import ( + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/config/toml" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/ethkey" +) + +type chainWriterConfig struct { + c toml.ChainWriter +} + +func (b *chainWriterConfig) FromAddress() *ethkey.EIP55Address { + return b.c.FromAddress +} + +func (b *chainWriterConfig) ForwarderAddress() *ethkey.EIP55Address { + return b.c.ForwarderAddress +} diff --git a/core/chains/evm/config/config.go b/core/chains/evm/config/config.go index 33e2c85eee5..5b397ddd574 100644 --- a/core/chains/evm/config/config.go +++ b/core/chains/evm/config/config.go @@ -10,6 +10,7 @@ import ( commonconfig "github.com/smartcontractkit/chainlink/v2/common/config" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" "github.com/smartcontractkit/chainlink/v2/core/config" + "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/ethkey" ) type EVM interface { @@ -19,6 +20,7 @@ type EVM interface { GasEstimator() GasEstimator OCR() OCR OCR2() OCR2 + ChainWriter() ChainWriter NodePool() NodePool AutoCreateKey() bool @@ -124,6 +126,11 @@ type BlockHistory interface { TransactionPercentile() uint16 } +type ChainWriter interface { + FromAddress() *ethkey.EIP55Address + ForwarderAddress() *ethkey.EIP55Address +} + type NodePool interface { PollFailureThreshold() uint32 PollInterval() time.Duration diff --git a/core/chains/evm/config/toml/config.go b/core/chains/evm/config/toml/config.go index 2348e648696..6ebf3ed0a94 100644 --- a/core/chains/evm/config/toml/config.go +++ b/core/chains/evm/config/toml/config.go @@ -368,6 +368,7 @@ type Chain struct { NodePool NodePool `toml:",omitempty"` OCR OCR `toml:",omitempty"` OCR2 OCR2 `toml:",omitempty"` + ChainWriter ChainWriter `toml:",omitempty"` } func (c *Chain) ValidateConfig() (err error) { @@ -447,6 +448,20 @@ func (a *Automation) setFrom(f *Automation) { } } +type ChainWriter struct { + FromAddress *ethkey.EIP55Address `toml:",omitempty"` + ForwarderAddress *ethkey.EIP55Address `toml:",omitempty"` +} + +func (m *ChainWriter) setFrom(f *ChainWriter) { + if v := f.FromAddress; v != nil { + m.FromAddress = v + } + if v := f.ForwarderAddress; v != nil { + m.ForwarderAddress = v + } +} + type BalanceMonitor struct { Enabled *bool } diff --git a/core/chains/evm/config/toml/defaults.go b/core/chains/evm/config/toml/defaults.go index 5e9a10de003..adb91b3a1bc 100644 --- a/core/chains/evm/config/toml/defaults.go +++ b/core/chains/evm/config/toml/defaults.go @@ -177,4 +177,5 @@ func (c *Chain) SetFrom(f *Chain) { c.NodePool.setFrom(&f.NodePool) c.OCR.setFrom(&f.OCR) c.OCR2.setFrom(&f.OCR2) + c.ChainWriter.setFrom(&f.ChainWriter) } diff --git a/core/config/docs/chains-evm.toml b/core/config/docs/chains-evm.toml index 975264be6d4..6f2322fd6db 100644 --- a/core/config/docs/chains-evm.toml +++ b/core/config/docs/chains-evm.toml @@ -363,3 +363,9 @@ Order = 100 # Default [EVM.OCR2.Automation] # GasLimit controls the gas limit for transmit transactions from ocr2automation job. GasLimit = 5400000 # Default + +[EVM.ChainWriter] +# FromAddress is Address of the transmitter key to use for workflow writes. +FromAddress = '0x2a3e23c6f242F5345320814aC8a1b4E58707D292' # Example +# ForwarderAddress is the keystone forwarder contract address on chain. +ForwarderAddress = '0x2a3e23c6f242F5345320814aC8a1b4E58707D292' # Example diff --git a/core/config/docs/docs_test.go b/core/config/docs/docs_test.go index dc1e0f2af12..8b4e38b980d 100644 --- a/core/config/docs/docs_test.go +++ b/core/config/docs/docs_test.go @@ -80,6 +80,10 @@ func TestDoc(t *testing.T) { docDefaults.FlagsContractAddress = nil docDefaults.LinkContractAddress = nil docDefaults.OperatorFactoryAddress = nil + require.Empty(t, docDefaults.ChainWriter.FromAddress) + require.Empty(t, docDefaults.ChainWriter.ForwarderAddress) + docDefaults.ChainWriter.FromAddress = nil + docDefaults.ChainWriter.ForwarderAddress = nil assertTOML(t, fallbackDefaults, docDefaults) }) diff --git a/core/gethwrappers/go_generate.go b/core/gethwrappers/go_generate.go index b6b64a96ae0..a72b6bf615d 100644 --- a/core/gethwrappers/go_generate.go +++ b/core/gethwrappers/go_generate.go @@ -155,6 +155,9 @@ package gethwrappers // Chainlink Functions //go:generate go generate ./functions +// Chainlink Keystone +//go:generate go generate ./keystone + // Mercury //go:generate go generate ./llo-feeds diff --git a/core/gethwrappers/keystone/generated/forwarder/forwarder.go b/core/gethwrappers/keystone/generated/forwarder/forwarder.go new file mode 100644 index 00000000000..c66e2886793 --- /dev/null +++ b/core/gethwrappers/keystone/generated/forwarder/forwarder.go @@ -0,0 +1,600 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package forwarder + +import ( + "errors" + "fmt" + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/generated" +) + +var ( + _ = errors.New + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription + _ = abi.ConvertType +) + +var KeystoneForwarderMetaData = &bind.MetaData{ + ABI: "[{\"inputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"inputs\":[],\"name\":\"ReentrantCall\",\"type\":\"error\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"}],\"name\":\"OwnershipTransferRequested\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"from\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"}],\"name\":\"OwnershipTransferred\",\"type\":\"event\"},{\"inputs\":[],\"name\":\"acceptOwnership\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"workflowExecutionId\",\"type\":\"bytes32\"}],\"name\":\"getTransmitter\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"owner\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"targetAddress\",\"type\":\"address\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"},{\"internalType\":\"bytes[]\",\"name\":\"signatures\",\"type\":\"bytes[]\"}],\"name\":\"report\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"to\",\"type\":\"address\"}],\"name\":\"transferOwnership\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"typeAndVersion\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"\",\"type\":\"string\"}],\"stateMutability\":\"pure\",\"type\":\"function\"}]", + Bin: "0x608060405234801561001057600080fd5b5033806000816100675760405162461bcd60e51b815260206004820152601860248201527f43616e6e6f7420736574206f776e657220746f207a65726f000000000000000060448201526064015b60405180910390fd5b600080546001600160a01b0319166001600160a01b0384811691909117909155811615610097576100978161009f565b505050610148565b336001600160a01b038216036100f75760405162461bcd60e51b815260206004820152601760248201527f43616e6e6f74207472616e7366657220746f2073656c66000000000000000000604482015260640161005e565b600180546001600160a01b0319166001600160a01b0383811691821790925560008054604051929316917fed8889f560326eb138920d842192f0eb3dd22b4f139c87a2c57538e05bae12789190a350565b610c12806101576000396000f3fe608060405234801561001057600080fd5b50600436106100725760003560e01c8063c0965dc311610050578063c0965dc314610108578063e6b714581461012b578063f2fde38b1461016157600080fd5b8063181f5a771461007757806379ba5097146100bf5780638da5cb5b146100c9575b600080fd5b604080518082018252601781527f4b657973746f6e65466f7277617264657220312e302e30000000000000000000602082015290516100b69190610827565b60405180910390f35b6100c7610174565b005b60005473ffffffffffffffffffffffffffffffffffffffff165b60405173ffffffffffffffffffffffffffffffffffffffff90911681526020016100b6565b61011b6101163660046108bc565b610276565b60405190151581526020016100b6565b6100e3610139366004610998565b60009081526003602052604090205473ffffffffffffffffffffffffffffffffffffffff1690565b6100c761016f3660046109b1565b61058e565b60015473ffffffffffffffffffffffffffffffffffffffff1633146101fa576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601660248201527f4d7573742062652070726f706f736564206f776e65720000000000000000000060448201526064015b60405180910390fd5b60008054337fffffffffffffffffffffffff00000000000000000000000000000000000000008083168217845560018054909116905560405173ffffffffffffffffffffffffffffffffffffffff90921692909183917f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e091a350565b60025460009060ff16156102b6576040517f37ed32e800000000000000000000000000000000000000000000000000000000815260040160405180910390fd5b600280547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff001660011790556044841161034b576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601360248201527f696e76616c69642064617461206c656e6774680000000000000000000000000060448201526064016101f1565b600061035a85600481896109d3565b8101906103679190610a2c565b8051602082012090915060005b848110156104655760008060006103e289898681811061039657610396610afb565b90506020028101906103a89190610b2a565b8080601f0160208091040260200160405190810160405280939291908181526020018383808284376000920191909152506105a292505050565b925092509250600060018683868660405160008152602001604052604051610426949392919093845260ff9290921660208401526040830152606082015260800190565b6020604051602081039080840390855afa158015610448573d6000803e3d6000fd5b5086955061045d9450859350610b9692505050565b915050610374565b5060008061047284610630565b600081815260036020526040902054919350915073ffffffffffffffffffffffffffffffffffffffff16156104ae57600094505050505061055d565b6000808b73ffffffffffffffffffffffffffffffffffffffff168b8b6040516104d8929190610bf5565b6000604051808303816000865af19150503d8060008114610515576040519150601f19603f3d011682016040523d82523d6000602084013e61051a565b606091505b5050506000928352505060036020526040902080547fffffffffffffffffffffffff00000000000000000000000000000000000000001633179055506001925050505b600280547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0016905595945050505050565b6105966106af565b61059f81610732565b50565b60008060008351604114610612576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601860248201527f696e76616c6964207369676e6174757265206c656e677468000000000000000060448201526064016101f1565b50505060208101516040820151606090920151909260009190911a90565b600080604083511161069e576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601560248201527f696e76616c6964207265706f7274206c656e677468000000000000000000000060448201526064016101f1565b505060208101516040909101519091565b60005473ffffffffffffffffffffffffffffffffffffffff163314610730576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601660248201527f4f6e6c792063616c6c61626c65206279206f776e65720000000000000000000060448201526064016101f1565b565b3373ffffffffffffffffffffffffffffffffffffffff8216036107b1576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601760248201527f43616e6e6f74207472616e7366657220746f2073656c6600000000000000000060448201526064016101f1565b600180547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff83811691821790925560008054604051929316917fed8889f560326eb138920d842192f0eb3dd22b4f139c87a2c57538e05bae12789190a350565b600060208083528351808285015260005b8181101561085457858101830151858201604001528201610838565b5060006040828601015260407fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f8301168501019250505092915050565b803573ffffffffffffffffffffffffffffffffffffffff811681146108b757600080fd5b919050565b6000806000806000606086880312156108d457600080fd5b6108dd86610893565b9450602086013567ffffffffffffffff808211156108fa57600080fd5b818801915088601f83011261090e57600080fd5b81358181111561091d57600080fd5b89602082850101111561092f57600080fd5b60208301965080955050604088013591508082111561094d57600080fd5b818801915088601f83011261096157600080fd5b81358181111561097057600080fd5b8960208260051b850101111561098557600080fd5b9699959850939650602001949392505050565b6000602082840312156109aa57600080fd5b5035919050565b6000602082840312156109c357600080fd5b6109cc82610893565b9392505050565b600080858511156109e357600080fd5b838611156109f057600080fd5b5050820193919092039150565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b600060208284031215610a3e57600080fd5b813567ffffffffffffffff80821115610a5657600080fd5b818401915084601f830112610a6a57600080fd5b813581811115610a7c57610a7c6109fd565b604051601f82017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0908116603f01168101908382118183101715610ac257610ac26109fd565b81604052828152876020848701011115610adb57600080fd5b826020860160208301376000928101602001929092525095945050505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b60008083357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe1843603018112610b5f57600080fd5b83018035915067ffffffffffffffff821115610b7a57600080fd5b602001915036819003821315610b8f57600080fd5b9250929050565b60007fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8203610bee577f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b5060010190565b818382376000910190815291905056fea164736f6c6343000813000a", +} + +var KeystoneForwarderABI = KeystoneForwarderMetaData.ABI + +var KeystoneForwarderBin = KeystoneForwarderMetaData.Bin + +func DeployKeystoneForwarder(auth *bind.TransactOpts, backend bind.ContractBackend) (common.Address, *types.Transaction, *KeystoneForwarder, error) { + parsed, err := KeystoneForwarderMetaData.GetAbi() + if err != nil { + return common.Address{}, nil, nil, err + } + if parsed == nil { + return common.Address{}, nil, nil, errors.New("GetABI returned nil") + } + + address, tx, contract, err := bind.DeployContract(auth, *parsed, common.FromHex(KeystoneForwarderBin), backend) + if err != nil { + return common.Address{}, nil, nil, err + } + return address, tx, &KeystoneForwarder{address: address, abi: *parsed, KeystoneForwarderCaller: KeystoneForwarderCaller{contract: contract}, KeystoneForwarderTransactor: KeystoneForwarderTransactor{contract: contract}, KeystoneForwarderFilterer: KeystoneForwarderFilterer{contract: contract}}, nil +} + +type KeystoneForwarder struct { + address common.Address + abi abi.ABI + KeystoneForwarderCaller + KeystoneForwarderTransactor + KeystoneForwarderFilterer +} + +type KeystoneForwarderCaller struct { + contract *bind.BoundContract +} + +type KeystoneForwarderTransactor struct { + contract *bind.BoundContract +} + +type KeystoneForwarderFilterer struct { + contract *bind.BoundContract +} + +type KeystoneForwarderSession struct { + Contract *KeystoneForwarder + CallOpts bind.CallOpts + TransactOpts bind.TransactOpts +} + +type KeystoneForwarderCallerSession struct { + Contract *KeystoneForwarderCaller + CallOpts bind.CallOpts +} + +type KeystoneForwarderTransactorSession struct { + Contract *KeystoneForwarderTransactor + TransactOpts bind.TransactOpts +} + +type KeystoneForwarderRaw struct { + Contract *KeystoneForwarder +} + +type KeystoneForwarderCallerRaw struct { + Contract *KeystoneForwarderCaller +} + +type KeystoneForwarderTransactorRaw struct { + Contract *KeystoneForwarderTransactor +} + +func NewKeystoneForwarder(address common.Address, backend bind.ContractBackend) (*KeystoneForwarder, error) { + abi, err := abi.JSON(strings.NewReader(KeystoneForwarderABI)) + if err != nil { + return nil, err + } + contract, err := bindKeystoneForwarder(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &KeystoneForwarder{address: address, abi: abi, KeystoneForwarderCaller: KeystoneForwarderCaller{contract: contract}, KeystoneForwarderTransactor: KeystoneForwarderTransactor{contract: contract}, KeystoneForwarderFilterer: KeystoneForwarderFilterer{contract: contract}}, nil +} + +func NewKeystoneForwarderCaller(address common.Address, caller bind.ContractCaller) (*KeystoneForwarderCaller, error) { + contract, err := bindKeystoneForwarder(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &KeystoneForwarderCaller{contract: contract}, nil +} + +func NewKeystoneForwarderTransactor(address common.Address, transactor bind.ContractTransactor) (*KeystoneForwarderTransactor, error) { + contract, err := bindKeystoneForwarder(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &KeystoneForwarderTransactor{contract: contract}, nil +} + +func NewKeystoneForwarderFilterer(address common.Address, filterer bind.ContractFilterer) (*KeystoneForwarderFilterer, error) { + contract, err := bindKeystoneForwarder(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &KeystoneForwarderFilterer{contract: contract}, nil +} + +func bindKeystoneForwarder(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := KeystoneForwarderMetaData.GetAbi() + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, *parsed, caller, transactor, filterer), nil +} + +func (_KeystoneForwarder *KeystoneForwarderRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _KeystoneForwarder.Contract.KeystoneForwarderCaller.contract.Call(opts, result, method, params...) +} + +func (_KeystoneForwarder *KeystoneForwarderRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _KeystoneForwarder.Contract.KeystoneForwarderTransactor.contract.Transfer(opts) +} + +func (_KeystoneForwarder *KeystoneForwarderRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _KeystoneForwarder.Contract.KeystoneForwarderTransactor.contract.Transact(opts, method, params...) +} + +func (_KeystoneForwarder *KeystoneForwarderCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _KeystoneForwarder.Contract.contract.Call(opts, result, method, params...) +} + +func (_KeystoneForwarder *KeystoneForwarderTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _KeystoneForwarder.Contract.contract.Transfer(opts) +} + +func (_KeystoneForwarder *KeystoneForwarderTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _KeystoneForwarder.Contract.contract.Transact(opts, method, params...) +} + +func (_KeystoneForwarder *KeystoneForwarderCaller) GetTransmitter(opts *bind.CallOpts, workflowExecutionId [32]byte) (common.Address, error) { + var out []interface{} + err := _KeystoneForwarder.contract.Call(opts, &out, "getTransmitter", workflowExecutionId) + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +func (_KeystoneForwarder *KeystoneForwarderSession) GetTransmitter(workflowExecutionId [32]byte) (common.Address, error) { + return _KeystoneForwarder.Contract.GetTransmitter(&_KeystoneForwarder.CallOpts, workflowExecutionId) +} + +func (_KeystoneForwarder *KeystoneForwarderCallerSession) GetTransmitter(workflowExecutionId [32]byte) (common.Address, error) { + return _KeystoneForwarder.Contract.GetTransmitter(&_KeystoneForwarder.CallOpts, workflowExecutionId) +} + +func (_KeystoneForwarder *KeystoneForwarderCaller) Owner(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _KeystoneForwarder.contract.Call(opts, &out, "owner") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +func (_KeystoneForwarder *KeystoneForwarderSession) Owner() (common.Address, error) { + return _KeystoneForwarder.Contract.Owner(&_KeystoneForwarder.CallOpts) +} + +func (_KeystoneForwarder *KeystoneForwarderCallerSession) Owner() (common.Address, error) { + return _KeystoneForwarder.Contract.Owner(&_KeystoneForwarder.CallOpts) +} + +func (_KeystoneForwarder *KeystoneForwarderCaller) TypeAndVersion(opts *bind.CallOpts) (string, error) { + var out []interface{} + err := _KeystoneForwarder.contract.Call(opts, &out, "typeAndVersion") + + if err != nil { + return *new(string), err + } + + out0 := *abi.ConvertType(out[0], new(string)).(*string) + + return out0, err + +} + +func (_KeystoneForwarder *KeystoneForwarderSession) TypeAndVersion() (string, error) { + return _KeystoneForwarder.Contract.TypeAndVersion(&_KeystoneForwarder.CallOpts) +} + +func (_KeystoneForwarder *KeystoneForwarderCallerSession) TypeAndVersion() (string, error) { + return _KeystoneForwarder.Contract.TypeAndVersion(&_KeystoneForwarder.CallOpts) +} + +func (_KeystoneForwarder *KeystoneForwarderTransactor) AcceptOwnership(opts *bind.TransactOpts) (*types.Transaction, error) { + return _KeystoneForwarder.contract.Transact(opts, "acceptOwnership") +} + +func (_KeystoneForwarder *KeystoneForwarderSession) AcceptOwnership() (*types.Transaction, error) { + return _KeystoneForwarder.Contract.AcceptOwnership(&_KeystoneForwarder.TransactOpts) +} + +func (_KeystoneForwarder *KeystoneForwarderTransactorSession) AcceptOwnership() (*types.Transaction, error) { + return _KeystoneForwarder.Contract.AcceptOwnership(&_KeystoneForwarder.TransactOpts) +} + +func (_KeystoneForwarder *KeystoneForwarderTransactor) Report(opts *bind.TransactOpts, targetAddress common.Address, data []byte, signatures [][]byte) (*types.Transaction, error) { + return _KeystoneForwarder.contract.Transact(opts, "report", targetAddress, data, signatures) +} + +func (_KeystoneForwarder *KeystoneForwarderSession) Report(targetAddress common.Address, data []byte, signatures [][]byte) (*types.Transaction, error) { + return _KeystoneForwarder.Contract.Report(&_KeystoneForwarder.TransactOpts, targetAddress, data, signatures) +} + +func (_KeystoneForwarder *KeystoneForwarderTransactorSession) Report(targetAddress common.Address, data []byte, signatures [][]byte) (*types.Transaction, error) { + return _KeystoneForwarder.Contract.Report(&_KeystoneForwarder.TransactOpts, targetAddress, data, signatures) +} + +func (_KeystoneForwarder *KeystoneForwarderTransactor) TransferOwnership(opts *bind.TransactOpts, to common.Address) (*types.Transaction, error) { + return _KeystoneForwarder.contract.Transact(opts, "transferOwnership", to) +} + +func (_KeystoneForwarder *KeystoneForwarderSession) TransferOwnership(to common.Address) (*types.Transaction, error) { + return _KeystoneForwarder.Contract.TransferOwnership(&_KeystoneForwarder.TransactOpts, to) +} + +func (_KeystoneForwarder *KeystoneForwarderTransactorSession) TransferOwnership(to common.Address) (*types.Transaction, error) { + return _KeystoneForwarder.Contract.TransferOwnership(&_KeystoneForwarder.TransactOpts, to) +} + +type KeystoneForwarderOwnershipTransferRequestedIterator struct { + Event *KeystoneForwarderOwnershipTransferRequested + + contract *bind.BoundContract + event string + + logs chan types.Log + sub ethereum.Subscription + done bool + fail error +} + +func (it *KeystoneForwarderOwnershipTransferRequestedIterator) Next() bool { + + if it.fail != nil { + return false + } + + if it.done { + select { + case log := <-it.logs: + it.Event = new(KeystoneForwarderOwnershipTransferRequested) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + + select { + case log := <-it.logs: + it.Event = new(KeystoneForwarderOwnershipTransferRequested) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +func (it *KeystoneForwarderOwnershipTransferRequestedIterator) Error() error { + return it.fail +} + +func (it *KeystoneForwarderOwnershipTransferRequestedIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +type KeystoneForwarderOwnershipTransferRequested struct { + From common.Address + To common.Address + Raw types.Log +} + +func (_KeystoneForwarder *KeystoneForwarderFilterer) FilterOwnershipTransferRequested(opts *bind.FilterOpts, from []common.Address, to []common.Address) (*KeystoneForwarderOwnershipTransferRequestedIterator, error) { + + var fromRule []interface{} + for _, fromItem := range from { + fromRule = append(fromRule, fromItem) + } + var toRule []interface{} + for _, toItem := range to { + toRule = append(toRule, toItem) + } + + logs, sub, err := _KeystoneForwarder.contract.FilterLogs(opts, "OwnershipTransferRequested", fromRule, toRule) + if err != nil { + return nil, err + } + return &KeystoneForwarderOwnershipTransferRequestedIterator{contract: _KeystoneForwarder.contract, event: "OwnershipTransferRequested", logs: logs, sub: sub}, nil +} + +func (_KeystoneForwarder *KeystoneForwarderFilterer) WatchOwnershipTransferRequested(opts *bind.WatchOpts, sink chan<- *KeystoneForwarderOwnershipTransferRequested, from []common.Address, to []common.Address) (event.Subscription, error) { + + var fromRule []interface{} + for _, fromItem := range from { + fromRule = append(fromRule, fromItem) + } + var toRule []interface{} + for _, toItem := range to { + toRule = append(toRule, toItem) + } + + logs, sub, err := _KeystoneForwarder.contract.WatchLogs(opts, "OwnershipTransferRequested", fromRule, toRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + + event := new(KeystoneForwarderOwnershipTransferRequested) + if err := _KeystoneForwarder.contract.UnpackLog(event, "OwnershipTransferRequested", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +func (_KeystoneForwarder *KeystoneForwarderFilterer) ParseOwnershipTransferRequested(log types.Log) (*KeystoneForwarderOwnershipTransferRequested, error) { + event := new(KeystoneForwarderOwnershipTransferRequested) + if err := _KeystoneForwarder.contract.UnpackLog(event, "OwnershipTransferRequested", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +type KeystoneForwarderOwnershipTransferredIterator struct { + Event *KeystoneForwarderOwnershipTransferred + + contract *bind.BoundContract + event string + + logs chan types.Log + sub ethereum.Subscription + done bool + fail error +} + +func (it *KeystoneForwarderOwnershipTransferredIterator) Next() bool { + + if it.fail != nil { + return false + } + + if it.done { + select { + case log := <-it.logs: + it.Event = new(KeystoneForwarderOwnershipTransferred) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + default: + return false + } + } + + select { + case log := <-it.logs: + it.Event = new(KeystoneForwarderOwnershipTransferred) + if err := it.contract.UnpackLog(it.Event, it.event, log); err != nil { + it.fail = err + return false + } + it.Event.Raw = log + return true + + case err := <-it.sub.Err(): + it.done = true + it.fail = err + return it.Next() + } +} + +func (it *KeystoneForwarderOwnershipTransferredIterator) Error() error { + return it.fail +} + +func (it *KeystoneForwarderOwnershipTransferredIterator) Close() error { + it.sub.Unsubscribe() + return nil +} + +type KeystoneForwarderOwnershipTransferred struct { + From common.Address + To common.Address + Raw types.Log +} + +func (_KeystoneForwarder *KeystoneForwarderFilterer) FilterOwnershipTransferred(opts *bind.FilterOpts, from []common.Address, to []common.Address) (*KeystoneForwarderOwnershipTransferredIterator, error) { + + var fromRule []interface{} + for _, fromItem := range from { + fromRule = append(fromRule, fromItem) + } + var toRule []interface{} + for _, toItem := range to { + toRule = append(toRule, toItem) + } + + logs, sub, err := _KeystoneForwarder.contract.FilterLogs(opts, "OwnershipTransferred", fromRule, toRule) + if err != nil { + return nil, err + } + return &KeystoneForwarderOwnershipTransferredIterator{contract: _KeystoneForwarder.contract, event: "OwnershipTransferred", logs: logs, sub: sub}, nil +} + +func (_KeystoneForwarder *KeystoneForwarderFilterer) WatchOwnershipTransferred(opts *bind.WatchOpts, sink chan<- *KeystoneForwarderOwnershipTransferred, from []common.Address, to []common.Address) (event.Subscription, error) { + + var fromRule []interface{} + for _, fromItem := range from { + fromRule = append(fromRule, fromItem) + } + var toRule []interface{} + for _, toItem := range to { + toRule = append(toRule, toItem) + } + + logs, sub, err := _KeystoneForwarder.contract.WatchLogs(opts, "OwnershipTransferred", fromRule, toRule) + if err != nil { + return nil, err + } + return event.NewSubscription(func(quit <-chan struct{}) error { + defer sub.Unsubscribe() + for { + select { + case log := <-logs: + + event := new(KeystoneForwarderOwnershipTransferred) + if err := _KeystoneForwarder.contract.UnpackLog(event, "OwnershipTransferred", log); err != nil { + return err + } + event.Raw = log + + select { + case sink <- event: + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + case err := <-sub.Err(): + return err + case <-quit: + return nil + } + } + }), nil +} + +func (_KeystoneForwarder *KeystoneForwarderFilterer) ParseOwnershipTransferred(log types.Log) (*KeystoneForwarderOwnershipTransferred, error) { + event := new(KeystoneForwarderOwnershipTransferred) + if err := _KeystoneForwarder.contract.UnpackLog(event, "OwnershipTransferred", log); err != nil { + return nil, err + } + event.Raw = log + return event, nil +} + +func (_KeystoneForwarder *KeystoneForwarder) ParseLog(log types.Log) (generated.AbigenLog, error) { + switch log.Topics[0] { + case _KeystoneForwarder.abi.Events["OwnershipTransferRequested"].ID: + return _KeystoneForwarder.ParseOwnershipTransferRequested(log) + case _KeystoneForwarder.abi.Events["OwnershipTransferred"].ID: + return _KeystoneForwarder.ParseOwnershipTransferred(log) + + default: + return nil, fmt.Errorf("abigen wrapper received unknown log topic: %v", log.Topics[0]) + } +} + +func (KeystoneForwarderOwnershipTransferRequested) Topic() common.Hash { + return common.HexToHash("0xed8889f560326eb138920d842192f0eb3dd22b4f139c87a2c57538e05bae1278") +} + +func (KeystoneForwarderOwnershipTransferred) Topic() common.Hash { + return common.HexToHash("0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0") +} + +func (_KeystoneForwarder *KeystoneForwarder) Address() common.Address { + return _KeystoneForwarder.address +} + +type KeystoneForwarderInterface interface { + GetTransmitter(opts *bind.CallOpts, workflowExecutionId [32]byte) (common.Address, error) + + Owner(opts *bind.CallOpts) (common.Address, error) + + TypeAndVersion(opts *bind.CallOpts) (string, error) + + AcceptOwnership(opts *bind.TransactOpts) (*types.Transaction, error) + + Report(opts *bind.TransactOpts, targetAddress common.Address, data []byte, signatures [][]byte) (*types.Transaction, error) + + TransferOwnership(opts *bind.TransactOpts, to common.Address) (*types.Transaction, error) + + FilterOwnershipTransferRequested(opts *bind.FilterOpts, from []common.Address, to []common.Address) (*KeystoneForwarderOwnershipTransferRequestedIterator, error) + + WatchOwnershipTransferRequested(opts *bind.WatchOpts, sink chan<- *KeystoneForwarderOwnershipTransferRequested, from []common.Address, to []common.Address) (event.Subscription, error) + + ParseOwnershipTransferRequested(log types.Log) (*KeystoneForwarderOwnershipTransferRequested, error) + + FilterOwnershipTransferred(opts *bind.FilterOpts, from []common.Address, to []common.Address) (*KeystoneForwarderOwnershipTransferredIterator, error) + + WatchOwnershipTransferred(opts *bind.WatchOpts, sink chan<- *KeystoneForwarderOwnershipTransferred, from []common.Address, to []common.Address) (event.Subscription, error) + + ParseOwnershipTransferred(log types.Log) (*KeystoneForwarderOwnershipTransferred, error) + + ParseLog(log types.Log) (generated.AbigenLog, error) + + Address() common.Address +} diff --git a/core/gethwrappers/keystone/generation/generated-wrapper-dependency-versions-do-not-edit.txt b/core/gethwrappers/keystone/generation/generated-wrapper-dependency-versions-do-not-edit.txt new file mode 100644 index 00000000000..8dad729b196 --- /dev/null +++ b/core/gethwrappers/keystone/generation/generated-wrapper-dependency-versions-do-not-edit.txt @@ -0,0 +1,2 @@ +GETH_VERSION: 1.13.8 +forwarder: ../../../contracts/solc/v0.8.19/KeystoneForwarder/KeystoneForwarder.abi ../../../contracts/solc/v0.8.19/KeystoneForwarder/KeystoneForwarder.bin 4886b538e1fdc8aaf860901de36269e0c35acfd3e6eb190654d693ff9dbd4b6d diff --git a/core/gethwrappers/keystone/go_generate.go b/core/gethwrappers/keystone/go_generate.go new file mode 100644 index 00000000000..75800132f8e --- /dev/null +++ b/core/gethwrappers/keystone/go_generate.go @@ -0,0 +1,7 @@ +// Package gethwrappers provides tools for wrapping solidity contracts with +// golang packages, using abigen. +package gethwrappers + +// Keystone + +//go:generate go run ../generation/generate/wrap.go ../../../contracts/solc/v0.8.19/KeystoneForwarder/KeystoneForwarder.abi ../../../contracts/solc/v0.8.19/KeystoneForwarder/KeystoneForwarder.bin KeystoneForwarder forwarder diff --git a/core/scripts/common/avalanche_subnet.go b/core/scripts/common/avalanche_subnet.go new file mode 100644 index 00000000000..238f193d2b8 --- /dev/null +++ b/core/scripts/common/avalanche_subnet.go @@ -0,0 +1,126 @@ +package common + +import ( + "encoding/json" + "errors" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" +) + +// AvaSubnetHeader is a copy of [github.com/ava-labs/subnet-evm/core/types.Header] to avoid importing the whole module. +type AvaSubnetHeader struct { + ParentHash common.Hash `json:"parentHash" gencodec:"required"` + UncleHash common.Hash `json:"sha3Uncles" gencodec:"required"` + Coinbase common.Address `json:"miner" gencodec:"required"` + Root common.Hash `json:"stateRoot" gencodec:"required"` + TxHash common.Hash `json:"transactionsRoot" gencodec:"required"` + ReceiptHash common.Hash `json:"receiptsRoot" gencodec:"required"` + Bloom AvaBloom `json:"logsBloom" gencodec:"required"` + Difficulty *big.Int `json:"difficulty" gencodec:"required"` + Number *big.Int `json:"number" gencodec:"required"` + GasLimit uint64 `json:"gasLimit" gencodec:"required"` + GasUsed uint64 `json:"gasUsed" gencodec:"required"` + Time uint64 `json:"timestamp" gencodec:"required"` + Extra []byte `json:"extraData" gencodec:"required"` + MixDigest common.Hash `json:"mixHash"` + Nonce AvaBlockNonce `json:"nonce"` + BaseFee *big.Int `json:"baseFeePerGas" rlp:"optional"` + BlockGasCost *big.Int `json:"blockGasCost" rlp:"optional"` +} + +func (h *AvaSubnetHeader) UnmarshalJSON(input []byte) error { + type Header struct { + ParentHash *common.Hash `json:"parentHash" gencodec:"required"` + UncleHash *common.Hash `json:"sha3Uncles" gencodec:"required"` + Coinbase *common.Address `json:"miner" gencodec:"required"` + Root *common.Hash `json:"stateRoot" gencodec:"required"` + TxHash *common.Hash `json:"transactionsRoot" gencodec:"required"` + ReceiptHash *common.Hash `json:"receiptsRoot" gencodec:"required"` + Bloom *AvaBloom `json:"logsBloom" gencodec:"required"` + Difficulty *hexutil.Big `json:"difficulty" gencodec:"required"` + Number *hexutil.Big `json:"number" gencodec:"required"` + GasLimit *hexutil.Uint64 `json:"gasLimit" gencodec:"required"` + GasUsed *hexutil.Uint64 `json:"gasUsed" gencodec:"required"` + Time *hexutil.Uint64 `json:"timestamp" gencodec:"required"` + Extra *hexutil.Bytes `json:"extraData" gencodec:"required"` + MixDigest *common.Hash `json:"mixHash"` + Nonce *AvaBlockNonce `json:"nonce"` + BaseFee *hexutil.Big `json:"baseFeePerGas" rlp:"optional"` + BlockGasCost *hexutil.Big `json:"blockGasCost" rlp:"optional"` + } + var dec Header + if err := json.Unmarshal(input, &dec); err != nil { + return err + } + if dec.ParentHash == nil { + return errors.New("missing required field 'parentHash' for Header") + } + h.ParentHash = *dec.ParentHash + if dec.UncleHash == nil { + return errors.New("missing required field 'sha3Uncles' for Header") + } + h.UncleHash = *dec.UncleHash + if dec.Coinbase == nil { + return errors.New("missing required field 'miner' for Header") + } + h.Coinbase = *dec.Coinbase + if dec.Root == nil { + return errors.New("missing required field 'stateRoot' for Header") + } + h.Root = *dec.Root + if dec.TxHash == nil { + return errors.New("missing required field 'transactionsRoot' for Header") + } + h.TxHash = *dec.TxHash + if dec.ReceiptHash == nil { + return errors.New("missing required field 'receiptsRoot' for Header") + } + h.ReceiptHash = *dec.ReceiptHash + if dec.Bloom == nil { + return errors.New("missing required field 'logsBloom' for Header") + } + h.Bloom = *dec.Bloom + if dec.Difficulty == nil { + return errors.New("missing required field 'difficulty' for Header") + } + h.Difficulty = (*big.Int)(dec.Difficulty) + if dec.Number == nil { + return errors.New("missing required field 'number' for Header") + } + h.Number = (*big.Int)(dec.Number) + if dec.GasLimit == nil { + return errors.New("missing required field 'gasLimit' for Header") + } + h.GasLimit = uint64(*dec.GasLimit) + if dec.GasUsed == nil { + return errors.New("missing required field 'gasUsed' for Header") + } + h.GasUsed = uint64(*dec.GasUsed) + if dec.Time == nil { + return errors.New("missing required field 'timestamp' for Header") + } + h.Time = uint64(*dec.Time) + if dec.Extra == nil { + return errors.New("missing required field 'extraData' for Header") + } + h.Extra = *dec.Extra + if dec.MixDigest != nil { + h.MixDigest = *dec.MixDigest + } + if dec.Nonce != nil { + h.Nonce = *dec.Nonce + } + if dec.BaseFee != nil { + h.BaseFee = (*big.Int)(dec.BaseFee) + } + if dec.BlockGasCost != nil { + h.BlockGasCost = (*big.Int)(dec.BlockGasCost) + } + return nil +} + +func (h *AvaSubnetHeader) Hash() common.Hash { + return rlpHash(h) +} diff --git a/core/scripts/common/helpers.go b/core/scripts/common/helpers.go index 0ed9929956a..0967991e62b 100644 --- a/core/scripts/common/helpers.go +++ b/core/scripts/common/helpers.go @@ -498,7 +498,20 @@ func GetRlpHeaders(env Environment, blockNumbers []*big.Int, getParentBlocks boo //fmt.Println("Calculated BH:", bh.String(), // "fetched BH:", h.Hash(), // "block number:", new(big.Int).Set(blockNum).Add(blockNum, offset).String()) + } else if IsAvaxSubnet(env.ChainID) { + var h AvaSubnetHeader + // Get child block since it's the one that has the parent hash in its header. + nextBlockNum := new(big.Int).Set(blockNum).Add(blockNum, offset) + err2 := env.Jc.CallContext(context.Background(), &h, "eth_getBlockByNumber", hexutil.EncodeBig(nextBlockNum), false) + if err2 != nil { + return nil, hashes, fmt.Errorf("failed to get header: %+v", err2) + } + rlpHeader, err2 = rlp.EncodeToBytes(h) + if err2 != nil { + return nil, hashes, fmt.Errorf("failed to encode rlp: %+v", err2) + } + hashes = append(hashes, h.Hash().String()) } else if IsPolygonEdgeNetwork(env.ChainID) { // Get child block since it's the one that has the parent hash in its header. @@ -570,12 +583,20 @@ func CalculateLatestBlockHeader(env Environment, blockNumberInput int) (err erro return err } -// IsAvaxNetwork returns true if the given chain ID corresponds to an avalanche network or subnet. +// IsAvaxNetwork returns true if the given chain ID corresponds to an avalanche network. func IsAvaxNetwork(chainID int64) bool { return chainID == 43114 || // C-chain mainnet - chainID == 43113 || // Fuji testnet - chainID == 335 || // DFK testnet - chainID == 53935 // DFK mainnet + chainID == 43113 // Fuji testnet +} + +// IsAvaxSubnet returns true if the given chain ID corresponds to an avalanche subnet. +func IsAvaxSubnet(chainID int64) bool { + return chainID == 335 || // DFK testnet + chainID == 53935 || // DFK mainnet + chainID == 955081 || // Nexon Dev + chainID == 595581 || // Nexon Test + chainID == 807424 || // Nexon QA + chainID == 847799 // Nexon Stage } func UpkeepLink(chainID int64, upkeepID *big.Int) string { diff --git a/core/scripts/go.mod b/core/scripts/go.mod index 2c877299deb..4dc3995ece4 100644 --- a/core/scripts/go.mod +++ b/core/scripts/go.mod @@ -247,6 +247,7 @@ require ( github.com/shirou/gopsutil/v3 v3.23.11 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/smartcontractkit/caigo v0.0.0-20230621050857-b29a4ca8c704 // indirect + github.com/smartcontractkit/chain-selectors v1.0.10 // indirect github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240213120401-01a23955f9f8 // indirect github.com/smartcontractkit/chainlink-data-streams v0.0.0-20240214203158-47dae5de1336 // indirect github.com/smartcontractkit/chainlink-feeds v0.0.0-20240119021347-3c541a78cdb8 // indirect diff --git a/core/scripts/go.sum b/core/scripts/go.sum index 7a4faf31ce3..3b0efd9053e 100644 --- a/core/scripts/go.sum +++ b/core/scripts/go.sum @@ -1167,6 +1167,8 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartcontractkit/caigo v0.0.0-20230621050857-b29a4ca8c704 h1:T3lFWumvbfM1u/etVq42Afwq/jtNSBSOA8n5jntnNPo= github.com/smartcontractkit/caigo v0.0.0-20230621050857-b29a4ca8c704/go.mod h1:2QuJdEouTWjh5BDy5o/vgGXQtR4Gz8yH1IYB5eT7u4M= +github.com/smartcontractkit/chain-selectors v1.0.10 h1:t9kJeE6B6G+hKD0GYR4kGJSCqR1LNS7aI3jT0V+xGrg= +github.com/smartcontractkit/chain-selectors v1.0.10/go.mod h1:d4Hi+E1zqjy9HqMkjBE5q1vcG9VGgxf5VxiRHfzi2kE= github.com/smartcontractkit/chainlink-automation v1.0.2-0.20240118014648-1ab6a88c9429 h1:xkejUBZhcBpBrTSfxc91Iwzadrb6SXw8ks69bHIQ9Ww= github.com/smartcontractkit/chainlink-automation v1.0.2-0.20240118014648-1ab6a88c9429/go.mod h1:wJmVvDf4XSjsahWtfUq3wvIAYEAuhr7oxmxYnEL/LGQ= github.com/smartcontractkit/chainlink-common v0.1.7-0.20240216174848-c7f1809138d6 h1:hpNkTpLtwWXKqguf7wYqetxpmxY/bSO+1PLpY8VBu2w= diff --git a/core/services/chainlink/application.go b/core/services/chainlink/application.go index d16bfc747c6..e9a128f861e 100644 --- a/core/services/chainlink/application.go +++ b/core/services/chainlink/application.go @@ -357,6 +357,7 @@ func NewApplication(opts ApplicationOpts) (Application, error) { job.Workflow: workflows.NewDelegate( globalLogger, registry, + legacyEVMChains, ), } webhookJobRunner = delegates[job.Webhook].(*webhook.Delegate).WebhookJobRunner() diff --git a/core/services/chainlink/config_test.go b/core/services/chainlink/config_test.go index b16551912b2..5b2f3be3788 100644 --- a/core/services/chainlink/config_test.go +++ b/core/services/chainlink/config_test.go @@ -1124,6 +1124,14 @@ func TestConfig_full(t *testing.T) { require.NoError(t, config.DecodeTOML(strings.NewReader(fullTOML), &got)) // Except for some EVM node fields. for c := range got.EVM { + addr, err := ethkey.NewEIP55Address("0x2a3e23c6f242F5345320814aC8a1b4E58707D292") + require.NoError(t, err) + if got.EVM[c].ChainWriter.FromAddress == nil { + got.EVM[c].ChainWriter.FromAddress = &addr + } + if got.EVM[c].ChainWriter.ForwarderAddress == nil { + got.EVM[c].ChainWriter.ForwarderAddress = &addr + } for n := range got.EVM[c].Nodes { if got.EVM[c].Nodes[n].WSURL == nil { got.EVM[c].Nodes[n].WSURL = new(commonconfig.URL) diff --git a/core/services/workflows/delegate.go b/core/services/workflows/delegate.go index 1e48e229da5..32307b9fb9e 100644 --- a/core/services/workflows/delegate.go +++ b/core/services/workflows/delegate.go @@ -2,6 +2,8 @@ package workflows import ( "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/logger" "github.com/smartcontractkit/chainlink/v2/core/services/job" "github.com/smartcontractkit/chainlink/v2/core/services/pg" @@ -35,6 +37,9 @@ func (d *Delegate) ServicesForSpec(spec job.Job) ([]job.ServiceCtx, error) { return []job.ServiceCtx{engine}, nil } -func NewDelegate(logger logger.Logger, registry types.CapabilitiesRegistry) *Delegate { +func NewDelegate(logger logger.Logger, registry types.CapabilitiesRegistry, legacyEVMChains legacyevm.LegacyChainContainer) *Delegate { + // NOTE: we temporarily do registration inside NewDelegate, this will be moved out of job specs in the future + _ = targets.InitializeWrite(registry, legacyEVMChains) + return &Delegate{logger: logger, registry: registry} } diff --git a/docs/CONFIG.md b/docs/CONFIG.md index 3bf68bbe6d0..2c4f3109421 100644 --- a/docs/CONFIG.md +++ b/docs/CONFIG.md @@ -6407,6 +6407,26 @@ GasLimit = 5400000 # Default ``` GasLimit controls the gas limit for transmit transactions from ocr2automation job. +## EVM.ChainWriter +```toml +[EVM.ChainWriter] +FromAddress = '0x2a3e23c6f242F5345320814aC8a1b4E58707D292' # Example +ForwarderAddress = '0x2a3e23c6f242F5345320814aC8a1b4E58707D292' # Example +``` + + +### FromAddress +```toml +FromAddress = '0x2a3e23c6f242F5345320814aC8a1b4E58707D292' # Example +``` +FromAddress is Address of the transmitter key to use for workflow writes. + +### ForwarderAddress +```toml +ForwarderAddress = '0x2a3e23c6f242F5345320814aC8a1b4E58707D292' # Example +``` +ForwarderAddress is the keystone forwarder contract address on chain. + ## Cosmos ```toml [[Cosmos]] diff --git a/flake.lock b/flake.lock index 9051b0252f6..bce30e58f58 100644 --- a/flake.lock +++ b/flake.lock @@ -1,12 +1,15 @@ { "nodes": { "flake-utils": { + "inputs": { + "systems": "systems" + }, "locked": { - "lastModified": 1659877975, - "narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=", + "lastModified": 1705309234, + "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", "owner": "numtide", "repo": "flake-utils", - "rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0", + "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", "type": "github" }, "original": { @@ -17,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1666109165, - "narHash": "sha256-BMLyNVkr0oONuq3lKlFCRVuYqF75CO68Z8EoCh81Zdk=", + "lastModified": 1707092692, + "narHash": "sha256-ZbHsm+mGk/izkWtT4xwwqz38fdlwu7nUUKXTOmm4SyE=", "owner": "nixos", "repo": "nixpkgs", - "rev": "32096899af23d49010bd8cf6a91695888d9d9e73", + "rev": "faf912b086576fd1a15fca610166c98d47bc667e", "type": "github" }, "original": { @@ -36,6 +39,21 @@ "flake-utils": "flake-utils", "nixpkgs": "nixpkgs" } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } } }, "root": "root", diff --git a/go.md b/go.md index 97330440a54..497f87d77b5 100644 --- a/go.md +++ b/go.md @@ -22,6 +22,8 @@ flowchart LR chainlink/v2 --> caigo click caigo href "https://github.com/smartcontractkit/caigo" + chainlink/v2 --> chain-selectors + click chain-selectors href "https://github.com/smartcontractkit/chain-selectors" chainlink/v2 --> chainlink-automation click chainlink-automation href "https://github.com/smartcontractkit/chainlink-automation" chainlink/v2 --> chainlink-common @@ -52,7 +54,6 @@ flowchart LR chainlink-cosmos --> chainlink-common chainlink-cosmos --> libocr chainlink-data-streams --> chain-selectors - click chain-selectors href "https://github.com/smartcontractkit/chain-selectors" chainlink-data-streams --> chainlink-common chainlink-data-streams --> libocr chainlink-feeds --> chainlink-common diff --git a/go.mod b/go.mod index 396201fdede..94c9c33f27d 100644 --- a/go.mod +++ b/go.mod @@ -66,6 +66,7 @@ require ( github.com/shirou/gopsutil/v3 v3.23.11 github.com/shopspring/decimal v1.3.1 github.com/smartcontractkit/caigo v0.0.0-20230621050857-b29a4ca8c704 + github.com/smartcontractkit/chain-selectors v1.0.10 github.com/smartcontractkit/chainlink-automation v1.0.2-0.20240118014648-1ab6a88c9429 github.com/smartcontractkit/chainlink-common v0.1.7-0.20240216174848-c7f1809138d6 github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240213120401-01a23955f9f8 diff --git a/go.sum b/go.sum index e02add4ee89..4073b235b7e 100644 --- a/go.sum +++ b/go.sum @@ -1162,6 +1162,8 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartcontractkit/caigo v0.0.0-20230621050857-b29a4ca8c704 h1:T3lFWumvbfM1u/etVq42Afwq/jtNSBSOA8n5jntnNPo= github.com/smartcontractkit/caigo v0.0.0-20230621050857-b29a4ca8c704/go.mod h1:2QuJdEouTWjh5BDy5o/vgGXQtR4Gz8yH1IYB5eT7u4M= +github.com/smartcontractkit/chain-selectors v1.0.10 h1:t9kJeE6B6G+hKD0GYR4kGJSCqR1LNS7aI3jT0V+xGrg= +github.com/smartcontractkit/chain-selectors v1.0.10/go.mod h1:d4Hi+E1zqjy9HqMkjBE5q1vcG9VGgxf5VxiRHfzi2kE= github.com/smartcontractkit/chainlink-automation v1.0.2-0.20240118014648-1ab6a88c9429 h1:xkejUBZhcBpBrTSfxc91Iwzadrb6SXw8ks69bHIQ9Ww= github.com/smartcontractkit/chainlink-automation v1.0.2-0.20240118014648-1ab6a88c9429/go.mod h1:wJmVvDf4XSjsahWtfUq3wvIAYEAuhr7oxmxYnEL/LGQ= github.com/smartcontractkit/chainlink-common v0.1.7-0.20240216174848-c7f1809138d6 h1:hpNkTpLtwWXKqguf7wYqetxpmxY/bSO+1PLpY8VBu2w= diff --git a/integration-tests/go.mod b/integration-tests/go.mod index ccb0810ab50..a1792a04617 100644 --- a/integration-tests/go.mod +++ b/integration-tests/go.mod @@ -369,6 +369,7 @@ require ( github.com/shopspring/decimal v1.3.1 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/smartcontractkit/caigo v0.0.0-20230621050857-b29a4ca8c704 // indirect + github.com/smartcontractkit/chain-selectors v1.0.10 // indirect github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20240213120401-01a23955f9f8 // indirect github.com/smartcontractkit/chainlink-data-streams v0.0.0-20240214203158-47dae5de1336 // indirect github.com/smartcontractkit/chainlink-feeds v0.0.0-20240119021347-3c541a78cdb8 // indirect diff --git a/integration-tests/go.sum b/integration-tests/go.sum index ed933fab701..e0f1e90c4a5 100644 --- a/integration-tests/go.sum +++ b/integration-tests/go.sum @@ -1501,6 +1501,8 @@ github.com/slack-go/slack v0.12.2 h1:x3OppyMyGIbbiyFhsBmpf9pwkUzMhthJMRNmNlA4LaQ github.com/slack-go/slack v0.12.2/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw= github.com/smartcontractkit/caigo v0.0.0-20230621050857-b29a4ca8c704 h1:T3lFWumvbfM1u/etVq42Afwq/jtNSBSOA8n5jntnNPo= github.com/smartcontractkit/caigo v0.0.0-20230621050857-b29a4ca8c704/go.mod h1:2QuJdEouTWjh5BDy5o/vgGXQtR4Gz8yH1IYB5eT7u4M= +github.com/smartcontractkit/chain-selectors v1.0.10 h1:t9kJeE6B6G+hKD0GYR4kGJSCqR1LNS7aI3jT0V+xGrg= +github.com/smartcontractkit/chain-selectors v1.0.10/go.mod h1:d4Hi+E1zqjy9HqMkjBE5q1vcG9VGgxf5VxiRHfzi2kE= github.com/smartcontractkit/chainlink-automation v1.0.2-0.20240118014648-1ab6a88c9429 h1:xkejUBZhcBpBrTSfxc91Iwzadrb6SXw8ks69bHIQ9Ww= github.com/smartcontractkit/chainlink-automation v1.0.2-0.20240118014648-1ab6a88c9429/go.mod h1:wJmVvDf4XSjsahWtfUq3wvIAYEAuhr7oxmxYnEL/LGQ= github.com/smartcontractkit/chainlink-common v0.1.7-0.20240216174848-c7f1809138d6 h1:hpNkTpLtwWXKqguf7wYqetxpmxY/bSO+1PLpY8VBu2w= diff --git a/shell.nix b/shell.nix index 7881af59ba2..7d219553368 100644 --- a/shell.nix +++ b/shell.nix @@ -1,9 +1,9 @@ { pkgs ? import { } }: with pkgs; let - go = go_1_19; + go = go_1_21; postgresql = postgresql_14; - nodejs = nodejs-16_x; + nodejs = nodejs-18_x; nodePackages = pkgs.nodePackages.override { inherit nodejs; }; in mkShell { @@ -11,15 +11,14 @@ mkShell { go postgresql + python3 python3Packages.pip + curl nodejs nodePackages.pnpm # TODO: compiler / gcc for secp compilation - nodePackages.ganache - # py3: web3 slither-analyzer crytic-compile - # echidna go-ethereum # geth # parity # openethereum go-mockery @@ -49,8 +48,4 @@ mkShell { PGDATA = "db"; CL_DATABASE_URL = "postgresql://chainlink:chainlink@localhost:5432/chainlink_test?sslmode=disable"; - shellHook = '' - export GOPATH=$HOME/go - export PATH=$GOPATH/bin:$PATH - ''; }