Repository navigation
feat(core/vm,core/state): EIP-8246 selfdestruct burn removal #35219, close #2766 - #2834
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the
|
36b117e to
6d08fde
Compare
2b719b4 to
4beef99
Compare
4beef99 to
31fe83a
Compare
EIP-7708 emits a system log for every value transfer from Amsterdam onwards: the transaction value, the value moved by a CALL or CREATE, and the value moved by SELFDESTRUCT. The log uses params.SystemAddress as emitter and the ERC-20 Transfer ABI (Transfer(address,address,uint256) plus a 32-byte amount), so indexers can consume it like a token transfer. The gate sits where the fork rules are available: core.Transfer takes the vm's params.Rules and emits after the balance move, while opSelfdestruct6780 emits when the beneficiary differs. Zero-value transfers and transfers to self emit nothing. No burn log is emitted, which is an ordering constraint rather than a statement about this tree: EIP-8246 (ethereum#35219) removes the SELFDESTRUCT burn and XinFinOrg#2834 lands that change here, but until XinFinOrg#2834 is merged a self-destruct still destroys balances in two corners, both unlogged by this commit: - a contract created in the same transaction that self-destructs to itself has its balance destroyed by SelfDestruct6780 and Finalise; - a contract created in the same transaction that self-destructs and then receives funds has those funds destroyed at Finalise. Amsterdam is not scheduled on any network (amsterdamBlock is nil), so the window is dormant; this prerequisite is why the change must merge after XinFinOrg#2834, or in the same batch. The log constructor takes *big.Int rather than upstream's *uint256.Int because this tree's StateDB moves balances as big.Int. Tests cover the upstream CALL/SELFDESTRUCT scenario through block processing (three transfer logs interleaved with the contract's events), the zero-value and pre-Amsterdam cases, a transfer to self, a reverted call frame, and the upstream boundary matrix: DELEGATECALL (no log), a CREATE2 endowment, a failed CREATE rolling its log back, precompile and system-address recipients, the priority-fee exclusion, and a direct core.Transfer fork-gate case pinning the 32-byte amount. core/state additionally pins that a log is reverted with its snapshot. Ref: ethereum#33645 Ref: ethereum#35617 Closes XinFinOrg#2767
…eum#33645 EIP-7708 emits a system log for every value transfer from Amsterdam onwards: the transaction value, the value moved by a CALL or CREATE, and the value moved by SELFDESTRUCT. The log uses params.SystemAddress as emitter and the ERC-20 Transfer ABI (Transfer(address,address,uint256) plus a 32-byte amount), so indexers can consume it like a token transfer. The gate sits where the fork rules are available: core.Transfer takes the vm's params.Rules and emits after the balance move, while opSelfdestruct6780 emits when the beneficiary differs. Zero-value transfers and transfers to self emit nothing. No burn log is emitted, which is an ordering constraint rather than a statement about this tree: EIP-8246 (ethereum#35219) removes the SELFDESTRUCT burn and XinFinOrg#2834 lands that change here, but until XinFinOrg#2834 is merged a self-destruct still destroys balances in two corners, both unlogged by this commit: - a contract created in the same transaction that self-destructs to itself has its balance destroyed by SelfDestruct6780 and Finalise; - a contract created in the same transaction that self-destructs and then receives funds has those funds destroyed at Finalise. Amsterdam is not scheduled on any network (amsterdamBlock is nil), so the window is dormant; this prerequisite is why the change must merge after XinFinOrg#2834, or in the same batch. The log constructor takes *big.Int rather than upstream's *uint256.Int because this tree's StateDB moves balances as big.Int. Tests cover the upstream CALL/SELFDESTRUCT scenario through block processing (three transfer logs interleaved with the contract's events), the zero-value and pre-Amsterdam cases, a transfer to self, a reverted call frame, and the upstream boundary matrix: DELEGATECALL (no log), a CREATE2 endowment, a failed CREATE rolling its log back, precompile and system-address recipients, the priority-fee exclusion, and a direct core.Transfer fork-gate case pinning the 32-byte amount. core/state additionally pins that a log is reverted with its snapshot. Ref: ethereum#33645 Ref: ethereum#35617 Closes XinFinOrg#2767
…eum#33645 EIP-7708 emits a system log for every value transfer from Amsterdam onwards: the transaction value, the value moved by a CALL or CREATE, and the value moved by SELFDESTRUCT. The log uses params.SystemAddress as emitter and the ERC-20 Transfer ABI (Transfer(address,address,uint256) plus a 32-byte amount), so indexers can consume it like a token transfer. The gate sits where the fork rules are available: core.Transfer takes the vm's params.Rules and emits after the balance move, while opSelfdestruct6780 emits when the beneficiary differs. Zero-value transfers and transfers to self emit nothing. No burn log is emitted, which is an ordering constraint rather than a statement about this tree: EIP-8246 (ethereum#35219) removes the SELFDESTRUCT burn and XinFinOrg#2834 lands that change here, but until XinFinOrg#2834 is merged a self-destruct still destroys balances in two corners, both unlogged by this commit: - a contract created in the same transaction that self-destructs to itself has its balance destroyed by SelfDestruct6780 and Finalise; - a contract created in the same transaction that self-destructs and then receives funds has those funds destroyed at Finalise. Amsterdam is not scheduled on any network (amsterdamBlock is nil), so the window is dormant; this prerequisite is why the change must merge after XinFinOrg#2834, or in the same batch. The log constructor takes *big.Int rather than upstream's *uint256.Int because this tree's StateDB moves balances as big.Int. Tests cover the upstream CALL/SELFDESTRUCT scenario through block processing (three transfer logs interleaved with the contract's events), the zero-value and pre-Amsterdam cases, a transfer to self, a reverted call frame, and the upstream boundary matrix: DELEGATECALL (no log), a CREATE2 endowment, a failed CREATE rolling its log back, precompile and system-address recipients, the priority-fee exclusion, and a direct core.Transfer fork-gate case pinning the 32-byte amount. core/state additionally pins that a log is reverted with its snapshot. Ref: ethereum#33645 Ref: ethereum#35617 Closes XinFinOrg#2767
d299682 to
88bcbff
Compare
88bcbff to
ee81b1a
Compare
…eum#33645 EIP-7708 emits a system log for every value transfer from Amsterdam onwards: the transaction value, the value moved by a CALL or CREATE, and the value moved by SELFDESTRUCT. The log uses params.SystemAddress as emitter and the ERC-20 Transfer ABI (Transfer(address,address,uint256) plus a 32-byte amount), so indexers can consume it like a token transfer. The gate sits where the fork rules are available: core.Transfer takes the vm's params.Rules and emits after the balance move, while opSelfdestruct6780 emits when the beneficiary differs. Zero-value transfers and transfers to self emit nothing. No burn log is emitted, which is an ordering constraint rather than a statement about this tree: EIP-8246 (ethereum#35219) removes the SELFDESTRUCT burn and XinFinOrg#2834 lands that change here, but until XinFinOrg#2834 is merged a self-destruct still destroys balances in two corners, both unlogged by this commit: - a contract created in the same transaction that self-destructs to itself has its balance destroyed by SelfDestruct6780 and Finalise; - a contract created in the same transaction that self-destructs and then receives funds has those funds destroyed at Finalise. Amsterdam is not scheduled on any network (amsterdamBlock is nil), so the window is dormant; this prerequisite is why the change must merge after XinFinOrg#2834, or in the same batch. The log constructor takes *big.Int rather than upstream's *uint256.Int because this tree's StateDB moves balances as big.Int. Tests cover the upstream CALL/SELFDESTRUCT scenario through block processing (three transfer logs interleaved with the contract's events), the zero-value and pre-Amsterdam cases, a transfer to self, a reverted call frame, and the upstream boundary matrix: DELEGATECALL (no log), a CREATE2 endowment, a failed CREATE rolling its log back, precompile and system-address recipients, the priority-fee exclusion, and a direct core.Transfer fork-gate case pinning the 32-byte amount. core/state additionally pins that a log is reverted with its snapshot. Ref: ethereum#33645 Ref: ethereum#35617 Closes XinFinOrg#2767
5be2dd5 to
defba4d
Compare
…eum#33645 EIP-7708 emits a system log for every value transfer from Amsterdam onwards: the transaction value, the value moved by a CALL or CREATE, and the value moved by SELFDESTRUCT. The log uses params.SystemAddress as emitter and the ERC-20 Transfer ABI (Transfer(address,address,uint256) plus a 32-byte amount), so indexers can consume it like a token transfer. The gate sits where the fork rules are available: core.Transfer takes the vm's params.Rules and emits after the balance move, while opSelfdestruct6780 emits when the beneficiary differs. Zero-value transfers and transfers to self emit nothing. No burn log is emitted, which is an ordering constraint rather than a statement about this tree: EIP-8246 (ethereum#35219) removes the SELFDESTRUCT burn and XinFinOrg#2834 lands that change here, but until XinFinOrg#2834 is merged a self-destruct still destroys balances in two corners, both unlogged by this commit: - a contract created in the same transaction that self-destructs to itself has its balance destroyed by SelfDestruct6780 and Finalise; - a contract created in the same transaction that self-destructs and then receives funds has those funds destroyed at Finalise. Amsterdam is not scheduled on any network (amsterdamBlock is nil), so the window is dormant; this prerequisite is why the change must merge after XinFinOrg#2834, or in the same batch. The log constructor takes *big.Int rather than upstream's *uint256.Int because this tree's StateDB moves balances as big.Int. Tests cover the upstream CALL/SELFDESTRUCT scenario through block processing (three transfer logs interleaved with the contract's events), the zero-value and pre-Amsterdam cases, a transfer to self, a reverted call frame, and the upstream boundary matrix: DELEGATECALL (no log), a CREATE2 endowment, a failed CREATE rolling its log back, precompile and system-address recipients, the priority-fee exclusion, and a direct core.Transfer fork-gate case pinning the 32-byte amount. core/state additionally pins that a log is reverted with its snapshot. Ref: ethereum#33645 Ref: ethereum#35617 Closes XinFinOrg#2767
defba4d to
d0d19de
Compare
…ethereum#35219 EIP-8246 removes the remaining SELFDESTRUCT burns from Amsterdam onwards: - a contract created in the same transaction that executes SELFDESTRUCT with itself as beneficiary keeps its balance instead of burning it; - at transaction finalisation every account marked for destruction is reduced to a balance-only account - nonce reset to 0, code and storage cleared, balance untouched - instead of being deleted. An account left with a zero balance is empty and is still removed by EIP-161. The upstream implementation (go-ethereum ethereum#35219) keys both halves on the fork rules at finalisation, but this tree's StateDB carries no rules and its SelfDestruct clears the balance eagerly. The decision is therefore taken where the interpreter already knows the fork: SelfDestruct6780 takes a preserveBalance flag derived from chainRules.IsAmsterdam, the destruct journal entry clears the object flag when it is reverted, and Finalise and hookedStateDB.Finalise read it off the object. The balance move itself stays fork-gated, so the pre-Amsterdam path keeps its historical transfer pair and only Amsterdam drops it. Everything else - gas, the transfer to a distinct beneficiary, readability of the account until the end of the transaction, and the EIP-6780 create-in-same-transaction restriction - is unchanged. A preserved account is tracked in stateObjectsDestruct and rebuilt from an empty object that keeps its pre-block origin, so handleDestruction deletes the old storage trie at commit, exactly like a delete-and-resurrect within one block, and the block-level diff records the account as having existed at block start rather than as a fresh one. Nonce 0, empty code and an empty storage root keep the address usable as a CREATE2 target, as the EIP requires. EIP-7708 is not implemented in this repository, so unlike upstream there is no burn log to remove from the SELFDESTRUCT path. Tests: core/state covers the preserved balance, the zero-balance deletion, ether arriving after the destruct, the destruct-to-elsewhere shape, revert, the pre-Amsterdam burn, the tracer hooks and the block diff of a preserved account; core drives the same contract through block processing with Amsterdam at block 0 and with Amsterdam disabled, checks a same-transaction destruct-to-other followed by refunded funds (7+8 wei from two CALLs), and re-creates a preserved balance-only account through CREATE2 in a later block. core/vm pins the balance-change events of a self-destruct to self on both sides of the fork (two decreases and one increase before Amsterdam, none on Amsterdam), the only observable effect of the interpreter's fork gate. Verified on c2725ee (the merge-base): make all and gofmt/goimports are clean, golangci-lint --new-from-rev=c2725ee4de reports 0 issues, and go test ./core/ ./core/state/... ./core/vm/... ./eth/tracers/... ./internal/ethapi/... passes. Closes XinFinOrg#2766 Ref: ethereum#35219
…ss the fork The event-level test only drove a self-destruct to the executing account, so nothing pinned that a distinct beneficiary keeps the SubBalance/AddBalance pair on both sides of the EIP-8246 fork: the interpreter gate has to drop only the self-beneficiary part, and dropping the transfer pair for a foreign beneficiary would move no funds at all. The helper now reports the beneficiary balance as well, and four table rows cover an existing and a new contract on each side of the fork. The extra decrease of pre-amsterdam/new-contract-to-other is the 0 -> 0 destruct decrease hookedStateDB.SelfDestruct6780 raises after the instruction already moved the balance to the beneficiary, the same quirk the self-beneficiary rows pin.
d0d19de to
fe4cf58
Compare
There was a problem hiding this comment.
🟡 Changes recommended
The prestate diff tracer still omits accounts whose balances survive SELFDESTRUCT under Amsterdam.
1 open issue
What changed in this PR
Implements Amsterdam’s EIP-8246 SELFDESTRUCT burn removal in the VM and state layer, complementing the previously merged supply-tracer gate.
Changes:
- Preserves destructed accounts’ remaining balances while clearing nonce, code, and storage.
- Propagates preservation flags through state copies and reverts, and suppresses removed-burn hooks.
- Adds state, VM, and block-processing tests for fork behavior, late funds, persistence, and CREATE2 reuse.
| File | Description |
|---|---|
| core/vm/interface.go | Adds the balance-preservation parameter to the state interface. |
| core/vm/instructions.go | Gates SELFDESTRUCT balance handling on Amsterdam. |
| core/vm/eip8246_test.go | Tests balance events across fork and beneficiary cases. |
| core/state/statedb.go | Preserves balance-only accounts during finalisation. |
| core/state/statedb_hooked.go | Suppresses balance-decrease and burn hooks for preserved balances. |
| core/state/statedb_eip8246_test.go | Tests persistence, reverts, late funds, and state bookkeeping. |
| core/state/state_object.go | Tracks and copies the preservation flag. |
| core/state/journal.go | Restores preservation flags during journal reverts. |
| core/eip8246_test.go | Tests block-level behavior and CREATE2 recreation. |
Review effort: Balanced
…eum#33645 EIP-7708 emits a system log for every value transfer from Amsterdam onwards: the transaction value, the value moved by a CALL or CREATE, and the value moved by SELFDESTRUCT. The log uses params.SystemAddress as emitter and the ERC-20 Transfer ABI (Transfer(address,address,uint256) plus a 32-byte amount), so indexers can consume it like a token transfer. The gate sits where the fork rules are available: core.Transfer takes the vm's params.Rules and emits after the balance move, while opSelfdestruct6780 emits when the beneficiary differs. Zero-value transfers and transfers to self emit nothing. No burn log is emitted, which is an ordering constraint rather than a statement about this tree: EIP-8246 (ethereum#35219) removes the SELFDESTRUCT burn and destroys balances in two corners, both unlogged by this commit: - a contract created in the same transaction that self-destructs to itself has its balance destroyed by SelfDestruct6780 and Finalise; - a contract created in the same transaction that self-destructs and then receives funds has those funds destroyed at Finalise. Amsterdam is not scheduled on any network (amsterdamBlock is nil), so the window is dormant; this prerequisite is why the change must merge after XinFinOrg#2834, or in the same batch. The log constructor takes *big.Int rather than upstream's *uint256.Int because this tree's StateDB moves balances as big.Int. Tests cover the upstream CALL/SELFDESTRUCT scenario through block processing (three transfer logs interleaved with the contract's events), the zero-value and pre-Amsterdam cases, a transfer to self, a reverted call frame, and the upstream boundary matrix: DELEGATECALL (no log), a CREATE2 endowment, a failed CREATE rolling its log back, precompile and system-address recipients, the priority-fee exclusion, and a direct core.Transfer fork-gate case pinning the 32-byte amount. core/state additionally pins that a log is reverted with its snapshot. Ref: ethereum#33645 Ref: ethereum#35617 Closes XinFinOrg#2767
…eum#33645 EIP-7708 emits a system log for every value transfer from Amsterdam onwards: the transaction value, the value moved by a CALL or CREATE, and the value moved by SELFDESTRUCT. The log uses params.SystemAddress as emitter and the ERC-20 Transfer ABI (Transfer(address,address,uint256) plus a 32-byte amount), so indexers can consume it like a token transfer. The gate sits where the fork rules are available: core.Transfer takes the vm's params.Rules and emits after the balance move, while opSelfdestruct6780 emits when the beneficiary differs. Zero-value transfers and transfers to self emit nothing. No burn log is emitted, which is an ordering constraint rather than a statement about this tree: EIP-8246 (ethereum#35219) removes the SELFDESTRUCT burn and destroys balances in two corners, both unlogged by this commit: - a contract created in the same transaction that self-destructs to itself has its balance destroyed by SelfDestruct6780 and Finalise; - a contract created in the same transaction that self-destructs and then receives funds has those funds destroyed at Finalise. Amsterdam is not scheduled on any network (amsterdamBlock is nil), so the window is dormant; this prerequisite is why the change must merge after XinFinOrg#2834, or in the same batch. The log constructor takes *big.Int rather than upstream's *uint256.Int because this tree's StateDB moves balances as big.Int. Tests cover the upstream CALL/SELFDESTRUCT scenario through block processing (three transfer logs interleaved with the contract's events), the zero-value and pre-Amsterdam cases, a transfer to self, a reverted call frame, and the upstream boundary matrix: DELEGATECALL (no log), a CREATE2 endowment, a failed CREATE rolling its log back, precompile and system-address recipients, the priority-fee exclusion, and a direct core.Transfer fork-gate case pinning the 32-byte amount. core/state additionally pins that a log is reverted with its snapshot. Ref: ethereum#33645 Ref: ethereum#35617 Closes XinFinOrg#2767
… logs ethereum#33645 EIP-7708 emits a system log for every value transfer from Amsterdam onwards: the transaction value, the value moved by a CALL or CREATE, and the value moved by SELFDESTRUCT. The log uses params.SystemAddress as emitter and the ERC-20 Transfer ABI (Transfer(address,address,uint256) plus a 32-byte amount), so indexers can consume it like a token transfer. The gate sits where the fork rules are available: core.Transfer takes the vm's params.Rules and emits after the balance move, while opSelfdestruct6780 emits when the beneficiary differs. Zero-value transfers and transfers to self emit nothing. No burn log is emitted: EIP-8246 (go-ethereum ethereum#35219) removes the SELFDESTRUCT burn entirely and landed before this change as XinFinOrg#2834, so a self-destruct that names a different beneficiary is the only value move the instruction makes. A self-destruct to self moves nothing and therefore logs nothing, in every fork. Amsterdam is not scheduled on any network (amsterdamBlock is nil), so the window stays dormant until it is. The log constructor takes *big.Int rather than upstream's *uint256.Int because this tree's StateDB moves balances as big.Int. Tests cover the upstream CALL/SELFDESTRUCT scenario through block processing (three transfer logs interleaved with the contract's events), the zero-value and pre-Amsterdam cases, a transfer to self, an inner revert that keeps the caller's own log, an outer revert that discards every log, the delegated-call attribution, and the upstream boundary matrix: DELEGATECALL and CALLCODE (no log), a CREATE2 endowment, a failed CREATE rolling its log back, precompile and system-address recipients, the priority-fee exclusion, and a direct core.Transfer fork-gate case pinning the 32-byte amount. core/state additionally pins that a log is reverted with its snapshot. Ref: ethereum#33645 Ref: ethereum#35617 Closes XinFinOrg#2767
… logs ethereum#33645 EIP-7708 emits a system log for every value transfer from Amsterdam onwards: the transaction value, the value moved by a CALL or CREATE, and the value moved by SELFDESTRUCT. The log uses params.SystemAddress as emitter and the ERC-20 Transfer ABI (Transfer(address,address,uint256) plus a 32-byte amount), so indexers can consume it like a token transfer. The gate sits where the fork rules are available: core.Transfer takes the vm's params.Rules and emits after the balance move, while opSelfdestruct6780 emits when the beneficiary differs. Zero-value transfers and transfers to self emit nothing. No burn log is emitted: EIP-8246 (go-ethereum ethereum#35219) removes the SELFDESTRUCT burn entirely and landed before this change as XinFinOrg#2834, so a self-destruct that names a different beneficiary is the only value move the instruction makes. A self-destruct to self moves nothing and therefore logs nothing, in every fork. Amsterdam is not scheduled on any network (amsterdamBlock is nil), so the window stays dormant until it is. The log constructor takes *big.Int rather than upstream's *uint256.Int because this tree's StateDB moves balances as big.Int. Tests cover the upstream CALL/SELFDESTRUCT scenario through block processing (three transfer logs interleaved with the contract's events), the zero-value and pre-Amsterdam cases, a transfer to self, an inner revert that keeps the caller's own log, an outer revert that discards every log, the delegated-call attribution, and the upstream boundary matrix: DELEGATECALL and CALLCODE (no log), a CREATE2 endowment, a failed CREATE rolling its log back, precompile and system-address recipients, the priority-fee exclusion, and a direct core.Transfer fork-gate case pinning the 32-byte amount. core/state additionally pins that a log is reverted with its snapshot. Ref: ethereum#33645 Ref: ethereum#35617 Closes XinFinOrg#2767
…708 ETH transfer logs, close #2767 (#2842) * feat(core,core/vm,core/types,params): implement EIP-7708 ETH transfer logs ethereum#33645 EIP-7708 emits a system log for every value transfer from Amsterdam onwards: the transaction value, the value moved by a CALL or CREATE, and the value moved by SELFDESTRUCT. The log uses params.SystemAddress as emitter and the ERC-20 Transfer ABI (Transfer(address,address,uint256) plus a 32-byte amount), so indexers can consume it like a token transfer. The gate sits where the fork rules are available: core.Transfer takes the vm's params.Rules and emits after the balance move, while opSelfdestruct6780 emits when the beneficiary differs. Zero-value transfers and transfers to self emit nothing. No burn log is emitted: EIP-8246 (go-ethereum ethereum#35219) removes the SELFDESTRUCT burn entirely and landed before this change as #2834, so a self-destruct that names a different beneficiary is the only value move the instruction makes. A self-destruct to self moves nothing and therefore logs nothing, in every fork. Amsterdam is not scheduled on any network (amsterdamBlock is nil), so the window stays dormant until it is. The log constructor takes *big.Int rather than upstream's *uint256.Int because this tree's StateDB moves balances as big.Int. Tests cover the upstream CALL/SELFDESTRUCT scenario through block processing (three transfer logs interleaved with the contract's events), the zero-value and pre-Amsterdam cases, a transfer to self, an inner revert that keeps the caller's own log, an outer revert that discards every log, the delegated-call attribution, and the upstream boundary matrix: DELEGATECALL and CALLCODE (no log), a CREATE2 endowment, a failed CREATE rolling its log back, precompile and system-address recipients, the priority-fee exclusion, and a direct core.Transfer fork-gate case pinning the 32-byte amount. core/state additionally pins that a log is reverted with its snapshot. Ref: ethereum#33645 Ref: ethereum#35617 Closes #2767 * fix(internal/ethapi): suppress synthetic transfer logs after Amsterdam ethereum#35617 eth_simulateV1 synthesizes an ERC-7528 transfer log for every value move when traceTransfers is set. From Amsterdam the protocol emits the EIP-7708 log for the same transfer, so every transfer was reported twice. Disable the synthetic log for Amsterdam blocks, mirroring upstream ethereum#35617. The tracer still captures the protocol log through the OnLog hook, so traceTransfers keeps reporting exactly one log per transfer. The new test pins both sides of the fork: the synthetic ERC-7528 log before Amsterdam and the single protocol log at params.SystemAddress after it. The tracer comment now says that from Amsterdam onwards the protocol emits these logs itself (EIP-7708) and the synthesis is off, so the documented format only describes the synthesized pre-fork log while protocol logs carry params.SystemAddress as their emitter. Upstream still carries the older comment (zero address, no EIP-7708 note), so this is a local correction with no geth PR to mirror. Ref: ethereum#35617

