mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-06-12 01:41:36 +00:00
core: calculate net state bytes
This commit is contained in:
parent
f8eb88e94a
commit
5ab5e7af1b
6 changed files with 516 additions and 25 deletions
|
|
@ -27,6 +27,15 @@ import (
|
|||
"github.com/holiman/uint256"
|
||||
)
|
||||
|
||||
// stateBytesPerSlot is the number of "state-creation bytes" billed for a slot
|
||||
// transitioning from zero to non-zero within a call frame, and refunded when
|
||||
// such a slot is cleared back to zero whose tx-original value was also zero.
|
||||
const stateBytesPerSlot = 64
|
||||
|
||||
// stateBytesPerAccount is the per-account overhead billed when a brand-new
|
||||
// account is created in a call frame.
|
||||
const stateBytesPerAccount = 120
|
||||
|
||||
// frameRange is a half-open interval [start, end) of journal entry indices,
|
||||
// used to record the slice of entries occupied by a closed child call frame.
|
||||
type frameRange struct {
|
||||
|
|
@ -45,6 +54,13 @@ type revision struct {
|
|||
// Invariant: ranges are appended in increasing order, are non-overlapping,
|
||||
// and lie entirely within [journalIndex, len(entries)).
|
||||
closedChildren []frameRange
|
||||
// childStateBytes is the sum of state-creation bytes that this frame's
|
||||
// successful child frames (and their successful descendants, transitively)
|
||||
// produced via closeSnapshot. It is propagated upwards each time a child
|
||||
// closes, so that if THIS frame is later reverted, the caller can recover
|
||||
// the total amount that was emitted for state changes which the revert is
|
||||
// now throwing away.
|
||||
childStateBytes int
|
||||
}
|
||||
|
||||
// journalEntry is a modification entry in the state change journal that can be
|
||||
|
|
@ -98,7 +114,13 @@ func (j *journal) snapshot() int {
|
|||
}
|
||||
|
||||
// revertToSnapshot reverts all state changes made since the given revision.
|
||||
func (j *journal) revertToSnapshot(revid int, s *StateDB) {
|
||||
//
|
||||
// It returns the sum of state-creation bytes that successful child frames
|
||||
// nested within the reverted scope(s) had previously emitted via
|
||||
// closeSnapshot. The caller can use this figure to undo whatever bookkeeping
|
||||
// (e.g. gas charging) it did at the time those bytes were reported, since the
|
||||
// state changes those bytes were paying for are now being thrown away.
|
||||
func (j *journal) revertToSnapshot(revid int, s *StateDB) int {
|
||||
// Find the snapshot in the stack of valid snapshots.
|
||||
idx := sort.Search(len(j.validRevisions), func(i int) bool {
|
||||
return j.validRevisions[i].id >= revid
|
||||
|
|
@ -108,9 +130,20 @@ func (j *journal) revertToSnapshot(revid int, s *StateDB) {
|
|||
}
|
||||
snapshot := j.validRevisions[idx].journalIndex
|
||||
|
||||
// Sum the child-state-bytes carried by every revision being unwound. When
|
||||
// revertToSnapshot tears down multiple stacked frames at once, each of
|
||||
// them may itself have closed children whose bytes were inherited but
|
||||
// never bubbled further up; collecting all of them here lets the caller
|
||||
// undo the full subtree's emissions in one go.
|
||||
var refund int
|
||||
for i := idx; i < len(j.validRevisions); i++ {
|
||||
refund += j.validRevisions[i].childStateBytes
|
||||
}
|
||||
|
||||
// Replay the journal to undo changes and remove invalidated snapshots
|
||||
j.revert(s, snapshot)
|
||||
j.validRevisions = j.validRevisions[:idx]
|
||||
return refund
|
||||
}
|
||||
|
||||
// closeSnapshot marks the end of the call frame identified by revid without
|
||||
|
|
@ -123,7 +156,21 @@ func (j *journal) revertToSnapshot(revid int, s *StateDB) {
|
|||
// closeSnapshot must be invoked in LIFO order: revid must identify the topmost
|
||||
// snapshot. It panics otherwise. The corresponding revision is popped, so a
|
||||
// subsequent revertToSnapshot on the same id is no longer valid.
|
||||
func (j *journal) closeSnapshot(revid int) {
|
||||
//
|
||||
// It returns the net state-creation bytes attributable to THIS frame's own
|
||||
// storage changes (descendant frames' contributions are excluded — they were
|
||||
// already reported when the descendants closed). For each storage slot that
|
||||
// this frame touched directly:
|
||||
// - if the slot is non-zero now and was zero when the frame first touched
|
||||
// it, +stateBytesPerSlot is accumulated;
|
||||
// - if the slot is zero now, was non-zero when the frame first touched it,
|
||||
// and was zero at the start of the transaction, -stateBytesPerSlot is
|
||||
// accumulated.
|
||||
//
|
||||
// The returned value is also folded into the parent's childStateBytes (along
|
||||
// with this frame's own childStateBytes) so a future revertToSnapshot on the
|
||||
// parent can recover the entire subtree's accumulated bytes.
|
||||
func (j *journal) closeSnapshot(revid int) int {
|
||||
if len(j.validRevisions) == 0 {
|
||||
panic(fmt.Errorf("revision id %v cannot be closed: no open snapshot", revid))
|
||||
}
|
||||
|
|
@ -132,20 +179,101 @@ func (j *journal) closeSnapshot(revid int) {
|
|||
panic(fmt.Errorf("revision id %v cannot be closed: top is %v",
|
||||
revid, j.validRevisions[top].id))
|
||||
}
|
||||
rev := &j.validRevisions[top]
|
||||
|
||||
// Compute net state-creation bytes for THIS frame's own slot changes,
|
||||
// skipping any entries that lie inside a closed child's range.
|
||||
thisBytes := j.computeFrameStateBytes(rev)
|
||||
|
||||
// Record this frame's range and propagate accumulated bytes to the
|
||||
// parent. The propagated total is "this frame's own bytes" + "this
|
||||
// frame's already-accumulated child bytes": from the parent's vantage
|
||||
// point the whole subtree is now a single closed child.
|
||||
closed := frameRange{
|
||||
start: j.validRevisions[top].journalIndex,
|
||||
start: rev.journalIndex,
|
||||
end: len(j.entries),
|
||||
}
|
||||
// Only propagate non-empty ranges, and only if there is a parent frame to
|
||||
// receive them. The outermost frame has nothing to bubble up to.
|
||||
if closed.start < closed.end && top > 0 {
|
||||
if top > 0 {
|
||||
parent := &j.validRevisions[top-1]
|
||||
parent.closedChildren = append(parent.closedChildren, closed)
|
||||
if closed.start < closed.end {
|
||||
parent.closedChildren = append(parent.closedChildren, closed)
|
||||
}
|
||||
parent.childStateBytes += thisBytes + rev.childStateBytes
|
||||
}
|
||||
// Drop this revision's bookkeeping. The slice is reused by the parent so
|
||||
// avoid pinning it via the popped tail.
|
||||
j.validRevisions[top].closedChildren = nil
|
||||
rev.closedChildren = nil
|
||||
rev.childStateBytes = 0
|
||||
j.validRevisions = j.validRevisions[:top]
|
||||
return thisBytes
|
||||
}
|
||||
|
||||
// computeFrameStateBytes walks the entries that belong directly to rev (skipping
|
||||
// any closed-child ranges) and sums the per-step state-creation contribution of
|
||||
// each individual SSTORE.
|
||||
//
|
||||
// State-creation accounting is the per-step sum of three independent
|
||||
// contributions, each applied locally to its own journal entry:
|
||||
//
|
||||
// 1. storageChange (slot SSTORE):
|
||||
// - origin != 0 → 0 (rearranging pre-existing storage)
|
||||
// - prev == 0 && new != 0 → +stateBytesPerSlot (new slot created)
|
||||
// - prev != 0 && new == 0 → -stateBytesPerSlot (in-tx creation undone)
|
||||
//
|
||||
// 2. codeChange (SetCode on an account): a brand-new contract publishes its
|
||||
// bytecode for the first time. Origin code is implicitly empty in this
|
||||
// accounting — we treat the prev-empty/new-non-empty transition as the
|
||||
// creation event and bill its byte size, with the inverse transition
|
||||
// refunding it for symmetry.
|
||||
// - len(prev) == 0 && len(new) > 0 → +len(new) (code committed)
|
||||
// - len(prev) > 0 && len(new) == 0 → -len(prev) (in-tx code committed then cleared)
|
||||
//
|
||||
// 3. createObjectChange (account materialised in state): each event adds
|
||||
// +stateBytesPerAccount of per-account overhead.
|
||||
//
|
||||
// The per-step formulation composes naturally: a frame's bytes is the sum of
|
||||
// deltas of its own entries, and the sum of every frame's bytes across the
|
||||
// subtree equals the sum of deltas across all entries — i.e. the same number
|
||||
// you would get from a single whole-frame walk. Slots/code/accounts whose
|
||||
// intermediate values bounce across frame boundaries reconcile automatically
|
||||
// without any need to dedup by "first touch".
|
||||
func (j *journal) computeFrameStateBytes(rev *revision) int {
|
||||
var total int
|
||||
zero := common.Hash{}
|
||||
visit := func(e journalEntry) {
|
||||
switch ch := e.(type) {
|
||||
case storageChange:
|
||||
switch {
|
||||
case ch.origvalue != zero:
|
||||
// Slot was already populated at tx-start; any in-tx
|
||||
// transition is rearranging existing storage.
|
||||
case ch.prevvalue == zero && ch.newvalue != zero:
|
||||
total += stateBytesPerSlot
|
||||
case ch.prevvalue != zero && ch.newvalue == zero:
|
||||
total -= stateBytesPerSlot
|
||||
}
|
||||
case codeChange:
|
||||
switch {
|
||||
case len(ch.prevCode) == 0 && len(ch.newCode) > 0:
|
||||
total += len(ch.newCode)
|
||||
case len(ch.prevCode) > 0 && len(ch.newCode) == 0:
|
||||
total -= len(ch.prevCode)
|
||||
}
|
||||
case createObjectChange:
|
||||
total += stateBytesPerAccount
|
||||
}
|
||||
}
|
||||
idx := rev.journalIndex
|
||||
for _, child := range rev.closedChildren {
|
||||
for ; idx < child.start; idx++ {
|
||||
visit(j.entries[idx])
|
||||
}
|
||||
idx = child.end
|
||||
}
|
||||
for ; idx < len(j.entries); idx++ {
|
||||
visit(j.entries[idx])
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
// frameEntries invokes visit for each entry that belongs directly to the
|
||||
|
|
@ -215,9 +343,10 @@ func (j *journal) copy() *journal {
|
|||
revisions := make([]revision, len(j.validRevisions))
|
||||
for i, r := range j.validRevisions {
|
||||
revisions[i] = revision{
|
||||
id: r.id,
|
||||
journalIndex: r.journalIndex,
|
||||
closedChildren: slices.Clone(r.closedChildren),
|
||||
id: r.id,
|
||||
journalIndex: r.journalIndex,
|
||||
closedChildren: slices.Clone(r.closedChildren),
|
||||
childStateBytes: r.childStateBytes,
|
||||
}
|
||||
}
|
||||
return &journal{
|
||||
|
|
@ -244,11 +373,12 @@ func (j *journal) destruct(addr common.Address) {
|
|||
j.append(selfDestructChange{account: addr})
|
||||
}
|
||||
|
||||
func (j *journal) storageChange(addr common.Address, key, prev, origin common.Hash) {
|
||||
func (j *journal) storageChange(addr common.Address, key, prev, newval, origin common.Hash) {
|
||||
j.append(storageChange{
|
||||
account: addr,
|
||||
key: key,
|
||||
prevvalue: prev,
|
||||
newvalue: newval,
|
||||
origvalue: origin,
|
||||
})
|
||||
}
|
||||
|
|
@ -272,10 +402,11 @@ func (j *journal) balanceChange(addr common.Address, previous *uint256.Int) {
|
|||
})
|
||||
}
|
||||
|
||||
func (j *journal) setCode(address common.Address, prevCode []byte) {
|
||||
func (j *journal) setCode(address common.Address, prevCode, newCode []byte) {
|
||||
j.append(codeChange{
|
||||
account: address,
|
||||
prevCode: prevCode,
|
||||
newCode: newCode,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -336,11 +467,13 @@ type (
|
|||
account common.Address
|
||||
key common.Hash
|
||||
prevvalue common.Hash
|
||||
newvalue common.Hash
|
||||
origvalue common.Hash
|
||||
}
|
||||
codeChange struct {
|
||||
account common.Address
|
||||
prevCode []byte
|
||||
newCode []byte
|
||||
}
|
||||
|
||||
// Changes to other state values.
|
||||
|
|
@ -472,6 +605,7 @@ func (ch codeChange) copy() journalEntry {
|
|||
return codeChange{
|
||||
account: ch.account,
|
||||
prevCode: ch.prevCode,
|
||||
newCode: ch.newCode,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -488,6 +622,7 @@ func (ch storageChange) copy() journalEntry {
|
|||
account: ch.account,
|
||||
key: ch.key,
|
||||
prevvalue: ch.prevvalue,
|
||||
newvalue: ch.newvalue,
|
||||
origvalue: ch.origvalue,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -184,6 +184,338 @@ func TestJournalRevertInteractions(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
// TestJournalStateCreationBytes exercises the slot-creation accounting in
|
||||
// closeSnapshot and the matching refund returned by revertToSnapshot.
|
||||
//
|
||||
// It uses a real StateDB (so SetState/GetState are wired up) and walks
|
||||
// through the cases the docstring promises:
|
||||
// - a slot transitioning 0→X within a frame contributes +stateBytesPerSlot;
|
||||
// - a slot transitioning X→0 within a frame whose tx-original was 0
|
||||
// contributes -stateBytesPerSlot;
|
||||
// - bytes attributed to a successful child frame are NOT re-counted by the
|
||||
// parent's own closeSnapshot (descendant filtering);
|
||||
// - when a parent is reverted, RevertToSnapshot returns the cumulative
|
||||
// bytes that successful children inside the reverted scope had emitted,
|
||||
// so the caller can undo whatever bookkeeping it kept.
|
||||
func TestJournalStateCreationBytes(t *testing.T) {
|
||||
addr := common.HexToAddress("0xaa")
|
||||
keyA := common.HexToHash("0x1")
|
||||
keyB := common.HexToHash("0x2")
|
||||
nonZero := common.HexToHash("0x42")
|
||||
otherNonZero := common.HexToHash("0x99")
|
||||
|
||||
// seedExistingAccount returns a fresh StateDB whose `addr` already exists
|
||||
// (so subsequent SetState calls won't journal a createObjectChange). Used
|
||||
// by storage-focused subtests so the account-creation contribution does
|
||||
// not bleed into the slot-accounting assertions.
|
||||
seedExistingAccount := func() *StateDB {
|
||||
st := newStateEnv().state
|
||||
st.getOrNewStateObject(addr)
|
||||
// The createObjectChange just journaled is at index 0; the upcoming
|
||||
// st.Snapshot() starts at index 1, so the createObject entry sits
|
||||
// outside any test scope and contributes nothing.
|
||||
return st
|
||||
}
|
||||
|
||||
t.Run("slotCreationInDirectFrame", func(t *testing.T) {
|
||||
st := seedExistingAccount()
|
||||
p := st.Snapshot()
|
||||
st.SetState(addr, keyA, nonZero)
|
||||
if got := st.CloseSnapshot(p); got != stateBytesPerSlot {
|
||||
t.Fatalf("0→X creation: have %d, want %d", got, stateBytesPerSlot)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("slotClearingRefundsCreation", func(t *testing.T) {
|
||||
st := seedExistingAccount()
|
||||
// Set the slot once so it has a non-zero value to clear, but make
|
||||
// the tx-original 0 by doing the set inside the tested scope.
|
||||
p := st.Snapshot()
|
||||
st.SetState(addr, keyA, nonZero) // 0 → X (creation)
|
||||
st.SetState(addr, keyA, common.Hash{}) // X → 0 (clear)
|
||||
// Net: nothing changed; the in-frame creation was undone, so
|
||||
// closeSnapshot must report -stateBytesPerSlot to refund the
|
||||
// would-be creation, but since +stateBytesPerSlot is also
|
||||
// counted... wait: the journal stores only the FIRST prevvalue
|
||||
// per slot, which is 0 for this slot in this frame. Current
|
||||
// state is 0. Per the rules: prev==0 && current==0 — neither
|
||||
// rule fires, so 0 bytes net. That correctly reflects no growth.
|
||||
if got := st.CloseSnapshot(p); got != 0 {
|
||||
t.Fatalf("0→X→0 net: have %d, want 0", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("childContributionNotDoubleCounted", func(t *testing.T) {
|
||||
st := seedExistingAccount()
|
||||
p := st.Snapshot()
|
||||
// Child creates slot A.
|
||||
c := st.Snapshot()
|
||||
st.SetState(addr, keyA, nonZero)
|
||||
childBytes := st.CloseSnapshot(c)
|
||||
if childBytes != stateBytesPerSlot {
|
||||
t.Fatalf("child closeSnapshot: have %d, want %d", childBytes, stateBytesPerSlot)
|
||||
}
|
||||
// Parent itself does not touch any slot. Its own closeSnapshot
|
||||
// must NOT re-count slot A — that contribution was already
|
||||
// reported by the child and lives in childStateBytes for the
|
||||
// purpose of revert refunds.
|
||||
parentBytes := st.CloseSnapshot(p)
|
||||
if parentBytes != 0 {
|
||||
t.Fatalf("parent closeSnapshot (no direct slots): have %d, want 0", parentBytes)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("parentSlotChangeIndependentOfChild", func(t *testing.T) {
|
||||
st := seedExistingAccount()
|
||||
p := st.Snapshot()
|
||||
// Parent creates slot A directly.
|
||||
st.SetState(addr, keyA, nonZero)
|
||||
// Child creates a different slot B.
|
||||
c := st.Snapshot()
|
||||
st.SetState(addr, keyB, otherNonZero)
|
||||
if got := st.CloseSnapshot(c); got != stateBytesPerSlot {
|
||||
t.Fatalf("child slot B creation: have %d, want %d", got, stateBytesPerSlot)
|
||||
}
|
||||
// Parent's own closeSnapshot must report only slot A (the child's
|
||||
// slot B was filtered via the closed-child range).
|
||||
if got := st.CloseSnapshot(p); got != stateBytesPerSlot {
|
||||
t.Fatalf("parent slot A creation: have %d, want %d", got, stateBytesPerSlot)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("revertReturnsAccumulatedChildBytes", func(t *testing.T) {
|
||||
st := seedExistingAccount()
|
||||
p := st.Snapshot()
|
||||
// Two successful children, each creating one slot.
|
||||
c1 := st.Snapshot()
|
||||
st.SetState(addr, keyA, nonZero)
|
||||
st.CloseSnapshot(c1)
|
||||
c2 := st.Snapshot()
|
||||
st.SetState(addr, keyB, otherNonZero)
|
||||
st.CloseSnapshot(c2)
|
||||
// Now revert the parent. The two children together emitted
|
||||
// 2 * stateBytesPerSlot, all of which should come back so the
|
||||
// caller can undo whatever was billed at close time.
|
||||
refund := st.RevertToSnapshot(p)
|
||||
want := 2 * stateBytesPerSlot
|
||||
if refund != want {
|
||||
t.Fatalf("revert refund: have %d, want %d", refund, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("perStepComposesWhenParentAndChildShareSlot", func(t *testing.T) {
|
||||
// The interleaved-slot case that used to diverge under the
|
||||
// "first-touch + current state" rule. Per-step accounting makes
|
||||
// each SSTORE carry its own delta, so the per-frame numbers may
|
||||
// look bigger but their sum is exactly what a whole-frame walk
|
||||
// over the entire subtree would produce.
|
||||
//
|
||||
// parent SSTORE S = X → entry: prev=0, new=X → +stateBytesPerSlot
|
||||
// child SSTORE S = 0 → entry: prev=X, new=0, ori=0 → -stateBytesPerSlot
|
||||
// parent SSTORE S = Y → entry: prev=0, new=Y → +stateBytesPerSlot
|
||||
//
|
||||
// child bytes = -stateBytesPerSlot
|
||||
// parent bytes = +2 * stateBytesPerSlot (two parent SSTOREs)
|
||||
// sum = +stateBytesPerSlot (= net 0→Y)
|
||||
st := seedExistingAccount()
|
||||
p := st.Snapshot()
|
||||
st.SetState(addr, keyA, nonZero) // parent direct: 0 → X
|
||||
c := st.Snapshot()
|
||||
st.SetState(addr, keyA, common.Hash{}) // child: X → 0
|
||||
childBytes := st.CloseSnapshot(c)
|
||||
st.SetState(addr, keyA, otherNonZero) // parent direct: 0 → Y
|
||||
parentBytes := st.CloseSnapshot(p)
|
||||
|
||||
if childBytes != -stateBytesPerSlot {
|
||||
t.Fatalf("child bytes (X→0 with origin 0): have %d, want %d",
|
||||
childBytes, -stateBytesPerSlot)
|
||||
}
|
||||
if parentBytes != 2*stateBytesPerSlot {
|
||||
t.Fatalf("parent bytes (two 0→nonZero SSTOREs): have %d, want %d",
|
||||
parentBytes, 2*stateBytesPerSlot)
|
||||
}
|
||||
if sum := childBytes + parentBytes; sum != stateBytesPerSlot {
|
||||
t.Fatalf("per-frame sum: have %d, want %d (= whole-frame net 0→Y)",
|
||||
sum, stateBytesPerSlot)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("perStepComposesAcrossSiblings", func(t *testing.T) {
|
||||
// Two siblings sharing a slot: A creates, B clears. Per-step,
|
||||
// each sibling's delta is independent and the parent's sum
|
||||
// matches the whole-frame net (0).
|
||||
st := seedExistingAccount()
|
||||
p := st.Snapshot()
|
||||
|
||||
a := st.Snapshot()
|
||||
st.SetState(addr, keyA, nonZero) // 0 → X
|
||||
aBytes := st.CloseSnapshot(a)
|
||||
|
||||
b := st.Snapshot()
|
||||
st.SetState(addr, keyA, common.Hash{}) // X → 0
|
||||
bBytes := st.CloseSnapshot(b)
|
||||
|
||||
pBytes := st.CloseSnapshot(p)
|
||||
|
||||
if aBytes != stateBytesPerSlot {
|
||||
t.Fatalf("sibling A bytes: have %d, want %d", aBytes, stateBytesPerSlot)
|
||||
}
|
||||
if bBytes != -stateBytesPerSlot {
|
||||
t.Fatalf("sibling B bytes: have %d, want %d", bBytes, -stateBytesPerSlot)
|
||||
}
|
||||
if pBytes != 0 {
|
||||
t.Fatalf("parent bytes (no own slots): have %d, want 0", pBytes)
|
||||
}
|
||||
if sum := aBytes + bBytes + pBytes; sum != 0 {
|
||||
t.Fatalf("per-frame sum: have %d, want 0", sum)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("perStepComposesAcrossDeepNesting", func(t *testing.T) {
|
||||
// Three-deep version of the divergence: grandparent SSTOREs S
|
||||
// before and after a child clears it. Each SSTORE contributes
|
||||
// independently and the sum equals the whole-frame net.
|
||||
st := seedExistingAccount()
|
||||
gp := st.Snapshot()
|
||||
st.SetState(addr, keyA, nonZero) // grandparent: 0 → X
|
||||
p := st.Snapshot()
|
||||
c := st.Snapshot()
|
||||
st.SetState(addr, keyA, common.Hash{}) // child: X → 0
|
||||
cBytes := st.CloseSnapshot(c)
|
||||
pBytes := st.CloseSnapshot(p)
|
||||
st.SetState(addr, keyA, otherNonZero) // grandparent: 0 → Y
|
||||
gpBytes := st.CloseSnapshot(gp)
|
||||
|
||||
if cBytes != -stateBytesPerSlot {
|
||||
t.Fatalf("child bytes: have %d, want %d", cBytes, -stateBytesPerSlot)
|
||||
}
|
||||
if pBytes != 0 {
|
||||
t.Fatalf("parent (no own SSTORE) bytes: have %d, want 0", pBytes)
|
||||
}
|
||||
if gpBytes != 2*stateBytesPerSlot {
|
||||
t.Fatalf("grandparent bytes: have %d, want %d", gpBytes, 2*stateBytesPerSlot)
|
||||
}
|
||||
if sum := cBytes + pBytes + gpBytes; sum != stateBytesPerSlot {
|
||||
t.Fatalf("per-frame sum: have %d, want %d", sum, stateBytesPerSlot)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("nonZeroOriginBouncesContributeNothing", func(t *testing.T) {
|
||||
// A slot whose tx-original is non-zero is not subject to creation
|
||||
// accounting at all: any in-tx transitions (X → 0 in parent, then
|
||||
// 0 → X in child) are merely rearranging pre-existing storage.
|
||||
// Both per-step deltas must be 0, and so must the per-frame sum.
|
||||
st := newStateEnv().state
|
||||
|
||||
// Seed the slot with a non-zero tx-original by writing directly
|
||||
// into the origin cache, simulating storage that was committed
|
||||
// before this transaction began.
|
||||
obj := st.getOrNewStateObject(addr)
|
||||
obj.originStorage[keyA] = nonZero
|
||||
|
||||
p := st.Snapshot()
|
||||
st.SetState(addr, keyA, common.Hash{}) // parent: X → 0 (origin = X)
|
||||
c := st.Snapshot()
|
||||
st.SetState(addr, keyA, nonZero) // child: 0 → X (origin = X)
|
||||
cBytes := st.CloseSnapshot(c)
|
||||
pBytes := st.CloseSnapshot(p)
|
||||
|
||||
if cBytes != 0 {
|
||||
t.Fatalf("child bytes (origin non-zero): have %d, want 0", cBytes)
|
||||
}
|
||||
if pBytes != 0 {
|
||||
t.Fatalf("parent bytes (origin non-zero): have %d, want 0", pBytes)
|
||||
}
|
||||
if sum := cBytes + pBytes; sum != 0 {
|
||||
t.Fatalf("sum (net X→X): have %d, want 0", sum)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("nestedDescendantsBubbleUp", func(t *testing.T) {
|
||||
st := seedExistingAccount()
|
||||
p := st.Snapshot()
|
||||
c := st.Snapshot()
|
||||
gc := st.Snapshot()
|
||||
// Grandchild creates a slot.
|
||||
st.SetState(addr, keyA, nonZero)
|
||||
if got := st.CloseSnapshot(gc); got != stateBytesPerSlot {
|
||||
t.Fatalf("grandchild close: have %d, want %d", got, stateBytesPerSlot)
|
||||
}
|
||||
// Child closes with no own direct slot work.
|
||||
if got := st.CloseSnapshot(c); got != 0 {
|
||||
t.Fatalf("child close (no own slots): have %d, want 0", got)
|
||||
}
|
||||
// If the parent is reverted now, the grandchild's bytes should
|
||||
// surface even though they were inherited via the intermediate
|
||||
// child.
|
||||
if refund := st.RevertToSnapshot(p); refund != stateBytesPerSlot {
|
||||
t.Fatalf("nested revert refund: have %d, want %d", refund, stateBytesPerSlot)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("accountCreationContributesPerAccountOverhead", func(t *testing.T) {
|
||||
// CreateAccount on a fresh address journals a createObjectChange,
|
||||
// which contributes +stateBytesPerAccount.
|
||||
st := newStateEnv().state
|
||||
p := st.Snapshot()
|
||||
st.CreateAccount(addr)
|
||||
if got := st.CloseSnapshot(p); got != stateBytesPerAccount {
|
||||
t.Fatalf("account creation: have %d, want %d", got, stateBytesPerAccount)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("codeCreationContributesCodeLength", func(t *testing.T) {
|
||||
// SetCode on an account whose previous code is empty contributes
|
||||
// +len(newCode); the inverse transition (non-empty → empty) refunds.
|
||||
st := seedExistingAccount()
|
||||
code := []byte{0x60, 0x00, 0x60, 0x00, 0xfd} // arbitrary 5 bytes
|
||||
|
||||
p := st.Snapshot()
|
||||
st.SetCode(addr, code, 0)
|
||||
if got := st.CloseSnapshot(p); got != len(code) {
|
||||
t.Fatalf("code creation (empty → %d bytes): have %d, want %d",
|
||||
len(code), got, len(code))
|
||||
}
|
||||
|
||||
// Now clear it again in a fresh frame: -len(code).
|
||||
p2 := st.Snapshot()
|
||||
st.SetCode(addr, nil, 0)
|
||||
if got := st.CloseSnapshot(p2); got != -len(code) {
|
||||
t.Fatalf("code clear (%d → empty bytes): have %d, want %d",
|
||||
len(code), got, -len(code))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("createAndDeployComposesAcrossFrames", func(t *testing.T) {
|
||||
// A typical CREATE: outer frame allocates an account in a child
|
||||
// frame, the child writes code and a slot. Per-step bytes:
|
||||
// child: +stateBytesPerAccount + len(code) + stateBytesPerSlot
|
||||
// outer: 0 (no own direct entries)
|
||||
// sum = child total
|
||||
st := newStateEnv().state
|
||||
code := []byte{0x60, 0x42, 0x60, 0x00, 0x55} // arbitrary 5 bytes
|
||||
p := st.Snapshot()
|
||||
c := st.Snapshot()
|
||||
st.CreateAccount(addr)
|
||||
st.SetCode(addr, code, 0)
|
||||
st.SetState(addr, keyA, nonZero)
|
||||
childBytes := st.CloseSnapshot(c)
|
||||
parentBytes := st.CloseSnapshot(p)
|
||||
|
||||
want := stateBytesPerAccount + len(code) + stateBytesPerSlot
|
||||
if childBytes != want {
|
||||
t.Fatalf("child bytes (account+code+slot): have %d, want %d",
|
||||
childBytes, want)
|
||||
}
|
||||
if parentBytes != 0 {
|
||||
t.Fatalf("parent bytes (no own work): have %d, want 0", parentBytes)
|
||||
}
|
||||
if sum := childBytes + parentBytes; sum != want {
|
||||
t.Fatalf("sum: have %d, want %d", sum, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestJournalCopyAndReset checks that the bookkeeping for closed-child ranges
|
||||
// participates in journal.copy (deep-copied, not aliased) and journal.reset
|
||||
// (cleared along with everything else).
|
||||
|
|
|
|||
|
|
@ -233,7 +233,7 @@ func (s *stateObject) SetState(key, value common.Hash) common.Hash {
|
|||
return prev
|
||||
}
|
||||
// New value is different, update and journal the change
|
||||
s.db.journal.storageChange(s.address, key, prev, origin)
|
||||
s.db.journal.storageChange(s.address, key, prev, value, origin)
|
||||
s.setState(key, value, origin)
|
||||
return prev
|
||||
}
|
||||
|
|
@ -588,7 +588,7 @@ func (s *stateObject) CodeSize() int {
|
|||
|
||||
func (s *stateObject) SetCode(codeHash common.Hash, code []byte) (prev []byte) {
|
||||
prev = slices.Clone(s.code)
|
||||
s.db.journal.setCode(s.address, prev)
|
||||
s.db.journal.setCode(s.address, prev, code)
|
||||
s.setCode(codeHash, code)
|
||||
return prev
|
||||
}
|
||||
|
|
|
|||
|
|
@ -749,8 +749,14 @@ func (s *StateDB) Snapshot() int {
|
|||
}
|
||||
|
||||
// RevertToSnapshot reverts all state changes made since the given revision.
|
||||
func (s *StateDB) RevertToSnapshot(revid int) {
|
||||
s.journal.revertToSnapshot(revid, s)
|
||||
//
|
||||
// It returns the sum of state-creation bytes that successful child frames
|
||||
// nested within the reverted scope(s) had previously emitted via
|
||||
// CloseSnapshot. The caller can use this figure to undo bookkeeping done at
|
||||
// the time those bytes were reported, since the state changes those bytes
|
||||
// were paying for are now being thrown away.
|
||||
func (s *StateDB) RevertToSnapshot(revid int) int {
|
||||
return s.journal.revertToSnapshot(revid, s)
|
||||
}
|
||||
|
||||
// CloseSnapshot marks the call frame identified by revid as completed without
|
||||
|
|
@ -758,8 +764,18 @@ func (s *StateDB) RevertToSnapshot(revid int) {
|
|||
// frame so the parent can later iterate its own entries while skipping over
|
||||
// closed children. revid must identify the topmost open snapshot (i.e. frames
|
||||
// must be closed in LIFO order). It panics otherwise.
|
||||
func (s *StateDB) CloseSnapshot(revid int) {
|
||||
s.journal.closeSnapshot(revid)
|
||||
//
|
||||
// It returns the net state-creation bytes attributable to this frame's own
|
||||
// storage changes (descendant frames' contributions are excluded — they were
|
||||
// already reported when the descendants closed). The contribution is summed
|
||||
// per individual SSTORE: each storage entry independently scores +1 slot for a
|
||||
// 0→non-zero transition and -1 for a non-zero→0 transition whose tx-original
|
||||
// was 0. Per-step accounting composes naturally — the sum across all frames
|
||||
// in a subtree equals the sum across all individual SSTORE deltas, so multiple
|
||||
// SSTOREs to the same slot at different nesting levels reconcile without any
|
||||
// "first touch" deduplication.
|
||||
func (s *StateDB) CloseSnapshot(revid int) int {
|
||||
return s.journal.closeSnapshot(revid)
|
||||
}
|
||||
|
||||
// GetRefund returns the current value of the refund counter.
|
||||
|
|
|
|||
|
|
@ -143,12 +143,12 @@ func (s *hookedStateDB) Prepare(rules params.Rules, sender, coinbase common.Addr
|
|||
s.inner.Prepare(rules, sender, coinbase, dest, precompiles, txAccesses)
|
||||
}
|
||||
|
||||
func (s *hookedStateDB) RevertToSnapshot(i int) {
|
||||
s.inner.RevertToSnapshot(i)
|
||||
func (s *hookedStateDB) RevertToSnapshot(i int) int {
|
||||
return s.inner.RevertToSnapshot(i)
|
||||
}
|
||||
|
||||
func (s *hookedStateDB) CloseSnapshot(i int) {
|
||||
s.inner.CloseSnapshot(i)
|
||||
func (s *hookedStateDB) CloseSnapshot(i int) int {
|
||||
return s.inner.CloseSnapshot(i)
|
||||
}
|
||||
|
||||
func (s *hookedStateDB) Snapshot() int {
|
||||
|
|
|
|||
|
|
@ -98,11 +98,19 @@ type StateDB interface {
|
|||
// reverting any state. The call frame's entry range is recorded on the
|
||||
// parent frame so the parent can later iterate its own entries while
|
||||
// skipping over closed children. Snapshots must be closed in LIFO order.
|
||||
CloseSnapshot(int)
|
||||
//
|
||||
// It returns the net state-creation bytes for this frame's own slot
|
||||
// changes (descendant frames' contributions are excluded — they were
|
||||
// reported when the descendants closed).
|
||||
CloseSnapshot(int) int
|
||||
|
||||
// RevertToSnapshot rolls back all state changes within the current frame
|
||||
// and discards the corresponding journal entries.
|
||||
RevertToSnapshot(int)
|
||||
//
|
||||
// It returns the sum of state-creation bytes that successful child frames
|
||||
// inside the reverted scope(s) had previously reported via CloseSnapshot,
|
||||
// so the caller can undo any bookkeeping it performed at that time.
|
||||
RevertToSnapshot(int) int
|
||||
|
||||
AddLog(*types.Log)
|
||||
LogsForBurnAccounts() []*types.Log
|
||||
|
|
|
|||
Loading…
Reference in a new issue