Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion yarn-project/ethereum/src/l1_tx_utils/l1_tx_utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ describe('L1TxUtils', () => {
address: l1Client.account.address,
});

// Next send fails at sendRawTransaction (e.g. network error)
// Next send fails at sendRawTransaction (e.g. network error / 429)
const originalSendRawTransaction = l1Client.sendRawTransaction.bind(l1Client);
using _sendSpy = jest
.spyOn(l1Client, 'sendRawTransaction')
Expand All @@ -163,6 +163,29 @@ describe('L1TxUtils', () => {
expect((await l1Client.getTransaction({ hash: txHash })).nonce).toBe(expectedNonce);
}, 30_000);

it('bumps nonce when getTransactionCount returns a stale value after a successful send', async () => {
// Send a successful tx first to advance the chain nonce
await gasUtils.sendAndMonitorTransaction(request);

const expectedNonce = await l1Client.getTransactionCount({
blockTag: 'pending',
address: l1Client.account.address,
});

// Simulate a stale fallback RPC node that returns the pre-send nonce
const originalGetTransactionCount = l1Client.getTransactionCount.bind(l1Client);
using _spy = jest
.spyOn(l1Client, 'getTransactionCount')
.mockImplementationOnce(() => Promise.resolve(expectedNonce - 1)) // stale: one behind
.mockImplementation(originalGetTransactionCount);

// Despite the stale count, the send should use lastSentNonce+1 = expectedNonce
const { txHash, state } = await gasUtils.sendTransaction(request);

expect(state.nonce).toBe(expectedNonce);
expect((await l1Client.getTransaction({ hash: txHash })).nonce).toBe(expectedNonce);
}, 30_000);

// Regression for TMNT-312
it('speed-up of blob tx sets non-zero maxFeePerBlobGas', async () => {
await cheatCodes.setAutomine(false);
Expand Down
15 changes: 14 additions & 1 deletion yarn-project/ethereum/src/l1_tx_utils/l1_tx_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ const MAX_L1_TX_STATES = 32;

export class L1TxUtils extends ReadOnlyL1TxUtils {
protected txs: L1TxState[] = [];
/** Last nonce successfully sent to the chain. Used as a lower bound when a fallback RPC node returns a stale count. */
private lastSentNonce: number | undefined;
/** Tx delayer for testing. Only set when enableDelayer config is true. */
public delayer?: Delayer;
/** KZG instance for blob operations. */
Expand Down Expand Up @@ -105,6 +107,11 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
this.metrics?.recordMinedTx(l1TxState, new Date(l1Timestamp));
} else if (newState === TxUtilsState.NOT_MINED) {
this.metrics?.recordDroppedTx(l1TxState);
// The tx was dropped: the chain nonce reverted to l1TxState.nonce, so our lower bound is
// no longer valid. Clear it so the next send fetches the real nonce from the chain.
if (this.lastSentNonce === l1TxState.nonce) {
this.lastSentNonce = undefined;
}
}

// Update state in the store
Expand Down Expand Up @@ -246,14 +253,20 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
);
}

const nonce = await this.client.getTransactionCount({ address: account, blockTag: 'pending' });
const chainNonce = await this.client.getTransactionCount({ address: account, blockTag: 'pending' });
// If a fallback RPC node returns a stale count (lower than what we last sent), use our
// local lower bound to avoid sending a duplicate of an already-pending transaction.
const nonce =
this.lastSentNonce !== undefined && chainNonce <= this.lastSentNonce ? this.lastSentNonce + 1 : chainNonce;

const baseState = { request, gasLimit, blobInputs, gasPrice, nonce };
const txData = this.makeTxData(baseState, { isCancelTx: false });

// Send the new tx
const signedRequest = await this.prepareSignedTransaction(txData);
const txHash = await this.client.sendRawTransaction({ serializedTransaction: signedRequest });
// Update after tx is sent successfully
this.lastSentNonce = nonce;

// Create the new state for monitoring
const l1TxState: L1TxState = {
Expand Down
Loading