Skip to content

Oev 771 flashbots batch#346

Merged
dimriou merged 15 commits intodevelopfrom
oev-771_flashbots_batch
Mar 2, 2026
Merged

Oev 771 flashbots batch#346
dimriou merged 15 commits intodevelopfrom
oev-771_flashbots_batch

Conversation

@dimriou
Copy link
Copy Markdown
Contributor

@dimriou dimriou commented Feb 4, 2026

This PR enables Flashbots bundles. Every time a transaction is broadcasted, it will fetch all the past in-flight transactions and create a bundle. The bundle will use the last attempt of each transaction. The bundle broadcasting is a fire and forget process that helps nonce unblocking, so it is not tracked.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Feb 4, 2026

⚠️ API Diff Results - Breaking changes detected

📦 Module: github-com-smartcontractkit-chainlink-evm

🔴 Breaking Changes (5)

pkg/config.TransactionManagerV2 (1)
  • Bundles — ➕ Added
pkg/txm.TxStore (2)
  • FetchUnconfirmedTransactions — ➕ Added

  • UpdateSignedAttempt — ➕ Added

pkg/txm/clientwrappers/dualbroadcast (2)
  • NewFlashbotsClient — Type changed:
func(
  - github.com/smartcontractkit/chainlink-evm/pkg/client.Client, 
  + github.com/smartcontractkit/chainlink-common/pkg/logger.Logger, 
  + FlashbotsClientRPC, 
  github.com/smartcontractkit/chainlink-evm/pkg/keys.MessageSigner, 
  - *net/url.URL
  + *net/url.URL, 
  + FlashbotsTxStore, 
  + *bool
)
*FlashbotsClient
  • SelectClient — Type changed:
func(
  github.com/smartcontractkit/chainlink-common/pkg/logger.Logger, 
  github.com/smartcontractkit/chainlink-evm/pkg/client.Client, 
  github.com/smartcontractkit/chainlink-evm/pkg/keys.ChainStore, 
  *net/url.URL, 
  *math/big.Int, 
  - MetaClientTxStore
  + github.com/smartcontractkit/chainlink-evm/pkg/txm.TxStore, 
  + *bool
)
(github.com/smartcontractkit/chainlink-evm/pkg/txm.Client, github.com/smartcontractkit/chainlink-evm/pkg/txm.ErrorHandler, error)

📄 View full apidiff report

@dimriou dimriou force-pushed the oev-771_flashbots_batch branch from 7f1a37b to 6fe2367 Compare February 17, 2026 11:32
@dimriou dimriou force-pushed the oev-771_flashbots_batch branch from 60f915c to a1d5fa8 Compare February 18, 2026 10:12
Comment thread pkg/txm/clientwrappers/dualbroadcast/flashbots_client.go Fixed
Copy link
Copy Markdown

@pszal pszal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! Some feedback below. Please consider adding more tests.

Comment thread pkg/txm/clientwrappers/dualbroadcast/flashbots_client.go
Comment thread pkg/txm/clientwrappers/dualbroadcast/flashbots_client.go Outdated
Comment thread pkg/txm/clientwrappers/dualbroadcast/flashbots_client.go Outdated
Comment thread pkg/txm/clientwrappers/dualbroadcast/flashbots_client.go
if err != nil {
return fmt.Errorf("failed to get current block height: %w", err)
}
targetBlock := currentBlock.NumberU64() + 24
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would maxBlock be a more accurate var name?


