Skip to content

Commit 696adbf

Browse files
pragmaximclaude
andcommitted
docs(eth): document the EVM private-relay broadcast + pending-tx cache
Add docs/evm-send.md: how Blockbook broadcasts a Trezor Suite EVM transaction through the private send-tx relay and reconciles its own pending-transaction cache (RBF/cancel eviction on relay ACK, generation ordering, single-metered removal, background reconcile vs read-path eviction), with a Mermaid flowchart and the cache-lifecycle metrics. Fee estimation is cross-linked to fees.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 4cef7da commit 696adbf

2 files changed

Lines changed: 96 additions & 0 deletions

File tree

docs/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@
1010
* [API](/docs/api.md) – Description of Blockbook API
1111
* [API (Tron specifics)](/docs/api-tron.md) – Tron-specific behavior and data extensions for API V2
1212
* [Sync](/docs/sync.md) – Sync-loop architecture and the `missingBlockRetry` troubleshooting knobs
13+
* [EVM send](/docs/evm-send.md) – EVM transaction broadcast through the private send-tx relay and its pending-transaction cache
1314
* [Testing](/docs/testing.md) – Description of tests used during Blockbook development

docs/evm-send.md

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# EVM transaction submission
2+
3+
This page documents how Blockbook broadcasts a **Trezor Suite** EVM transaction through the
4+
private send-tx relay and tracks it in its own pending-transaction cache until the chain confirms
5+
or supersedes it. Fee estimation — a separate step of the same send flow — is covered in
6+
[fees.md](/docs/fees.md) and is not repeated here.
7+
8+
The private-relay pieces (the alternative send-tx provider and its pending-tx cache) are active
9+
only when a coin is configured with the `*_ALTERNATIVE_SENDTX_URLS`, `*_ALTERNATIVE_SENDTX_ONLY`,
10+
and `*_ALTERNATIVE_FETCH_MEMPOOL_TX` environment variables. Without them Blockbook sends through
11+
the normal backend RPC and the cache path below is skipped.
12+
13+
## Broadcast and the pending-transaction cache
14+
15+
`SendRawTransaction` broadcasts and — when the relay accepts and the coin runs in
16+
`ALTERNATIVE_SENDTX_ONLY` + `ALTERNATIVE_FETCH_MEMPOOL_TX` mode — keeps its own pending-tx cache,
17+
because the relay exposes no mempool to reconcile against. A background loop and the read path
18+
reconcile that cache against the chain.
19+
20+
```mermaid
21+
%%{init: {"theme": "base", "themeVariables": {"lineColor": "#6b7280", "primaryTextColor": "#111827"}}}%%
22+
flowchart TD
23+
send["SendRawTransaction(hex, disableAlternativeRPC)"]
24+
route{"provider configured<br/>and not disabled?"}
25+
primary["primary eth_sendRawTransaction"]
26+
relay["broadcast to relay URLs"]
27+
acc{"any URL accepted?"}
28+
reject["onlyAlternative: return error<br/>else: fall back to primary"]
29+
reg["registerSuccessfulSend<br/>(record sender + URL, assign gen)"]
30+
evict["evictReplacedByNonce<br/>retire same-(from,nonce) predecessor<br/>on ACK (RBF / cancel), gen-ordered"]
31+
handle["handleMempoolTransaction<br/>fetch-back → cache pending (gen-ordered)<br/>→ AddTransactionToMempool → notify"]
32+
33+
subgraph rec ["reconcileMempoolTxs (every minute, per cached tx)"]
34+
mined["mined → evict"]
35+
super["nonce_superseded<br/>(confirmed nonce > tx nonce) → evict"]
36+
miss["provider_missing<br/>(relay stopped surfacing) → keep to timeout"]
37+
to["past cache timeout → evict"]
38+
end
39+
40+
readpath["GetTransaction read path<br/>expired entry → evict"]
41+
remove[("removeMempoolTx<br/>clear cache + wrapped mempool<br/>+ release nonce routing<br/>metered once")]
42+
43+
send --> route
44+
route -- "no" --> primary
45+
route -- "yes" --> relay --> acc
46+
acc -- "no" --> reject
47+
acc -- "yes" --> reg --> evict --> handle
48+
evict -. removes predecessor .-> remove
49+
handle -. caches new tx .-> readpath
50+
mined --> remove
51+
super --> remove
52+
to --> remove
53+
readpath --> remove
54+
55+
classDef normal fill:#e7f0ff,stroke:#4078c0,color:#10243e;
56+
classDef store fill:#e8f7ed,stroke:#2e8b57,color:#0b2c19;
57+
classDef error fill:#ffecec,stroke:#c03535,color:#3b0a0a;
58+
class send,route,relay,acc,reg,evict,handle,mined,super,miss,to,readpath,primary normal;
59+
class remove store;
60+
class reject error;
61+
```
62+
63+
Key invariants:
64+
65+
- **A same-`(from, nonce)` predecessor is retired the moment the relay ACKs its replacement**, from
66+
the raw hex — not by waiting for the relay to surface the replacement. A Blink drop-mode cancel
67+
is never surfaced and its nonce is never consumed on-chain, so without this the superseded tx
68+
lingered as "Unconfirmed" until the cache timeout. This is deliberately distinct from an *empty*
69+
`eth_getTransactionByHash` probe, which is **not** authoritative — a private relay stops
70+
surfacing a still-mineable tx while it stays broadcast, so `provider_missing` is kept until the
71+
timeout rather than evicted early. `mined` and `nonce_superseded` are the only deterministic
72+
early evictions the reconcile loop makes.
73+
- **Send generations order concurrent same-nonce sends.** Each accepted send gets a monotonic
74+
generation; an older submission's slow fetch-back neither caches itself over, nor evicts, a
75+
newer replacement that already holds the nonce slot.
76+
- **Every exit funnels through `removeMempoolTx`**, which clears the provider cache *and* the
77+
wrapped Blockbook mempool (address index), releases the sender's nonce routing once nothing
78+
private remains pending, and records the lifecycle metric exactly once (gated on the actual
79+
removal, so concurrent reconcile / read-path / RBF evictions of the same entry don't
80+
double-count).
81+
- **Removals are not pushed to the wallet.** Blockbook pushes only *added* txs; a wallet learns a
82+
pending tx is gone on its next account re-fetch (the initiating device also removes it
83+
optimistically). The cache timeout is the backstop for anything the deterministic evictions miss.
84+
85+
## Observability
86+
87+
Prometheus counters for the cache lifecycle:
88+
89+
- `blockbook_eth_alternative_mempool_reconciliation_events_total{action}` — cache exits by reason
90+
(`mined`, `nonce_superseded`, `provider_missing`, `timeout`, `rbf_replaced`, plus the kept
91+
actions `skipped_fresh`, `provider_missing_pending`, `kept`, `provider_error`).
92+
- `blockbook_eth_alternative_mempool_tx_residence_seconds{action}` — how long an entry lived before
93+
each eviction reason fired (e.g. `provider_missing` clustering near the timeout rather than at
94+
~1–2 min would flag a premature-eviction regression).
95+
- `blockbook_eth_alternative_mempool_cache_size` — current cache depth.

0 commit comments

Comments
 (0)