# Erigon-1 (txpool reload drops self-transfers): disproof attempt

## VERDICT: CONFIRMED (txpool-only, cosmetic severity)

On reload from the database, `fromDBLocked` parses each persisted transaction with `withSender=false` and no sender buffer. No sender is ever attached to the parsed transaction before `validateTx`. `validateTx` therefore sees `GetSender()` return `ok=false`, and `IsSelfTransfer` evaluates to `false`. For an Amsterdam self-transfer, intrinsic gas is then overstated by `ColdAccountAccessEIP2780` (3,000) when the value is zero, or by 3,000 + `TxValueCostEIP2780` (6,000) = 9,000 when the value is nonzero. A persisted self-transfer whose gas limit is below the overstated figure but at least the spec figure is discarded with `IntrinsicGas`. The same transaction was admitted when first received, because the add path recovers and caches the sender. I found no check that rescues it, and no later commit that fixes it. The only impact is on the txpool: block validation is unaffected.

All code was read from raw.githubusercontent.com at the pin, not from a clone. Nothing was run against any network or node, and no tests were run.

## Strongest refutation attempt, and why it fails

I tried all five routes:

1. **Another check giving the spec result.** There is none. `validateTx` computes `gas` once from `mdgas.CalcIntrinsicGas`, using `IsSelfTransfer: senderOk && to != nil && txnSender.Value() == *to`. It returns `IntrinsicGas` if `gas > txn.GetGas()` (`txnprovider/txpool/pool.go#L1046-L1085`). There is no second, sender-aware intrinsic check before that return.
2. **The condition cannot occur on a valid chain.** It can. A self-transfer with gas ≥ spec intrinsic is valid in a block. The spec has 12,000 base for a self-transfer, against 15,000 or 21,000 for a non-self-transfer with zero or nonzero value. This also passes the normal pool add path, so it can be persisted.
3. **The spec was misread.** It was not. execution-specs `forks/bogota` at 7f2f4c9b, `src/ethereum/forks/amsterdam/transactions.py`, lines 736-747:
   ```
   is_create = tx.to == Bytes0(b"")
   is_self_transfer = tx.to == sender
   ...
   elif not is_self_transfer:
       recipient_execution_gas = GasCosts.COLD_ACCOUNT_ACCESS
       if tx.value > U256(0):
           recipient_execution_gas += GasCosts.TX_VALUE_COST
   ```
   The `is_self_transfer` assignment is at L737, and `is_create` at L736. Erigon's `CalcIntrinsicGas` implements this correctly when `IsSelfTransfer` is true: `execution/protocol/mdgas/intrinsic_gas.go#L77-L85`, with constants at `execution/protocol/params/protocol.go#L253-L255` (TX_BASE 12,000, TX_VALUE_COST 6,000, COLD_ACCOUNT_ACCESS 3,000). The defect is only that the input flag is wrong on reload.
4. **A later commit fixes it.** I found none. The pin's commit date is 2026-10-27T16:04:32Z, which is later than every commit in the first 40 results of the GitHub commits API for `txnprovider/txpool/pool.go`. The newest is dedabcf0 (#24978), dated 2026-10-27T09:04Z. I did not read the diffs of the reload-related commits dated before the pin: #24849 "refresh state views before pool reload", #24445 "copy pooled txn rows before parsing them in fromDB", #23511 "load pool from db under p.lock", and #25358 "stop keeping sender ids of accounts outside the pool". At the pin, the code is unchanged in the respects that matter here. No post-pin commit exists in that listing to check.
5. **Sender assigned between parse and `validateTx`, or re-validation later with the sender set.**
   - `fromDBLocked` (`pool.go#L2756-L2823`) calls `parseCtx.WithSender(false)` at L2796.
   - L2812 calls `ParseTransaction(txnRlp, 0, txn, nil, false, true, nil)` with `sender=nil`.
   - L2819 calls `p.senders.getOrCreateID(addr, ...)`, which sets only `txn.SenderID` (a number). The address `addr` is kept only for `txns.Senders` (L2842).
   - `ParseTransaction` Step 8 (`pool_txn_parser.go#L396-L415`) recovers the sender only if `ctx.withSender && len(sender)==20`. It is skipped here, and `Sender()` is never called, so the cached `from` stays nil.
   - `CommonTx.GetSender()` (`execution/types/legacy_tx.go#L87-L92`) returns `(NilAddress, false)` when `from` is nil.
   - `SetSender` is called only in `pool.go#L939`, on the block-builder path after `best`, never in `fromDBLocked`.
   - `validateTx` does not look up the sender from the senders table. It uses `p.senders.info(stateCache, txn.SenderID)` (L1118-L1122) only for nonce, balance and code hash.
   - The rejection happens before the transaction reaches `addTxns`, so later promotion or `onSenderStateChange` never sees it. At L2823-L2827 the code does `continue` on any reason other than `NotSet` or `Success`.
   - The `best` path (L840-L860) does use `to != nil && *to == sender`, but only for transactions that are already in the pool.
   - Normal receive (`validateTxns`, L1454-L1480) does set the cache. Its parse uses `withSender=true` and a sender buffer, and `txn.Sender(signer)` caches `tx.from` (`legacy_tx.go#L422-L434`). That is why the same transaction is accepted when first received and dropped on reload.

