diff --git a/.github/actions/build-sign-publish-chainlink/action.yml b/.github/actions/build-sign-publish-chainlink/action.yml index fe4ef858f58..62add53092a 100644 --- a/.github/actions/build-sign-publish-chainlink/action.yml +++ b/.github/actions/build-sign-publish-chainlink/action.yml @@ -223,7 +223,7 @@ runs: - if: inputs.sign-images == 'true' name: Install cosign - uses: sigstore/cosign-installer@581838fbedd492d2350a9ecd427a95d6de1e5d01 # v2.1.0 + uses: sigstore/cosign-installer@11086d25041f77fe8fe7b9ea4e48e3b9192b8f19 # v3.1.2 with: cosign-release: "v1.6.0" diff --git a/.github/actions/goreleaser-build-sign-publish/action.yml b/.github/actions/goreleaser-build-sign-publish/action.yml index 845d2443fc1..b2d42c1234e 100644 --- a/.github/actions/goreleaser-build-sign-publish/action.yml +++ b/.github/actions/goreleaser-build-sign-publish/action.yml @@ -84,7 +84,7 @@ runs: version: ${{ inputs.zig-version }} - name: Setup cosign if: inputs.enable-cosign == 'true' - uses: sigstore/cosign-installer@581838fbedd492d2350a9ecd427a95d6de1e5d01 # v2.1.0 + uses: sigstore/cosign-installer@11086d25041f77fe8fe7b9ea4e48e3b9192b8f19 # v3.1.2 with: cosign-release: ${{ inputs.cosign-version }} - name: Login to docker registry diff --git a/.github/workflows/automation-benchmark-tests.yml b/.github/workflows/automation-benchmark-tests.yml index 7bdb66c919e..a4338d642bc 100644 --- a/.github/workflows/automation-benchmark-tests.yml +++ b/.github/workflows/automation-benchmark-tests.yml @@ -57,7 +57,7 @@ jobs: id-token: write contents: read name: ${{ inputs.network }} Automation Benchmark Test - runs-on: ubuntu-latest + runs-on: ubuntu20.04-16cores-64GB env: SELECTED_NETWORKS: ${{ inputs.network }} SLACK_API_KEY: ${{ secrets.QA_SLACK_API_KEY }} diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index f2092ee9072..fc7ce071c89 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -201,8 +201,6 @@ jobs: cl_repo: ${{ env.CHAINLINK_IMAGE }} cl_image_tag: ${{ github.sha }} aws_registries: ${{ secrets.QA_AWS_ACCOUNT_NUMBER }} - dockerhub_username: ${{ secrets.DOCKERHUB_READONLY_USERNAME }} - dockerhub_password: ${{ secrets.DOCKERHUB_READONLY_PASSWORD }} artifacts_location: ./integration-tests/smoke/logs/ publish_check_name: ${{ matrix.product.name }} token: ${{ secrets.GITHUB_TOKEN }} @@ -402,8 +400,6 @@ jobs: cl_repo: ${{ env.CHAINLINK_IMAGE }} cl_image_tag: ${{ github.sha }}${{ matrix.product.tag_suffix }} aws_registries: ${{ secrets.QA_AWS_ACCOUNT_NUMBER }} - dockerhub_username: ${{ secrets.DOCKERHUB_READONLY_USERNAME }} - dockerhub_password: ${{ secrets.DOCKERHUB_READONLY_PASSWORD }} artifacts_name: ${{ matrix.product.name }}-test-logs artifacts_location: ./integration-tests/smoke/logs/ publish_check_name: ${{ matrix.product.name }} diff --git a/.github/workflows/on-demand-ocr-soak-test.yml b/.github/workflows/on-demand-ocr-soak-test.yml index 1e510c23be3..b6ccad22467 100644 --- a/.github/workflows/on-demand-ocr-soak-test.yml +++ b/.github/workflows/on-demand-ocr-soak-test.yml @@ -28,6 +28,8 @@ on: - "FANTOM_MAINNET" - "KROMA_MAINNET" - "KROMA_SEPOLIA" + - "WEMIX_TESTNET" + - "WEMIX_MAINNET" fundingPrivateKey: description: Private funding key (Skip for Simulated) required: false diff --git a/contracts/GNUmakefile b/contracts/GNUmakefile index b477164a496..e41d6422c2f 100644 --- a/contracts/GNUmakefile +++ b/contracts/GNUmakefile @@ -34,7 +34,7 @@ abigen: ## Build & install abigen. .PHONY: mockery mockery: $(mockery) ## Install mockery. - go install github.com/vektra/mockery/v2@v2.28.1 + go install github.com/vektra/mockery/v2@v2.35.4 .PHONY: foundry foundry: ## Install foundry. diff --git a/core/chains/evm/client/errors.go b/core/chains/evm/client/errors.go index 0d177455e33..4cb505dc9eb 100644 --- a/core/chains/evm/client/errors.go +++ b/core/chains/evm/client/errors.go @@ -207,7 +207,23 @@ var harmony = ClientErrors{ Fatal: harmonyFatal, } -var clients = []ClientErrors{parity, geth, arbitrum, metis, substrate, avalanche, nethermind, harmony, besu, erigon, klaytn, celo} +var zkSync = ClientErrors{ + NonceTooLow: regexp.MustCompile(`(?:: |^)nonce too low\..+actual: \d*$`), + NonceTooHigh: regexp.MustCompile(`(?:: |^)nonce too high\..+actual: \d*$`), + TerminallyUnderpriced: regexp.MustCompile(`(?:: |^)max fee per gas less than block base fee$`), + InsufficientEth: regexp.MustCompile(`(?:: |^)(?:insufficient balance for transfer$|insufficient funds for gas + value)`), + TxFeeExceedsCap: regexp.MustCompile(`(?:: |^)max priority fee per gas higher than max fee per gas$`), + // intrinsic gas too low - gas limit less than 14700 + // Not enough gas for transaction validation - gas limit less than L2 fee + // Failed to pay the fee to the operator - gas limit less than L2+L1 fee + // Error function_selector = 0x, data = 0x - contract call with gas limit of 0 + // can't start a transaction from a non-account - trying to send from an invalid address, e.g. estimating a contract -> contract tx + // max fee per gas higher than 2^64-1 - uint64 overflow + // oversized data - data too large + Fatal: regexp.MustCompile(`(?:: |^)(?:exceeds block gas limit|intrinsic gas too low|Not enough gas for transaction validation|Failed to pay the fee to the operator|Error function_selector = 0x, data = 0x|invalid sender. can't start a transaction from a non-account|max(?: priority)? fee per (?:gas|pubdata byte) higher than 2\^64-1|oversized data. max: \d+; actual: \d+)$`), +} + +var clients = []ClientErrors{parity, geth, arbitrum, metis, substrate, avalanche, nethermind, harmony, besu, erigon, klaytn, celo, zkSync} func (s *SendError) is(errorType int) bool { if s == nil || s.err == nil { diff --git a/core/chains/evm/client/errors_test.go b/core/chains/evm/client/errors_test.go index a5a3cc15eb6..ad8079824ab 100644 --- a/core/chains/evm/client/errors_test.go +++ b/core/chains/evm/client/errors_test.go @@ -40,6 +40,7 @@ func Test_Eth_Errors(t *testing.T) { {"call failed: nonce too low: address 0x0499BEA33347cb62D79A9C0b1EDA01d8d329894c current nonce (5833) > tx nonce (5511)", true, "Avalanche"}, {"call failed: OldNonce", true, "Nethermind"}, {"call failed: OldNonce, Current nonce: 22, nonce of rejected tx: 17", true, "Nethermind"}, + {"nonce too low. allowed nonce range: 427 - 447, actual: 426", true, "zkSync"}, } for _, test := range tests { @@ -60,6 +61,7 @@ func Test_Eth_Errors(t *testing.T) { {"nonce too high: address 0x336394A3219e71D9d9bd18201d34E95C1Bb7122C, tx: 8089 state: 8090", true, "Arbitrum"}, {"nonce too high", true, "Geth"}, {"nonce too high", true, "Erigon"}, + {"nonce too high. allowed nonce range: 427 - 477, actual: 527", true, "zkSync"}, } for _, test := range tests { @@ -152,6 +154,7 @@ func Test_Eth_Errors(t *testing.T) { {"FeeTooLowToCompete", true, "Nethermind"}, {"transaction underpriced", true, "Klaytn"}, {"intrinsic gas too low", true, "Klaytn"}, + {"max fee per gas less than block base fee", true, "zkSync"}, } for _, test := range tests { @@ -194,6 +197,8 @@ func Test_Eth_Errors(t *testing.T) { {"call failed: InsufficientFunds, Account balance: 4740799397601480913, cumulative cost: 22019342038993800000", true, "Nethermind"}, {"insufficient funds", true, "Klaytn"}, {"insufficient funds for gas * price + value + gatewayFee", true, "celo"}, + {"insufficient balance for transfer", true, "zkSync"}, + {"insufficient funds for gas + value. balance: 42719769622667482000, fee: 48098250000000, value: 42719769622667482000", true, "celo"}, } for _, test := range tests { err = evmclient.NewSendErrorS(test.message) @@ -213,6 +218,7 @@ func Test_Eth_Errors(t *testing.T) { {"invalid gas fee cap", true, "Klaytn"}, {"max fee per gas higher than max priority fee per gas", true, "Klaytn"}, {"tx fee (1.10 of currency celo) exceeds the configured cap (1.00 celo)", true, "celo"}, + {"max priority fee per gas higher than max fee per gas", true, "zkSync"}, } for _, test := range tests { err = evmclient.NewSendErrorS(test.message) @@ -329,6 +335,16 @@ func Test_Eth_Errors_Fatal(t *testing.T) { {"`to` address of transaction in blacklist", true, "Harmony"}, {"`from` address of transaction in blacklist", true, "Harmony"}, {"staking message does not match directive message", true, "Harmony"}, + + {"intrinsic gas too low", true, "zkSync"}, + {"failed to validate the transaction. reason: Validation revert: Account validation error: Not enough gas for transaction validation", true, "zkSync"}, + {"failed to validate the transaction. reason: Validation revert: Failed to pay for the transaction: Failed to pay the fee to the operator", true, "zkSync"}, + {"failed to validate the transaction. reason: Validation revert: Account validation error: Error function_selector = 0x, data = 0x", true, "zkSync"}, + {"invalid sender. can't start a transaction from a non-account", true, "zkSync"}, + {"Failed to serialize transaction: max fee per gas higher than 2^64-1", true, "zkSync"}, + {"Failed to serialize transaction: max fee per pubdata byte higher than 2^64-1", true, "zkSync"}, + {"Failed to serialize transaction: max priority fee per gas higher than 2^64-1", true, "zkSync"}, + {"Failed to serialize transaction: oversized data. max: 1000000; actual: 1000000", true, "zkSync"}, } for _, test := range tests { diff --git a/core/chains/evm/client/simulated_backend_client.go b/core/chains/evm/client/simulated_backend_client.go index 78239089676..d542e98e6eb 100644 --- a/core/chains/evm/client/simulated_backend_client.go +++ b/core/chains/evm/client/simulated_backend_client.go @@ -25,6 +25,41 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/utils" ) +func init() { + var err error + + balanceOfABI, err = abi.JSON(strings.NewReader(balanceOfABIString)) + if err != nil { + panic(fmt.Errorf("%w: while parsing erc20ABI", err)) + } +} + +var ( + balanceOfABIString = `[ + { + "constant": true, + "inputs": [ + { + "name": "_owner", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "name": "balance", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + } +]` + + balanceOfABI abi.ABI +) + // SimulatedBackendClient is an Client implementation using a simulated // blockchain backend. Note that not all RPC methods are implemented here. type SimulatedBackendClient struct { @@ -51,69 +86,6 @@ func (c *SimulatedBackendClient) Dial(context.Context) error { // other simulated clients might still be using it func (c *SimulatedBackendClient) Close() {} -// checkEthCallArgs extracts and verifies the arguments for an eth_call RPC -func (c *SimulatedBackendClient) checkEthCallArgs( - args []interface{}) (*CallArgs, *big.Int, error) { - if len(args) != 2 { - return nil, nil, fmt.Errorf( - "should have two arguments after \"eth_call\", got %d", len(args)) - } - callArgs, ok := args[0].(map[string]interface{}) - if !ok { - return nil, nil, fmt.Errorf("third arg to SimulatedBackendClient.Call "+ - "must be an eth.CallArgs, got %+#v", args[0]) - } - blockNumber, err := c.blockNumber(args[1]) - if err != nil { - return nil, nil, fmt.Errorf("fourth arg to SimulatedBackendClient.Call "+ - "must be the string \"latest\", or a *big.Int, got %#+v", args[1]) - } - - // to and from need to map to a common.Address but could come in as a string - var ( - toAddr common.Address - frmAddr common.Address - ) - - toAddr, err = interfaceToAddress(callArgs["to"]) - if err != nil { - return nil, nil, err - } - - // from is optional in the standard client; default to 0x when missing - if value, ok := callArgs["from"]; ok { - addr, err := interfaceToAddress(value) - if err != nil { - return nil, nil, err - } - - frmAddr = addr - } else { - frmAddr = common.HexToAddress("0x") - } - - ca := CallArgs{ - To: toAddr, - From: frmAddr, - Data: callArgs["data"].(hexutil.Bytes), - } - - return &ca, blockNumber, nil -} - -func interfaceToAddress(value interface{}) (common.Address, error) { - switch v := value.(type) { - case common.Address: - return v, nil - case string: - return common.HexToAddress(v), nil - case *big.Int: - return common.BigToAddress(v), nil - default: - return common.HexToAddress("0x"), fmt.Errorf("unrecognized value type for converting value to common.Address; try string, *big.Int, or common.Address") - } -} - // CallContext mocks the ethereum client RPC calls used by chainlink, copying the // return value into result. // The simulated client avoids the old block error from the simulated backend by @@ -121,41 +93,16 @@ func interfaceToAddress(value interface{}) (common.Address, error) { // and will not return an error when an old block is used. func (c *SimulatedBackendClient) CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error { switch method { + case "eth_getTransactionReceipt": + return c.ethGetTransactionReceipt(ctx, result, args...) + case "eth_getBlockByNumber": + return c.ethGetBlockByNumber(ctx, result, args...) case "eth_call": - var ( - callArgs *CallArgs - b []byte - err error - ) - - if callArgs, _, err = c.checkEthCallArgs(args); err != nil { - return err - } - - callMsg := ethereum.CallMsg{From: callArgs.From, To: &callArgs.To, Data: callArgs.Data} - - if b, err = c.b.CallContract(ctx, callMsg, nil /* always latest block */); err != nil { - return fmt.Errorf("%w: while calling contract at address %x with "+ - "data %x", err, callArgs.To, callArgs.Data) - } - - switch r := result.(type) { - case *hexutil.Bytes: - *r = append(*r, b...) - - if !bytes.Equal(*r, b) { - return fmt.Errorf("was passed a non-empty array, or failed to copy "+ - "answer. Expected %x = %x", *r, b) - } - return nil - default: - return fmt.Errorf("first arg to SimulatedBackendClient.Call is an "+ - "unrecognized type: %T; add processing logic for it here", result) - } + return c.ethCall(ctx, result, args...) + case "eth_getHeaderByNumber": + return c.ethGetHeaderByNumber(ctx, result, args...) default: - return fmt.Errorf("second arg to SimulatedBackendClient.Call is an RPC "+ - "API method which has not yet been implemented: %s. Add processing for "+ - "it here", method) + return fmt.Errorf("second arg to SimulatedBackendClient.Call is an RPC API method which has not yet been implemented: %s. Add processing for it here", method) } } @@ -175,38 +122,6 @@ func (c *SimulatedBackendClient) currentBlockNumber() *big.Int { return c.b.Blockchain().CurrentBlock().Number } -var balanceOfABIString = `[ - { - "constant": true, - "inputs": [ - { - "name": "_owner", - "type": "address" - } - ], - "name": "balanceOf", - "outputs": [ - { - "name": "balance", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - } -]` - -var balanceOfABI abi.ABI - -func init() { - var err error - balanceOfABI, err = abi.JSON(strings.NewReader(balanceOfABIString)) - if err != nil { - panic(fmt.Errorf("%w: while parsing erc20ABI", err)) - } -} - func (c *SimulatedBackendClient) TokenBalance(ctx context.Context, address common.Address, contractAddress common.Address) (balance *big.Int, err error) { callData, err := balanceOfABI.Pack("balanceOf", address) if err != nil { @@ -251,13 +166,12 @@ func (c *SimulatedBackendClient) blockNumber(number interface{}) (blockNumber *b case "earliest": return big.NewInt(0), nil case "pending": - panic("not implemented") // I don't understand the semantics of this. + panic("pending block not supported by simulated backend client") // I don't understand the semantics of this. // return big.NewInt(0).Add(c.currentBlockNumber(), big.NewInt(1)), nil default: - blockNumber, err = utils.HexToUint256(n) + blockNumber, err := hexutil.DecodeBig(n) if err != nil { - return nil, fmt.Errorf("%w: while parsing '%s' as hex-encoded"+ - "block number", err, n) + return nil, fmt.Errorf("%w: while parsing '%s' as hex-encoded block number", err, n) } return blockNumber, nil } @@ -521,114 +435,18 @@ func (c *SimulatedBackendClient) BatchCallContext(ctx context.Context, b []rpc.B for i, elem := range b { switch elem.Method { case "eth_getTransactionReceipt": - if _, ok := elem.Result.(*evmtypes.Receipt); !ok { - return fmt.Errorf("SimulatedBackendClient expected return type of *evmtypes.Receipt for eth_getTransactionReceipt, got type %T", elem.Result) - } - if len(elem.Args) != 1 { - return fmt.Errorf("SimulatedBackendClient expected 1 arg, got %d for eth_getTransactionReceipt", len(elem.Args)) - } - hash, is := elem.Args[0].(common.Hash) - if !is { - return fmt.Errorf("SimulatedBackendClient expected arg to be a hash, got: %T", elem.Args[0]) - } - receipt, err := c.b.TransactionReceipt(ctx, hash) - if receipt != nil { - *(b[i].Result.(*evmtypes.Receipt)) = *evmtypes.FromGethReceipt(receipt) - } - b[i].Error = err + b[i].Error = c.ethGetTransactionReceipt(ctx, b[i].Result, b[i].Args...) case "eth_getBlockByNumber": - switch v := elem.Result.(type) { - case *evmtypes.Head: - case *evmtypes.Block: - default: - return fmt.Errorf("SimulatedBackendClient expected return type of [*evmtypes.Head] or [*evmtypes.Block] for eth_getBlockByNumber, got type %T", v) - } - if len(elem.Args) != 2 { - return fmt.Errorf("SimulatedBackendClient expected 2 args, got %d for eth_getBlockByNumber", len(elem.Args)) - } - blockNumOrTag, is := elem.Args[0].(string) - if !is { - return fmt.Errorf("SimulatedBackendClient expected first arg to be a string for eth_getBlockByNumber, got: %T", elem.Args[0]) - } - _, is = elem.Args[1].(bool) - if !is { - return fmt.Errorf("SimulatedBackendClient expected second arg to be a boolean for eth_getBlockByNumber, got: %T", elem.Args[1]) - } - header, err := c.fetchHeader(ctx, blockNumOrTag) - if err != nil { - return err - } - switch res := elem.Result.(type) { - case *evmtypes.Head: - res.Number = header.Number.Int64() - res.Hash = header.Hash() - res.ParentHash = header.ParentHash - res.Timestamp = time.Unix(int64(header.Time), 0).UTC() - case *evmtypes.Block: - res.Number = header.Number.Int64() - res.Hash = header.Hash() - res.ParentHash = header.ParentHash - res.Timestamp = time.Unix(int64(header.Time), 0).UTC() - default: - return fmt.Errorf("SimulatedBackendClient Unexpected Type %T", elem.Result) - } - b[i].Error = err + b[i].Error = c.ethGetBlockByNumber(ctx, b[i].Result, b[i].Args...) case "eth_call": - if len(elem.Args) != 2 { - return fmt.Errorf("SimulatedBackendClient expected 2 args, got %d for eth_call", len(elem.Args)) - } - - _, ok := elem.Result.(*string) - if !ok { - return fmt.Errorf("SimulatedBackendClient expected result to be *string for eth_call, got: %T", elem.Result) - } - - params, ok := elem.Args[0].(map[string]interface{}) - if !ok { - return fmt.Errorf("SimulatedBackendClient expected first arg to be map[string]interface{} for eth_call, got: %T", elem.Args[0]) - } - - blockNum, ok := elem.Args[1].(string) - if !ok { - return fmt.Errorf("SimulatedBackendClient expected second arg to be a string for eth_call, got: %T", elem.Args[1]) - } - - if blockNum != "" { - if _, ok = new(big.Int).SetString(blockNum, 0); !ok { - return fmt.Errorf("error while converting block number string: %s to big.Int ", blockNum) - } - } - - callMsg := toCallMsg(params) - resp, err := c.b.CallContract(ctx, callMsg, nil) - *(b[i].Result.(*string)) = hexutil.Encode(resp) - b[i].Error = err + b[i].Error = c.ethCall(ctx, b[i].Result, b[i].Args...) case "eth_getHeaderByNumber": - if len(elem.Args) != 1 { - return fmt.Errorf("SimulatedBackendClient expected 2 args, got %d for eth_getHeaderByNumber", len(elem.Args)) - } - blockNum, is := elem.Args[0].(string) - if !is { - return fmt.Errorf("SimulatedBackendClient expected first arg to be a string for eth_getHeaderByNumber, got: %T", elem.Args[0]) - } - n, err := hexutil.DecodeBig(blockNum) - if err != nil { - return fmt.Errorf("error while converting hex block number %s to big.Int ", blockNum) - } - header, err := c.b.HeaderByNumber(ctx, n) - if err != nil { - return err - } - switch v := elem.Result.(type) { - case *types.Header: - b[i].Result = header - default: - return fmt.Errorf("SimulatedBackendClient Unexpected Type %T", v) - } + b[i].Error = c.ethGetHeaderByNumber(ctx, b[i].Result, b[i].Args...) default: return fmt.Errorf("SimulatedBackendClient got unsupported method %s", elem.Method) } } + return nil } @@ -655,32 +473,175 @@ func (c *SimulatedBackendClient) Commit() common.Hash { return c.b.Commit() } -func toCallMsg(params map[string]interface{}) ethereum.CallMsg { - var callMsg ethereum.CallMsg +func (c *SimulatedBackendClient) IsL2() bool { + return false +} - switch to := params["to"].(type) { - case string: - toAddr := common.HexToAddress(to) - callMsg.To = &toAddr - case common.Address: - callMsg.To = &to - case *common.Address: - callMsg.To = to +func (c *SimulatedBackendClient) fetchHeader(ctx context.Context, blockNumOrTag string) (*types.Header, error) { + switch blockNumOrTag { + case rpc.SafeBlockNumber.String(): + return c.b.Blockchain().CurrentSafeBlock(), nil + case rpc.LatestBlockNumber.String(): + return c.b.Blockchain().CurrentHeader(), nil + case rpc.FinalizedBlockNumber.String(): + return c.b.Blockchain().CurrentFinalBlock(), nil default: - panic("unexpected type of 'to' parameter") + blockNum, ok := new(big.Int).SetString(blockNumOrTag, 0) + if !ok { + return nil, fmt.Errorf("error while converting block number string: %s to big.Int ", blockNumOrTag) + } + return c.b.HeaderByNumber(ctx, blockNum) } +} - switch from := params["from"].(type) { - case nil: - // This parameter is not required so nil is acceptable - case string: - callMsg.From = common.HexToAddress(from) - case common.Address: - callMsg.From = from - case *common.Address: - callMsg.From = *from +func (c *SimulatedBackendClient) ethGetTransactionReceipt(ctx context.Context, result interface{}, args ...interface{}) error { + if len(args) != 1 { + return fmt.Errorf("SimulatedBackendClient expected 1 arg, got %d for eth_getTransactionReceipt", len(args)) + } + + hash, is := args[0].(common.Hash) + if !is { + return fmt.Errorf("SimulatedBackendClient expected arg to be a hash, got: %T", args[0]) + } + + receipt, err := c.b.TransactionReceipt(ctx, hash) + if err != nil { + return err + } + + // strongly typing the result here has the consequence of not being flexible in + // custom types where a real-world RPC client would allow for custom types with + // custom marshalling. + switch typed := result.(type) { + case *types.Receipt: + *typed = *receipt + case *evmtypes.Receipt: + *typed = *evmtypes.FromGethReceipt(receipt) + default: + return fmt.Errorf("SimulatedBackendClient expected return type of *evmtypes.Receipt for eth_getTransactionReceipt, got type %T", result) + } + + return nil +} + +func (c *SimulatedBackendClient) ethGetBlockByNumber(ctx context.Context, result interface{}, args ...interface{}) error { + if len(args) != 2 { + return fmt.Errorf("SimulatedBackendClient expected 2 args, got %d for eth_getBlockByNumber", len(args)) + } + + blockNumOrTag, is := args[0].(string) + if !is { + return fmt.Errorf("SimulatedBackendClient expected first arg to be a string for eth_getBlockByNumber, got: %T", args[0]) + } + + _, is = args[1].(bool) + if !is { + return fmt.Errorf("SimulatedBackendClient expected second arg to be a boolean for eth_getBlockByNumber, got: %T", args[1]) + } + + header, err := c.fetchHeader(ctx, blockNumOrTag) + if err != nil { + return err + } + + switch res := result.(type) { + case *evmtypes.Head: + res.Number = header.Number.Int64() + res.Hash = header.Hash() + res.ParentHash = header.ParentHash + res.Timestamp = time.Unix(int64(header.Time), 0).UTC() + case *evmtypes.Block: + res.Number = header.Number.Int64() + res.Hash = header.Hash() + res.ParentHash = header.ParentHash + res.Timestamp = time.Unix(int64(header.Time), 0).UTC() + default: + return fmt.Errorf("SimulatedBackendClient Unexpected Type %T", res) + } + + return nil +} + +func (c *SimulatedBackendClient) ethCall(ctx context.Context, result interface{}, args ...interface{}) error { + if len(args) != 2 { + return fmt.Errorf("SimulatedBackendClient expected 2 args, got %d for eth_call", len(args)) + } + + params, ok := args[0].(map[string]interface{}) + if !ok { + return fmt.Errorf("SimulatedBackendClient expected first arg to be map[string]interface{} for eth_call, got: %T", args[0]) + } + + if _, err := c.blockNumber(args[1]); err != nil { + return fmt.Errorf("SimulatedBackendClient expected second arg to be the string 'latest' or a *big.Int for eth_call, got: %T", args[1]) + } + + resp, err := c.b.CallContract(ctx, toCallMsg(params), nil /* always latest block on simulated backend */) + if err != nil { + return err + } + + switch typedResult := result.(type) { + case *hexutil.Bytes: + *typedResult = append(*typedResult, resp...) + + if !bytes.Equal(*typedResult, resp) { + return fmt.Errorf("SimulatedBackendClient was passed a non-empty array, or failed to copy answer. Expected %x = %x", *typedResult, resp) + } + case *string: + *typedResult = hexutil.Encode(resp) default: - panic("unexpected type of 'from' parameter") + return fmt.Errorf("SimulatedBackendClient unexpected type %T", result) + } + + return nil +} + +func (c *SimulatedBackendClient) ethGetHeaderByNumber(ctx context.Context, result interface{}, args ...interface{}) error { + if len(args) != 1 { + return fmt.Errorf("SimulatedBackendClient expected 1 arg, got %d for eth_getHeaderByNumber", len(args)) + } + + blockNumber, err := c.blockNumber(args[0]) + if err != nil { + return fmt.Errorf("SimulatedBackendClient expected first arg to be a string for eth_getHeaderByNumber: %w", err) + } + + header, err := c.b.HeaderByNumber(ctx, blockNumber) + if err != nil { + return err + } + + switch typedResult := result.(type) { + case *types.Header: + *typedResult = *header + default: + return fmt.Errorf("SimulatedBackendClient unexpected Type %T", typedResult) + } + + return nil +} + +func toCallMsg(params map[string]interface{}) ethereum.CallMsg { + var callMsg ethereum.CallMsg + + toAddr, err := interfaceToAddress(params["to"]) + if err != nil { + panic(fmt.Errorf("unexpected 'to' parameter: %s", err)) + } + + callMsg.To = &toAddr + + // from is optional in the standard client; default to 0x when missing + if value, ok := params["from"]; ok { + addr, err := interfaceToAddress(value) + if err != nil { + panic(fmt.Errorf("unexpected 'from' parameter: %s", err)) + } + + callMsg.From = addr + } else { + callMsg.From = common.HexToAddress("0x") } switch data := params["data"].(type) { @@ -691,7 +652,7 @@ func toCallMsg(params map[string]interface{}) ethereum.CallMsg { case []byte: callMsg.Data = data default: - panic("unexpected type of 'data' parameter") + panic("unexpected type of 'data' parameter; try hexutil.Bytes, []byte, or nil") } if value, ok := params["value"].(*big.Int); ok { @@ -709,23 +670,23 @@ func toCallMsg(params map[string]interface{}) ethereum.CallMsg { return callMsg } -func (c *SimulatedBackendClient) IsL2() bool { - return false -} +func interfaceToAddress(value interface{}) (common.Address, error) { + switch v := value.(type) { + case common.Address: + return v, nil + case string: + if ok := common.IsHexAddress(v); !ok { + return common.Address{}, fmt.Errorf("string not formatted as a hex encoded evm address") + } -func (c *SimulatedBackendClient) fetchHeader(ctx context.Context, blockNumOrTag string) (*types.Header, error) { - switch blockNumOrTag { - case rpc.SafeBlockNumber.String(): - return c.b.Blockchain().CurrentSafeBlock(), nil - case rpc.LatestBlockNumber.String(): - return c.b.Blockchain().CurrentHeader(), nil - case rpc.FinalizedBlockNumber.String(): - return c.b.Blockchain().CurrentFinalBlock(), nil - default: - blockNum, ok := new(big.Int).SetString(blockNumOrTag, 0) - if !ok { - return nil, fmt.Errorf("error while converting block number string: %s to big.Int ", blockNumOrTag) + return common.HexToAddress(v), nil + case *big.Int: + if v.Uint64() > 0 || len(v.Bytes()) > 20 { + return common.Address{}, fmt.Errorf("invalid *big.Int; value must be larger than 0 with a byte length <= 20") } - return c.b.HeaderByNumber(ctx, blockNum) + + return common.BigToAddress(v), nil + default: + return common.Address{}, fmt.Errorf("unrecognized value type for converting value to common.Address; use hex encoded string, *big.Int, or common.Address") } } diff --git a/core/chains/evm/config/toml/defaults/WeMix_Mainnet.toml b/core/chains/evm/config/toml/defaults/WeMix_Mainnet.toml new file mode 100644 index 00000000000..ee50a9844a4 --- /dev/null +++ b/core/chains/evm/config/toml/defaults/WeMix_Mainnet.toml @@ -0,0 +1,14 @@ +ChainID = '1111' +ChainType = 'wemix' +FinalityDepth = 1 +MinIncomingConfirmations = 1 +# WeMix emits a block every 1 second, regardless of transactions +LogPollInterval = '3s' +NoNewHeadsThreshold = '30s' + +[OCR] +ContractConfirmations = 1 + +[GasEstimator] +EIP1559DynamicFees = true +TipCapDefault = '100 gwei' diff --git a/core/chains/evm/config/toml/defaults/WeMix_Testnet.toml b/core/chains/evm/config/toml/defaults/WeMix_Testnet.toml new file mode 100644 index 00000000000..6cdb451eb1d --- /dev/null +++ b/core/chains/evm/config/toml/defaults/WeMix_Testnet.toml @@ -0,0 +1,14 @@ +ChainID = '1112' +ChainType = 'wemix' +FinalityDepth = 1 +MinIncomingConfirmations = 1 +# WeMix emits a block every 1 second, regardless of transactions +LogPollInterval = '3s' +NoNewHeadsThreshold = '30s' + +[OCR] +ContractConfirmations = 1 + +[GasEstimator] +EIP1559DynamicFees = true +TipCapDefault = '100 gwei' diff --git a/core/chains/evm/config/toml/defaults/zkSync_Goerli.toml b/core/chains/evm/config/toml/defaults/zkSync_Goerli.toml new file mode 100644 index 00000000000..04529a41b81 --- /dev/null +++ b/core/chains/evm/config/toml/defaults/zkSync_Goerli.toml @@ -0,0 +1,14 @@ +ChainID = '280' +ChainType = 'zksync' +FinalityDepth = 1 +LogPollInterval = '5s' +MinIncomingConfirmations = 1 +NoNewHeadsThreshold = '1m' + +[GasEstimator] +LimitDefault = 3_500_000 +PriceMax = 18446744073709551615 +PriceMin = 0 + +[HeadTracker] +HistoryDepth = 5 diff --git a/core/chains/evm/config/toml/defaults/zkSync_Mainnet.toml b/core/chains/evm/config/toml/defaults/zkSync_Mainnet.toml new file mode 100644 index 00000000000..d7808edd15f --- /dev/null +++ b/core/chains/evm/config/toml/defaults/zkSync_Mainnet.toml @@ -0,0 +1,14 @@ +ChainID = '324' +ChainType = 'zksync' +FinalityDepth = 1 +LogPollInterval = '5s' +MinIncomingConfirmations = 1 +NoNewHeadsThreshold = '1m' + +[GasEstimator] +LimitDefault = 3_500_000 +PriceMax = 18446744073709551615 +PriceMin = 0 + +[HeadTracker] +HistoryDepth = 5 diff --git a/core/chains/evm/gas/block_history_estimator_test.go b/core/chains/evm/gas/block_history_estimator_test.go index 7f4d157e37a..c8b193c4435 100644 --- a/core/chains/evm/gas/block_history_estimator_test.go +++ b/core/chains/evm/gas/block_history_estimator_test.go @@ -23,6 +23,7 @@ import ( evmclient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas" evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" + "github.com/smartcontractkit/chainlink/v2/core/config" "github.com/smartcontractkit/chainlink/v2/core/internal/cltest" "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/evmtest" @@ -1329,6 +1330,12 @@ func TestBlockHistoryEstimator_IsUsable(t *testing.T) { assert.Equal(t, true, bhe.IsUsable(tx2, block, cfg.ChainType(), geCfg.PriceMin(), logger.TestLogger(t))) }) + t.Run("returns false if transaction is of type 0x16 only on WeMix", func(t *testing.T) { + cfg.ChainTypeF = "wemix" + tx := evmtypes.Transaction{Type: 0x16, GasPrice: assets.NewWeiI(10), GasLimit: 42, Hash: utils.NewHash()} + assert.Equal(t, false, bhe.IsUsable(tx, block, cfg.ChainType(), geCfg.PriceMin(), logger.TestLogger(t))) + }) + t.Run("returns false if transaction has base fee higher than the gas price only on Celo", func(t *testing.T) { cfg.ChainTypeF = "celo" tx := evmtypes.Transaction{Type: 0x0, GasPrice: assets.NewWeiI(10), GasLimit: 42, Hash: utils.NewHash()} @@ -1342,6 +1349,21 @@ func TestBlockHistoryEstimator_IsUsable(t *testing.T) { assert.Equal(t, true, bhe.IsUsable(tx, block, cfg.ChainType(), geCfg.PriceMin(), logger.TestLogger(t))) assert.Equal(t, true, bhe.IsUsable(tx2, block, cfg.ChainType(), geCfg.PriceMin(), logger.TestLogger(t))) }) + + t.Run("returns false if transaction is of type 0x71 or 0xff only on zkSync", func(t *testing.T) { + cfg.ChainTypeF = string(config.ChainZkSync) + tx := evmtypes.Transaction{Type: 0x71, GasPrice: assets.NewWeiI(10), GasLimit: 42, Hash: utils.NewHash()} + assert.Equal(t, false, bhe.IsUsable(tx, block, cfg.ChainType(), geCfg.PriceMin(), logger.TestLogger(t))) + + tx.Type = 0x02 + assert.Equal(t, true, bhe.IsUsable(tx, block, cfg.ChainType(), geCfg.PriceMin(), logger.TestLogger(t))) + + tx.Type = 0xff + assert.Equal(t, false, bhe.IsUsable(tx, block, cfg.ChainType(), geCfg.PriceMin(), logger.TestLogger(t))) + + cfg.ChainTypeF = "" + assert.Equal(t, true, bhe.IsUsable(tx, block, cfg.ChainType(), geCfg.PriceMin(), logger.TestLogger(t))) + }) } func TestBlockHistoryEstimator_EffectiveTipCap(t *testing.T) { diff --git a/core/chains/evm/gas/chain_specific.go b/core/chains/evm/gas/chain_specific.go index 4d87b8b454e..4f0d2e6b2f8 100644 --- a/core/chains/evm/gas/chain_specific.go +++ b/core/chains/evm/gas/chain_specific.go @@ -42,5 +42,19 @@ func chainSpecificIsUsable(tx evmtypes.Transaction, baseFee *assets.Wei, chainTy return false } } + if chainType == config.ChainWeMix { + // WeMix specific transaction types that enables fee delegation. + // https://docs.wemix.com/v/en/design/fee-delegation + if tx.Type == 0x16 { + return false + } + } + if chainType == config.ChainZkSync { + // zKSync specific type for contract deployment & priority transactions + // https://era.zksync.io/docs/reference/concepts/transactions.html#eip-712-0x71 + if tx.Type == 0x71 || tx.Type == 0xff { + return false + } + } return true } diff --git a/core/chains/evm/txmgr/transmitchecker.go b/core/chains/evm/txmgr/transmitchecker.go index 4636b708489..eb6edd3f587 100644 --- a/core/chains/evm/txmgr/transmitchecker.go +++ b/core/chains/evm/txmgr/transmitchecker.go @@ -217,7 +217,7 @@ func (v *VRFV1Checker) Check( requestTransactionReceipt := &gethtypes.Receipt{} batch := []rpc.BatchElem{{ Method: "eth_getBlockByNumber", - Args: []interface{}{nil}, + Args: []interface{}{"latest", false}, Result: mostRecentHead, }, { Method: "eth_getTransactionReceipt", diff --git a/core/config/chaintype.go b/core/config/chaintype.go index c99099ee616..21fb8cd297d 100644 --- a/core/config/chaintype.go +++ b/core/config/chaintype.go @@ -15,18 +15,19 @@ const ( ChainOptimismBedrock ChainType = "optimismBedrock" ChainXDai ChainType = "xdai" ChainCelo ChainType = "celo" + ChainWeMix ChainType = "wemix" ChainKroma ChainType = "kroma" + ChainZkSync ChainType = "zksync" ) var ErrInvalidChainType = fmt.Errorf("must be one of %s or omitted", strings.Join([]string{ string(ChainArbitrum), string(ChainMetis), string(ChainXDai), string(ChainOptimismBedrock), string(ChainCelo), - string(ChainKroma), -}, ", ")) + string(ChainKroma), string(ChainWeMix), string(ChainZkSync)}, ", ")) // IsValid returns true if the ChainType value is known or empty. func (c ChainType) IsValid() bool { switch c { - case "", ChainArbitrum, ChainMetis, ChainOptimismBedrock, ChainXDai, ChainCelo, ChainKroma: + case "", ChainArbitrum, ChainMetis, ChainOptimismBedrock, ChainXDai, ChainCelo, ChainKroma, ChainWeMix, ChainZkSync: return true } return false diff --git a/core/config/docs/chains-evm.toml b/core/config/docs/chains-evm.toml index 082bbd6cd19..381ab794d60 100644 --- a/core/config/docs/chains-evm.toml +++ b/core/config/docs/chains-evm.toml @@ -14,7 +14,7 @@ BlockBackfillDepth = 10 # Default # BlockBackfillSkip enables skipping of very long backfills. BlockBackfillSkip = false # Default # ChainType is automatically detected from chain ID. Set this to force a certain chain type regardless of chain ID. -# Available types: arbitrum, metis, optimismBedrock, xdai, celo, kroma +# Available types: arbitrum, metis, optimismBedrock, xdai, celo, kroma, wemix, zksync ChainType = 'arbitrum' # Example # FinalityDepth is the number of blocks after which an ethereum transaction is considered "final". Note that the default is automatically set based on chain ID so it should not be necessary to change this under normal operation. # BlocksConsideredFinal determines how deeply we look back to ensure that transactions are confirmed onto the longest chain diff --git a/core/scripts/common/helpers.go b/core/scripts/common/helpers.go index d03dcec097f..c141e8a29c4 100644 --- a/core/scripts/common/helpers.go +++ b/core/scripts/common/helpers.go @@ -219,6 +219,11 @@ func explorerLinkPrefix(chainID int64) (prefix string) { case 8453: prefix = "https://basescan.org" + case 280: // zkSync Goerli testnet + prefix = "https://goerli.explorer.zksync.io" + case 324: // zkSync mainnet + prefix = "https://explorer.zksync.io" + default: // Unknown chain, return prefix as-is prefix = "" } diff --git a/core/scripts/go.mod b/core/scripts/go.mod index 8d731f3f41d..9dbe132346c 100644 --- a/core/scripts/go.mod +++ b/core/scripts/go.mod @@ -305,7 +305,7 @@ require ( github.com/sirupsen/logrus v1.9.3 // indirect github.com/smartcontractkit/caigo v0.0.0-20230621050857-b29a4ca8c704 // indirect github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20231101160906-7acebcc1b353 // indirect - github.com/smartcontractkit/chainlink-relay v0.1.7-0.20231102162027-5fdce33763de // indirect + github.com/smartcontractkit/chainlink-relay v0.1.7-0.20231107132621-6de9cc4fb264 // indirect github.com/smartcontractkit/chainlink-solana v1.0.3-0.20231023133638-72f4e799ab05 // indirect github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20231024133459-1ef3a11319eb // indirect github.com/smartcontractkit/tdh2/go/ocr2/decryptionplugin v0.0.0-20230906073235-9e478e5e19f1 // indirect diff --git a/core/scripts/go.sum b/core/scripts/go.sum index ebab8c990d3..7025d38c20d 100644 --- a/core/scripts/go.sum +++ b/core/scripts/go.sum @@ -1466,8 +1466,8 @@ github.com/smartcontractkit/caigo v0.0.0-20230621050857-b29a4ca8c704 h1:T3lFWumv github.com/smartcontractkit/caigo v0.0.0-20230621050857-b29a4ca8c704/go.mod h1:2QuJdEouTWjh5BDy5o/vgGXQtR4Gz8yH1IYB5eT7u4M= github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20231101160906-7acebcc1b353 h1:4iO3Ei1b/Lb0yprzclk93e1aQnYF92sIe+EJzMG87y4= github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20231101160906-7acebcc1b353/go.mod h1:hMhGr9ok3p4442keFtK6u6Ei9yWfG66fmDwsFi3aHcw= -github.com/smartcontractkit/chainlink-relay v0.1.7-0.20231102162027-5fdce33763de h1:CeVpn5xEdmuEsYE8ss2b7bSq9h3BY4OPvpqXeYIPnHw= -github.com/smartcontractkit/chainlink-relay v0.1.7-0.20231102162027-5fdce33763de/go.mod h1:M9U1JV7IQi8Sfj4JR1qSi1tIh6omgW78W/8SHN/8BUQ= +github.com/smartcontractkit/chainlink-relay v0.1.7-0.20231107132621-6de9cc4fb264 h1:64bH7MmWzcy5tB16x40266DzgKr2iIVcDPjOro6Q3Us= +github.com/smartcontractkit/chainlink-relay v0.1.7-0.20231107132621-6de9cc4fb264/go.mod h1:M9U1JV7IQi8Sfj4JR1qSi1tIh6omgW78W/8SHN/8BUQ= github.com/smartcontractkit/chainlink-solana v1.0.3-0.20231023133638-72f4e799ab05 h1:DaPSVnxe7oz1QJ+AVIhQWs1W3ubQvwvGo9NbHpMs1OQ= github.com/smartcontractkit/chainlink-solana v1.0.3-0.20231023133638-72f4e799ab05/go.mod h1:o0Pn1pbaUluboaK6/yhf8xf7TiFCkyFl6WUOdwqamuU= github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20231024133459-1ef3a11319eb h1:HiluOfEVGOQTM6BTDImOqYdMZZ7qq7fkZ3TJdmItNr8= diff --git a/core/services/chainlink/config_test.go b/core/services/chainlink/config_test.go index 59a02f1dcf9..34fcc4bbe91 100644 --- a/core/services/chainlink/config_test.go +++ b/core/services/chainlink/config_test.go @@ -1190,7 +1190,7 @@ func TestConfig_Validate(t *testing.T) { - 1: 6 errors: - ChainType: invalid value (Foo): must not be set with this chain id - Nodes: missing: must have at least one node - - ChainType: invalid value (Foo): must be one of arbitrum, metis, xdai, optimismBedrock, celo, kroma or omitted + - ChainType: invalid value (Foo): must be one of arbitrum, metis, xdai, optimismBedrock, celo, kroma, wemix, zksync or omitted - HeadTracker.HistoryDepth: invalid value (30): must be equal to or greater than FinalityDepth - GasEstimator: 2 errors: - FeeCapDefault: invalid value (101 wei): must be equal to PriceMax (99 wei) since you are using FixedPrice estimation with gas bumping disabled in EIP1559 mode - PriceMax will be used as the FeeCap for transactions instead of FeeCapDefault @@ -1199,7 +1199,7 @@ func TestConfig_Validate(t *testing.T) { - 2: 5 errors: - ChainType: invalid value (Arbitrum): only "optimismBedrock" can be used with this chain id - Nodes: missing: must have at least one node - - ChainType: invalid value (Arbitrum): must be one of arbitrum, metis, xdai, optimismBedrock, celo, kroma or omitted + - ChainType: invalid value (Arbitrum): must be one of arbitrum, metis, xdai, optimismBedrock, celo, kroma, wemix, zksync or omitted - FinalityDepth: invalid value (0): must be greater than or equal to 1 - MinIncomingConfirmations: invalid value (0): must be greater than or equal to 1 - 3.Nodes: 5 errors: diff --git a/core/services/ocr/contract_tracker.go b/core/services/ocr/contract_tracker.go index 3a216e025f0..f49e556d4e8 100644 --- a/core/services/ocr/contract_tracker.go +++ b/core/services/ocr/contract_tracker.go @@ -401,7 +401,7 @@ func (t *OCRContractTracker) LatestBlockHeight(ctx context.Context) (blockheight // care about the block height; we have no way of getting the L1 block // height anyway return 0, nil - case "", config.ChainArbitrum, config.ChainCelo, config.ChainOptimismBedrock, config.ChainXDai, config.ChainKroma: + case "", config.ChainArbitrum, config.ChainCelo, config.ChainOptimismBedrock, config.ChainXDai, config.ChainKroma, config.ChainWeMix, config.ChainZkSync: // continue } latestBlockHeight := t.getLatestBlockHeight() diff --git a/core/services/ocr/delegate.go b/core/services/ocr/delegate.go index bbed43c151b..6eb6714a474 100644 --- a/core/services/ocr/delegate.go +++ b/core/services/ocr/delegate.go @@ -295,10 +295,13 @@ func (d *Delegate) ServicesForSpec(jb job.Job) (services []job.ServiceCtx, err e configOverrider = configOverriderService } + jb.OCROracleSpec.CaptureEATelemetry = chain.Config().OCR().CaptureEATelemetry() enhancedTelemChan := make(chan ocrcommon.EnhancedTelemetryData, 100) if ocrcommon.ShouldCollectEnhancedTelemetry(&jb) { enhancedTelemService := ocrcommon.NewEnhancedTelemetryService(&jb, enhancedTelemChan, make(chan struct{}), d.monitoringEndpointGen.GenMonitoringEndpoint("EVM", chain.ID().String(), concreteSpec.ContractAddress.String(), synchronization.EnhancedEA), lggr.Named("EnhancedTelemetry")) services = append(services, enhancedTelemService) + } else { + lggr.Infow("Enhanced telemetry is disabled for job", "job", jb.Name) } oracle, err := ocr.NewOracle(ocr.OracleArgs{ diff --git a/core/services/ocr2/delegate.go b/core/services/ocr2/delegate.go index 39a8c84d6b9..75147ca2333 100644 --- a/core/services/ocr2/delegate.go +++ b/core/services/ocr2/delegate.go @@ -596,10 +596,11 @@ func (d *Delegate) newServicesGenericPlugin( } pluginConfig := types.ReportingPluginServiceConfig{ - PluginName: cconf.PluginName, - Command: command, - ProviderType: cconf.ProviderType, - PluginConfig: string(p.PluginConfig), + PluginName: cconf.PluginName, + Command: command, + ProviderType: cconf.ProviderType, + TelemetryType: cconf.TelemetryType, + PluginConfig: string(p.PluginConfig), } pr := generic.NewPipelineRunnerAdapter(pluginLggr, jb, d.pipelineRunner) @@ -746,6 +747,8 @@ func (d *Delegate) newServicesMedian( if ocrcommon.ShouldCollectEnhancedTelemetry(&jb) { enhancedTelemService := ocrcommon.NewEnhancedTelemetryService(&jb, enhancedTelemChan, make(chan struct{}), d.monitoringEndpointGen.GenMonitoringEndpoint(rid.Network, rid.ChainID, spec.ContractID, synchronization.EnhancedEA), lggr.Named("EnhancedTelemetry")) medianServices = append(medianServices, enhancedTelemService) + } else { + lggr.Infow("Enhanced telemetry is disabled for job", "job", jb.Name) } return medianServices, err2 diff --git a/core/services/ocr2/plugins/generic/pipeline_runner_adapter.go b/core/services/ocr2/plugins/generic/pipeline_runner_adapter.go index 6afb35ca758..def33114e8c 100644 --- a/core/services/ocr2/plugins/generic/pipeline_runner_adapter.go +++ b/core/services/ocr2/plugins/generic/pipeline_runner_adapter.go @@ -23,7 +23,7 @@ type PipelineRunnerAdapter struct { logger logger.Logger } -func (p *PipelineRunnerAdapter) ExecuteRun(ctx context.Context, spec string, vars types.Vars, options types.Options) ([]types.TaskResult, error) { +func (p *PipelineRunnerAdapter) ExecuteRun(ctx context.Context, spec string, vars types.Vars, options types.Options) (types.TaskResults, error) { s := pipeline.Spec{ DotDagSource: spec, CreatedAt: time.Now(), @@ -54,9 +54,13 @@ func (p *PipelineRunnerAdapter) ExecuteRun(ctx context.Context, spec string, var taskResults[i] = types.TaskResult{ ID: trr.ID.String(), Type: string(trr.Task.Type()), - Value: trr.Result.Value, - Error: trr.Result.Error, - Index: int(trr.TaskRun.Index), + Index: int(trr.Task.OutputIndex()), + + TaskValue: types.TaskValue{ + Value: trr.Result.Value, + Error: trr.Result.Error, + IsTerminal: len(trr.Task.Outputs()) == 0, + }, } } return taskResults, nil diff --git a/core/services/ocr2/plugins/ocr2keeper/evm21/core/utils.go b/core/services/ocr2/plugins/ocr2keeper/evm21/core/utils.go index 6a31b938fc6..1da28c1ad09 100644 --- a/core/services/ocr2/plugins/ocr2keeper/evm21/core/utils.go +++ b/core/services/ocr2/plugins/ocr2keeper/evm21/core/utils.go @@ -3,7 +3,6 @@ package core import ( "context" "math/big" - "strings" "github.com/ethereum/go-ethereum/common" @@ -14,20 +13,8 @@ import ( // GetTxBlock calls eth_getTransactionReceipt on the eth client to obtain a tx receipt func GetTxBlock(ctx context.Context, client client.Client, txHash common.Hash) (*big.Int, common.Hash, error) { receipt := types.Receipt{} - err := client.CallContext(ctx, &receipt, "eth_getTransactionReceipt", txHash) - if err != nil { - if strings.Contains(err.Error(), "not yet been implemented") { - // workaround for simulated chains - // Exploratory: fix this properly (e.g. in the simulated backend) - r, err1 := client.TransactionReceipt(ctx, txHash) - if err1 != nil { - return nil, common.Hash{}, err1 - } - if r.Status != 1 { - return nil, common.Hash{}, nil - } - return r.BlockNumber, r.BlockHash, nil - } + + if err := client.CallContext(ctx, &receipt, "eth_getTransactionReceipt", txHash); err != nil { return nil, common.Hash{}, err } diff --git a/core/services/ocr2/plugins/ocr2keeper/evm21/streams_lookup.go b/core/services/ocr2/plugins/ocr2keeper/evm21/streams_lookup.go index f183e1f6bbe..660550afe97 100644 --- a/core/services/ocr2/plugins/ocr2keeper/evm21/streams_lookup.go +++ b/core/services/ocr2/plugins/ocr2keeper/evm21/streams_lookup.go @@ -514,20 +514,7 @@ func (r *EvmRegistry) multiFeedsRequest(ctx context.Context, ch chan<- MercuryDa state = encoding.MercuryFlakyFailure return fmt.Errorf("%d", resp.StatusCode) } else if resp.StatusCode == http.StatusPartialContent { - //var response MercuryV03Response - //err1 = json.Unmarshal(body, &response) - //if err1 != nil { - // lggr.Warnf("at timestamp %s upkeep %s failed to unmarshal body to MercuryV03Response from mercury v0.3: %v", sl.Time.String(), sl.upkeepId.String(), err1) - // retryable = false - // state = encoding.MercuryUnmarshalError - // return err1 - //} - // in v0.3, if some feeds are not available, the server will only return available feeds, but we need to make sure ALL feeds are retrieved before calling user contract - // hence, retry in this case. retry will help when we send a very new timestamp and reports are not yet generated - //var receivedFeeds []string - //for _, f := range response.Reports { - // receivedFeeds = append(receivedFeeds, f.FeedID) - //} + // TODO (AUTO-5044): handle response code 206 entirely with errors field parsing lggr.Warnf("at timestamp %s upkeep %s requested [%s] feeds but mercury v0.3 server returned 206 status, treating it as 404 and retrying", sl.Time.String(), sl.upkeepId.String(), sl.Feeds) retryable = true state = encoding.MercuryFlakyFailure diff --git a/core/services/ocrcommon/data_source.go b/core/services/ocrcommon/data_source.go index ed832e45fcf..0363a7124b6 100644 --- a/core/services/ocrcommon/data_source.go +++ b/core/services/ocrcommon/data_source.go @@ -144,6 +144,8 @@ func (ds *inMemoryDataSource) executeRun(ctx context.Context, timestamp Observat FinalResults: finalResult, RepTimestamp: timestamp, }) + } else { + ds.lggr.Infow("Enhanced telemetry is disabled for job", "job", ds.jb.Name) } return run, finalResult, err diff --git a/core/services/relay/evm/mercury/transmitter.go b/core/services/relay/evm/mercury/transmitter.go index 88c3113abc6..557210e58a5 100644 --- a/core/services/relay/evm/mercury/transmitter.go +++ b/core/services/relay/evm/mercury/transmitter.go @@ -33,6 +33,7 @@ import ( var ( maxTransmitQueueSize = 10_000 + maxDeleteQueueSize = 10_000 transmitTimeout = 5 * time.Second ) @@ -60,6 +61,24 @@ var ( }, []string{"feedID"}, ) + transmitQueueDeleteErrorCount = promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "mercury_transmit_queue_delete_error_count", + Help: "Running count of DB errors when trying to delete an item from the queue DB", + }, + []string{"feedID"}, + ) + transmitQueueInsertErrorCount = promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "mercury_transmit_queue_insert_error_count", + Help: "Running count of DB errors when trying to insert an item into the queue DB", + }, + []string{"feedID"}, + ) + transmitQueuePushErrorCount = promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "mercury_transmit_queue_push_error_count", + Help: "Running count of DB errors when trying to push an item onto the queue", + }, + []string{"feedID"}, + ) transmitServerErrorCount = promauto.NewCounterVec(prometheus.CounterOpts{ Name: "mercury_transmit_server_error_count", Help: "Number of errored transmissions that failed due to an error returned by the mercury server", @@ -99,9 +118,14 @@ type mercuryTransmitter struct { queue *TransmitQueue wg sync.WaitGroup - transmitSuccessCount prometheus.Counter - transmitDuplicateCount prometheus.Counter - transmitConnectionErrorCount prometheus.Counter + deleteQueue chan *pb.TransmitRequest + + transmitSuccessCount prometheus.Counter + transmitDuplicateCount prometheus.Counter + transmitConnectionErrorCount prometheus.Counter + transmitQueueDeleteErrorCount prometheus.Counter + transmitQueueInsertErrorCount prometheus.Counter + transmitQueuePushErrorCount prometheus.Counter } var PayloadTypes = getPayloadTypes() @@ -139,9 +163,13 @@ func NewTransmitter(lggr logger.Logger, cfgTracker ConfigTracker, rpcClient wsrp make(chan (struct{})), nil, sync.WaitGroup{}, + make(chan *pb.TransmitRequest, maxDeleteQueueSize), transmitSuccessCount.WithLabelValues(feedIDHex), transmitDuplicateCount.WithLabelValues(feedIDHex), transmitConnectionErrorCount.WithLabelValues(feedIDHex), + transmitQueueDeleteErrorCount.WithLabelValues(feedIDHex), + transmitQueueInsertErrorCount.WithLabelValues(feedIDHex), + transmitQueuePushErrorCount.WithLabelValues(feedIDHex), } } @@ -164,6 +192,8 @@ func (mt *mercuryTransmitter) Start(ctx context.Context) (err error) { return err } mt.wg.Add(1) + go mt.runDeleteQueueLoop() + mt.wg.Add(1) go mt.runQueueLoop() return nil }) @@ -192,6 +222,46 @@ func (mt *mercuryTransmitter) HealthReport() map[string]error { return report } +func (mt *mercuryTransmitter) runDeleteQueueLoop() { + defer mt.wg.Done() + runloopCtx, cancel := mt.stopCh.Ctx(context.Background()) + defer cancel() + + // Exponential backoff for very rarely occurring errors (DB disconnect etc) + b := backoff.Backoff{ + Min: 1 * time.Second, + Max: 120 * time.Second, + Factor: 2, + Jitter: true, + } + + for { + select { + case req := <-mt.deleteQueue: + for { + if err := mt.persistenceManager.Delete(runloopCtx, req); err != nil { + mt.lggr.Errorw("Failed to delete transmit request record", "error", err, "req", req) + mt.transmitQueueDeleteErrorCount.Inc() + select { + case <-time.After(b.Duration()): + // Wait a backoff duration before trying to delete again + continue + case <-mt.stopCh: + // abort and return immediately on stop even if items remain in queue + return + } + } + break + } + // success + b.Reset() + case <-mt.stopCh: + // abort and return immediately on stop even if items remain in queue + return + } + } +} + func (mt *mercuryTransmitter) runQueueLoop() { defer mt.wg.Done() // Exponential backoff with very short retry interval (since latency is a priority) @@ -253,9 +323,10 @@ func (mt *mercuryTransmitter) runQueueLoop() { } } - if err := mt.persistenceManager.Delete(runloopCtx, t.Req); err != nil { - mt.lggr.Errorw("Failed to delete transmit request record", "error", err, "reportCtx", t.ReportCtx) - return + select { + case mt.deleteQueue <- t.Req: + default: + mt.lggr.Criticalw("Delete queue is full", "reportCtx", t.ReportCtx) } } } @@ -288,9 +359,11 @@ func (mt *mercuryTransmitter) Transmit(ctx context.Context, reportCtx ocrtypes.R mt.lggr.Tracew("Transmit enqueue", "req", req, "report", report, "reportCtx", reportCtx, "signatures", signatures) if err := mt.persistenceManager.Insert(ctx, req, reportCtx); err != nil { + mt.transmitQueueInsertErrorCount.Inc() return err } if ok := mt.queue.Push(req, reportCtx); !ok { + mt.transmitQueuePushErrorCount.Inc() return errors.New("transmit queue is closed") } return nil diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index a9f9d080f42..a10f9dd1c6d 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -12,6 +12,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added a new, optional WebServer authentication option that supports LDAP as a user identity provider. This enables user login access and user roles to be managed and provisioned via a centralized remote server that supports the LDAP protocol, which can be helpful when running multiple nodes. See the documentation for more information and config setup instructions. There is a new `[WebServer].AuthenticationMethod` config option, when set to `ldap` requires the new `[WebServer.LDAP]` config section to be defined, see the reference `docs/core.toml`. +- New prom metrics for mercury: + `mercury_transmit_queue_delete_error_count` + `mercury_transmit_queue_insert_error_count` + `mercury_transmit_queue_push_error_count` + Nops should consider alerting on these. ### Changed diff --git a/docs/CONFIG.md b/docs/CONFIG.md index 4b55c804a3d..1eb9cd5023d 100644 --- a/docs/CONFIG.md +++ b/docs/CONFIG.md @@ -2986,6 +2986,164 @@ GasLimit = 5300000
++ +```toml +AutoCreateKey = true +BlockBackfillDepth = 10 +BlockBackfillSkip = false +ChainType = 'zksync' +FinalityDepth = 1 +FinalityTagEnabled = false +LogBackfillBatchSize = 1000 +LogPollInterval = '5s' +LogKeepBlocksDepth = 100000 +MinIncomingConfirmations = 1 +MinContractPayment = '0.00001 link' +NonceAutoSync = true +NoNewHeadsThreshold = '1m0s' +RPCDefaultBatchSize = 250 +RPCBlockQueryDelay = 1 + +[Transactions] +ForwardersEnabled = false +MaxInFlight = 16 +MaxQueued = 250 +ReaperInterval = '1h0m0s' +ReaperThreshold = '168h0m0s' +ResendAfterThreshold = '1m0s' + +[BalanceMonitor] +Enabled = true + +[GasEstimator] +Mode = 'BlockHistory' +PriceDefault = '20 gwei' +PriceMax = '18.446744073709551615 ether' +PriceMin = '0' +LimitDefault = 3500000 +LimitMax = 500000 +LimitMultiplier = '1' +LimitTransfer = 21000 +BumpMin = '5 gwei' +BumpPercent = 20 +BumpThreshold = 3 +EIP1559DynamicFees = false +FeeCapDefault = '100 gwei' +TipCapDefault = '1 wei' +TipCapMin = '1 wei' + +[GasEstimator.BlockHistory] +BatchSize = 25 +BlockHistorySize = 8 +CheckInclusionBlocks = 12 +CheckInclusionPercentile = 90 +TransactionPercentile = 60 + +[HeadTracker] +HistoryDepth = 5 +MaxBufferSize = 3 +SamplingInterval = '1s' + +[NodePool] +PollFailureThreshold = 5 +PollInterval = '10s' +SelectionMode = 'HighestHead' +SyncThreshold = 5 +LeaseDuration = '0s' + +[OCR] +ContractConfirmations = 4 +ContractTransmitterTransmitTimeout = '10s' +DatabaseTimeout = '10s' +ObservationGracePeriod = '1s' + +[OCR2] +[OCR2.Automation] +GasLimit = 5300000 +``` + +
+ +```toml +AutoCreateKey = true +BlockBackfillDepth = 10 +BlockBackfillSkip = false +ChainType = 'zksync' +FinalityDepth = 1 +FinalityTagEnabled = false +LogBackfillBatchSize = 1000 +LogPollInterval = '5s' +LogKeepBlocksDepth = 100000 +MinIncomingConfirmations = 1 +MinContractPayment = '0.00001 link' +NonceAutoSync = true +NoNewHeadsThreshold = '1m0s' +RPCDefaultBatchSize = 250 +RPCBlockQueryDelay = 1 + +[Transactions] +ForwardersEnabled = false +MaxInFlight = 16 +MaxQueued = 250 +ReaperInterval = '1h0m0s' +ReaperThreshold = '168h0m0s' +ResendAfterThreshold = '1m0s' + +[BalanceMonitor] +Enabled = true + +[GasEstimator] +Mode = 'BlockHistory' +PriceDefault = '20 gwei' +PriceMax = '18.446744073709551615 ether' +PriceMin = '0' +LimitDefault = 3500000 +LimitMax = 500000 +LimitMultiplier = '1' +LimitTransfer = 21000 +BumpMin = '5 gwei' +BumpPercent = 20 +BumpThreshold = 3 +EIP1559DynamicFees = false +FeeCapDefault = '100 gwei' +TipCapDefault = '1 wei' +TipCapMin = '1 wei' + +[GasEstimator.BlockHistory] +BatchSize = 25 +BlockHistorySize = 8 +CheckInclusionBlocks = 12 +CheckInclusionPercentile = 90 +TransactionPercentile = 60 + +[HeadTracker] +HistoryDepth = 5 +MaxBufferSize = 3 +SamplingInterval = '1s' + +[NodePool] +PollFailureThreshold = 5 +PollInterval = '10s' +SelectionMode = 'HighestHead' +SyncThreshold = 5 +LeaseDuration = '0s' + +[OCR] +ContractConfirmations = 4 +ContractTransmitterTransmitTimeout = '10s' +DatabaseTimeout = '10s' +ObservationGracePeriod = '1s' + +[OCR2] +[OCR2.Automation] +GasLimit = 5300000 +``` + +
```toml @@ -3302,6 +3460,164 @@ GasLimit = 5300000
+ +```toml +AutoCreateKey = true +BlockBackfillDepth = 10 +BlockBackfillSkip = false +ChainType = 'wemix' +FinalityDepth = 1 +FinalityTagEnabled = false +LogBackfillBatchSize = 1000 +LogPollInterval = '3s' +LogKeepBlocksDepth = 100000 +MinIncomingConfirmations = 1 +MinContractPayment = '0.00001 link' +NonceAutoSync = true +NoNewHeadsThreshold = '30s' +RPCDefaultBatchSize = 250 +RPCBlockQueryDelay = 1 + +[Transactions] +ForwardersEnabled = false +MaxInFlight = 16 +MaxQueued = 250 +ReaperInterval = '1h0m0s' +ReaperThreshold = '168h0m0s' +ResendAfterThreshold = '1m0s' + +[BalanceMonitor] +Enabled = true + +[GasEstimator] +Mode = 'BlockHistory' +PriceDefault = '20 gwei' +PriceMax = '115792089237316195423570985008687907853269984665.640564039457584007913129639935 tether' +PriceMin = '1 gwei' +LimitDefault = 500000 +LimitMax = 500000 +LimitMultiplier = '1' +LimitTransfer = 21000 +BumpMin = '5 gwei' +BumpPercent = 20 +BumpThreshold = 3 +EIP1559DynamicFees = true +FeeCapDefault = '100 gwei' +TipCapDefault = '100 gwei' +TipCapMin = '1 wei' + +[GasEstimator.BlockHistory] +BatchSize = 25 +BlockHistorySize = 8 +CheckInclusionBlocks = 12 +CheckInclusionPercentile = 90 +TransactionPercentile = 60 + +[HeadTracker] +HistoryDepth = 100 +MaxBufferSize = 3 +SamplingInterval = '1s' + +[NodePool] +PollFailureThreshold = 5 +PollInterval = '10s' +SelectionMode = 'HighestHead' +SyncThreshold = 5 +LeaseDuration = '0s' + +[OCR] +ContractConfirmations = 1 +ContractTransmitterTransmitTimeout = '10s' +DatabaseTimeout = '10s' +ObservationGracePeriod = '1s' + +[OCR2] +[OCR2.Automation] +GasLimit = 5300000 +``` + +
+ +```toml +AutoCreateKey = true +BlockBackfillDepth = 10 +BlockBackfillSkip = false +ChainType = 'wemix' +FinalityDepth = 1 +FinalityTagEnabled = false +LogBackfillBatchSize = 1000 +LogPollInterval = '3s' +LogKeepBlocksDepth = 100000 +MinIncomingConfirmations = 1 +MinContractPayment = '0.00001 link' +NonceAutoSync = true +NoNewHeadsThreshold = '30s' +RPCDefaultBatchSize = 250 +RPCBlockQueryDelay = 1 + +[Transactions] +ForwardersEnabled = false +MaxInFlight = 16 +MaxQueued = 250 +ReaperInterval = '1h0m0s' +ReaperThreshold = '168h0m0s' +ResendAfterThreshold = '1m0s' + +[BalanceMonitor] +Enabled = true + +[GasEstimator] +Mode = 'BlockHistory' +PriceDefault = '20 gwei' +PriceMax = '115792089237316195423570985008687907853269984665.640564039457584007913129639935 tether' +PriceMin = '1 gwei' +LimitDefault = 500000 +LimitMax = 500000 +LimitMultiplier = '1' +LimitTransfer = 21000 +BumpMin = '5 gwei' +BumpPercent = 20 +BumpThreshold = 3 +EIP1559DynamicFees = true +FeeCapDefault = '100 gwei' +TipCapDefault = '100 gwei' +TipCapMin = '1 wei' + +[GasEstimator.BlockHistory] +BatchSize = 25 +BlockHistorySize = 8 +CheckInclusionBlocks = 12 +CheckInclusionPercentile = 90 +TransactionPercentile = 60 + +[HeadTracker] +HistoryDepth = 100 +MaxBufferSize = 3 +SamplingInterval = '1s' + +[NodePool] +PollFailureThreshold = 5 +PollInterval = '10s' +SelectionMode = 'HighestHead' +SyncThreshold = 5 +LeaseDuration = '0s' + +[OCR] +ContractConfirmations = 1 +ContractTransmitterTransmitTimeout = '10s' +DatabaseTimeout = '10s' +ObservationGracePeriod = '1s' + +[OCR2] +[OCR2.Automation] +GasLimit = 5300000 +``` + +
```toml @@ -5074,7 +5390,7 @@ BlockBackfillSkip enables skipping of very long backfills. ChainType = 'arbitrum' # Example ``` ChainType is automatically detected from chain ID. Set this to force a certain chain type regardless of chain ID. -Available types: arbitrum, metis, optimismBedrock, xdai, celo, kroma +Available types: arbitrum, metis, optimismBedrock, xdai, celo, kroma, wemix, zksync ### FinalityDepth ```toml diff --git a/go.mod b/go.mod index 4d8a9294020..d61b7b6f61b 100644 --- a/go.mod +++ b/go.mod @@ -66,7 +66,7 @@ require ( github.com/shopspring/decimal v1.3.1 github.com/smartcontractkit/caigo v0.0.0-20230621050857-b29a4ca8c704 github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20231101160906-7acebcc1b353 - github.com/smartcontractkit/chainlink-relay v0.1.7-0.20231102162027-5fdce33763de + github.com/smartcontractkit/chainlink-relay v0.1.7-0.20231107132621-6de9cc4fb264 github.com/smartcontractkit/chainlink-solana v1.0.3-0.20231023133638-72f4e799ab05 github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20231024133459-1ef3a11319eb github.com/smartcontractkit/libocr v0.0.0-20231020123319-d255366a6545 diff --git a/go.sum b/go.sum index acd966e8aa3..f2737ab3aea 100644 --- a/go.sum +++ b/go.sum @@ -1467,8 +1467,8 @@ github.com/smartcontractkit/caigo v0.0.0-20230621050857-b29a4ca8c704 h1:T3lFWumv github.com/smartcontractkit/caigo v0.0.0-20230621050857-b29a4ca8c704/go.mod h1:2QuJdEouTWjh5BDy5o/vgGXQtR4Gz8yH1IYB5eT7u4M= github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20231101160906-7acebcc1b353 h1:4iO3Ei1b/Lb0yprzclk93e1aQnYF92sIe+EJzMG87y4= github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20231101160906-7acebcc1b353/go.mod h1:hMhGr9ok3p4442keFtK6u6Ei9yWfG66fmDwsFi3aHcw= -github.com/smartcontractkit/chainlink-relay v0.1.7-0.20231102162027-5fdce33763de h1:CeVpn5xEdmuEsYE8ss2b7bSq9h3BY4OPvpqXeYIPnHw= -github.com/smartcontractkit/chainlink-relay v0.1.7-0.20231102162027-5fdce33763de/go.mod h1:M9U1JV7IQi8Sfj4JR1qSi1tIh6omgW78W/8SHN/8BUQ= +github.com/smartcontractkit/chainlink-relay v0.1.7-0.20231107132621-6de9cc4fb264 h1:64bH7MmWzcy5tB16x40266DzgKr2iIVcDPjOro6Q3Us= +github.com/smartcontractkit/chainlink-relay v0.1.7-0.20231107132621-6de9cc4fb264/go.mod h1:M9U1JV7IQi8Sfj4JR1qSi1tIh6omgW78W/8SHN/8BUQ= github.com/smartcontractkit/chainlink-solana v1.0.3-0.20231023133638-72f4e799ab05 h1:DaPSVnxe7oz1QJ+AVIhQWs1W3ubQvwvGo9NbHpMs1OQ= github.com/smartcontractkit/chainlink-solana v1.0.3-0.20231023133638-72f4e799ab05/go.mod h1:o0Pn1pbaUluboaK6/yhf8xf7TiFCkyFl6WUOdwqamuU= github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20231024133459-1ef3a11319eb h1:HiluOfEVGOQTM6BTDImOqYdMZZ7qq7fkZ3TJdmItNr8= diff --git a/integration-tests/benchmark/keeper_test.go b/integration-tests/benchmark/keeper_test.go index 25a147fbf0b..9342f3629b9 100644 --- a/integration-tests/benchmark/keeper_test.go +++ b/integration-tests/benchmark/keeper_test.go @@ -227,7 +227,7 @@ func addRegistry(registryToTest string) []eth_contracts.KeeperRegistryVersion { case "2_0-Multiple": return repeatRegistries(eth_contracts.RegistryVersion_2_0, NumberOfRegistries) case "2_1-Multiple": - return repeatRegistries(eth_contracts.RegistryVersion_1_0, NumberOfRegistries) + return repeatRegistries(eth_contracts.RegistryVersion_2_1, NumberOfRegistries) default: return []eth_contracts.KeeperRegistryVersion{eth_contracts.RegistryVersion_2_0} } diff --git a/integration-tests/contracts/contract_deployer.go b/integration-tests/contracts/contract_deployer.go index 0c36a260815..916971f82d3 100644 --- a/integration-tests/contracts/contract_deployer.go +++ b/integration-tests/contracts/contract_deployer.go @@ -175,6 +175,8 @@ func NewContractDeployer(bcClient blockchain.EVMClient, logger zerolog.Logger) ( return &FantomContractDeployer{NewEthereumContractDeployer(clientImpl, logger)}, nil case *blockchain.KromaClient: return &KromaContractDeployer{NewEthereumContractDeployer(clientImpl, logger)}, nil + case *blockchain.WeMixClient: + return &WeMixContractDeployer{NewEthereumContractDeployer(clientImpl, logger)}, nil } return nil, errors.New("unknown blockchain client implementation for contract deployer, register blockchain client in NewContractDeployer") } @@ -246,6 +248,10 @@ type KromaContractDeployer struct { *EthereumContractDeployer } +type WeMixContractDeployer struct { + *EthereumContractDeployer +} + // NewEthereumContractDeployer returns an instantiated instance of the ETH contract deployer func NewEthereumContractDeployer(ethClient blockchain.EVMClient, logger zerolog.Logger) *EthereumContractDeployer { return &EthereumContractDeployer{ diff --git a/integration-tests/contracts/contract_loader.go b/integration-tests/contracts/contract_loader.go index 4dda2d3f0c4..cfe7a35467e 100644 --- a/integration-tests/contracts/contract_loader.go +++ b/integration-tests/contracts/contract_loader.go @@ -64,6 +64,8 @@ func NewContractLoader(bcClient blockchain.EVMClient, logger zerolog.Logger) (Co return &OptimismContractLoader{NewEthereumContractLoader(clientImpl, logger)}, nil case *blockchain.PolygonZkEvmClient: return &PolygonZkEvmContractLoader{NewEthereumContractLoader(clientImpl, logger)}, nil + case *blockchain.WeMixClient: + return &WeMixContractLoader{NewEthereumContractLoader(clientImpl, logger)}, nil } return nil, errors.New("unknown blockchain client implementation for contract Loader, register blockchain client in NewContractLoader") } @@ -107,6 +109,11 @@ type PolygonZKEVMContractLoader struct { *EthereumContractLoader } +// WeMixContractLoader wraps for WeMix +type WeMixContractLoader struct { + *EthereumContractLoader +} + // NewEthereumContractLoader returns an instantiated instance of the ETH contract Loader func NewEthereumContractLoader(ethClient blockchain.EVMClient, logger zerolog.Logger) *EthereumContractLoader { return &EthereumContractLoader{ diff --git a/integration-tests/go.mod b/integration-tests/go.mod index 936729944f8..6f2809df8c1 100644 --- a/integration-tests/go.mod +++ b/integration-tests/go.mod @@ -22,7 +22,7 @@ require ( github.com/scylladb/go-reflectx v1.0.1 github.com/segmentio/ksuid v1.0.4 github.com/slack-go/slack v0.12.2 - github.com/smartcontractkit/chainlink-testing-framework v1.18.4-0.20231106173929-20fe04d6ad66 + github.com/smartcontractkit/chainlink-testing-framework v1.18.5-0.20231107092923-3aa655167f65 github.com/smartcontractkit/chainlink/v2 v2.0.0-00010101000000-000000000000 github.com/smartcontractkit/libocr v0.0.0-20231020123319-d255366a6545 github.com/smartcontractkit/ocr2keepers v0.7.28 @@ -388,7 +388,7 @@ require ( github.com/sirupsen/logrus v1.9.3 // indirect github.com/smartcontractkit/caigo v0.0.0-20230621050857-b29a4ca8c704 // indirect github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20231101160906-7acebcc1b353 // indirect - github.com/smartcontractkit/chainlink-relay v0.1.7-0.20231102162027-5fdce33763de // indirect + github.com/smartcontractkit/chainlink-relay v0.1.7-0.20231107132621-6de9cc4fb264 // indirect github.com/smartcontractkit/chainlink-solana v1.0.3-0.20231023133638-72f4e799ab05 // indirect github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20231024133459-1ef3a11319eb // indirect github.com/smartcontractkit/tdh2/go/ocr2/decryptionplugin v0.0.0-20230906073235-9e478e5e19f1 // indirect diff --git a/integration-tests/go.sum b/integration-tests/go.sum index 7bd733023c5..9e1be8b8144 100644 --- a/integration-tests/go.sum +++ b/integration-tests/go.sum @@ -2370,14 +2370,14 @@ github.com/smartcontractkit/caigo v0.0.0-20230621050857-b29a4ca8c704 h1:T3lFWumv github.com/smartcontractkit/caigo v0.0.0-20230621050857-b29a4ca8c704/go.mod h1:2QuJdEouTWjh5BDy5o/vgGXQtR4Gz8yH1IYB5eT7u4M= github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20231101160906-7acebcc1b353 h1:4iO3Ei1b/Lb0yprzclk93e1aQnYF92sIe+EJzMG87y4= github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20231101160906-7acebcc1b353/go.mod h1:hMhGr9ok3p4442keFtK6u6Ei9yWfG66fmDwsFi3aHcw= -github.com/smartcontractkit/chainlink-relay v0.1.7-0.20231102162027-5fdce33763de h1:CeVpn5xEdmuEsYE8ss2b7bSq9h3BY4OPvpqXeYIPnHw= -github.com/smartcontractkit/chainlink-relay v0.1.7-0.20231102162027-5fdce33763de/go.mod h1:M9U1JV7IQi8Sfj4JR1qSi1tIh6omgW78W/8SHN/8BUQ= +github.com/smartcontractkit/chainlink-relay v0.1.7-0.20231107132621-6de9cc4fb264 h1:64bH7MmWzcy5tB16x40266DzgKr2iIVcDPjOro6Q3Us= +github.com/smartcontractkit/chainlink-relay v0.1.7-0.20231107132621-6de9cc4fb264/go.mod h1:M9U1JV7IQi8Sfj4JR1qSi1tIh6omgW78W/8SHN/8BUQ= github.com/smartcontractkit/chainlink-solana v1.0.3-0.20231023133638-72f4e799ab05 h1:DaPSVnxe7oz1QJ+AVIhQWs1W3ubQvwvGo9NbHpMs1OQ= github.com/smartcontractkit/chainlink-solana v1.0.3-0.20231023133638-72f4e799ab05/go.mod h1:o0Pn1pbaUluboaK6/yhf8xf7TiFCkyFl6WUOdwqamuU= github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20231024133459-1ef3a11319eb h1:HiluOfEVGOQTM6BTDImOqYdMZZ7qq7fkZ3TJdmItNr8= github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20231024133459-1ef3a11319eb/go.mod h1:/30flFG4L/iCYAFeA3DUzR0xuHSxAMONiWTzyzvsNwo= -github.com/smartcontractkit/chainlink-testing-framework v1.18.4-0.20231106173929-20fe04d6ad66 h1:AOqcHiAppMoIvM2WSJNIZzJDnOQNXyElbLFK3ZqoJeM= -github.com/smartcontractkit/chainlink-testing-framework v1.18.4-0.20231106173929-20fe04d6ad66/go.mod h1:zScXRqmvbyTFUooyLYrOp4+V/sFPUbFJNRc72YmnuIk= +github.com/smartcontractkit/chainlink-testing-framework v1.18.5-0.20231107092923-3aa655167f65 h1:/iRhwYy5KFsaS9Zo1T64QxAd11HGZB5p/LHI5oVc4BU= +github.com/smartcontractkit/chainlink-testing-framework v1.18.5-0.20231107092923-3aa655167f65/go.mod h1:zScXRqmvbyTFUooyLYrOp4+V/sFPUbFJNRc72YmnuIk= github.com/smartcontractkit/go-plugin v0.0.0-20231003134350-e49dad63b306 h1:ko88+ZznniNJZbZPWAvHQU8SwKAdHngdDZ+pvVgB5ss= github.com/smartcontractkit/go-plugin v0.0.0-20231003134350-e49dad63b306/go.mod h1:w1sAEES3g3PuV/RzUrgow20W2uErMly84hhD3um1WL4= github.com/smartcontractkit/grpc-proxy v0.0.0-20230731113816-f1be6620749f h1:hgJif132UCdjo8u43i7iPN1/MFnu49hv7lFGFftCHKU= diff --git a/plugins/cmd/chainlink-medianpoc/main.go b/plugins/cmd/chainlink-medianpoc/main.go new file mode 100644 index 00000000000..325de6538fa --- /dev/null +++ b/plugins/cmd/chainlink-medianpoc/main.go @@ -0,0 +1,42 @@ +package main + +import ( + "github.com/hashicorp/go-plugin" + + "github.com/smartcontractkit/chainlink-relay/pkg/loop" + "github.com/smartcontractkit/chainlink-relay/pkg/loop/reportingplugins" + "github.com/smartcontractkit/chainlink-relay/pkg/types" + "github.com/smartcontractkit/chainlink/v2/plugins/medianpoc" +) + +const ( + loggerName = "PluginMedianPoc" +) + +func main() { + s := loop.MustNewStartedServer(loggerName) + defer s.Stop() + + p := medianpoc.NewPlugin(s.Logger) + defer s.Logger.ErrorIfFn(p.Close, "Failed to close") + + s.MustRegister(p) + + stop := make(chan struct{}) + defer close(stop) + + plugin.Serve(&plugin.ServeConfig{ + HandshakeConfig: reportingplugins.ReportingPluginHandshakeConfig(), + Plugins: map[string]plugin.Plugin{ + reportingplugins.PluginServiceName: &reportingplugins.GRPCService[types.MedianProvider]{ + PluginServer: p, + BrokerConfig: loop.BrokerConfig{ + Logger: s.Logger, + StopCh: stop, + GRPCOpts: s.GRPCOpts, + }, + }, + }, + GRPCServer: s.GRPCOpts.NewServer, + }) +} diff --git a/plugins/medianpoc/data_source.go b/plugins/medianpoc/data_source.go new file mode 100644 index 00000000000..7b20f1e5eb3 --- /dev/null +++ b/plugins/medianpoc/data_source.go @@ -0,0 +1,79 @@ +package medianpoc + +import ( + "context" + "errors" + "fmt" + "math/big" + "sync" + "time" + + ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + + "github.com/smartcontractkit/chainlink-relay/pkg/logger" + "github.com/smartcontractkit/chainlink-relay/pkg/types" + "github.com/smartcontractkit/chainlink/v2/core/bridges" + "github.com/smartcontractkit/chainlink/v2/core/utils" +) + +type DataSource struct { + pipelineRunner types.PipelineRunnerService + spec string + lggr logger.Logger + + current bridges.BridgeMetaData + mu sync.RWMutex +} + +func (d *DataSource) Observe(ctx context.Context, reportTimestamp ocrtypes.ReportTimestamp) (*big.Int, error) { + md, err := bridges.MarshalBridgeMetaData(d.currentAnswer()) + if err != nil { + d.lggr.Warnw("unable to attach metadata for run", "err", err) + } + + // NOTE: job metadata is automatically attached by the pipeline runner service + vars := types.Vars{ + Vars: map[string]interface{}{ + "jobRun": md, + }, + } + + results, err := d.pipelineRunner.ExecuteRun(ctx, d.spec, vars, types.Options{}) + if err != nil { + return nil, err + } + + finalResults := results.FinalResults() + if len(finalResults) == 0 { + return nil, errors.New("pipeline execution failed: not enough results") + } + + finalResult := finalResults[0] + if finalResult.Error != nil { + return nil, fmt.Errorf("pipeline execution failed: %w", finalResult.Error) + } + + asDecimal, err := utils.ToDecimal(finalResult.Value) + if err != nil { + return nil, errors.New("cannot convert observation to decimal") + } + + resultAsBigInt := asDecimal.BigInt() + d.updateAnswer(resultAsBigInt) + return resultAsBigInt, nil +} + +func (d *DataSource) currentAnswer() (*big.Int, *big.Int) { + d.mu.RLock() + defer d.mu.RUnlock() + return d.current.LatestAnswer, d.current.UpdatedAt +} + +func (d *DataSource) updateAnswer(latestAnswer *big.Int) { + d.mu.Lock() + defer d.mu.Unlock() + d.current = bridges.BridgeMetaData{ + LatestAnswer: latestAnswer, + UpdatedAt: big.NewInt(time.Now().Unix()), + } +} diff --git a/plugins/medianpoc/data_source_test.go b/plugins/medianpoc/data_source_test.go new file mode 100644 index 00000000000..e9a7945cee4 --- /dev/null +++ b/plugins/medianpoc/data_source_test.go @@ -0,0 +1,115 @@ +package medianpoc + +import ( + "context" + "errors" + "math/big" + "testing" + + ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink/v2/core/logger" + + "github.com/smartcontractkit/chainlink-relay/pkg/types" +) + +type mockPipelineRunner struct { + results types.TaskResults + err error + spec string + vars types.Vars + options types.Options +} + +func (m *mockPipelineRunner) ExecuteRun(ctx context.Context, spec string, vars types.Vars, options types.Options) (types.TaskResults, error) { + m.spec = spec + m.vars = vars + m.options = options + return m.results, m.err +} + +func TestDataSource(t *testing.T) { + lggr := logger.TestLogger(t) + expect := int64(3) + pr := &mockPipelineRunner{ + results: types.TaskResults{ + { + TaskValue: types.TaskValue{ + Value: expect, + Error: nil, + IsTerminal: true, + }, + Index: 2, + }, + { + TaskValue: types.TaskValue{ + Value: int(4), + Error: nil, + IsTerminal: false, + }, + Index: 1, + }, + }, + } + spec := "SPEC" + ds := &DataSource{ + pipelineRunner: pr, + spec: spec, + lggr: lggr, + } + res, err := ds.Observe(context.Background(), ocrtypes.ReportTimestamp{}) + require.NoError(t, err) + assert.Equal(t, big.NewInt(expect), res) + assert.Equal(t, spec, pr.spec) + assert.Equal(t, big.NewInt(expect), ds.current.LatestAnswer) +} + +func TestDataSource_ResultErrors(t *testing.T) { + lggr := logger.TestLogger(t) + pr := &mockPipelineRunner{ + results: types.TaskResults{ + { + TaskValue: types.TaskValue{ + Error: errors.New("something went wrong"), + IsTerminal: true, + }, + Index: 0, + }, + }, + } + spec := "SPEC" + ds := &DataSource{ + pipelineRunner: pr, + spec: spec, + lggr: lggr, + } + _, err := ds.Observe(context.Background(), ocrtypes.ReportTimestamp{}) + assert.ErrorContains(t, err, "something went wrong") +} + +func TestDataSource_ResultNotAnInt(t *testing.T) { + lggr := logger.TestLogger(t) + + expect := "string-result" + pr := &mockPipelineRunner{ + results: types.TaskResults{ + { + TaskValue: types.TaskValue{ + Value: expect, + IsTerminal: true, + }, + Index: 0, + }, + }, + } + spec := "SPEC" + ds := &DataSource{ + pipelineRunner: pr, + spec: spec, + lggr: lggr, + } + _, err := ds.Observe(context.Background(), ocrtypes.ReportTimestamp{}) + assert.ErrorContains(t, err, "cannot convert observation to decimal") +} diff --git a/plugins/medianpoc/plugin.go b/plugins/medianpoc/plugin.go new file mode 100644 index 00000000000..ceea1eb84f5 --- /dev/null +++ b/plugins/medianpoc/plugin.go @@ -0,0 +1,126 @@ +package medianpoc + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/smartcontractkit/libocr/offchainreporting2/reportingplugin/median" + + ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + + "github.com/smartcontractkit/chainlink-relay/pkg/logger" + "github.com/smartcontractkit/chainlink-relay/pkg/loop" + "github.com/smartcontractkit/chainlink-relay/pkg/loop/reportingplugins" + "github.com/smartcontractkit/chainlink-relay/pkg/services" + "github.com/smartcontractkit/chainlink-relay/pkg/types" + "github.com/smartcontractkit/chainlink/v2/core/utils" +) + +func NewPlugin(lggr logger.Logger) *Plugin { + return &Plugin{ + Plugin: loop.Plugin{Logger: lggr}, + MedianProviderServer: reportingplugins.MedianProviderServer{}, + stop: make(utils.StopChan), + } +} + +type Plugin struct { + loop.Plugin + stop utils.StopChan + reportingplugins.MedianProviderServer +} + +type jsonConfig struct { + Pipelines map[string]string `json:"pipelines"` +} + +func (j jsonConfig) defaultPipeline() (string, error) { + return j.getPipeline("__DEFAULT_PIPELINE__") +} + +func (j jsonConfig) getPipeline(key string) (string, error) { + v, ok := j.Pipelines[key] + if ok { + return v, nil + } + return "", fmt.Errorf("no pipeline found for %s", key) +} + +func (p *Plugin) NewReportingPluginFactory( + ctx context.Context, + config types.ReportingPluginServiceConfig, + provider types.MedianProvider, + pipelineRunner types.PipelineRunnerService, + telemetry types.TelemetryClient, + errorLog types.ErrorLog, +) (types.ReportingPluginFactory, error) { + f, err := p.newFactory(ctx, config, provider, pipelineRunner, telemetry, errorLog) + if err != nil { + return nil, err + } + s := &reportingPluginFactoryService{lggr: p.Logger, ReportingPluginFactory: f} + p.SubService(s) + return s, nil +} + +func (p *Plugin) newFactory(ctx context.Context, config types.ReportingPluginServiceConfig, provider types.MedianProvider, pipelineRunner types.PipelineRunnerService, telemetry types.TelemetryClient, errorLog types.ErrorLog) (*median.NumericalMedianFactory, error) { + jc := &jsonConfig{} + err := json.Unmarshal([]byte(config.PluginConfig), jc) + if err != nil { + return nil, err + } + + dp, err := jc.defaultPipeline() + if err != nil { + return nil, err + } + ds := &DataSource{ + pipelineRunner: pipelineRunner, + spec: dp, + lggr: p.Logger, + } + + jfp, err := jc.getPipeline("juelsPerFeeCoinPipeline") + if err != nil { + return nil, err + } + jds := &DataSource{ + pipelineRunner: pipelineRunner, + spec: jfp, + lggr: p.Logger, + } + factory := &median.NumericalMedianFactory{ + ContractTransmitter: provider.MedianContract(), + DataSource: ds, + JuelsPerFeeCoinDataSource: jds, + Logger: logger.NewOCRWrapper( + p.Logger, + true, + func(msg string) {}, + ), + OnchainConfigCodec: provider.OnchainConfigCodec(), + ReportCodec: provider.ReportCodec(), + } + return factory, nil +} + +type reportingPluginFactoryService struct { + services.StateMachine + lggr logger.Logger + ocrtypes.ReportingPluginFactory +} + +func (r *reportingPluginFactoryService) Name() string { return r.lggr.Name() } + +func (r *reportingPluginFactoryService) Start(ctx context.Context) error { + return r.StartOnce("ReportingPluginFactory", func() error { return nil }) +} + +func (r *reportingPluginFactoryService) Close() error { + return r.StopOnce("ReportingPluginFactory", func() error { return nil }) +} + +func (r *reportingPluginFactoryService) HealthReport() map[string]error { + return map[string]error{r.Name(): r.Healthy()} +} diff --git a/plugins/medianpoc/plugin_test.go b/plugins/medianpoc/plugin_test.go new file mode 100644 index 00000000000..74a0695c6c9 --- /dev/null +++ b/plugins/medianpoc/plugin_test.go @@ -0,0 +1,105 @@ +package medianpoc + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + + "github.com/smartcontractkit/libocr/offchainreporting2/reportingplugin/median" + + "github.com/smartcontractkit/chainlink-relay/pkg/types" + "github.com/smartcontractkit/chainlink/v2/core/logger" +) + +type mockErrorLog struct { + types.ErrorLog +} + +type mockOffchainConfigDigester struct { + ocrtypes.OffchainConfigDigester +} + +type mockContractTransmitter struct { + ocrtypes.ContractTransmitter +} + +type mockContractConfigTracker struct { + ocrtypes.ContractConfigTracker +} + +type mockReportCodec struct { + median.ReportCodec +} + +type mockMedianContract struct { + median.MedianContract +} + +type mockOnchainConfigCodec struct { + median.OnchainConfigCodec +} + +type provider struct { + types.Service +} + +func (p provider) OffchainConfigDigester() ocrtypes.OffchainConfigDigester { + return mockOffchainConfigDigester{} +} + +func (p provider) ContractTransmitter() ocrtypes.ContractTransmitter { + return mockContractTransmitter{} +} + +func (p provider) ContractConfigTracker() ocrtypes.ContractConfigTracker { + return mockContractConfigTracker{} +} + +func (p provider) ReportCodec() median.ReportCodec { + return mockReportCodec{} +} + +func (p provider) MedianContract() median.MedianContract { + return mockMedianContract{} +} + +func (p provider) OnchainConfigCodec() median.OnchainConfigCodec { + return mockOnchainConfigCodec{} +} + +func TestNewPlugin(t *testing.T) { + lggr := logger.TestLogger(t) + p := NewPlugin(lggr) + + defaultSpec := "default-spec" + juelsPerFeeCoinSpec := "jpfc-spec" + config := types.ReportingPluginServiceConfig{ + PluginConfig: fmt.Sprintf( + `{"pipelines": {"__DEFAULT_PIPELINE__": "%s", "juelsPerFeeCoinPipeline": "%s"}}`, + defaultSpec, + juelsPerFeeCoinSpec, + ), + } + pr := &mockPipelineRunner{} + prov := provider{} + + f, err := p.newFactory( + context.Background(), + config, + prov, + pr, + nil, + mockErrorLog{}, + ) + require.NoError(t, err) + + ds := f.DataSource.(*DataSource) + assert.Equal(t, defaultSpec, ds.spec) + jpfcDs := f.JuelsPerFeeCoinDataSource.(*DataSource) + assert.Equal(t, juelsPerFeeCoinSpec, jpfcDs.spec) +} diff --git a/tools/flakeytests/utils.go b/tools/flakeytests/utils.go index 18ab43980b3..7ead45c8587 100644 --- a/tools/flakeytests/utils.go +++ b/tools/flakeytests/utils.go @@ -29,23 +29,16 @@ func DigString(mp map[string]interface{}, path []string) (string, error) { return vs, nil } -func GetGithubMetadata(repo string, eventName string, sha string, path string) Context { - event := map[string]interface{}{} - if path != "" { - r, err := os.Open(path) - if err != nil { - log.Fatalf("Error reading gh event at path: %s", path) - } - - d, err := io.ReadAll(r) - if err != nil { - log.Fatal("Error reading gh event into string") - } +func getGithubMetadata(repo string, eventName string, sha string, e io.Reader) Context { + d, err := io.ReadAll(e) + if err != nil { + log.Fatal("Error reading gh event into string") + } - err = json.Unmarshal(d, &event) - if err != nil { - log.Fatalf("Error unmarshaling gh event at path: %s", path) - } + event := map[string]interface{}{} + err = json.Unmarshal(d, &event) + if err != nil { + log.Fatalf("Error unmarshaling gh event at path") } basicCtx := &Context{Repository: repo, CommitSHA: sha, Type: eventName} @@ -58,8 +51,27 @@ func GetGithubMetadata(repo string, eventName string, sha string, path string) C } basicCtx.PullRequestURL = prURL + + // For pull request events, the $GITHUB_SHA variable doesn't actually + // contain the sha for the latest commit, as documented here: + // https://stackoverflow.com/a/68068674 + var newSha string + s, err := DigString(event, []string{"pull_request", "head", "sha"}) + if err == nil { + newSha = s + } + + basicCtx.CommitSHA = newSha return *basicCtx default: return *basicCtx } } + +func GetGithubMetadata(repo string, eventName string, sha string, path string) Context { + event, err := os.Open(path) + if err != nil { + log.Fatalf("Error reading gh event at path: %s", path) + } + return getGithubMetadata(repo, eventName, sha, event) +} diff --git a/tools/flakeytests/utils_test.go b/tools/flakeytests/utils_test.go index d3ef8eb602d..17d597c3c02 100644 --- a/tools/flakeytests/utils_test.go +++ b/tools/flakeytests/utils_test.go @@ -1,6 +1,8 @@ package flakeytests import ( + "fmt" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -17,3 +19,30 @@ func TestDigString(t *testing.T) { require.NoError(t, err) assert.Equal(t, "some-url", out) } + +var prEventTemplate = ` +{ + "pull_request": { + "head": { + "sha": "%s" + }, + "_links": { + "html": { + "href": "%s" + } + } + } +} +` + +func TestGetGithubMetadata(t *testing.T) { + repo, eventName, sha, event := "chainlink", "merge_group", "a-sha", `{}` + ctx := getGithubMetadata(repo, eventName, sha, strings.NewReader(event)) + assert.Equal(t, Context{Repository: repo, CommitSHA: sha, Type: eventName}, ctx) + + anotherSha, eventName, url := "another-sha", "pull_request", "a-url" + event = fmt.Sprintf(prEventTemplate, anotherSha, url) + sha = "302eb05d592132309b264e316f443f1ceb81b6c3" + ctx = getGithubMetadata(repo, eventName, sha, strings.NewReader(event)) + assert.Equal(t, Context{Repository: repo, CommitSHA: anotherSha, Type: eventName, PullRequestURL: url}, ctx) +}