Skip to content

Add gas tiering for multiOFA client#462

Open
dimriou wants to merge 4 commits intodevelopfrom
oev-1287_separate_gas_limit
Open

Add gas tiering for multiOFA client#462
dimriou wants to merge 4 commits intodevelopfrom
oev-1287_separate_gas_limit

Conversation

@dimriou
Copy link
Copy Markdown
Contributor

@dimriou dimriou commented May 6, 2026

SVR needs an easy way to keep track which OFA included the transaction on-chain. To accomplish this we reserve the last two digits of the gas limit and we assign 10 values space for each one of them, for example:

  • OFA1: 10 - 19
  • OFA2: 20 - 29
    ... and so on.
    For now only the first position is used, i.e. 10, 20, etc. However in the future we might want to include per-feed tiering across each OFA, so we reserve 10 spaces upfront.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 6, 2026

⚠️ API Diff Results - github.com/smartcontractkit/chainlink-evm

⚠️ Breaking Changes (1)

pkg/txm/clientwrappers/dualbroadcast (1)
  • FlashbotsTxStore — 🗑️ Removed

✅ Compatible Changes (1)

pkg/txm/clientwrappers/dualbroadcast (1)
  • NewMultiOfaClient — ➕ Added

📄 View full apidiff report

return nil, fmt.Errorf("failed to sign tiered attempt for txID: %v: %w", tx.ID, err)
}

d.lggr.Infow("Intercepting attempt for tx", "txID", tx.ID, "hash", signedTx.Hash(), "toAddress", tx.ToAddress, "gasLimit", tieredGasLimit,
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.

Does this need to be info level?
I'd rather have an info level log after the attempt has been sent to the OFA to print all the context

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.

The existing log has inaccurate information of the transaction since it has the wrong details (gasLimit). This log is the only way to get the hash of the different OFAs accordingly so we can track them on-chain.

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.

What if we would log this directly after a tx gets sent to an OFA? Then the hash and gasLimit would be correct and we wouldn't have to distinguish mentally between intercepted and non-intercepted attempts.

Comment thread pkg/txm/clientwrappers/dualbroadcast/ofa_backend.go
Comment thread pkg/txm/clientwrappers/dualbroadcast/multiofa_client.go
@@ -38,10 +36,10 @@ type multiOfaClient struct {
var _ txm.Client = (*multiOfaClient)(nil)

// newMultiOfaClient builds backends from URLs: index 0 is primary (outcome and nonces); the rest are secondaries.
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
// newMultiOfaClient builds backends from URLs: index 0 is primary (outcome and nonces); the rest are secondaries.
// NewMultiOfaClient builds backends from URLs: index 0 is primary (outcome and nonces); the rest are secondaries.

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.

We need to be able to import this client for local testing, similar to the rest of the clients.

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.

Ah ok, clear. Can you point me to the way you do local testing for this? Is there a doc/some guidelines?

// createAttemptWithTiering creates a tiered attempt that has a different gas limit for each OFA backend, by reserving the last two digits.
// Each OFA has 10 values spaced 10 units apart for the gas limit so there is room for feed-specific tiers per OFA in the future.
// Unfortunately, the per-OFA attempts are not tracked by the txm. We can track them by looking at the individual logs per attempt stored.
func (d *ofaBackend) createAttemptWithTiering(ctx context.Context, tx *types.Transaction, attempt *types.Attempt) (*types.Attempt, 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.

I'd prefer to not use the keyword tiering here since this is more about tracking, and the term might get confused with feed tiering

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.

Any suggestions?

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 createAttemptWithAccountability or createAttemptForOFAAccountability?
We can add to the docs that we've reserved space to support feed tiering in the future.

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.

How about createAttemptWithTracedFee

Comment thread pkg/txm/clientwrappers/dualbroadcast/ofa_backend.go
Comment thread pkg/txm/clientwrappers/dualbroadcast/ofa_backend.go
lhmerino
lhmerino previously approved these changes May 6, 2026
Copy link
Copy Markdown

@lhmerino lhmerino left a comment

Choose a reason for hiding this comment

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

Looks great, thank you!

I have just one safety comment regarding the gas limit.

if suffix >= 100 {
return 0, fmt.Errorf("cannot calculate OFA gas limit tier for %s: suffix %d is not two digits", k.name(), suffix)
}
return gasLimit - gasLimit%100 + suffix, 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.

I would suggest bumping the gas limit to the next hundred to ensure the new gas limit is always greater than or equal to the original gas limit for safety reasons.
Could be as simple as the following statement after applying the suffix change tieredLimit := gasLimit - (gasLimit % 100) + suffix:

if tieredLimit < gasLimit {
		tieredLimit += 100
}

Copy link
Copy Markdown

@lhmerino lhmerino May 6, 2026

Choose a reason for hiding this comment

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

Following on, might also be nice to make 100 be a constant since its used in multiple locations as well.

@dimriou dimriou marked this pull request as ready for review May 8, 2026 09:06
@dimriou dimriou requested a review from a team as a code owner May 8, 2026 09:06
Copilot AI review requested due to automatic review settings May 8, 2026 09:06
@dimriou dimriou requested a review from a team as a code owner May 8, 2026 09:06
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 introduces gas-limit “tiering” for dual-broadcast via the multi-OFA client so downstream systems can infer which OFA relay included a transaction on-chain by inspecting the last two digits of the gas limit.

Changes:

  • Re-sign dual-broadcast transactions with a tiered gas limit suffix prior to OFA submission.
  • Export and refactor NewMultiOfaClient to depend on narrower interfaces and pass keystore support through to Nova.
  • Update/add unit tests and fixtures to use dynamic-fee transactions and validate tiering behavior.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
pkg/txm/clientwrappers/dualbroadcast/selector.go Switches to the exported NewMultiOfaClient constructor.
pkg/txm/clientwrappers/dualbroadcast/ofa_backend.go Adds gas-limit tiering via re-signing; refactors keystore/txstore interfaces.
pkg/txm/clientwrappers/dualbroadcast/ofa_backend_test.go Updates fixtures to dynamic-fee txs, adds tiering tests, introduces a test keystore helper.
pkg/txm/clientwrappers/dualbroadcast/multiofa_client.go Exports constructor and adjusts dependencies to interfaces; passes keystore to Nova client creation.
pkg/txm/clientwrappers/dualbroadcast/multiofa_client_test.go Updates tests to use NewMultiOfaClient and provide a keystore where needed.

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

@@ -38,10 +36,10 @@ type multiOfaClient struct {
var _ txm.Client = (*multiOfaClient)(nil)

// newMultiOfaClient builds backends from URLs: index 0 is primary (outcome and nonces); the rest are secondaries.
Comment on lines +194 to +196
if !attempt.Fee.ValidDynamic() {
return nil, errors.New("attempt is not a dynamic fee attempt") // We could potentially support legacy transactions, but dynamic transactions are the default so this is ok.
}
GasTipCap: attempt.Fee.GasTipCap.ToInt(),
Data: tx.Data,
}

Comment on lines +229 to +234
func (k ofa) tieredGasLimit(gasLimit uint64) (uint64, error) {
suffix := (uint64(k) + 1) * 10
if suffix >= tieringSpace {
return 0, fmt.Errorf("cannot calculate OFA gas limit tier for %s: suffix %d is not in the range 0-%d", k.name(), suffix, tieringSpace-1)
}
tieredLimit := gasLimit - gasLimit%tieringSpace + suffix
alejoberardino
alejoberardino previously approved these changes May 8, 2026
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