Summary
Implements EIP-8246 (Remove SELFDESTRUCT Burn) for Amsterdam:
SELFDESTRUCTwith itself as beneficiary keeps its balance instead of burning it;Implementation
Upstream go-ethereum ethereum#35219 keys both halves on the fork rules at finalisation, but this tree's
StateDBcarries no rules and itsSelfDestructclears the balance eagerly. The fork decision is therefore taken where the interpreter already knows it:StateDB.SelfDestruct6780(addr, preserveBalance)takes the flag, derived fromchainRules.IsAmsterdaminopSelfdestruct6780;stateObject.selfDestructedPreserveBalancedrivesFinaliseandhookedStateDB.Finalise;opSelfdestruct6780keeps the historical transfer pair for self-destruct-to-self until Amsterdam and drops it from Amsterdam onwards, so the pre-Amsterdam path is unchanged;FinalisecallspreserveDestructedBalancefor a destructed account with a non-zero balance: it records the previous incarnation instateObjectsDestruct, clears the per-address diff caches, rebuilds the account from an empty object that keeps its pre-block origin, and marks it updated. Keeping the origin keeps the block-level diff honest: the account is recorded as having existed at block start rather than as a fresh one, instead of relying onhandleDestructionto repair the value at commit.handleDestructionthen deletes the old storage trie at commit, exactly like a delete-and-resurrect within one block. The resulting nonce 0, empty code and empty storage root keep the address usable as a CREATE2 target, as the EIP requires.Gas, the transfer to a distinct beneficiary, readability of the account until the end of the transaction, and the EIP-6780 create-in-same-transaction restriction are unchanged. EIP-7708 is not implemented in this repository, so unlike upstream there is no burn log to remove from the
SELFDESTRUCTpath.Tests
core/state/statedb_eip8246_test.go:evm.createreuses the account instead of resetting it).core/vm/eip8246_test.gopins the balance-change events of a self-destruct to self on both sides of the fork: two decreases and one increase before Amsterdam, none on Amsterdam with the balance preserved. That event sequence is the only observable effect of the interpreter's fork gate, so removing the gate makes the test fail. A distinct beneficiary is covered the same way: the transfer pair is pinned on both sides of the fork, and only a self-beneficiary destruct drops it (an existing and a new contract in each shape).core/eip8246_test.godrives the same contract through block processing withparams.AllEthashProtocolChanges(Amsterdam at block 0) and with Amsterdam disabled, checking the account fields in the post-state (balance, nonce, code, storage); checks that a same-transaction destruct-to-other whose marked account is refunded 7+8 wei by two laterCALLs ends as a balance-only account holding 15 (ported from upstream ethereum#35364,TestEIP8246SelfdestructRefunded); and verifies that a preserved balance-only account can be re-created throughCREATE2in a later block (the second creation adds 1 wei to the preserved 5).Verification
make allpasses;gofmt/goimportsclean.golangci-lint run --new-from-rev=c2725ee4de ./core ./core/state/... ./core/vm/...reports 0 issues (basec2725ee4de, the currentdev-upgradetip).c2725ee4deand every command above was re-run on that base.go test ./core/ ./core/state/... ./core/vm/... ./eth/tracers/... ./internal/ethapi/... -count=1passes.go mod tidy -diffis empty.SELFDESTRUCTstate roots are byte-identical to the pre-EIP-8246 baseline for four shapes (created-to-self, created-to-other, pre-existing-to-self, pre-existing-to-other), and the balance-change event sequence of a self-destruct to self is unchanged (2 decreases, 1 increase, as asserted byTestSelfdestruct6780EventForkGate); on Amsterdam the same call emits no balance events and keeps the balance.Follow-ups
#2833 (the
supplylive tracer gate) has been merged intodev-upgradeasc2725ee4de, and this branch is rebased on top of it.eth/tracers/live/supply.gorecords that its Amsterdam gate is only correct once the consensus side of EIP-8246 is in effect: a chain scheduling Amsterdam without it would still burn the balance while the tracer stops reporting it. This PR is that consensus side, so the two have to ship together.Ref: ethereum#35219
Closes #2766