core/tracing: fix nonce revert edge case (#33978)
Some checks are pending
/ Linux Build (push) Waiting to run
/ Linux Build (arm) (push) Waiting to run
/ Keeper Build (push) Waiting to run
/ Windows Build (push) Waiting to run
/ Docker Image (push) Waiting to run

We got a report for a bug in the tracing journal which has the
responsibility to emit events for all state that must be reverted.

The edge case is as follows: on CREATE operations the nonce is
incremented. When a create frame reverts, the nonce increment associated
with it does **not** revert. This works fine on master. Now one step
further: if the parent frame reverts tho, the nonce **should** revert
and there is the bug.
This commit is contained in:
Sina M 2026-03-10 16:53:21 +01:00 committed by GitHub
parent 91cec92bf3
commit aa417b03a6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 48 additions and 4 deletions

View file

@ -155,10 +155,18 @@ func (j *journal) OnBalanceChange(addr common.Address, prev, new *big.Int, reaso
}
func (j *journal) OnNonceChangeV2(addr common.Address, prev, new uint64, reason NonceChangeReason) {
// When a contract is created, the nonce of the creator is incremented.
// This change is not reverted when the creation fails.
if reason != NonceChangeContractCreator {
j.entries = append(j.entries, nonceChange{addr: addr, prev: prev, new: new})
j.entries = append(j.entries, nonceChange{addr: addr, prev: prev, new: new})
if reason == NonceChangeContractCreator {
// When a contract is created via CREATE/CREATE2, the creator's nonce is
// incremented. The EVM does not revert this when the CREATE frame itself
// fails (the nonce change happens before the EVM snapshot). However, if
// a parent frame reverts, the nonce must be reverted along with everything
// else.
//
// To achieve this, advance the current frame's revision point past this
// entry. The CREATE frame's revert won't touch it (it's below the revision),
// but a parent frame's revert will (it's above the parent's revision).
j.revisions[len(j.revisions)-1] = len(j.entries)
}
if j.hooks.OnNonceChangeV2 != nil {
j.hooks.OnNonceChangeV2(addr, prev, new, reason)

View file

@ -219,6 +219,42 @@ func TestNonceIncOnCreate(t *testing.T) {
}
}
// TestNonceIncOnCreateParentReverts checks that the creator's nonce increment
// from CREATE survives the CREATE frame's own revert but is properly reverted
// when the parent call frame reverts.
func TestNonceIncOnCreateParentReverts(t *testing.T) {
const opCREATE = 0xf0
tr := &testTracer{t: t}
wr, err := WrapWithJournal(&Hooks{OnNonceChange: tr.OnNonceChange})
if err != nil {
t.Fatalf("failed to wrap test tracer: %v", err)
}
addr := common.HexToAddress("0x1234")
{
// Parent call frame
wr.OnEnter(0, 0, addr, addr, nil, 1000, big.NewInt(0))
{
// CREATE frame — creator nonce incremented, then CREATE reverts
wr.OnEnter(1, opCREATE, addr, addr, nil, 1000, big.NewInt(0))
wr.OnNonceChangeV2(addr, 0, 1, NonceChangeContractCreator)
wr.OnExit(1, nil, 100, errors.New("revert"), true)
}
// After CREATE reverts, nonce should still be 1
if tr.nonce != 1 {
t.Fatalf("nonce after CREATE revert: got %v, want 1", tr.nonce)
}
// Parent frame also reverts
wr.OnExit(0, nil, 150, errors.New("revert"), true)
}
// After parent reverts, nonce should be back to 0
if tr.nonce != 0 {
t.Fatalf("nonce after parent revert: got %v, want 0", tr.nonce)
}
}
func TestOnNonceChangeV2(t *testing.T) {
tr := &testTracer{t: t}
wr, err := WrapWithJournal(&Hooks{OnNonceChangeV2: tr.OnNonceChangeV2})