When xOneRS switches to another branch, it takes the transactions of the blocks it left and removes those that the new branch already includes. It offers the rest to the transaction pool once, through normal admission on the new head. The state of the new head decides which ones stay. If the switch is deeper than 64 blocks, nothing is reinjected.
This post covers the pool side of a reorg. For the state side, read the reorg post-mortem for PR #606 and spec/chain/reorg-and-set-head.md. The rule comes from xOneRS PR #434, merged on 2026-11-14, and from spec/txpool/canonical-head.md.
What happens to the transactions of the abandoned branch?
The node records them at the moment of the switch and hands them to the pool once. Core::adopt unwinds the old blocks, imports the new ones, and then stores a BranchDelta. The delta holds four things:
- the encodings of the transactions of the blocks taken off;
- the hashes of the transactions of the blocks put on;
- the depth
|old head - new head|, as geth measures it; - the hash of the new head.
note_branch stores the delta and announces the head again. The pool runs on that event and calls take_branch, which hands the delta out once. See core.rs.
PR #434 replaced PR #430, which kept a pool-own cache of observed blocks. That plan reinjected nothing for a branch the pool never saw. The delta needs no observation: adopt holds the dropped blocks in memory.
Which transactions come back, and which are dropped?
Only "discarded minus included" is offered. Each of those goes through the same admission as a new transaction. The spec lists the checks that run again: nonce, balance, fork, fee, special-transaction authority and pool bounds.
| Transaction | Result |
|---|---|
| On the new branch too | Not offered. Its hash is in the included set. |
| Nonce already used on the new branch | Refused: NonceTooLow. |
| Sender cannot pay on the new head | Refused: InsufficientFunds. |
| Offered a second time | Refused: already known. |
| Valid on the new head | Back in the pool. |
| Switch deeper than 64 blocks | None offered. |
The builder tests listed below assert these results.
How does nonce handling work?
The pool resets first, then reinjects, on the same head. PoolShared::reset reads the number, hash and gas limit of the tip in one read. Pool::reset revalidates what the pool holds on that state. The reinjection then runs on the state of that same hash.
The nonce comes from the new head state. In crates/xone-builder/tests/reorg.rs, sender a is back at nonce 0 on the new head. Its two transactions at nonces 0 and 1 are both admitted as pending. In another test, the new branch has spent nonce 0 with a different transaction. The old transaction is refused with NonceTooLow { next: 1, tx: 0 }, and the pool stays empty.
What about sign transactions (0x89) and other special transactions?
They follow the same path, with the extra authority rule. A special transaction targets 0x89 (block signers) or 0x90 (randomize). It is admitted only from an address that the authority accepts, as spec/txpool/special-txs.md says.
- On a V2 head, that set is the epoch's candidate pool: masternodes, standby candidates and penalised candidates. A penalised node can still send sign transactions, which is how it comes back. See penalties and comeback.
- While the node syncs, nobody is authorised.
- A sign transaction from any other address is refused:
special transaction sender is not a signer. - A sign transaction skips the price floor. It goes straight to pending when its nonce is the sender's pending nonce and its calldata has the known shape.
The txpool_reorg.rs test uses real Apothem blocks 16 and 17, which carry sign transactions. Their senders are set as candidates, and the transactions come back.
One gap is open. The producer sets the authority, and reset does not re-read it. A candidate who leaves or returns between the two heads is not noticed. PR #434 lists this as not covered.
What if several switches happen, or the reorg is deep?
Switches before the pool runs merge into one delta (BranchDelta::merge):
- a transaction put on by one switch and taken off by a later one is discarded again;
- one taken off and then put back counts as included;
- the depths add up.
Past MAX_REORG_DEPTH (64) the lists are dropped and only the depth is kept. The pool follows the head and reinjects nothing. Memory holds the transactions of at most 64 blocks. The pool runs in its own task, so fork choice and import never wait for it.
Which geth-XDC lines does the spec follow?
The spec cites core/txpool/legacypool/legacypool.go in geth-XDC at commit 6fc8e8c2088b7f18264f5663036669d04fa7becf:
reset, lines 1736-1816: discarded minus included;- line 1747: skip when the depth is above 64;
- lines 1868-1872:
addTxsLockedon the reinject set.
xOne follows the same rule on all three points. The delta replaces geth's ancestor walk over its chain store.
How is it tested?
crates/xone-node/tests/txpool_reorg.rs,a_branch_switch_reinjects_discarded_minus_included_once: real Apothem blocks 16 and 17. Only the not-included transactions come back, a repeated head event takes nothing back, and the same delta delivered twice is refused as already known.merged_switches_follow_the_last_branch_and_stay_bounded: a flip-flop merge and a fork storm past 64.crates/xone-builder/tests/reorg.rs:reorg_of_two_blocks_reinjects_only_eligible_transactions,nonce_already_used_on_the_new_branch_is_not_reinjected,deep_reorg_resets_without_reinjection.- Mutant: with the delta ignored in
PoolShared::reset, the reinjection test fails.
The producer has a second path: when a leader takes an uncertified head off to build a sibling, it calls reinject. The sibling.rs test covers it.
What is still open?
Issue #63 is still open. No test compares xOne with pinned geth on a controlled fork (pending, included on A, A replaced by B, reinclusion). That test needs two producers on a private devnet with a forced fork. No test feeds a real peer branch through adopt, because the Apothem seals cannot be re-signed. The tests feed a delta of the same shape through note_branch.
The RPC surface post lists the txpool namespace.
Frequently asked questions
Does xOne reinject every transaction of the abandoned branch?
No. It offers only the transactions that the new branch does not include. Each one must then pass admission on the new head: nonce, balance, fork, fee, special-transaction authority and pool bounds.
What happens to a sign transaction after a reorg?
It is offered like any other transaction. It is admitted if its sender is in the authority set, which on a V2 head includes penalised candidates. Otherwise it is refused.
How deep a reorg is reinjected?
Up to 64 blocks, the same bound as geth. Past MAX_REORG_DEPTH the pool follows the new head and reinjects nothing.
Sources
This article describes the source and evidence at its review date. Check the linked repository for current implementation and qualification status.
Explore the engineering evidenceEdit this page on GitHub opens the Markdown source (content/blog/reorg-txpool-reinject/index.md). GitHub may ask you to sign in. Report a correction opens a prefilled GitHub issue for this page.