Skip to content

Split MetaClient errors from standard RPC errors#343

Merged
dimriou merged 7 commits into
developfrom
oev-795_split_meta_client_from_rpc_errors
Feb 3, 2026
Merged

Split MetaClient errors from standard RPC errors#343
dimriou merged 7 commits into
developfrom
oev-795_split_meta_client_from_rpc_errors

Conversation

@dimriou
Copy link
Copy Markdown
Contributor

@dimriou dimriou commented Jan 30, 2026

This PR distinguishes the errors returned by the MetaClient from the standard RPC errors and marks as fatal the former. Additionally, it for the RPC errors it now rebroadcasts a past attempt instead of reauctioning.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jan 30, 2026

⚠️ API Diff Results - Breaking changes detected

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

🔴 Breaking Changes (5)

pkg/txm.TxStore (1)
  • AppendAttemptToTransaction — Type changed:
func(
  context.Context, 
  uint64, 
  github.com/ethereum/go-ethereum/common.Address, 
  *github.com/smartcontractkit/chainlink-evm/pkg/txm/types.Attempt
)
- error
+ ([]*github.com/smartcontractkit/chainlink-evm/pkg/txm/types.Attempt, error)
pkg/txm/clientwrappers/dualbroadcast (2)
  • NewMetaClient — Type changed:
func(
  github.com/smartcontractkit/chainlink-common/pkg/logger.Logger, 
  MetaClientRPC, 
  MetaClientKeystore, 
  *net/url.URL, 
  - *math/big.Int
  + *math/big.Int, 
  + MetaClientTxStore
)
(*MetaClient, error)
  • 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
  + *math/big.Int, 
  + MetaClientTxStore
)
(github.com/smartcontractkit/chainlink-evm/pkg/txm.Client, github.com/smartcontractkit/chainlink-evm/pkg/txm.ErrorHandler, error)
pkg/txm/storage.(*InMemoryStore) (1)
  • AppendAttemptToTransaction — Type changed:
func(
  uint64, 
  *github.com/smartcontractkit/chainlink-evm/pkg/txm/types.Attempt
)
- error
+ ([]*github.com/smartcontractkit/chainlink-evm/pkg/txm/types.Attempt, error)
pkg/txm/storage.(*InMemoryStoreManager) (1)
  • AppendAttemptToTransaction — Type changed:
func(
  context.Context, 
  uint64, 
  github.com/ethereum/go-ethereum/common.Address, 
  *github.com/smartcontractkit/chainlink-evm/pkg/txm/types.Attempt
)
- error
+ ([]*github.com/smartcontractkit/chainlink-evm/pkg/txm/types.Attempt, error)

📄 View full apidiff report

return a.c.PendingNonceAt(ctx, address)
}

// SendTransactions handles three diffenent cases:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
// SendTransactions handles three diffenent cases:
// SendTransactions handles three different cases:

Comment on lines +183 to +204
if !tx.IsPurgeable {
if meta != nil &&
meta.DualBroadcast != nil && *meta.DualBroadcast && meta.DualBroadcastParams != nil && meta.FwdrDestAddress != nil &&
tx.AttemptCount == 1 {
meta, err := a.SendRequest(ctx, tx, attempt, *meta.DualBroadcastParams, tx.ToAddress)
if err != nil {
a.metrics.RecordSendRequestError(ctx)
return fmt.Errorf("error sending request for transactionID(%d): %w", tx.ID, ErrAuction)
}
return nil
if meta != nil {
if err := a.SendOperation(ctx, tx, attempt, *meta); err != nil {
a.metrics.RecordSendOperationError(ctx)
return fmt.Errorf("failed to send operation for transactionID(%d): %w", tx.ID, ErrAuction)
}
return nil
}
a.lggr.Infof("No bids for transactionID(%d): ", tx.ID)
return ErrNoBids
}
if len(tx.Attempts) > 1 {
a.lggr.Infow("Intercepted attempt for tx(rebroadcasting first attempt)", "txID", tx.ID, "attempt", tx.Attempts[0])
return a.c.SendTransaction(ctx, tx.Attempts[0].SignedTransaction)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Maybe it's me, but I find it hard to reason about this many ifs. Is there any way to make this less indented, maybe through an early return?

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.

Wasn't sure about simplifying the is.IsPurgeable statement as well, so I can revert this back to three main ifs which are explained at the top of the method.

  1. First failed transmission
  2. Other attempts
  3. Empty transaction
    Note: Part of the reason why this doesn't have much info, is because the Atlas integration was not publicly known before.

@dimriou dimriou marked this pull request as ready for review February 2, 2026 10:50
@dimriou dimriou requested review from a team as code owners February 2, 2026 10:50
Copilot AI review requested due to automatic review settings February 2, 2026 10:50
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 separates MetaClient-specific errors from standard RPC errors in the dual broadcast system. MetaClient errors are now treated as fatal on first attempt, while RPC errors trigger rebroadcasting of the first attempt instead of re-auctioning.

Changes:

  • Introduced ErrAuction error type to distinguish MetaClient auction/validation failures from other errors
  • Modified error handling to mark transactions as fatal when MetaClient errors occur on first attempt
  • Updated SendTransaction logic to rebroadcast the first attempt for subsequent broadcast failures instead of re-auctioning
  • Added UpdateSignedAttempt functionality to store and retrieve signed transactions for rebroadcasting

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
pkg/txmgr/builder.go Passes inMemoryStoreManager to SelectClient for transaction store access
pkg/txm/storage/inmemory_store_test.go Adds test coverage for UpdateSignedAttempt functionality
pkg/txm/storage/inmemory_store_manager.go Implements UpdateSignedAttempt method in store manager
pkg/txm/storage/inmemory_store.go Adds UpdateSignedAttempt method to update signed transactions on attempts
pkg/txm/clientwrappers/dualbroadcast/selector.go Updates SelectClient signature to accept transaction store
pkg/txm/clientwrappers/dualbroadcast/meta_error_handler_test.go Adds test for auction error handling on first attempt
pkg/txm/clientwrappers/dualbroadcast/meta_error_handler.go Extends fatal error marking to include ErrAuction
pkg/txm/clientwrappers/dualbroadcast/meta_client.go Implements three-case SendTransaction logic with auction error wrapping and first-attempt rebroadcasting

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

if err != nil {
a.metrics.RecordSendRequestError(ctx)
return fmt.Errorf("error sending request for transactionID(%d): %w", tx.ID, err)
return fmt.Errorf("error sending request for transactionID(%d): %w", tx.ID, ErrAuction)
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

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

The error wrapping is incorrect. When wrapping ErrAuction with fmt.Errorf using %w, the actual underlying error from SendRequest is being discarded. This loses important diagnostic information about why the auction failed. The line should wrap both the original error and ErrAuction, or include the original error message in the format string.

Copilot uses AI. Check for mistakes.
if err := a.SendOperation(ctx, tx, attempt, *meta); err != nil {
a.metrics.RecordSendOperationError(ctx)
return fmt.Errorf("failed to send operation for transactionID(%d): %w", tx.ID, err)
return fmt.Errorf("failed to send operation for transactionID(%d): %w", tx.ID, ErrAuction)
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

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

The error wrapping is incorrect. When wrapping ErrAuction with fmt.Errorf using %w, the actual underlying error from SendOperation is being discarded. This loses important diagnostic information about why the operation failed. The line should wrap both the original error and ErrAuction, or include the original error message in the format string.

Copilot uses AI. Check for mistakes.
return ErrNoBids
}
// #2
if !tx.IsPurgeable && len(tx.Attempts) > 1 {
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

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

The condition checks len(tx.Attempts) > 1 but accesses tx.Attempts[0] on line 207. If the slice is empty or has only one element when this condition is false, accessing index 0 in case #3 (line 211) could cause a panic. The logic should ensure that tx.Attempts has at least one element before accessing index 0, or the conditions need to be restructured to guarantee safe access.

Suggested change
if !tx.IsPurgeable && len(tx.Attempts) > 1 {
if !tx.IsPurgeable && len(tx.Attempts) > 0 {

Copilot uses AI. Check for mistakes.
@dimriou dimriou force-pushed the oev-795_split_meta_client_from_rpc_errors branch from 231b531 to 4a2d4cc Compare February 2, 2026 12:50
@dimriou dimriou marked this pull request as draft February 2, 2026 15:54
@dimriou dimriou marked this pull request as ready for review February 3, 2026 12:15
var _ txm.Client = &MetaClient{}

type MetaClientTxStore interface {
UpdateSignedAttempt(_ context.Context, txID uint64, attemptID uint64, signedTransaction *evmtypes.Transaction, fromAddress common.Address) error
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This could use a comment/bit of documentation

cll-gg
cll-gg previously approved these changes Feb 3, 2026
@dimriou dimriou enabled auto-merge (squash) February 3, 2026 15:11
@dimriou dimriou merged commit cb489cf into develop Feb 3, 2026
33 checks passed
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.

6 participants