Repository: ethereum/go-ethereum (2026-04-07T11:07:06Z)

Issue:
debug_traceTransaction returns wrong data for deauthorizations of EIP-7702
#### System information

Geth version: `1.17.1` (archive mode)
CL client & version: Maru (Linea mainnet)
OS & Version: Linux
Commit hash : (if `develop`)

#### Expected behaviour

As stated in https://geth.ethereum.org/docs/developers/evm-tracing/built-in-tracers#prestate-tracer-config point 4:

> The post object is more selective - it only contains the specific fields that were actually modified during the transaction. For example, if only the storage was modified, post will not include unchanged fields like nonce, balance, or code.

When an EIP-7702 deauthorization occurs (authorization to `address(0)`), the account's code is cleared — it transitions from `0xef0100 || <delegate>` to empty code. Per [EIP-7702](https://eips.ethereum.org/EIPS/eip-7702):

> If address is `0x0000000000000000000000000000000000000000`, do not write the delegation indicator. Clear the account's code by resetting the account's code hash to the empty code hash `0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470`.

Since `code` and `codeHash` are modified by the transaction, the `post` object should reflect their new values:

```json
"pre": {
    "0x7ec0e5a95169d31d6fa7d44e3308872e090c5bd1": {
        "balance": "0x0",
        "code": "0xef0100d313d93607c016a85e63e557a11ca5ab0b53ad83",
        "codeHash": "0x9eea9f41ed2b35e6234d1e1c14e88c1136f85d56ed1f32a7efc0096d998dad3d",
        "nonce": 1
    }
},
"post": {
    "0x7ec0e5a95169d31d6fa7d44e3308872e090c5bd1": {
        "code": "0x",
        "codeHash": "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470",
        "nonce": 2
    }
}
```

#### Actual behaviour

We encountered two issues in the prestate tracer diff mode output for EIP-7702 deauthorization transactions:

**Bug 1: `code` field missing from `post` object**

The account's code changed from `0xef0100d313d93607c016a85e63e557a11ca5ab0b53ad83` (delegation indicator) to `0x` (empty). This is a state modification, so `code` should appear in the `post` object — but it is absent.

**Bug 2: `codeHash` in `post` is `0x0` instead of `EMPTY_KECCAK`**

The `post` object reports `codeHash` as all zeros instead of the canonical empty code hash. After deauthorization the account still exists (nonce = 2), so its `codeHash` should be `keccak256("") = 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470`. We verified this on-chain via `eth_getProof` — the state trie correctly stores `EMPTY_KECCAK` as the codeHash. Only the tracer output is wrong.

Actual tracer output:

```json
"pre": {
    "0x7ec0e5a95169d31d6fa7d44e3308872e090c5bd1": {
        "balance": "0x0",
        "code": "0xef0100d313d93607c016a85e63e557a11ca5ab0b53ad83",
        "codeHash": "0x9eea9f41ed2b35e6234d1e1c14e88c1136f85d56ed1f32a7efc0096d998dad3d",
        "nonce": 1
    }
},
"post": {
    "0x7ec0e5a95169d31d6fa7d44e3308872e090c5bd1": {
        "codeHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
        "nonce": 2
    }
}
```

#### Steps to reproduce the behaviour

1. Find an EIP-7702 (type 4) transaction that deauthorizes an account (authorization to `address(0)`). Example transaction: `0xd2f0012ecbcb7c50ea5415c94268c08bf951d7714209437458d6e6436ab0cd0c` on Linea mainnet (block 29869295).

2. Trace the transaction with the prestate tracer in diff mode:

```bash
curl -s -X POST "<your-archive-node-rpc>" \
  -H "Content-Type: application/json" \
  --data '{
    "jsonrpc": "2.0",
    "method": "debug_traceTransaction",
    "params": [
      "0xd2f0012ecbcb7c50ea5415c94268c08bf951d7714209437458d6e6436ab0cd0c",
      {"tracer": "prestateTracer", "tracerConfig": {"diffMode": true}}
    ],
    "id": 1
  }'
```

3. Observe the `post` object for `0x7ec0e5a95169d31d6fa7d44e3308872e090c5bd1`:
   - `code` field is missing (should be `"0x"`)
   - `codeHash` is `0x0000...0000` (should be `0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470`)

4. Verify the actual on-chain state is correct via `eth_getProof` — the state trie has `EMPTY_KECCAK` as expected. The bug is isolated to the prestate tracer output.