bodyItems = append(bodyItems, map[string]any{
"tx": hexutil.Encode(txData),
"revertMode": "allow", // we always want to allow reverts so bundles are valid even if a single transaction within the bundle goes through
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, but I followed the spec they gave me in our conversation.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it make sense to document this spec somewhere (e.g., doc.go)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a link to the docs.

Comment thread pkg/txm/clientwrappers/dualbroadcast/flashbots_client.go
@dimriou dimriou marked this pull request as ready for review February 27, 2026 10:26
@dimriou dimriou requested a review from a team as a code owner February 27, 2026 10:26
Copilot AI review requested due to automatic review settings February 27, 2026 10:26
@dimriou dimriou requested review from a team as code owners February 27, 2026 10:26
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds optional Flashbots bundle broadcasting to the TXM dual-broadcast client path, gated by a new Bundles config flag. When enabled, after successfully sending a transaction to the Flashbots endpoint, the client builds and submits a bundle composed of in-flight (unconfirmed) transactions.

Changes:

  • Add Bundles configuration for TransactionManagerV2 (TOML + docs + config interfaces).
  • Extend txm.TxStore with APIs needed for bundle building (fetch unconfirmed txs; update signed attempts).
  • Implement Flashbots bundle creation + submission and add unit tests for URL param parsing / bundle request construction.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
pkg/txmgr/builder.go Passes Bundles config into dual-broadcast client selection.
pkg/txm/txm.go Extends TxStore interface with FetchUnconfirmedTransactions and UpdateSignedAttempt.
pkg/txm/storage/inmemory_store_manager.go Adds store-manager forwarding method for fetching all unconfirmed txs.
pkg/txm/storage/inmemory_store.go Adds FetchUnconfirmedTransactions() (deep-copy + nonce sort).
pkg/txm/mock_tx_store_test.go Updates mock store to satisfy new TxStore interface methods.
pkg/txm/clientwrappers/dualbroadcast/selector.go Threads bundles flag and tx store into Flashbots client constructor.
pkg/txm/clientwrappers/dualbroadcast/flashbots_client.go Implements bundle send flow, request signing/posting changes, URL param parsing.
pkg/txm/clientwrappers/dualbroadcast/flashbots_client_test.go Adds tests for URL param parsing and bundle request composition.
pkg/config/toml/testdata/config-full.toml Adds Bundles to the full TOML test config.
pkg/config/toml/docs.toml Documents Bundles TOML setting.
pkg/config/toml/config_test.go Updates config tests to include Bundles defaults/docs handling.
pkg/config/toml/config.go Adds Bundles field to TransactionManagerV2Config and merges it in setFrom.
pkg/config/config.go Extends TransactionManagerV2 interface with Bundles().
pkg/config/chain_scoped_transactions.go Implements Bundles() accessor for chain-scoped config wrapper.
CONFIG.md Adds Bundles to user-facing configuration documentation.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +106 to +110
// After successfully sending the transaction, send a bundle with all unconfirmed transactions
// Don't act on a bundle error - this is a fire and forget operation but we do want to log the error.
if d.bundles {
if err := d.SendBundle(ctx, tx.FromAddress, params); err != nil {
d.lggr.Error("error sending bundle: ", err)
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description says bundle broadcasting is “fire and forget”, but this call is synchronous: SendTransaction will block until SendBundle completes (including an extra BlockByNumber RPC + HTTP request). Consider running SendBundle asynchronously (e.g., in a goroutine with its own bounded context) so transaction broadcasting latency isn’t increased and bundle failures can’t delay TXM.

Copilot uses AI. Check for mistakes.
Comment thread pkg/txm/clientwrappers/dualbroadcast/flashbots_client.go Outdated
Comment thread pkg/txm/clientwrappers/dualbroadcast/flashbots_client.go
Comment thread pkg/txm/clientwrappers/dualbroadcast/flashbots_client.go
Comment thread pkg/txm/clientwrappers/dualbroadcast/flashbots_client.go
Comment on lines +210 to +213
// Sort by nonce in ascending order
sort.Slice(transactions, func(i, j int) bool {
return *transactions[i].Nonce < *transactions[j].Nonce
})
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FetchUnconfirmedTransactions adds nonce-sorting and returns an error on nil nonces, but there are no unit tests covering this new behavior. Add tests to validate sorted order and the nil-nonce error so the bundle logic can rely on it safely.

Copilot uses AI. Check for mistakes.
Comment thread pkg/txm/clientwrappers/dualbroadcast/flashbots_client_test.go
Copy link
Copy Markdown

@pszal pszal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

nonce, err := hexutil.DecodeUint64(response)
var resultStr string
if err := json.Unmarshal(raw, &resultStr); err != nil {
return 0, fmt.Errorf("failed to unmarshal response into string: %w", err)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that you log arg when it fails in other places, any reason to not log raw here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No reason, I added raw as well.

for _, unconfirmedTx := range unconfirmedTxs {
if len(unconfirmedTx.Attempts) > 0 {
attempts = append(attempts, unconfirmedTx.Attempts[len(unconfirmedTx.Attempts)-1])
if unconfirmedTx.Nonce != nil {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you check for that here, then it makes sense to check or guarantee in the if above that attempts and nonces are consistent (i.e., you append to both or neither).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call

refundRaw := values.Get("refund")
if refundRaw != "" {
parts := strings.Split(refundRaw, ":")
if len(parts) == 2 {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it ok if its not 2? shouldn't we err?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, agree

if err := json.Unmarshal(raw, &bundleResult); err != nil {
return fmt.Errorf("failed to decode response %v into bundle result: %w", raw, err)
}
d.lggr.Infow("Broadcasted transaction bundle", "nonces", nonces, "bundleHash", bundleResult.BundleHash)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it intentional to log only nonces and not the actual bundle (or at least IDs of its txs)?

Copy link
Copy Markdown
Contributor Author

@dimriou dimriou Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Logs can be very noisy in Core, so the idea here is avoid replicating information that can be recovered by past logs. Similar to how we confirm transactions, we use nonce as an identifier to retrieve information if needed, as the individual transaction details should be available a few lines before this message. I'll add the IDs though to make it cleaner.

@dimriou dimriou enabled auto-merge (squash) March 2, 2026 16:10
@dimriou dimriou disabled auto-merge March 2, 2026 16:10
dhaidashenko
dhaidashenko previously approved these changes Mar 2, 2026
Comment thread pkg/txm/clientwrappers/dualbroadcast/flashbots_client.go Outdated
Comment thread pkg/txm/clientwrappers/dualbroadcast/flashbots_client.go
Comment thread pkg/txm/clientwrappers/dualbroadcast/flashbots_client.go Outdated
Comment thread pkg/txm/clientwrappers/dualbroadcast/flashbots_client.go Outdated
// Don't act on a bundle error - this is a fire and forget operation but we do want to log the error.
if d.bundles {
if err := d.SendBundle(ctx, tx.FromAddress, params); err != nil {
d.lggr.Errorw("error sending bundle", "err", err)

Check failure

Code scanning / CodeQL

Log entries created from user input High

This log entry depends on a
user-provided value
.

Copilot Autofix

AI about 2 months ago

In general, to fix “log entries created from user input,” any user-influenced string that is written to logs should be sanitized/encoded so it cannot inject new log entries or markup. For plain-text logs, that usually means removing or replacing \n and \r. For HTML-rendered logs, it also means HTML-escaping. In this codebase, the flagged sink is a structured logger call that logs an error value; we can mitigate the issue by converting the error to a string and stripping line breaks (and optionally carriage returns) before logging.

The best minimal fix without changing existing functionality is to sanitize the err value only at the point where CodeQL flags the problem, by converting err to a string, removing \n and \r, and logging that sanitized string instead of the raw error. This keeps the log message content almost identical (minus dangerous control characters) while eliminating the risk of log forging via embedded line breaks. We can use the already-imported strings package, so no new imports are required.

Concretely, in pkg/txm/clientwrappers/dualbroadcast/flashbots_client.go, around line 111–114:

if d.bundles {
    if err := d.SendBundle(ctx, tx.FromAddress, params); err != nil {
        d.lggr.Errorw("error sending bundle", "err", err)
    }
}

we will introduce a small sanitization step:

if d.bundles {
    if err := d.SendBundle(ctx, tx.FromAddress, params); err != nil {
        sanitizedErr := strings.ReplaceAll(err.Error(), "\n", "")
        sanitizedErr = strings.ReplaceAll(sanitizedErr, "\r", "")
        d.lggr.Errorw("error sending bundle", "err", sanitizedErr)
    }
}

This uses strings.ReplaceAll (already imported) and keeps all other logic intact.

Suggested changeset 1
pkg/txm/clientwrappers/dualbroadcast/flashbots_client.go

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/pkg/txm/clientwrappers/dualbroadcast/flashbots_client.go b/pkg/txm/clientwrappers/dualbroadcast/flashbots_client.go
--- a/pkg/txm/clientwrappers/dualbroadcast/flashbots_client.go
+++ b/pkg/txm/clientwrappers/dualbroadcast/flashbots_client.go
@@ -110,7 +110,9 @@
 		// Don't act on a bundle error - this is a fire and forget operation but we do want to log the error.
 		if d.bundles {
 			if err := d.SendBundle(ctx, tx.FromAddress, params); err != nil {
-				d.lggr.Errorw("error sending bundle", "err", err)
+				sanitizedErr := strings.ReplaceAll(err.Error(), "\n", "")
+				sanitizedErr = strings.ReplaceAll(sanitizedErr, "\r", "")
+				d.lggr.Errorw("error sending bundle", "err", sanitizedErr)
 			}
 		}
 		return nil
EOF
@@ -110,7 +110,9 @@
// Don't act on a bundle error - this is a fire and forget operation but we do want to log the error.
if d.bundles {
if err := d.SendBundle(ctx, tx.FromAddress, params); err != nil {
d.lggr.Errorw("error sending bundle", "err", err)
sanitizedErr := strings.ReplaceAll(err.Error(), "\n", "")
sanitizedErr = strings.ReplaceAll(sanitizedErr, "\r", "")
d.lggr.Errorw("error sending bundle", "err", sanitizedErr)
}
}
return nil
Copilot is powered by AI and may make mistakes. Always verify output.
@dimriou dimriou merged commit 1e75633 into develop Mar 2, 2026
36 of 41 checks passed
@dimriou dimriou deleted the oev-771_flashbots_batch branch March 2, 2026 18:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants