From 12311ee59ef18de56539bb9352a71388b8b02996 Mon Sep 17 00:00:00 2001 From: Jared Wasinger Date: Sun, 5 Oct 2025 14:59:09 +0800 Subject: [PATCH] add dedicated selfdestruct hook (TODO: this commit is bugged because a prefunded account which is the target of a create2 initcode can become empty without explicitly selfdestructing --- core/block_access_list_creation.go | 25 +++++++++++++++---------- core/state/statedb_hooked.go | 5 ++++- core/tracing/hooks.go | 17 ++++++++++------- core/types/bal/bal.go | 10 ++++++++++ 4 files changed, 39 insertions(+), 18 deletions(-) diff --git a/core/block_access_list_creation.go b/core/block_access_list_creation.go index 84033b741b..06038938a0 100644 --- a/core/block_access_list_creation.go +++ b/core/block_access_list_creation.go @@ -39,16 +39,17 @@ func NewBlockAccessListTracer(startIdx int) (*BlockAccessListTracer, *tracing.Ho accessListBuilder: bal.NewAccessListBuilder(), } hooks := &tracing.Hooks{ - OnTxEnd: balTracer.TxEndHook, - OnTxStart: balTracer.TxStartHook, - OnEnter: balTracer.OnEnter, - OnExit: balTracer.OnExit, - OnCodeChangeV2: balTracer.OnCodeChange, - OnBalanceChange: balTracer.OnBalanceChange, - OnNonceChangeV2: balTracer.OnNonceChange, - OnStorageChange: balTracer.OnStorageChange, - OnColdAccountRead: balTracer.OnColdAccountRead, - OnColdStorageRead: balTracer.OnColdStorageRead, + OnTxEnd: balTracer.TxEndHook, + OnTxStart: balTracer.TxStartHook, + OnEnter: balTracer.OnEnter, + OnExit: balTracer.OnExit, + OnCodeChangeV2: balTracer.OnCodeChange, + OnBalanceChange: balTracer.OnBalanceChange, + OnNonceChangeV2: balTracer.OnNonceChange, + OnStorageChange: balTracer.OnStorageChange, + OnColdAccountRead: balTracer.OnColdAccountRead, + OnColdStorageRead: balTracer.OnColdStorageRead, + OnSelfDestructChange: balTracer.OnSelfDestruct, } wrappedHooks, err := tracing.WrapWithJournal(hooks) if err != nil { @@ -107,6 +108,10 @@ func (a *BlockAccessListTracer) OnCodeChange(addr common.Address, prevCodeHash c a.accessListBuilder.CodeChange(addr, prevCode, code) } +func (a *BlockAccessListTracer) OnSelfDestruct(addr common.Address) { + a.accessListBuilder.SelfDestruct(addr) +} + func (a *BlockAccessListTracer) OnBalanceChange(addr common.Address, prevBalance, newBalance *big.Int, _ tracing.BalanceChangeReason) { newU256 := new(uint256.Int).SetBytes(newBalance.Bytes()) prevU256 := new(uint256.Int).SetBytes(prevBalance.Bytes()) diff --git a/core/state/statedb_hooked.go b/core/state/statedb_hooked.go index 1f016e8fde..1f2677ed1e 100644 --- a/core/state/statedb_hooked.go +++ b/core/state/statedb_hooked.go @@ -254,10 +254,13 @@ func (s *hookedStateDB) AddLog(log *types.Log) { func (s *hookedStateDB) Finalise(deleteEmptyObjects bool) { defer s.inner.Finalise(deleteEmptyObjects) - if s.hooks.OnBalanceChange != nil || s.hooks.OnNonceChangeV2 != nil || s.hooks.OnCodeChangeV2 != nil || s.hooks.OnCodeChange != nil { + if s.hooks.OnSelfDestructChange != nil || s.hooks.OnBalanceChange != nil || s.hooks.OnNonceChangeV2 != nil || s.hooks.OnCodeChangeV2 != nil || s.hooks.OnCodeChange != nil { for addr := range s.inner.journal.dirties { obj := s.inner.stateObjects[addr] if obj != nil && obj.selfDestructed { + if s.hooks.OnSelfDestructChange != nil { + s.hooks.OnSelfDestructChange(obj.address) + } // If ether was sent to account post-selfdestruct it is burnt. if s.hooks.OnBalanceChange != nil { if bal := obj.Balance(); bal.Sign() != 0 { diff --git a/core/tracing/hooks.go b/core/tracing/hooks.go index f6bba9965d..5818fac4c8 100644 --- a/core/tracing/hooks.go +++ b/core/tracing/hooks.go @@ -189,6 +189,8 @@ type ( // ColdAccountReadHook is called before an previously-unread account is read. ColdAccountReadHook = func(address common.Address) + SelfDestructHook = func(address common.Address) + // LogHook is called when a log is emitted. LogHook = func(log *types.Log) @@ -217,13 +219,14 @@ type Hooks struct { OnSystemCallEnd OnSystemCallEndHook // State events - OnBalanceChange BalanceChangeHook - OnNonceChange NonceChangeHook - OnNonceChangeV2 NonceChangeHookV2 - OnCodeChange CodeChangeHook - OnCodeChangeV2 CodeChangeHookV2 - OnStorageChange StorageChangeHook - OnLog LogHook + OnBalanceChange BalanceChangeHook + OnNonceChange NonceChangeHook + OnNonceChangeV2 NonceChangeHookV2 + OnCodeChange CodeChangeHook + OnCodeChangeV2 CodeChangeHookV2 + OnStorageChange StorageChangeHook + OnLog LogHook + OnSelfDestructChange SelfDestructHook //State read events OnColdStorageRead ColdStorageReadHook OnColdAccountRead ColdAccountReadHook diff --git a/core/types/bal/bal.go b/core/types/bal/bal.go index fcfc154b80..8a205fbb47 100644 --- a/core/types/bal/bal.go +++ b/core/types/bal/bal.go @@ -133,6 +133,10 @@ func (c *AccessListBuilder) CodeChange(address common.Address, prev, cur []byte) acctAccesses.CodeChange(cur) } +func (c *AccessListBuilder) SelfDestruct(address common.Address) { + delete(c.accessesStack[len(c.accessesStack)-1], address) +} + func (c *AccessListBuilder) NonceChange(address common.Address, prev, cur uint64) { if _, ok := c.prestates[address]; !ok { c.prestates[address] = &AccountState{} @@ -203,6 +207,12 @@ func (a *AccessListBuilder) FinaliseIdxChanges() (*StateDiff, StateAccesses) { } } + // two cases of account being removed: + // * initcode runs at address, calls SENDALL (account could be prefunded, but not pre-existing as a contract) + // - if the account makes storage mutations, we have no way to distinguish it from a regular contract that didn't selfdestruct + // + // * contract created in same tx calls SENDALL + // two scenarios where an account can become non-existent: // account was created/deleted in the same transaction // account only had balance set (prefunded), was target of create2 initcode which called SENDALL leaving the account empty