From cd0835c273470d3993220f3ec64086c58bdd749d Mon Sep 17 00:00:00 2001 From: Anindita Ghosh <88458927+AnieeG@users.noreply.github.com> Date: Fri, 1 Sep 2023 09:44:53 -0700 Subject: [PATCH 1/4] upgrade ctf versions (#110) --- integration-tests/ccip-tests/testsetups/ccip.go | 13 +++++-------- integration-tests/go.mod | 4 ++-- integration-tests/go.sum | 8 ++++---- 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/integration-tests/ccip-tests/testsetups/ccip.go b/integration-tests/ccip-tests/testsetups/ccip.go index 3d0c4bddc9..c078cc0ca7 100644 --- a/integration-tests/ccip-tests/testsetups/ccip.go +++ b/integration-tests/ccip-tests/testsetups/ccip.go @@ -21,23 +21,20 @@ import ( mockserver_cfg "github.com/smartcontractkit/chainlink-env/pkg/helm/mockserver-cfg" "github.com/smartcontractkit/chainlink-env/pkg/helm/reorg" "github.com/smartcontractkit/chainlink-testing-framework/blockchain" + "github.com/smartcontractkit/chainlink-testing-framework/networks" "github.com/smartcontractkit/chainlink-testing-framework/utils" "github.com/stretchr/testify/require" "go.uber.org/multierr" "go.uber.org/zap/zapcore" "golang.org/x/sync/errgroup" - ccipnode "github.com/smartcontractkit/chainlink/integration-tests/ccip-tests/types/config/node" - integrationactions "github.com/smartcontractkit/chainlink/integration-tests/actions" - "github.com/smartcontractkit/chainlink/integration-tests/docker/test_env" - "github.com/smartcontractkit/chainlink/integration-tests/networks" - "github.com/smartcontractkit/chainlink/integration-tests/types/config/node" - - "github.com/smartcontractkit/chainlink/integration-tests/ccip-tests/contracts/laneconfig" - "github.com/smartcontractkit/chainlink/integration-tests/ccip-tests/actions" + "github.com/smartcontractkit/chainlink/integration-tests/ccip-tests/contracts/laneconfig" "github.com/smartcontractkit/chainlink/integration-tests/ccip-tests/testreporters" + ccipnode "github.com/smartcontractkit/chainlink/integration-tests/ccip-tests/types/config/node" + "github.com/smartcontractkit/chainlink/integration-tests/docker/test_env" + "github.com/smartcontractkit/chainlink/integration-tests/types/config/node" ) const ( diff --git a/integration-tests/go.mod b/integration-tests/go.mod index c15e1055ab..9ce140bb67 100644 --- a/integration-tests/go.mod +++ b/integration-tests/go.mod @@ -21,7 +21,7 @@ require ( github.com/slack-go/slack v0.12.2 github.com/smartcontractkit/chain-selectors v1.0.1 github.com/smartcontractkit/chainlink-env v0.36.0 - github.com/smartcontractkit/chainlink-testing-framework v1.16.1-0.20230825001100-85c8b45d8005 + github.com/smartcontractkit/chainlink-testing-framework v1.16.2 github.com/smartcontractkit/chainlink/integration-tests v0.0.0-20230828183543-6d0939746966 github.com/smartcontractkit/chainlink/v2 v2.0.0-00010101000000-000000000000 github.com/smartcontractkit/libocr v0.0.0-20230816220705-665e93233ae5 @@ -367,7 +367,7 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/pressly/goose/v3 v3.15.0 // indirect - github.com/prometheus/alertmanager v0.25.0 // indirect + github.com/prometheus/alertmanager v0.25.1 // indirect github.com/prometheus/client_golang v1.16.0 // indirect github.com/prometheus/client_model v0.4.0 // indirect github.com/prometheus/common v0.44.0 // indirect diff --git a/integration-tests/go.sum b/integration-tests/go.sum index 30fa9e179c..32ea030905 100644 --- a/integration-tests/go.sum +++ b/integration-tests/go.sum @@ -2127,8 +2127,8 @@ github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/pressly/goose/v3 v3.15.0 h1:6tY5aDqFknY6VZkorFGgZtWygodZQxfmmEF4rqyJW9k= github.com/pressly/goose/v3 v3.15.0/go.mod h1:LlIo3zGccjb/YUgG+Svdb9Er14vefRdlDI7URCDrwYo= -github.com/prometheus/alertmanager v0.25.0 h1:vbXKUR6PYRiZPRIKfmXaG+dmCKG52RtPL4Btl8hQGvg= -github.com/prometheus/alertmanager v0.25.0/go.mod h1:MEZ3rFVHqKZsw7IcNS/m4AWZeXThmJhumpiWR4eHU/w= +github.com/prometheus/alertmanager v0.25.1 h1:LGBNMspOfv8h7brb+LWj2wnwBCg2ZuuKWTh6CAVw2/Y= +github.com/prometheus/alertmanager v0.25.1/go.mod h1:MEZ3rFVHqKZsw7IcNS/m4AWZeXThmJhumpiWR4eHU/w= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= @@ -2269,8 +2269,8 @@ github.com/smartcontractkit/chainlink-solana v1.0.3-0.20230802143301-165000751a8 github.com/smartcontractkit/chainlink-solana v1.0.3-0.20230802143301-165000751a85/go.mod h1:H3/j2l84FsxYevCLNERdVasI7FVr+t2mkpv+BCJLSVw= github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20230802150127-d2c95679d61a h1:b3rjvZLpTV45TmCV+ALX+EDDslf91pnDUugP54Lu9FA= github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20230802150127-d2c95679d61a/go.mod h1:LL+FLf10gOUHrF3aUsRGEZlT/w8DaW5T/eEo/54W68c= -github.com/smartcontractkit/chainlink-testing-framework v1.16.1-0.20230825001100-85c8b45d8005 h1:c1RWSbfF+rvkxAcwrXEJVGiIr3cpdZb+zok8o+qEWwQ= -github.com/smartcontractkit/chainlink-testing-framework v1.16.1-0.20230825001100-85c8b45d8005/go.mod h1:t6FJX3akEfAO31p96ru0ilNPfE9P2UshUlXTIkI58LM= +github.com/smartcontractkit/chainlink-testing-framework v1.16.2 h1:+m/8wd443+ZpRL+GS86dVnMDeZ6+pnut0uVTZhrtIvU= +github.com/smartcontractkit/chainlink-testing-framework v1.16.2/go.mod h1:xtLIwNaVw/4zWSMnA7j8u1t9tKh0OykvIsYI4xZT3B4= github.com/smartcontractkit/go-plugin v0.0.0-20230605132010-0f4d515d1472 h1:x3kNwgFlDmbE/n0gTSRMt9GBDfsfGrs4X9b9arPZtFI= github.com/smartcontractkit/go-plugin v0.0.0-20230605132010-0f4d515d1472/go.mod h1:6/1TEzT0eQznvI/gV2CM29DLSkAK/e58mUWKVsPaph0= github.com/smartcontractkit/grpc-proxy v0.0.0-20230731113816-f1be6620749f h1:hgJif132UCdjo8u43i7iPN1/MFnu49hv7lFGFftCHKU= From b93b97714f636d84b0e7fe2525a0951a79e4ed1c Mon Sep 17 00:00:00 2001 From: dimitris Date: Mon, 4 Sep 2023 11:05:37 +0300 Subject: [PATCH 2/4] Offchain - Keep token decimals in cache (#88) --- .../ocr2/plugins/ccip/cache/tokens.go | 25 +++++++++++++++++++ .../ocr2/plugins/ccip/cache/tokens_test.go | 25 +++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/core/services/ocr2/plugins/ccip/cache/tokens.go b/core/services/ocr2/plugins/ccip/cache/tokens.go index fa5cefa3dc..5a53cf964b 100644 --- a/core/services/ocr2/plugins/ccip/cache/tokens.go +++ b/core/services/ocr2/plugins/ccip/cache/tokens.go @@ -196,6 +196,7 @@ type tokenToDecimals struct { offRamp evm_2_evm_offramp.EVM2EVMOffRampInterface priceRegistry price_registry.PriceRegistryInterface tokenFactory func(address common.Address) (link_token_interface.LinkTokenInterface, error) + tokenDecimals sync.Map } func (t *tokenToDecimals) Copy(value map[common.Address]uint8) map[common.Address]uint8 { @@ -225,6 +226,11 @@ func (t *tokenToDecimals) CallOrigin(ctx context.Context) (map[common.Address]ui } for _, token := range destTokens { + if decimals, exists := t.getCachedDecimals(token); exists { + mapping[token] = decimals + continue + } + tokenContract, err := t.tokenFactory(token) if err != nil { return nil, err @@ -235,7 +241,26 @@ func (t *tokenToDecimals) CallOrigin(ctx context.Context) (map[common.Address]ui return nil, fmt.Errorf("get token %s decimals: %w", token, err) } + t.setCachedDecimals(token, decimals) mapping[token] = decimals } return mapping, nil } + +func (t *tokenToDecimals) getCachedDecimals(token common.Address) (uint8, bool) { + rawVal, exists := t.tokenDecimals.Load(token.String()) + if !exists { + return 0, false + } + + decimals, isUint8 := rawVal.(uint8) + if !isUint8 { + return 0, false + } + + return decimals, true +} + +func (t *tokenToDecimals) setCachedDecimals(token common.Address, decimals uint8) { + t.tokenDecimals.Store(token.String(), decimals) +} diff --git a/core/services/ocr2/plugins/ccip/cache/tokens_test.go b/core/services/ocr2/plugins/ccip/cache/tokens_test.go index 082781f286..b562c44563 100644 --- a/core/services/ocr2/plugins/ccip/cache/tokens_test.go +++ b/core/services/ocr2/plugins/ccip/cache/tokens_test.go @@ -2,6 +2,7 @@ package cache import ( "context" + "fmt" "math/big" "testing" @@ -15,6 +16,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/generated/link_token_interface" "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/utils" ) func Test_tokenToDecimals(t *testing.T) { @@ -95,6 +97,15 @@ func Test_tokenToDecimals(t *testing.T) { require.NoError(t, err) assert.Equal(t, tt.want, got) + + // we set token factory to always return an error + // we don't expect it to be used again, decimals should be in cache. + tokenToDecimal.tokenFactory = func(address common.Address) (link_token_interface.LinkTokenInterface, error) { + return nil, fmt.Errorf("some error") + } + got, err = tokenToDecimal.CallOrigin(testutils.Context(t)) + require.NoError(t, err) + assert.Equal(t, tt.want, got) }) } } @@ -188,6 +199,20 @@ func Test_copyMap(t *testing.T) { }) } +func Test_cachedDecimals(t *testing.T) { + tokenDecimalsCache := &tokenToDecimals{} + addr := utils.RandomAddress() + + decimals, exists := tokenDecimalsCache.getCachedDecimals(addr) + assert.Zero(t, decimals) + assert.False(t, exists) + + tokenDecimalsCache.setCachedDecimals(addr, 123) + decimals, exists = tokenDecimalsCache.getCachedDecimals(addr) + assert.Equal(t, uint8(123), decimals) + assert.True(t, exists) +} + func createTokenFactory(decimalMapping map[common.Address]uint8) func(address common.Address) (link_token_interface.LinkTokenInterface, error) { return func(address common.Address) (link_token_interface.LinkTokenInterface, error) { linkToken := &mock_contracts.LinkTokenInterface{} From f7a0cf2108882940ef7284b6e469a73cfb43ae46 Mon Sep 17 00:00:00 2001 From: Rens Rooimans Date: Mon, 4 Sep 2023 17:54:46 +0200 Subject: [PATCH 3/4] Revert reason scripts: rm duplicate code, improve error handling (#100) --- .gitignore | 4 +- core/scripts/ccip/ccip-revert-reason/main.go | 198 +++++------------- .../revert-reason/command/revert_reason.go | 10 +- .../ccip/revert-reason/handler/reason.go | 111 ++++++---- .../ccip/revert-reason/handler/reason_test.go | 10 +- core/scripts/ccip/secrets/secrets.go | 3 +- core/scripts/go.mod | 2 + core/scripts/go.sum | 1 + 8 files changed, 136 insertions(+), 203 deletions(-) diff --git a/.gitignore b/.gitignore index e1621e97c3..de57cfb672 100644 --- a/.gitignore +++ b/.gitignore @@ -93,4 +93,6 @@ contracts/yarn.lock # Ignore DevSpace cache and log folder .devspace/ -core/scripts/ccip/json/credentials \ No newline at end of file +/core/scripts/ccip/json/credentials +/core/scripts/ccip/revert-reason/bin/ccip-revert-reason + diff --git a/core/scripts/ccip/ccip-revert-reason/main.go b/core/scripts/ccip/ccip-revert-reason/main.go index 48219def4a..df8ab67742 100644 --- a/core/scripts/ccip/ccip-revert-reason/main.go +++ b/core/scripts/ccip/ccip-revert-reason/main.go @@ -1,173 +1,71 @@ package main import ( - "bytes" - "context" - "encoding/hex" - "encoding/json" "fmt" - "strings" - "github.com/ethereum/go-ethereum" - "github.com/ethereum/go-ethereum/accounts/abi" - "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethclient" + "github.com/joho/godotenv" + "github.com/smartcontractkit/chainlink/core/scripts/ccip/revert-reason/handler" "github.com/smartcontractkit/chainlink/core/scripts/ccip/secrets" - "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/arm_contract" - "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/burn_mint_token_pool" - "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/commit_store" - "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_offramp" - "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_onramp" - "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/lock_release_token_pool" - "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/price_registry" - "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/router" ) -func panicErr(err error) { +// How to use +// Set either an error code string OR set the chainId, txHash and txRequester. +// Setting an error code allows the script to run offline and doesn't require any RPC +// endpoint. Using the chainId, txHash and txRequester requires an RPC endpoint, and if +// the tx is old, the node needs to run in archive mode. +// +// Set the variable(s) and run main.go. The script will try to match the error code to the +// ABIs of various CCIP contracts. If it finds a match, it will check if it's a CCIP wrapped error +// like ExecutionError and TokenRateLimitError, and if so, it will decode the inner error. +// +// To configure an RPC endpoint, set the RPC_ environment variable to the RPC endpoint. +// e.g. RPC_420=https://rpc..com +const ( + ErrorCodeString = "0x4e487b710000000000000000000000000000000000000000000000000000000000000032" + + // The following inputs are only used if ERROR_CODE_STRING is empty + // Need a node URL + // NOTE: this node needs to run in archive mode if the tx is old + ChainId = uint64(420) + TxHash = "0x97be8559164442595aba46b5f849c23257905b78e72ee43d9b998b28eee78b84" + TxRequester = "0xe88ff73814fb891bb0e149f5578796fa41f20242" + EnvFileName = ".env" +) + +func main() { + errorString, err := getErrorString() if err != nil { panic(err) } -} - -// You can either add an error string (like "0x4e487b710000000000000000000000000000000000000000000000000000000000000032") -// or you can specify an ethURL, txHash and requester. -func main() { - errorCodeString := "" - - if errorCodeString == "" { - // Need a node URL - // NOTE: this node needs to run in archive mode - ethUrl := secrets.GetRPC(420) - txHash := "0x97be8559164442595aba46b5f849c23257905b78e72ee43d9b998b28eee78b84" - requester := "0xe88ff73814fb891bb0e149f5578796fa41f20242" - - ec, ethErr := ethclient.Dial(ethUrl) - panicErr(ethErr) - errorString, _ := getErrorForTx(ec, txHash, requester) - // Some nodes prepend "Reverted " and we also remove the 0x - trimmed := strings.TrimPrefix(errorString, "Reverted ")[2:] - - contractABIs := getAllABIs() - - DecodeErrorStringFromABI(trimmed, contractABIs) - } else { - errorCodeString = strings.TrimPrefix(errorCodeString, "0x") - DecodeErrorStringFromABI(errorCodeString, getAllABIs()) + decodedError, err := handler.DecodeErrorStringFromABI(errorString) + if err != nil { + panic(err) } -} -func DecodeErrorStringFromABI(errorString string, contractABIs []string) { - data, err := hex.DecodeString(errorString) - panicErr(err) - - for _, contractABI := range contractABIs { - parsedAbi, err2 := abi.JSON(strings.NewReader(contractABI)) - panicErr(err2) + fmt.Println(decodedError) +} - for errorName, abiError := range parsedAbi.Errors { - if bytes.Equal(data[:4], abiError.ID.Bytes()[:4]) { - // Found a matching error - v, err3 := abiError.Unpack(data) - panicErr(err3) +func getErrorString() (string, error) { + errorCodeString := ErrorCodeString - // If exec error, the actual error is within the revert reason - if errorName == "ExecutionError" { - // Get the inner type, which is `bytes` - fmt.Printf("Error is \"%v\" inner error: ", errorName) - errorBytes := v.([]interface{})[0].([]byte) - DecodeErrorStringFromABI(hex.EncodeToString(errorBytes), contractABIs) - return - } - fmt.Printf("Error is \"%v\" args %v\n", errorName, v) - return - } + if errorCodeString == "" { + // Try to load env vars from .env file + err := godotenv.Load(EnvFileName) + if err != nil { + fmt.Println("No .env file found, using env vars from shell") } - } - if len(errorString) > 8 && errorString[:8] == "4e487b71" { - fmt.Println("Assertion failure") - indicator := errorString[len(errorString)-2:] - switch indicator { - case "01": - fmt.Printf("If you call assert with an argument that evaluates to false.") - case "11": - fmt.Printf("If an arithmetic operation results in underflow or overflow outside of an unchecked { ... } block.") - case "12": - fmt.Printf("If you divide or modulo by zero (e.g. 5 / 0 or 23 modulo 0).") - case "21": - fmt.Printf("If you convert a value that is too big or negative into an enum type.") - case "31": - fmt.Printf("If you call .pop() on an empty array.") - case "32": - fmt.Printf("If you access an array, bytesN or an array slice at an out-of-bounds or negative index (i.e. x[i] where i >= x.length or i < 0).") - case "41": - fmt.Printf("If you allocate too much memory or create an array that is too large.") - case "51": - fmt.Printf("If you call a zero-initialized variable of internal function type.") - default: - fmt.Printf("This is a revert produced by an assertion failure. Exact code not found \"%s\"", indicator) + ec, err := ethclient.Dial(secrets.GetRPC(ChainId)) + if err != nil { + return "", err + } + errorCodeString, err = handler.GetErrorForTx(ec, TxHash, TxRequester) + if err != nil { + return "", err } - return - } - - stringErr, err := abi.UnpackRevert(data) - if err == nil { - fmt.Print("string error: ") - fmt.Printf("%s\n", stringErr) - return - } - - fmt.Printf("Cannot match error with contract ABI. Error code \"%v\"\n", errorString) -} - -func getAllABIs() []string { - return []string{ - arm_contract.ARMContractABI, - lock_release_token_pool.LockReleaseTokenPoolABI, - burn_mint_token_pool.BurnMintTokenPoolABI, - commit_store.CommitStoreABI, - price_registry.PriceRegistryABI, - evm_2_evm_onramp.EVM2EVMOnRampABI, - evm_2_evm_offramp.EVM2EVMOffRampABI, - router.RouterABI, - } -} - -func getErrorForTx(client *ethclient.Client, txHash string, requester string) (string, common.Address) { - tx, _, err := client.TransactionByHash(context.Background(), common.HexToHash(txHash)) - panicErr(err) - re, err := client.TransactionReceipt(context.Background(), common.HexToHash(txHash)) - panicErr(err) - - call := ethereum.CallMsg{ - From: common.HexToAddress(requester), - To: tx.To(), - Data: tx.Data(), - Gas: tx.Gas(), - GasPrice: tx.GasPrice(), - } - _, err = client.CallContract(context.Background(), call, re.BlockNumber) - if err == nil { - panic("no error calling contract") - } - - return parseError(err), *tx.To() -} - -func parseError(txError error) string { - b, err := json.Marshal(txError) - panicErr(err) - var callErr struct { - Code int - Data string `json:"data"` - Message string `json:"message"` } - err = json.Unmarshal(b, &callErr) - panicErr(err) - if callErr.Data == "" && strings.Contains(callErr.Message, "missing trie node") { - panic("Use an archive node") - } - return callErr.Data + return errorCodeString, nil } diff --git a/core/scripts/ccip/revert-reason/command/revert_reason.go b/core/scripts/ccip/revert-reason/command/revert_reason.go index 2672b6a40a..7258eaba71 100644 --- a/core/scripts/ccip/revert-reason/command/revert_reason.go +++ b/core/scripts/ccip/revert-reason/command/revert_reason.go @@ -26,10 +26,16 @@ var RevertReasonCmd = &cobra.Command{ } if decodeFromError { - result := baseHandler.RevertReasonFromErrorCodeString(args[0]) + result, err := baseHandler.RevertReasonFromErrorCodeString(args[0]) + if err != nil { + log.Fatal("failed to decode error code string: ", err) + } fmt.Print(result) } else { - result := baseHandler.RevertReasonFromTx(args[0]) + result, err := baseHandler.RevertReasonFromTx(args[0]) + if err != nil { + log.Fatal("failed to decode error code string: ", err) + } fmt.Print(result) } }, diff --git a/core/scripts/ccip/revert-reason/handler/reason.go b/core/scripts/ccip/revert-reason/handler/reason.go index 146ac51af6..4a72cfa001 100644 --- a/core/scripts/ccip/revert-reason/handler/reason.go +++ b/core/scripts/ccip/revert-reason/handler/reason.go @@ -22,91 +22,101 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/lock_release_token_pool" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/price_registry" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/router" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/usdc_token_pool" ) // RevertReasonFromErrorCodeString attempts to decode an error code string -func (h *BaseHandler) RevertReasonFromErrorCodeString(errorCodeString string) string { +func (h *BaseHandler) RevertReasonFromErrorCodeString(errorCodeString string) (string, error) { errorCodeString = strings.TrimPrefix(errorCodeString, "0x") - return decodeErrorStringFromABI(errorCodeString, getAllABIs()) + return DecodeErrorStringFromABI(errorCodeString) } // RevertReasonFromTx attempts to fetch more info on failed TX -func (h *BaseHandler) RevertReasonFromTx(txHash string) string { +func (h *BaseHandler) RevertReasonFromTx(txHash string) (string, error) { // Need a node URL // NOTE: this node needs to run in archive mode ethUrl := h.cfg.NodeURL if ethUrl == "" { - panicErr(errors.New("You must define ETH_NODE env variable")) + panicErr(errors.New("you must define ETH_NODE env variable")) } requester := h.cfg.FromAddress - ec, ethErr := ethclient.Dial(ethUrl) - panicErr(ethErr) - errorString, _ := getErrorForTx(ec, txHash, requester) - // Some nodes prepend "Reverted " and we also remove the 0x - trimmed := strings.TrimPrefix(errorString, "Reverted ")[2:] - - contractABIs := getAllABIs() + ec, err := ethclient.Dial(ethUrl) + panicErr(err) + errorString, _ := GetErrorForTx(ec, txHash, requester) - return decodeErrorStringFromABI(trimmed, contractABIs) + return DecodeErrorStringFromABI(errorString) } -func decodeErrorStringFromABI(errorString string, contractABIs []string) string { - builder := strings.Builder{} +func DecodeErrorStringFromABI(errorString string) (string, error) { + contractABIs := getAllABIs() + + // Sanitize error string + errorString = strings.TrimPrefix(errorString, "Reverted ") + errorString = strings.TrimPrefix(errorString, "0x") data, err := hex.DecodeString(errorString) - panicErr(err) + if err != nil { + return "", errors.Wrap(err, "error decoding error string") + } for _, contractABI := range contractABIs { parsedAbi, err2 := abi.JSON(strings.NewReader(contractABI)) - panicErr(err2) + if err2 != nil { + return "", errors.Wrap(err2, "error loading ABI") + } - for k, abiError := range parsedAbi.Errors { + for errorName, abiError := range parsedAbi.Errors { if bytes.Equal(data[:4], abiError.ID.Bytes()[:4]) { // Found a matching error v, err3 := abiError.Unpack(data) - panicErr(err3) - builder.WriteString(fmt.Sprintf("Error is \"%v\" args %v\n", k, v)) - return builder.String() + if err3 != nil { + return "", errors.Wrap(err3, "error unpacking data") + } + + // If exec error, the actual error is within the revert reason + if errorName == "ExecutionError" || errorName == "TokenRateLimitError" { + // Get the inner type, which is `bytes` + fmt.Printf("Error is \"%v\" inner error: ", errorName) + errorBytes := v.([]interface{})[0].([]byte) + return DecodeErrorStringFromABI(hex.EncodeToString(errorBytes)) + } + return fmt.Sprintf("error is \"%v\" args %v\n", errorName, v), nil } } } if len(errorString) > 8 && errorString[:8] == "4e487b71" { - builder.WriteString("Decoded error: Assertion failure\n") + fmt.Println("Assertion failure") indicator := errorString[len(errorString)-2:] switch indicator { case "01": - builder.WriteString("If you call assert with an argument that evaluates to false.\n") + return fmt.Sprintf("If you call assert with an argument that evaluates to false."), nil case "11": - builder.WriteString("If an arithmetic operation results in underflow or overflow outside of an unchecked { ... } block.\n") + return fmt.Sprintf("If an arithmetic operation results in underflow or overflow outside of an unchecked { ... } block."), nil case "12": - builder.WriteString("If you divide or modulo by zero (e.g. 5 / 0 or 23 modulo 0).\n") + return fmt.Sprintf("If you divide or modulo by zero (e.g. 5 / 0 or 23 modulo 0)."), nil case "21": - builder.WriteString("If you convert a value that is too big or negative into an enum type.\n") + return fmt.Sprintf("If you convert a value that is too big or negative into an enum type."), nil case "31": - builder.WriteString("If you call .pop() on an empty array.\n") + return fmt.Sprintf("If you call .pop() on an empty array."), nil case "32": - builder.WriteString("If you access an array, bytesN or an array slice at an out-of-bounds or negative index (i.e. x[i] where i >= x.length or i < 0).\n") + return fmt.Sprintf("If you access an array, bytesN or an array slice at an out-of-bounds or negative index (i.e. x[i] where i >= x.length or i < 0)."), nil case "41": - builder.WriteString("If you allocate too much memory or create an array that is too large.\n") + return fmt.Sprintf("If you allocate too much memory or create an array that is too large."), nil case "51": - builder.WriteString("If you call a zero-initialized variable of internal function type.\n") + return fmt.Sprintf("If you call a zero-initialized variable of internal function type."), nil default: - builder.WriteString(fmt.Sprintf("This is a revert produced by an assertion failure. Exact code not found \"%s\"\n", indicator)) + return fmt.Sprintf("This is a revert produced by an assertion failure. Exact code not found \"%s\"", indicator), nil } - return builder.String() } stringErr, err := abi.UnpackRevert(data) if err == nil { - builder.WriteString("String error thrown") - builder.WriteString(fmt.Sprintf("error: %s", stringErr)) - return builder.String() + return fmt.Sprintf("string error: %s", stringErr), nil } - builder.WriteString(fmt.Sprintf("Cannot match error with contract ABI. Error code \"%v\"\n", "trimmed")) - return builder.String() + return "", errors.Errorf("Cannot match error with contract ABI. Error code \"%v\"\n", errorString) } func getAllABIs() []string { @@ -114,6 +124,7 @@ func getAllABIs() []string { arm_contract.ARMContractABI, lock_release_token_pool.LockReleaseTokenPoolABI, burn_mint_token_pool.BurnMintTokenPoolABI, + usdc_token_pool.USDCTokenPoolABI, commit_store.CommitStoreABI, price_registry.PriceRegistryABI, evm_2_evm_onramp.EVM2EVMOnRampABI, @@ -122,11 +133,15 @@ func getAllABIs() []string { } } -func getErrorForTx(client *ethclient.Client, txHash string, requester string) (string, common.Address) { +func GetErrorForTx(client *ethclient.Client, txHash string, requester string) (string, error) { tx, _, err := client.TransactionByHash(context.Background(), common.HexToHash(txHash)) - panicErr(err) + if err != nil { + return "", errors.Wrap(err, "error getting transaction from hash") + } re, err := client.TransactionReceipt(context.Background(), common.HexToHash(txHash)) - panicErr(err) + if err != nil { + return "", errors.Wrap(err, "error getting transaction receipt") + } call := ethereum.CallMsg{ From: common.HexToAddress(requester), @@ -140,24 +155,28 @@ func getErrorForTx(client *ethclient.Client, txHash string, requester string) (s panic("no error calling contract") } - return parseError(err), *tx.To() + return parseError(err) } -func parseError(txError error) string { +func parseError(txError error) (string, error) { b, err := json.Marshal(txError) - panicErr(err) + if err != nil { + return "", err + } var callErr struct { Code int Data string `json:"data"` Message string `json:"message"` } - err = json.Unmarshal(b, &callErr) - panicErr(err) + if json.Unmarshal(b, &callErr) != nil { + return "", err + } if callErr.Data == "" && strings.Contains(callErr.Message, "missing trie node") { - panic("Use an archive node") + return "", errors.Errorf("please use an archive node") } - return callErr.Data + + return callErr.Data, nil } func panicErr(err error) { diff --git a/core/scripts/ccip/revert-reason/handler/reason_test.go b/core/scripts/ccip/revert-reason/handler/reason_test.go index 5d49187992..4a9363550c 100644 --- a/core/scripts/ccip/revert-reason/handler/reason_test.go +++ b/core/scripts/ccip/revert-reason/handler/reason_test.go @@ -26,7 +26,9 @@ func Test_RevertReasonFromTx(t *testing.T) { h := &BaseHandler{ cfg: tt.fields.cfg, } - require.Equal(t, tt.expected, h.RevertReasonFromTx(tt.args.txHash)) + got, err := h.RevertReasonFromTx(tt.args.txHash) + require.NoError(t, err) + require.Equal(t, tt.expected, got) }) } } @@ -50,7 +52,7 @@ func Test_RevertReasonFromErrorCodeString(t *testing.T) { args: args{ errorCodeString: "0x4e487b710000000000000000000000000000000000000000000000000000000000000032", }, - expected: "Decoded error: Assertion failure\nIf you access an array, bytesN or an array slice at an out-of-bounds or negative index (i.e. x[i] where i >= x.length or i < 0).\n", + expected: "If you access an array, bytesN or an array slice at an out-of-bounds or negative index (i.e. x[i] where i >= x.length or i < 0).", }, } for _, tt := range tests { @@ -58,7 +60,9 @@ func Test_RevertReasonFromErrorCodeString(t *testing.T) { h := &BaseHandler{ cfg: tt.fields.cfg, } - require.Equal(t, tt.expected, h.RevertReasonFromErrorCodeString(tt.args.errorCodeString)) + got, err := h.RevertReasonFromErrorCodeString(tt.args.errorCodeString) + require.NoError(t, err) + require.Equal(t, tt.expected, got) }) } } diff --git a/core/scripts/ccip/secrets/secrets.go b/core/scripts/ccip/secrets/secrets.go index 118aa739a7..e47f644b37 100644 --- a/core/scripts/ccip/secrets/secrets.go +++ b/core/scripts/ccip/secrets/secrets.go @@ -1,6 +1,7 @@ package secrets import ( + "fmt" "os" "strconv" ) @@ -11,5 +12,5 @@ func GetRPC(chainID uint64) string { if rpc != "" { return rpc } - panic("RPC not found. Please check secrets.go for chainID " + strconv.FormatUint(chainID, 10)) + panic(fmt.Errorf("RPC not found. Please set the environment variable for chain %d e.g. RPC_420=https://rpc.420.com", chainID)) } diff --git a/core/scripts/go.mod b/core/scripts/go.mod index 2d2953cec6..0070400e19 100644 --- a/core/scripts/go.mod +++ b/core/scripts/go.mod @@ -39,6 +39,8 @@ require ( gonum.org/v1/gonum v0.13.0 // indirect ) +require github.com/joho/godotenv v1.4.0 + require ( contrib.go.opencensus.io/exporter/stackdriver v0.13.5 // indirect cosmossdk.io/api v0.3.1 // indirect diff --git a/core/scripts/go.sum b/core/scripts/go.sum index d7f7b8fec0..e8bf49d190 100644 --- a/core/scripts/go.sum +++ b/core/scripts/go.sum @@ -808,6 +808,7 @@ github.com/jmhodges/levigo v1.0.0/go.mod h1:Q6Qx+uH3RAqyK4rFQroq9RL7mdkABMcfhEI+ github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= +github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= From d1c8f6c61c6d74eabfb51833cd53315825fb5b5e Mon Sep 17 00:00:00 2001 From: dimitris Date: Tue, 5 Sep 2023 13:10:52 +0300 Subject: [PATCH 4/4] offchain - create "ccipevents" logPoller abstraction layer (#108) --- .../ocr2/plugins/ccip/ccipevents/client.go | 44 +++ .../ocr2/plugins/ccip/ccipevents/logpoller.go | 299 ++++++++++++++++++ .../plugins/ccip/ccipevents/logpoller_test.go | 136 ++++++++ .../ocr2/plugins/ccip/commit_plugin.go | 15 +- .../plugins/ccip/commit_reporting_plugin.go | 131 +++----- .../ccip/commit_reporting_plugin_test.go | 6 +- .../plugins/ccip/execution_batch_building.go | 22 +- .../ocr2/plugins/ccip/execution_plugin.go | 4 + .../ccip/execution_reporting_plugin.go | 85 ++--- .../ccip/execution_reporting_plugin_test.go | 5 + .../ocr2/plugins/ccip/plugins_common.go | 18 +- .../testhelpers/plugins/plugin_harness.go | 8 +- 12 files changed, 592 insertions(+), 181 deletions(-) create mode 100644 core/services/ocr2/plugins/ccip/ccipevents/client.go create mode 100644 core/services/ocr2/plugins/ccip/ccipevents/logpoller.go create mode 100644 core/services/ocr2/plugins/ccip/ccipevents/logpoller_test.go diff --git a/core/services/ocr2/plugins/ccip/ccipevents/client.go b/core/services/ocr2/plugins/ccip/ccipevents/client.go new file mode 100644 index 0000000000..d3419b744a --- /dev/null +++ b/core/services/ocr2/plugins/ccip/ccipevents/client.go @@ -0,0 +1,44 @@ +package ccipevents + +import ( + "context" + "time" + + "github.com/ethereum/go-ethereum/common" + + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_offramp" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_onramp" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/price_registry" +) + +type Event[T any] struct { + Data T + BlockMeta +} + +type BlockMeta struct { + BlockTimestamp time.Time + BlockNumber int64 +} + +// Client can be used to fetch CCIP related parsed on-chain events. +type Client interface { + // GetSendRequestsGteSeqNum returns all the message send requests with sequence number greater than or equal to the provided. + // If checkFinalityTags is set to true then confs param is ignored, the latest finalized block is used in the query. + GetSendRequestsGteSeqNum(ctx context.Context, onRamp common.Address, seqNum uint64, checkFinalityTags bool, confs int) ([]Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested], error) + + // GetSendRequestsBetweenSeqNums returns all the message send requests in the provided sequence numbers range (inclusive). + GetSendRequestsBetweenSeqNums(ctx context.Context, onRamp common.Address, seqNumMin, seqNumMax uint64, confs int) ([]Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested], error) + + // GetTokenPriceUpdatesCreatedAfter returns all the token price updates that happened after the provided timestamp. + GetTokenPriceUpdatesCreatedAfter(ctx context.Context, priceRegistry common.Address, ts time.Time, confs int) ([]Event[price_registry.PriceRegistryUsdPerTokenUpdated], error) + + // GetGasPriceUpdatesCreatedAfter returns all the gas price updates that happened after the provided timestamp. + GetGasPriceUpdatesCreatedAfter(ctx context.Context, priceRegistry common.Address, chainSelector uint64, ts time.Time, confs int) ([]Event[price_registry.PriceRegistryUsdPerUnitGasUpdated], error) + + // GetExecutionStateChangesBetweenSeqNums returns all the execution state change events for the provided message sequence numbers (inclusive). + GetExecutionStateChangesBetweenSeqNums(ctx context.Context, offRamp common.Address, seqNumMin, seqNumMax uint64, confs int) ([]Event[evm_2_evm_offramp.EVM2EVMOffRampExecutionStateChanged], error) + + // LatestBlock returns the latest known/parsed block of the underlying implementation. + LatestBlock(ctx context.Context) (int64, error) +} diff --git a/core/services/ocr2/plugins/ccip/ccipevents/logpoller.go b/core/services/ocr2/plugins/ccip/ccipevents/logpoller.go new file mode 100644 index 0000000000..2fe7411d01 --- /dev/null +++ b/core/services/ocr2/plugins/ccip/ccipevents/logpoller.go @@ -0,0 +1,299 @@ +package ccipevents + +import ( + "context" + "fmt" + "math/big" + "sync" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/rpc" + "github.com/pkg/errors" + + evmclient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_offramp" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_onramp" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/price_registry" + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/abihelpers" + "github.com/smartcontractkit/chainlink/v2/core/services/pg" +) + +var _ Client = &LogPollerClient{} + +// LogPollerClient implements the Client interface by using a logPoller instance to fetch the events. +type LogPollerClient struct { + lp logpoller.LogPoller + lggr logger.Logger + client evmclient.Client + + dependencyCache sync.Map +} + +func NewLogPollerClient(lp logpoller.LogPoller, lggr logger.Logger, client evmclient.Client) *LogPollerClient { + return &LogPollerClient{ + lp: lp, + lggr: lggr, + client: client, + } +} + +func (c *LogPollerClient) GetSendRequestsGteSeqNum(ctx context.Context, onRampAddress common.Address, seqNum uint64, checkFinalityTags bool, confs int) (sendReqs []Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested], err error) { + onRamp, err := c.loadOnRamp(onRampAddress) + if err != nil { + return nil, err + } + + if !checkFinalityTags { + logs, err2 := c.lp.LogsDataWordGreaterThan( + abihelpers.EventSignatures.SendRequested, + onRampAddress, + abihelpers.EventSignatures.SendRequestedSequenceNumberWord, + abihelpers.EvmWord(seqNum), + confs, + pg.WithParentCtx(ctx), + ) + if err2 != nil { + return nil, fmt.Errorf("logs data word greater than: %w", err2) + } + return parseLogs[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested]( + logs, + c.lggr, + func(log types.Log) (*evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested, error) { + return onRamp.ParseCCIPSendRequested(log) + }, + ) + } + + // If the chain is based on explicit finality we only examine logs less than or equal to the latest finalized block number. + // NOTE: there appears to be a bug in ethclient whereby BlockByNumber fails with "unsupported txtype" when trying to parse the block + // when querying L2s, headers however work. + // TODO (CCIP-778): Migrate to core finalized tags, below doesn't work for some chains e.g. Celo. + latestFinalizedHeader, err := c.client.HeaderByNumber( + ctx, + big.NewInt(rpc.FinalizedBlockNumber.Int64()), + ) + if err != nil { + return nil, err + } + + if latestFinalizedHeader == nil { + return nil, errors.New("latest finalized header is nil") + } + if latestFinalizedHeader.Number == nil { + return nil, errors.New("latest finalized number is nil") + } + logs, err := c.lp.LogsUntilBlockHashDataWordGreaterThan( + abihelpers.EventSignatures.SendRequested, + onRampAddress, + abihelpers.EventSignatures.SendRequestedSequenceNumberWord, + abihelpers.EvmWord(seqNum), + latestFinalizedHeader.Hash(), + pg.WithParentCtx(ctx), + ) + if err != nil { + return nil, fmt.Errorf("logs until block hash data word greater than: %w", err) + } + + return parseLogs[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested]( + logs, + c.lggr, + func(log types.Log) (*evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested, error) { + return onRamp.ParseCCIPSendRequested(log) + }, + ) +} + +func (c *LogPollerClient) GetSendRequestsBetweenSeqNums(ctx context.Context, onRampAddress common.Address, seqNumMin, seqNumMax uint64, confs int) ([]Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested], error) { + onRamp, err := c.loadOnRamp(onRampAddress) + if err != nil { + return nil, err + } + + logs, err := c.lp.LogsDataWordRange( + abihelpers.EventSignatures.SendRequested, + onRampAddress, + abihelpers.EventSignatures.SendRequestedSequenceNumberWord, + logpoller.EvmWord(seqNumMin), + logpoller.EvmWord(seqNumMax), + confs, + pg.WithParentCtx(ctx)) + if err != nil { + return nil, err + } + + return parseLogs[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested]( + logs, + c.lggr, + func(log types.Log) (*evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested, error) { + return onRamp.ParseCCIPSendRequested(log) + }, + ) +} + +func (c *LogPollerClient) GetTokenPriceUpdatesCreatedAfter(ctx context.Context, priceRegistryAddress common.Address, ts time.Time, confs int) ([]Event[price_registry.PriceRegistryUsdPerTokenUpdated], error) { + priceRegistry, err := c.loadPriceRegistry(priceRegistryAddress) + if err != nil { + return nil, err + } + + logs, err := c.lp.LogsCreatedAfter( + abihelpers.EventSignatures.UsdPerTokenUpdated, + priceRegistryAddress, + ts, + confs, + pg.WithParentCtx(ctx), + ) + if err != nil { + return nil, err + } + + return parseLogs[price_registry.PriceRegistryUsdPerTokenUpdated]( + logs, + c.lggr, + func(log types.Log) (*price_registry.PriceRegistryUsdPerTokenUpdated, error) { + return priceRegistry.ParseUsdPerTokenUpdated(log) + }, + ) +} + +func (c *LogPollerClient) GetGasPriceUpdatesCreatedAfter(ctx context.Context, priceRegistryAddress common.Address, chainSelector uint64, ts time.Time, confs int) ([]Event[price_registry.PriceRegistryUsdPerUnitGasUpdated], error) { + priceRegistry, err := c.loadPriceRegistry(priceRegistryAddress) + if err != nil { + return nil, err + } + + logs, err := c.lp.IndexedLogsCreatedAfter( + abihelpers.EventSignatures.UsdPerUnitGasUpdated, + priceRegistryAddress, + 1, + []common.Hash{abihelpers.EvmWord(chainSelector)}, + ts, + confs, + pg.WithParentCtx(ctx), + ) + if err != nil { + return nil, err + } + + return parseLogs[price_registry.PriceRegistryUsdPerUnitGasUpdated]( + logs, + c.lggr, + func(log types.Log) (*price_registry.PriceRegistryUsdPerUnitGasUpdated, error) { + return priceRegistry.ParseUsdPerUnitGasUpdated(log) + }, + ) +} + +func (c *LogPollerClient) GetExecutionStateChangesBetweenSeqNums(ctx context.Context, offRampAddress common.Address, seqNumMin, seqNumMax uint64, confs int) ([]Event[evm_2_evm_offramp.EVM2EVMOffRampExecutionStateChanged], error) { + offRamp, err := c.loadOffRamp(offRampAddress) + if err != nil { + return nil, err + } + + logs, err := c.lp.IndexedLogsTopicRange( + abihelpers.EventSignatures.ExecutionStateChanged, + offRampAddress, + abihelpers.EventSignatures.ExecutionStateChangedSequenceNumberIndex, + logpoller.EvmWord(seqNumMin), + logpoller.EvmWord(seqNumMax), + confs, + pg.WithParentCtx(ctx), + ) + if err != nil { + return nil, err + } + + return parseLogs[evm_2_evm_offramp.EVM2EVMOffRampExecutionStateChanged]( + logs, + c.lggr, + func(log types.Log) (*evm_2_evm_offramp.EVM2EVMOffRampExecutionStateChanged, error) { + return offRamp.ParseExecutionStateChanged(log) + }, + ) +} + +func (c *LogPollerClient) LatestBlock(ctx context.Context) (int64, error) { + return c.lp.LatestBlock(pg.WithParentCtx(ctx)) +} + +func parseLogs[T any](logs []logpoller.Log, lggr logger.Logger, parseFunc func(log types.Log) (*T, error)) ([]Event[T], error) { + reqs := make([]Event[T], 0, len(logs)) + for _, log := range logs { + data, err := parseFunc(log.ToGethLog()) + if err == nil { + reqs = append(reqs, Event[T]{ + Data: *data, + BlockMeta: BlockMeta{ + BlockTimestamp: log.BlockTimestamp, + BlockNumber: log.BlockNumber, + }, + }) + } + } + + if len(logs) != len(reqs) { + lggr.Warnw("Some logs were not parsed", "logs", len(logs), "requests", len(reqs)) + } + return reqs, nil +} + +func (c *LogPollerClient) loadOnRamp(addr common.Address) (*evm_2_evm_onramp.EVM2EVMOnRampFilterer, error) { + onRamp, exists := loadCachedDependency[*evm_2_evm_onramp.EVM2EVMOnRampFilterer](&c.dependencyCache, addr) + if exists { + return onRamp, nil + } + + onRamp, err := evm_2_evm_onramp.NewEVM2EVMOnRampFilterer(addr, c.client) + if err != nil { + return nil, err + } + + c.dependencyCache.Store(addr, onRamp) + return onRamp, nil +} + +func (c *LogPollerClient) loadPriceRegistry(addr common.Address) (*price_registry.PriceRegistryFilterer, error) { + priceRegistry, exists := loadCachedDependency[*price_registry.PriceRegistryFilterer](&c.dependencyCache, addr) + if exists { + return priceRegistry, nil + } + + priceRegistry, err := price_registry.NewPriceRegistryFilterer(addr, c.client) + if err != nil { + return nil, err + } + + c.dependencyCache.Store(addr, priceRegistry) + return priceRegistry, nil +} + +func (c *LogPollerClient) loadOffRamp(addr common.Address) (*evm_2_evm_offramp.EVM2EVMOffRampFilterer, error) { + offRamp, exists := loadCachedDependency[*evm_2_evm_offramp.EVM2EVMOffRampFilterer](&c.dependencyCache, addr) + if exists { + return offRamp, nil + } + + offRamp, err := evm_2_evm_offramp.NewEVM2EVMOffRampFilterer(addr, c.client) + if err != nil { + return nil, err + } + + c.dependencyCache.Store(addr, offRamp) + return offRamp, nil +} + +func loadCachedDependency[T any](cache *sync.Map, addr common.Address) (T, bool) { + var empty T + + if rawVal, exists := cache.Load(addr); exists { + if dep, is := rawVal.(T); is { + return dep, true + } + } + + return empty, false +} diff --git a/core/services/ocr2/plugins/ccip/ccipevents/logpoller_test.go b/core/services/ocr2/plugins/ccip/ccipevents/logpoller_test.go new file mode 100644 index 0000000000..600de7634b --- /dev/null +++ b/core/services/ocr2/plugins/ccip/ccipevents/logpoller_test.go @@ -0,0 +1,136 @@ +package ccipevents + +import ( + "context" + "fmt" + "math/big" + "testing" + "time" + + "github.com/ethereum/go-ethereum/core/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + evmClientMocks "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client/mocks" + + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller/mocks" + "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/abihelpers" + "github.com/smartcontractkit/chainlink/v2/core/utils" +) + +func TestLogPollerClient_loadDependency(t *testing.T) { + c := &LogPollerClient{} + + someAddr := utils.RandomAddress() + + onRamp, err := c.loadOnRamp(someAddr) + assert.NoError(t, err) + onRamp2, err := c.loadOnRamp(someAddr) + assert.NoError(t, err) + // the objects should have the same pointer + // since the second time the dependency should've been loaded from cache instead of initializing a new instance. + assert.True(t, onRamp == onRamp2) + + offRamp, err := c.loadOffRamp(someAddr) + assert.NoError(t, err) + offRamp2, err := c.loadOffRamp(someAddr) + assert.NoError(t, err) + assert.True(t, offRamp == offRamp2) + + priceReg, err := c.loadPriceRegistry(someAddr) + assert.NoError(t, err) + priceReg2, err := c.loadPriceRegistry(someAddr) + assert.NoError(t, err) + assert.True(t, priceReg == priceReg2) +} + +func Test_parseLogs(t *testing.T) { + // generate 100 logs + logs := make([]logpoller.Log, 100) + for i := range logs { + logs[i].LogIndex = int64(i + 1) + logs[i].BlockNumber = int64(i) * 1000 + logs[i].BlockTimestamp = time.Now() + } + + parseFn := func(log types.Log) (*uint, error) { + // Simulate some random error + if log.Index == 100 { + return nil, fmt.Errorf("some error") + } + return &log.Index, nil + } + + parsedEvents, err := parseLogs[uint](logs, logger.TestLogger(t), parseFn) + assert.NoError(t, err) + assert.Len(t, parsedEvents, 99) + + // Make sure everything is parsed according to the parse func + for i, ev := range parsedEvents { + assert.Equal(t, i+1, int(ev.Data)) + assert.Equal(t, int(i)*1000, int(ev.BlockNumber)) + assert.Greater(t, ev.BlockTimestamp, time.Now().Add(-time.Minute)) + } +} + +func TestLogPollerClient_GetSendRequestsGteSeqNum(t *testing.T) { + onRampAddr := utils.RandomAddress() + seqNum := uint64(100) + confs := 4 + + t.Run("using confs", func(t *testing.T) { + lp := mocks.NewLogPoller(t) + lp.On("LogsDataWordGreaterThan", + abihelpers.EventSignatures.SendRequested, + onRampAddr, + abihelpers.EventSignatures.SendRequestedSequenceNumberWord, + abihelpers.EvmWord(seqNum), + confs, + mock.Anything, + ).Return([]logpoller.Log{}, nil) + + c := &LogPollerClient{lp: lp} + events, err := c.GetSendRequestsGteSeqNum( + context.Background(), + onRampAddr, + seqNum, + false, + confs, + ) + assert.NoError(t, err) + assert.Empty(t, events) + lp.AssertExpectations(t) + }) + + t.Run("using latest confirmed block", func(t *testing.T) { + h := &types.Header{Number: big.NewInt(100000)} + + lp := mocks.NewLogPoller(t) + lp.On("LogsUntilBlockHashDataWordGreaterThan", + abihelpers.EventSignatures.SendRequested, + onRampAddr, + abihelpers.EventSignatures.SendRequestedSequenceNumberWord, + abihelpers.EvmWord(seqNum), + h.Hash(), + mock.Anything, + ).Return([]logpoller.Log{}, nil) + + cl := evmClientMocks.NewClient(t) + cl.On("HeaderByNumber", mock.Anything, mock.Anything).Return(h, nil) + + c := &LogPollerClient{lp: lp, client: cl} + events, err := c.GetSendRequestsGteSeqNum( + context.Background(), + onRampAddr, + seqNum, + true, + confs, + ) + assert.NoError(t, err) + assert.Empty(t, events) + lp.AssertExpectations(t) + cl.AssertExpectations(t) + }) +} diff --git a/core/services/ocr2/plugins/ccip/commit_plugin.go b/core/services/ocr2/plugins/ccip/commit_plugin.go index 491c923cf7..93fcd19cde 100644 --- a/core/services/ocr2/plugins/ccip/commit_plugin.go +++ b/core/services/ocr2/plugins/ccip/commit_plugin.go @@ -19,11 +19,11 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/txmgr" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/commit_store" - "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_onramp" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/router" "github.com/smartcontractkit/chainlink/v2/core/logger" "github.com/smartcontractkit/chainlink/v2/core/services/job" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/abihelpers" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/ccipevents" ccipconfig "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/config" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/hasher" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/promwrapper" @@ -104,6 +104,8 @@ func NewCommitServices(lggr logger.Logger, jb job.Job, chainSet evm.LegacyChainC lggr: commitLggr, sourceLP: sourceChain.LogPoller(), destLP: destChain.LogPoller(), + sourceEvents: ccipevents.NewLogPollerClient(sourceChain.LogPoller(), commitLggr, sourceChain.Client()), + destEvents: ccipevents.NewLogPollerClient(destChain.LogPoller(), commitLggr, destChain.Client()), offRamp: offRamp, onRampAddress: onRamp.Address(), priceGetter: priceGetterObject, @@ -114,7 +116,6 @@ func NewCommitServices(lggr logger.Logger, jb job.Job, chainSet evm.LegacyChainC sourceClient: sourceChain.Client(), commitStore: commitStore, leafHasher: leafHasher, - getSeqNumFromLog: getSeqNumFromLog(onRamp), checkFinalityTags: sourceChain.Config().EVM().FinalityTagEnabled(), }) @@ -149,16 +150,6 @@ func NewCommitServices(lggr logger.Logger, jb job.Job, chainSet evm.LegacyChainC return []job.ServiceCtx{job.NewServiceAdapter(oracle)}, nil } -func getSeqNumFromLog(onRamp evm_2_evm_onramp.EVM2EVMOnRampInterface) func(log logpoller.Log) (uint64, error) { - return func(log logpoller.Log) (uint64, error) { - req, err := onRamp.ParseCCIPSendRequested(log.ToGethLog()) - if err != nil { - return 0, err - } - return req.Message.SequenceNumber, nil - } -} - // CommitReportToEthTxMeta generates a txmgr.EthTxMeta from the given commit report. // sequence numbers of the committed messages will be added to tx metadata func CommitReportToEthTxMeta(report []byte) (*txmgr.TxMeta, error) { diff --git a/core/services/ocr2/plugins/ccip/commit_reporting_plugin.go b/core/services/ocr2/plugins/ccip/commit_reporting_plugin.go index 1dd7d02c0a..47acd57362 100644 --- a/core/services/ocr2/plugins/ccip/commit_reporting_plugin.go +++ b/core/services/ocr2/plugins/ccip/commit_reporting_plugin.go @@ -12,7 +12,6 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/rpc" "github.com/pkg/errors" "github.com/smartcontractkit/libocr/offchainreporting2plus/types" @@ -26,10 +25,10 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/logger" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/abihelpers" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/cache" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/ccipevents" ccipconfig "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/config" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/hasher" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/merklemulti" - "github.com/smartcontractkit/chainlink/v2/core/services/pg" ) const ( @@ -54,6 +53,8 @@ type update struct { type CommitPluginConfig struct { lggr logger.Logger sourceLP, destLP logpoller.LogPoller + sourceEvents ccipevents.Client + destEvents ccipevents.Client offRamp evm_2_evm_offramp.EVM2EVMOffRampInterface onRampAddress common.Address commitStore commit_store.CommitStoreInterface @@ -63,7 +64,6 @@ type CommitPluginConfig struct { sourceFeeEstimator gas.EvmFeeEstimator sourceClient, destClient evmclient.Client leafHasher hasher.LeafHasherInterface[[32]byte] - getSeqNumFromLog func(log logpoller.Log) (uint64, error) checkFinalityTags bool } @@ -229,81 +229,36 @@ func (rf *CommitReportingPluginFactory) UpdateLogPollerFilters(destPriceRegistry return nil } -func (r *CommitReportingPlugin) finalizedLogsGreaterThanMinSeq(ctx context.Context, nextMin uint64) ([]logpoller.Log, error) { - if !r.config.checkFinalityTags { - return r.config.sourceLP.LogsDataWordGreaterThan( - abihelpers.EventSignatures.SendRequested, - r.config.onRampAddress, - abihelpers.EventSignatures.SendRequestedSequenceNumberWord, - abihelpers.EvmWord(nextMin), - int(r.offchainConfig.SourceFinalityDepth), - pg.WithParentCtx(ctx), - ) - } - // If the chain is based on explicit finality we only examine logs less than or equal to the latest finalized block number. - // NOTE: there appears to be a bug in ethclient whereby BlockByNumber fails with "unsupported txtype" when trying to parse the block - // when querying L2s, headers however work. - // TODO (CCIP-778): Migrate to core finalized tags, below doesn't work for some chains e.g. Celo. - latestFinalizedHeader, err := r.config.sourceClient.HeaderByNumber(ctx, big.NewInt(rpc.FinalizedBlockNumber.Int64())) - if err != nil { - return nil, err - } - if latestFinalizedHeader == nil { - return nil, errors.New("latest finalized header is nil") - } - if latestFinalizedHeader.Number == nil { - return nil, errors.New("latest finalized number is nil") - } - return r.config.sourceLP.LogsUntilBlockHashDataWordGreaterThan( - abihelpers.EventSignatures.SendRequested, - r.config.onRampAddress, - abihelpers.EventSignatures.SendRequestedSequenceNumberWord, - abihelpers.EvmWord(nextMin), - latestFinalizedHeader.Hash(), - pg.WithParentCtx(ctx), - ) -} - func (r *CommitReportingPlugin) calculateMinMaxSequenceNumbers(ctx context.Context, lggr logger.Logger) (uint64, uint64, error) { nextInflightMin, _, err := r.nextMinSeqNum(ctx, lggr) if err != nil { return 0, 0, err } - // Gather only finalized logs. - reqs, err := r.finalizedLogsGreaterThanMinSeq(ctx, nextInflightMin) + msgRequests, err := r.config.sourceEvents.GetSendRequestsGteSeqNum(ctx, r.config.onRampAddress, nextInflightMin, r.config.checkFinalityTags, int(r.offchainConfig.SourceFinalityDepth)) if err != nil { return 0, 0, err } - if len(reqs) == 0 { + if len(msgRequests) == 0 { lggr.Infow("No new requests", "minSeqNr", nextInflightMin) return 0, 0, nil } - var seqNrs []uint64 - for _, req := range reqs { - seqNr, err2 := r.config.getSeqNumFromLog(req) - if err2 != nil { - lggr.Errorw("Error parsing seq num", "err", err2) - continue - } - seqNrs = append(seqNrs, seqNr) + seqNrs := make([]uint64, 0, len(msgRequests)) + for _, msgReq := range msgRequests { + seqNrs = append(seqNrs, msgReq.Data.Message.SequenceNumber) } - if len(seqNrs) == 0 { - lggr.Infow("Could not parse any sequence number", "minSeqNr", nextInflightMin, "reqs", len(reqs)) - return 0, 0, nil - } - min := seqNrs[0] - max := seqNrs[len(seqNrs)-1] - if min != nextInflightMin { + minSeqNr := seqNrs[0] + maxSeqNr := seqNrs[len(seqNrs)-1] + if minSeqNr != nextInflightMin { // Still report the observation as even partial reports have value e.g. all nodes are // missing a single, different log each, they would still be able to produce a valid report. - lggr.Warnf("Missing sequence number range [%d-%d]", nextInflightMin, min) + lggr.Warnf("Missing sequence number range [%d-%d]", nextInflightMin, minSeqNr) } - if !contiguousReqs(lggr, min, max, seqNrs) { + if !contiguousReqs(lggr, minSeqNr, maxSeqNr, seqNrs) { return 0, 0, errors.New("unexpected gap in seq nums") } - return min, max, nil + return minSeqNr, maxSeqNr, nil } func (r *CommitReportingPlugin) nextMinSeqNum(ctx context.Context, lggr logger.Logger) (inflightMin, onChainMin uint64, err error) { @@ -409,23 +364,25 @@ func calculateUsdPer1e18TokenAmount(price *big.Int, decimals uint8) *big.Int { // Gets the latest token price updates based on logs within the heartbeat // The updates returned by this function are guaranteed to not contain nil values. func (r *CommitReportingPlugin) getLatestTokenPriceUpdates(ctx context.Context, now time.Time, checkInflight bool) (map[common.Address]update, error) { - tokenUpdatesWithinHeartBeat, err := r.config.destLP.LogsCreatedAfter(abihelpers.EventSignatures.UsdPerTokenUpdated, r.destPriceRegistry.Address(), now.Add(-r.offchainConfig.FeeUpdateHeartBeat.Duration()), 0, pg.WithParentCtx(ctx)) - latestUpdates := make(map[common.Address]update) - + tokenPriceUpdates, err := r.config.destEvents.GetTokenPriceUpdatesCreatedAfter( + ctx, + r.destPriceRegistry.Address(), + now.Add(-r.offchainConfig.FeeUpdateHeartBeat.Duration()), + 0, + ) if err != nil { return nil, err } - for _, log := range tokenUpdatesWithinHeartBeat { + + latestUpdates := make(map[common.Address]update) + for _, tokenPriceUpdate := range tokenPriceUpdates { + priceUpdate := tokenPriceUpdate.Data // Ordered by ascending timestamps - tokenUpdate, err := r.destPriceRegistry.ParseUsdPerTokenUpdated(log.ToGethLog()) - if err != nil { - return nil, err - } - timestamp := time.Unix(tokenUpdate.Timestamp.Int64(), 0) - if tokenUpdate.Value != nil && !timestamp.Before(latestUpdates[tokenUpdate.Token].timestamp) { - latestUpdates[tokenUpdate.Token] = update{ + timestamp := time.Unix(priceUpdate.Timestamp.Int64(), 0) + if priceUpdate.Value != nil && !timestamp.Before(latestUpdates[priceUpdate.Token].timestamp) { + latestUpdates[priceUpdate.Token] = update{ timestamp: timestamp, - value: tokenUpdate.Value, + value: priceUpdate.Value, } } } @@ -463,30 +420,24 @@ func (r *CommitReportingPlugin) getLatestGasPriceUpdate(ctx context.Context, now } // If there are no price updates inflight, check latest prices onchain - gasUpdatesWithinHeartBeat, err := r.config.destLP.IndexedLogsCreatedAfter( - abihelpers.EventSignatures.UsdPerUnitGasUpdated, + gasPriceUpdates, err := r.config.destEvents.GetGasPriceUpdatesCreatedAfter( + ctx, r.destPriceRegistry.Address(), - 1, - []common.Hash{abihelpers.EvmWord(r.config.sourceChainSelector)}, + r.config.sourceChainSelector, now.Add(-r.offchainConfig.FeeUpdateHeartBeat.Duration()), 0, - pg.WithParentCtx(ctx), ) if err != nil { return update{}, err } - for _, log := range gasUpdatesWithinHeartBeat { + for _, priceUpdate := range gasPriceUpdates { // Ordered by ascending timestamps - priceUpdate, err2 := r.destPriceRegistry.ParseUsdPerUnitGasUpdated(log.ToGethLog()) - if err2 != nil { - return update{}, err2 - } - timestamp := time.Unix(priceUpdate.Timestamp.Int64(), 0) + timestamp := time.Unix(priceUpdate.Data.Timestamp.Int64(), 0) if !timestamp.Before(gasPriceUpdate.timestamp) { gasPriceUpdate = update{ timestamp: timestamp, - value: priceUpdate.Value, + value: priceUpdate.Data.Value, } } } @@ -703,18 +654,18 @@ func (r *CommitReportingPlugin) buildReport(ctx context.Context, lggr logger.Log // Logs are guaranteed to be in order of seq num, since these are finalized logs only // and the contract's seq num is auto-incrementing. - logs, err := r.config.sourceLP.LogsDataWordRange( - abihelpers.EventSignatures.SendRequested, + sendRequests, err := r.config.sourceEvents.GetSendRequestsBetweenSeqNums( + ctx, r.config.onRampAddress, - abihelpers.EventSignatures.SendRequestedSequenceNumberWord, - logpoller.EvmWord(interval.Min), - logpoller.EvmWord(interval.Max), + interval.Min, + interval.Max, int(r.offchainConfig.SourceFinalityDepth), - pg.WithParentCtx(ctx)) + ) if err != nil { return commit_store.CommitStoreCommitReport{}, err } - leaves, err := leavesFromIntervals(lggr, r.config.getSeqNumFromLog, interval, r.config.leafHasher, logs) + + leaves, err := leavesFromIntervals(lggr, interval, r.config.leafHasher, sendRequests) if err != nil { return commit_store.CommitStoreCommitReport{}, err } diff --git a/core/services/ocr2/plugins/ccip/commit_reporting_plugin_test.go b/core/services/ocr2/plugins/ccip/commit_reporting_plugin_test.go index 38194c851a..be1037d0b2 100644 --- a/core/services/ocr2/plugins/ccip/commit_reporting_plugin_test.go +++ b/core/services/ocr2/plugins/ccip/commit_reporting_plugin_test.go @@ -34,6 +34,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/logger" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/abihelpers" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/cache" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/ccipevents" ccipconfig "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/config" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/hasher" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/merklemulti" @@ -64,6 +65,7 @@ func setupCommitTestHarness(t *testing.T) commitTestHarness { mock.Anything, ).Maybe().Return(gas.EvmFee{Legacy: assets.NewWei(defaultGasPrice)}, uint32(200e3), nil) + lggr := logger.TestLogger(t) priceGetter := newMockPriceGetter() backendClient := client.NewSimulatedBackendClient(t, th.Dest.Chain, new(big.Int).SetUint64(th.Dest.ChainID)) @@ -72,6 +74,8 @@ func setupCommitTestHarness(t *testing.T) commitTestHarness { lggr: th.Lggr, sourceLP: th.SourceLP, destLP: th.DestLP, + sourceEvents: ccipevents.NewLogPollerClient(th.SourceLP, lggr, backendClient), + destEvents: ccipevents.NewLogPollerClient(th.DestLP, lggr, backendClient), offRamp: th.Dest.OffRamp, onRampAddress: th.Source.OnRamp.Address(), commitStore: th.Dest.CommitStore, @@ -80,8 +84,8 @@ func setupCommitTestHarness(t *testing.T) commitTestHarness { sourceFeeEstimator: sourceFeeEstimator, sourceChainSelector: th.Source.ChainSelector, destClient: backendClient, + sourceClient: backendClient, leafHasher: hasher.NewLeafHasher(th.Source.ChainSelector, th.Dest.ChainSelector, th.Source.OnRamp.Address(), hasher.NewKeccakCtx()), - getSeqNumFromLog: getSeqNumFromLog(th.Source.OnRamp), }, inflightReports: newInflightCommitReportsContainer(time.Hour), onchainConfig: th.CommitOnchainConfig, diff --git a/core/services/ocr2/plugins/ccip/execution_batch_building.go b/core/services/ocr2/plugins/ccip/execution_batch_building.go index cb8ec6c1aa..ede8b919a1 100644 --- a/core/services/ocr2/plugins/ccip/execution_batch_building.go +++ b/core/services/ocr2/plugins/ccip/execution_batch_building.go @@ -10,8 +10,10 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/commit_store" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_offramp" + "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_onramp" "github.com/smartcontractkit/chainlink/v2/core/logger" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/abihelpers" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/ccipevents" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/hasher" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/merklemulti" "github.com/smartcontractkit/chainlink/v2/core/services/pg" @@ -21,23 +23,21 @@ func getProofData( ctx context.Context, lggr logger.Logger, hashLeaf hasher.LeafHasherInterface[[32]byte], - seqParser func(log logpoller.Log) (uint64, error), onRampAddress common.Address, - sourceLP logpoller.LogPoller, + sourceEventsClient ccipevents.Client, interval commit_store.CommitStoreInterval, -) (msgsInRoot []logpoller.Log, leaves [][32]byte, tree *merklemulti.Tree[[32]byte], err error) { - msgsInRoot, err = sourceLP.LogsDataWordRange( - abihelpers.EventSignatures.SendRequested, +) (sendReqsInRoot []ccipevents.Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested], leaves [][32]byte, tree *merklemulti.Tree[[32]byte], err error) { + sendReqs, err := sourceEventsClient.GetSendRequestsBetweenSeqNums( + ctx, onRampAddress, - abihelpers.EventSignatures.SendRequestedSequenceNumberWord, - abihelpers.EvmWord(interval.Min), - abihelpers.EvmWord(interval.Max), + interval.Min, + interval.Max, 0, // no need for confirmations, commitReport was already confirmed and we need all msgs in it - pg.WithParentCtx(ctx)) + ) if err != nil { return nil, nil, nil, err } - leaves, err = leavesFromIntervals(lggr, seqParser, interval, hashLeaf, msgsInRoot) + leaves, err = leavesFromIntervals(lggr, interval, hashLeaf, sendReqs) if err != nil { return nil, nil, nil, err } @@ -45,7 +45,7 @@ func getProofData( if err != nil { return nil, nil, nil, err } - return msgsInRoot, leaves, tree, nil + return sendReqs, leaves, tree, nil } func buildExecutionReportForMessages( diff --git a/core/services/ocr2/plugins/ccip/execution_plugin.go b/core/services/ocr2/plugins/ccip/execution_plugin.go index 8fbb527f29..45185988a7 100644 --- a/core/services/ocr2/plugins/ccip/execution_plugin.go +++ b/core/services/ocr2/plugins/ccip/execution_plugin.go @@ -26,6 +26,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/logger" "github.com/smartcontractkit/chainlink/v2/core/services/job" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/abihelpers" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/ccipevents" ccipconfig "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/config" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/hasher" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/observability" @@ -110,12 +111,15 @@ func NewExecutionServices(lggr logger.Logger, jb job.Job, chainSet evm.LegacyCha lggr: execLggr, sourceLP: sourceChain.LogPoller(), destLP: destChain.LogPoller(), + sourceEvents: ccipevents.NewLogPollerClient(sourceChain.LogPoller(), execLggr, sourceChain.Client()), + destEvents: ccipevents.NewLogPollerClient(destChain.LogPoller(), execLggr, destChain.Client()), onRamp: onRamp, offRamp: offRamp, commitStore: commitStore, sourcePriceRegistry: sourcePriceRegistry, sourceWrappedNativeToken: sourceWrappedNative, destClient: destChain.Client(), + sourceClient: sourceChain.Client(), destGasEstimator: destChain.GasEstimator(), leafHasher: hasher.NewLeafHasher(offRampConfig.SourceChainSelector, offRampConfig.ChainSelector, onRamp.Address(), hasher.NewKeccakCtx()), }) diff --git a/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go b/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go index 7a055d1a2a..21224cdc87 100644 --- a/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go +++ b/core/services/ocr2/plugins/ccip/execution_reporting_plugin.go @@ -32,6 +32,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/logger" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/abihelpers" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/cache" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/ccipevents" ccipconfig "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/config" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/hasher" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/observability" @@ -54,12 +55,15 @@ var ( type ExecutionPluginConfig struct { lggr logger.Logger sourceLP, destLP logpoller.LogPoller + sourceEvents ccipevents.Client + destEvents ccipevents.Client onRamp evm_2_evm_onramp.EVM2EVMOnRampInterface offRamp evm_2_evm_offramp.EVM2EVMOffRampInterface commitStore commit_store.CommitStoreInterface sourcePriceRegistry price_registry.PriceRegistryInterface sourceWrappedNativeToken common.Address destClient evmclient.Client + sourceClient evmclient.Client destGasEstimator gas.EvmFeeEstimator leafHasher hasher.LeafHasherInterface[[32]byte] } @@ -432,26 +436,20 @@ func (r *ExecutionReportingPlugin) sourceDestinationTokens(ctx context.Context) // before. It doesn't matter if the executed succeeded, since we don't retry previous // attempts even if they failed. Value in the map indicates whether the log is finalized or not. func (r *ExecutionReportingPlugin) getExecutedSeqNrsInRange(ctx context.Context, min, max uint64, latestBlock int64) (map[uint64]bool, error) { - executedLogs, err := r.config.destLP.IndexedLogsTopicRange( - abihelpers.EventSignatures.ExecutionStateChanged, + stateChanges, err := r.config.destEvents.GetExecutionStateChangesBetweenSeqNums( + ctx, r.config.offRamp.Address(), - abihelpers.EventSignatures.ExecutionStateChangedSequenceNumberIndex, - logpoller.EvmWord(min), - logpoller.EvmWord(max), + min, + max, int(r.offchainConfig.DestOptimisticConfirmations), - pg.WithParentCtx(ctx), ) if err != nil { return nil, err } - executedMp := make(map[uint64]bool) - for _, executedLog := range executedLogs { - exec, err := r.config.offRamp.ParseExecutionStateChanged(executedLog.ToGethLog()) - if err != nil { - return nil, err - } - finalized := (latestBlock - executedLog.BlockNumber) >= int64(r.offchainConfig.DestFinalityDepth) - executedMp[exec.SequenceNumber] = finalized + executedMp := make(map[uint64]bool, len(stateChanges)) + for _, stateChange := range stateChanges { + finalized := (latestBlock - stateChange.BlockNumber) >= int64(r.offchainConfig.DestFinalityDepth) + executedMp[stateChange.Data.SequenceNumber] = finalized } return executedMp, nil } @@ -733,31 +731,29 @@ func (r *ExecutionReportingPlugin) getReportsWithSendRequests( // use errgroup to fetch send request logs and executed sequence numbers in parallel eg := &errgroup.Group{} - var sendRequestLogs []logpoller.Log + var sendRequests []ccipevents.Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested] eg.Go(func() error { - // get logs from all the reports - rawLogs, err := r.config.sourceLP.LogsDataWordRange( - abihelpers.EventSignatures.SendRequested, + sendReqs, err := r.config.sourceEvents.GetSendRequestsBetweenSeqNums( + ctx, r.config.onRamp.Address(), - abihelpers.EventSignatures.SendRequestedSequenceNumberWord, - logpoller.EvmWord(intervalMin), - logpoller.EvmWord(intervalMax), + intervalMin, + intervalMax, int(r.offchainConfig.SourceFinalityDepth), - pg.WithParentCtx(ctx), ) if err != nil { return err } - sendRequestLogs = rawLogs + sendRequests = sendReqs return nil }) var executedSeqNums map[uint64]bool eg.Go(func() error { - latestBlock, err := r.config.destLP.LatestBlock() + latestBlock, err := r.config.destEvents.LatestBlock(ctx) if err != nil { return err } + // get executable sequence numbers executedMp, err := r.getExecutedSeqNrsInRange(ctx, intervalMin, intervalMax, latestBlock) if err != nil { @@ -779,32 +775,24 @@ func (r *ExecutionReportingPlugin) getReportsWithSendRequests( } } - for _, rawLog := range sendRequestLogs { - ccipSendRequested, err := r.config.onRamp.ParseCCIPSendRequested(gethtypes.Log{ - Topics: rawLog.GetTopics(), - Data: rawLog.Data, - }) - if err != nil { - r.lggr.Errorw("unable to parse message", "err", err, "tx", rawLog.TxHash, "logIdx", rawLog.LogIndex) - continue - } - msg := abihelpers.OnRampMessageToOffRampMessage(ccipSendRequested.Message) + for _, sendReq := range sendRequests { + msg := abihelpers.OnRampMessageToOffRampMessage(sendReq.Data.Message) // if value exists in the map then it's executed // if value exists, and it's true then it's considered finalized finalized, executed := executedSeqNums[msg.SequenceNumber] - sendReq := evm2EVMOnRampCCIPSendRequestedWithMeta{ + reqWithMeta := evm2EVMOnRampCCIPSendRequestedWithMeta{ InternalEVM2EVMMessage: msg, - blockTimestamp: rawLog.BlockTimestamp, + blockTimestamp: sendReq.BlockTimestamp, executed: executed, finalized: finalized, } // attach the msg to the appropriate reports for i := range reportsWithSendReqs { - if reportsWithSendReqs[i].sendReqFits(sendReq) { - reportsWithSendReqs[i].sendRequestsWithMeta = append(reportsWithSendReqs[i].sendRequestsWithMeta, sendReq) + if reportsWithSendReqs[i].sendReqFits(reqWithMeta) { + reportsWithSendReqs[i].sendRequestsWithMeta = append(reportsWithSendReqs[i].sendRequestsWithMeta, reqWithMeta) } } } @@ -824,14 +812,6 @@ func aggregateTokenValue(destTokenPricesUSD map[common.Address]*big.Int, sourceT return sum, nil } -func (r *ExecutionReportingPlugin) parseSeqNr(log logpoller.Log) (uint64, error) { - s, err := r.config.onRamp.ParseCCIPSendRequested(log.ToGethLog()) - if err != nil { - return 0, err - } - return s.Message.SequenceNumber, nil -} - // Assumes non-empty report. Messages to execute can span more than one report, but are assumed to be in order of increasing // sequence number. func (r *ExecutionReportingPlugin) buildReport(ctx context.Context, lggr logger.Logger, observedMessages []ObservedMessage) ([]byte, error) { @@ -844,18 +824,15 @@ func (r *ExecutionReportingPlugin) buildReport(ctx context.Context, lggr logger. } lggr.Infow("Building execution report", "observations", observedMessages, "merkleRoot", hexutil.Encode(commitReport.MerkleRoot[:]), "report", commitReport) - msgsInRoot, leaves, tree, err := getProofData(ctx, lggr, r.config.leafHasher, r.parseSeqNr, r.config.onRamp.Address(), r.config.sourceLP, commitReport.Interval) + sendReqsInRoot, leaves, tree, err := getProofData(ctx, lggr, r.config.leafHasher, r.config.onRamp.Address(), r.config.sourceEvents, commitReport.Interval) if err != nil { return nil, err } - messages := make([]*evm_2_evm_offramp.InternalEVM2EVMMessage, len(msgsInRoot)) - for i, msg := range msgsInRoot { - decodedMessage, err2 := abihelpers.DecodeOffRampMessage(msg.Data) - if err2 != nil { - return nil, err - } - messages[i] = decodedMessage + messages := make([]*evm_2_evm_offramp.InternalEVM2EVMMessage, len(sendReqsInRoot)) + for i, msg := range sendReqsInRoot { + offRampMsg := abihelpers.OnRampMessageToOffRampMessage(msg.Data.Message) + messages[i] = &offRampMsg } // cap messages which fits MaxExecutionReportLength (after serialized) diff --git a/core/services/ocr2/plugins/ccip/execution_reporting_plugin_test.go b/core/services/ocr2/plugins/ccip/execution_reporting_plugin_test.go index 4c6fa641c2..2c2d8c7a3d 100644 --- a/core/services/ocr2/plugins/ccip/execution_reporting_plugin_test.go +++ b/core/services/ocr2/plugins/ccip/execution_reporting_plugin_test.go @@ -28,6 +28,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/logger" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/abihelpers" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/cache" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/ccipevents" ccipconfig "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/config" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/testhelpers" plugintesthelpers "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/testhelpers/plugins" @@ -55,6 +56,7 @@ type execTestHarness = struct { func setupExecTestHarness(t *testing.T) execTestHarness { th := plugintesthelpers.SetupCCIPTestHarness(t) + lggr := logger.TestLogger(t) destFeeEstimator := mocks.NewEvmFeeEstimator(t) destFeeEstimator.On( @@ -79,11 +81,14 @@ func setupExecTestHarness(t *testing.T) execTestHarness { lggr: th.Lggr, sourceLP: th.SourceLP, destLP: th.DestLP, + sourceEvents: ccipevents.NewLogPollerClient(th.SourceLP, lggr, th.SourceClient), + destEvents: ccipevents.NewLogPollerClient(th.DestLP, lggr, th.DestClient), sourcePriceRegistry: th.Source.PriceRegistry, onRamp: th.Source.OnRamp, commitStore: th.Dest.CommitStore, offRamp: th.Dest.OffRamp, destClient: th.DestClient, + sourceClient: th.SourceClient, sourceWrappedNativeToken: th.Source.WrappedNative.Address(), leafHasher: hasher.NewLeafHasher(th.Source.ChainSelector, th.Dest.ChainSelector, th.Source.OnRamp.Address(), hasher.NewKeccakCtx()), destGasEstimator: destFeeEstimator, diff --git a/core/services/ocr2/plugins/ccip/plugins_common.go b/core/services/ocr2/plugins/ccip/plugins_common.go index 1cf7ac20d2..c12f4f215d 100644 --- a/core/services/ocr2/plugins/ccip/plugins_common.go +++ b/core/services/ocr2/plugins/ccip/plugins_common.go @@ -18,6 +18,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_onramp" "github.com/smartcontractkit/chainlink/v2/core/gethwrappers/ccip/generated/evm_2_evm_onramp_1_0_0" "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/ccipevents" ccipconfig "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/config" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/hasher" "github.com/smartcontractkit/chainlink/v2/core/services/ocr2/plugins/ccip/observability" @@ -122,25 +123,22 @@ func calculateUsdPerUnitGas(sourceGasPrice *big.Int, usdPerFeeCoin *big.Int) *bi // Extracts the hashed leaves from a given set of logs func leavesFromIntervals( lggr logger.Logger, - seqParser func(logpoller.Log) (uint64, error), interval commit_store.CommitStoreInterval, hasher hasher.LeafHasherInterface[[32]byte], - logs []logpoller.Log, + sendReqs []ccipevents.Event[evm_2_evm_onramp.EVM2EVMOnRampCCIPSendRequested], ) ([][32]byte, error) { var seqNrs []uint64 - for _, log := range logs { - seqNr, err2 := seqParser(log) - if err2 != nil { - return nil, err2 - } - seqNrs = append(seqNrs, seqNr) + for _, req := range sendReqs { + seqNrs = append(seqNrs, req.Data.Message.SequenceNumber) } + if !contiguousReqs(lggr, interval.Min, interval.Max, seqNrs) { return nil, errors.Errorf("do not have full range [%v, %v] have %v", interval.Min, interval.Max, seqNrs) } var leaves [][32]byte - for _, log := range logs { - hash, err2 := hasher.HashLeaf(log.ToGethLog()) + + for _, sendReq := range sendReqs { + hash, err2 := hasher.HashLeaf(sendReq.Data.Raw) if err2 != nil { return nil, err2 } diff --git a/core/services/ocr2/plugins/ccip/testhelpers/plugins/plugin_harness.go b/core/services/ocr2/plugins/ccip/testhelpers/plugins/plugin_harness.go index 9967482689..3ac293d6ae 100644 --- a/core/services/ocr2/plugins/ccip/testhelpers/plugins/plugin_harness.go +++ b/core/services/ocr2/plugins/ccip/testhelpers/plugins/plugin_harness.go @@ -47,9 +47,10 @@ type CCIPPluginTestHarness struct { testhelpers.CCIPContracts Lggr logger.Logger - SourceLP logpoller.LogPollerTest - DestLP logpoller.LogPollerTest - DestClient evmclient.Client + SourceLP logpoller.LogPollerTest + DestLP logpoller.LogPollerTest + DestClient evmclient.Client + SourceClient evmclient.Client CommitOnchainConfig ccipconfig.CommitOnchainConfig ExecOnchainConfig ccipconfig.ExecOnchainConfig @@ -165,6 +166,7 @@ func SetupCCIPTestHarness(t *testing.T) CCIPPluginTestHarness { SourceLP: sourceLP, DestLP: destLP, DestClient: evmclient.NewSimulatedBackendClient(t, c.Dest.Chain, new(big.Int).SetUint64(c.Dest.ChainID)), + SourceClient: evmclient.NewSimulatedBackendClient(t, c.Source.Chain, new(big.Int).SetUint64(c.Source.ChainID)), CommitOnchainConfig: commitOnchainConfig, ExecOnchainConfig: execOnchainConfig, }