eth: wallet-declared privatePending hint for nonce + gas routing (backs design gist) - #1639
Open
pragmaxim wants to merge 8 commits into
Open
eth: wallet-declared privatePending hint for nonce + gas routing (backs design gist)#1639pragmaxim wants to merge 8 commits into
pragmaxim wants to merge 8 commits into
Conversation
pragmaxim
force-pushed
the
fix/eth-alt-provider-1573-1629
branch
from
July 21, 2026 19:21
fa0e5dc to
696adbf
Compare
pragmaxim
force-pushed
the
feat/eth-private-pending-hint
branch
from
July 21, 2026 19:21
b8be7ba to
b8093ce
Compare
pragmaxim
force-pushed
the
fix/eth-alt-provider-1573-1629
branch
from
July 23, 2026 06:08
696adbf to
b8d7460
Compare
pragmaxim
force-pushed
the
feat/eth-private-pending-hint
branch
2 times, most recently
from
July 23, 2026 07:26
ffe0d7b to
ee3c5af
Compare
pragmaxim
marked this pull request as ready for review
July 23, 2026 08:55
pragmaxim
added a commit
to trezor/trezor-suite
that referenced
this pull request
Jul 23, 2026
…delivery + prune) Second of three stacked changes for the blockbook privatePending hint (trezor/blockbook#1639). Adds the durable model and the delivery/prune machinery; no producer writes yet, so the slice stays empty and this is still a functional no-op end to end. Why a dedicated slice: the private/relay bit is transient (computed only at broadcast in getMevProtectedTxData) and lives on neither the fake nor the real tx object, and blockbook overwrites the fake pending tx with its real (unmarked) tx mid-flight - so a tx-object marker cannot survive and a tx-list-derived set cannot tell private from public. A small Redux slice keyed by (accountKey, nonce) is the sole authoritative source; nonce is invariant across the fake->real swap. - new suite-common/wallet-core/src/privatePending slice (Redux only for now; IndexedDB persistence deferred - Redux already survives WS reconnect, only an app restart mid-tx regresses, matching today's heuristic fallback); - DELIVERY: attach the hint (and confirmedNonce:true) to the UNCONDITIONAL basic getAccountInfo call in fetchAndUpdateAccountThunk, gated on there being an in-flight private nonce. It must ride the basic call, not the gated details:'txs' one, because a blockbook-invisible private tx does not make the account "outdated" so the txs call is skipped for exactly the flows that need the hint; - PRUNE (STOP): drop entries with nonce < confirmedNonce (mined) or past a TTL backstop, on every basic refresh - instance-agnostic, so it self-heals across load-balanced blockbook replicas. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
pragmaxim
added a commit
to trezor/trezor-suite
that referenced
this pull request
Jul 23, 2026
… privatePending Third of three stacked changes for the blockbook privatePending hint (trezor/blockbook#1639). First change with observable behavior: it fills the slice from the two flows whose signed nonce is already in scope, and routes their gas estimates. - isMevPrivateSend(symbol, isMevProtectionEnabled) predicate (wallet-utils) - the exact condition under which getMevProtectedTxData broadcasts through the relay; - record the private nonce+txid after a successful pushTransaction in the send flow (pushSendFormTransactionThunk) and in WalletConnect eth_sendTransaction, gated by isMevPrivateSend so public sends never declare; - adopt buildEvmEstimateSpecific at the send and allowance estimate sites, passing the account's private-pending nonces so the estimate simulates against the relay's pending-private state - presence-only, so a keystroke with nothing in flight never routes to the relay. Deferred (see PR description): staking / Merkl-claim / stablecoin-yield writes need signed-nonce capture first (PrecomposedTransactionFinal carries no nonce); until then those private flows fall back to blockbook's recentSenders heuristic, which is acceptable because #1639 keeps that heuristic as the fallback. IndexedDB persistence also deferred. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
pragmaxim
force-pushed
the
feat/eth-private-pending-hint
branch
2 times, most recently
from
July 28, 2026 12:50
61976d9 to
e8b2293
Compare
Add an optional privatePending field to the getAccountInfo WebSocket request so
a wallet can declare the in-flight private (alternative send-tx / relay)
transactions it is tracking for an Ethereum-like address. A private relay
exposes no mempool, so a tx pending only there is invisible to the public
backend RPC; today Blockbook infers "this address may have a private pending
tx" from which addresses recently sent through this instance (recentSenders),
which is fragile across restarts and load-balanced replicas. The wallet already
knows this authoritatively.
This commit is plumbing only — no behavior change:
- WsPrivatePending{nonces, txids} + WsAccountInfoReq.PrivatePending (ws_types.go)
and the matching blockbook-api.ts types (hand-edited to keep the diff minimal,
as the checked-in file predates the current typescriptify version).
- privatePendingNonces() extracts the declared nonces, capped and defensively
copied, into a new AddressFilter.PrivatePendingNonces carried to the worker.
- The field is parsed and threaded but not yet consumed; the short-circuit that
routes the nonce lookup on it follows in the next commit.
Only nonces drive behavior; txids are accepted for forward compatibility. Unknown
request fields remain ignored (json.Unmarshal), so an older Blockbook tolerates a
wallet that sends the field before this ships.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Consume the privatePending nonces declared on getAccountInfo (previous commit). When a request carries them, EthereumTypeGetNonces routes the lookup to the alternative send-tx provider deterministically instead of consulting the recentSenders heuristic, and raises the reported pending nonce to max(declared nonce)+1. The wallet knows its own in-flight private nonces authoritatively, so this is correct even for a tx this instance never cached — one accepted by a different load-balanced replica, or lost to a restart — the two cases the recentSenders inference cannot cover. - EthereumTypeGetNonces takes a variadic privatePendingNonces ...uint64, keeping every existing caller and test call site source-compatible; only the worker passes filter.PrivatePendingNonces... The interface and the base / metrics / Tron / fake implementations carry the parameter (Tron has no relay, ignores it). - declaredPendingFloor computes the floor; raiseToFloor folds it over the cache-derived floor on both the provider-success and primary-fallback paths. - Gated on a configured provider: with no relay the hint is ignored. The recentSenders path remains as the fallback for requests without the hint, so this is additive; retiring it can follow once wallet adoption is confirmed. A metric distinguishing hint-routed from heuristic-routed lookups is left as a follow-up. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 10-agent review of the two feature commits found one latent correctness smell: privatePendingNonces() capped with n[:maxPrivatePendingNonces], keeping the FIRST N by position. Only the highest declared nonce drives the pending floor, and nonces are not guaranteed sorted, so an over-cap request whose maximum sits past index N would compute the floor too low (weaker protection). Unreachable for an honest wallet (a handful of sequential in-flight nonces), but the code then contradicted its own "only the highest matters" comment. Collapse an over-cap array to its single highest value instead of positional truncation, so the floor is correct regardless of order. Tests updated to assert the boundary (==cap kept in full) and that the max survives an over-cap ascending array. Also tidied a self-correcting comment in nonce_hint_test.go. Reviewers found no blockers or majors otherwise: correct on all paths, race-free, fully backward-compatible, the hand-edited blockbook-api.ts matches the generator, and trezor-suite can populate the field from its own pending-tx set. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
estimateFee carries its Ethereum parameters in a freeform `specific` object, so
no struct change is needed to accept the field — but the wire contract should
say it exists. Extend the WsEstimateFeeReq.specific ts_type (and the generated
blockbook-api.ts) with the optional privatePending {nonces, txids}, the same
shape declared top-level on getAccountInfo. Consumption (routing the estimate on
it) follows in the next commit; this commit is contract-only, no behavior change.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Consume the privatePending declared on the estimateFee request's specific object
(previous commit). When present, EthereumTypeEstimateGas routes eth_estimateGas
to the alternative send-tx provider deterministically instead of consulting the
recentSenders/useForNonces heuristic — the gas half of the same declared-routing
change already applied to the nonce path. Together the two paths are the only
consumers of the recentSenders machinery, which a later change can then retire.
Unlike the nonce hint, a declared estimate is only a routing signal: the wallet
cannot simulate gas itself, so Blockbook still calls the relay — but now against
the right pending-private state (e.g. a privately-submitted approve a following
swap's gas depends on) rather than guessing from recent sends. The declaration is
authoritative and independent of this instance having accepted the send, so it
covers the restart / load-balanced-replica gaps the heuristic cannot.
- estimatePrivatePendingDeclared reads privatePending.nonces presence from the
freeform params (no signature change — specific is already map[string]interface{}).
- estimateParamsWithoutPrivatePending strips the bookkeeping field from the
forwarded eth_estimateGas call object (copy-on-write; zero-cost when absent).
- Non-declaring requests are unchanged: they fall back to useForNonces (#1629),
and a missing from or no provider still takes the primary backend.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review follow-ups for the wallet-declared privatePending hint; no behavior change. - docs/evm-send.md: add the "Wallet-declared privatePending hint" section the ts_doc/generated contract already point to but that was never written, including the accepted trust-boundary rationale (the hint is an unauthenticated, per-request signal: it routes only the caller's own request to the relay, never touches shared state, and does not re-open the #1629 hot-path quota drain). - server/ws_types.go: reference WsPrivatePending by name in WsEstimateFeeReq.specific instead of duplicating the inline {nonces?;txids?} shape (single source of truth); document that Nonces entries are literal (0 is meaningful, not a sentinel). Mirror both edits into the generated blockbook-api.ts by hand (matching the file's style). - bchain/coins/eth/ethrpc.go: note the trust boundary at the estimate routing gate. - tests: add the production confirmedNonce=true + declared-floor combination (floor raises only pending, leaves confirmed untouched), a declared-nonce-0 routing case, the MaxUint64 benign-overflow boundary, and an estimate-side wire-decode test (specific.privatePending through a real WsEstimateFeeReq envelope). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
raiseToFloor(pending, floor) is exactly max(pending, floor); go.mod declares go 1.25.0 and the builtin max is already used elsewhere in the tree. Drop the helper, use max at both nonce-floor call sites, and collapse declaredPendingFloor's loop to floor = max(floor, n+1). Pure refactor, no behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…de-off Document two previously-implicit contract points on the privatePending hint: - estimateFee URL selection is best-effort (nonceURL, else urls[0]); unlike the nonce floor, gas has no client value to compensate a wrong relay node, so a declared-but-unknown sender may miss a predecessor held by another relay. - the deliberate trade-off vs pre-#1629 behavior: hint-less senders whose private tx was accepted by another replica are estimated on the primary RPC; widening routing for them would reopen the #1629 drain, so declaring the hint is the fix. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
pragmaxim
force-pushed
the
feat/eth-private-pending-hint
branch
from
August 5, 2026 06:53
e8b2293 to
fbf7d53
Compare
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.
Draft — proposal, backs the design note: https://gist.github.com/pragmaxim/f71b03a4ed4c0030b6cba7fb0a302e96
Lets a wallet declare its in-flight private (alternative send-tx / relay) transactions on its EVM read requests, so Blockbook routes the nonce lookup and the gas estimate deterministically from authoritative wallet state instead of inferring "did this instance recently accept a send" via the
recentSendersheuristic (fragile across restarts and load-balanced replicas).The field is added in two places — top-level
privatePendingongetAccountInfo, and inside the existingspecificobject onestimateFee— becauseEthereumTypeGetNoncesandEthereumTypeEstimateGasare the only two consumers of therecentSenders/useForNonces/nonceURLmachinery. Declaring both is the prerequisite for a later change to retire that machinery (not done here; this is additive and keeps the heuristic as the fallback). The contract and its trust boundary are documented indocs/evm-send.md.Commits
WsPrivatePending{nonces,txids}+ blockbook-api.ts +AddressFilter.PrivatePendingNonces(plumbing, no behavior change).EthereumTypeGetNoncesroutes to the relay when declared and raises the pending nonce tomax(declared)+1; correct even for a tx this instance never cached.specificts_type (contract only).EthereumTypeEstimateGasroutes on the declared hint; strips the bookkeeping from the forwardedeth_estimateGascall.Notes
useForNoncesfallback); older Blockbook ignores the field (json.Unmarshal).privatePendingis an unauthenticated, per-request client hint. It is never written intorecentSendersor the pending-tx cache, so a hostile client can distort only its own request's answer — never shared state — and it does not re-open the eth: every gas estimate is sent to the alternative provider, burning its rate-limit quota #1629 hot-path quota drain, since a normal wallet declares it only when it genuinely has a private tx in flight. Documented indocs/evm-send.md.Review follow-ups
A broad multi-angle, adversarially-verified review found no blockers. Applied in the last commit (no behavior change):
privatePendinghint" section ofdocs/evm-send.mdthat thets_doc/generated contract already pointed to, including the trust-boundary note above.WsEstimateFeeReq.specificnow referencesWsPrivatePendingby name instead of duplicating the inline{nonces?; txids?}shape (single source of truth); documented thatnoncesentries are literal (0 is meaningful, not a sentinel). Both mirrored into the generatedblockbook-api.tsand verified to match generator output.confirmedNonce=true+ declared-floor combination (floor raises only pending, leaves confirmed untouched), a declared-nonce-0routing case, theMaxUint64benign-overflow boundary, and an estimate-side wire-decode test (specific.privatePendingthrough a realWsEstimateFeeReqenvelope).blockbook-api.tsis globally stale vs the generator (2-space vs 4-space indent + a couple of missing Tron fields) — pre-existing onmaster/base and passing the type-parity CI gate; these additions match generator output modulo that indent. A full regeneration is a separate cleanup.🤖 Generated with Claude Code