#### Backtrace

```
N/A — no crash, incorrect tracer output only.
```

CC @mateo-mro


Discussion:
Thanks for reporting.

> Bug 2: codeHash in post is 0x0 instead of EMPTY_KECCAK

That's an issue yes. In no sense it should be 0x0. Will submit a fix.

> Bug 1: code field missing from post object

This one is the intended behaviour. The general pattern is if something is removed during the transaction it shows up in `pre` and is absent in `post`. It is a compact way of saying it was deleted without explicitly including the empty value in `post`.

Thanks for addressing so quickly! 

>This one is the intended behaviour. The general pattern is if something is removed during the transaction it shows up in pre and is absent in post. It is a compact way of saying it was deleted without explicitly including the empty value in post. 

Regarding Bug 1: I would argue that the definition of deleting does not apply here as the Account remains existing and with it the code field remains existing as well but changed from a non-zero value to a zero value.
I have conducted some tests for the balance field which is - to my knowledge - the only other field from an account (balance, nonce, code, code hash) which can be modified from a non-zero value to zero and the tracer actually shows `balance = 0x0` in the post object.

According to your docs regarding deletion: 

```
Deletion operations are represented by:

Account selfdestruct: Account appears in pre but not in post
Storage clearing (setting a storage value to zero is also treated as clearing): Storage slot appears in pre but not in post
```

It addresses __real__ deletions, i.e. an account which gets removed from the trie or a storage value. I would consider the code field changing from a non-zero value to a zero value is not a deletion and should be more consistend with the behavior of the balance field which is also present in the post if it is set to 0. 

