Conversation
… of max_fee_per_gas Expiring-nonce transactions (TIP-1009) need distinct signed payloads even when two generated transactions are otherwise identical. txgen achieved that by bumping `max_fee_per_gas` with a monotonic counter, which leaks straight into the node's pool priority: the pool ranks by effective tip per gas, so every newly generated transaction outranked the ones already queued and the pool selected newest-first. Under load the oldest entries starved until their `valid_for_secs` expiry, producing periodic eviction waves. Route the same `encoded_uniqueness` value into `valid_after` instead. It is signed (so the hash still changes), but it is invisible to pool ordering and to block packing, and the node only bounds it from above, so small positive values are always satisfiable. Templates that set `valid_after` explicitly keep their value and fall back to the previous fee bump, with a one-time warning.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Expiring-nonce Tempo transactions (TIP-1009,
nonce_key == U256::MAX) have no sequential nonce, sotheir replay protection is hash-based: two otherwise identical generated transactions must not
produce the same signed payload. txgen guaranteed that by adding a monotonic counter-derived bump to
max_fee_per_gas.That bump leaks straight into the node's transaction-pool priority. The Tempo pool ranks by
effective tip per gas, the standard EIP-1559
min(max_priority_fee_per_gas, max_fee_per_gas - base_fee)(crates/transaction-pool/src/ordering.rs,EvictionOrderKeyincrates/transaction-pool/src/tt_2d_pool.rs; highest priority first, oldest first only on exactties). The bench presets set
max_fee_per_gas == max_priority_fee_per_gas == 100 gweiand underload the T7 base fee sits at its 12 gwei cap, so the effective tip was
88 gwei + bumpand grewstrictly with generation order. The pool therefore selected newest-first.
In the multi-region bench run 35329155258 the oldest 20-30k of the 50k-entry pool starved until
their 25 s
valid_for_secsexpiry. That produced eviction waves every 26 s, ~81k expiredtransactions (7% of included volume), and visibly smaller blocks right after each wave.
Fix
Route the same
encoded_uniquenessvalue intovalid_afterinstead ofmax_fee_per_gas. Thecounter derivation is unchanged, only the destination field is different.
valid_afteris safe for this:TempoTransaction), so the transaction hashstill changes.
gas_pricefeed the effective tip,gas_limitdrives how builders pack, and calldata length is validated strictly by some Tempoprecompiles - none of those can carry the value.
valid_afterfrom above: the pool validator checksvalid_after <= now + aa_valid_after_max_secs(
crates/transaction-pool/src/validator.rs::ensure_pool_time_bounds, backed byensure_valid_afterincrates/primitives/src/transaction/tempo_transaction.rs), pool-side EVMvalidation skips the check entirely (
crates/evm/src/pool.rs), and execution only requiresblock_timestamp >= valid_after(validate_time_windowincrates/revm/src/handler.rs). Smallpositive values such as
1 + encoded_uniquenessare always in the past and always satisfiable.Option<NonZeroU64>, hence values start at1. The protocol also requiresvalid_before > valid_after; that invariant is now checked explicitly and produces a clear errorinstead of a transaction the node would reject.
Determinism is preserved: scenario-assigned identities still encode to odd values (
2 * hint + 1)and ordinary generation counters to even ones (
2 * counter), so the two domains stay disjoint andretrying a scenario step reproduces the same identity. Since
valid_after = 1 + encoded_uniqueness,scenario identities land on even
valid_aftervalues and generation counters on odd ones.Templates with an explicit
valid_afterThe template value wins - a workload that pins
valid_afteris assumed to be exercising the timewindow on purpose, and silently overwriting it would change the behaviour the spec asked for. Those
templates fall back to the previous
max_fee_per_gasbump for uniqueness and print a one-timewarning on stderr explaining that the fallback makes the pool prefer newly generated transactions.
The trade-off is documented in the function's doc comment and in the README.
Deferred (
--defer-signing) relative expiry is unaffected: the late signer clones the request andonly sets
valid_before, so the uniqueness value carries through to the final signature.Test plan
Commands run locally (macOS,
rustc 1.99.0-nightly):cargo fmt --all/cargo fmt --all --checkcargo build --workspacecargo clippy --workspace --all-targets -- -D warnings.github/workflows/lint.yml)cargo test -p txgen-tempocargo test --workspace --no-fail-fast./tests/decode-tx.sh txgen-tempo tests/specs/tempo-all-types.yaml./tests/decode-tx.sh txgen-ethereum tests/specs/ethereum-all-types.yamlUpdated tests:
test_expiring_nonce_max_fee_bumps_leave_zero_priority_fee_unchanged->test_expiring_nonce_uniqueness_leaves_fees_unchanged: both fee fields stay exactly asconfigured,
valid_afterwalks1, 3, 5, ....test_sponsored_expiring_nonce_uniqueness_happens_before_fee_payer_signing: asserts identicalfees and differing
valid_after, and still asserts the fee-payer signature differs.scenario_expiring_nonce.rs: the probe's "scenario identity" detector now keys off an evenvalid_afterinstead of an evenmax_fee_per_gasdelta, and the concurrency test assertsidentical fees with differing
valid_after.New tests:
test_expiring_nonce_uniqueness_does_not_change_pool_priority: two generated transactions fromthe same template have identical
max_fee_per_gas,max_priority_fee_per_gas,gas_limitandcalldata, differing
valid_after, and differing signed payloads/hashes.test_expiring_nonce_uniqueness_keeps_explicit_valid_after: a template with an explicitvalid_afterkeeps it and falls back to the fee bump.test_expiring_nonce_uniqueness_rejects_unusable_valid_before: avalid_beforeat or below theuniqueness value is rejected with a clear error.
test_expiring_nonce_scenario_identities_stay_disjoint_from_counters: generation counters land onodd values, scenario identities on even ones, and a retried scenario step reproduces its identity.
End-to-end spot check with
cast decode-txontests/specs/tempo-all-types.yaml, showing constantfees, unique
valid_afterand unique hashes:Pre-existing failure
bench-clicall_replay::replays_a_mixed_corpus_identically_twicefails withassertion left == right failed, left: 4, right: 5. It fails identically on a cleanorigin/mainworktree (verified at 41bddc3), so it is unrelated to this change.
scenario_two_chainpasseslocally here.
🤖 Generated with Claude Code