From 452781058e08f78db14f6ebb8a8c3d16fb517c31 Mon Sep 17 00:00:00 2001 From: chainchad <96362174+chainchad@users.noreply.github.com> Date: Tue, 22 Oct 2024 14:49:36 -0400 Subject: [PATCH 1/7] Bump version and update CHANGELOG fore core v2.18.0 Signed-off-by: chainchad <96362174+chainchad@users.noreply.github.com> --- .changeset/angry-fishes-ring.md | 8 -- .changeset/brave-ads-explode.md | 9 -- .changeset/chilled-months-bow.md | 5 -- .changeset/chilled-plants-clap.md | 5 -- .changeset/chilly-crews-retire.md | 5 -- .changeset/cool-owls-laugh.md | 5 ++ .changeset/cuddly-colts-speak.md | 5 -- .changeset/curly-baboons-enjoy.md | 5 -- .changeset/curvy-eyes-own.md | 5 -- .changeset/early-rules-yell.md | 5 -- .changeset/eighty-bulldogs-smile.md | 5 -- .changeset/eleven-olives-accept.md | 5 -- .changeset/empty-bees-fix.md | 5 -- .changeset/empty-pianos-attack.md | 5 -- .changeset/fair-swans-accept.md | 5 -- .changeset/few-pillows-yawn.md | 5 -- .changeset/fifty-pillows-peel.md | 5 -- .changeset/fifty-squids-juggle.md | 5 -- .changeset/five-chicken-talk.md | 5 -- .changeset/flat-horses-argue.md | 5 -- .changeset/flat-spiders-sing.md | 5 -- .changeset/forty-lizards-camp.md | 5 -- .changeset/four-buses-invite.md | 5 -- .changeset/fresh-falcons-cheer.md | 5 -- .changeset/giant-pillows-sort.md | 5 -- .changeset/gorgeous-wolves-collect.md | 5 -- .changeset/healthy-flowers-nail.md | 5 -- .changeset/honest-cameras-cross.md | 5 -- .changeset/khaki-pants-melt.md | 5 -- .changeset/late-pillows-shake.md | 5 -- .changeset/lemon-apples-explain.md | 5 -- .changeset/metal-eels-check.md | 5 -- .changeset/metal-meals-mix.md | 5 -- .changeset/metal-pots-lie.md | 5 -- .changeset/moody-rules-agree.md | 8 -- .changeset/moody-trains-grow.md | 5 -- .changeset/neat-numbers-lay.md | 5 -- .changeset/nervous-books-push.md | 5 -- .changeset/nice-cows-yell.md | 5 -- .changeset/ninety-feet-change.md | 5 -- .changeset/old-humans-watch.md | 5 -- .changeset/olive-bugs-relax.md | 5 -- .changeset/orange-feet-share.md | 25 ------ .changeset/orange-humans-laugh.md | 5 -- .changeset/polite-tips-sneeze.md | 5 -- .changeset/pretty-worms-smell.md | 5 -- .changeset/real-doors-visit.md | 6 -- .changeset/red-jobs-marry.md | 5 -- .changeset/rude-spies-serve.md | 5 -- .changeset/seven-forks-hug.md | 5 -- .changeset/silent-foxes-battle.md | 5 -- .changeset/silly-lies-boil.md | 8 -- .changeset/silly-starfishes-destroy.md | 5 -- .changeset/silver-pens-float.md | 5 -- .changeset/six-frogs-juggle.md | 5 -- .changeset/smooth-queens-dance.md | 5 -- .changeset/stale-pugs-sin.md | 5 -- .changeset/ten-rats-tie.md | 5 -- .changeset/three-otters-drum.md | 5 -- .changeset/tidy-apricots-care.md | 5 -- .changeset/tough-boats-shop.md | 5 -- .changeset/two-snails-mix.md | 5 -- .changeset/wise-pandas-join.md | 5 -- CHANGELOG.md | 109 +++++++++++++++++++++++++ package.json | 2 +- 65 files changed, 115 insertions(+), 345 deletions(-) delete mode 100644 .changeset/angry-fishes-ring.md delete mode 100644 .changeset/brave-ads-explode.md delete mode 100644 .changeset/chilled-months-bow.md delete mode 100644 .changeset/chilled-plants-clap.md delete mode 100644 .changeset/chilly-crews-retire.md create mode 100644 .changeset/cool-owls-laugh.md delete mode 100644 .changeset/cuddly-colts-speak.md delete mode 100644 .changeset/curly-baboons-enjoy.md delete mode 100644 .changeset/curvy-eyes-own.md delete mode 100644 .changeset/early-rules-yell.md delete mode 100644 .changeset/eighty-bulldogs-smile.md delete mode 100644 .changeset/eleven-olives-accept.md delete mode 100644 .changeset/empty-bees-fix.md delete mode 100644 .changeset/empty-pianos-attack.md delete mode 100644 .changeset/fair-swans-accept.md delete mode 100644 .changeset/few-pillows-yawn.md delete mode 100644 .changeset/fifty-pillows-peel.md delete mode 100644 .changeset/fifty-squids-juggle.md delete mode 100644 .changeset/five-chicken-talk.md delete mode 100644 .changeset/flat-horses-argue.md delete mode 100644 .changeset/flat-spiders-sing.md delete mode 100644 .changeset/forty-lizards-camp.md delete mode 100644 .changeset/four-buses-invite.md delete mode 100644 .changeset/fresh-falcons-cheer.md delete mode 100644 .changeset/giant-pillows-sort.md delete mode 100644 .changeset/gorgeous-wolves-collect.md delete mode 100644 .changeset/healthy-flowers-nail.md delete mode 100644 .changeset/honest-cameras-cross.md delete mode 100644 .changeset/khaki-pants-melt.md delete mode 100644 .changeset/late-pillows-shake.md delete mode 100644 .changeset/lemon-apples-explain.md delete mode 100644 .changeset/metal-eels-check.md delete mode 100644 .changeset/metal-meals-mix.md delete mode 100644 .changeset/metal-pots-lie.md delete mode 100644 .changeset/moody-rules-agree.md delete mode 100644 .changeset/moody-trains-grow.md delete mode 100644 .changeset/neat-numbers-lay.md delete mode 100644 .changeset/nervous-books-push.md delete mode 100644 .changeset/nice-cows-yell.md delete mode 100644 .changeset/ninety-feet-change.md delete mode 100644 .changeset/old-humans-watch.md delete mode 100644 .changeset/olive-bugs-relax.md delete mode 100644 .changeset/orange-feet-share.md delete mode 100644 .changeset/orange-humans-laugh.md delete mode 100644 .changeset/polite-tips-sneeze.md delete mode 100644 .changeset/pretty-worms-smell.md delete mode 100644 .changeset/real-doors-visit.md delete mode 100644 .changeset/red-jobs-marry.md delete mode 100644 .changeset/rude-spies-serve.md delete mode 100644 .changeset/seven-forks-hug.md delete mode 100644 .changeset/silent-foxes-battle.md delete mode 100644 .changeset/silly-lies-boil.md delete mode 100644 .changeset/silly-starfishes-destroy.md delete mode 100644 .changeset/silver-pens-float.md delete mode 100644 .changeset/six-frogs-juggle.md delete mode 100644 .changeset/smooth-queens-dance.md delete mode 100644 .changeset/stale-pugs-sin.md delete mode 100644 .changeset/ten-rats-tie.md delete mode 100644 .changeset/three-otters-drum.md delete mode 100644 .changeset/tidy-apricots-care.md delete mode 100644 .changeset/tough-boats-shop.md delete mode 100644 .changeset/two-snails-mix.md delete mode 100644 .changeset/wise-pandas-join.md diff --git a/.changeset/angry-fishes-ring.md b/.changeset/angry-fishes-ring.md deleted file mode 100644 index 86671967372..00000000000 --- a/.changeset/angry-fishes-ring.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -"chainlink": minor ---- - -#internal add getNextDonId() and getNodes(bytes32[] calldata p2pIds) in CapabilitiesRegistry and define interface for node info - - -PR issue: CCIP-3569 \ No newline at end of file diff --git a/.changeset/brave-ads-explode.md b/.changeset/brave-ads-explode.md deleted file mode 100644 index 4be608e2d1d..00000000000 --- a/.changeset/brave-ads-explode.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"chainlink": patch ---- - -Remove finality depth as the default value for minConfirmation for tx jobs. -Update the sql query for fetching pending callback transactions: -if minConfirmation is not null, we check difference if the current block - tx block > minConfirmation -else we check if the tx block is <= finalizedBlock -#updated diff --git a/.changeset/chilled-months-bow.md b/.changeset/chilled-months-bow.md deleted file mode 100644 index d3bbf7f97e3..00000000000 --- a/.changeset/chilled-months-bow.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"chainlink": minor ---- - -#added oracle support in standard capabilities diff --git a/.changeset/chilled-plants-clap.md b/.changeset/chilled-plants-clap.md deleted file mode 100644 index 2a23b0960f1..00000000000 --- a/.changeset/chilled-plants-clap.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"chainlink": patch ---- - -The findBroadcastedAttempts in detectStuckTransactionsHeuristic can returns uninitialized struct that potentially cause nil pointer error. Changed the return type of findBroadcastedAttempts to be pointers and added nil pointer check. #bugfix diff --git a/.changeset/chilly-crews-retire.md b/.changeset/chilly-crews-retire.md deleted file mode 100644 index 28b531a9ddb..00000000000 --- a/.changeset/chilly-crews-retire.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"chainlink": minor ---- - -#added log-event-trigger LOOPP capability, using ChainReader diff --git a/.changeset/cool-owls-laugh.md b/.changeset/cool-owls-laugh.md new file mode 100644 index 00000000000..a3667ed20e0 --- /dev/null +++ b/.changeset/cool-owls-laugh.md @@ -0,0 +1,5 @@ +--- +"chainlink": minor +--- + +Bump to start the next version diff --git a/.changeset/cuddly-colts-speak.md b/.changeset/cuddly-colts-speak.md deleted file mode 100644 index ab16cc83123..00000000000 --- a/.changeset/cuddly-colts-speak.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"chainlink": patch ---- - -Fix for Mercury transmitter decoding reports of a different codec version #internal diff --git a/.changeset/curly-baboons-enjoy.md b/.changeset/curly-baboons-enjoy.md deleted file mode 100644 index 440f394d23b..00000000000 --- a/.changeset/curly-baboons-enjoy.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"chainlink": minor ---- - -Added the compute unit limit estimation feature for the Solana TXM #added diff --git a/.changeset/curvy-eyes-own.md b/.changeset/curvy-eyes-own.md deleted file mode 100644 index 30e72a7cf57..00000000000 --- a/.changeset/curvy-eyes-own.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"chainlink": minor ---- - -Update dynamic fee types to align with geth #internal diff --git a/.changeset/early-rules-yell.md b/.changeset/early-rules-yell.md deleted file mode 100644 index 718f48063e8..00000000000 --- a/.changeset/early-rules-yell.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"chainlink": minor ---- - -#updated support aptos creation in chain config UI diff --git a/.changeset/eighty-bulldogs-smile.md b/.changeset/eighty-bulldogs-smile.md deleted file mode 100644 index c91d0cf58b7..00000000000 --- a/.changeset/eighty-bulldogs-smile.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"chainlink": minor ---- - -#added Use ApplyDefaultsAndValidate diff --git a/.changeset/eleven-olives-accept.md b/.changeset/eleven-olives-accept.md deleted file mode 100644 index c06cd65c9fe..00000000000 --- a/.changeset/eleven-olives-accept.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"chainlink": patch ---- - -#added #fix add chain fee tests to initial deploy diff --git a/.changeset/empty-bees-fix.md b/.changeset/empty-bees-fix.md deleted file mode 100644 index e76ee621253..00000000000 --- a/.changeset/empty-bees-fix.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"chainlink": minor ---- - -#wip implement gateway handler that forwards outgoing request from http target capability. introduce gateway http client diff --git a/.changeset/empty-pianos-attack.md b/.changeset/empty-pianos-attack.md deleted file mode 100644 index 5ec420b14ca..00000000000 --- a/.changeset/empty-pianos-attack.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"chainlink": patch ---- - -#internal Update gethwrapper for FeeQuoter, Internal.sol comment and byte calc update diff --git a/.changeset/fair-swans-accept.md b/.changeset/fair-swans-accept.md deleted file mode 100644 index dd834aa2d80..00000000000 --- a/.changeset/fair-swans-accept.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"chainlink": minor ---- - -#internal Add support for data word detail manual input in Contract Reader for searching through EVM log event data with Contract Reader QueryKey ValueComparators. diff --git a/.changeset/few-pillows-yawn.md b/.changeset/few-pillows-yawn.md deleted file mode 100644 index ada48200d53..00000000000 --- a/.changeset/few-pillows-yawn.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"chainlink": minor ---- - -#added Add RMNRemote in the chain reader definition diff --git a/.changeset/fifty-pillows-peel.md b/.changeset/fifty-pillows-peel.md deleted file mode 100644 index 711a8869aa4..00000000000 --- a/.changeset/fifty-pillows-peel.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"chainlink": patch ---- - -Added `EVM.HeadTracker.PersistenceEnabled` config option to disable persistence for HeadTracker. #added diff --git a/.changeset/fifty-squids-juggle.md b/.changeset/fifty-squids-juggle.md deleted file mode 100644 index 5c5530bf03c..00000000000 --- a/.changeset/fifty-squids-juggle.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"chainlink": minor ---- - -FHE empty reward fix #internal diff --git a/.changeset/five-chicken-talk.md b/.changeset/five-chicken-talk.md deleted file mode 100644 index 5f93b29364b..00000000000 --- a/.changeset/five-chicken-talk.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"chainlink": minor ---- - -#updated introduce network field on chain resolver diff --git a/.changeset/flat-horses-argue.md b/.changeset/flat-horses-argue.md deleted file mode 100644 index 08f151cd5aa..00000000000 --- a/.changeset/flat-horses-argue.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"chainlink": minor ---- - -#added LogPoller MaxLogsKept feature: recency count-based instead of time based log retention diff --git a/.changeset/flat-spiders-sing.md b/.changeset/flat-spiders-sing.md deleted file mode 100644 index 6fba341b191..00000000000 --- a/.changeset/flat-spiders-sing.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"chainlink": patch ---- - -Set chainType in chain client #internal diff --git a/.changeset/forty-lizards-camp.md b/.changeset/forty-lizards-camp.md deleted file mode 100644 index 3e0a5a59b0e..00000000000 --- a/.changeset/forty-lizards-camp.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"chainlink": patch ---- - -Enable rotating encryptionPublicKey in CapabilitiesRegistry contract diff --git a/.changeset/four-buses-invite.md b/.changeset/four-buses-invite.md deleted file mode 100644 index 2f657dd4503..00000000000 --- a/.changeset/four-buses-invite.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"chainlink": minor ---- - -#added Introduce aptosKeys Graphql query diff --git a/.changeset/fresh-falcons-cheer.md b/.changeset/fresh-falcons-cheer.md deleted file mode 100644 index 505582cc4fd..00000000000 --- a/.changeset/fresh-falcons-cheer.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"chainlink": minor ---- - -#added solana: compute unit limit configuration and transaction instruction diff --git a/.changeset/giant-pillows-sort.md b/.changeset/giant-pillows-sort.md deleted file mode 100644 index a2b44319ecb..00000000000 --- a/.changeset/giant-pillows-sort.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"chainlink": minor ---- - -#added Add prometheus metrics exposing health of telemetry client diff --git a/.changeset/gorgeous-wolves-collect.md b/.changeset/gorgeous-wolves-collect.md deleted file mode 100644 index 62b11100151..00000000000 --- a/.changeset/gorgeous-wolves-collect.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"chainlink": patch ---- - -Add DA oracle type to arbitrum and zksync configs #bugfix diff --git a/.changeset/healthy-flowers-nail.md b/.changeset/healthy-flowers-nail.md deleted file mode 100644 index aefc463472d..00000000000 --- a/.changeset/healthy-flowers-nail.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"chainlink": minor ---- - -Return ErrConnectivity error when halting bumping #internal diff --git a/.changeset/honest-cameras-cross.md b/.changeset/honest-cameras-cross.md deleted file mode 100644 index 1da50c97bd5..00000000000 --- a/.changeset/honest-cameras-cross.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"chainlink": patch ---- - -Implementing evm specific token data encoder for CCIP #internal diff --git a/.changeset/khaki-pants-melt.md b/.changeset/khaki-pants-melt.md deleted file mode 100644 index 133fd480f56..00000000000 --- a/.changeset/khaki-pants-melt.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"chainlink": patch ---- - -#updated Workflows Engine loop refactored diff --git a/.changeset/late-pillows-shake.md b/.changeset/late-pillows-shake.md deleted file mode 100644 index f27f09519db..00000000000 --- a/.changeset/late-pillows-shake.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"chainlink": patch ---- - -Refactor OP oracle to accept generic DA oracle config #wip diff --git a/.changeset/lemon-apples-explain.md b/.changeset/lemon-apples-explain.md deleted file mode 100644 index 75f2b874fc4..00000000000 --- a/.changeset/lemon-apples-explain.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"chainlink": patch ---- - -#internal update ccip chainreader config. diff --git a/.changeset/metal-eels-check.md b/.changeset/metal-eels-check.md deleted file mode 100644 index 387eb51fc6d..00000000000 --- a/.changeset/metal-eels-check.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"chainlink": patch ---- - -#updated Consume Feeds Manager WSRPC protos from Chainlink Protos Repository. diff --git a/.changeset/metal-meals-mix.md b/.changeset/metal-meals-mix.md deleted file mode 100644 index 66ebcec9982..00000000000 --- a/.changeset/metal-meals-mix.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"chainlink": patch ---- - -Adjustments for usdc reader tests #internal diff --git a/.changeset/metal-pots-lie.md b/.changeset/metal-pots-lie.md deleted file mode 100644 index bfa498cd4ac..00000000000 --- a/.changeset/metal-pots-lie.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"chainlink": patch ---- - -Registering USDC/CCTP events in the ChainReader during oracle creation #internal diff --git a/.changeset/moody-rules-agree.md b/.changeset/moody-rules-agree.md deleted file mode 100644 index ef1f3bcaf62..00000000000 --- a/.changeset/moody-rules-agree.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -"chainlink": patch ---- - -- register polling subscription to avoid subscription leaking when rpc client gets closed. -- add a temporary special treatment for SubscribeNewHead before we replace it with SubscribeToHeads. Add a goroutine that forwards new head from poller to caller channel. -- fix a deadlock in poller, by using a new lock for subs slice in rpc client. -#bugfix diff --git a/.changeset/moody-trains-grow.md b/.changeset/moody-trains-grow.md deleted file mode 100644 index 304e91c1664..00000000000 --- a/.changeset/moody-trains-grow.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"chainlink": patch ---- - -#added graceful shutdown for ccip oracles diff --git a/.changeset/neat-numbers-lay.md b/.changeset/neat-numbers-lay.md deleted file mode 100644 index 69276e84f68..00000000000 --- a/.changeset/neat-numbers-lay.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"chainlink": minor ---- - -#removed Removing unreferenced unused files. diff --git a/.changeset/nervous-books-push.md b/.changeset/nervous-books-push.md deleted file mode 100644 index b0f797344e3..00000000000 --- a/.changeset/nervous-books-push.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"chainlink": minor ---- - -#internal Updated QueryKey to be able to do advanced queries on contract event data words diff --git a/.changeset/nice-cows-yell.md b/.changeset/nice-cows-yell.md deleted file mode 100644 index 1e7f045b476..00000000000 --- a/.changeset/nice-cows-yell.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"chainlink": patch ---- - -#updated Refactors store_db diff --git a/.changeset/ninety-feet-change.md b/.changeset/ninety-feet-change.md deleted file mode 100644 index 2c4cea72106..00000000000 --- a/.changeset/ninety-feet-change.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"chainlink": patch ---- - -#updated default config values for FinalityTagEnabled to match CCIP configs diff --git a/.changeset/old-humans-watch.md b/.changeset/old-humans-watch.md deleted file mode 100644 index e58dedcd368..00000000000 --- a/.changeset/old-humans-watch.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"chainlink": minor ---- - -Fix TXM flakey test #internal diff --git a/.changeset/olive-bugs-relax.md b/.changeset/olive-bugs-relax.md deleted file mode 100644 index 6e681000358..00000000000 --- a/.changeset/olive-bugs-relax.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"chainlink": minor ---- - -add address bytes to string modifier to chainReader. #internal diff --git a/.changeset/orange-feet-share.md b/.changeset/orange-feet-share.md deleted file mode 100644 index 9583bce3d64..00000000000 --- a/.changeset/orange-feet-share.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -"chainlink": minor ---- - -Implemented new chain agnostic MultiNode design along with the corresponding EVM implementation. The chain agnostic components enable Multinode to be integrated with Solana and other non-EVM chains. Previously the Multinode was coupled with EVM specific actions, and was called to execute these actions direclty. With this change, the MultiNode's responsibility has been simplified to focus on RPC selection along with performing health checks. Chain specific actions will instead be executed on the RPC directly after being selected by MultiNode. The Chain Agnostic MultiNode provides improved reliability and metrics for all chain integrations using it. - -These are following main components: -Node: Common component which wraps an RPC with state information, health checks, and an alive loop to handle state changes along with maintaining chain information. -RPCClient: Chain-specific RPC wrapper which implements required interface for MultiNode along with any chain-specific functionality needed. -MultiNode: Perform RPCClient selection and performs health checks on all RPCs. -TransactionSender: Chain agnostic component which broadcasts transactions to all healthy RPCs and aggregates results. A chain-specific error classifier must be implemented. - -MultiNode picks the "best" RPC based on one of the configurable criteria: -- Priority defined in the config. -- Highest latest block. -- Round-robin within the same priority level (or using other configurable selection algorithms) - -Benefits of Chain Agnostic MultiNode: - Reliability: Improved RPC reliability scaleable to all chains - Maintainability: Can apply changes across all chain integrations through the use of common code - Extendability: Can add new health checks, RPC selection and ranking algorithms - Integration Speed: Much faster to integrate MultiNode with new chains - Reduced Generics: Significantly less bulky code! - -#updated #changed #internal diff --git a/.changeset/orange-humans-laugh.md b/.changeset/orange-humans-laugh.md deleted file mode 100644 index b9f8fa74f54..00000000000 --- a/.changeset/orange-humans-laugh.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"chainlink": minor ---- - -Support Zircuit fraud transactions detection and zk overflow detection #added diff --git a/.changeset/polite-tips-sneeze.md b/.changeset/polite-tips-sneeze.md deleted file mode 100644 index 4dc0e000046..00000000000 --- a/.changeset/polite-tips-sneeze.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"chainlink": minor ---- - -#added keystone contract deployment diff --git a/.changeset/pretty-worms-smell.md b/.changeset/pretty-worms-smell.md deleted file mode 100644 index 9633de09502..00000000000 --- a/.changeset/pretty-worms-smell.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"chainlink": patch ---- - -Added a custom client error message for Mantle to capture NonceTooLow error. #added diff --git a/.changeset/real-doors-visit.md b/.changeset/real-doors-visit.md deleted file mode 100644 index 39fa064c3a8..00000000000 --- a/.changeset/real-doors-visit.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"chainlink": patch ---- - -Implement blue-green Configurator contract and retirement report handover for LLO -#added diff --git a/.changeset/red-jobs-marry.md b/.changeset/red-jobs-marry.md deleted file mode 100644 index d66ad019027..00000000000 --- a/.changeset/red-jobs-marry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"chainlink": patch ---- - -Rename DA oracle consts to be more descriptive #wip diff --git a/.changeset/rude-spies-serve.md b/.changeset/rude-spies-serve.md deleted file mode 100644 index d861b5cf6ba..00000000000 --- a/.changeset/rude-spies-serve.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"chainlink": patch ---- - -#added new config field to FeeQuoter diff --git a/.changeset/seven-forks-hug.md b/.changeset/seven-forks-hug.md deleted file mode 100644 index 5ddcbfe3fec..00000000000 --- a/.changeset/seven-forks-hug.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"chainlink": minor ---- - -Add encryptionPublicKey to CapabilitiesRegistry.sol diff --git a/.changeset/silent-foxes-battle.md b/.changeset/silent-foxes-battle.md deleted file mode 100644 index f3f4b48962c..00000000000 --- a/.changeset/silent-foxes-battle.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"chainlink": patch ---- - -Bump chainlink-solana and fix tests #internal diff --git a/.changeset/silly-lies-boil.md b/.changeset/silly-lies-boil.md deleted file mode 100644 index b2a5084a36c..00000000000 --- a/.changeset/silly-lies-boil.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -"chainlink": minor ---- - -Make websocket URL `WSURL` for `EVM.Nodes` optional, and apply logic so that: -* If WS URL was not provided, SubscribeFilterLogs should fail with an explicit error -* If WS URL was not provided LogBroadcaster should be disabled -#nops diff --git a/.changeset/silly-starfishes-destroy.md b/.changeset/silly-starfishes-destroy.md deleted file mode 100644 index ebb552fdca2..00000000000 --- a/.changeset/silly-starfishes-destroy.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"chainlink": patch ---- - -#internal ccip contract reader config. diff --git a/.changeset/silver-pens-float.md b/.changeset/silver-pens-float.md deleted file mode 100644 index e8a135b9de4..00000000000 --- a/.changeset/silver-pens-float.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"chainlink": minor ---- - -#internal: Adding test for rmn home reader diff --git a/.changeset/six-frogs-juggle.md b/.changeset/six-frogs-juggle.md deleted file mode 100644 index 2b960c4be0f..00000000000 --- a/.changeset/six-frogs-juggle.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"chainlink": minor ---- - -#added HTTP target capability and gateway connector handler diff --git a/.changeset/smooth-queens-dance.md b/.changeset/smooth-queens-dance.md deleted file mode 100644 index 861c5284984..00000000000 --- a/.changeset/smooth-queens-dance.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"chainlink": minor ---- - -Add compute capability #internal diff --git a/.changeset/stale-pugs-sin.md b/.changeset/stale-pugs-sin.md deleted file mode 100644 index 58ebd2d7acd..00000000000 --- a/.changeset/stale-pugs-sin.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"chainlink": minor ---- - -#added introduce cosmosKeys and starknetKeys graphql query diff --git a/.changeset/ten-rats-tie.md b/.changeset/ten-rats-tie.md deleted file mode 100644 index f97594914cd..00000000000 --- a/.changeset/ten-rats-tie.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"chainlink": patch ---- - -Fix testWSServer issue causing panic in testing #internal diff --git a/.changeset/three-otters-drum.md b/.changeset/three-otters-drum.md deleted file mode 100644 index c3888df8580..00000000000 --- a/.changeset/three-otters-drum.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"chainlink": patch ---- - -Hedera chain type: broadcast transactions only to a single healthy RPC instead of all healthy RPCs to avoid redundant relay fees. #changed diff --git a/.changeset/tidy-apricots-care.md b/.changeset/tidy-apricots-care.md deleted file mode 100644 index 60429107f11..00000000000 --- a/.changeset/tidy-apricots-care.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"chainlink": minor ---- - -#added Pass the home chain selector to the commit plugin factory diff --git a/.changeset/tough-boats-shop.md b/.changeset/tough-boats-shop.md deleted file mode 100644 index 45a16fc965a..00000000000 --- a/.changeset/tough-boats-shop.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"chainlink": minor ---- - -start of next release diff --git a/.changeset/two-snails-mix.md b/.changeset/two-snails-mix.md deleted file mode 100644 index 0fb49dce9b2..00000000000 --- a/.changeset/two-snails-mix.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"chainlink": minor ---- - -Rename SequenceAt to NonceAt #internal diff --git a/.changeset/wise-pandas-join.md b/.changeset/wise-pandas-join.md deleted file mode 100644 index 7b375b0895d..00000000000 --- a/.changeset/wise-pandas-join.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"chainlink": minor ---- - -Fix BHE PriceMax bug #bugfix diff --git a/CHANGELOG.md b/CHANGELOG.md index c1a09677ba7..52b5cd967ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,114 @@ # Changelog Chainlink Core +## 2.18.0 - UNRELEASED + +### Minor Changes + +- [#14696](https://github.com/smartcontractkit/chainlink/pull/14696) [`072bfb667a`](https://github.com/smartcontractkit/chainlink/commit/072bfb667a4e1f7cc0c874409ebfe6ef7f7b6cbe) Thanks [@0xsuryansh](https://github.com/0xsuryansh)! - #internal add getNextDonId() and getNodes(bytes32[] calldata p2pIds) in CapabilitiesRegistry and define interface for node info + + PR issue: CCIP-3569 + +- [#14305](https://github.com/smartcontractkit/chainlink/pull/14305) [`5ca0d1f19f`](https://github.com/smartcontractkit/chainlink/commit/5ca0d1f19f90c7b42c3cb1ae7b6b860802c92f64) Thanks [@DeividasK](https://github.com/DeividasK)! - #added oracle support in standard capabilities +- [#14308](https://github.com/smartcontractkit/chainlink/pull/14308) [`3e9e058da4`](https://github.com/smartcontractkit/chainlink/commit/3e9e058da4f2ba4b51286d4f05aa7efcba179e79) Thanks [@kidambisrinivas](https://github.com/kidambisrinivas)! - #added log-event-trigger LOOPP capability, using ChainReader +- [#14741](https://github.com/smartcontractkit/chainlink/pull/14741) [`89fcfea8a4`](https://github.com/smartcontractkit/chainlink/commit/89fcfea8a42f67a592cb1187538f9b3a9791b58f) Thanks [@amit-momin](https://github.com/amit-momin)! - Added the compute unit limit estimation feature for the Solana TXM #added +- [#14602](https://github.com/smartcontractkit/chainlink/pull/14602) [`002296d9db`](https://github.com/smartcontractkit/chainlink/commit/002296d9dbbcb356d6f66d5e274b18bfb16681be) Thanks [@dimriou](https://github.com/dimriou)! - Update dynamic fee types to align with geth #internal +- [#14765](https://github.com/smartcontractkit/chainlink/pull/14765) [`ca31213970`](https://github.com/smartcontractkit/chainlink/commit/ca31213970eac30d46ffbb0e6551e12fc31ce7e5) Thanks [@graham-chainlink](https://github.com/graham-chainlink)! - #updated support aptos creation in chain config UI +- [#14651](https://github.com/smartcontractkit/chainlink/pull/14651) [`8ca41fc8f7`](https://github.com/smartcontractkit/chainlink/commit/8ca41fc8f722accfccccb4b1778db2df8fef5437) Thanks [@0xnogo](https://github.com/0xnogo)! - #added Use ApplyDefaultsAndValidate +- [#14536](https://github.com/smartcontractkit/chainlink/pull/14536) [`4b977021ed`](https://github.com/smartcontractkit/chainlink/commit/4b977021ed2150678bbe497afeb1312aa64e62cf) Thanks [@jinhoonbang](https://github.com/jinhoonbang)! - #wip implement gateway handler that forwards outgoing request from http target capability. introduce gateway http client +- [#14622](https://github.com/smartcontractkit/chainlink/pull/14622) [`c654322ace`](https://github.com/smartcontractkit/chainlink/commit/c654322acea7da8e6bd84a8a045690002f1f172d) Thanks [@ilija42](https://github.com/ilija42)! - #internal Add support for data word detail manual input in Contract Reader for searching through EVM log event data with Contract Reader QueryKey ValueComparators. +- [#14588](https://github.com/smartcontractkit/chainlink/pull/14588) [`fc1fefcba7`](https://github.com/smartcontractkit/chainlink/commit/fc1fefcba7a09699dade771d992a388cf71e82b4) Thanks [@0xnogo](https://github.com/0xnogo)! - #added Add RMNRemote in the chain reader definition +- [#14857](https://github.com/smartcontractkit/chainlink/pull/14857) [`b8ae4adcfc`](https://github.com/smartcontractkit/chainlink/commit/b8ae4adcfc1ab6b92f287a2c2a7051b140782a56) Thanks [@dimriou](https://github.com/dimriou)! - FHE empty reward fix #internal +- [#14718](https://github.com/smartcontractkit/chainlink/pull/14718) [`16499e5cb7`](https://github.com/smartcontractkit/chainlink/commit/16499e5cb79e3ccba654070d13d9af7be3d33a07) Thanks [@graham-chainlink](https://github.com/graham-chainlink)! - #updated introduce network field on chain resolver +- [#14574](https://github.com/smartcontractkit/chainlink/pull/14574) [`accbf0fe96`](https://github.com/smartcontractkit/chainlink/commit/accbf0fe9647f36bab9016f75b48a9338546ae7d) Thanks [@reductionista](https://github.com/reductionista)! - #added LogPoller MaxLogsKept feature: recency count-based instead of time based log retention +- [#14729](https://github.com/smartcontractkit/chainlink/pull/14729) [`770d2bc9b0`](https://github.com/smartcontractkit/chainlink/commit/770d2bc9b097b78b6396d7912eb64ddc2950afa6) Thanks [@graham-chainlink](https://github.com/graham-chainlink)! - #added Introduce aptosKeys Graphql query +- [#14576](https://github.com/smartcontractkit/chainlink/pull/14576) [`0e35bc794c`](https://github.com/smartcontractkit/chainlink/commit/0e35bc794ce666f13fb24a6918f7617928c17235) Thanks [@aalu1418](https://github.com/aalu1418)! - #added solana: compute unit limit configuration and transaction instruction +- [#14661](https://github.com/smartcontractkit/chainlink/pull/14661) [`9641ea29da`](https://github.com/smartcontractkit/chainlink/commit/9641ea29daa9da459975afaddc78120f5b15d38c) Thanks [@emate](https://github.com/emate)! - #added Add prometheus metrics exposing health of telemetry client +- [#14702](https://github.com/smartcontractkit/chainlink/pull/14702) [`ca9fb64356`](https://github.com/smartcontractkit/chainlink/commit/ca9fb64356735a4d4b6b6c1914256e74cd127d4b) Thanks [@dimriou](https://github.com/dimriou)! - Return ErrConnectivity error when halting bumping #internal +- [#14706](https://github.com/smartcontractkit/chainlink/pull/14706) [`f7abc3eb0e`](https://github.com/smartcontractkit/chainlink/commit/f7abc3eb0e9c6525a2d470205f730c888c8b929a) Thanks [@pavel-raykov](https://github.com/pavel-raykov)! - #removed Removing unreferenced unused files. +- [#14511](https://github.com/smartcontractkit/chainlink/pull/14511) [`8fa9a67dfc`](https://github.com/smartcontractkit/chainlink/commit/8fa9a67dfc130feab290860f0b7bf860ddc86bb3) Thanks [@silaslenihan](https://github.com/silaslenihan)! - #internal Updated QueryKey to be able to do advanced queries on contract event data words +- [#14697](https://github.com/smartcontractkit/chainlink/pull/14697) [`4c3e7ec8c3`](https://github.com/smartcontractkit/chainlink/commit/4c3e7ec8c3b334a13be13780dbfe19dc1f2eacd1) Thanks [@dimriou](https://github.com/dimriou)! - Fix TXM flakey test #internal +- [#14620](https://github.com/smartcontractkit/chainlink/pull/14620) [`09145baa9b`](https://github.com/smartcontractkit/chainlink/commit/09145baa9bb37103605cbebaad87c5059d1d454b) Thanks [@Farber98](https://github.com/Farber98)! - add address bytes to string modifier to chainReader. #internal +- [#13386](https://github.com/smartcontractkit/chainlink/pull/13386) [`4b21c32e25`](https://github.com/smartcontractkit/chainlink/commit/4b21c32e257f534f97829f50aaac4bbe8e9c1ae7) Thanks [@DylanTinianov](https://github.com/DylanTinianov)! - Implemented new chain agnostic MultiNode design along with the corresponding EVM implementation. The chain agnostic components enable Multinode to be integrated with Solana and other non-EVM chains. Previously the Multinode was coupled with EVM specific actions, and was called to execute these actions direclty. With this change, the MultiNode's responsibility has been simplified to focus on RPC selection along with performing health checks. Chain specific actions will instead be executed on the RPC directly after being selected by MultiNode. The Chain Agnostic MultiNode provides improved reliability and metrics for all chain integrations using it. + + These are following main components: + Node: Common component which wraps an RPC with state information, health checks, and an alive loop to handle state changes along with maintaining chain information. + RPCClient: Chain-specific RPC wrapper which implements required interface for MultiNode along with any chain-specific functionality needed. + MultiNode: Perform RPCClient selection and performs health checks on all RPCs. + TransactionSender: Chain agnostic component which broadcasts transactions to all healthy RPCs and aggregates results. A chain-specific error classifier must be implemented. + + MultiNode picks the "best" RPC based on one of the configurable criteria: + + - Priority defined in the config. + - Highest latest block. + - Round-robin within the same priority level (or using other configurable selection algorithms) + + Benefits of Chain Agnostic MultiNode: + Reliability: Improved RPC reliability scaleable to all chains + Maintainability: Can apply changes across all chain integrations through the use of common code + Extendability: Can add new health checks, RPC selection and ranking algorithms + Integration Speed: Much faster to integrate MultiNode with new chains + Reduced Generics: Significantly less bulky code! + + #updated #changed #internal +- [#14629](https://github.com/smartcontractkit/chainlink/pull/14629) [`4928e60ddf`](https://github.com/smartcontractkit/chainlink/commit/4928e60ddfe375e4a0c644cb210802b4c4db5dbd) Thanks [@huangzhen1997](https://github.com/huangzhen1997)! - Support Zircuit fraud transactions detection and zk overflow detection #added +- [#14334](https://github.com/smartcontractkit/chainlink/pull/14334) [`0ad624673f`](https://github.com/smartcontractkit/chainlink/commit/0ad624673f6f1a8e155fc43c67a8ae6caddefa90) Thanks [@krehermann](https://github.com/krehermann)! - #added keystone contract deployment +- [#14709](https://github.com/smartcontractkit/chainlink/pull/14709) [`1560aa9167`](https://github.com/smartcontractkit/chainlink/commit/1560aa9167a812abe3a8370c033b3290dcbcb261) Thanks [@KuphJr](https://github.com/KuphJr)! - Add encryptionPublicKey to CapabilitiesRegistry.sol +- [#14364](https://github.com/smartcontractkit/chainlink/pull/14364) [`5d96be59a2`](https://github.com/smartcontractkit/chainlink/commit/5d96be59a27f68f2f491a7d9f8cb0b2af4e0e835) Thanks [@huangzhen1997](https://github.com/huangzhen1997)! - Make websocket URL `WSURL` for `EVM.Nodes` optional, and apply logic so that: + + - If WS URL was not provided, SubscribeFilterLogs should fail with an explicit error + - If WS URL was not provided LogBroadcaster should be disabled + #nops + +- [#14789](https://github.com/smartcontractkit/chainlink/pull/14789) [`5dfb13071e`](https://github.com/smartcontractkit/chainlink/commit/5dfb13071e96f8e0cda7b79a4c1797c1f064972b) Thanks [@0xnogo](https://github.com/0xnogo)! - #internal: Adding test for rmn home reader +- [#14491](https://github.com/smartcontractkit/chainlink/pull/14491) [`c6e6e213a6`](https://github.com/smartcontractkit/chainlink/commit/c6e6e213a698b558d3302277566587ba0c30a6fe) Thanks [@jinhoonbang](https://github.com/jinhoonbang)! - #added HTTP target capability and gateway connector handler +- [#14496](https://github.com/smartcontractkit/chainlink/pull/14496) [`25c469880f`](https://github.com/smartcontractkit/chainlink/commit/25c469880f537556278e3d4eff3e87f185917834) Thanks [@cedric-cordenier](https://github.com/cedric-cordenier)! - Add compute capability #internal +- [#14764](https://github.com/smartcontractkit/chainlink/pull/14764) [`56d4b84cc2`](https://github.com/smartcontractkit/chainlink/commit/56d4b84cc2ccde953241a81bcaeb1929724c77a2) Thanks [@graham-chainlink](https://github.com/graham-chainlink)! - #added introduce cosmosKeys and starknetKeys graphql query +- [#14500](https://github.com/smartcontractkit/chainlink/pull/14500) [`d435981038`](https://github.com/smartcontractkit/chainlink/commit/d4359810380a34fbcdd2839e0fc23a35c5236acd) Thanks [@0xnogo](https://github.com/0xnogo)! - #added Pass the home chain selector to the commit plugin factory +- [#14564](https://github.com/smartcontractkit/chainlink/pull/14564) [`8b01661e03`](https://github.com/smartcontractkit/chainlink/commit/8b01661e032add54c07f269624b46bddd4476b95) Thanks [@momentmaker](https://github.com/momentmaker)! - start of next release +- [#14855](https://github.com/smartcontractkit/chainlink/pull/14855) [`7cd384b945`](https://github.com/smartcontractkit/chainlink/commit/7cd384b945ec66532b68cbdfffa3b48a8e375158) Thanks [@dimriou](https://github.com/dimriou)! - Rename SequenceAt to NonceAt #internal +- [#14670](https://github.com/smartcontractkit/chainlink/pull/14670) [`ad29b19612`](https://github.com/smartcontractkit/chainlink/commit/ad29b19612dbd9e2aab093bd0e7df85daa995662) Thanks [@dimriou](https://github.com/dimriou)! - Fix BHE PriceMax bug #bugfix + +### Patch Changes + +- [#14509](https://github.com/smartcontractkit/chainlink/pull/14509) [`dbd42db9b8`](https://github.com/smartcontractkit/chainlink/commit/dbd42db9b838b619d0f8a5acd21328ecd5043cd3) Thanks [@huangzhen1997](https://github.com/huangzhen1997)! - Remove finality depth as the default value for minConfirmation for tx jobs. + Update the sql query for fetching pending callback transactions: + if minConfirmation is not null, we check difference if the current block - tx block > minConfirmation + else we check if the tx block is <= finalizedBlock + #updated +- [#14685](https://github.com/smartcontractkit/chainlink/pull/14685) [`c5e9f932ca`](https://github.com/smartcontractkit/chainlink/commit/c5e9f932ca2115b833675d9c098e96676c2b6b58) Thanks [@huangzhen1997](https://github.com/huangzhen1997)! - The findBroadcastedAttempts in detectStuckTransactionsHeuristic can returns uninitialized struct that potentially cause nil pointer error. Changed the return type of findBroadcastedAttempts to be pointers and added nil pointer check. #bugfix +- [#14631](https://github.com/smartcontractkit/chainlink/pull/14631) [`ad9398a8fd`](https://github.com/smartcontractkit/chainlink/commit/ad9398a8fd2c2e95f3b4241bbfcfe264d8cda030) Thanks [@martin-cll](https://github.com/martin-cll)! - Fix for Mercury transmitter decoding reports of a different codec version #internal +- [#14829](https://github.com/smartcontractkit/chainlink/pull/14829) [`83c3a329b8`](https://github.com/smartcontractkit/chainlink/commit/83c3a329b87e08693d347deb588ad9725dbc681a) Thanks [@asoliman92](https://github.com/asoliman92)! - #added #fix add chain fee tests to initial deploy +- [#14798](https://github.com/smartcontractkit/chainlink/pull/14798) [`26e22eb6cf`](https://github.com/smartcontractkit/chainlink/commit/26e22eb6cfc320d981c91b1bfeb87c3c645f10d6) Thanks [@0xsuryansh](https://github.com/0xsuryansh)! - #internal Update gethwrapper for FeeQuoter, Internal.sol comment and byte calc update +- [#14558](https://github.com/smartcontractkit/chainlink/pull/14558) [`133e347872`](https://github.com/smartcontractkit/chainlink/commit/133e3478727814575636d5989af9d61cbf7997cb) Thanks [@dhaidashenko](https://github.com/dhaidashenko)! - Added `EVM.HeadTracker.PersistenceEnabled` config option to disable persistence for HeadTracker. #added +- [#14719](https://github.com/smartcontractkit/chainlink/pull/14719) [`de9ce674c6`](https://github.com/smartcontractkit/chainlink/commit/de9ce674c6fd18c1c0a2ce7896682d7b711eea8c) Thanks [@amit-momin](https://github.com/amit-momin)! - Set chainType in chain client #internal +- [#14760](https://github.com/smartcontractkit/chainlink/pull/14760) [`3af39c8032`](https://github.com/smartcontractkit/chainlink/commit/3af39c803201461009ef63f709851fe6a24f0284) Thanks [@KuphJr](https://github.com/KuphJr)! - Enable rotating encryptionPublicKey in CapabilitiesRegistry contract +- [#14835](https://github.com/smartcontractkit/chainlink/pull/14835) [`468fc4cd35`](https://github.com/smartcontractkit/chainlink/commit/468fc4cd352b5be8cfd0e65f7da370fdf544be46) Thanks [@ogtownsend](https://github.com/ogtownsend)! - Add DA oracle type to arbitrum and zksync configs #bugfix +- [#14603](https://github.com/smartcontractkit/chainlink/pull/14603) [`19690b0c44`](https://github.com/smartcontractkit/chainlink/commit/19690b0c4499cf01379f98396917be170ba2b333) Thanks [@mateusz-sekara](https://github.com/mateusz-sekara)! - Implementing evm specific token data encoder for CCIP #internal +- [#14375](https://github.com/smartcontractkit/chainlink/pull/14375) [`816b25c001`](https://github.com/smartcontractkit/chainlink/commit/816b25c00122003e14e3d214d9de7a6e58b3323e) Thanks [@vyzaldysanchez](https://github.com/vyzaldysanchez)! - #updated Workflows Engine loop refactored +- [#14599](https://github.com/smartcontractkit/chainlink/pull/14599) [`4ac405a6d5`](https://github.com/smartcontractkit/chainlink/commit/4ac405a6d5e3bc668ee96eac6bdbecc524467fd6) Thanks [@ogtownsend](https://github.com/ogtownsend)! - Refactor OP oracle to accept generic DA oracle config #wip +- [#14607](https://github.com/smartcontractkit/chainlink/pull/14607) [`eec0ff9469`](https://github.com/smartcontractkit/chainlink/commit/eec0ff946986dfbeeb46ed3d311ff9b8d0e21735) Thanks [@winder](https://github.com/winder)! - #internal update ccip chainreader config. +- [#14693](https://github.com/smartcontractkit/chainlink/pull/14693) [`03df8989e8`](https://github.com/smartcontractkit/chainlink/commit/03df8989e8e7549afb05bb49c765c0c07db8669e) Thanks [@ChrisAmora](https://github.com/ChrisAmora)! - #updated Consume Feeds Manager WSRPC protos from Chainlink Protos Repository. +- [#14694](https://github.com/smartcontractkit/chainlink/pull/14694) [`e9b3397f46`](https://github.com/smartcontractkit/chainlink/commit/e9b3397f465e26209c8c8ccfed66aa9595f8246b) Thanks [@mateusz-sekara](https://github.com/mateusz-sekara)! - Adjustments for usdc reader tests #internal +- [#14624](https://github.com/smartcontractkit/chainlink/pull/14624) [`be774f00a9`](https://github.com/smartcontractkit/chainlink/commit/be774f00a961d9a7361d9ae5b10c97996f7ab164) Thanks [@mateusz-sekara](https://github.com/mateusz-sekara)! - Registering USDC/CCTP events in the ChainReader during oracle creation #internal +- [#14534](https://github.com/smartcontractkit/chainlink/pull/14534) [`de268e98b8`](https://github.com/smartcontractkit/chainlink/commit/de268e98b8d68a284e1260297925b91c5d2411bc) Thanks [@huangzhen1997](https://github.com/huangzhen1997)! - - register polling subscription to avoid subscription leaking when rpc client gets closed. + + - add a temporary special treatment for SubscribeNewHead before we replace it with SubscribeToHeads. Add a goroutine that forwards new head from poller to caller channel. + - fix a deadlock in poller, by using a new lock for subs slice in rpc client. + #bugfix + +- [#14656](https://github.com/smartcontractkit/chainlink/pull/14656) [`004a0de233`](https://github.com/smartcontractkit/chainlink/commit/004a0de2337b0312558ae7c045e7fc2fb4a05916) Thanks [@dimkouv](https://github.com/dimkouv)! - #added graceful shutdown for ccip oracles +- [#14720](https://github.com/smartcontractkit/chainlink/pull/14720) [`4f8c55eb01`](https://github.com/smartcontractkit/chainlink/commit/4f8c55eb01b5823f43f49761344e92dc37ec0114) Thanks [@vyzaldysanchez](https://github.com/vyzaldysanchez)! - #updated Refactors store_db +- [#14530](https://github.com/smartcontractkit/chainlink/pull/14530) [`2c16f46311`](https://github.com/smartcontractkit/chainlink/commit/2c16f4631184a6e3da7f2f3957173500e2c4837b) Thanks [@Madalosso](https://github.com/Madalosso)! - #updated default config values for FinalityTagEnabled to match CCIP configs +- [#14859](https://github.com/smartcontractkit/chainlink/pull/14859) [`80b0501bee`](https://github.com/smartcontractkit/chainlink/commit/80b0501bee805f30da170f779e7ceeb4b0ebe179) Thanks [@ma33r](https://github.com/ma33r)! - Added a custom client error message for Mantle to capture NonceTooLow error. #added +- [#14628](https://github.com/smartcontractkit/chainlink/pull/14628) [`6ec5ab8315`](https://github.com/smartcontractkit/chainlink/commit/6ec5ab83158172ac0c8e591bfa8dd9ffb35880ea) Thanks [@samsondav](https://github.com/samsondav)! - Implement blue-green Configurator contract and retirement report handover for LLO + #added +- [#14743](https://github.com/smartcontractkit/chainlink/pull/14743) [`d5dfe4af8c`](https://github.com/smartcontractkit/chainlink/commit/d5dfe4af8c87d481e9963275caf95407d38361f1) Thanks [@ogtownsend](https://github.com/ogtownsend)! - Rename DA oracle consts to be more descriptive #wip +- [#14734](https://github.com/smartcontractkit/chainlink/pull/14734) [`ca71878aa5`](https://github.com/smartcontractkit/chainlink/commit/ca71878aa5a55fe239a456d7b564ffeba9bc84d7) Thanks [@RyanRHall](https://github.com/RyanRHall)! - #added new config field to FeeQuoter +- [#14575](https://github.com/smartcontractkit/chainlink/pull/14575) [`1b41e69cca`](https://github.com/smartcontractkit/chainlink/commit/1b41e69cca2f4622d367ef18733c36fcae433505) Thanks [@DylanTinianov](https://github.com/DylanTinianov)! - Bump chainlink-solana and fix tests #internal +- [#14668](https://github.com/smartcontractkit/chainlink/pull/14668) [`dacb6a8c70`](https://github.com/smartcontractkit/chainlink/commit/dacb6a8c708e8d7cb94aa63ae7463f58a38d0e59) Thanks [@winder](https://github.com/winder)! - #internal ccip contract reader config. +- [#14814](https://github.com/smartcontractkit/chainlink/pull/14814) [`f708ebb094`](https://github.com/smartcontractkit/chainlink/commit/f708ebb094ecd6f4f77e9c480ceacd250fc1fadc) Thanks [@DylanTinianov](https://github.com/DylanTinianov)! - Fix testWSServer issue causing panic in testing #internal +- [#14635](https://github.com/smartcontractkit/chainlink/pull/14635) [`ee1d6e3b1a`](https://github.com/smartcontractkit/chainlink/commit/ee1d6e3b1a60dc657a5cab869aac0897e33dc76d) Thanks [@dhaidashenko](https://github.com/dhaidashenko)! - Hedera chain type: broadcast transactions only to a single healthy RPC instead of all healthy RPCs to avoid redundant relay fees. #changed + ## 2.17.0 - 2024-10-10 ### Minor Changes diff --git a/package.json b/package.json index 0382ca4e5cb..1da5d63f6d8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "chainlink", - "version": "2.17.0", + "version": "2.18.0", "description": "node of the decentralized oracle network, bridging on and off-chain computation", "main": "index.js", "scripts": { From bbd887e3eb493fc4bc7a1301e219b1cf712fbf05 Mon Sep 17 00:00:00 2001 From: Jordan Krage Date: Tue, 22 Oct 2024 14:06:50 -0500 Subject: [PATCH 2/7] core/services/ocr2/plugins/ccip/internal/ccipdata/factory: check error from TypeAndVersion (#14861) * core/services/ocr2/plugins/ccip/internal/ccipdata/factory: check error from TypeAndVersion * .github: CCIP CODEOWNERS (cherry picked from commit cc9bafa3916e4aa56ea48deb7834d20f914e67eb) --- .github/CODEOWNERS | 4 ++++ .../plugins/ccip/internal/ccipdata/factory/price_registry.go | 3 +++ 2 files changed, 7 insertions(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 5fc21e8c159..3bba74af7ce 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -31,6 +31,10 @@ /core/services/webhook @smartcontractkit/foundations @smartcontractkit/bix-framework /core/services/llo @smartcontractkit/data-streams-engineers +# CCIP +/core/services/ccip @smartcontractkit/ccip +/core/services/ocr2/plugins/ccip @smartcontractkit/ccip + # VRF-related services /core/services/vrf @smartcontractkit/dev-services /core/services/blockhashstore @smartcontractkit/dev-services diff --git a/core/services/ocr2/plugins/ccip/internal/ccipdata/factory/price_registry.go b/core/services/ocr2/plugins/ccip/internal/ccipdata/factory/price_registry.go index 214c3bad73e..cb82e7273bf 100644 --- a/core/services/ocr2/plugins/ccip/internal/ccipdata/factory/price_registry.go +++ b/core/services/ocr2/plugins/ccip/internal/ccipdata/factory/price_registry.go @@ -35,6 +35,9 @@ func initOrClosePriceRegistryReader(ctx context.Context, lggr logger.Logger, ver } contractType, version, err := versionFinder.TypeAndVersion(priceRegistryAddress, cl) + if err != nil { + return nil, err + } if contractType != ccipconfig.PriceRegistry { return nil, errors.Errorf("expected %v got %v", ccipconfig.PriceRegistry, contractType) } From 1d72e0cd3a7a8986a40fd31f9cad76815c6feee4 Mon Sep 17 00:00:00 2001 From: Dmytro Haidashenko <34754799+dhaidashenko@users.noreply.github.com> Date: Wed, 23 Oct 2024 23:12:34 +0200 Subject: [PATCH 3/7] TXM Configmer logs warning only if the provided chain does not contain finalized block (#14914) (cherry picked from commit 47aaff4320c800a9c85241949981cee687051aac) --- common/txmgr/confirmer.go | 27 +++++++++++++------------ core/chains/evm/txmgr/confirmer_test.go | 21 ++++++++----------- 2 files changed, 22 insertions(+), 26 deletions(-) diff --git a/common/txmgr/confirmer.go b/common/txmgr/confirmer.go index d2d491d794c..dd3dc4e4b33 100644 --- a/common/txmgr/confirmer.go +++ b/common/txmgr/confirmer.go @@ -339,7 +339,7 @@ func (ec *Confirmer[CHAIN_ID, HEAD, ADDR, TX_HASH, BLOCK_HASH, R, SEQ, FEE]) pro ec.lggr.Debugw("Finished RebroadcastWhereNecessary", "headNum", head.BlockNumber(), "time", time.Since(mark), "id", "confirmer") mark = time.Now() - if err := ec.EnsureConfirmedTransactionsInLongestChain(ctx, head, latestFinalizedHead.BlockNumber()); err != nil { + if err := ec.EnsureConfirmedTransactionsInLongestChain(ctx, head); err != nil { return fmt.Errorf("EnsureConfirmedTransactionsInLongestChain failed: %w", err) } @@ -1072,19 +1072,20 @@ func (ec *Confirmer[CHAIN_ID, HEAD, ADDR, TX_HASH, BLOCK_HASH, R, SEQ, FEE]) han // // If any of the confirmed transactions does not have a receipt in the chain, it has been // re-org'd out and will be rebroadcast. -func (ec *Confirmer[CHAIN_ID, HEAD, ADDR, TX_HASH, BLOCK_HASH, R, SEQ, FEE]) EnsureConfirmedTransactionsInLongestChain(ctx context.Context, head types.Head[BLOCK_HASH], latestFinalizedHeadNumber int64) error { +func (ec *Confirmer[CHAIN_ID, HEAD, ADDR, TX_HASH, BLOCK_HASH, R, SEQ, FEE]) EnsureConfirmedTransactionsInLongestChain(ctx context.Context, head types.Head[BLOCK_HASH]) error { logArgs := []interface{}{ - "chainLength", head.ChainLength(), "latestFinalizedHead number", latestFinalizedHeadNumber, - } - - if head.BlockNumber() < latestFinalizedHeadNumber { - errMsg := "current head is shorter than latest finalized head" - ec.lggr.Errorw(errMsg, append(logArgs, "head block number", head.BlockNumber())...) - return errors.New(errMsg) - } - - calculatedFinalityDepth := uint32(head.BlockNumber() - latestFinalizedHeadNumber) - if head.ChainLength() < calculatedFinalityDepth { + "chainLength", head.ChainLength(), + } + + //Here, we rely on the finalized block provided in the chain instead of the one + //provided via a dedicated method to avoid the false warning of the chain being + //too short. When `FinalityTagBypass = true,` HeadTracker tracks `finality depth + //+ history depth` to prevent excessive CPU usage. Thus, the provided chain may + //be shorter than the chain from the latest to the latest finalized, marked with + //a tag. A proper fix of this issue and complete switch to finality tag support + //will be introduced in BCFR-620 + latestFinalized := head.LatestFinalizedHead() + if latestFinalized == nil || !latestFinalized.IsValid() { if ec.nConsecutiveBlocksChainTooShort > logAfterNConsecutiveBlocksChainTooShort { warnMsg := "Chain length supplied for re-org detection was shorter than the depth from the latest head to the finalized head. Re-org protection is not working properly. This could indicate a problem with the remote RPC endpoint, a compatibility issue with a particular blockchain, a bug with this particular blockchain, heads table being truncated too early, remote node out of sync, or something else. If this happens a lot please raise a bug with the Chainlink team including a log output sample and details of the chain and RPC endpoint you are using." ec.lggr.Warnw(warnMsg, append(logArgs, "nConsecutiveBlocksChainTooShort", ec.nConsecutiveBlocksChainTooShort)...) diff --git a/core/chains/evm/txmgr/confirmer_test.go b/core/chains/evm/txmgr/confirmer_test.go index 03a43a61cf0..d468d1b4c10 100644 --- a/core/chains/evm/txmgr/confirmer_test.go +++ b/core/chains/evm/txmgr/confirmer_test.go @@ -2742,11 +2742,6 @@ func TestEthConfirmer_EnsureConfirmedTransactionsInLongestChain(t *testing.T) { gconfig, config := newTestChainScopedConfig(t) ec := newEthConfirmer(t, txStore, ethClient, gconfig, config, ethKeyStore, nil) - latestFinalizedHead := evmtypes.Head{ - Number: 8, - Hash: testutils.NewHash(), - } - h8 := &evmtypes.Head{ Number: 8, Hash: testutils.NewHash(), @@ -2762,14 +2757,14 @@ func TestEthConfirmer_EnsureConfirmedTransactionsInLongestChain(t *testing.T) { } head.Parent.Store(h9) t.Run("does nothing if there aren't any transactions", func(t *testing.T) { - require.NoError(t, ec.EnsureConfirmedTransactionsInLongestChain(tests.Context(t), head, latestFinalizedHead.BlockNumber())) + require.NoError(t, ec.EnsureConfirmedTransactionsInLongestChain(tests.Context(t), head)) }) t.Run("does nothing to unconfirmed transactions", func(t *testing.T) { etx := cltest.MustInsertUnconfirmedEthTxWithBroadcastLegacyAttempt(t, txStore, 0, fromAddress) // Do the thing - require.NoError(t, ec.EnsureConfirmedTransactionsInLongestChain(tests.Context(t), head, latestFinalizedHead.BlockNumber())) + require.NoError(t, ec.EnsureConfirmedTransactionsInLongestChain(tests.Context(t), head)) etx, err := txStore.FindTxWithAttempts(ctx, etx.ID) require.NoError(t, err) @@ -2781,7 +2776,7 @@ func TestEthConfirmer_EnsureConfirmedTransactionsInLongestChain(t *testing.T) { mustInsertEthReceipt(t, txStore, head.Number, head.Hash, etx.TxAttempts[0].Hash) // Do the thing - require.NoError(t, ec.EnsureConfirmedTransactionsInLongestChain(tests.Context(t), head, latestFinalizedHead.BlockNumber())) + require.NoError(t, ec.EnsureConfirmedTransactionsInLongestChain(tests.Context(t), head)) etx, err := txStore.FindTxWithAttempts(ctx, etx.ID) require.NoError(t, err) @@ -2794,7 +2789,7 @@ func TestEthConfirmer_EnsureConfirmedTransactionsInLongestChain(t *testing.T) { mustInsertEthReceipt(t, txStore, h8.Number-1, testutils.NewHash(), etx.TxAttempts[0].Hash) // Do the thing - require.NoError(t, ec.EnsureConfirmedTransactionsInLongestChain(tests.Context(t), head, latestFinalizedHead.BlockNumber())) + require.NoError(t, ec.EnsureConfirmedTransactionsInLongestChain(tests.Context(t), head)) etx, err := txStore.FindTxWithAttempts(ctx, etx.ID) require.NoError(t, err) @@ -2815,7 +2810,7 @@ func TestEthConfirmer_EnsureConfirmedTransactionsInLongestChain(t *testing.T) { }), fromAddress).Return(commonclient.Successful, nil).Once() // Do the thing - require.NoError(t, ec.EnsureConfirmedTransactionsInLongestChain(tests.Context(t), head, latestFinalizedHead.BlockNumber())) + require.NoError(t, ec.EnsureConfirmedTransactionsInLongestChain(tests.Context(t), head)) etx, err := txStore.FindTxWithAttempts(ctx, etx.ID) require.NoError(t, err) @@ -2838,7 +2833,7 @@ func TestEthConfirmer_EnsureConfirmedTransactionsInLongestChain(t *testing.T) { commonclient.Successful, nil).Once() // Do the thing - require.NoError(t, ec.EnsureConfirmedTransactionsInLongestChain(tests.Context(t), head, latestFinalizedHead.BlockNumber())) + require.NoError(t, ec.EnsureConfirmedTransactionsInLongestChain(tests.Context(t), head)) etx, err := txStore.FindTxWithAttempts(ctx, etx.ID) require.NoError(t, err) @@ -2873,7 +2868,7 @@ func TestEthConfirmer_EnsureConfirmedTransactionsInLongestChain(t *testing.T) { }), fromAddress).Return(commonclient.Successful, nil).Once() // Do the thing - require.NoError(t, ec.EnsureConfirmedTransactionsInLongestChain(tests.Context(t), head, latestFinalizedHead.BlockNumber())) + require.NoError(t, ec.EnsureConfirmedTransactionsInLongestChain(tests.Context(t), head)) etx, err := txStore.FindTxWithAttempts(ctx, etx.ID) require.NoError(t, err) @@ -2893,7 +2888,7 @@ func TestEthConfirmer_EnsureConfirmedTransactionsInLongestChain(t *testing.T) { // Add receipt that is higher than head mustInsertEthReceipt(t, txStore, head.Number+1, testutils.NewHash(), attempt.Hash) - require.NoError(t, ec.EnsureConfirmedTransactionsInLongestChain(tests.Context(t), head, latestFinalizedHead.BlockNumber())) + require.NoError(t, ec.EnsureConfirmedTransactionsInLongestChain(tests.Context(t), head)) etx, err := txStore.FindTxWithAttempts(ctx, etx.ID) require.NoError(t, err) From 6951f9e74ae016fab1548b682d4fd8ed5b818d3c Mon Sep 17 00:00:00 2001 From: Oliver Townsend <133903322+ogtownsend@users.noreply.github.com> Date: Wed, 30 Oct 2024 01:27:57 -0700 Subject: [PATCH 4/7] Update DA oracle config struct members to pointers (#15008) * Update DA oracle config struct members to pointers * Update tests and remake config docs * suggestions and fix docs_test * fix config test (cherry picked from commit b5856542d0c5cb887e6d95a9c663e20ae8a6544f) --- .changeset/green-crabs-joke.md | 5 ++ .../ocrimpls/contract_transmitter_test.go | 11 +++- .../evm/config/chain_scoped_gas_estimator.go | 6 +-- core/chains/evm/config/config.go | 4 +- core/chains/evm/config/toml/config.go | 23 ++++++-- core/chains/evm/gas/models.go | 1 + .../evm/gas/rollups/da_oracle_test_helper.go | 8 +-- core/chains/evm/gas/rollups/l1_oracle.go | 8 ++- core/chains/evm/gas/rollups/op_l1_oracle.go | 54 ++++++++++++------- core/chains/evm/txmgr/test_helpers.go | 7 ++- core/config/docs/docs_test.go | 4 +- core/services/chainlink/config_test.go | 7 +++ docs/CONFIG.md | 22 -------- 13 files changed, 99 insertions(+), 61 deletions(-) create mode 100644 .changeset/green-crabs-joke.md diff --git a/.changeset/green-crabs-joke.md b/.changeset/green-crabs-joke.md new file mode 100644 index 00000000000..4e9480e9c89 --- /dev/null +++ b/.changeset/green-crabs-joke.md @@ -0,0 +1,5 @@ +--- +"chainlink": patch +--- + +#bugfix Update DA oracle config struct members to pointers diff --git a/core/capabilities/ccip/ocrimpls/contract_transmitter_test.go b/core/capabilities/ccip/ocrimpls/contract_transmitter_test.go index 0f84d80a1f8..fa40dca6b85 100644 --- a/core/capabilities/ccip/ocrimpls/contract_transmitter_test.go +++ b/core/capabilities/ccip/ocrimpls/contract_transmitter_test.go @@ -600,7 +600,11 @@ type TestDAOracleConfig struct { evmconfig.DAOracle } -func (d *TestDAOracleConfig) OracleType() toml.DAOracleType { return toml.DAOracleOPStack } +func (d *TestDAOracleConfig) OracleType() *toml.DAOracleType { + oracleType := toml.DAOracleOPStack + return &oracleType +} + func (d *TestDAOracleConfig) OracleAddress() *types.EIP55Address { a, err := types.NewEIP55Address("0x420000000000000000000000000000000000000F") if err != nil { @@ -608,7 +612,10 @@ func (d *TestDAOracleConfig) OracleAddress() *types.EIP55Address { } return &a } -func (d *TestDAOracleConfig) CustomGasPriceCalldata() string { return "" } + +func (d *TestDAOracleConfig) CustomGasPriceCalldata() *string { + return nil +} func (g *TestGasEstimatorConfig) BlockHistory() evmconfig.BlockHistory { return &TestBlockHistoryConfig{} diff --git a/core/chains/evm/config/chain_scoped_gas_estimator.go b/core/chains/evm/config/chain_scoped_gas_estimator.go index 1e04690b12f..764efc20191 100644 --- a/core/chains/evm/config/chain_scoped_gas_estimator.go +++ b/core/chains/evm/config/chain_scoped_gas_estimator.go @@ -127,7 +127,7 @@ type daOracleConfig struct { c toml.DAOracle } -func (d *daOracleConfig) OracleType() toml.DAOracleType { +func (d *daOracleConfig) OracleType() *toml.DAOracleType { return d.c.OracleType } @@ -137,9 +137,9 @@ func (d *daOracleConfig) OracleAddress() *types.EIP55Address { } // CustomGasPriceCalldata returns the calldata for a custom gas price API. -func (d *daOracleConfig) CustomGasPriceCalldata() string { +func (d *daOracleConfig) CustomGasPriceCalldata() *string { // TODO: CCIP-3710 update once custom calldata oracle is added - return "" + return nil } type limitJobTypeConfig struct { diff --git a/core/chains/evm/config/config.go b/core/chains/evm/config/config.go index 6d4bcf159b5..f2a571f94b0 100644 --- a/core/chains/evm/config/config.go +++ b/core/chains/evm/config/config.go @@ -165,9 +165,9 @@ type BlockHistory interface { } type DAOracle interface { - OracleType() toml.DAOracleType + OracleType() *toml.DAOracleType OracleAddress() *types.EIP55Address - CustomGasPriceCalldata() string + CustomGasPriceCalldata() *string } type FeeHistory interface { diff --git a/core/chains/evm/config/toml/config.go b/core/chains/evm/config/toml/config.go index cc6a8d4eabf..4a1f73094d5 100644 --- a/core/chains/evm/config/toml/config.go +++ b/core/chains/evm/config/toml/config.go @@ -759,9 +759,9 @@ func (u *FeeHistoryEstimator) setFrom(f *FeeHistoryEstimator) { } type DAOracle struct { - OracleType DAOracleType + OracleType *DAOracleType OracleAddress *types.EIP55Address - CustomGasPriceCalldata string + CustomGasPriceCalldata *string } type DAOracleType string @@ -772,6 +772,17 @@ const ( DAOracleZKSync = DAOracleType("zksync") ) +func (o *DAOracle) ValidateConfig() (err error) { + if o.OracleType != nil { + if *o.OracleType == DAOracleOPStack { + if o.OracleAddress == nil { + err = multierr.Append(err, commonconfig.ErrMissing{Name: "OracleAddress", Msg: "required for 'opstack' oracle types"}) + } + } + } + return +} + func (o DAOracleType) IsValid() bool { switch o { case "", DAOracleOPStack, DAOracleArbitrum, DAOracleZKSync: @@ -781,11 +792,15 @@ func (o DAOracleType) IsValid() bool { } func (d *DAOracle) setFrom(f *DAOracle) { - d.OracleType = f.OracleType + if v := f.OracleType; v != nil { + d.OracleType = v + } if v := f.OracleAddress; v != nil { d.OracleAddress = v } - d.CustomGasPriceCalldata = f.CustomGasPriceCalldata + if v := f.CustomGasPriceCalldata; v != nil { + d.CustomGasPriceCalldata = v + } } type KeySpecificConfig []KeySpecific diff --git a/core/chains/evm/gas/models.go b/core/chains/evm/gas/models.go index cac6efb7271..a162cdd014a 100644 --- a/core/chains/evm/gas/models.go +++ b/core/chains/evm/gas/models.go @@ -76,6 +76,7 @@ func NewEstimator(lggr logger.Logger, ethClient feeEstimatorClient, chaintype ch "priceMax", geCfg.PriceMax(), "priceMin", geCfg.PriceMin(), "estimateLimit", geCfg.EstimateLimit(), + "daOracleType", geCfg.DAOracle().OracleType(), "daOracleAddress", geCfg.DAOracle().OracleAddress(), ) df := geCfg.EIP1559DynamicFees() diff --git a/core/chains/evm/gas/rollups/da_oracle_test_helper.go b/core/chains/evm/gas/rollups/da_oracle_test_helper.go index 6dfa96d3118..ce5343f9863 100644 --- a/core/chains/evm/gas/rollups/da_oracle_test_helper.go +++ b/core/chains/evm/gas/rollups/da_oracle_test_helper.go @@ -13,7 +13,7 @@ type TestDAOracle struct { toml.DAOracle } -func (d *TestDAOracle) OracleType() toml.DAOracleType { +func (d *TestDAOracle) OracleType() *toml.DAOracleType { return d.DAOracle.OracleType } @@ -21,7 +21,7 @@ func (d *TestDAOracle) OracleAddress() *types.EIP55Address { return d.DAOracle.OracleAddress } -func (d *TestDAOracle) CustomGasPriceCalldata() string { +func (d *TestDAOracle) CustomGasPriceCalldata() *string { return d.DAOracle.CustomGasPriceCalldata } @@ -31,9 +31,9 @@ func CreateTestDAOracle(t *testing.T, oracleType toml.DAOracleType, oracleAddres return &TestDAOracle{ DAOracle: toml.DAOracle{ - OracleType: oracleType, + OracleType: &oracleType, OracleAddress: &oracleAddr, - CustomGasPriceCalldata: customGasPriceCalldata, + CustomGasPriceCalldata: &customGasPriceCalldata, }, } } diff --git a/core/chains/evm/gas/rollups/l1_oracle.go b/core/chains/evm/gas/rollups/l1_oracle.go index 28172051fb7..df6fbd9fa44 100644 --- a/core/chains/evm/gas/rollups/l1_oracle.go +++ b/core/chains/evm/gas/rollups/l1_oracle.go @@ -2,6 +2,7 @@ package rollups import ( "context" + "errors" "fmt" "math/big" "slices" @@ -56,7 +57,12 @@ func NewL1GasOracle(lggr logger.Logger, ethClient l1OracleClient, chainType chai var l1Oracle L1Oracle var err error if daOracle != nil { - switch daOracle.OracleType() { + oracleType := daOracle.OracleType() + if oracleType == nil { + return nil, errors.New("required field OracleType is nil in non-nil DAOracle config") + } + + switch *oracleType { case toml.DAOracleOPStack: l1Oracle, err = NewOpStackL1GasOracle(lggr, ethClient, chainType, daOracle) case toml.DAOracleArbitrum: diff --git a/core/chains/evm/gas/rollups/op_l1_oracle.go b/core/chains/evm/gas/rollups/op_l1_oracle.go index 4f1e8c67cb7..764a1bfb875 100644 --- a/core/chains/evm/gas/rollups/op_l1_oracle.go +++ b/core/chains/evm/gas/rollups/op_l1_oracle.go @@ -2,6 +2,7 @@ package rollups import ( "context" + "errors" "fmt" "math/big" "strings" @@ -21,6 +22,7 @@ import ( evmclient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" evmconfig "github.com/smartcontractkit/chainlink/v2/core/chains/evm/config" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/config/chaintype" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/config/toml" ) // Reads L2-specific precompiles and caches the l1GasPrice set by the L2. @@ -30,12 +32,12 @@ type optimismL1Oracle struct { pollPeriod time.Duration logger logger.SugaredLogger - daOracleConfig evmconfig.DAOracle - l1GasPriceMu sync.RWMutex - l1GasPrice priceEntry - isEcotone bool - isFjord bool - upgradeCheckTs time.Time + daOracleAddress common.Address + l1GasPriceMu sync.RWMutex + l1GasPrice priceEntry + isEcotone bool + isFjord bool + upgradeCheckTs time.Time chInitialised chan struct{} chStop services.StopChan @@ -88,6 +90,20 @@ const ( ) func NewOpStackL1GasOracle(lggr logger.Logger, ethClient l1OracleClient, chainType chaintype.ChainType, daOracle evmconfig.DAOracle) (*optimismL1Oracle, error) { + if daOracle.OracleType() == nil { + return nil, errors.New("OracleType is required but was nil") + } + if *daOracle.OracleType() != toml.DAOracleOPStack { + return nil, fmt.Errorf("expected %s oracle type, got %s", toml.DAOracleOPStack, *daOracle.OracleType()) + } + if daOracle.CustomGasPriceCalldata() != nil && *daOracle.CustomGasPriceCalldata() != "" { + lggr.Warnf("CustomGasPriceCalldata is set but will be ignored for OPStack DA oracle") + } + if daOracle.OracleAddress() == nil || *daOracle.OracleAddress() == "" { + return nil, errors.New("OracleAddress is required but was nil or empty") + } + oracleAddress := *daOracle.OracleAddress() + getL1FeeMethodAbi, err := abi.JSON(strings.NewReader(GetL1FeeAbiString)) if err != nil { return nil, fmt.Errorf("failed to parse L1 gas cost method ABI for chain: %s", chainType) @@ -141,10 +157,10 @@ func NewOpStackL1GasOracle(lggr logger.Logger, ethClient l1OracleClient, chainTy pollPeriod: PollPeriod, logger: logger.Sugared(logger.Named(lggr, fmt.Sprintf("L1GasOracle(%s)", chainType))), - daOracleConfig: daOracle, - isEcotone: false, - isFjord: false, - upgradeCheckTs: time.Time{}, + daOracleAddress: oracleAddress.Address(), + isEcotone: false, + isFjord: false, + upgradeCheckTs: time.Time{}, chInitialised: make(chan struct{}), chStop: make(chan struct{}), @@ -276,6 +292,7 @@ func (o *optimismL1Oracle) checkForUpgrade(ctx context.Context) error { if time.Since(o.upgradeCheckTs) < upgradePollingPeriod { return nil } + o.upgradeCheckTs = time.Now() rpcBatchCalls := []rpc.BatchElem{ { @@ -283,7 +300,7 @@ func (o *optimismL1Oracle) checkForUpgrade(ctx context.Context) error { Args: []any{ map[string]interface{}{ "from": common.Address{}, - "to": o.daOracleConfig.OracleAddress().String(), + "to": o.daOracleAddress.String(), "data": hexutil.Bytes(o.isFjordCalldata), }, "latest", @@ -295,7 +312,7 @@ func (o *optimismL1Oracle) checkForUpgrade(ctx context.Context) error { Args: []any{ map[string]interface{}{ "from": common.Address{}, - "to": o.daOracleConfig.OracleAddress().String(), + "to": o.daOracleAddress.String(), "data": hexutil.Bytes(o.isEcotoneCalldata), }, "latest", @@ -336,9 +353,8 @@ func (o *optimismL1Oracle) checkForUpgrade(ctx context.Context) error { } func (o *optimismL1Oracle) getV1GasPrice(ctx context.Context) (*big.Int, error) { - l1OracleAddress := o.daOracleConfig.OracleAddress().Address() b, err := o.client.CallContract(ctx, ethereum.CallMsg{ - To: &l1OracleAddress, + To: &o.daOracleAddress, Data: o.l1BaseFeeCalldata, }, nil) if err != nil { @@ -360,7 +376,7 @@ func (o *optimismL1Oracle) getEcotoneFjordGasPrice(ctx context.Context) (*big.In Args: []any{ map[string]interface{}{ "from": common.Address{}, - "to": o.daOracleConfig.OracleAddress().String(), + "to": o.daOracleAddress.String(), "data": hexutil.Bytes(o.l1BaseFeeCalldata), }, "latest", @@ -372,7 +388,7 @@ func (o *optimismL1Oracle) getEcotoneFjordGasPrice(ctx context.Context) (*big.In Args: []any{ map[string]interface{}{ "from": common.Address{}, - "to": o.daOracleConfig.OracleAddress().String(), + "to": o.daOracleAddress.String(), "data": hexutil.Bytes(o.baseFeeScalarCalldata), }, "latest", @@ -384,7 +400,7 @@ func (o *optimismL1Oracle) getEcotoneFjordGasPrice(ctx context.Context) (*big.In Args: []any{ map[string]interface{}{ "from": common.Address{}, - "to": o.daOracleConfig.OracleAddress().String(), + "to": o.daOracleAddress.String(), "data": hexutil.Bytes(o.blobBaseFeeCalldata), }, "latest", @@ -396,7 +412,7 @@ func (o *optimismL1Oracle) getEcotoneFjordGasPrice(ctx context.Context) (*big.In Args: []any{ map[string]interface{}{ "from": common.Address{}, - "to": o.daOracleConfig.OracleAddress().String(), + "to": o.daOracleAddress.String(), "data": hexutil.Bytes(o.blobBaseFeeScalarCalldata), }, "latest", @@ -408,7 +424,7 @@ func (o *optimismL1Oracle) getEcotoneFjordGasPrice(ctx context.Context) (*big.In Args: []any{ map[string]interface{}{ "from": common.Address{}, - "to": o.daOracleConfig.OracleAddress().String(), + "to": o.daOracleAddress.String(), "data": hexutil.Bytes(o.decimalsCalldata), }, "latest", diff --git a/core/chains/evm/txmgr/test_helpers.go b/core/chains/evm/txmgr/test_helpers.go index 960d921e879..27f94b28ee7 100644 --- a/core/chains/evm/txmgr/test_helpers.go +++ b/core/chains/evm/txmgr/test_helpers.go @@ -82,7 +82,10 @@ type TestDAOracleConfig struct { evmconfig.DAOracle } -func (d *TestDAOracleConfig) OracleType() toml.DAOracleType { return toml.DAOracleOPStack } +func (d *TestDAOracleConfig) OracleType() *toml.DAOracleType { + oracleType := toml.DAOracleOPStack + return &oracleType +} func (d *TestDAOracleConfig) OracleAddress() *types.EIP55Address { a, err := types.NewEIP55Address("0x420000000000000000000000000000000000000F") if err != nil { @@ -90,7 +93,7 @@ func (d *TestDAOracleConfig) OracleAddress() *types.EIP55Address { } return &a } -func (d *TestDAOracleConfig) CustomGasPriceCalldata() string { return "" } +func (d *TestDAOracleConfig) CustomGasPriceCalldata() *string { return nil } func (g *TestGasEstimatorConfig) BlockHistory() evmconfig.BlockHistory { return &TestBlockHistoryConfig{} diff --git a/core/config/docs/docs_test.go b/core/config/docs/docs_test.go index bb572773c4a..9fca08ee99b 100644 --- a/core/config/docs/docs_test.go +++ b/core/config/docs/docs_test.go @@ -97,8 +97,8 @@ func TestDoc(t *testing.T) { docDefaults.Transactions.AutoPurge.Threshold = nil docDefaults.Transactions.AutoPurge.MinAttempts = nil - // GasEstimator.DAOracle.OracleAddress is only set if DA oracle config is used - docDefaults.GasEstimator.DAOracle.OracleAddress = nil + // Fallback DA oracle is not set + docDefaults.GasEstimator.DAOracle = evmcfg.DAOracle{} assertTOML(t, fallbackDefaults, docDefaults) }) diff --git a/core/services/chainlink/config_test.go b/core/services/chainlink/config_test.go index 4171c7af164..b3533398518 100644 --- a/core/services/chainlink/config_test.go +++ b/core/services/chainlink/config_test.go @@ -1398,9 +1398,16 @@ func TestConfig_full(t *testing.T) { if got.EVM[c].Transactions.AutoPurge.DetectionApiUrl == nil { got.EVM[c].Transactions.AutoPurge.DetectionApiUrl = new(commoncfg.URL) } + if got.EVM[c].GasEstimator.DAOracle.OracleType == nil { + oracleType := evmcfg.DAOracleOPStack + got.EVM[c].GasEstimator.DAOracle.OracleType = &oracleType + } if got.EVM[c].GasEstimator.DAOracle.OracleAddress == nil { got.EVM[c].GasEstimator.DAOracle.OracleAddress = new(types.EIP55Address) } + if got.EVM[c].GasEstimator.DAOracle.CustomGasPriceCalldata == nil { + got.EVM[c].GasEstimator.DAOracle.CustomGasPriceCalldata = new(string) + } } cfgtest.AssertFieldsNotNil(t, got) diff --git a/docs/CONFIG.md b/docs/CONFIG.md index ba36c8d258d..0b52274cff8 100644 --- a/docs/CONFIG.md +++ b/docs/CONFIG.md @@ -2445,7 +2445,6 @@ CacheTimeout = '10s' [GasEstimator.DAOracle] OracleType = 'opstack' OracleAddress = '0x420000000000000000000000000000000000000F' -CustomGasPriceCalldata = '' [HeadTracker] HistoryDepth = 300 @@ -3906,7 +3905,6 @@ CacheTimeout = '10s' [GasEstimator.DAOracle] OracleType = 'opstack' OracleAddress = '0x4200000000000000000000000000000000000005' -CustomGasPriceCalldata = '' [HeadTracker] HistoryDepth = 400 @@ -4014,7 +4012,6 @@ CacheTimeout = '10s' [GasEstimator.DAOracle] OracleType = 'zksync' -CustomGasPriceCalldata = '' [HeadTracker] HistoryDepth = 50 @@ -4330,7 +4327,6 @@ CacheTimeout = '10s' [GasEstimator.DAOracle] OracleType = 'zksync' -CustomGasPriceCalldata = '' [HeadTracker] HistoryDepth = 50 @@ -4438,7 +4434,6 @@ CacheTimeout = '10s' [GasEstimator.DAOracle] OracleType = 'zksync' -CustomGasPriceCalldata = '' [HeadTracker] HistoryDepth = 50 @@ -4548,7 +4543,6 @@ CacheTimeout = '10s' [GasEstimator.DAOracle] OracleType = 'opstack' OracleAddress = '0x420000000000000000000000000000000000000F' -CustomGasPriceCalldata = '' [HeadTracker] HistoryDepth = 300 @@ -5384,7 +5378,6 @@ CacheTimeout = '10s' [GasEstimator.DAOracle] OracleType = 'opstack' OracleAddress = '0x420000000000000000000000000000000000000F' -CustomGasPriceCalldata = '' [HeadTracker] HistoryDepth = 300 @@ -5493,7 +5486,6 @@ CacheTimeout = '10s' [GasEstimator.DAOracle] OracleType = 'opstack' OracleAddress = '0x4200000000000000000000000000000000000005' -CustomGasPriceCalldata = '' [HeadTracker] HistoryDepth = 400 @@ -5913,7 +5905,6 @@ CacheTimeout = '10s' [GasEstimator.DAOracle] OracleType = 'opstack' OracleAddress = '0x420000000000000000000000000000000000000F' -CustomGasPriceCalldata = '' [HeadTracker] HistoryDepth = 300 @@ -6126,7 +6117,6 @@ CacheTimeout = '10s' [GasEstimator.DAOracle] OracleType = 'arbitrum' -CustomGasPriceCalldata = '' [HeadTracker] HistoryDepth = 100 @@ -6235,7 +6225,6 @@ CacheTimeout = '10s' [GasEstimator.DAOracle] OracleType = 'arbitrum' -CustomGasPriceCalldata = '' [HeadTracker] HistoryDepth = 100 @@ -6344,7 +6333,6 @@ CacheTimeout = '10s' [GasEstimator.DAOracle] OracleType = 'arbitrum' -CustomGasPriceCalldata = '' [HeadTracker] HistoryDepth = 100 @@ -6872,7 +6860,6 @@ CacheTimeout = '10s' [GasEstimator.DAOracle] OracleType = 'opstack' OracleAddress = '0x420000000000000000000000000000000000000F' -CustomGasPriceCalldata = '' [HeadTracker] HistoryDepth = 2000 @@ -6984,7 +6971,6 @@ CacheTimeout = '10s' [GasEstimator.DAOracle] OracleType = 'opstack' OracleAddress = '0x420000000000000000000000000000000000000F' -CustomGasPriceCalldata = '' [HeadTracker] HistoryDepth = 2000 @@ -7713,7 +7699,6 @@ CacheTimeout = '10s' [GasEstimator.DAOracle] OracleType = 'opstack' OracleAddress = '0x420000000000000000000000000000000000000F' -CustomGasPriceCalldata = '' [HeadTracker] HistoryDepth = 300 @@ -7822,7 +7807,6 @@ CacheTimeout = '10s' [GasEstimator.DAOracle] OracleType = 'opstack' OracleAddress = '0x420000000000000000000000000000000000000F' -CustomGasPriceCalldata = '' [HeadTracker] HistoryDepth = 300 @@ -7931,7 +7915,6 @@ CacheTimeout = '10s' [GasEstimator.DAOracle] OracleType = 'arbitrum' -CustomGasPriceCalldata = '' [HeadTracker] HistoryDepth = 100 @@ -8040,7 +8023,6 @@ CacheTimeout = '10s' [GasEstimator.DAOracle] OracleType = 'arbitrum' -CustomGasPriceCalldata = '' [HeadTracker] HistoryDepth = 100 @@ -8148,7 +8130,6 @@ CacheTimeout = '10s' [GasEstimator.DAOracle] OracleType = 'arbitrum' -CustomGasPriceCalldata = '' [HeadTracker] HistoryDepth = 100 @@ -8257,7 +8238,6 @@ CacheTimeout = '10s' [GasEstimator.DAOracle] OracleType = 'opstack' OracleAddress = '0x5300000000000000000000000000000000000002' -CustomGasPriceCalldata = '' [HeadTracker] HistoryDepth = 50 @@ -8366,7 +8346,6 @@ CacheTimeout = '10s' [GasEstimator.DAOracle] OracleType = 'opstack' OracleAddress = '0x5300000000000000000000000000000000000002' -CustomGasPriceCalldata = '' [HeadTracker] HistoryDepth = 50 @@ -8579,7 +8558,6 @@ CacheTimeout = '10s' [GasEstimator.DAOracle] OracleType = 'opstack' OracleAddress = '0x420000000000000000000000000000000000000F' -CustomGasPriceCalldata = '' [HeadTracker] HistoryDepth = 300 From 0e855379b9f4ff54944f8ee9918b7bbfc0a67469 Mon Sep 17 00:00:00 2001 From: chainchad <96362174+chainchad@users.noreply.github.com> Date: Wed, 30 Oct 2024 16:43:47 -0400 Subject: [PATCH 5/7] Consume latest changeset and update changelog (#15036) --- .changeset/green-crabs-joke.md | 5 ----- CHANGELOG.md | 2 ++ 2 files changed, 2 insertions(+), 5 deletions(-) delete mode 100644 .changeset/green-crabs-joke.md diff --git a/.changeset/green-crabs-joke.md b/.changeset/green-crabs-joke.md deleted file mode 100644 index 4e9480e9c89..00000000000 --- a/.changeset/green-crabs-joke.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"chainlink": patch ---- - -#bugfix Update DA oracle config struct members to pointers diff --git a/CHANGELOG.md b/CHANGELOG.md index 52b5cd967ce..eb8569f1d33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,7 @@ Reduced Generics: Significantly less bulky code! #updated #changed #internal + - [#14629](https://github.com/smartcontractkit/chainlink/pull/14629) [`4928e60ddf`](https://github.com/smartcontractkit/chainlink/commit/4928e60ddfe375e4a0c644cb210802b4c4db5dbd) Thanks [@huangzhen1997](https://github.com/huangzhen1997)! - Support Zircuit fraud transactions detection and zk overflow detection #added - [#14334](https://github.com/smartcontractkit/chainlink/pull/14334) [`0ad624673f`](https://github.com/smartcontractkit/chainlink/commit/0ad624673f6f1a8e155fc43c67a8ae6caddefa90) Thanks [@krehermann](https://github.com/krehermann)! - #added keystone contract deployment - [#14709](https://github.com/smartcontractkit/chainlink/pull/14709) [`1560aa9167`](https://github.com/smartcontractkit/chainlink/commit/1560aa9167a812abe3a8370c033b3290dcbcb261) Thanks [@KuphJr](https://github.com/KuphJr)! - Add encryptionPublicKey to CapabilitiesRegistry.sol @@ -108,6 +109,7 @@ - [#14668](https://github.com/smartcontractkit/chainlink/pull/14668) [`dacb6a8c70`](https://github.com/smartcontractkit/chainlink/commit/dacb6a8c708e8d7cb94aa63ae7463f58a38d0e59) Thanks [@winder](https://github.com/winder)! - #internal ccip contract reader config. - [#14814](https://github.com/smartcontractkit/chainlink/pull/14814) [`f708ebb094`](https://github.com/smartcontractkit/chainlink/commit/f708ebb094ecd6f4f77e9c480ceacd250fc1fadc) Thanks [@DylanTinianov](https://github.com/DylanTinianov)! - Fix testWSServer issue causing panic in testing #internal - [#14635](https://github.com/smartcontractkit/chainlink/pull/14635) [`ee1d6e3b1a`](https://github.com/smartcontractkit/chainlink/commit/ee1d6e3b1a60dc657a5cab869aac0897e33dc76d) Thanks [@dhaidashenko](https://github.com/dhaidashenko)! - Hedera chain type: broadcast transactions only to a single healthy RPC instead of all healthy RPCs to avoid redundant relay fees. #changed +- [#15031](https://github.com/smartcontractkit/chainlink/pull/15031) [`6951f9e74a`](https://github.com/smartcontractkit/chainlink/commit/6951f9e74ae016fab1548b682d4fd8ed5b818d3c) Thanks [@ogtownsend](https://github.com/ogtownsend)! - #bugfix Update DA oracle config struct members to pointers ## 2.17.0 - 2024-10-10 From fb7d6e88ea3471909a4f9aa29992ec080bba9057 Mon Sep 17 00:00:00 2001 From: chainchad <96362174+chainchad@users.noreply.github.com> Date: Fri, 1 Nov 2024 13:34:15 -0400 Subject: [PATCH 6/7] Finalize date on changelog for 2.18.0 (#15073) Signed-off-by: chainchad <96362174+chainchad@users.noreply.github.com> --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb8569f1d33..f4326bd705d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog Chainlink Core -## 2.18.0 - UNRELEASED +## 2.18.0 - 2024-11-01 ### Minor Changes From 37f1edfa775e67ca3ab726fff3148f6c3b77cc0c Mon Sep 17 00:00:00 2001 From: Dimitris Date: Fri, 29 Nov 2024 16:55:18 +0200 Subject: [PATCH 7/7] Txmv2_from_2.18 --- .mockery.yaml | 6 + common/txmgr/txmgr.go | 33 +- common/txmgr/types/tx.go | 4 + .../ocrimpls/contract_transmitter_test.go | 3 +- core/chains/evm/config/chain_scoped.go | 4 + core/chains/evm/config/chain_scoped_txmv2.go | 25 + core/chains/evm/config/chaintype/chaintype.go | 6 +- core/chains/evm/config/config.go | 7 + core/chains/evm/config/toml/config.go | 27 +- core/chains/evm/config/toml/defaults.go | 1 + .../evm/config/toml/defaults/fallback.toml | 3 + core/chains/evm/keystore/eth.go | 1 + core/chains/evm/keystore/mocks/eth.go | 60 ++ core/chains/evm/txm/attempt_builder.go | 154 +++++ .../evm/txm/clientwrappers/chain_client.go | 31 + .../clientwrappers/dual_broadcast_client.go | 130 ++++ .../evm/txm/clientwrappers/geth_client.go | 51 ++ core/chains/evm/txm/dummy_keystore.go | 56 ++ core/chains/evm/txm/mocks/attempt_builder.go | 161 +++++ core/chains/evm/txm/mocks/client.go | 204 ++++++ core/chains/evm/txm/mocks/keystore.go | 98 +++ core/chains/evm/txm/mocks/tx_store.go | 647 ++++++++++++++++++ core/chains/evm/txm/orchestrator.go | 365 ++++++++++ core/chains/evm/txm/storage/inmemory_store.go | 324 +++++++++ .../evm/txm/storage/inmemory_store_manager.go | 135 ++++ .../storage/inmemory_store_manager_test.go | 35 + .../evm/txm/storage/inmemory_store_test.go | 476 +++++++++++++ core/chains/evm/txm/stuck_tx_detector.go | 115 ++++ core/chains/evm/txm/txm.go | 395 +++++++++++ core/chains/evm/txm/txm_test.go | 230 +++++++ core/chains/evm/txm/types/transaction.go | 170 +++++ core/chains/evm/txmgr/builder.go | 59 +- core/chains/evm/txmgr/evm_tx_store_test.go | 2 +- core/chains/evm/txmgr/txmgr_test.go | 3 +- core/chains/legacyevm/evm_txm.go | 22 +- core/config/docs/chains-evm.toml | 8 + core/config/docs/docs_test.go | 4 + core/services/chainlink/config_test.go | 16 +- .../chainlink/testdata/config-full.toml | 3 + .../chainlink/testdata/config-invalid.toml | 6 + .../config-multi-chain-effective.toml | 9 + .../headreporter/prometheus_reporter_test.go | 3 +- core/services/keystore/eth.go | 22 + core/services/keystore/eth_test.go | 27 + core/services/keystore/mocks/eth.go | 60 ++ core/services/vrf/delegate_test.go | 2 +- core/services/vrf/v2/integration_v2_test.go | 2 +- core/services/vrf/v2/listener_v2_test.go | 2 +- core/web/resolver/testdata/config-full.toml | 3 + .../config-multi-chain-effective.toml | 9 + docs/CONFIG.md | 222 ++++++ .../node/validate/defaults-override.txtar | 3 + .../disk-based-logging-disabled.txtar | 3 + .../validate/disk-based-logging-no-dir.txtar | 3 + .../node/validate/disk-based-logging.txtar | 3 + testdata/scripts/node/validate/invalid.txtar | 3 + testdata/scripts/node/validate/valid.txtar | 3 + 57 files changed, 4443 insertions(+), 16 deletions(-) create mode 100644 core/chains/evm/config/chain_scoped_txmv2.go create mode 100644 core/chains/evm/txm/attempt_builder.go create mode 100644 core/chains/evm/txm/clientwrappers/chain_client.go create mode 100644 core/chains/evm/txm/clientwrappers/dual_broadcast_client.go create mode 100644 core/chains/evm/txm/clientwrappers/geth_client.go create mode 100644 core/chains/evm/txm/dummy_keystore.go create mode 100644 core/chains/evm/txm/mocks/attempt_builder.go create mode 100644 core/chains/evm/txm/mocks/client.go create mode 100644 core/chains/evm/txm/mocks/keystore.go create mode 100644 core/chains/evm/txm/mocks/tx_store.go create mode 100644 core/chains/evm/txm/orchestrator.go create mode 100644 core/chains/evm/txm/storage/inmemory_store.go create mode 100644 core/chains/evm/txm/storage/inmemory_store_manager.go create mode 100644 core/chains/evm/txm/storage/inmemory_store_manager_test.go create mode 100644 core/chains/evm/txm/storage/inmemory_store_test.go create mode 100644 core/chains/evm/txm/stuck_tx_detector.go create mode 100644 core/chains/evm/txm/txm.go create mode 100644 core/chains/evm/txm/txm_test.go create mode 100644 core/chains/evm/txm/types/transaction.go diff --git a/.mockery.yaml b/.mockery.yaml index 711d70f59e9..c4ee5dd5a33 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -93,6 +93,12 @@ packages: BalanceMonitor: config: dir: "{{ .InterfaceDir }}/../mocks" + github.com/smartcontractkit/chainlink/v2/core/chains/evm/txm: + interfaces: + Client: + TxStore: + AttemptBuilder: + Keystore: github.com/smartcontractkit/chainlink/v2/core/chains/evm/txmgr: interfaces: ChainConfig: diff --git a/common/txmgr/txmgr.go b/common/txmgr/txmgr.go index 28d505e5e05..608d838d851 100644 --- a/common/txmgr/txmgr.go +++ b/common/txmgr/txmgr.go @@ -65,6 +65,19 @@ type TxManager[ GetTransactionStatus(ctx context.Context, transactionID string) (state commontypes.TransactionStatus, err error) } +type TxmV2Wrapper[ + CHAIN_ID types.ID, + HEAD types.Head[BLOCK_HASH], + ADDR types.Hashable, + TX_HASH types.Hashable, + BLOCK_HASH types.Hashable, + SEQ types.Sequence, + FEE feetypes.Fee, +] interface { + services.Service + CreateTransaction(ctx context.Context, txRequest txmgrtypes.TxRequest[ADDR, TX_HASH]) (etx txmgrtypes.Tx[CHAIN_ID, ADDR, TX_HASH, BLOCK_HASH, SEQ, FEE], err error) +} + type reset struct { // f is the function to execute between stopping/starting the // Broadcaster and Confirmer @@ -112,6 +125,7 @@ type Txm[ fwdMgr txmgrtypes.ForwarderManager[ADDR] txAttemptBuilder txmgrtypes.TxAttemptBuilder[CHAIN_ID, HEAD, ADDR, TX_HASH, BLOCK_HASH, SEQ, FEE] newErrorClassifier NewErrorClassifier + txmv2wrapper TxmV2Wrapper[CHAIN_ID, HEAD, ADDR, TX_HASH, BLOCK_HASH, SEQ, FEE] } func (b *Txm[CHAIN_ID, HEAD, ADDR, TX_HASH, BLOCK_HASH, R, SEQ, FEE]) RegisterResumeCallback(fn ResumeCallback) { @@ -146,6 +160,7 @@ func NewTxm[ tracker *Tracker[CHAIN_ID, ADDR, TX_HASH, BLOCK_HASH, R, SEQ, FEE], finalizer txmgrtypes.Finalizer[BLOCK_HASH, HEAD], newErrorClassifierFunc NewErrorClassifier, + txmv2wrapper TxmV2Wrapper[CHAIN_ID, HEAD, ADDR, TX_HASH, BLOCK_HASH, SEQ, FEE], ) *Txm[CHAIN_ID, HEAD, ADDR, TX_HASH, BLOCK_HASH, R, SEQ, FEE] { b := Txm[CHAIN_ID, HEAD, ADDR, TX_HASH, BLOCK_HASH, R, SEQ, FEE]{ logger: logger.Sugared(lggr), @@ -168,6 +183,7 @@ func NewTxm[ tracker: tracker, newErrorClassifier: newErrorClassifierFunc, finalizer: finalizer, + txmv2wrapper: txmv2wrapper, } if txCfg.ResendAfterThreshold() <= 0 { @@ -206,6 +222,12 @@ func (b *Txm[CHAIN_ID, HEAD, ADDR, TX_HASH, BLOCK_HASH, R, SEQ, FEE]) Start(ctx return fmt.Errorf("Txm: Finalizer failed to start: %w", err) } + if b.txmv2wrapper != nil { + if err := ms.Start(ctx, b.txmv2wrapper); err != nil { + return fmt.Errorf("Txm: Txmv2 failed to start: %w", err) + } + } + b.logger.Info("Txm starting runLoop") b.wg.Add(1) go b.runLoop() @@ -459,6 +481,12 @@ func (b *Txm[CHAIN_ID, HEAD, ADDR, TX_HASH, BLOCK_HASH, R, SEQ, FEE]) runLoop() if err != nil && (!errors.Is(err, services.ErrAlreadyStopped) || !errors.Is(err, services.ErrCannotStopUnstarted)) { b.logger.Errorw(fmt.Sprintf("Failed to Close Finalizer: %v", err), "err", err) } + if b.txmv2wrapper != nil { + err = b.txmv2wrapper.Close() + if err != nil && (!errors.Is(err, services.ErrAlreadyStopped) || !errors.Is(err, services.ErrCannotStopUnstarted)) { + b.logger.Errorw(fmt.Sprintf("Failed to Close Finalizer: %v", err), "err", err) + } + } return case <-keysChanged: // This check prevents the weird edge-case where you can select @@ -512,11 +540,14 @@ func (b *Txm[CHAIN_ID, HEAD, ADDR, TX_HASH, BLOCK_HASH, R, SEQ, FEE]) Trigger(ad func (b *Txm[CHAIN_ID, HEAD, ADDR, TX_HASH, BLOCK_HASH, R, SEQ, FEE]) CreateTransaction(ctx context.Context, txRequest txmgrtypes.TxRequest[ADDR, TX_HASH]) (tx txmgrtypes.Tx[CHAIN_ID, ADDR, TX_HASH, BLOCK_HASH, SEQ, FEE], err error) { // Check for existing Tx with IdempotencyKey. If found, return the Tx and do nothing // Skipping CreateTransaction to avoid double send + if b.txmv2wrapper != nil && txRequest.Meta != nil && txRequest.Meta.DualBroadcast != nil && *txRequest.Meta.DualBroadcast { + return b.txmv2wrapper.CreateTransaction(ctx, txRequest) + } if txRequest.IdempotencyKey != nil { var existingTx *txmgrtypes.Tx[CHAIN_ID, ADDR, TX_HASH, BLOCK_HASH, SEQ, FEE] existingTx, err = b.txStore.FindTxWithIdempotencyKey(ctx, *txRequest.IdempotencyKey, b.chainID) if err != nil { - return tx, fmt.Errorf("Failed to search for transaction with IdempotencyKey: %w", err) + return tx, fmt.Errorf("failed to search for transaction with IdempotencyKey: %w", err) } if existingTx != nil { b.logger.Infow("Found a Tx with IdempotencyKey. Returning existing Tx without creating a new one.", "IdempotencyKey", *txRequest.IdempotencyKey) diff --git a/common/txmgr/types/tx.go b/common/txmgr/types/tx.go index b65f7edf6e5..f04047a36c1 100644 --- a/common/txmgr/types/tx.go +++ b/common/txmgr/types/tx.go @@ -159,6 +159,10 @@ type TxMeta[ADDR types.Hashable, TX_HASH types.Hashable] struct { MessageIDs []string `json:"MessageIDs,omitempty"` // SeqNumbers is used by CCIP for tx to committed sequence numbers correlation in logs SeqNumbers []uint64 `json:"SeqNumbers,omitempty"` + + // Dual Broadcast + DualBroadcast *bool `json:"DualBroadcast,omitempty"` + DualBroadcastParams *string `json:"DualBroadcastParams,omitempty"` } type TxAttempt[ diff --git a/core/capabilities/ccip/ocrimpls/contract_transmitter_test.go b/core/capabilities/ccip/ocrimpls/contract_transmitter_test.go index fa40dca6b85..40c243a5774 100644 --- a/core/capabilities/ccip/ocrimpls/contract_transmitter_test.go +++ b/core/capabilities/ccip/ocrimpls/contract_transmitter_test.go @@ -478,7 +478,8 @@ func makeTestEvmTxm( lp, keyStore, estimator, - ht) + ht, + nil) require.NoError(t, err, "can't create tx manager") _, unsub := broadcaster.Subscribe(txm) diff --git a/core/chains/evm/config/chain_scoped.go b/core/chains/evm/config/chain_scoped.go index de89272b5e2..3a7ff43d8a6 100644 --- a/core/chains/evm/config/chain_scoped.go +++ b/core/chains/evm/config/chain_scoped.go @@ -52,6 +52,10 @@ func (e *EVMConfig) BalanceMonitor() BalanceMonitor { return &balanceMonitorConfig{c: e.C.BalanceMonitor} } +func (e *EVMConfig) TxmV2() TxmV2 { + return &txmv2Config{c: e.C.TxmV2} +} + func (e *EVMConfig) Transactions() Transactions { return &transactionsConfig{c: e.C.Transactions} } diff --git a/core/chains/evm/config/chain_scoped_txmv2.go b/core/chains/evm/config/chain_scoped_txmv2.go new file mode 100644 index 00000000000..e50148cfae4 --- /dev/null +++ b/core/chains/evm/config/chain_scoped_txmv2.go @@ -0,0 +1,25 @@ +package config + +import ( + "net/url" + "time" + + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/config/toml" +) + +type txmv2Config struct { + c toml.TxmV2 +} + +func (t *txmv2Config) Enabled() bool { + return *t.c.Enabled +} + +func (t *txmv2Config) BlockTime() *time.Duration { + d := t.c.BlockTime.Duration() + return &d +} + +func (t *txmv2Config) CustomURL() *url.URL { + return t.c.CustomURL.URL() +} diff --git a/core/chains/evm/config/chaintype/chaintype.go b/core/chains/evm/config/chaintype/chaintype.go index b2eff02834b..bc2eace8ca3 100644 --- a/core/chains/evm/config/chaintype/chaintype.go +++ b/core/chains/evm/config/chaintype/chaintype.go @@ -23,6 +23,7 @@ const ( ChainZkEvm ChainType = "zkevm" ChainZkSync ChainType = "zksync" ChainZircuit ChainType = "zircuit" + ChainDualBroadcast ChainType = "dualBroadcast" ) // IsL2 returns true if this chain is a Layer 2 chain. Notably: @@ -39,7 +40,7 @@ func (c ChainType) IsL2() bool { func (c ChainType) IsValid() bool { switch c { - case "", ChainArbitrum, ChainAstar, ChainCelo, ChainGnosis, ChainHedera, ChainKroma, ChainMantle, ChainMetis, ChainOptimismBedrock, ChainScroll, ChainWeMix, ChainXLayer, ChainZkEvm, ChainZkSync, ChainZircuit: + case "", ChainArbitrum, ChainAstar, ChainCelo, ChainGnosis, ChainHedera, ChainKroma, ChainMantle, ChainMetis, ChainOptimismBedrock, ChainScroll, ChainWeMix, ChainXLayer, ChainZkEvm, ChainZkSync, ChainZircuit, ChainDualBroadcast: return true } return false @@ -77,6 +78,8 @@ func FromSlug(slug string) ChainType { return ChainZkSync case "zircuit": return ChainZircuit + case "dualBroadcast": + return ChainDualBroadcast default: return ChainType(slug) } @@ -144,4 +147,5 @@ var ErrInvalid = fmt.Errorf("must be one of %s or omitted", strings.Join([]strin string(ChainZkEvm), string(ChainZkSync), string(ChainZircuit), + string(ChainDualBroadcast), }, ", ")) diff --git a/core/chains/evm/config/config.go b/core/chains/evm/config/config.go index f2a571f94b0..be61bfe02ee 100644 --- a/core/chains/evm/config/config.go +++ b/core/chains/evm/config/config.go @@ -18,6 +18,7 @@ import ( type EVM interface { HeadTracker() HeadTracker BalanceMonitor() BalanceMonitor + TxmV2() TxmV2 Transactions() Transactions GasEstimator() GasEstimator OCR() OCR @@ -102,6 +103,12 @@ type ClientErrors interface { TooManyResults() string } +type TxmV2 interface { + Enabled() bool + BlockTime() *time.Duration + CustomURL() *url.URL +} + type Transactions interface { ForwardersEnabled() bool ReaperInterval() time.Duration diff --git a/core/chains/evm/config/toml/config.go b/core/chains/evm/config/toml/config.go index 4a1f73094d5..b899ee771c0 100644 --- a/core/chains/evm/config/toml/config.go +++ b/core/chains/evm/config/toml/config.go @@ -300,8 +300,10 @@ func (c *EVMConfig) ValidateConfig() (err error) { is := c.ChainType.ChainType() if is != must { if must == "" { - err = multierr.Append(err, commonconfig.ErrInvalid{Name: "ChainType", Value: c.ChainType.ChainType(), - Msg: "must not be set with this chain id"}) + if c.ChainType.ChainType() != chaintype.ChainDualBroadcast { + err = multierr.Append(err, commonconfig.ErrInvalid{Name: "ChainType", Value: c.ChainType.ChainType(), + Msg: "must not be set with this chain id"}) + } } else { err = multierr.Append(err, commonconfig.ErrInvalid{Name: "ChainType", Value: c.ChainType.ChainType(), Msg: fmt.Sprintf("only %q can be used with this chain id", must)}) @@ -376,6 +378,7 @@ type Chain struct { FinalizedBlockOffset *uint32 NoNewFinalizedHeadsThreshold *commonconfig.Duration + TxmV2 TxmV2 `toml:",omitempty"` Transactions Transactions `toml:",omitempty"` BalanceMonitor BalanceMonitor `toml:",omitempty"` GasEstimator GasEstimator `toml:",omitempty"` @@ -460,6 +463,26 @@ func (c *Chain) ValidateConfig() (err error) { return } +type TxmV2 struct { + Enabled *bool `toml:",omitempty"` + BlockTime *commonconfig.Duration `toml:",omitempty"` + CustomURL *commonconfig.URL `toml:",omitempty"` +} + +func (t *TxmV2) setFrom(f *TxmV2) { + if v := f.Enabled; v != nil { + t.Enabled = f.Enabled + } + + if v := f.BlockTime; v != nil { + t.BlockTime = f.BlockTime + } + + if v := f.CustomURL; v != nil { + t.CustomURL = f.CustomURL + } +} + type Transactions struct { ForwardersEnabled *bool MaxInFlight *uint32 diff --git a/core/chains/evm/config/toml/defaults.go b/core/chains/evm/config/toml/defaults.go index 0885d94e6df..5ce014921f4 100644 --- a/core/chains/evm/config/toml/defaults.go +++ b/core/chains/evm/config/toml/defaults.go @@ -241,6 +241,7 @@ func (c *Chain) SetFrom(f *Chain) { c.NoNewFinalizedHeadsThreshold = v } + c.TxmV2.setFrom(&f.TxmV2) c.Transactions.setFrom(&f.Transactions) c.BalanceMonitor.setFrom(&f.BalanceMonitor) c.GasEstimator.setFrom(&f.GasEstimator) diff --git a/core/chains/evm/config/toml/defaults/fallback.toml b/core/chains/evm/config/toml/defaults/fallback.toml index 1e18f4a4ebf..f5189670991 100644 --- a/core/chains/evm/config/toml/defaults/fallback.toml +++ b/core/chains/evm/config/toml/defaults/fallback.toml @@ -18,6 +18,9 @@ FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '0' LogBroadcasterEnabled = true +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 diff --git a/core/chains/evm/keystore/eth.go b/core/chains/evm/keystore/eth.go index ff71e0a4f18..9c0986d9c3d 100644 --- a/core/chains/evm/keystore/eth.go +++ b/core/chains/evm/keystore/eth.go @@ -13,5 +13,6 @@ type Eth interface { CheckEnabled(ctx context.Context, address common.Address, chainID *big.Int) error EnabledAddressesForChain(ctx context.Context, chainID *big.Int) (addresses []common.Address, err error) SignTx(ctx context.Context, fromAddress common.Address, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) + SignMessage(ctx context.Context, address common.Address, message []byte) ([]byte, error) SubscribeToKeyChanges(ctx context.Context) (ch chan struct{}, unsub func()) } diff --git a/core/chains/evm/keystore/mocks/eth.go b/core/chains/evm/keystore/mocks/eth.go index d0563702b7b..021ff938787 100644 --- a/core/chains/evm/keystore/mocks/eth.go +++ b/core/chains/evm/keystore/mocks/eth.go @@ -133,6 +133,66 @@ func (_c *Eth_EnabledAddressesForChain_Call) RunAndReturn(run func(context.Conte return _c } +// SignMessage provides a mock function with given fields: ctx, address, message +func (_m *Eth) SignMessage(ctx context.Context, address common.Address, message []byte) ([]byte, error) { + ret := _m.Called(ctx, address, message) + + if len(ret) == 0 { + panic("no return value specified for SignMessage") + } + + var r0 []byte + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, common.Address, []byte) ([]byte, error)); ok { + return rf(ctx, address, message) + } + if rf, ok := ret.Get(0).(func(context.Context, common.Address, []byte) []byte); ok { + r0 = rf(ctx, address, message) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]byte) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, common.Address, []byte) error); ok { + r1 = rf(ctx, address, message) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Eth_SignMessage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SignMessage' +type Eth_SignMessage_Call struct { + *mock.Call +} + +// SignMessage is a helper method to define mock.On call +// - ctx context.Context +// - address common.Address +// - message []byte +func (_e *Eth_Expecter) SignMessage(ctx interface{}, address interface{}, message interface{}) *Eth_SignMessage_Call { + return &Eth_SignMessage_Call{Call: _e.mock.On("SignMessage", ctx, address, message)} +} + +func (_c *Eth_SignMessage_Call) Run(run func(ctx context.Context, address common.Address, message []byte)) *Eth_SignMessage_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(common.Address), args[2].([]byte)) + }) + return _c +} + +func (_c *Eth_SignMessage_Call) Return(_a0 []byte, _a1 error) *Eth_SignMessage_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *Eth_SignMessage_Call) RunAndReturn(run func(context.Context, common.Address, []byte) ([]byte, error)) *Eth_SignMessage_Call { + _c.Call.Return(run) + return _c +} + // SignTx provides a mock function with given fields: ctx, fromAddress, tx, chainID func (_m *Eth) SignTx(ctx context.Context, fromAddress common.Address, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { ret := _m.Called(ctx, fromAddress, tx, chainID) diff --git a/core/chains/evm/txm/attempt_builder.go b/core/chains/evm/txm/attempt_builder.go new file mode 100644 index 00000000000..ceae96f1b28 --- /dev/null +++ b/core/chains/evm/txm/attempt_builder.go @@ -0,0 +1,154 @@ +package txm + +import ( + "context" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/common" + evmtypes "github.com/ethereum/go-ethereum/core/types" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/txm/types" +) + +type AttemptBuilderKeystore interface { + SignTx(ctx context.Context, fromAddress common.Address, tx *evmtypes.Transaction, chainID *big.Int) (*evmtypes.Transaction, error) +} + +type attemptBuilder struct { + chainID *big.Int + priceMax *assets.Wei + gas.EvmFeeEstimator + keystore AttemptBuilderKeystore +} + +func NewAttemptBuilder(chainID *big.Int, priceMax *assets.Wei, estimator gas.EvmFeeEstimator, keystore AttemptBuilderKeystore) *attemptBuilder { + return &attemptBuilder{ + chainID: chainID, + priceMax: priceMax, + EvmFeeEstimator: estimator, + keystore: keystore, + } +} + +func (a *attemptBuilder) NewAttempt(ctx context.Context, lggr logger.Logger, tx *types.Transaction, dynamic bool) (*types.Attempt, error) { + fee, estimatedGasLimit, err := a.EvmFeeEstimator.GetFee(ctx, tx.Data, tx.SpecifiedGasLimit, a.priceMax, &tx.FromAddress, &tx.ToAddress) + if err != nil { + return nil, err + } + txType := evmtypes.LegacyTxType + if dynamic { + txType = evmtypes.DynamicFeeTxType + } + return a.newCustomAttempt(ctx, tx, fee, estimatedGasLimit, byte(txType), lggr) +} + +func (a *attemptBuilder) NewBumpAttempt(ctx context.Context, lggr logger.Logger, tx *types.Transaction, previousAttempt types.Attempt) (*types.Attempt, error) { + bumpedFee, bumpedFeeLimit, err := a.EvmFeeEstimator.BumpFee(ctx, previousAttempt.Fee, tx.SpecifiedGasLimit, a.priceMax, nil) + if err != nil { + return nil, err + } + return a.newCustomAttempt(ctx, tx, bumpedFee, bumpedFeeLimit, previousAttempt.Type, lggr) +} + +func (a *attemptBuilder) newCustomAttempt( + ctx context.Context, + tx *types.Transaction, + fee gas.EvmFee, + estimatedGasLimit uint64, + txType byte, + lggr logger.Logger, +) (attempt *types.Attempt, err error) { + switch txType { + case 0x0: + if fee.GasPrice == nil { + err = fmt.Errorf("tried to create attempt of type %v for txID: %v but estimator did not return legacy fee", txType, tx.ID) + logger.Sugared(lggr).AssumptionViolation(err.Error()) + return + } + return a.newLegacyAttempt(ctx, tx, fee.GasPrice, estimatedGasLimit) + case 0x2: + if !fee.ValidDynamic() { + err = fmt.Errorf("tried to create attempt of type %v for txID: %v but estimator did not return dynamic fee", txType, tx.ID) + logger.Sugared(lggr).AssumptionViolation(err.Error()) + return + } + return a.newDynamicFeeAttempt(ctx, tx, fee.DynamicFee, estimatedGasLimit) + default: + return nil, fmt.Errorf("cannot build attempt, unrecognized transaction type: %v", txType) + } +} + +func (a *attemptBuilder) newLegacyAttempt(ctx context.Context, tx *types.Transaction, gasPrice *assets.Wei, estimatedGasLimit uint64) (*types.Attempt, error) { + var data []byte + var toAddress common.Address + value := big.NewInt(0) + if !tx.IsPurgeable { + data = tx.Data + toAddress = tx.ToAddress + value = tx.Value + } + legacyTx := evmtypes.LegacyTx{ + Nonce: tx.Nonce, + To: &toAddress, + Value: value, + Gas: estimatedGasLimit, + GasPrice: gasPrice.ToInt(), + Data: data, + } + + signedTx, err := a.keystore.SignTx(ctx, tx.FromAddress, evmtypes.NewTx(&legacyTx), a.chainID) + if err != nil { + return nil, fmt.Errorf("failed to sign attempt for txID: %v, err: %w", tx.ID, err) + } + + attempt := &types.Attempt{ + TxID: tx.ID, + Fee: gas.EvmFee{GasPrice: gasPrice}, + Hash: signedTx.Hash(), + GasLimit: estimatedGasLimit, + SignedTransaction: signedTx, + } + + return attempt, nil +} + +func (a *attemptBuilder) newDynamicFeeAttempt(ctx context.Context, tx *types.Transaction, dynamicFee gas.DynamicFee, estimatedGasLimit uint64) (*types.Attempt, error) { + var data []byte + var toAddress common.Address + value := big.NewInt(0) + if !tx.IsPurgeable { + data = tx.Data + toAddress = tx.ToAddress + value = tx.Value + } + dynamicTx := evmtypes.DynamicFeeTx{ + Nonce: tx.Nonce, + To: &toAddress, + Value: value, + Gas: estimatedGasLimit, + GasFeeCap: dynamicFee.GasFeeCap.ToInt(), + GasTipCap: dynamicFee.GasTipCap.ToInt(), + Data: data, + } + + signedTx, err := a.keystore.SignTx(ctx, tx.FromAddress, evmtypes.NewTx(&dynamicTx), a.chainID) + if err != nil { + return nil, fmt.Errorf("failed to sign attempt for txID: %v, err: %w", tx.ID, err) + } + + attempt := &types.Attempt{ + TxID: tx.ID, + Fee: gas.EvmFee{DynamicFee: gas.DynamicFee{GasFeeCap: dynamicFee.GasFeeCap, GasTipCap: dynamicFee.GasTipCap}}, + Hash: signedTx.Hash(), + GasLimit: estimatedGasLimit, + Type: evmtypes.DynamicFeeTxType, + SignedTransaction: signedTx, + } + + return attempt, nil +} diff --git a/core/chains/evm/txm/clientwrappers/chain_client.go b/core/chains/evm/txm/clientwrappers/chain_client.go new file mode 100644 index 00000000000..7638cc53443 --- /dev/null +++ b/core/chains/evm/txm/clientwrappers/chain_client.go @@ -0,0 +1,31 @@ +package clientwrappers + +import ( + "context" + "math/big" + + "github.com/ethereum/go-ethereum/common" + + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/txm/types" +) + +type ChainClient struct { + c client.Client +} + +func NewChainClient(client client.Client) *ChainClient { + return &ChainClient{c: client} +} + +func (c *ChainClient) NonceAt(ctx context.Context, address common.Address, blockNumber *big.Int) (uint64, error) { + return c.c.NonceAt(ctx, address, blockNumber) +} + +func (c *ChainClient) PendingNonceAt(ctx context.Context, address common.Address) (uint64, error) { + return c.c.PendingNonceAt(ctx, address) +} + +func (c *ChainClient) SendTransaction(ctx context.Context, _ *types.Transaction, attempt *types.Attempt) error { + return c.c.SendTransaction(ctx, attempt.SignedTransaction) +} diff --git a/core/chains/evm/txm/clientwrappers/dual_broadcast_client.go b/core/chains/evm/txm/clientwrappers/dual_broadcast_client.go new file mode 100644 index 00000000000..0bbd2530765 --- /dev/null +++ b/core/chains/evm/txm/clientwrappers/dual_broadcast_client.go @@ -0,0 +1,130 @@ +package clientwrappers + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "math/big" + "net/http" + "net/url" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/crypto" + + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/txm/types" +) + +type DualBroadcastClientKeystore interface { + SignMessage(ctx context.Context, address common.Address, message []byte) ([]byte, error) +} + +type DualBroadcastClient struct { + c client.Client + keystore DualBroadcastClientKeystore + customURL *url.URL +} + +func NewDualBroadcastClient(c client.Client, keystore DualBroadcastClientKeystore, customURL *url.URL) *DualBroadcastClient { + return &DualBroadcastClient{ + c: c, + keystore: keystore, + customURL: customURL, + } +} + +func (d *DualBroadcastClient) NonceAt(ctx context.Context, address common.Address, blockNumber *big.Int) (uint64, error) { + return d.c.NonceAt(ctx, address, blockNumber) +} + +func (d *DualBroadcastClient) PendingNonceAt(ctx context.Context, address common.Address) (uint64, error) { + body := []byte(fmt.Sprintf(`{"jsonrpc":"2.0","method":"eth_getTransactionCount","params":["%s","pending"]}`, address.String())) + response, err := d.signAndPostMessage(ctx, address, body, "") + if err != nil { + return 0, err + } + + nonce, err := hexutil.DecodeUint64(response) + if err != nil { + return 0, fmt.Errorf("failed to decode response %v into uint64: %w", response, err) + } + return nonce, nil +} + +func (d *DualBroadcastClient) SendTransaction(ctx context.Context, tx *types.Transaction, attempt *types.Attempt) error { + meta, err := tx.GetMeta() + if err != nil { + return err + } + //nolint:revive //linter nonsense + if meta != nil && meta.DualBroadcast != nil && *meta.DualBroadcast && !tx.IsPurgeable { + data, err := attempt.SignedTransaction.MarshalBinary() + if err != nil { + return err + } + params := "" + if meta.DualBroadcastParams != nil { + params = *meta.DualBroadcastParams + } + body := []byte(fmt.Sprintf(`{"jsonrpc":"2.0","method":"eth_sendRawTransaction","params":["%s"]}`, hexutil.Encode(data))) + if _, err = d.signAndPostMessage(ctx, tx.FromAddress, body, params); err != nil { + return err + } + return nil + } else { + return d.c.SendTransaction(ctx, attempt.SignedTransaction) + } +} + +func (d *DualBroadcastClient) signAndPostMessage(ctx context.Context, address common.Address, body []byte, urlParams string) (result string, err error) { + bodyReader := bytes.NewReader(body) + postReq, err := http.NewRequestWithContext(ctx, http.MethodPost, d.customURL.String()+"?"+urlParams, bodyReader) + if err != nil { + return + } + + hashedBody := crypto.Keccak256Hash(body).Hex() + signedMessage, err := d.keystore.SignMessage(ctx, address, []byte(hashedBody)) + if err != nil { + return + } + + postReq.Header.Add("X-Flashbots-signature", address.String()+":"+hexutil.Encode(signedMessage)) + postReq.Header.Add("Content-Type", "application/json") + + resp, err := http.DefaultClient.Do(postReq) + if err != nil { + return result, fmt.Errorf("request %v failed: %w", postReq, err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return result, fmt.Errorf("request %v failed with status: %d", postReq, resp.StatusCode) + } + + keyJSON, err := io.ReadAll(resp.Body) + if err != nil { + return + } + var response postResponse + err = json.Unmarshal(keyJSON, &response) + if err != nil { + return result, fmt.Errorf("failed to unmarshal response into struct: %w: %s", err, string(keyJSON)) + } + if response.Error.Message != "" { + return result, errors.New(response.Error.Message) + } + return response.Result, nil +} + +type postResponse struct { + Result string `json:"result,omitempty"` + Error postError +} + +type postError struct { + Message string `json:"message,omitempty"` +} diff --git a/core/chains/evm/txm/clientwrappers/geth_client.go b/core/chains/evm/txm/clientwrappers/geth_client.go new file mode 100644 index 00000000000..d97e5cfae35 --- /dev/null +++ b/core/chains/evm/txm/clientwrappers/geth_client.go @@ -0,0 +1,51 @@ +package clientwrappers + +import ( + "context" + "math/big" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/rpc" + + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/txm/types" + evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" +) + +type GethClient struct { + *ethclient.Client +} + +func NewGethClient(client *ethclient.Client) *GethClient { + return &GethClient{ + Client: client, + } +} + +func (g *GethClient) BatchCallContext(ctx context.Context, b []rpc.BatchElem) error { + return g.Client.Client().BatchCallContext(ctx, b) +} + +func (g *GethClient) CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error { + return g.Client.Client().CallContext(ctx, result, method, args...) +} + +func (g *GethClient) CallContract(ctx context.Context, message ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) { + var hex hexutil.Bytes + err := g.CallContext(ctx, &hex, "eth_call", client.ToBackwardCompatibleCallArg(message), client.ToBackwardCompatibleBlockNumArg(blockNumber)) + return hex, err +} + +func (g *GethClient) HeadByNumber(ctx context.Context, number *big.Int) (*evmtypes.Head, error) { + hexNumber := client.ToBlockNumArg(number) + args := []interface{}{hexNumber, false} + head := new(evmtypes.Head) + err := g.CallContext(ctx, head, "eth_getBlockByNumber", args...) + return head, err +} + +func (g *GethClient) SendTransaction(ctx context.Context, _ *types.Transaction, attempt *types.Attempt) error { + return g.Client.SendTransaction(ctx, attempt.SignedTransaction) +} diff --git a/core/chains/evm/txm/dummy_keystore.go b/core/chains/evm/txm/dummy_keystore.go new file mode 100644 index 00000000000..e17d1645acb --- /dev/null +++ b/core/chains/evm/txm/dummy_keystore.go @@ -0,0 +1,56 @@ +package txm + +import ( + "context" + "crypto/ecdsa" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" +) + +type DummyKeystore struct { + privateKeyMap map[common.Address]*ecdsa.PrivateKey +} + +func NewKeystore() *DummyKeystore { + return &DummyKeystore{privateKeyMap: make(map[common.Address]*ecdsa.PrivateKey)} +} + +func (k *DummyKeystore) Add(privateKeyString string, address common.Address) error { + privateKey, err := crypto.HexToECDSA(privateKeyString) + if err != nil { + return err + } + k.privateKeyMap[address] = privateKey + return nil +} + +func (k *DummyKeystore) SignTx(_ context.Context, fromAddress common.Address, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { + if key, exists := k.privateKeyMap[fromAddress]; exists { + return types.SignTx(tx, types.LatestSignerForChainID(chainID), key) + } + return nil, fmt.Errorf("private key for address: %v not found", fromAddress) +} + +func (k *DummyKeystore) SignMessage(ctx context.Context, address common.Address, data []byte) ([]byte, error) { + key, exists := k.privateKeyMap[address] + if !exists { + return nil, fmt.Errorf("private key for address: %v not found", address) + } + signature, err := crypto.Sign(accounts.TextHash(data), key) + if err != nil { + return nil, fmt.Errorf("failed to sign message for address: %v", address) + } + return signature, nil +} + +func (k *DummyKeystore) EnabledAddressesForChain(_ context.Context, _ *big.Int) (addresses []common.Address, err error) { + for address := range k.privateKeyMap { + addresses = append(addresses, address) + } + return +} diff --git a/core/chains/evm/txm/mocks/attempt_builder.go b/core/chains/evm/txm/mocks/attempt_builder.go new file mode 100644 index 00000000000..aad06745cf4 --- /dev/null +++ b/core/chains/evm/txm/mocks/attempt_builder.go @@ -0,0 +1,161 @@ +// Code generated by mockery v2.46.3. DO NOT EDIT. + +package mocks + +import ( + context "context" + + logger "github.com/smartcontractkit/chainlink-common/pkg/logger" + mock "github.com/stretchr/testify/mock" + + types "github.com/smartcontractkit/chainlink/v2/core/chains/evm/txm/types" +) + +// AttemptBuilder is an autogenerated mock type for the AttemptBuilder type +type AttemptBuilder struct { + mock.Mock +} + +type AttemptBuilder_Expecter struct { + mock *mock.Mock +} + +func (_m *AttemptBuilder) EXPECT() *AttemptBuilder_Expecter { + return &AttemptBuilder_Expecter{mock: &_m.Mock} +} + +// NewAttempt provides a mock function with given fields: _a0, _a1, _a2, _a3 +func (_m *AttemptBuilder) NewAttempt(_a0 context.Context, _a1 logger.Logger, _a2 *types.Transaction, _a3 bool) (*types.Attempt, error) { + ret := _m.Called(_a0, _a1, _a2, _a3) + + if len(ret) == 0 { + panic("no return value specified for NewAttempt") + } + + var r0 *types.Attempt + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, logger.Logger, *types.Transaction, bool) (*types.Attempt, error)); ok { + return rf(_a0, _a1, _a2, _a3) + } + if rf, ok := ret.Get(0).(func(context.Context, logger.Logger, *types.Transaction, bool) *types.Attempt); ok { + r0 = rf(_a0, _a1, _a2, _a3) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.Attempt) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, logger.Logger, *types.Transaction, bool) error); ok { + r1 = rf(_a0, _a1, _a2, _a3) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// AttemptBuilder_NewAttempt_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'NewAttempt' +type AttemptBuilder_NewAttempt_Call struct { + *mock.Call +} + +// NewAttempt is a helper method to define mock.On call +// - _a0 context.Context +// - _a1 logger.Logger +// - _a2 *types.Transaction +// - _a3 bool +func (_e *AttemptBuilder_Expecter) NewAttempt(_a0 interface{}, _a1 interface{}, _a2 interface{}, _a3 interface{}) *AttemptBuilder_NewAttempt_Call { + return &AttemptBuilder_NewAttempt_Call{Call: _e.mock.On("NewAttempt", _a0, _a1, _a2, _a3)} +} + +func (_c *AttemptBuilder_NewAttempt_Call) Run(run func(_a0 context.Context, _a1 logger.Logger, _a2 *types.Transaction, _a3 bool)) *AttemptBuilder_NewAttempt_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(logger.Logger), args[2].(*types.Transaction), args[3].(bool)) + }) + return _c +} + +func (_c *AttemptBuilder_NewAttempt_Call) Return(_a0 *types.Attempt, _a1 error) *AttemptBuilder_NewAttempt_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *AttemptBuilder_NewAttempt_Call) RunAndReturn(run func(context.Context, logger.Logger, *types.Transaction, bool) (*types.Attempt, error)) *AttemptBuilder_NewAttempt_Call { + _c.Call.Return(run) + return _c +} + +// NewBumpAttempt provides a mock function with given fields: _a0, _a1, _a2, _a3 +func (_m *AttemptBuilder) NewBumpAttempt(_a0 context.Context, _a1 logger.Logger, _a2 *types.Transaction, _a3 types.Attempt) (*types.Attempt, error) { + ret := _m.Called(_a0, _a1, _a2, _a3) + + if len(ret) == 0 { + panic("no return value specified for NewBumpAttempt") + } + + var r0 *types.Attempt + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, logger.Logger, *types.Transaction, types.Attempt) (*types.Attempt, error)); ok { + return rf(_a0, _a1, _a2, _a3) + } + if rf, ok := ret.Get(0).(func(context.Context, logger.Logger, *types.Transaction, types.Attempt) *types.Attempt); ok { + r0 = rf(_a0, _a1, _a2, _a3) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.Attempt) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, logger.Logger, *types.Transaction, types.Attempt) error); ok { + r1 = rf(_a0, _a1, _a2, _a3) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// AttemptBuilder_NewBumpAttempt_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'NewBumpAttempt' +type AttemptBuilder_NewBumpAttempt_Call struct { + *mock.Call +} + +// NewBumpAttempt is a helper method to define mock.On call +// - _a0 context.Context +// - _a1 logger.Logger +// - _a2 *types.Transaction +// - _a3 types.Attempt +func (_e *AttemptBuilder_Expecter) NewBumpAttempt(_a0 interface{}, _a1 interface{}, _a2 interface{}, _a3 interface{}) *AttemptBuilder_NewBumpAttempt_Call { + return &AttemptBuilder_NewBumpAttempt_Call{Call: _e.mock.On("NewBumpAttempt", _a0, _a1, _a2, _a3)} +} + +func (_c *AttemptBuilder_NewBumpAttempt_Call) Run(run func(_a0 context.Context, _a1 logger.Logger, _a2 *types.Transaction, _a3 types.Attempt)) *AttemptBuilder_NewBumpAttempt_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(logger.Logger), args[2].(*types.Transaction), args[3].(types.Attempt)) + }) + return _c +} + +func (_c *AttemptBuilder_NewBumpAttempt_Call) Return(_a0 *types.Attempt, _a1 error) *AttemptBuilder_NewBumpAttempt_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *AttemptBuilder_NewBumpAttempt_Call) RunAndReturn(run func(context.Context, logger.Logger, *types.Transaction, types.Attempt) (*types.Attempt, error)) *AttemptBuilder_NewBumpAttempt_Call { + _c.Call.Return(run) + return _c +} + +// NewAttemptBuilder creates a new instance of AttemptBuilder. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewAttemptBuilder(t interface { + mock.TestingT + Cleanup(func()) +}) *AttemptBuilder { + mock := &AttemptBuilder{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/core/chains/evm/txm/mocks/client.go b/core/chains/evm/txm/mocks/client.go new file mode 100644 index 00000000000..03849ad7e82 --- /dev/null +++ b/core/chains/evm/txm/mocks/client.go @@ -0,0 +1,204 @@ +// Code generated by mockery v2.46.3. DO NOT EDIT. + +package mocks + +import ( + context "context" + big "math/big" + + common "github.com/ethereum/go-ethereum/common" + + mock "github.com/stretchr/testify/mock" + + types "github.com/smartcontractkit/chainlink/v2/core/chains/evm/txm/types" +) + +// Client is an autogenerated mock type for the Client type +type Client struct { + mock.Mock +} + +type Client_Expecter struct { + mock *mock.Mock +} + +func (_m *Client) EXPECT() *Client_Expecter { + return &Client_Expecter{mock: &_m.Mock} +} + +// NonceAt provides a mock function with given fields: _a0, _a1, _a2 +func (_m *Client) NonceAt(_a0 context.Context, _a1 common.Address, _a2 *big.Int) (uint64, error) { + ret := _m.Called(_a0, _a1, _a2) + + if len(ret) == 0 { + panic("no return value specified for NonceAt") + } + + var r0 uint64 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, common.Address, *big.Int) (uint64, error)); ok { + return rf(_a0, _a1, _a2) + } + if rf, ok := ret.Get(0).(func(context.Context, common.Address, *big.Int) uint64); ok { + r0 = rf(_a0, _a1, _a2) + } else { + r0 = ret.Get(0).(uint64) + } + + if rf, ok := ret.Get(1).(func(context.Context, common.Address, *big.Int) error); ok { + r1 = rf(_a0, _a1, _a2) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Client_NonceAt_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'NonceAt' +type Client_NonceAt_Call struct { + *mock.Call +} + +// NonceAt is a helper method to define mock.On call +// - _a0 context.Context +// - _a1 common.Address +// - _a2 *big.Int +func (_e *Client_Expecter) NonceAt(_a0 interface{}, _a1 interface{}, _a2 interface{}) *Client_NonceAt_Call { + return &Client_NonceAt_Call{Call: _e.mock.On("NonceAt", _a0, _a1, _a2)} +} + +func (_c *Client_NonceAt_Call) Run(run func(_a0 context.Context, _a1 common.Address, _a2 *big.Int)) *Client_NonceAt_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(common.Address), args[2].(*big.Int)) + }) + return _c +} + +func (_c *Client_NonceAt_Call) Return(_a0 uint64, _a1 error) *Client_NonceAt_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *Client_NonceAt_Call) RunAndReturn(run func(context.Context, common.Address, *big.Int) (uint64, error)) *Client_NonceAt_Call { + _c.Call.Return(run) + return _c +} + +// PendingNonceAt provides a mock function with given fields: _a0, _a1 +func (_m *Client) PendingNonceAt(_a0 context.Context, _a1 common.Address) (uint64, error) { + ret := _m.Called(_a0, _a1) + + if len(ret) == 0 { + panic("no return value specified for PendingNonceAt") + } + + var r0 uint64 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, common.Address) (uint64, error)); ok { + return rf(_a0, _a1) + } + if rf, ok := ret.Get(0).(func(context.Context, common.Address) uint64); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Get(0).(uint64) + } + + if rf, ok := ret.Get(1).(func(context.Context, common.Address) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Client_PendingNonceAt_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PendingNonceAt' +type Client_PendingNonceAt_Call struct { + *mock.Call +} + +// PendingNonceAt is a helper method to define mock.On call +// - _a0 context.Context +// - _a1 common.Address +func (_e *Client_Expecter) PendingNonceAt(_a0 interface{}, _a1 interface{}) *Client_PendingNonceAt_Call { + return &Client_PendingNonceAt_Call{Call: _e.mock.On("PendingNonceAt", _a0, _a1)} +} + +func (_c *Client_PendingNonceAt_Call) Run(run func(_a0 context.Context, _a1 common.Address)) *Client_PendingNonceAt_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(common.Address)) + }) + return _c +} + +func (_c *Client_PendingNonceAt_Call) Return(_a0 uint64, _a1 error) *Client_PendingNonceAt_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *Client_PendingNonceAt_Call) RunAndReturn(run func(context.Context, common.Address) (uint64, error)) *Client_PendingNonceAt_Call { + _c.Call.Return(run) + return _c +} + +// SendTransaction provides a mock function with given fields: ctx, tx, attempt +func (_m *Client) SendTransaction(ctx context.Context, tx *types.Transaction, attempt *types.Attempt) error { + ret := _m.Called(ctx, tx, attempt) + + if len(ret) == 0 { + panic("no return value specified for SendTransaction") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *types.Transaction, *types.Attempt) error); ok { + r0 = rf(ctx, tx, attempt) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Client_SendTransaction_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendTransaction' +type Client_SendTransaction_Call struct { + *mock.Call +} + +// SendTransaction is a helper method to define mock.On call +// - ctx context.Context +// - tx *types.Transaction +// - attempt *types.Attempt +func (_e *Client_Expecter) SendTransaction(ctx interface{}, tx interface{}, attempt interface{}) *Client_SendTransaction_Call { + return &Client_SendTransaction_Call{Call: _e.mock.On("SendTransaction", ctx, tx, attempt)} +} + +func (_c *Client_SendTransaction_Call) Run(run func(ctx context.Context, tx *types.Transaction, attempt *types.Attempt)) *Client_SendTransaction_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*types.Transaction), args[2].(*types.Attempt)) + }) + return _c +} + +func (_c *Client_SendTransaction_Call) Return(_a0 error) *Client_SendTransaction_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Client_SendTransaction_Call) RunAndReturn(run func(context.Context, *types.Transaction, *types.Attempt) error) *Client_SendTransaction_Call { + _c.Call.Return(run) + return _c +} + +// NewClient creates a new instance of Client. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewClient(t interface { + mock.TestingT + Cleanup(func()) +}) *Client { + mock := &Client{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/core/chains/evm/txm/mocks/keystore.go b/core/chains/evm/txm/mocks/keystore.go new file mode 100644 index 00000000000..c61ddb8db04 --- /dev/null +++ b/core/chains/evm/txm/mocks/keystore.go @@ -0,0 +1,98 @@ +// Code generated by mockery v2.46.3. DO NOT EDIT. + +package mocks + +import ( + context "context" + big "math/big" + + common "github.com/ethereum/go-ethereum/common" + + mock "github.com/stretchr/testify/mock" +) + +// Keystore is an autogenerated mock type for the Keystore type +type Keystore struct { + mock.Mock +} + +type Keystore_Expecter struct { + mock *mock.Mock +} + +func (_m *Keystore) EXPECT() *Keystore_Expecter { + return &Keystore_Expecter{mock: &_m.Mock} +} + +// EnabledAddressesForChain provides a mock function with given fields: ctx, chainID +func (_m *Keystore) EnabledAddressesForChain(ctx context.Context, chainID *big.Int) ([]common.Address, error) { + ret := _m.Called(ctx, chainID) + + if len(ret) == 0 { + panic("no return value specified for EnabledAddressesForChain") + } + + var r0 []common.Address + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *big.Int) ([]common.Address, error)); ok { + return rf(ctx, chainID) + } + if rf, ok := ret.Get(0).(func(context.Context, *big.Int) []common.Address); ok { + r0 = rf(ctx, chainID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]common.Address) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *big.Int) error); ok { + r1 = rf(ctx, chainID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Keystore_EnabledAddressesForChain_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EnabledAddressesForChain' +type Keystore_EnabledAddressesForChain_Call struct { + *mock.Call +} + +// EnabledAddressesForChain is a helper method to define mock.On call +// - ctx context.Context +// - chainID *big.Int +func (_e *Keystore_Expecter) EnabledAddressesForChain(ctx interface{}, chainID interface{}) *Keystore_EnabledAddressesForChain_Call { + return &Keystore_EnabledAddressesForChain_Call{Call: _e.mock.On("EnabledAddressesForChain", ctx, chainID)} +} + +func (_c *Keystore_EnabledAddressesForChain_Call) Run(run func(ctx context.Context, chainID *big.Int)) *Keystore_EnabledAddressesForChain_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*big.Int)) + }) + return _c +} + +func (_c *Keystore_EnabledAddressesForChain_Call) Return(addresses []common.Address, err error) *Keystore_EnabledAddressesForChain_Call { + _c.Call.Return(addresses, err) + return _c +} + +func (_c *Keystore_EnabledAddressesForChain_Call) RunAndReturn(run func(context.Context, *big.Int) ([]common.Address, error)) *Keystore_EnabledAddressesForChain_Call { + _c.Call.Return(run) + return _c +} + +// NewKeystore creates a new instance of Keystore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewKeystore(t interface { + mock.TestingT + Cleanup(func()) +}) *Keystore { + mock := &Keystore{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/core/chains/evm/txm/mocks/tx_store.go b/core/chains/evm/txm/mocks/tx_store.go new file mode 100644 index 00000000000..866e095b1a1 --- /dev/null +++ b/core/chains/evm/txm/mocks/tx_store.go @@ -0,0 +1,647 @@ +// Code generated by mockery v2.46.3. DO NOT EDIT. + +package mocks + +import ( + context "context" + + common "github.com/ethereum/go-ethereum/common" + + mock "github.com/stretchr/testify/mock" + + types "github.com/smartcontractkit/chainlink/v2/core/chains/evm/txm/types" +) + +// TxStore is an autogenerated mock type for the TxStore type +type TxStore struct { + mock.Mock +} + +type TxStore_Expecter struct { + mock *mock.Mock +} + +func (_m *TxStore) EXPECT() *TxStore_Expecter { + return &TxStore_Expecter{mock: &_m.Mock} +} + +// AbandonPendingTransactions provides a mock function with given fields: _a0, _a1 +func (_m *TxStore) AbandonPendingTransactions(_a0 context.Context, _a1 common.Address) error { + ret := _m.Called(_a0, _a1) + + if len(ret) == 0 { + panic("no return value specified for AbandonPendingTransactions") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, common.Address) error); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// TxStore_AbandonPendingTransactions_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AbandonPendingTransactions' +type TxStore_AbandonPendingTransactions_Call struct { + *mock.Call +} + +// AbandonPendingTransactions is a helper method to define mock.On call +// - _a0 context.Context +// - _a1 common.Address +func (_e *TxStore_Expecter) AbandonPendingTransactions(_a0 interface{}, _a1 interface{}) *TxStore_AbandonPendingTransactions_Call { + return &TxStore_AbandonPendingTransactions_Call{Call: _e.mock.On("AbandonPendingTransactions", _a0, _a1)} +} + +func (_c *TxStore_AbandonPendingTransactions_Call) Run(run func(_a0 context.Context, _a1 common.Address)) *TxStore_AbandonPendingTransactions_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(common.Address)) + }) + return _c +} + +func (_c *TxStore_AbandonPendingTransactions_Call) Return(_a0 error) *TxStore_AbandonPendingTransactions_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *TxStore_AbandonPendingTransactions_Call) RunAndReturn(run func(context.Context, common.Address) error) *TxStore_AbandonPendingTransactions_Call { + _c.Call.Return(run) + return _c +} + +// AppendAttemptToTransaction provides a mock function with given fields: _a0, _a1, _a2, _a3 +func (_m *TxStore) AppendAttemptToTransaction(_a0 context.Context, _a1 uint64, _a2 common.Address, _a3 *types.Attempt) error { + ret := _m.Called(_a0, _a1, _a2, _a3) + + if len(ret) == 0 { + panic("no return value specified for AppendAttemptToTransaction") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, uint64, common.Address, *types.Attempt) error); ok { + r0 = rf(_a0, _a1, _a2, _a3) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// TxStore_AppendAttemptToTransaction_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AppendAttemptToTransaction' +type TxStore_AppendAttemptToTransaction_Call struct { + *mock.Call +} + +// AppendAttemptToTransaction is a helper method to define mock.On call +// - _a0 context.Context +// - _a1 uint64 +// - _a2 common.Address +// - _a3 *types.Attempt +func (_e *TxStore_Expecter) AppendAttemptToTransaction(_a0 interface{}, _a1 interface{}, _a2 interface{}, _a3 interface{}) *TxStore_AppendAttemptToTransaction_Call { + return &TxStore_AppendAttemptToTransaction_Call{Call: _e.mock.On("AppendAttemptToTransaction", _a0, _a1, _a2, _a3)} +} + +func (_c *TxStore_AppendAttemptToTransaction_Call) Run(run func(_a0 context.Context, _a1 uint64, _a2 common.Address, _a3 *types.Attempt)) *TxStore_AppendAttemptToTransaction_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(uint64), args[2].(common.Address), args[3].(*types.Attempt)) + }) + return _c +} + +func (_c *TxStore_AppendAttemptToTransaction_Call) Return(_a0 error) *TxStore_AppendAttemptToTransaction_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *TxStore_AppendAttemptToTransaction_Call) RunAndReturn(run func(context.Context, uint64, common.Address, *types.Attempt) error) *TxStore_AppendAttemptToTransaction_Call { + _c.Call.Return(run) + return _c +} + +// CreateEmptyUnconfirmedTransaction provides a mock function with given fields: _a0, _a1, _a2, _a3 +func (_m *TxStore) CreateEmptyUnconfirmedTransaction(_a0 context.Context, _a1 common.Address, _a2 uint64, _a3 uint64) (*types.Transaction, error) { + ret := _m.Called(_a0, _a1, _a2, _a3) + + if len(ret) == 0 { + panic("no return value specified for CreateEmptyUnconfirmedTransaction") + } + + var r0 *types.Transaction + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, common.Address, uint64, uint64) (*types.Transaction, error)); ok { + return rf(_a0, _a1, _a2, _a3) + } + if rf, ok := ret.Get(0).(func(context.Context, common.Address, uint64, uint64) *types.Transaction); ok { + r0 = rf(_a0, _a1, _a2, _a3) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.Transaction) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, common.Address, uint64, uint64) error); ok { + r1 = rf(_a0, _a1, _a2, _a3) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// TxStore_CreateEmptyUnconfirmedTransaction_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateEmptyUnconfirmedTransaction' +type TxStore_CreateEmptyUnconfirmedTransaction_Call struct { + *mock.Call +} + +// CreateEmptyUnconfirmedTransaction is a helper method to define mock.On call +// - _a0 context.Context +// - _a1 common.Address +// - _a2 uint64 +// - _a3 uint64 +func (_e *TxStore_Expecter) CreateEmptyUnconfirmedTransaction(_a0 interface{}, _a1 interface{}, _a2 interface{}, _a3 interface{}) *TxStore_CreateEmptyUnconfirmedTransaction_Call { + return &TxStore_CreateEmptyUnconfirmedTransaction_Call{Call: _e.mock.On("CreateEmptyUnconfirmedTransaction", _a0, _a1, _a2, _a3)} +} + +func (_c *TxStore_CreateEmptyUnconfirmedTransaction_Call) Run(run func(_a0 context.Context, _a1 common.Address, _a2 uint64, _a3 uint64)) *TxStore_CreateEmptyUnconfirmedTransaction_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(common.Address), args[2].(uint64), args[3].(uint64)) + }) + return _c +} + +func (_c *TxStore_CreateEmptyUnconfirmedTransaction_Call) Return(_a0 *types.Transaction, _a1 error) *TxStore_CreateEmptyUnconfirmedTransaction_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *TxStore_CreateEmptyUnconfirmedTransaction_Call) RunAndReturn(run func(context.Context, common.Address, uint64, uint64) (*types.Transaction, error)) *TxStore_CreateEmptyUnconfirmedTransaction_Call { + _c.Call.Return(run) + return _c +} + +// CreateTransaction provides a mock function with given fields: _a0, _a1 +func (_m *TxStore) CreateTransaction(_a0 context.Context, _a1 *types.TxRequest) (*types.Transaction, error) { + ret := _m.Called(_a0, _a1) + + if len(ret) == 0 { + panic("no return value specified for CreateTransaction") + } + + var r0 *types.Transaction + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *types.TxRequest) (*types.Transaction, error)); ok { + return rf(_a0, _a1) + } + if rf, ok := ret.Get(0).(func(context.Context, *types.TxRequest) *types.Transaction); ok { + r0 = rf(_a0, _a1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.Transaction) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *types.TxRequest) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// TxStore_CreateTransaction_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateTransaction' +type TxStore_CreateTransaction_Call struct { + *mock.Call +} + +// CreateTransaction is a helper method to define mock.On call +// - _a0 context.Context +// - _a1 *types.TxRequest +func (_e *TxStore_Expecter) CreateTransaction(_a0 interface{}, _a1 interface{}) *TxStore_CreateTransaction_Call { + return &TxStore_CreateTransaction_Call{Call: _e.mock.On("CreateTransaction", _a0, _a1)} +} + +func (_c *TxStore_CreateTransaction_Call) Run(run func(_a0 context.Context, _a1 *types.TxRequest)) *TxStore_CreateTransaction_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*types.TxRequest)) + }) + return _c +} + +func (_c *TxStore_CreateTransaction_Call) Return(_a0 *types.Transaction, _a1 error) *TxStore_CreateTransaction_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *TxStore_CreateTransaction_Call) RunAndReturn(run func(context.Context, *types.TxRequest) (*types.Transaction, error)) *TxStore_CreateTransaction_Call { + _c.Call.Return(run) + return _c +} + +// DeleteAttemptForUnconfirmedTx provides a mock function with given fields: _a0, _a1, _a2, _a3 +func (_m *TxStore) DeleteAttemptForUnconfirmedTx(_a0 context.Context, _a1 uint64, _a2 *types.Attempt, _a3 common.Address) error { + ret := _m.Called(_a0, _a1, _a2, _a3) + + if len(ret) == 0 { + panic("no return value specified for DeleteAttemptForUnconfirmedTx") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, uint64, *types.Attempt, common.Address) error); ok { + r0 = rf(_a0, _a1, _a2, _a3) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// TxStore_DeleteAttemptForUnconfirmedTx_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteAttemptForUnconfirmedTx' +type TxStore_DeleteAttemptForUnconfirmedTx_Call struct { + *mock.Call +} + +// DeleteAttemptForUnconfirmedTx is a helper method to define mock.On call +// - _a0 context.Context +// - _a1 uint64 +// - _a2 *types.Attempt +// - _a3 common.Address +func (_e *TxStore_Expecter) DeleteAttemptForUnconfirmedTx(_a0 interface{}, _a1 interface{}, _a2 interface{}, _a3 interface{}) *TxStore_DeleteAttemptForUnconfirmedTx_Call { + return &TxStore_DeleteAttemptForUnconfirmedTx_Call{Call: _e.mock.On("DeleteAttemptForUnconfirmedTx", _a0, _a1, _a2, _a3)} +} + +func (_c *TxStore_DeleteAttemptForUnconfirmedTx_Call) Run(run func(_a0 context.Context, _a1 uint64, _a2 *types.Attempt, _a3 common.Address)) *TxStore_DeleteAttemptForUnconfirmedTx_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(uint64), args[2].(*types.Attempt), args[3].(common.Address)) + }) + return _c +} + +func (_c *TxStore_DeleteAttemptForUnconfirmedTx_Call) Return(_a0 error) *TxStore_DeleteAttemptForUnconfirmedTx_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *TxStore_DeleteAttemptForUnconfirmedTx_Call) RunAndReturn(run func(context.Context, uint64, *types.Attempt, common.Address) error) *TxStore_DeleteAttemptForUnconfirmedTx_Call { + _c.Call.Return(run) + return _c +} + +// FetchUnconfirmedTransactionAtNonceWithCount provides a mock function with given fields: _a0, _a1, _a2 +func (_m *TxStore) FetchUnconfirmedTransactionAtNonceWithCount(_a0 context.Context, _a1 uint64, _a2 common.Address) (*types.Transaction, int, error) { + ret := _m.Called(_a0, _a1, _a2) + + if len(ret) == 0 { + panic("no return value specified for FetchUnconfirmedTransactionAtNonceWithCount") + } + + var r0 *types.Transaction + var r1 int + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, uint64, common.Address) (*types.Transaction, int, error)); ok { + return rf(_a0, _a1, _a2) + } + if rf, ok := ret.Get(0).(func(context.Context, uint64, common.Address) *types.Transaction); ok { + r0 = rf(_a0, _a1, _a2) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.Transaction) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, uint64, common.Address) int); ok { + r1 = rf(_a0, _a1, _a2) + } else { + r1 = ret.Get(1).(int) + } + + if rf, ok := ret.Get(2).(func(context.Context, uint64, common.Address) error); ok { + r2 = rf(_a0, _a1, _a2) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// TxStore_FetchUnconfirmedTransactionAtNonceWithCount_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FetchUnconfirmedTransactionAtNonceWithCount' +type TxStore_FetchUnconfirmedTransactionAtNonceWithCount_Call struct { + *mock.Call +} + +// FetchUnconfirmedTransactionAtNonceWithCount is a helper method to define mock.On call +// - _a0 context.Context +// - _a1 uint64 +// - _a2 common.Address +func (_e *TxStore_Expecter) FetchUnconfirmedTransactionAtNonceWithCount(_a0 interface{}, _a1 interface{}, _a2 interface{}) *TxStore_FetchUnconfirmedTransactionAtNonceWithCount_Call { + return &TxStore_FetchUnconfirmedTransactionAtNonceWithCount_Call{Call: _e.mock.On("FetchUnconfirmedTransactionAtNonceWithCount", _a0, _a1, _a2)} +} + +func (_c *TxStore_FetchUnconfirmedTransactionAtNonceWithCount_Call) Run(run func(_a0 context.Context, _a1 uint64, _a2 common.Address)) *TxStore_FetchUnconfirmedTransactionAtNonceWithCount_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(uint64), args[2].(common.Address)) + }) + return _c +} + +func (_c *TxStore_FetchUnconfirmedTransactionAtNonceWithCount_Call) Return(_a0 *types.Transaction, _a1 int, _a2 error) *TxStore_FetchUnconfirmedTransactionAtNonceWithCount_Call { + _c.Call.Return(_a0, _a1, _a2) + return _c +} + +func (_c *TxStore_FetchUnconfirmedTransactionAtNonceWithCount_Call) RunAndReturn(run func(context.Context, uint64, common.Address) (*types.Transaction, int, error)) *TxStore_FetchUnconfirmedTransactionAtNonceWithCount_Call { + _c.Call.Return(run) + return _c +} + +// MarkTransactionsConfirmed provides a mock function with given fields: _a0, _a1, _a2 +func (_m *TxStore) MarkTransactionsConfirmed(_a0 context.Context, _a1 uint64, _a2 common.Address) ([]uint64, []uint64, error) { + ret := _m.Called(_a0, _a1, _a2) + + if len(ret) == 0 { + panic("no return value specified for MarkTransactionsConfirmed") + } + + var r0 []uint64 + var r1 []uint64 + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, uint64, common.Address) ([]uint64, []uint64, error)); ok { + return rf(_a0, _a1, _a2) + } + if rf, ok := ret.Get(0).(func(context.Context, uint64, common.Address) []uint64); ok { + r0 = rf(_a0, _a1, _a2) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]uint64) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, uint64, common.Address) []uint64); ok { + r1 = rf(_a0, _a1, _a2) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).([]uint64) + } + } + + if rf, ok := ret.Get(2).(func(context.Context, uint64, common.Address) error); ok { + r2 = rf(_a0, _a1, _a2) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// TxStore_MarkTransactionsConfirmed_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'MarkTransactionsConfirmed' +type TxStore_MarkTransactionsConfirmed_Call struct { + *mock.Call +} + +// MarkTransactionsConfirmed is a helper method to define mock.On call +// - _a0 context.Context +// - _a1 uint64 +// - _a2 common.Address +func (_e *TxStore_Expecter) MarkTransactionsConfirmed(_a0 interface{}, _a1 interface{}, _a2 interface{}) *TxStore_MarkTransactionsConfirmed_Call { + return &TxStore_MarkTransactionsConfirmed_Call{Call: _e.mock.On("MarkTransactionsConfirmed", _a0, _a1, _a2)} +} + +func (_c *TxStore_MarkTransactionsConfirmed_Call) Run(run func(_a0 context.Context, _a1 uint64, _a2 common.Address)) *TxStore_MarkTransactionsConfirmed_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(uint64), args[2].(common.Address)) + }) + return _c +} + +func (_c *TxStore_MarkTransactionsConfirmed_Call) Return(_a0 []uint64, _a1 []uint64, _a2 error) *TxStore_MarkTransactionsConfirmed_Call { + _c.Call.Return(_a0, _a1, _a2) + return _c +} + +func (_c *TxStore_MarkTransactionsConfirmed_Call) RunAndReturn(run func(context.Context, uint64, common.Address) ([]uint64, []uint64, error)) *TxStore_MarkTransactionsConfirmed_Call { + _c.Call.Return(run) + return _c +} + +// MarkTxFatal provides a mock function with given fields: _a0, _a1, _a2 +func (_m *TxStore) MarkTxFatal(_a0 context.Context, _a1 *types.Transaction, _a2 common.Address) error { + ret := _m.Called(_a0, _a1, _a2) + + if len(ret) == 0 { + panic("no return value specified for MarkTxFatal") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *types.Transaction, common.Address) error); ok { + r0 = rf(_a0, _a1, _a2) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// TxStore_MarkTxFatal_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'MarkTxFatal' +type TxStore_MarkTxFatal_Call struct { + *mock.Call +} + +// MarkTxFatal is a helper method to define mock.On call +// - _a0 context.Context +// - _a1 *types.Transaction +// - _a2 common.Address +func (_e *TxStore_Expecter) MarkTxFatal(_a0 interface{}, _a1 interface{}, _a2 interface{}) *TxStore_MarkTxFatal_Call { + return &TxStore_MarkTxFatal_Call{Call: _e.mock.On("MarkTxFatal", _a0, _a1, _a2)} +} + +func (_c *TxStore_MarkTxFatal_Call) Run(run func(_a0 context.Context, _a1 *types.Transaction, _a2 common.Address)) *TxStore_MarkTxFatal_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*types.Transaction), args[2].(common.Address)) + }) + return _c +} + +func (_c *TxStore_MarkTxFatal_Call) Return(_a0 error) *TxStore_MarkTxFatal_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *TxStore_MarkTxFatal_Call) RunAndReturn(run func(context.Context, *types.Transaction, common.Address) error) *TxStore_MarkTxFatal_Call { + _c.Call.Return(run) + return _c +} + +// MarkUnconfirmedTransactionPurgeable provides a mock function with given fields: _a0, _a1, _a2 +func (_m *TxStore) MarkUnconfirmedTransactionPurgeable(_a0 context.Context, _a1 uint64, _a2 common.Address) error { + ret := _m.Called(_a0, _a1, _a2) + + if len(ret) == 0 { + panic("no return value specified for MarkUnconfirmedTransactionPurgeable") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, uint64, common.Address) error); ok { + r0 = rf(_a0, _a1, _a2) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// TxStore_MarkUnconfirmedTransactionPurgeable_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'MarkUnconfirmedTransactionPurgeable' +type TxStore_MarkUnconfirmedTransactionPurgeable_Call struct { + *mock.Call +} + +// MarkUnconfirmedTransactionPurgeable is a helper method to define mock.On call +// - _a0 context.Context +// - _a1 uint64 +// - _a2 common.Address +func (_e *TxStore_Expecter) MarkUnconfirmedTransactionPurgeable(_a0 interface{}, _a1 interface{}, _a2 interface{}) *TxStore_MarkUnconfirmedTransactionPurgeable_Call { + return &TxStore_MarkUnconfirmedTransactionPurgeable_Call{Call: _e.mock.On("MarkUnconfirmedTransactionPurgeable", _a0, _a1, _a2)} +} + +func (_c *TxStore_MarkUnconfirmedTransactionPurgeable_Call) Run(run func(_a0 context.Context, _a1 uint64, _a2 common.Address)) *TxStore_MarkUnconfirmedTransactionPurgeable_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(uint64), args[2].(common.Address)) + }) + return _c +} + +func (_c *TxStore_MarkUnconfirmedTransactionPurgeable_Call) Return(_a0 error) *TxStore_MarkUnconfirmedTransactionPurgeable_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *TxStore_MarkUnconfirmedTransactionPurgeable_Call) RunAndReturn(run func(context.Context, uint64, common.Address) error) *TxStore_MarkUnconfirmedTransactionPurgeable_Call { + _c.Call.Return(run) + return _c +} + +// UpdateTransactionBroadcast provides a mock function with given fields: _a0, _a1, _a2, _a3, _a4 +func (_m *TxStore) UpdateTransactionBroadcast(_a0 context.Context, _a1 uint64, _a2 uint64, _a3 common.Hash, _a4 common.Address) error { + ret := _m.Called(_a0, _a1, _a2, _a3, _a4) + + if len(ret) == 0 { + panic("no return value specified for UpdateTransactionBroadcast") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, uint64, uint64, common.Hash, common.Address) error); ok { + r0 = rf(_a0, _a1, _a2, _a3, _a4) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// TxStore_UpdateTransactionBroadcast_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateTransactionBroadcast' +type TxStore_UpdateTransactionBroadcast_Call struct { + *mock.Call +} + +// UpdateTransactionBroadcast is a helper method to define mock.On call +// - _a0 context.Context +// - _a1 uint64 +// - _a2 uint64 +// - _a3 common.Hash +// - _a4 common.Address +func (_e *TxStore_Expecter) UpdateTransactionBroadcast(_a0 interface{}, _a1 interface{}, _a2 interface{}, _a3 interface{}, _a4 interface{}) *TxStore_UpdateTransactionBroadcast_Call { + return &TxStore_UpdateTransactionBroadcast_Call{Call: _e.mock.On("UpdateTransactionBroadcast", _a0, _a1, _a2, _a3, _a4)} +} + +func (_c *TxStore_UpdateTransactionBroadcast_Call) Run(run func(_a0 context.Context, _a1 uint64, _a2 uint64, _a3 common.Hash, _a4 common.Address)) *TxStore_UpdateTransactionBroadcast_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(uint64), args[2].(uint64), args[3].(common.Hash), args[4].(common.Address)) + }) + return _c +} + +func (_c *TxStore_UpdateTransactionBroadcast_Call) Return(_a0 error) *TxStore_UpdateTransactionBroadcast_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *TxStore_UpdateTransactionBroadcast_Call) RunAndReturn(run func(context.Context, uint64, uint64, common.Hash, common.Address) error) *TxStore_UpdateTransactionBroadcast_Call { + _c.Call.Return(run) + return _c +} + +// UpdateUnstartedTransactionWithNonce provides a mock function with given fields: _a0, _a1, _a2 +func (_m *TxStore) UpdateUnstartedTransactionWithNonce(_a0 context.Context, _a1 common.Address, _a2 uint64) (*types.Transaction, error) { + ret := _m.Called(_a0, _a1, _a2) + + if len(ret) == 0 { + panic("no return value specified for UpdateUnstartedTransactionWithNonce") + } + + var r0 *types.Transaction + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, common.Address, uint64) (*types.Transaction, error)); ok { + return rf(_a0, _a1, _a2) + } + if rf, ok := ret.Get(0).(func(context.Context, common.Address, uint64) *types.Transaction); ok { + r0 = rf(_a0, _a1, _a2) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.Transaction) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, common.Address, uint64) error); ok { + r1 = rf(_a0, _a1, _a2) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// TxStore_UpdateUnstartedTransactionWithNonce_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateUnstartedTransactionWithNonce' +type TxStore_UpdateUnstartedTransactionWithNonce_Call struct { + *mock.Call +} + +// UpdateUnstartedTransactionWithNonce is a helper method to define mock.On call +// - _a0 context.Context +// - _a1 common.Address +// - _a2 uint64 +func (_e *TxStore_Expecter) UpdateUnstartedTransactionWithNonce(_a0 interface{}, _a1 interface{}, _a2 interface{}) *TxStore_UpdateUnstartedTransactionWithNonce_Call { + return &TxStore_UpdateUnstartedTransactionWithNonce_Call{Call: _e.mock.On("UpdateUnstartedTransactionWithNonce", _a0, _a1, _a2)} +} + +func (_c *TxStore_UpdateUnstartedTransactionWithNonce_Call) Run(run func(_a0 context.Context, _a1 common.Address, _a2 uint64)) *TxStore_UpdateUnstartedTransactionWithNonce_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(common.Address), args[2].(uint64)) + }) + return _c +} + +func (_c *TxStore_UpdateUnstartedTransactionWithNonce_Call) Return(_a0 *types.Transaction, _a1 error) *TxStore_UpdateUnstartedTransactionWithNonce_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *TxStore_UpdateUnstartedTransactionWithNonce_Call) RunAndReturn(run func(context.Context, common.Address, uint64) (*types.Transaction, error)) *TxStore_UpdateUnstartedTransactionWithNonce_Call { + _c.Call.Return(run) + return _c +} + +// NewTxStore creates a new instance of TxStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewTxStore(t interface { + mock.TestingT + Cleanup(func()) +}) *TxStore { + mock := &TxStore{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/core/chains/evm/txm/orchestrator.go b/core/chains/evm/txm/orchestrator.go new file mode 100644 index 00000000000..8915a534253 --- /dev/null +++ b/core/chains/evm/txm/orchestrator.go @@ -0,0 +1,365 @@ +package txm + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "math" + "math/big" + "strings" + + "github.com/ethereum/go-ethereum/common" + "github.com/google/uuid" + nullv4 "gopkg.in/guregu/null.v4" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/services" + "github.com/smartcontractkit/chainlink-common/pkg/sqlutil" + commontypes "github.com/smartcontractkit/chainlink-common/pkg/types" + "github.com/smartcontractkit/chainlink-common/pkg/utils" + + "github.com/smartcontractkit/chainlink/v2/common/txmgr" + txmgrtypes "github.com/smartcontractkit/chainlink/v2/common/txmgr/types" + "github.com/smartcontractkit/chainlink/v2/common/types" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/forwarders" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas" + txmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/txm/types" + evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" +) + +type OrchestratorTxStore interface { + Add(addresses ...common.Address) error + FetchUnconfirmedTransactionAtNonceWithCount(context.Context, uint64, common.Address) (*txmtypes.Transaction, int, error) + FindTxWithIdempotencyKey(context.Context, *string) (*txmtypes.Transaction, error) +} + +type OrchestratorKeystore interface { + EnabledAddressesForChain(ctx context.Context, chainID *big.Int) (addresses []common.Address, err error) +} + +type OrchestratorAttemptBuilder[ + BLOCK_HASH types.Hashable, + HEAD types.Head[BLOCK_HASH], +] interface { + services.Service + OnNewLongestChain(ctx context.Context, head HEAD) +} + +// Generics are necessary to keep TXMv2 backwards compatible +type Orchestrator[ + BLOCK_HASH types.Hashable, + HEAD types.Head[BLOCK_HASH], +] struct { + services.StateMachine + lggr logger.SugaredLogger + chainID *big.Int + txm *Txm + txStore OrchestratorTxStore + fwdMgr *forwarders.FwdMgr + keystore OrchestratorKeystore + attemptBuilder OrchestratorAttemptBuilder[BLOCK_HASH, HEAD] + resumeCallback txmgr.ResumeCallback +} + +func NewTxmOrchestrator[BLOCK_HASH types.Hashable, HEAD types.Head[BLOCK_HASH]]( + lggr logger.Logger, + chainID *big.Int, + txm *Txm, + txStore OrchestratorTxStore, + fwdMgr *forwarders.FwdMgr, + keystore OrchestratorKeystore, + attemptBuilder OrchestratorAttemptBuilder[BLOCK_HASH, HEAD], +) *Orchestrator[BLOCK_HASH, HEAD] { + return &Orchestrator[BLOCK_HASH, HEAD]{ + lggr: logger.Sugared(logger.Named(lggr, "Orchestrator")), + chainID: chainID, + txm: txm, + txStore: txStore, + keystore: keystore, + attemptBuilder: attemptBuilder, + fwdMgr: fwdMgr, + } +} + +func (o *Orchestrator[BLOCK_HASH, HEAD]) Start(ctx context.Context) error { + return o.StartOnce("Orchestrator", func() error { + var ms services.MultiStart + if err := ms.Start(ctx, o.attemptBuilder); err != nil { + // TODO: hacky fix for DualBroadcast + if !strings.Contains(err.Error(), "already been started once") { + return fmt.Errorf("Orchestrator: AttemptBuilder failed to start: %w", err) + } + } + addresses, err := o.keystore.EnabledAddressesForChain(ctx, o.chainID) + if err != nil { + return err + } + for _, address := range addresses { + err := o.txStore.Add(address) + if err != nil { + return err + } + } + if err := ms.Start(ctx, o.txm); err != nil { + return fmt.Errorf("Orchestrator: Txm failed to start: %w", err) + } + if o.fwdMgr != nil { + if err := ms.Start(ctx, o.fwdMgr); err != nil { + return fmt.Errorf("Orchestrator: ForwarderManager failed to start: %w", err) + } + } + return nil + }) +} + +func (o *Orchestrator[BLOCK_HASH, HEAD]) Close() (merr error) { + return o.StopOnce("Orchestrator", func() error { + if o.fwdMgr != nil { + if err := o.fwdMgr.Close(); err != nil { + merr = errors.Join(merr, fmt.Errorf("Orchestrator failed to stop ForwarderManager: %w", err)) + } + } + if err := o.attemptBuilder.Close(); err != nil { + // TODO: hacky fix for DualBroadcast + if !strings.Contains(err.Error(), "already been stopped") { + merr = errors.Join(merr, fmt.Errorf("Orchestrator failed to stop AttemptBuilder: %w", err)) + } + } + if err := o.txm.Close(); err != nil { + merr = errors.Join(merr, fmt.Errorf("Orchestrator failed to stop Txm: %w", err)) + } + return merr + }) +} + +func (o *Orchestrator[BLOCK_HASH, HEAD]) Trigger(addr common.Address) { + o.txm.Trigger(addr) +} + +func (o *Orchestrator[BLOCK_HASH, HEAD]) Name() string { + return o.lggr.Name() +} + +func (o *Orchestrator[BLOCK_HASH, HEAD]) HealthReport() map[string]error { + return map[string]error{o.Name(): o.Healthy()} +} + +func (o *Orchestrator[BLOCK_HASH, HEAD]) RegisterResumeCallback(fn txmgr.ResumeCallback) { + o.resumeCallback = fn +} + +func (o *Orchestrator[BLOCK_HASH, HEAD]) Reset(addr common.Address, abandon bool) error { + ok := o.IfStarted(func() { + if err := o.txm.Abandon(addr); err != nil { + o.lggr.Error(err) + } + }) + if !ok { + return errors.New("Orchestrator not started yet") + } + return nil +} + +func (o *Orchestrator[BLOCK_HASH, HEAD]) OnNewLongestChain(ctx context.Context, head HEAD) { + ok := o.IfStarted(func() { + o.attemptBuilder.OnNewLongestChain(ctx, head) + }) + if !ok { + o.lggr.Debugw("Not started; ignoring head", "head", head, "state", o.State()) + } +} + +func (o *Orchestrator[BLOCK_HASH, HEAD]) CreateTransaction(ctx context.Context, request txmgrtypes.TxRequest[common.Address, common.Hash]) (tx txmgrtypes.Tx[*big.Int, common.Address, common.Hash, common.Hash, evmtypes.Nonce, gas.EvmFee], err error) { + var wrappedTx *txmtypes.Transaction + wrappedTx, err = o.txStore.FindTxWithIdempotencyKey(ctx, request.IdempotencyKey) + if err != nil { + return + } + + if wrappedTx != nil { + o.lggr.Infof("Found Tx with IdempotencyKey: %v. Returning existing Tx without creating a new one.", *wrappedTx.IdempotencyKey) + } else { + var pipelineTaskRunID uuid.NullUUID + if request.PipelineTaskRunID != nil { + pipelineTaskRunID.UUID = *request.PipelineTaskRunID + pipelineTaskRunID.Valid = true + } + + if o.fwdMgr != nil && (!utils.IsZero(request.ForwarderAddress)) { + fwdPayload, fwdErr := o.fwdMgr.ConvertPayload(request.ToAddress, request.EncodedPayload) + if fwdErr == nil { + // Handling meta not set at caller. + if request.Meta != nil { + request.Meta.FwdrDestAddress = &request.ToAddress + } else { + request.Meta = &txmgrtypes.TxMeta[common.Address, common.Hash]{ + FwdrDestAddress: &request.ToAddress, + } + } + request.ToAddress = request.ForwarderAddress + request.EncodedPayload = fwdPayload + } else { + o.lggr.Errorf("Failed to use forwarder set upstream: %v", fwdErr.Error()) + } + } + + var meta *sqlutil.JSON + if request.Meta != nil { + raw, mErr := json.Marshal(request.Meta) + if mErr != nil { + return tx, mErr + } + m := sqlutil.JSON(raw) + meta = &m + } + + wrappedTxRequest := &txmtypes.TxRequest{ + IdempotencyKey: request.IdempotencyKey, + ChainID: o.chainID, + FromAddress: request.FromAddress, + ToAddress: request.ToAddress, + Value: &request.Value, + Data: request.EncodedPayload, + SpecifiedGasLimit: request.FeeLimit, + Meta: meta, + ForwarderAddress: request.ForwarderAddress, + + PipelineTaskRunID: pipelineTaskRunID, + MinConfirmations: request.MinConfirmations, + SignalCallback: request.SignalCallback, + } + + wrappedTx, err = o.txm.CreateTransaction(ctx, wrappedTxRequest) + if err != nil { + return + } + o.txm.Trigger(request.FromAddress) + } + + if wrappedTx.ID > math.MaxInt64 { + return tx, fmt.Errorf("overflow for int64: %d", wrappedTx.ID) + } + + tx = txmgrtypes.Tx[*big.Int, common.Address, common.Hash, common.Hash, evmtypes.Nonce, gas.EvmFee]{ + //nolint:gosec // disable G115 + ID: int64(wrappedTx.ID), + IdempotencyKey: wrappedTx.IdempotencyKey, + FromAddress: wrappedTx.FromAddress, + ToAddress: wrappedTx.ToAddress, + EncodedPayload: wrappedTx.Data, + Value: *wrappedTx.Value, + FeeLimit: wrappedTx.SpecifiedGasLimit, + CreatedAt: wrappedTx.CreatedAt, + Meta: wrappedTx.Meta, + // Subject: wrappedTx.Subject, + + // TransmitChecker: wrappedTx.TransmitChecker, + ChainID: wrappedTx.ChainID, + + PipelineTaskRunID: wrappedTx.PipelineTaskRunID, + MinConfirmations: wrappedTx.MinConfirmations, + SignalCallback: wrappedTx.SignalCallback, + CallbackCompleted: wrappedTx.CallbackCompleted, + } + return +} + +func (o *Orchestrator[BLOCK_HASH, HEAD]) CountTransactionsByState(ctx context.Context, state txmgrtypes.TxState) (uint32, error) { + addresses, err := o.keystore.EnabledAddressesForChain(ctx, o.chainID) + if err != nil { + return 0, err + } + total := 0 + for _, address := range addresses { + _, count, err := o.txStore.FetchUnconfirmedTransactionAtNonceWithCount(ctx, 0, address) + if err != nil { + return 0, err + } + total += count + } + + //nolint:gosec // disable G115 + return uint32(total), nil +} + +func (o *Orchestrator[BLOCK_HASH, HEAD]) FindEarliestUnconfirmedBroadcastTime(ctx context.Context) (time nullv4.Time, err error) { + return +} + +func (o *Orchestrator[BLOCK_HASH, HEAD]) FindEarliestUnconfirmedTxAttemptBlock(ctx context.Context) (time nullv4.Int, err error) { + return +} + +func (o *Orchestrator[BLOCK_HASH, HEAD]) FindTxesByMetaFieldAndStates(ctx context.Context, metaField string, metaValue string, states []txmgrtypes.TxState, chainID *big.Int) (txs []*txmgrtypes.Tx[*big.Int, common.Address, common.Hash, common.Hash, evmtypes.Nonce, gas.EvmFee], err error) { + return +} + +func (o *Orchestrator[BLOCK_HASH, HEAD]) FindTxesWithMetaFieldByStates(ctx context.Context, metaField string, states []txmgrtypes.TxState, chainID *big.Int) (txs []*txmgrtypes.Tx[*big.Int, common.Address, common.Hash, common.Hash, evmtypes.Nonce, gas.EvmFee], err error) { + return +} + +func (o *Orchestrator[BLOCK_HASH, HEAD]) FindTxesWithMetaFieldByReceiptBlockNum(ctx context.Context, metaField string, blockNum int64, chainID *big.Int) (txs []*txmgrtypes.Tx[*big.Int, common.Address, common.Hash, common.Hash, evmtypes.Nonce, gas.EvmFee], err error) { + return +} + +//nolint:revive // keep API backwards compatible +func (o *Orchestrator[BLOCK_HASH, HEAD]) FindTxesWithAttemptsAndReceiptsByIdsAndState(ctx context.Context, ids []int64, states []txmgrtypes.TxState, chainID *big.Int) (txs []*txmgrtypes.Tx[*big.Int, common.Address, common.Hash, common.Hash, evmtypes.Nonce, gas.EvmFee], err error) { + return +} + +func (o *Orchestrator[BLOCK_HASH, HEAD]) GetForwarderForEOA(ctx context.Context, eoa common.Address) (forwarder common.Address, err error) { + if o.fwdMgr != nil { + forwarder, err = o.fwdMgr.ForwarderFor(ctx, eoa) + } + return +} + +func (o *Orchestrator[BLOCK_HASH, HEAD]) GetForwarderForEOAOCR2Feeds(ctx context.Context, eoa, ocr2AggregatorID common.Address) (forwarder common.Address, err error) { + if o.fwdMgr != nil { + forwarder, err = o.fwdMgr.ForwarderForOCR2Feeds(ctx, eoa, ocr2AggregatorID) + } + return +} + +func (o *Orchestrator[BLOCK_HASH, HEAD]) GetTransactionStatus(ctx context.Context, transactionID string) (status commontypes.TransactionStatus, err error) { + // Loads attempts and receipts in the transaction + tx, err := o.txStore.FindTxWithIdempotencyKey(ctx, &transactionID) + if err != nil || tx == nil { + return status, fmt.Errorf("failed to find transaction with IdempotencyKey %s: %w", transactionID, err) + } + + switch tx.State { + case txmtypes.TxUnconfirmed: + return commontypes.Pending, nil + case txmtypes.TxConfirmed: + // Return unconfirmed for confirmed transactions because they are not yet finalized + return commontypes.Unconfirmed, nil + case txmtypes.TxFinalized: + return commontypes.Finalized, nil + case txmtypes.TxFatalError: + return commontypes.Fatal, nil + default: + return commontypes.Unknown, nil + } +} + +func (o *Orchestrator[BLOCK_HASH, HEAD]) SendNativeToken(ctx context.Context, chainID *big.Int, from, to common.Address, value big.Int, gasLimit uint64) (tx txmgrtypes.Tx[*big.Int, common.Address, common.Hash, common.Hash, evmtypes.Nonce, gas.EvmFee], err error) { + txRequest := txmgrtypes.TxRequest[common.Address, common.Hash]{ + FromAddress: from, + ToAddress: to, + EncodedPayload: []byte{}, + Value: value, + FeeLimit: gasLimit, + //Strategy: NewSendEveryStrategy(), + } + + tx, err = o.CreateTransaction(ctx, txRequest) + if err != nil { + return + } + + // Trigger the Txm to check for new transaction + o.txm.Trigger(from) + return tx, err +} diff --git a/core/chains/evm/txm/storage/inmemory_store.go b/core/chains/evm/txm/storage/inmemory_store.go new file mode 100644 index 00000000000..857f3656ddd --- /dev/null +++ b/core/chains/evm/txm/storage/inmemory_store.go @@ -0,0 +1,324 @@ +package storage + +import ( + "errors" + "fmt" + "math/big" + "sort" + "sync" + "time" + + "github.com/ethereum/go-ethereum/common" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/txm/types" +) + +const ( + maxQueuedTransactions = 250 + pruneSubset = 3 +) + +type InMemoryStore struct { + sync.RWMutex + lggr logger.Logger + txIDCount uint64 + address common.Address + chainID *big.Int + + UnstartedTransactions []*types.Transaction + UnconfirmedTransactions map[uint64]*types.Transaction + ConfirmedTransactions map[uint64]*types.Transaction + FatalTransactions []*types.Transaction + + Transactions map[uint64]*types.Transaction +} + +func NewInMemoryStore(lggr logger.Logger, address common.Address, chainID *big.Int) *InMemoryStore { + return &InMemoryStore{ + lggr: logger.Named(lggr, "InMemoryStore"), + address: address, + chainID: chainID, + UnconfirmedTransactions: make(map[uint64]*types.Transaction), + ConfirmedTransactions: make(map[uint64]*types.Transaction), + Transactions: make(map[uint64]*types.Transaction), + } +} + +func (m *InMemoryStore) AbandonPendingTransactions() { + m.Lock() + defer m.Unlock() + + for _, tx := range m.UnstartedTransactions { + tx.State = types.TxFatalError + } + m.FatalTransactions = m.UnstartedTransactions + m.UnstartedTransactions = []*types.Transaction{} + + for _, tx := range m.UnconfirmedTransactions { + tx.State = types.TxFatalError + m.FatalTransactions = append(m.FatalTransactions, tx) + } + m.UnconfirmedTransactions = make(map[uint64]*types.Transaction) +} + +func (m *InMemoryStore) AppendAttemptToTransaction(txNonce uint64, attempt *types.Attempt) error { + m.Lock() + defer m.Unlock() + + tx, exists := m.UnconfirmedTransactions[txNonce] + if !exists { + return fmt.Errorf("unconfirmed tx was not found for nonce: %d - txID: %v", txNonce, attempt.TxID) + } + + if tx.ID != attempt.TxID { + return fmt.Errorf("unconfirmed tx with nonce exists but attempt points to a different txID. Found Tx: %v - txID: %v", m.UnconfirmedTransactions[txNonce], attempt.TxID) + } + + attempt.CreatedAt = time.Now() + attempt.ID = uint64(len(tx.Attempts)) // Attempts are not collectively tracked by the in-memory store so attemptIDs are not unique between transactions and can be reused. + tx.AttemptCount++ + m.UnconfirmedTransactions[txNonce].Attempts = append(m.UnconfirmedTransactions[txNonce].Attempts, attempt.DeepCopy()) + + return nil +} + +func (m *InMemoryStore) CountUnstartedTransactions() int { + m.RLock() + defer m.RUnlock() + + return len(m.UnstartedTransactions) +} + +func (m *InMemoryStore) CreateEmptyUnconfirmedTransaction(nonce uint64, gasLimit uint64) (*types.Transaction, error) { + m.Lock() + defer m.Unlock() + + m.txIDCount++ + emptyTx := &types.Transaction{ + ID: m.txIDCount, + ChainID: m.chainID, + Nonce: nonce, + FromAddress: m.address, + ToAddress: common.Address{}, + Value: big.NewInt(0), + SpecifiedGasLimit: gasLimit, + CreatedAt: time.Now(), + State: types.TxUnconfirmed, + } + + if _, exists := m.UnconfirmedTransactions[nonce]; exists { + return nil, fmt.Errorf("an unconfirmed tx with the same nonce already exists: %v", m.UnconfirmedTransactions[nonce]) + } + + m.UnconfirmedTransactions[nonce] = emptyTx + m.Transactions[emptyTx.ID] = emptyTx + + return emptyTx.DeepCopy(), nil +} + +func (m *InMemoryStore) CreateTransaction(txRequest *types.TxRequest) *types.Transaction { + m.Lock() + defer m.Unlock() + + m.txIDCount++ + + tx := &types.Transaction{ + ID: m.txIDCount, + IdempotencyKey: txRequest.IdempotencyKey, + ChainID: m.chainID, + FromAddress: m.address, + ToAddress: txRequest.ToAddress, + Value: txRequest.Value, + Data: txRequest.Data, + SpecifiedGasLimit: txRequest.SpecifiedGasLimit, + CreatedAt: time.Now(), + State: types.TxUnstarted, + Meta: txRequest.Meta, + MinConfirmations: txRequest.MinConfirmations, + PipelineTaskRunID: txRequest.PipelineTaskRunID, + SignalCallback: txRequest.SignalCallback, + } + + if len(m.UnstartedTransactions) == maxQueuedTransactions { + m.lggr.Warnf("Unstarted transactions queue for address: %v reached max limit of: %d. Dropping oldest transaction: %v.", + m.address, maxQueuedTransactions, m.UnstartedTransactions[0]) + delete(m.Transactions, m.UnstartedTransactions[0].ID) + m.UnstartedTransactions = m.UnstartedTransactions[1:maxQueuedTransactions] + } + + txCopy := tx.DeepCopy() + m.Transactions[txCopy.ID] = txCopy + m.UnstartedTransactions = append(m.UnstartedTransactions, txCopy) + return tx +} + +func (m *InMemoryStore) FetchUnconfirmedTransactionAtNonceWithCount(latestNonce uint64) (txCopy *types.Transaction, unconfirmedCount int) { + m.RLock() + defer m.RUnlock() + + tx := m.UnconfirmedTransactions[latestNonce] + if tx != nil { + txCopy = tx.DeepCopy() + } + unconfirmedCount = len(m.UnconfirmedTransactions) + return +} + +func (m *InMemoryStore) MarkTransactionsConfirmed(latestNonce uint64) ([]uint64, []uint64) { + m.Lock() + defer m.Unlock() + + var confirmedTransactionIDs []uint64 + for _, tx := range m.UnconfirmedTransactions { + if tx.Nonce < latestNonce { + tx.State = types.TxConfirmed + confirmedTransactionIDs = append(confirmedTransactionIDs, tx.ID) + m.ConfirmedTransactions[tx.Nonce] = tx + delete(m.UnconfirmedTransactions, tx.Nonce) + } + } + + var unconfirmedTransactionIDs []uint64 + for _, tx := range m.ConfirmedTransactions { + if tx.Nonce >= latestNonce { + tx.State = types.TxUnconfirmed + tx.LastBroadcastAt = time.Time{} // Mark reorged transaction as if it wasn't broadcasted before + unconfirmedTransactionIDs = append(unconfirmedTransactionIDs, tx.ID) + m.UnconfirmedTransactions[tx.Nonce] = tx + delete(m.ConfirmedTransactions, tx.Nonce) + } + } + + if len(m.ConfirmedTransactions) >= maxQueuedTransactions { + prunedTxIDs := m.pruneConfirmedTransactions() + m.lggr.Debugf("Confirmed transactions map for address: %v reached max limit of: %d. Pruned 1/3 of the oldest confirmed transactions. TxIDs: %v", + m.address, maxQueuedTransactions, prunedTxIDs) + } + sort.Slice(confirmedTransactionIDs, func(i, j int) bool { return confirmedTransactionIDs[i] < confirmedTransactionIDs[j] }) + sort.Slice(unconfirmedTransactionIDs, func(i, j int) bool { return unconfirmedTransactionIDs[i] < unconfirmedTransactionIDs[j] }) + return confirmedTransactionIDs, unconfirmedTransactionIDs +} + +func (m *InMemoryStore) MarkUnconfirmedTransactionPurgeable(nonce uint64) error { + m.Lock() + defer m.Unlock() + + tx, exists := m.UnconfirmedTransactions[nonce] + if !exists { + return fmt.Errorf("unconfirmed tx with nonce: %d was not found", nonce) + } + + tx.IsPurgeable = true + + return nil +} + +func (m *InMemoryStore) UpdateTransactionBroadcast(txID uint64, txNonce uint64, attemptHash common.Hash) error { + m.Lock() + defer m.Unlock() + + unconfirmedTx, exists := m.UnconfirmedTransactions[txNonce] + if !exists { + return fmt.Errorf("unconfirmed tx was not found for nonce: %d - txID: %v", txNonce, txID) + } + + // Set the same time for both the tx and its attempt + now := time.Now() + unconfirmedTx.LastBroadcastAt = now + a, err := unconfirmedTx.FindAttemptByHash(attemptHash) + if err != nil { + return err + } + a.BroadcastAt = now + + return nil +} + +func (m *InMemoryStore) UpdateUnstartedTransactionWithNonce(nonce uint64) (*types.Transaction, error) { + m.Lock() + defer m.Unlock() + + if len(m.UnstartedTransactions) == 0 { + m.lggr.Debugf("Unstarted transactions queue is empty for address: %v", m.address) + return nil, nil + } + + if _, exists := m.UnconfirmedTransactions[nonce]; exists { + return nil, fmt.Errorf("an unconfirmed tx with the same nonce already exists: %v", m.UnconfirmedTransactions[nonce]) + } + + tx := m.UnstartedTransactions[0] + tx.Nonce = nonce + tx.State = types.TxUnconfirmed + + m.UnstartedTransactions = m.UnstartedTransactions[1:] + m.UnconfirmedTransactions[nonce] = tx + + return tx.DeepCopy(), nil +} + +// Shouldn't call lock because it's being called by a method that already has the lock +func (m *InMemoryStore) pruneConfirmedTransactions() []uint64 { + noncesToPrune := make([]uint64, 0, len(m.ConfirmedTransactions)) + for nonce := range m.ConfirmedTransactions { + noncesToPrune = append(noncesToPrune, nonce) + } + if len(noncesToPrune) == 0 { + return nil + } + sort.Slice(noncesToPrune, func(i, j int) bool { return noncesToPrune[i] < noncesToPrune[j] }) + minNonce := noncesToPrune[len(noncesToPrune)/pruneSubset] + + var txIDsToPrune []uint64 + for nonce, tx := range m.ConfirmedTransactions { + if nonce < minNonce { + txIDsToPrune = append(txIDsToPrune, tx.ID) + delete(m.Transactions, tx.ID) + delete(m.ConfirmedTransactions, nonce) + } + } + + sort.Slice(txIDsToPrune, func(i, j int) bool { return txIDsToPrune[i] < txIDsToPrune[j] }) + return txIDsToPrune +} + +// Error Handler +func (m *InMemoryStore) DeleteAttemptForUnconfirmedTx(transactionNonce uint64, attempt *types.Attempt) error { + m.Lock() + defer m.Unlock() + + tx, exists := m.UnconfirmedTransactions[transactionNonce] + if !exists { + return fmt.Errorf("unconfirmed tx was not found for nonce: %d - txID: %v", transactionNonce, attempt.TxID) + } + + for i, a := range tx.Attempts { + if a.Hash == attempt.Hash { + tx.Attempts = append(tx.Attempts[:i], tx.Attempts[i+1:]...) + return nil + } + } + + return fmt.Errorf("attempt with hash: %v for txID: %v was not found", attempt.Hash, attempt.TxID) +} + +func (m *InMemoryStore) MarkTxFatal(*types.Transaction) error { + return errors.New("not implemented") +} + +// Orchestrator +func (m *InMemoryStore) FindTxWithIdempotencyKey(idempotencyKey *string) *types.Transaction { + m.Lock() + defer m.Unlock() + + if idempotencyKey != nil { + for _, tx := range m.Transactions { + if tx.IdempotencyKey != nil && tx.IdempotencyKey == idempotencyKey { + return tx.DeepCopy() + } + } + } + + return nil +} diff --git a/core/chains/evm/txm/storage/inmemory_store_manager.go b/core/chains/evm/txm/storage/inmemory_store_manager.go new file mode 100644 index 00000000000..dfb777ab22b --- /dev/null +++ b/core/chains/evm/txm/storage/inmemory_store_manager.go @@ -0,0 +1,135 @@ +package storage + +import ( + "context" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/common" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/txm/types" +) + +const StoreNotFoundForAddress string = "InMemoryStore for address: %v not found" + +type InMemoryStoreManager struct { + lggr logger.Logger + chainID *big.Int + InMemoryStoreMap map[common.Address]*InMemoryStore +} + +func NewInMemoryStoreManager(lggr logger.Logger, chainID *big.Int) *InMemoryStoreManager { + inMemoryStoreMap := make(map[common.Address]*InMemoryStore) + return &InMemoryStoreManager{ + lggr: lggr, + chainID: chainID, + InMemoryStoreMap: inMemoryStoreMap} +} + +func (m *InMemoryStoreManager) AbandonPendingTransactions(_ context.Context, fromAddress common.Address) error { + if store, exists := m.InMemoryStoreMap[fromAddress]; exists { + store.AbandonPendingTransactions() + return nil + } + return fmt.Errorf(StoreNotFoundForAddress, fromAddress) +} + +func (m *InMemoryStoreManager) Add(addresses ...common.Address) error { + for _, address := range addresses { + if _, exists := m.InMemoryStoreMap[address]; exists { + return fmt.Errorf("address %v already exists in store manager", address) + } + m.InMemoryStoreMap[address] = NewInMemoryStore(m.lggr, address, m.chainID) + } + return nil +} + +func (m *InMemoryStoreManager) AppendAttemptToTransaction(_ context.Context, txNonce uint64, fromAddress common.Address, attempt *types.Attempt) error { + if store, exists := m.InMemoryStoreMap[fromAddress]; exists { + return store.AppendAttemptToTransaction(txNonce, attempt) + } + return fmt.Errorf(StoreNotFoundForAddress, fromAddress) +} + +func (m *InMemoryStoreManager) CountUnstartedTransactions(fromAddress common.Address) (int, error) { + if store, exists := m.InMemoryStoreMap[fromAddress]; exists { + return store.CountUnstartedTransactions(), nil + } + return 0, fmt.Errorf(StoreNotFoundForAddress, fromAddress) +} + +func (m *InMemoryStoreManager) CreateEmptyUnconfirmedTransaction(_ context.Context, fromAddress common.Address, nonce uint64, gasLimit uint64) (*types.Transaction, error) { + if store, exists := m.InMemoryStoreMap[fromAddress]; exists { + return store.CreateEmptyUnconfirmedTransaction(nonce, gasLimit) + } + return nil, fmt.Errorf(StoreNotFoundForAddress, fromAddress) +} + +func (m *InMemoryStoreManager) CreateTransaction(_ context.Context, txRequest *types.TxRequest) (*types.Transaction, error) { + if store, exists := m.InMemoryStoreMap[txRequest.FromAddress]; exists { + return store.CreateTransaction(txRequest), nil + } + return nil, fmt.Errorf(StoreNotFoundForAddress, txRequest.FromAddress) +} + +func (m *InMemoryStoreManager) FetchUnconfirmedTransactionAtNonceWithCount(_ context.Context, nonce uint64, fromAddress common.Address) (tx *types.Transaction, count int, err error) { + if store, exists := m.InMemoryStoreMap[fromAddress]; exists { + tx, count = store.FetchUnconfirmedTransactionAtNonceWithCount(nonce) + return + } + return nil, 0, fmt.Errorf(StoreNotFoundForAddress, fromAddress) +} + +func (m *InMemoryStoreManager) MarkTransactionsConfirmed(_ context.Context, nonce uint64, fromAddress common.Address) (confirmedTxIDs []uint64, unconfirmedTxIDs []uint64, err error) { + if store, exists := m.InMemoryStoreMap[fromAddress]; exists { + confirmedTxIDs, unconfirmedTxIDs = store.MarkTransactionsConfirmed(nonce) + return + } + return nil, nil, fmt.Errorf(StoreNotFoundForAddress, fromAddress) +} + +func (m *InMemoryStoreManager) MarkUnconfirmedTransactionPurgeable(_ context.Context, nonce uint64, fromAddress common.Address) error { + if store, exists := m.InMemoryStoreMap[fromAddress]; exists { + return store.MarkUnconfirmedTransactionPurgeable(nonce) + } + return fmt.Errorf(StoreNotFoundForAddress, fromAddress) +} + +func (m *InMemoryStoreManager) UpdateTransactionBroadcast(_ context.Context, txID uint64, nonce uint64, attemptHash common.Hash, fromAddress common.Address) error { + if store, exists := m.InMemoryStoreMap[fromAddress]; exists { + return store.UpdateTransactionBroadcast(txID, nonce, attemptHash) + } + return fmt.Errorf(StoreNotFoundForAddress, fromAddress) +} + +func (m *InMemoryStoreManager) UpdateUnstartedTransactionWithNonce(_ context.Context, fromAddress common.Address, nonce uint64) (*types.Transaction, error) { + if store, exists := m.InMemoryStoreMap[fromAddress]; exists { + return store.UpdateUnstartedTransactionWithNonce(nonce) + } + return nil, fmt.Errorf(StoreNotFoundForAddress, fromAddress) +} + +func (m *InMemoryStoreManager) DeleteAttemptForUnconfirmedTx(_ context.Context, nonce uint64, attempt *types.Attempt, fromAddress common.Address) error { + if store, exists := m.InMemoryStoreMap[fromAddress]; exists { + return store.DeleteAttemptForUnconfirmedTx(nonce, attempt) + } + return fmt.Errorf(StoreNotFoundForAddress, fromAddress) +} + +func (m *InMemoryStoreManager) MarkTxFatal(_ context.Context, tx *types.Transaction, fromAddress common.Address) error { + if store, exists := m.InMemoryStoreMap[fromAddress]; exists { + return store.MarkTxFatal(tx) + } + return fmt.Errorf(StoreNotFoundForAddress, fromAddress) +} + +func (m *InMemoryStoreManager) FindTxWithIdempotencyKey(_ context.Context, idempotencyKey *string) (*types.Transaction, error) { + for _, store := range m.InMemoryStoreMap { + tx := store.FindTxWithIdempotencyKey(idempotencyKey) + if tx != nil { + return tx, nil + } + } + return nil, nil +} diff --git a/core/chains/evm/txm/storage/inmemory_store_manager_test.go b/core/chains/evm/txm/storage/inmemory_store_manager_test.go new file mode 100644 index 00000000000..e10870a9942 --- /dev/null +++ b/core/chains/evm/txm/storage/inmemory_store_manager_test.go @@ -0,0 +1,35 @@ +package storage + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/assert" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/testutils" +) + +func TestAdd(t *testing.T) { + t.Parallel() + + fromAddress := testutils.NewAddress() + m := NewInMemoryStoreManager(logger.Test(t), testutils.FixtureChainID) + // Adds a new address + err := m.Add(fromAddress) + assert.NoError(t, err) + assert.Len(t, m.InMemoryStoreMap, 1) + + // Fails if address exists + err = m.Add(fromAddress) + assert.Error(t, err) + + // Adds multiple addresses + fromAddress1 := testutils.NewAddress() + fromAddress2 := testutils.NewAddress() + addresses := []common.Address{fromAddress1, fromAddress2} + err = m.Add(addresses...) + assert.NoError(t, err) + assert.Len(t, m.InMemoryStoreMap, 3) +} diff --git a/core/chains/evm/txm/storage/inmemory_store_test.go b/core/chains/evm/txm/storage/inmemory_store_test.go new file mode 100644 index 00000000000..68c791cb400 --- /dev/null +++ b/core/chains/evm/txm/storage/inmemory_store_test.go @@ -0,0 +1,476 @@ +package storage + +import ( + "fmt" + "math/big" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/testutils" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/txm/types" +) + +func TestAbandonPendingTransactions(t *testing.T) { + t.Parallel() + + fromAddress := testutils.NewAddress() + m := NewInMemoryStore(logger.Test(t), fromAddress, testutils.FixtureChainID) + t.Run("abandons unstarted and unconfirmed transactions", func(t *testing.T) { + // Unstarted + tx1 := insertUnstartedTransaction(m) + tx2 := insertUnstartedTransaction(m) + + // Unconfirmed + tx3, err := insertUnconfirmedTransaction(m, 3) + assert.NoError(t, err) + tx4, err := insertUnconfirmedTransaction(m, 4) + assert.NoError(t, err) + + m.AbandonPendingTransactions() + + assert.Equal(t, types.TxFatalError, tx1.State) + assert.Equal(t, types.TxFatalError, tx2.State) + assert.Equal(t, types.TxFatalError, tx3.State) + assert.Equal(t, types.TxFatalError, tx4.State) + }) + + t.Run("skips all types apart from unstarted and unconfirmed transactions", func(t *testing.T) { + // Fatal + tx1 := insertFataTransaction(m) + tx2 := insertFataTransaction(m) + + // Confirmed + tx3, err := insertConfirmedTransaction(m, 3) + assert.NoError(t, err) + tx4, err := insertConfirmedTransaction(m, 4) + assert.NoError(t, err) + + m.AbandonPendingTransactions() + + assert.Equal(t, types.TxFatalError, tx1.State) + assert.Equal(t, types.TxFatalError, tx2.State) + assert.Equal(t, types.TxConfirmed, tx3.State) + assert.Equal(t, types.TxConfirmed, tx4.State) + }) +} + +func TestAppendAttemptToTransaction(t *testing.T) { + t.Parallel() + + fromAddress := testutils.NewAddress() + m := NewInMemoryStore(logger.Test(t), fromAddress, testutils.FixtureChainID) + + _, err := insertUnconfirmedTransaction(m, 0) // txID = 1 + assert.NoError(t, err) + _, err = insertConfirmedTransaction(m, 2) // txID = 1 + assert.NoError(t, err) + + t.Run("fails if corresponding unconfirmed transaction for attempt was not found", func(t *testing.T) { + var nonce uint64 = 1 + newAttempt := &types.Attempt{ + TxID: 1, + } + assert.Error(t, m.AppendAttemptToTransaction(nonce, newAttempt)) + }) + + t.Run("fails if unconfirmed transaction was found but has doesn't match the txID", func(t *testing.T) { + var nonce uint64 + newAttempt := &types.Attempt{ + TxID: 2, + } + assert.Error(t, m.AppendAttemptToTransaction(nonce, newAttempt)) + }) + + t.Run("appends attempt to transaction", func(t *testing.T) { + var nonce uint64 + newAttempt := &types.Attempt{ + TxID: 1, + } + assert.NoError(t, m.AppendAttemptToTransaction(nonce, newAttempt)) + }) +} + +func TestCountUnstartedTransactions(t *testing.T) { + t.Parallel() + + fromAddress := testutils.NewAddress() + m := NewInMemoryStore(logger.Test(t), fromAddress, testutils.FixtureChainID) + + assert.Equal(t, 0, m.CountUnstartedTransactions()) + + insertUnstartedTransaction(m) + assert.Equal(t, 1, m.CountUnstartedTransactions()) +} + +func TestCreateEmptyUnconfirmedTransaction(t *testing.T) { + t.Parallel() + + fromAddress := testutils.NewAddress() + m := NewInMemoryStore(logger.Test(t), fromAddress, testutils.FixtureChainID) + _, err := insertUnconfirmedTransaction(m, 0) + assert.NoError(t, err) + + t.Run("fails if unconfirmed transaction with the same nonce exists", func(t *testing.T) { + _, err := m.CreateEmptyUnconfirmedTransaction(0, 0) + assert.Error(t, err) + }) + + t.Run("creates a new empty unconfirmed transaction", func(t *testing.T) { + tx, err := m.CreateEmptyUnconfirmedTransaction(1, 0) + assert.NoError(t, err) + assert.Equal(t, types.TxUnconfirmed, tx.State) + }) +} + +func TestCreateTransaction(t *testing.T) { + t.Parallel() + + fromAddress := testutils.NewAddress() + + t.Run("creates new transactions", func(t *testing.T) { + m := NewInMemoryStore(logger.Test(t), fromAddress, testutils.FixtureChainID) + now := time.Now() + txR1 := &types.TxRequest{} + txR2 := &types.TxRequest{} + tx1 := m.CreateTransaction(txR1) + assert.Equal(t, uint64(1), tx1.ID) + assert.LessOrEqual(t, now, tx1.CreatedAt) + + tx2 := m.CreateTransaction(txR2) + assert.Equal(t, uint64(2), tx2.ID) + assert.LessOrEqual(t, now, tx2.CreatedAt) + + assert.Equal(t, 2, m.CountUnstartedTransactions()) + }) + + t.Run("prunes oldest unstarted transactions if limit is reached", func(t *testing.T) { + m := NewInMemoryStore(logger.Test(t), fromAddress, testutils.FixtureChainID) + overshot := 5 + for i := 1; i < maxQueuedTransactions+overshot; i++ { + r := &types.TxRequest{} + tx := m.CreateTransaction(r) + //nolint:gosec // this won't overflow + assert.Equal(t, uint64(i), tx.ID) + } + // total shouldn't exceed maxQueuedTransactions + assert.Equal(t, maxQueuedTransactions, m.CountUnstartedTransactions()) + // earliest tx ID should be the same amount of the number of transactions that we dropped + tx, err := m.UpdateUnstartedTransactionWithNonce(0) + assert.NoError(t, err) + //nolint:gosec // this won't overflow + assert.Equal(t, uint64(overshot), tx.ID) + }) +} + +func TestFetchUnconfirmedTransactionAtNonceWithCount(t *testing.T) { + t.Parallel() + + fromAddress := testutils.NewAddress() + m := NewInMemoryStore(logger.Test(t), fromAddress, testutils.FixtureChainID) + + tx, count := m.FetchUnconfirmedTransactionAtNonceWithCount(0) + assert.Nil(t, tx) + assert.Equal(t, 0, count) + + var nonce uint64 + _, err := insertUnconfirmedTransaction(m, nonce) + assert.NoError(t, err) + tx, count = m.FetchUnconfirmedTransactionAtNonceWithCount(0) + assert.Equal(t, tx.Nonce, nonce) + assert.Equal(t, 1, count) +} + +func TestMarkTransactionsConfirmed(t *testing.T) { + t.Parallel() + + fromAddress := testutils.NewAddress() + + t.Run("returns 0 if there are no transactions", func(t *testing.T) { + m := NewInMemoryStore(logger.Test(t), fromAddress, testutils.FixtureChainID) + un, cn := m.MarkTransactionsConfirmed(100) + assert.Empty(t, un) + assert.Empty(t, cn) + }) + + t.Run("confirms transaction with nonce lower than the latest", func(t *testing.T) { + m := NewInMemoryStore(logger.Test(t), fromAddress, testutils.FixtureChainID) + ctx1, err := insertUnconfirmedTransaction(m, 0) + assert.NoError(t, err) + + ctx2, err := insertUnconfirmedTransaction(m, 1) + assert.NoError(t, err) + + ctxs, utxs := m.MarkTransactionsConfirmed(1) + assert.NoError(t, err) + assert.Equal(t, types.TxConfirmed, ctx1.State) + assert.Equal(t, types.TxUnconfirmed, ctx2.State) + assert.Equal(t, ctxs[0], ctx1.ID) + assert.Empty(t, utxs) + }) + + t.Run("unconfirms transaction with nonce equal to or higher than the latest", func(t *testing.T) { + m := NewInMemoryStore(logger.Test(t), fromAddress, testutils.FixtureChainID) + ctx1, err := insertConfirmedTransaction(m, 0) + assert.NoError(t, err) + + ctx2, err := insertConfirmedTransaction(m, 1) + assert.NoError(t, err) + + ctxs, utxs := m.MarkTransactionsConfirmed(1) + assert.NoError(t, err) + assert.Equal(t, types.TxConfirmed, ctx1.State) + assert.Equal(t, types.TxUnconfirmed, ctx2.State) + assert.Equal(t, utxs[0], ctx2.ID) + assert.Empty(t, ctxs) + }) + t.Run("prunes confirmed transactions map if it reaches the limit", func(t *testing.T) { + m := NewInMemoryStore(logger.Test(t), fromAddress, testutils.FixtureChainID) + for i := 0; i < maxQueuedTransactions; i++ { + //nolint:gosec // this won't overflow + _, err := insertConfirmedTransaction(m, uint64(i)) + assert.NoError(t, err) + } + assert.Len(t, m.ConfirmedTransactions, maxQueuedTransactions) + m.MarkTransactionsConfirmed(maxQueuedTransactions) + assert.Len(t, m.ConfirmedTransactions, (maxQueuedTransactions - maxQueuedTransactions/pruneSubset)) + }) +} + +func TestMarkUnconfirmedTransactionPurgeable(t *testing.T) { + t.Parallel() + + fromAddress := testutils.NewAddress() + m := NewInMemoryStore(logger.Test(t), fromAddress, testutils.FixtureChainID) + + // fails if tx was not found + err := m.MarkUnconfirmedTransactionPurgeable(0) + assert.Error(t, err) + + tx, err := insertUnconfirmedTransaction(m, 0) + assert.NoError(t, err) + err = m.MarkUnconfirmedTransactionPurgeable(0) + assert.NoError(t, err) + assert.True(t, tx.IsPurgeable) +} + +func TestUpdateTransactionBroadcast(t *testing.T) { + t.Parallel() + + fromAddress := testutils.NewAddress() + hash := testutils.NewHash() + t.Run("fails if unconfirmed transaction was not found", func(t *testing.T) { + m := NewInMemoryStore(logger.Test(t), fromAddress, testutils.FixtureChainID) + var nonce uint64 + assert.Error(t, m.UpdateTransactionBroadcast(0, nonce, hash)) + }) + + t.Run("fails if attempt was not found for a given transaction", func(t *testing.T) { + m := NewInMemoryStore(logger.Test(t), fromAddress, testutils.FixtureChainID) + var nonce uint64 + tx, err := insertUnconfirmedTransaction(m, nonce) + assert.NoError(t, err) + assert.Error(t, m.UpdateTransactionBroadcast(0, nonce, hash)) + + // Attempt with different hash + attempt := &types.Attempt{TxID: tx.ID, Hash: testutils.NewHash()} + tx.Attempts = append(tx.Attempts, attempt) + assert.Error(t, m.UpdateTransactionBroadcast(0, nonce, hash)) + }) + + t.Run("updates transaction's and attempt's broadcast times", func(t *testing.T) { + m := NewInMemoryStore(logger.Test(t), fromAddress, testutils.FixtureChainID) + var nonce uint64 + tx, err := insertUnconfirmedTransaction(m, nonce) + assert.NoError(t, err) + attempt := &types.Attempt{TxID: tx.ID, Hash: hash} + tx.Attempts = append(tx.Attempts, attempt) + assert.NoError(t, m.UpdateTransactionBroadcast(0, nonce, hash)) + assert.False(t, tx.LastBroadcastAt.IsZero()) + assert.False(t, attempt.BroadcastAt.IsZero()) + }) +} + +func TestUpdateUnstartedTransactionWithNonce(t *testing.T) { + t.Parallel() + + fromAddress := testutils.NewAddress() + t.Run("returns nil if there are no unstarted transactions", func(t *testing.T) { + m := NewInMemoryStore(logger.Test(t), fromAddress, testutils.FixtureChainID) + tx, err := m.UpdateUnstartedTransactionWithNonce(0) + assert.NoError(t, err) + assert.Nil(t, tx) + }) + + t.Run("fails if there is already another unstarted transaction with the same nonce", func(t *testing.T) { + var nonce uint64 + m := NewInMemoryStore(logger.Test(t), fromAddress, testutils.FixtureChainID) + insertUnstartedTransaction(m) + _, err := insertUnconfirmedTransaction(m, nonce) + assert.NoError(t, err) + + _, err = m.UpdateUnstartedTransactionWithNonce(nonce) + assert.Error(t, err) + }) + + t.Run("updates unstarted transaction to unconfirmed and assigns a nonce", func(t *testing.T) { + var nonce uint64 + m := NewInMemoryStore(logger.Test(t), fromAddress, testutils.FixtureChainID) + insertUnstartedTransaction(m) + + tx, err := m.UpdateUnstartedTransactionWithNonce(nonce) + assert.NoError(t, err) + assert.Equal(t, nonce, tx.Nonce) + assert.Equal(t, types.TxUnconfirmed, tx.State) + }) +} + +func TestDeleteAttemptForUnconfirmedTx(t *testing.T) { + t.Parallel() + + fromAddress := testutils.NewAddress() + t.Run("fails if corresponding unconfirmed transaction for attempt was not found", func(t *testing.T) { + m := NewInMemoryStore(logger.Test(t), fromAddress, testutils.FixtureChainID) + tx := &types.Transaction{Nonce: 0} + attempt := &types.Attempt{TxID: 0} + err := m.DeleteAttemptForUnconfirmedTx(tx.Nonce, attempt) + assert.Error(t, err) + }) + + t.Run("fails if corresponding unconfirmed attempt for txID was not found", func(t *testing.T) { + m := NewInMemoryStore(logger.Test(t), fromAddress, testutils.FixtureChainID) + _, err := insertUnconfirmedTransaction(m, 0) + assert.NoError(t, err) + + attempt := &types.Attempt{TxID: 2, Hash: testutils.NewHash()} + err = m.DeleteAttemptForUnconfirmedTx(0, attempt) + + assert.Error(t, err) + }) + + t.Run("deletes attempt of unconfirmed transaction", func(t *testing.T) { + hash := testutils.NewHash() + var nonce uint64 + m := NewInMemoryStore(logger.Test(t), fromAddress, testutils.FixtureChainID) + tx, err := insertUnconfirmedTransaction(m, nonce) + assert.NoError(t, err) + + attempt := &types.Attempt{TxID: 0, Hash: hash} + tx.Attempts = append(tx.Attempts, attempt) + err = m.DeleteAttemptForUnconfirmedTx(nonce, attempt) + assert.NoError(t, err) + + assert.Empty(t, tx.Attempts) + }) +} + +func TestPruneConfirmedTransactions(t *testing.T) { + t.Parallel() + fromAddress := testutils.NewAddress() + m := NewInMemoryStore(logger.Test(t), fromAddress, testutils.FixtureChainID) + total := 5 + for i := 0; i < total; i++ { + //nolint:gosec // this won't overflow + _, err := insertConfirmedTransaction(m, uint64(i)) + assert.NoError(t, err) + } + prunedTxIDs := m.pruneConfirmedTransactions() + left := total - total/pruneSubset + assert.Len(t, m.ConfirmedTransactions, left) + assert.Len(t, prunedTxIDs, total/pruneSubset) +} + +func insertUnstartedTransaction(m *InMemoryStore) *types.Transaction { + m.Lock() + defer m.Unlock() + + m.txIDCount++ + tx := &types.Transaction{ + ID: m.txIDCount, + ChainID: testutils.FixtureChainID, + Nonce: 0, + FromAddress: m.address, + ToAddress: testutils.NewAddress(), + Value: big.NewInt(0), + SpecifiedGasLimit: 0, + CreatedAt: time.Now(), + State: types.TxUnstarted, + } + + m.UnstartedTransactions = append(m.UnstartedTransactions, tx) + return tx +} + +func insertUnconfirmedTransaction(m *InMemoryStore, nonce uint64) (*types.Transaction, error) { + m.Lock() + defer m.Unlock() + + m.txIDCount++ + tx := &types.Transaction{ + ID: m.txIDCount, + ChainID: testutils.FixtureChainID, + Nonce: nonce, + FromAddress: m.address, + ToAddress: testutils.NewAddress(), + Value: big.NewInt(0), + SpecifiedGasLimit: 0, + CreatedAt: time.Now(), + State: types.TxUnconfirmed, + } + + if _, exists := m.UnconfirmedTransactions[nonce]; exists { + return nil, fmt.Errorf("an unconfirmed tx with the same nonce already exists: %v", m.UnconfirmedTransactions[nonce]) + } + + m.UnconfirmedTransactions[nonce] = tx + return tx, nil +} + +func insertConfirmedTransaction(m *InMemoryStore, nonce uint64) (*types.Transaction, error) { + m.Lock() + defer m.Unlock() + + m.txIDCount++ + tx := &types.Transaction{ + ID: m.txIDCount, + ChainID: testutils.FixtureChainID, + Nonce: nonce, + FromAddress: m.address, + ToAddress: testutils.NewAddress(), + Value: big.NewInt(0), + SpecifiedGasLimit: 0, + CreatedAt: time.Now(), + State: types.TxConfirmed, + } + + if _, exists := m.ConfirmedTransactions[nonce]; exists { + return nil, fmt.Errorf("a confirmed tx with the same nonce already exists: %v", m.ConfirmedTransactions[nonce]) + } + + m.ConfirmedTransactions[nonce] = tx + return tx, nil +} + +func insertFataTransaction(m *InMemoryStore) *types.Transaction { + m.Lock() + defer m.Unlock() + + m.txIDCount++ + tx := &types.Transaction{ + ID: m.txIDCount, + ChainID: testutils.FixtureChainID, + Nonce: 0, + FromAddress: m.address, + ToAddress: testutils.NewAddress(), + Value: big.NewInt(0), + SpecifiedGasLimit: 0, + CreatedAt: time.Now(), + State: types.TxFatalError, + } + + m.FatalTransactions = append(m.FatalTransactions, tx) + return tx +} diff --git a/core/chains/evm/txm/stuck_tx_detector.go b/core/chains/evm/txm/stuck_tx_detector.go new file mode 100644 index 00000000000..68d8caf0ed1 --- /dev/null +++ b/core/chains/evm/txm/stuck_tx_detector.go @@ -0,0 +1,115 @@ +package txm + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "time" + + "github.com/ethereum/go-ethereum/common" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/config/chaintype" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/txm/types" +) + +type StuckTxDetectorConfig struct { + BlockTime time.Duration + StuckTxBlockThreshold uint32 + DetectionURL string +} + +type stuckTxDetector struct { + lggr logger.Logger + chainType chaintype.ChainType + config StuckTxDetectorConfig +} + +func NewStuckTxDetector(lggr logger.Logger, chaintype chaintype.ChainType, config StuckTxDetectorConfig) *stuckTxDetector { + return &stuckTxDetector{ + lggr: lggr, + chainType: chaintype, + config: config, + } +} + +func (s *stuckTxDetector) DetectStuckTransaction(ctx context.Context, tx *types.Transaction) (bool, error) { + switch s.chainType { + // TODO: rename + case chaintype.ChainDualBroadcast: + result, err := s.dualBroadcastDetection(ctx, tx) + if result || err != nil { + return result, err + } + return s.timeBasedDetection(tx), nil + default: + return s.timeBasedDetection(tx), nil + } +} + +func (s *stuckTxDetector) timeBasedDetection(tx *types.Transaction) bool { + threshold := (s.config.BlockTime * time.Duration(s.config.StuckTxBlockThreshold)) + if time.Since(tx.LastBroadcastAt) > threshold && !tx.LastBroadcastAt.IsZero() { + s.lggr.Debugf("TxID: %v last broadcast was: %v which is more than the max configured duration: %v. Transaction is now considered stuck and will be purged.", + tx.ID, tx.LastBroadcastAt, threshold) + return true + } + return false +} + +type APIResponse struct { + Status string `json:"status,omitempty"` + Hash common.Hash `json:"hash,omitempty"` +} + +const ( + APIStatusPending = "PENDING" + APIStatusIncluded = "INCLUDED" + APIStatusFailed = "FAILED" + APIStatusCancelled = "CANCELLED" + APIStatusUnknown = "UNKNOWN" +) + +func (s *stuckTxDetector) dualBroadcastDetection(ctx context.Context, tx *types.Transaction) (bool, error) { + for _, attempt := range tx.Attempts { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, s.config.DetectionURL+attempt.Hash.String(), nil) + if err != nil { + return false, fmt.Errorf("failed to make request for txID: %v, attemptHash: %v - %w", tx.ID, attempt.Hash, err) + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + return false, fmt.Errorf("failed to get transaction status for txID: %v, attemptHash: %v - %w", tx.ID, attempt.Hash, err) + } + if resp.StatusCode != http.StatusOK { + resp.Body.Close() + return false, fmt.Errorf("request %v failed with status: %d", req, resp.StatusCode) + } + body, err := io.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + return false, err + } + + var apiResponse APIResponse + err = json.Unmarshal(body, &apiResponse) + if err != nil { + return false, fmt.Errorf("failed to unmarshal response for txID: %v, attemptHash: %v - %w: %s", tx.ID, attempt.Hash, err, string(body)) + } + switch apiResponse.Status { + case APIStatusPending, APIStatusIncluded: + return false, nil + case APIStatusFailed, APIStatusCancelled: + s.lggr.Debugf("TxID: %v with attempHash: %v was marked as failed/cancelled by the RPC. Transaction is now considered stuck and will be purged.", + tx.ID, attempt.Hash) + return true, nil + case APIStatusUnknown: + continue + default: + continue + } + } + return false, nil +} diff --git a/core/chains/evm/txm/txm.go b/core/chains/evm/txm/txm.go new file mode 100644 index 00000000000..042744cb218 --- /dev/null +++ b/core/chains/evm/txm/txm.go @@ -0,0 +1,395 @@ +package txm + +import ( + "context" + "fmt" + "math/big" + "sync" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/jpillora/backoff" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/services" + "github.com/smartcontractkit/chainlink-common/pkg/utils" + + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/txm/types" +) + +const ( + broadcastInterval time.Duration = 30 * time.Second + maxInFlightTransactions int = 16 + maxInFlightSubset int = 3 + maxAllowedAttempts uint16 = 10 +) + +type Client interface { + PendingNonceAt(context.Context, common.Address) (uint64, error) + NonceAt(context.Context, common.Address, *big.Int) (uint64, error) + SendTransaction(ctx context.Context, tx *types.Transaction, attempt *types.Attempt) error +} + +type TxStore interface { + AbandonPendingTransactions(context.Context, common.Address) error + AppendAttemptToTransaction(context.Context, uint64, common.Address, *types.Attempt) error + CreateEmptyUnconfirmedTransaction(context.Context, common.Address, uint64, uint64) (*types.Transaction, error) + CreateTransaction(context.Context, *types.TxRequest) (*types.Transaction, error) + FetchUnconfirmedTransactionAtNonceWithCount(context.Context, uint64, common.Address) (*types.Transaction, int, error) + MarkTransactionsConfirmed(context.Context, uint64, common.Address) ([]uint64, []uint64, error) + MarkUnconfirmedTransactionPurgeable(context.Context, uint64, common.Address) error + UpdateTransactionBroadcast(context.Context, uint64, uint64, common.Hash, common.Address) error + UpdateUnstartedTransactionWithNonce(context.Context, common.Address, uint64) (*types.Transaction, error) + + // ErrorHandler + DeleteAttemptForUnconfirmedTx(context.Context, uint64, *types.Attempt, common.Address) error + MarkTxFatal(context.Context, *types.Transaction, common.Address) error +} + +type AttemptBuilder interface { + NewAttempt(context.Context, logger.Logger, *types.Transaction, bool) (*types.Attempt, error) + NewBumpAttempt(context.Context, logger.Logger, *types.Transaction, types.Attempt) (*types.Attempt, error) +} + +type ErrorHandler interface { + HandleError(*types.Transaction, error, AttemptBuilder, Client, TxStore, func(common.Address, uint64), bool) (err error) +} + +type StuckTxDetector interface { + DetectStuckTransaction(ctx context.Context, tx *types.Transaction) (bool, error) +} + +type Keystore interface { + EnabledAddressesForChain(ctx context.Context, chainID *big.Int) (addresses []common.Address, err error) +} + +type Config struct { + EIP1559 bool + BlockTime time.Duration + RetryBlockThreshold uint16 + EmptyTxLimitDefault uint64 +} + +type Txm struct { + services.StateMachine + lggr logger.SugaredLogger + chainID *big.Int + client Client + attemptBuilder AttemptBuilder + errorHandler ErrorHandler + stuckTxDetector StuckTxDetector + txStore TxStore + keystore Keystore + config Config + + nonceMapMu sync.Mutex + nonceMap map[common.Address]uint64 + + triggerCh map[common.Address]chan struct{} + stopCh services.StopChan + wg sync.WaitGroup +} + +func NewTxm(lggr logger.Logger, chainID *big.Int, client Client, attemptBuilder AttemptBuilder, txStore TxStore, stuckTxDetector StuckTxDetector, config Config, keystore Keystore) *Txm { + return &Txm{ + lggr: logger.Sugared(logger.Named(lggr, "Txm")), + keystore: keystore, + chainID: chainID, + client: client, + attemptBuilder: attemptBuilder, + txStore: txStore, + stuckTxDetector: stuckTxDetector, + config: config, + nonceMap: make(map[common.Address]uint64), + triggerCh: make(map[common.Address]chan struct{}), + } +} + +func (t *Txm) Start(ctx context.Context) error { + return t.StartOnce("Txm", func() error { + t.stopCh = make(chan struct{}) + + addresses, err := t.keystore.EnabledAddressesForChain(ctx, t.chainID) + if err != nil { + return err + } + for _, address := range addresses { + err := t.startAddress(address) + if err != nil { + return err + } + } + return nil + }) +} + +func (t *Txm) startAddress(address common.Address) error { + t.triggerCh[address] = make(chan struct{}, 1) + pendingNonce, err := t.client.PendingNonceAt(context.TODO(), address) + if err != nil { + return err + } + t.setNonce(address, pendingNonce) + + t.wg.Add(2) + go t.broadcastLoop(address) + go t.backfillLoop(address) + return nil +} + +func (t *Txm) Close() error { + return t.StopOnce("Txm", func() error { + close(t.stopCh) + t.wg.Wait() + return nil + }) +} + +func (t *Txm) CreateTransaction(ctx context.Context, txRequest *types.TxRequest) (tx *types.Transaction, err error) { + tx, err = t.txStore.CreateTransaction(ctx, txRequest) + if err == nil { + t.lggr.Infow("Created transaction", "tx", tx) + } + return +} + +func (t *Txm) Trigger(address common.Address) { + if !t.IfStarted(func() { + t.triggerCh[address] <- struct{}{} + }) { + t.lggr.Error("Txm unstarted") + } +} + +func (t *Txm) Abandon(address common.Address) error { + return t.txStore.AbandonPendingTransactions(context.TODO(), address) +} + +func (t *Txm) getNonce(address common.Address) uint64 { + t.nonceMapMu.Lock() + defer t.nonceMapMu.Unlock() + return t.nonceMap[address] +} + +func (t *Txm) setNonce(address common.Address, nonce uint64) { + t.nonceMapMu.Lock() + t.nonceMap[address] = nonce + defer t.nonceMapMu.Unlock() +} + +func newBackoff(min time.Duration) backoff.Backoff { + return backoff.Backoff{ + Min: min, + Max: 1 * time.Minute, + Jitter: true, + } +} + +func (t *Txm) broadcastLoop(address common.Address) { + defer t.wg.Done() + ctx, cancel := t.stopCh.NewCtx() + defer cancel() + broadcastWithBackoff := newBackoff(1 * time.Second) + var broadcastCh <-chan time.Time + + for { + start := time.Now() + bo, err := t.broadcastTransaction(ctx, address) + if err != nil { + t.lggr.Errorf("Error during transaction broadcasting: %v", err) + } else { + t.lggr.Debug("Transaction broadcasting time elapsed: ", time.Since(start)) + } + if bo { + broadcastCh = time.After(broadcastWithBackoff.Duration()) + } else { + broadcastWithBackoff.Reset() + broadcastCh = time.After(utils.WithJitter(broadcastInterval)) + } + select { + case <-ctx.Done(): + return + case <-t.triggerCh[address]: + continue + case <-broadcastCh: + continue + } + } +} + +func (t *Txm) backfillLoop(address common.Address) { + defer t.wg.Done() + ctx, cancel := t.stopCh.NewCtx() + defer cancel() + backfillWithBackoff := newBackoff(t.config.BlockTime) + backfillCh := time.After(utils.WithJitter(t.config.BlockTime)) + + for { + select { + case <-ctx.Done(): + return + case <-backfillCh: + start := time.Now() + bo, err := t.backfillTransactions(ctx, address) + if err != nil { + t.lggr.Errorf("Error during backfill: %v", err) + } else { + t.lggr.Debug("Backfill time elapsed: ", time.Since(start)) + } + if bo { + backfillCh = time.After(backfillWithBackoff.Duration()) + } else { + backfillWithBackoff.Reset() + backfillCh = time.After(utils.WithJitter(t.config.BlockTime)) + } + } + } +} + +func (t *Txm) broadcastTransaction(ctx context.Context, address common.Address) (bool, error) { + for { + _, unconfirmedCount, err := t.txStore.FetchUnconfirmedTransactionAtNonceWithCount(ctx, 0, address) + if err != nil { + return false, err + } + + // Optimistically send up to 1/3 of the maxInFlightTransactions. After that threshold, broadcast more cautiously + // by checking the pending nonce so no more than maxInFlightTransactions/3 can get stuck simultaneously i.e. due + // to insufficient balance. We're making this trade-off to avoid storing stuck transactions and making unnecessary + // RPC calls. The upper limit is always maxInFlightTransactions regardless of the pending nonce. + if unconfirmedCount >= maxInFlightTransactions/maxInFlightSubset { + if unconfirmedCount > maxInFlightTransactions { + t.lggr.Warnf("Reached transaction limit: %d for unconfirmed transactions", maxInFlightTransactions) + return true, nil + } + pendingNonce, e := t.client.PendingNonceAt(ctx, address) + if e != nil { + return false, e + } + nonce := t.getNonce(address) + if nonce > pendingNonce { + t.lggr.Warnf("Reached transaction limit. LocalNonce: %d, PendingNonce %d, unconfirmedCount: %d", + nonce, pendingNonce, unconfirmedCount) + return true, nil + } + } + + tx, err := t.txStore.UpdateUnstartedTransactionWithNonce(ctx, address, t.getNonce(address)) + if err != nil { + return false, err + } + if tx == nil { + return false, nil + } + tx.Nonce = t.getNonce(address) + t.setNonce(address, tx.Nonce+1) + tx.State = types.TxUnconfirmed + + if err := t.createAndSendAttempt(ctx, tx, address); err != nil { + return true, err + } + } +} + +func (t *Txm) createAndSendAttempt(ctx context.Context, tx *types.Transaction, address common.Address) error { + attempt, err := t.attemptBuilder.NewAttempt(ctx, t.lggr, tx, t.config.EIP1559) + if err != nil { + return err + } + + if err = t.txStore.AppendAttemptToTransaction(ctx, tx.Nonce, address, attempt); err != nil { + return err + } + + return t.sendTransactionWithError(ctx, tx, attempt, address) +} + +func (t *Txm) sendTransactionWithError(ctx context.Context, tx *types.Transaction, attempt *types.Attempt, address common.Address) (err error) { + start := time.Now() + txErr := t.client.SendTransaction(ctx, tx, attempt) + tx.AttemptCount++ + t.lggr.Infow("Broadcasted attempt", "tx", tx, "attempt", attempt, "duration", time.Since(start), "txErr: ", txErr) + if txErr != nil && t.errorHandler != nil { + if err = t.errorHandler.HandleError(tx, txErr, t.attemptBuilder, t.client, t.txStore, t.setNonce, false); err != nil { + return + } + } else if txErr != nil { + pendingNonce, err := t.client.PendingNonceAt(ctx, address) + if err != nil { + return err + } + if pendingNonce <= tx.Nonce { + t.lggr.Debugf("Pending nonce for txID: %v didn't increase. PendingNonce: %d, TxNonce: %d", tx.ID, pendingNonce, tx.Nonce) + return nil + } + } + + return t.txStore.UpdateTransactionBroadcast(ctx, attempt.TxID, tx.Nonce, attempt.Hash, address) +} + +func (t *Txm) backfillTransactions(ctx context.Context, address common.Address) (bool, error) { + latestNonce, err := t.client.NonceAt(ctx, address, nil) + if err != nil { + return false, err + } + + confirmedTransactionIDs, unconfirmedTransactionIDs, err := t.txStore.MarkTransactionsConfirmed(ctx, latestNonce, address) + if err != nil { + return false, err + } + if len(confirmedTransactionIDs) > 0 || len(unconfirmedTransactionIDs) > 0 { + t.lggr.Infof("Confirmed transaction IDs: %v . Re-orged transaction IDs: %v", confirmedTransactionIDs, unconfirmedTransactionIDs) + } + + tx, unconfirmedCount, err := t.txStore.FetchUnconfirmedTransactionAtNonceWithCount(ctx, latestNonce, address) + if err != nil { + return false, err + } + if unconfirmedCount == 0 { + t.lggr.Debugf("All transactions confirmed for address: %v", address) + return false, err // TODO: add backoff to optimize requests + } + + if tx == nil || tx.Nonce != latestNonce { + t.lggr.Warnf("Nonce gap at nonce: %d - address: %v. Creating a new transaction\n", latestNonce, address) + return false, t.createAndSendEmptyTx(ctx, latestNonce, address) + //nolint:revive //linter nonsense + } else { + if !tx.IsPurgeable && t.stuckTxDetector != nil { + isStuck, err := t.stuckTxDetector.DetectStuckTransaction(ctx, tx) + if err != nil { + return false, err + } + if isStuck { + tx.IsPurgeable = true + err = t.txStore.MarkUnconfirmedTransactionPurgeable(ctx, tx.Nonce, address) + if err != nil { + return false, err + } + t.lggr.Infof("Marked tx as purgeable. Sending purge attempt for txID: %d", tx.ID) + return false, t.createAndSendAttempt(ctx, tx, address) + } + } + + if tx.AttemptCount >= maxAllowedAttempts { + return true, fmt.Errorf("reached max allowed attempts for txID: %d. TXM won't broadcast any more attempts."+ + "If this error persists, it means the transaction won't be confirmed and the TXM needs to be restarted."+ + "Look for any error messages from previous attempts that may indicate why this happened, i.e. wallet is out of funds. Tx: %v", tx.ID, tx) + } + + if time.Since(tx.LastBroadcastAt) > (t.config.BlockTime*time.Duration(t.config.RetryBlockThreshold)) || tx.LastBroadcastAt.IsZero() { + // TODO: add optional graceful bumping strategy + t.lggr.Info("Rebroadcasting attempt for txID: ", tx.ID) + return false, t.createAndSendAttempt(ctx, tx, address) + } + } + return false, nil +} + +func (t *Txm) createAndSendEmptyTx(ctx context.Context, latestNonce uint64, address common.Address) error { + tx, err := t.txStore.CreateEmptyUnconfirmedTransaction(ctx, address, latestNonce, t.config.EmptyTxLimitDefault) + if err != nil { + return err + } + return t.createAndSendAttempt(ctx, tx, address) +} diff --git a/core/chains/evm/txm/txm_test.go b/core/chains/evm/txm/txm_test.go new file mode 100644 index 00000000000..ddd75d92693 --- /dev/null +++ b/core/chains/evm/txm/txm_test.go @@ -0,0 +1,230 @@ +package txm + +import ( + "errors" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "go.uber.org/zap" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/services/servicetest" + "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" + + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/testutils" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/txm/mocks" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/txm/storage" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/txm/types" +) + +func TestLifecycle(t *testing.T) { + t.Parallel() + + client := mocks.NewClient(t) + ab := mocks.NewAttemptBuilder(t) + config := Config{BlockTime: 10 * time.Millisecond} + address1 := testutils.NewAddress() + address2 := testutils.NewAddress() + assert.NotEqual(t, address1, address2) + addresses := []common.Address{address1, address2} + keystore := mocks.NewKeystore(t) + keystore.On("EnabledAddressesForChain", mock.Anything, mock.Anything).Return(addresses, nil) + + t.Run("fails to start if initial pending nonce call fails", func(t *testing.T) { + txm := NewTxm(logger.Test(t), testutils.FixtureChainID, client, ab, nil, nil, config, keystore) + client.On("PendingNonceAt", mock.Anything, address1).Return(uint64(0), errors.New("error")).Once() + require.Error(t, txm.Start(tests.Context(t))) + }) + + t.Run("tests lifecycle successfully without any transactions", func(t *testing.T) { + lggr, observedLogs := logger.TestObserved(t, zap.DebugLevel) + txStore := storage.NewInMemoryStoreManager(lggr, testutils.FixtureChainID) + require.NoError(t, txStore.Add(addresses...)) + txm := NewTxm(lggr, testutils.FixtureChainID, client, ab, txStore, nil, config, keystore) + var nonce uint64 + // Start + client.On("PendingNonceAt", mock.Anything, address1).Return(nonce, nil).Once() + client.On("PendingNonceAt", mock.Anything, address2).Return(nonce, nil).Once() + // backfill loop (may or may not be executed multiple times) + client.On("NonceAt", mock.Anything, address1, mock.Anything).Return(nonce, nil) + client.On("NonceAt", mock.Anything, address2, mock.Anything).Return(nonce, nil) + + servicetest.Run(t, txm) + tests.AssertLogEventually(t, observedLogs, "Backfill time elapsed") + }) +} + +func TestTrigger(t *testing.T) { + t.Parallel() + + address := testutils.NewAddress() + keystore := mocks.NewKeystore(t) + keystore.On("EnabledAddressesForChain", mock.Anything, mock.Anything).Return([]common.Address{address}, nil) + t.Run("Trigger fails if Txm is unstarted", func(t *testing.T) { + lggr, observedLogs := logger.TestObserved(t, zap.ErrorLevel) + txm := NewTxm(lggr, nil, nil, nil, nil, nil, Config{}, keystore) + txm.Trigger(address) + tests.AssertLogEventually(t, observedLogs, "Txm unstarted") + }) + + t.Run("executes Trigger", func(t *testing.T) { + lggr := logger.Test(t) + txStore := storage.NewInMemoryStoreManager(lggr, testutils.FixtureChainID) + require.NoError(t, txStore.Add(address)) + client := mocks.NewClient(t) + ab := mocks.NewAttemptBuilder(t) + config := Config{BlockTime: 1 * time.Minute, RetryBlockThreshold: 10} + txm := NewTxm(lggr, testutils.FixtureChainID, client, ab, txStore, nil, config, keystore) + var nonce uint64 + // Start + client.On("PendingNonceAt", mock.Anything, address).Return(nonce, nil).Once() + servicetest.Run(t, txm) + }) +} + +func TestBroadcastTransaction(t *testing.T) { + t.Parallel() + + ctx := tests.Context(t) + client := mocks.NewClient(t) + ab := mocks.NewAttemptBuilder(t) + config := Config{} + address := testutils.NewAddress() + keystore := mocks.NewKeystore(t) + + t.Run("fails if FetchUnconfirmedTransactionAtNonceWithCount for unconfirmed transactions fails", func(t *testing.T) { + mTxStore := mocks.NewTxStore(t) + mTxStore.On("FetchUnconfirmedTransactionAtNonceWithCount", mock.Anything, mock.Anything, mock.Anything).Return(nil, 0, errors.New("call failed")).Once() + txm := NewTxm(logger.Test(t), testutils.FixtureChainID, client, ab, mTxStore, nil, config, keystore) + bo, err := txm.broadcastTransaction(ctx, address) + require.Error(t, err) + assert.False(t, bo) + require.ErrorContains(t, err, "call failed") + }) + + t.Run("throws a warning and returns if unconfirmed transactions exceed maxInFlightTransactions", func(t *testing.T) { + lggr, observedLogs := logger.TestObserved(t, zap.DebugLevel) + mTxStore := mocks.NewTxStore(t) + mTxStore.On("FetchUnconfirmedTransactionAtNonceWithCount", mock.Anything, mock.Anything, mock.Anything).Return(nil, maxInFlightTransactions+1, nil).Once() + txm := NewTxm(lggr, testutils.FixtureChainID, client, ab, mTxStore, nil, config, keystore) + bo, err := txm.broadcastTransaction(ctx, address) + assert.True(t, bo) + require.NoError(t, err) + tests.AssertLogEventually(t, observedLogs, "Reached transaction limit") + }) + + t.Run("checks pending nonce if unconfirmed transactions are more than 1/3 of maxInFlightTransactions", func(t *testing.T) { + lggr, observedLogs := logger.TestObserved(t, zap.DebugLevel) + mTxStore := mocks.NewTxStore(t) + txm := NewTxm(lggr, testutils.FixtureChainID, client, ab, mTxStore, nil, config, keystore) + txm.setNonce(address, 1) + mTxStore.On("FetchUnconfirmedTransactionAtNonceWithCount", mock.Anything, mock.Anything, mock.Anything).Return(nil, maxInFlightTransactions/3, nil).Twice() + + client.On("PendingNonceAt", mock.Anything, address).Return(uint64(0), nil).Once() // LocalNonce: 1, PendingNonce: 0 + bo, err := txm.broadcastTransaction(ctx, address) + assert.True(t, bo) + require.NoError(t, err) + + client.On("PendingNonceAt", mock.Anything, address).Return(uint64(1), nil).Once() // LocalNonce: 1, PendingNonce: 1 + mTxStore.On("UpdateUnstartedTransactionWithNonce", mock.Anything, mock.Anything, mock.Anything).Return(nil, nil).Once() + bo, err = txm.broadcastTransaction(ctx, address) + assert.False(t, bo) + require.NoError(t, err) + tests.AssertLogCountEventually(t, observedLogs, "Reached transaction limit.", 1) + }) + + t.Run("fails if UpdateUnstartedTransactionWithNonce fails", func(t *testing.T) { + mTxStore := mocks.NewTxStore(t) + mTxStore.On("FetchUnconfirmedTransactionAtNonceWithCount", mock.Anything, mock.Anything, mock.Anything).Return(nil, 0, nil).Once() + txm := NewTxm(logger.Test(t), testutils.FixtureChainID, client, ab, mTxStore, nil, config, keystore) + mTxStore.On("UpdateUnstartedTransactionWithNonce", mock.Anything, mock.Anything, mock.Anything).Return(nil, errors.New("call failed")).Once() + bo, err := txm.broadcastTransaction(ctx, address) + assert.False(t, bo) + require.Error(t, err) + require.ErrorContains(t, err, "call failed") + }) + + t.Run("returns if there are no unstarted transactions", func(t *testing.T) { + lggr := logger.Test(t) + txStore := storage.NewInMemoryStoreManager(lggr, testutils.FixtureChainID) + require.NoError(t, txStore.Add(address)) + txm := NewTxm(lggr, testutils.FixtureChainID, client, ab, txStore, nil, config, keystore) + bo, err := txm.broadcastTransaction(ctx, address) + require.NoError(t, err) + assert.False(t, bo) + assert.Equal(t, uint64(0), txm.getNonce(address)) + }) + + t.Run("picks a new tx and creates a new attempt then sends it and updates the broadcast time", func(t *testing.T) { + lggr := logger.Test(t) + txStore := storage.NewInMemoryStoreManager(lggr, testutils.FixtureChainID) + require.NoError(t, txStore.Add(address)) + txm := NewTxm(lggr, testutils.FixtureChainID, client, ab, txStore, nil, config, keystore) + txm.setNonce(address, 8) + IDK := "IDK" + txRequest := &types.TxRequest{ + IdempotencyKey: &IDK, + ChainID: testutils.FixtureChainID, + FromAddress: address, + ToAddress: testutils.NewAddress(), + SpecifiedGasLimit: 22000, + } + tx, err := txm.CreateTransaction(tests.Context(t), txRequest) + require.NoError(t, err) + attempt := &types.Attempt{ + TxID: tx.ID, + Fee: gas.EvmFee{GasPrice: assets.NewWeiI(1)}, + GasLimit: 22000, + } + ab.On("NewAttempt", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(attempt, nil).Once() + client.On("SendTransaction", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() + + bo, err := txm.broadcastTransaction(ctx, address) + require.NoError(t, err) + assert.False(t, bo) + assert.Equal(t, uint64(9), txm.getNonce(address)) + tx, err = txStore.FindTxWithIdempotencyKey(tests.Context(t), &IDK) + require.NoError(t, err) + assert.Len(t, tx.Attempts, 1) + var zeroTime time.Time + assert.Greater(t, tx.LastBroadcastAt, zeroTime) + assert.Greater(t, tx.Attempts[0].BroadcastAt, zeroTime) + }) +} + +func TestBackfillTransactions(t *testing.T) { + t.Parallel() + + ctx := tests.Context(t) + client := mocks.NewClient(t) + ab := mocks.NewAttemptBuilder(t) + storage := mocks.NewTxStore(t) + config := Config{} + address := testutils.NewAddress() + keystore := mocks.NewKeystore(t) + + t.Run("fails if latest nonce fetching fails", func(t *testing.T) { + txm := NewTxm(logger.Test(t), testutils.FixtureChainID, client, ab, storage, nil, config, keystore) + client.On("NonceAt", mock.Anything, address, mock.Anything).Return(uint64(0), errors.New("latest nonce fail")).Once() + bo, err := txm.backfillTransactions(ctx, address) + require.Error(t, err) + assert.False(t, bo) + require.ErrorContains(t, err, "latest nonce fail") + }) + + t.Run("fails if MarkTransactionsConfirmed fails", func(t *testing.T) { + txm := NewTxm(logger.Test(t), testutils.FixtureChainID, client, ab, storage, nil, config, keystore) + client.On("NonceAt", mock.Anything, address, mock.Anything).Return(uint64(0), nil).Once() + storage.On("MarkTransactionsConfirmed", mock.Anything, mock.Anything, address).Return([]uint64{}, []uint64{}, errors.New("marking transactions confirmed failed")).Once() + bo, err := txm.backfillTransactions(ctx, address) + require.Error(t, err) + assert.False(t, bo) + require.ErrorContains(t, err, "marking transactions confirmed failed") + }) +} diff --git a/core/chains/evm/txm/types/transaction.go b/core/chains/evm/txm/types/transaction.go new file mode 100644 index 00000000000..62d788861ad --- /dev/null +++ b/core/chains/evm/txm/types/transaction.go @@ -0,0 +1,170 @@ +package types + +import ( + "encoding/json" + "fmt" + "math/big" + "time" + + "github.com/google/uuid" + "gopkg.in/guregu/null.v4" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + + "github.com/smartcontractkit/chainlink-common/pkg/sqlutil" + clnull "github.com/smartcontractkit/chainlink-common/pkg/utils/null" + + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas" +) + +type TxState string + +const ( + TxUnstarted = TxState("unstarted") + TxUnconfirmed = TxState("unconfirmed") + TxConfirmed = TxState("confirmed") + + TxFatalError = TxState("fatal") + TxFinalized = TxState("finalized") +) + +type Transaction struct { + ID uint64 + IdempotencyKey *string + ChainID *big.Int + Nonce uint64 + FromAddress common.Address + ToAddress common.Address + Value *big.Int + Data []byte + SpecifiedGasLimit uint64 + + CreatedAt time.Time + LastBroadcastAt time.Time + + State TxState + IsPurgeable bool + Attempts []*Attempt + AttemptCount uint16 // AttempCount is strictly kept in memory and prevents indefinite retrying + Meta *sqlutil.JSON + Subject uuid.NullUUID + + // Pipeline variables - if you aren't calling this from chain tx task within + // the pipeline, you don't need these variables + PipelineTaskRunID uuid.NullUUID + MinConfirmations clnull.Uint32 + SignalCallback bool + CallbackCompleted bool +} + +func (t *Transaction) FindAttemptByHash(attemptHash common.Hash) (*Attempt, error) { + for _, a := range t.Attempts { + if a.Hash == attemptHash { + return a, nil + } + } + return nil, fmt.Errorf("attempt with hash: %v was not found", attemptHash) +} + +func (t *Transaction) DeepCopy() *Transaction { + txCopy := *t + attemptsCopy := make([]*Attempt, 0, len(t.Attempts)) + for _, attempt := range t.Attempts { + attemptsCopy = append(attemptsCopy, attempt.DeepCopy()) + } + txCopy.Attempts = attemptsCopy + return &txCopy +} + +func (t *Transaction) GetMeta() (*TxMeta, error) { + if t.Meta == nil { + return nil, nil + } + var m TxMeta + if err := json.Unmarshal(*t.Meta, &m); err != nil { + return nil, fmt.Errorf("unmarshalling meta: %w", err) + } + return &m, nil +} + +type Attempt struct { + ID uint64 + TxID uint64 + Hash common.Hash + Fee gas.EvmFee + GasLimit uint64 + Type byte + SignedTransaction *types.Transaction + + CreatedAt time.Time + BroadcastAt time.Time +} + +func (a *Attempt) DeepCopy() *Attempt { + txCopy := *a + if a.SignedTransaction != nil { + txCopy.SignedTransaction = a.SignedTransaction.WithoutBlobTxSidecar() + } + return &txCopy +} + +type TxRequest struct { + IdempotencyKey *string + ChainID *big.Int + FromAddress common.Address + ToAddress common.Address + Value *big.Int + Data []byte + SpecifiedGasLimit uint64 + + Meta *sqlutil.JSON // TODO: *TxMeta after migration + ForwarderAddress common.Address + // QueueingTxStrategy QueueingTxStrategy + + // Pipeline variables - if you aren't calling this from chain tx task within + // the pipeline, you don't need these variables + PipelineTaskRunID uuid.NullUUID + MinConfirmations clnull.Uint32 + SignalCallback bool +} + +type TxMeta struct { + // Pipeline + JobID *int32 `json:"JobID,omitempty"` + FailOnRevert null.Bool `json:"FailOnRevert,omitempty"` + + // VRF + RequestID *common.Hash `json:"RequestID,omitempty"` + RequestTxHash *common.Hash `json:"RequestTxHash,omitempty"` + RequestIDs []common.Hash `json:"RequestIDs,omitempty"` + RequestTxHashes []common.Hash `json:"RequestTxHashes,omitempty"` + MaxLink *string `json:"MaxLink,omitempty"` + SubID *uint64 `json:"SubId,omitempty"` + GlobalSubID *string `json:"GlobalSubId,omitempty"` + MaxEth *string `json:"MaxEth,omitempty"` + ForceFulfilled *bool `json:"ForceFulfilled,omitempty"` + ForceFulfillmentAttempt *uint64 `json:"ForceFulfillmentAttempt,omitempty"` + + // Used for keepers + UpkeepID *string `json:"UpkeepID,omitempty"` + + // Used for Keystone Workflows + WorkflowExecutionID *string `json:"WorkflowExecutionID,omitempty"` + + // Forwarders + FwdrDestAddress *common.Address `json:"ForwarderDestAddress,omitempty"` + + // CCIP + MessageIDs []string `json:"MessageIDs,omitempty"` + SeqNumbers []uint64 `json:"SeqNumbers,omitempty"` + + // Dual Broadcast + DualBroadcast *bool `json:"DualBroadcast,omitempty"` + DualBroadcastParams *string `json:"DualBroadcastParams,omitempty"` +} + +type QueueingTxStrategy struct { + QueueSize uint32 + Subject uuid.NullUUID +} diff --git a/core/chains/evm/txmgr/builder.go b/core/chains/evm/txmgr/builder.go index cbfb8775cfb..2bf583c0afb 100644 --- a/core/chains/evm/txmgr/builder.go +++ b/core/chains/evm/txmgr/builder.go @@ -16,6 +16,9 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/keystore" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/txm" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/txm/clientwrappers" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/txm/storage" evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" ) @@ -38,6 +41,7 @@ func NewTxm( keyStore keystore.Eth, estimator gas.EvmFeeEstimator, headTracker latestAndFinalizedBlockHeadTracker, + txmv2wrapper TxManager, ) (txm TxManager, err error, ) { @@ -65,7 +69,7 @@ func NewTxm( if txConfig.ResendAfterThreshold() > 0 { evmResender = NewEvmResender(lggr, txStore, txmClient, evmTracker, keyStore, txmgr.DefaultResenderPollInterval, chainConfig, txConfig) } - txm = NewEvmTxm(chainID, txmCfg, txConfig, keyStore, lggr, checker, fwdMgr, txAttemptBuilder, txStore, evmBroadcaster, evmConfirmer, evmResender, evmTracker, evmFinalizer) + txm = NewEvmTxm(chainID, txmCfg, txConfig, keyStore, lggr, checker, fwdMgr, txAttemptBuilder, txStore, evmBroadcaster, evmConfirmer, evmResender, evmTracker, evmFinalizer, txmv2wrapper) return txm, nil } @@ -85,8 +89,59 @@ func NewEvmTxm( resender *Resender, tracker *Tracker, finalizer Finalizer, + txmv2wrapper TxManager, ) *Txm { - return txmgr.NewTxm(chainId, cfg, txCfg, keyStore, lggr, checkerFactory, fwdMgr, txAttemptBuilder, txStore, broadcaster, confirmer, resender, tracker, finalizer, client.NewTxError) + return txmgr.NewTxm(chainId, cfg, txCfg, keyStore, lggr, checkerFactory, fwdMgr, txAttemptBuilder, txStore, broadcaster, confirmer, resender, tracker, finalizer, client.NewTxError, txmv2wrapper) +} + +func NewTxmV2( + ds sqlutil.DataSource, + chainConfig ChainConfig, + fCfg FeeConfig, + txConfig config.Transactions, + txmV2Config config.TxmV2, + client client.Client, + lggr logger.Logger, + logPoller logpoller.LogPoller, + keyStore keystore.Eth, + estimator gas.EvmFeeEstimator, +) (TxManager, error) { + var fwdMgr *forwarders.FwdMgr + if txConfig.ForwardersEnabled() { + fwdMgr = forwarders.NewFwdMgr(ds, client, logPoller, lggr, chainConfig) + } else { + lggr.Info("ForwarderManager: Disabled") + } + + chainID := client.ConfiguredChainID() + + var stuckTxDetector txm.StuckTxDetector + if txConfig.AutoPurge().Enabled() { + stuckTxDetectorConfig := txm.StuckTxDetectorConfig{ + BlockTime: *txmV2Config.BlockTime(), + StuckTxBlockThreshold: *txConfig.AutoPurge().Threshold(), + DetectionURL: txConfig.AutoPurge().DetectionApiUrl().String(), + } + stuckTxDetector = txm.NewStuckTxDetector(lggr, chainConfig.ChainType(), stuckTxDetectorConfig) + } + + attemptBuilder := txm.NewAttemptBuilder(chainID, fCfg.PriceMax(), estimator, keyStore) + inMemoryStoreManager := storage.NewInMemoryStoreManager(lggr, chainID) + config := txm.Config{ + EIP1559: fCfg.EIP1559DynamicFees(), + BlockTime: *txmV2Config.BlockTime(), + //nolint:gosec // reuse existing config until migration + RetryBlockThreshold: uint16(fCfg.BumpThreshold()), + EmptyTxLimitDefault: fCfg.LimitDefault(), + } + var c txm.Client + if chainConfig.ChainType() == chaintype.ChainDualBroadcast { + c = clientwrappers.NewDualBroadcastClient(client, keyStore, txmV2Config.CustomURL()) + } else { + c = clientwrappers.NewChainClient(client) + } + t := txm.NewTxm(lggr, chainID, c, attemptBuilder, inMemoryStoreManager, stuckTxDetector, config, keyStore) + return txm.NewTxmOrchestrator(lggr, chainID, t, inMemoryStoreManager, fwdMgr, keyStore, attemptBuilder), nil } // NewEvmResender creates a new concrete EvmResender diff --git a/core/chains/evm/txmgr/evm_tx_store_test.go b/core/chains/evm/txmgr/evm_tx_store_test.go index 9e1f135e0b2..1a89d46c9e9 100644 --- a/core/chains/evm/txmgr/evm_tx_store_test.go +++ b/core/chains/evm/txmgr/evm_tx_store_test.go @@ -1383,7 +1383,7 @@ func TestORM_UpdateTxUnstartedToInProgress(t *testing.T) { evmTxmCfg := txmgr.NewEvmTxmConfig(ccfg.EVM()) ec := evmtest.NewEthClientMockWithDefaultChain(t) txMgr := txmgr.NewEvmTxm(ec.ConfiguredChainID(), evmTxmCfg, ccfg.EVM().Transactions(), nil, logger.Test(t), nil, nil, - nil, txStore, nil, nil, nil, nil, nil) + nil, txStore, nil, nil, nil, nil, nil, nil) err := txMgr.XXXTestAbandon(fromAddress) // mark transaction as abandoned require.NoError(t, err) diff --git a/core/chains/evm/txmgr/txmgr_test.go b/core/chains/evm/txmgr/txmgr_test.go index c47ca85737b..4a516add423 100644 --- a/core/chains/evm/txmgr/txmgr_test.go +++ b/core/chains/evm/txmgr/txmgr_test.go @@ -86,7 +86,8 @@ func makeTestEvmTxm( lp, keyStore, estimator, - ht) + ht, + nil) } func TestTxm_SendNativeToken_DoesNotSendToZero(t *testing.T) { diff --git a/core/chains/legacyevm/evm_txm.go b/core/chains/legacyevm/evm_txm.go index ec7098ab56c..21335377e8b 100644 --- a/core/chains/legacyevm/evm_txm.go +++ b/core/chains/legacyevm/evm_txm.go @@ -6,6 +6,7 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/sqlutil" evmclient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" evmconfig "github.com/smartcontractkit/chainlink/v2/core/chains/evm/config" + "github.com/smartcontractkit/chainlink/v2/core/chains/evm/config/chaintype" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/gas" httypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/headtracker/types" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/logpoller" @@ -53,6 +54,24 @@ func newEvmTxm( } if opts.GenTxManager == nil { + var txmv2 txmgr.TxManager + if cfg.TxmV2().Enabled() { + txmv2, err = txmgr.NewTxmV2( + ds, + cfg, + txmgr.NewEvmTxmFeeConfig(cfg.GasEstimator()), + cfg.Transactions(), + cfg.TxmV2(), + client, + lggr, + logPoller, + opts.KeyStore, + estimator, + ) + if cfg.ChainType() != chaintype.ChainDualBroadcast { + return txmv2, estimator, err + } + } txm, err = txmgr.NewTxm( ds, cfg, @@ -66,7 +85,8 @@ func newEvmTxm( logPoller, opts.KeyStore, estimator, - headTracker) + headTracker, + txmv2) } else { txm = opts.GenTxManager(chainID) } diff --git a/core/config/docs/chains-evm.toml b/core/config/docs/chains-evm.toml index 683e59d74b8..3dff11e0d39 100644 --- a/core/config/docs/chains-evm.toml +++ b/core/config/docs/chains-evm.toml @@ -105,6 +105,14 @@ LogBroadcasterEnabled = true # Default # Set to zero to disable. NoNewFinalizedHeadsThreshold = '0' # Default +[EVM.TxmV2] +# Enabled enables TxmV2. +Enabled = false # Default +# BlockTime controls the frequency of the backfill loop of TxmV2. +BlockTime = '10s' # Example +# CustomURL configures the base url of a custom endpoint used by the ChainDualBroadcast chain type. +CustomURL = 'https://example.api.io' # Example + [EVM.Transactions] # ForwardersEnabled enables or disables sending transactions through forwarder contracts. ForwardersEnabled = false # Default diff --git a/core/config/docs/docs_test.go b/core/config/docs/docs_test.go index 9fca08ee99b..6ec0e2acb6b 100644 --- a/core/config/docs/docs_test.go +++ b/core/config/docs/docs_test.go @@ -92,6 +92,10 @@ func TestDoc(t *testing.T) { docDefaults.Workflow.GasLimitDefault = &gasLimitDefault docDefaults.NodePool.Errors = evmcfg.ClientErrors{} + // TxmV2 configs are only set if the feature is enabled + docDefaults.TxmV2.BlockTime = nil + docDefaults.TxmV2.CustomURL = nil + // Transactions.AutoPurge configs are only set if the feature is enabled docDefaults.Transactions.AutoPurge.DetectionApiUrl = nil docDefaults.Transactions.AutoPurge.Threshold = nil diff --git a/core/services/chainlink/config_test.go b/core/services/chainlink/config_test.go index b3533398518..bfba0ce1d4c 100644 --- a/core/services/chainlink/config_test.go +++ b/core/services/chainlink/config_test.go @@ -646,6 +646,9 @@ func TestConfig_Marshal(t *testing.T) { RPCBlockQueryDelay: ptr[uint16](10), NoNewFinalizedHeadsThreshold: &hour, + TxmV2: evmcfg.TxmV2{ + Enabled: ptr(false), + }, Transactions: evmcfg.Transactions{ MaxInFlight: ptr[uint32](19), MaxQueued: ptr[uint32](99), @@ -1107,6 +1110,9 @@ RPCBlockQueryDelay = 10 FinalizedBlockOffset = 16 NoNewFinalizedHeadsThreshold = '1h0m0s' +[EVM.TxmV2] +Enabled = false + [EVM.Transactions] ForwardersEnabled = true MaxInFlight = 19 @@ -1389,6 +1395,12 @@ func TestConfig_full(t *testing.T) { got.EVM[c].Nodes[n].Order = ptr(int32(100)) } } + if got.EVM[c].TxmV2.BlockTime == nil { + got.EVM[c].TxmV2.BlockTime = new(commoncfg.Duration) + } + if got.EVM[c].TxmV2.CustomURL == nil { + got.EVM[c].TxmV2.CustomURL = new(commoncfg.URL) + } if got.EVM[c].Transactions.AutoPurge.Threshold == nil { got.EVM[c].Transactions.AutoPurge.Threshold = ptr(uint32(0)) } @@ -1454,7 +1466,7 @@ func TestConfig_Validate(t *testing.T) { - 1: 10 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, astar, celo, gnosis, hedera, kroma, mantle, metis, optimismBedrock, scroll, wemix, xlayer, zkevm, zksync, zircuit or omitted + - ChainType: invalid value (Foo): must be one of arbitrum, astar, celo, gnosis, hedera, kroma, mantle, metis, optimismBedrock, scroll, wemix, xlayer, zkevm, zksync, zircuit, dualBroadcast or omitted - HeadTracker.HistoryDepth: invalid value (30): must be greater than or equal to FinalizedBlockOffset - GasEstimator.BumpThreshold: invalid value (0): cannot be 0 if auto-purge feature is enabled for Foo - Transactions.AutoPurge.Threshold: missing: needs to be set if auto-purge feature is enabled for Foo @@ -1467,7 +1479,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, astar, celo, gnosis, hedera, kroma, mantle, metis, optimismBedrock, scroll, wemix, xlayer, zkevm, zksync, zircuit or omitted + - ChainType: invalid value (Arbitrum): must be one of arbitrum, astar, celo, gnosis, hedera, kroma, mantle, metis, optimismBedrock, scroll, wemix, xlayer, zkevm, zksync, zircuit, dualBroadcast 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: 3 errors: diff --git a/core/services/chainlink/testdata/config-full.toml b/core/services/chainlink/testdata/config-full.toml index 44362761bd1..2865e7c4a95 100644 --- a/core/services/chainlink/testdata/config-full.toml +++ b/core/services/chainlink/testdata/config-full.toml @@ -328,6 +328,9 @@ RPCBlockQueryDelay = 10 FinalizedBlockOffset = 16 NoNewFinalizedHeadsThreshold = '1h0m0s' +[EVM.TxmV2] +Enabled = false + [EVM.Transactions] ForwardersEnabled = true MaxInFlight = 19 diff --git a/core/services/chainlink/testdata/config-invalid.toml b/core/services/chainlink/testdata/config-invalid.toml index ca22e68c22c..c2d1b4b0da4 100644 --- a/core/services/chainlink/testdata/config-invalid.toml +++ b/core/services/chainlink/testdata/config-invalid.toml @@ -55,6 +55,9 @@ ChainType = 'Foo' FinalityDepth = 32 FinalizedBlockOffset = 64 +[EVM.TxmV2] +Enabled = true + [EVM.Transactions.AutoPurge] Enabled = true @@ -108,6 +111,9 @@ WSURL = 'ws://dupe.com' ChainID = '534352' ChainType = 'scroll' +[EVM.TxmV2] +Enabled = true + [EVM.Transactions.AutoPurge] Enabled = true DetectionApiUrl = '' diff --git a/core/services/chainlink/testdata/config-multi-chain-effective.toml b/core/services/chainlink/testdata/config-multi-chain-effective.toml index f9d34ffbde6..74f99531e4f 100644 --- a/core/services/chainlink/testdata/config-multi-chain-effective.toml +++ b/core/services/chainlink/testdata/config-multi-chain-effective.toml @@ -319,6 +319,9 @@ ReaperInterval = '1h0m0s' ReaperThreshold = '168h0m0s' ResendAfterThreshold = '1m0s' +[EVM.TxmV2] +Enabled = false + [EVM.Transactions.AutoPurge] Enabled = false @@ -421,6 +424,9 @@ RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '0s' +[EVM.TxmV2] +Enabled = false + [EVM.Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -533,6 +539,9 @@ ReaperInterval = '1h0m0s' ReaperThreshold = '168h0m0s' ResendAfterThreshold = '1m0s' +[EVM.TxmV2] +Enabled = false + [EVM.Transactions.AutoPurge] Enabled = false diff --git a/core/services/headreporter/prometheus_reporter_test.go b/core/services/headreporter/prometheus_reporter_test.go index d6fc0f8e938..c962dabdccb 100644 --- a/core/services/headreporter/prometheus_reporter_test.go +++ b/core/services/headreporter/prometheus_reporter_test.go @@ -135,7 +135,8 @@ func newLegacyChainContainer(t *testing.T, db *sqlx.DB) legacyevm.LegacyChainCon lp, keyStore, estimator, - ht) + ht, + nil) require.NoError(t, err) cfg := configtest.NewGeneralConfig(t, nil) diff --git a/core/services/keystore/eth.go b/core/services/keystore/eth.go index ee1c4f23a91..f69bbec28d2 100644 --- a/core/services/keystore/eth.go +++ b/core/services/keystore/eth.go @@ -8,9 +8,11 @@ import ( "strings" "sync" + "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" "github.com/pkg/errors" "github.com/smartcontractkit/chainlink-common/pkg/sqlutil" @@ -35,6 +37,7 @@ type Eth interface { SubscribeToKeyChanges(ctx context.Context) (ch chan struct{}, unsub func()) SignTx(ctx context.Context, fromAddress common.Address, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) + SignMessage(ctx context.Context, address common.Address, message []byte) ([]byte, error) EnabledKeysForChain(ctx context.Context, chainID *big.Int) (keys []ethkey.KeyV2, err error) GetRoundRobinAddress(ctx context.Context, chainID *big.Int, addresses ...common.Address) (address common.Address, err error) @@ -532,6 +535,25 @@ func (ks *eth) XXXTestingOnlyAdd(ctx context.Context, key ethkey.KeyV2) { } } +// SignMessage signs the provided message using the private key associated with the given address, +// following the EIP-191 specific identifier (e.g., keccak256("\x19Ethereum Signed Message:\n"${message length}${message})) +func (ks *eth) SignMessage(ctx context.Context, address common.Address, data []byte) ([]byte, error) { + ks.lock.RLock() + defer ks.lock.RUnlock() + if ks.isLocked() { + return nil, ErrLocked + } + key, err := ks.getByID(address.Hex()) + if err != nil { + return nil, err + } + signature, err := crypto.Sign(accounts.TextHash(data), key.ToEcdsaPrivKey()) + if err != nil { + return nil, errors.Wrap(err, "failed to sign data") + } + return signature, nil +} + // caller must hold lock! func (ks *eth) getByID(id string) (ethkey.KeyV2, error) { key, found := ks.keyRing.Eth[id] diff --git a/core/services/keystore/eth_test.go b/core/services/keystore/eth_test.go index b27ec54956b..1f4a5835016 100644 --- a/core/services/keystore/eth_test.go +++ b/core/services/keystore/eth_test.go @@ -9,7 +9,9 @@ import ( "testing" "time" + "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -357,6 +359,31 @@ func Test_EthKeyStore_SignTx(t *testing.T) { require.NotEqual(t, tx, signed) } +func Test_EthKeyStore_SignMessage(t *testing.T) { + t.Parallel() + + ctx := testutils.Context(t) + + db := pgtest.NewSqlxDB(t) + keyStore := cltest.NewKeyStore(t, db) + ethKeyStore := keyStore.Eth() + + k, _ := cltest.MustInsertRandomKey(t, ethKeyStore) + + pubKeyBytes := crypto.FromECDSAPub(&k.ToEcdsaPrivKey().PublicKey) + + message := []byte("this is a message") + + signedMessage, err := keyStore.Eth().SignMessage(ctx, k.Address, message) + require.NoError(t, err) + sigPublicKey, err := crypto.Ecrecover(accounts.TextHash(message), signedMessage) + require.NoError(t, err) + require.Equal(t, pubKeyBytes, sigPublicKey) + + _, err = keyStore.Eth().SignMessage(ctx, utils.RandomAddress(), message) + require.ErrorContains(t, err, "Key not found") +} + func Test_EthKeyStore_E2E(t *testing.T) { t.Parallel() diff --git a/core/services/keystore/mocks/eth.go b/core/services/keystore/mocks/eth.go index 4a9dd68fbc2..5cbc26fb511 100644 --- a/core/services/keystore/mocks/eth.go +++ b/core/services/keystore/mocks/eth.go @@ -1082,6 +1082,66 @@ func (_c *Eth_Import_Call) RunAndReturn(run func(context.Context, []byte, string return _c } +// SignMessage provides a mock function with given fields: ctx, address, message +func (_m *Eth) SignMessage(ctx context.Context, address common.Address, message []byte) ([]byte, error) { + ret := _m.Called(ctx, address, message) + + if len(ret) == 0 { + panic("no return value specified for SignMessage") + } + + var r0 []byte + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, common.Address, []byte) ([]byte, error)); ok { + return rf(ctx, address, message) + } + if rf, ok := ret.Get(0).(func(context.Context, common.Address, []byte) []byte); ok { + r0 = rf(ctx, address, message) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]byte) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, common.Address, []byte) error); ok { + r1 = rf(ctx, address, message) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Eth_SignMessage_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SignMessage' +type Eth_SignMessage_Call struct { + *mock.Call +} + +// SignMessage is a helper method to define mock.On call +// - ctx context.Context +// - address common.Address +// - message []byte +func (_e *Eth_Expecter) SignMessage(ctx interface{}, address interface{}, message interface{}) *Eth_SignMessage_Call { + return &Eth_SignMessage_Call{Call: _e.mock.On("SignMessage", ctx, address, message)} +} + +func (_c *Eth_SignMessage_Call) Run(run func(ctx context.Context, address common.Address, message []byte)) *Eth_SignMessage_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(common.Address), args[2].([]byte)) + }) + return _c +} + +func (_c *Eth_SignMessage_Call) Return(_a0 []byte, _a1 error) *Eth_SignMessage_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *Eth_SignMessage_Call) RunAndReturn(run func(context.Context, common.Address, []byte) ([]byte, error)) *Eth_SignMessage_Call { + _c.Call.Return(run) + return _c +} + // SignTx provides a mock function with given fields: ctx, fromAddress, tx, chainID func (_m *Eth) SignTx(ctx context.Context, fromAddress common.Address, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { ret := _m.Called(ctx, fromAddress, tx, chainID) diff --git a/core/services/vrf/delegate_test.go b/core/services/vrf/delegate_test.go index 7f94e98ff9f..e3b204510da 100644 --- a/core/services/vrf/delegate_test.go +++ b/core/services/vrf/delegate_test.go @@ -82,7 +82,7 @@ func buildVrfUni(t *testing.T, db *sqlx.DB, cfg chainlink.GeneralConfig) vrfUniv btORM := bridges.NewORM(db) ks := keystore.NewInMemory(db, utils.FastScryptParams, lggr) _, dbConfig, evmConfig := txmgr.MakeTestConfigs(t) - txm, err := txmgr.NewTxm(db, evmConfig, evmConfig.GasEstimator(), evmConfig.Transactions(), nil, dbConfig, dbConfig.Listener(), ec, logger.TestLogger(t), nil, ks.Eth(), nil, nil) + txm, err := txmgr.NewTxm(db, evmConfig, evmConfig.GasEstimator(), evmConfig.Transactions(), nil, dbConfig, dbConfig.Listener(), ec, logger.TestLogger(t), nil, ks.Eth(), nil, nil, nil) orm := headtracker.NewORM(*testutils.FixtureChainID, db) require.NoError(t, orm.IdempotentInsertHead(testutils.Context(t), cltest.Head(51))) jrm := job.NewORM(db, prm, btORM, ks, lggr) diff --git a/core/services/vrf/v2/integration_v2_test.go b/core/services/vrf/v2/integration_v2_test.go index 4869cca0926..973affa8f08 100644 --- a/core/services/vrf/v2/integration_v2_test.go +++ b/core/services/vrf/v2/integration_v2_test.go @@ -141,7 +141,7 @@ func makeTestTxm(t *testing.T, txStore txmgr.TestEvmTxStore, keyStore keystore.M _, _, evmConfig := txmgr.MakeTestConfigs(t) txmConfig := txmgr.NewEvmTxmConfig(evmConfig) txm := txmgr.NewEvmTxm(ec.ConfiguredChainID(), txmConfig, evmConfig.Transactions(), keyStore.Eth(), logger.TestLogger(t), nil, nil, - nil, txStore, nil, nil, nil, nil, nil) + nil, txStore, nil, nil, nil, nil, nil, nil) return txm } diff --git a/core/services/vrf/v2/listener_v2_test.go b/core/services/vrf/v2/listener_v2_test.go index b7a8710c4f8..e2a2a703de8 100644 --- a/core/services/vrf/v2/listener_v2_test.go +++ b/core/services/vrf/v2/listener_v2_test.go @@ -40,7 +40,7 @@ func makeTestTxm(t *testing.T, txStore txmgr.TestEvmTxStore, keyStore keystore.M ec := evmtest.NewEthClientMockWithDefaultChain(t) txmConfig := txmgr.NewEvmTxmConfig(evmConfig) txm := txmgr.NewEvmTxm(ec.ConfiguredChainID(), txmConfig, evmConfig.Transactions(), keyStore.Eth(), logger.TestLogger(t), nil, nil, - nil, txStore, nil, nil, nil, nil, nil) + nil, txStore, nil, nil, nil, nil, nil, nil) return txm } diff --git a/core/web/resolver/testdata/config-full.toml b/core/web/resolver/testdata/config-full.toml index c319a7bf8ec..e14fe4d2d7e 100644 --- a/core/web/resolver/testdata/config-full.toml +++ b/core/web/resolver/testdata/config-full.toml @@ -328,6 +328,9 @@ RPCBlockQueryDelay = 10 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '15m0s' +[EVM.TxmV2] +Enabled = false + [EVM.Transactions] ForwardersEnabled = true MaxInFlight = 19 diff --git a/core/web/resolver/testdata/config-multi-chain-effective.toml b/core/web/resolver/testdata/config-multi-chain-effective.toml index e29c763b74c..23fbd202305 100644 --- a/core/web/resolver/testdata/config-multi-chain-effective.toml +++ b/core/web/resolver/testdata/config-multi-chain-effective.toml @@ -319,6 +319,9 @@ ReaperInterval = '1h0m0s' ReaperThreshold = '168h0m0s' ResendAfterThreshold = '1m0s' +[EVM.TxmV2] +Enabled = false + [EVM.Transactions.AutoPurge] Enabled = false @@ -421,6 +424,9 @@ RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '0s' +[EVM.TxmV2] +Enabled = false + [EVM.Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -533,6 +539,9 @@ ReaperInterval = '1h0m0s' ReaperThreshold = '168h0m0s' ResendAfterThreshold = '1m0s' +[EVM.TxmV2] +Enabled = false + [EVM.Transactions.AutoPurge] Enabled = false diff --git a/docs/CONFIG.md b/docs/CONFIG.md index 0b52274cff8..9e0bbebe1ff 100644 --- a/docs/CONFIG.md +++ b/docs/CONFIG.md @@ -1983,6 +1983,9 @@ RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '9m0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -2087,6 +2090,9 @@ RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -2191,6 +2197,9 @@ RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -2295,6 +2304,9 @@ RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -2400,6 +2412,9 @@ RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '13m0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -2508,6 +2523,9 @@ RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -2612,6 +2630,9 @@ RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -2717,6 +2738,9 @@ RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -2821,6 +2845,9 @@ RPCBlockQueryDelay = 2 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '45s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -2924,6 +2951,9 @@ RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -3027,6 +3057,9 @@ RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -3131,6 +3164,9 @@ RPCBlockQueryDelay = 2 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '40s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -3236,6 +3272,9 @@ RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '2m0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -3340,6 +3379,9 @@ RPCBlockQueryDelay = 2 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -3444,6 +3486,9 @@ RPCBlockQueryDelay = 10 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '6m0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -3548,6 +3593,9 @@ RPCBlockQueryDelay = 15 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -3652,6 +3700,9 @@ RPCBlockQueryDelay = 15 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -3756,6 +3807,9 @@ RPCBlockQueryDelay = 2 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -3860,6 +3914,9 @@ RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -3968,6 +4025,9 @@ RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -4075,6 +4135,9 @@ RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -4179,6 +4242,9 @@ RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -4283,6 +4349,9 @@ RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -4390,6 +4459,9 @@ RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -4498,6 +4570,9 @@ RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -4606,6 +4681,9 @@ RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -4709,6 +4787,9 @@ RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -4813,6 +4894,9 @@ RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -4917,6 +5001,9 @@ RPCBlockQueryDelay = 15 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -5021,6 +5108,9 @@ RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '40s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -5125,6 +5215,9 @@ RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '40s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -5228,6 +5321,9 @@ RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -5333,6 +5429,9 @@ RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '2h0m0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -5441,6 +5540,9 @@ RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -5549,6 +5651,9 @@ RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -5653,6 +5758,9 @@ RPCBlockQueryDelay = 2 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -5756,6 +5864,9 @@ RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -5860,6 +5971,9 @@ RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '15m0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -5968,6 +6082,9 @@ RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '2m0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -6073,6 +6190,9 @@ RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -6181,6 +6301,9 @@ RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -6289,6 +6412,9 @@ RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -6396,6 +6522,9 @@ RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '1m0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -6500,6 +6629,9 @@ RPCBlockQueryDelay = 2 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '1m0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -6604,6 +6736,9 @@ RPCBlockQueryDelay = 2 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '1m0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -6708,6 +6843,9 @@ RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '1m0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -6813,6 +6951,9 @@ RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '15m0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -6924,6 +7065,9 @@ RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '15m0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -7033,6 +7177,9 @@ RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -7136,6 +7283,9 @@ RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -7239,6 +7389,9 @@ RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -7343,6 +7496,9 @@ RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -7447,6 +7603,9 @@ RPCBlockQueryDelay = 10 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -7550,6 +7709,9 @@ RPCBlockQueryDelay = 10 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '12m0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -7654,6 +7816,9 @@ RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -7762,6 +7927,9 @@ RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '12m0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -7871,6 +8039,9 @@ RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -7979,6 +8150,9 @@ RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -8086,6 +8260,9 @@ RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -8193,6 +8370,9 @@ RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -8301,6 +8481,9 @@ RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -8409,6 +8592,9 @@ RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -8513,6 +8699,9 @@ RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '15m0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -8621,6 +8810,9 @@ RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -8725,6 +8917,9 @@ RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '0s' +[TxmV2] +Enabled = false + [Transactions] ForwardersEnabled = false MaxInFlight = 16 @@ -9008,6 +9203,33 @@ out-of-sync. Only applicable if `FinalityTagEnabled=true` Set to zero to disable. +## EVM.TxmV2 +```toml +[EVM.TxmV2] +Enabled = false # Default +BlockTime = '10s' # Example +CustomURL = 'https://example.api.io' # Example +``` + + +### Enabled +```toml +Enabled = false # Default +``` +Enabled enables TxmV2. + +### BlockTime +```toml +BlockTime = '10s' # Example +``` +BlockTime controls the frequency of the backfill loop of TxmV2. + +### CustomURL +```toml +CustomURL = 'https://example.api.io' # Example +``` +CustomURL configures the base url of a custom endpoint used by the ChainDualBroadcast chain type. + ## EVM.Transactions ```toml [EVM.Transactions] diff --git a/testdata/scripts/node/validate/defaults-override.txtar b/testdata/scripts/node/validate/defaults-override.txtar index 5c36fddeaf1..655e176ff70 100644 --- a/testdata/scripts/node/validate/defaults-override.txtar +++ b/testdata/scripts/node/validate/defaults-override.txtar @@ -384,6 +384,9 @@ RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '9m0s' +[EVM.TxmV2] +Enabled = false + [EVM.Transactions] ForwardersEnabled = false MaxInFlight = 16 diff --git a/testdata/scripts/node/validate/disk-based-logging-disabled.txtar b/testdata/scripts/node/validate/disk-based-logging-disabled.txtar index 9a9f5e88811..e30185f2985 100644 --- a/testdata/scripts/node/validate/disk-based-logging-disabled.txtar +++ b/testdata/scripts/node/validate/disk-based-logging-disabled.txtar @@ -367,6 +367,9 @@ RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '9m0s' +[EVM.TxmV2] +Enabled = false + [EVM.Transactions] ForwardersEnabled = false MaxInFlight = 16 diff --git a/testdata/scripts/node/validate/disk-based-logging-no-dir.txtar b/testdata/scripts/node/validate/disk-based-logging-no-dir.txtar index 8b4089832eb..0ddf4a0e89b 100644 --- a/testdata/scripts/node/validate/disk-based-logging-no-dir.txtar +++ b/testdata/scripts/node/validate/disk-based-logging-no-dir.txtar @@ -367,6 +367,9 @@ RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '9m0s' +[EVM.TxmV2] +Enabled = false + [EVM.Transactions] ForwardersEnabled = false MaxInFlight = 16 diff --git a/testdata/scripts/node/validate/disk-based-logging.txtar b/testdata/scripts/node/validate/disk-based-logging.txtar index ef496ddfd8b..b207c637d9c 100644 --- a/testdata/scripts/node/validate/disk-based-logging.txtar +++ b/testdata/scripts/node/validate/disk-based-logging.txtar @@ -367,6 +367,9 @@ RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '9m0s' +[EVM.TxmV2] +Enabled = false + [EVM.Transactions] ForwardersEnabled = false MaxInFlight = 16 diff --git a/testdata/scripts/node/validate/invalid.txtar b/testdata/scripts/node/validate/invalid.txtar index 81c5a494440..f79682a0e3e 100644 --- a/testdata/scripts/node/validate/invalid.txtar +++ b/testdata/scripts/node/validate/invalid.txtar @@ -357,6 +357,9 @@ RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '9m0s' +[EVM.TxmV2] +Enabled = false + [EVM.Transactions] ForwardersEnabled = false MaxInFlight = 16 diff --git a/testdata/scripts/node/validate/valid.txtar b/testdata/scripts/node/validate/valid.txtar index 063436f1079..e92c16c8442 100644 --- a/testdata/scripts/node/validate/valid.txtar +++ b/testdata/scripts/node/validate/valid.txtar @@ -364,6 +364,9 @@ RPCBlockQueryDelay = 1 FinalizedBlockOffset = 0 NoNewFinalizedHeadsThreshold = '9m0s' +[EVM.TxmV2] +Enabled = false + [EVM.Transactions] ForwardersEnabled = false MaxInFlight = 16