Let me know what you think
The root cause for Bug 2 is in `processDiffState()` — [`prestate.go:275-276`](https://github.com/ethereum/go-ethereum/blob/master/eth/tracers/native/prestate.go#L275-L276) normalizes `EmptyCodeHash` to `common.Hash{}` before the comparison. After an EIP-7702 deauthorization the account's code hash reverts to `EmptyCodeHash` (`0xc5d246...`), but the normalization flattens it to zero, so the `post` object reports `0x0` instead of the real Keccak-256 of empty bytes.

I agree with @fredo on Bug 1 — a deauthorization is a state transition (delegation code → empty), not an account deletion. The tracer already includes `balance: 0x0` in `post` when balance is zeroed, so `code` and `codeHash` should follow the same pattern for consistency. The "absent from post means deleted" semantic should only apply to actual account selfdestructs and storage clearing, not field-level zero values.

It's a good point we clear at the account level but when it comes to fields we keep balance. Since the other fields are not "removable" makes sense to make these 2 consistent. I think both way it's fine (to omit balance and code when empty) or emit them both. Going as per your suggestion with emitting them since the overhead is not that large.

I've updated the PR



Patch:
diff --git a/eth/tracers/native/gen_account_json.go b/eth/tracers/native/gen_account_json.go
index 5fec2648b7fc..9417536a235a 100644
--- a/eth/tracers/native/gen_account_json.go
+++ b/eth/tracers/native/gen_account_json.go
@@ -16,14 +16,14 @@ var _ = (*accountMarshaling)(nil)
 func (a account) MarshalJSON() ([]byte, error) {
 	type account struct {
 		Balance  *hexutil.Big                `json:"balance,omitempty"`
-		Code     hexutil.Bytes               `json:"code,omitempty"`
+		Code     *hexutil.Bytes              `json:"code,omitempty"`
 		CodeHash *common.Hash                `json:"codeHash,omitempty"`
 		Nonce    uint64                      `json:"nonce,omitempty"`
 		Storage  map[common.Hash]common.Hash `json:"storage,omitempty"`
 	}
 	var enc account
 	enc.Balance = (*hexutil.Big)(a.Balance)
-	enc.Code = a.Code
+	enc.Code = (*hexutil.Bytes)(a.Code)
 	enc.CodeHash = a.CodeHash
 	enc.Nonce = a.Nonce
 	enc.Storage = a.Storage
@@ -47,7 +47,7 @@ func (a *account) UnmarshalJSON(input []byte) error {
 		a.Balance = (*big.Int)(dec.Balance)
 	}
 	if dec.Code != nil {
-		a.Code = *dec.Code
+		a.Code = (*[]byte)(dec.Code)
 	}
 	if dec.CodeHash != nil {
 		a.CodeHash = dec.CodeHash
diff --git a/eth/tracers/native/prestate.go b/eth/tracers/native/prestate.go
index 159a91b310ff..36cb16e44b45 100644
--- a/eth/tracers/native/prestate.go
+++ b/eth/tracers/native/prestate.go
@@ -44,21 +44,24 @@ func init() {
 type stateMap = map[common.Address]*account
 
 type account struct {
-	Balance  *big.Int                    `json:"balance,omitempty"`
-	Code     []byte                      `json:"code,omitempty"`
+	Balance *big.Int `json:"balance,omitempty"`
+	// Code is a pointer so omitempty can omit unchanged code (nil) while
+	// still emitting "0x" when code is cleared (e.g. EIP-7702 deauth).
+	Code     *[]byte                     `json:"code,omitempty"`
 	CodeHash *common.Hash                `json:"codeHash,omitempty"`
 	Nonce    uint64                      `json:"nonce,omitempty"`
 	Storage  map[common.Hash]common.Hash `json:"storage,omitempty"`
-	empty    bool
+
+	empty bool
 }
 
 func (a *account) exists() bool {
-	return a.Nonce > 0 || len(a.Code) > 0 || len(a.Storage) > 0 || (a.Balance != nil && a.Balance.Sign() != 0)
+	return a.Nonce > 0 || (a.Code != nil && len(*a.Code) > 0) || len(a.Storage) > 0 || (a.Balance != nil && a.Balance.Sign() != 0)
 }
 
 type accountMarshaling struct {
 	Balance *hexutil.Big
-	Code    hexutil.Bytes
+	Code    *hexutil.Bytes
 }
 
 type prestateTracer struct {
@@ -266,24 +269,28 @@ func (t *prestateTracer) processDiffState() {
 			modified = true
 			postAccount.Nonce = newNonce
 		}
-		prevCodeHash := common.Hash{}
+		// Empty code hashes are excluded from the prestate, so default
+		// to EmptyCodeHash to match what GetCodeHash returns for codeless accounts.
+		prevCodeHash := types.EmptyCodeHash
 		if t.pre[addr].CodeHash != nil {
 			prevCodeHash = *t.pre[addr].CodeHash
 		}
-		// Empty code hashes are excluded from the prestate. Normalize
-		// the empty code hash to a zero hash to make it comparable.
-		if newCodeHash == types.EmptyCodeHash {
-			newCodeHash = common.Hash{}
-		}
 		if newCodeHash != prevCodeHash {
 			modified = true
 			postAccount.CodeHash = &newCodeHash
 		}
 		if !t.config.DisableCode {
 			newCode := t.env.StateDB.GetCode(addr)
-			if !bytes.Equal(newCode, t.pre[addr].Code) {
+			var prevCode []byte
+			if t.pre[addr].Code != nil {
+				prevCode = *t.pre[addr].Code
+			}
+			if !bytes.Equal(newCode, prevCode) {
 				modified = true
-				postAccount.Code = newCode
+				if newCode == nil {
+					newCode = []byte{}
+				}
+				postAccount.Code = &newCode
 			}
 		}
 
@@ -323,10 +330,13 @@ func (t *prestateTracer) lookupAccount(addr common.Address) {
 		return
 	}
 
+	code := t.env.StateDB.GetCode(addr)
 	acc := &account{
 		Balance: t.env.StateDB.GetBalance(addr).ToBig(),
 		Nonce:   t.env.StateDB.GetNonce(addr),
-		Code:    t.env.StateDB.GetCode(addr),
+	}
+	if len(code) > 0 {
+		acc.Code = &code
 	}
 	codeHash := t.env.StateDB.GetCodeHash(addr)
 	// If the code is empty, we don't need to store it in the prestate.


Test patch:
diff --git a/eth/tracers/internal/tracetest/testdata/prestate_tracer_with_diff_mode/eip7702_deauth.json b/eth/tracers/internal/tracetest/testdata/prestate_tracer_with_diff_mode/eip7702_deauth.json
new file mode 100644
index 000000000000..e376a98946de
--- /dev/null
+++ b/eth/tracers/internal/tracetest/testdata/prestate_tracer_with_diff_mode/eip7702_deauth.json
@@ -0,0 +1,98 @@
+{
+  "genesis": {
+    "blobGasUsed": "0",
+    "difficulty": "0",
+    "excessBlobGas": "0",
+    "extraData": "0x",
+    "gasLimit": "11511229",
+    "hash": "0x455b93a512baa4ed5e117508b184a6bb03904b94d665ce38931728eca9cdd8fe",
+    "miner": "0x71562b71999873db5b286df957af199ec94617f7",
+    "mixHash": "0x042877c4fab9f022d29590ae83bad89d6181afb1d6e107619911ea52e5901364",
+    "nonce": "0x0000000000000000",
+    "number": "1",
+    "parentBeaconBlockRoot": "0x0000000000000000000000000000000000000000000000000000000000000000",
+    "stateRoot": "0xc8688ad6433e6b9f4edeb82360d2b99c8e919f493a01cacbe7c4a97184f5d043",
+    "timestamp": "1775654796",
+    "alloc": {
+      "0x71562b71999873db5b286df957af199ec94617f7": {
+        "balance": "0xffffffffffffffffffffffffffffffffffffffffffffffffffffdb64910c3bf7",
+        "nonce": "1"
+      },
+      "0xe85a1c0e9d5b1c9b417c6c1b34c22cd77f623f50": {
+        "balance": "0x0",
+        "code": "0xef0100d313d93607c016a85e63e557a11ca5ab0b53ad83",
+        "codeHash": "0x9eea9f41ed2b35e6234d1e1c14e88c1136f85d56ed1f32a7efc0096d998dad3d",
+        "nonce": "1"
+      }
+    },
+    "config": {
+      "chainId": 1337,
+      "homesteadBlock": 0,
+      "eip150Block": 0,
+      "eip155Block": 0,
+      "eip158Block": 0,
+      "byzantiumBlock": 0,
+      "constantinopleBlock": 0,
+      "petersburgBlock": 0,
+      "istanbulBlock": 0,
+      "muirGlacierBlock": 0,
+      "berlinBlock": 0,
+      "londonBlock": 0,
+      "arrowGlacierBlock": 0,
+      "grayGlacierBlock": 0,
+      "shanghaiTime": 0,
+      "cancunTime": 0,
+      "pragueTime": 0,
+      "terminalTotalDifficulty": 0,
+      "blobSchedule": {
+        "cancun": {
+          "target": 3,
+          "max": 6,
+          "baseFeeUpdateFraction": 3338477
+        },
+        "prague": {
+          "target": 6,
+          "max": 9,
+          "baseFeeUpdateFraction": 5007716
+        }
+      }
+    }
+  },
+  "context": {
+    "number": "2",
+    "difficulty": "0",
+    "timestamp": "1775654797",
+    "gasLimit": "11522469",
+    "miner": "0x71562b71999873db5b286df957af199ec94617f7",
+    "baseFeePerGas": "766499147"
+  },
+  "input": "0x04f8cd82053901843b9aca008477359400830186a09471562b71999873db5b286df957af199ec94617f78080c0f85ef85c8205399400000000000000000000000000000000000000000101a011fc0271f2566e7ebe5ddbff6d48ea97a19afa248452a392781096b7e3b89177a0020107ecefe99c90429b416fe4d1eead5a7fa253761e85cd7cdc7df6e5032d7f80a098495fb16c904f0b67b49afe868b28b0159c8df07522bed99ef6ff2cc2ac2935a048857a9c385d91735a9fdccabc66de7a5ea1897f523a5b9a352e281642a76e6b",
+  "tracerConfig": {
+    "diffMode": true
+  },
+  "result": {
+    "post": {
+      "0x71562b71999873db5b286df957af199ec94617f7": {
+        "balance": "0xffffffffffffffffffffffffffffffffffffffffffffffffffffc1bd12c85eb7",
+        "nonce": 2
+      },
+      "0xe85a1c0e9d5b1c9b417c6c1b34c22cd77f623f50": {
+        "code": "0x",
+        "codeHash": "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470",
+        "nonce": 2
+      }
+    },
+    "pre": {
+      "0x71562b71999873db5b286df957af199ec94617f7": {
+        "balance": "0xffffffffffffffffffffffffffffffffffffffffffffffffffffdb64910c3bf7",
+        "nonce": 1
+      },
+      "0xe85a1c0e9d5b1c9b417c6c1b34c22cd77f623f50": {
+        "balance": "0x0",
+        "code": "0xef0100d313d93607c016a85e63e557a11ca5ab0b53ad83",
+        "codeHash": "0x9eea9f41ed2b35e6234d1e1c14e88c1136f85d56ed1f32a7efc0096d998dad3d",
+        "nonce": 1
+      }
+    }
+  }
+}