fix(evm-normalization): tolerate SELFDESTRUCT trace frames without to - #546
fix(evm-normalization): tolerate SELFDESTRUCT trace frames without to#546elina-chertova wants to merge 1 commit into
to#546Conversation
geth's callTracer omits the `to` field on a self-referential SELFDESTRUCT (beneficiary == the destroyed account). `mapDebugFrame` guarded it with `assertNotNull(frame.to)`, so such a frame threw and crash-looped the ingest, freezing the block writer. The RPC-level `DebugFrame` validator already declares `to` optional, so normalization must not assert it. Fall back to the account's own address (`frame.from`) — the beneficiary in the omitted case, cross-checked against independent providers. Adds a regression test covering the missing-`to` and explicit-`to` cases.
|
Confirming this same root cause is still live: One correctness caveat on the reconstruction, from probing the real data this run: The configured provider (Alchemy, the eRPC upstream) returns the malformed frame with {"from":"0x0000000000000000000000000000000000000000","gas":"0x0","gasUsed":"0x0","input":"0x","type":"SELFDESTRUCT"}So on the actual production frame The crash fix is still valid and unblocks the writer (that's the priority). If preserving the canonical address matters, the destroyed account is recoverable from the parent CALL frame's Separately, the underlying data defect is an Alchemy |
Cause (proven)
The
ethereum-sepoliaEVM archive ingest crash-loops deterministically (every ~8s) on block 11319411 (0xacb873), freezing the block writer →ethereum-sepolia_Writer_Short_Stall. Theingestcontainer throws:The offending record is the single
SELFDESTRUCTframe in tx index 17 (0xf6c6e39b…): a self-referential, zero-value self-destruct of0xe22a1e72…53e0. geth'scallTraceromits theto(beneficiary) field for this case, butmapDebugFrameguarded it withassertNotNull(frame.to), turning an expected-optional field into a fatal crash.This is a code contradiction: the RPC-level
DebugFramevalidator declarestoasoption(BYTES)(optional), yet normalization asserted it non-null.Not a chain / provider-data problem to "accept"
Cross-checked block 11319411's trace against two independent debug-capable archival providers (drpc, Tatum): both return
to == fromon this frame and no null fields anywhere. So the canonical value is well-defined — the fix reconstructs it rather than fabricating data. (The configured ingest provider returns the malformed frame; swapping it is a separate operator mitigation and is not part of this PR — the durable fix is to stop the crash, per the "fix the fatality" convention.)Fix
refundAddress: (frame.to ?? frame.from).toLowerCase()— whentois omitted, the beneficiary of a self-referential SELFDESTRUCT is the account itself (frame.from). Keeps the process alive on the field the validator already allows to be absent, without changing the public data model.Test (red→green)
evm/evm-normalization/src/mapping.test.tsbuilds a raw block with a SELFDESTRUCT frame missingtoand drives it through the productionmapRawBlockpath.vitest --runfails — throws atmapping.ts:142 assertNotNull(frame.to).refundAddress === frame.from); the explicit-tocase is also asserted.rush build --to @subsquid/evm-normalizationcompiles green.Falsification
If a provider ever omits
toon a SELFDESTRUCT whose beneficiary is not the destroyed account,?? frame.fromwould record the wrong refund address — but no such frame has been observed, the value moved is zero, and the alternative (assert) crash-loops the whole dataset. Independent-provider disagreement onrefundAddressfor such a block would falsify the self-referential assumption.