## Corrected record

- **Client / EIP:** Erigon, EIP-2780 (intrinsic gas for self-transfer), within Amsterdam. Amsterdam is not live on mainnet, so the live-code withholding rule does not apply.
- **Spec quote:** EIP-2780 per the brief: "A self-transfer charges neither the recipient touch nor the value cost". I did not re-open the EIP text at d4c57552. The execution-specs lines quoted above are the normative reference.
- **Location at the pin:** `txnprovider/txpool/pool.go#L1046-L1085` (`IsSelfTransfer: senderOk && ...` at L1056, with the `IntrinsicGas` return around L1081-L1085); `#L2794-L2796` (`WithSender(false)`); `#L2808-L2823` (parse, `getOrCreateID`, `validateTx`); `txnprovider/txpool/pool_txn_parser.go#L396-L415`; `execution/types/legacy_tx.go#L87-L92`.
- **Condition:** an Amsterdam node restarts with a persisted self-transfer (`to == sender`, not a create) whose gas limit is at least the spec intrinsic and below intrinsic + 3,000 (zero value) or + 9,000 (nonzero value). It is dropped on every restart. This only matters for a transaction that is valid in a block but sits in the pool with an almost-minimal gas limit.
- **Other client:** Reth passes the recovered sender into pool validation, per the brief. I did not read Reth's code, so that comparison is unverified.
- **Confidence:** high on the code path. The reading is purely static, and I did not execute a reload to observe it.
- **Severity:** cosmetic. It affects the txpool only: a locally valid self-transfer is lost on restart. It causes no consensus split, no invalid block accepted, no valid block rejected, no wrong gas or fee, and no fork-choice divergence. Block execution uses a different path.

## Known-check

- erigontech/erigon #25273: open. "txnprovider/txpool: the startup reload can block payload builds and does not restore the persisted pool". It concerns reload blocking `p.lock` and `blockGasLimit=0` rejecting everything. I read only the first part of the body, and it does not mention self-transfers or sender resolution.
- erigontech/erigon #21942: open. "Harmonize/de-duplicate consensus admission checks: TxnExecutor.preCheck vs TxPool.validateTx". I read only the title and metadata, not the body, so I cannot say whether it covers this case.
- A search of repository issues and PRs for "self-transfer txpool" found only #23052, #22053 and #22136 (all closed, EIP-2780/8246 implementation work) and an unrelated old panic issue. No issue or PR reports this reload mismatch. The search is not exhaustive.
- Hive and EEST results for Glamsterdam devnets: not checked.

## URLs read

- https://raw.githubusercontent.com/erigontech/erigon/f32a39d1e5f87a9ec3a9ce2858171c74acc3c242/txnprovider/txpool/pool.go
- https://raw.githubusercontent.com/erigontech/erigon/f32a39d1e5f87a9ec3a9ce2858171c74acc3c242/txnprovider/txpool/pool_txn_parser.go
- https://raw.githubusercontent.com/erigontech/erigon/f32a39d1e5f87a9ec3a9ce2858171c74acc3c242/execution/types/legacy_tx.go
- https://raw.githubusercontent.com/erigontech/erigon/f32a39d1e5f87a9ec3a9ce2858171c74acc3c242/execution/protocol/mdgas/intrinsic_gas.go
- https://raw.githubusercontent.com/erigontech/erigon/f32a39d1e5f87a9ec3a9ce2858171c74acc3c242/execution/protocol/params/protocol.go
- https://api.github.com/repos/ethereum/execution-specs/contents/src/ethereum/forks/amsterdam/transactions.py?ref=forks/bogota (the ref's current head, not the pinned 7f2f4c9b; I did not verify the line numbers match that commit)
- https://api.github.com/repos/erigontech/erigon/commits?path=txnprovider/txpool/pool.go
- https://api.github.com/repos/erigontech/erigon/commits/f32a39d1e5f87a9ec3a9ce2858171c74acc3c242
- https://api.github.com/repos/erigontech/erigon/issues/25273
- https://api.github.com/repos/erigontech/erigon/issues/21942
- https://api.github.com/search/issues?q=repo:erigontech/erigon+self-transfer+txpool
