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"
|
"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,
|
// 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.
|
// used to record the slice of entries occupied by a closed child call frame.
|
||||||
type frameRange struct {
|
type frameRange struct {
|
||||||
|
|
@ -45,6 +54,13 @@ type revision struct {
|
||||||
// Invariant: ranges are appended in increasing order, are non-overlapping,
|
// Invariant: ranges are appended in increasing order, are non-overlapping,
|
||||||
// and lie entirely within [journalIndex, len(entries)).
|
// and lie entirely within [journalIndex, len(entries)).
|
||||||
closedChildren []frameRange
|
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
|
// 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.
|
// 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.
|
// Find the snapshot in the stack of valid snapshots.
|
||||||
idx := sort.Search(len(j.validRevisions), func(i int) bool {
|
idx := sort.Search(len(j.validRevisions), func(i int) bool {
|
||||||
return j.validRevisions[i].id >= revid
|
return j.validRevisions[i].id >= revid
|
||||||
|
|
@ -108,9 +130,20 @@ func (j *journal) revertToSnapshot(revid int, s *StateDB) {
|
||||||
}
|
}
|
||||||
snapshot := j.validRevisions[idx].journalIndex
|
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
|
// Replay the journal to undo changes and remove invalidated snapshots
|
||||||
j.revert(s, snapshot)
|
j.revert(s, snapshot)
|
||||||
j.validRevisions = j.validRevisions[:idx]
|
j.validRevisions = j.validRevisions[:idx]
|
||||||
|
return refund
|
||||||
}
|
}
|
||||||
|
|
||||||
// closeSnapshot marks the end of the call frame identified by revid without
|
// 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
|
// closeSnapshot must be invoked in LIFO order: revid must identify the topmost
|
||||||
// snapshot. It panics otherwise. The corresponding revision is popped, so a
|
// snapshot. It panics otherwise. The corresponding revision is popped, so a
|
||||||
// subsequent revertToSnapshot on the same id is no longer valid.
|
// 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 {
|
if len(j.validRevisions) == 0 {
|
||||||
panic(fmt.Errorf("revision id %v cannot be closed: no open snapshot", revid))
|
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",
|
panic(fmt.Errorf("revision id %v cannot be closed: top is %v",
|
||||||
revid, j.validRevisions[top].id))
|
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{
|
closed := frameRange{
|
||||||
start: j.validRevisions[top].journalIndex,
|
start: rev.journalIndex,
|
||||||
end: len(j.entries),
|
end: len(j.entries),
|
||||||
}
|
}
|
||||||
// Only propagate non-empty ranges, and only if there is a parent frame to
|
if top > 0 {
|
||||||
// receive them. The outermost frame has nothing to bubble up to.
|
|
||||||
if closed.start < closed.end && top > 0 {
|
|
||||||
parent := &j.validRevisions[top-1]
|
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
|
// Drop this revision's bookkeeping. The slice is reused by the parent so
|
||||||
// avoid pinning it via the popped tail.
|
// avoid pinning it via the popped tail.
|
||||||
j.validRevisions[top].closedChildren = nil
|
rev.closedChildren = nil
|
||||||
|
rev.childStateBytes = 0
|
||||||
j.validRevisions = j.validRevisions[:top]
|
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
|
// 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))
|
revisions := make([]revision, len(j.validRevisions))
|
||||||
for i, r := range j.validRevisions {
|
for i, r := range j.validRevisions {
|
||||||
revisions[i] = revision{
|
revisions[i] = revision{
|
||||||
id: r.id,
|
id: r.id,
|
||||||
journalIndex: r.journalIndex,
|
journalIndex: r.journalIndex,
|
||||||
closedChildren: slices.Clone(r.closedChildren),
|
closedChildren: slices.Clone(r.closedChildren),
|
||||||
|
childStateBytes: r.childStateBytes,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return &journal{
|
return &journal{
|
||||||
|
|
@ -244,11 +373,12 @@ func (j *journal) destruct(addr common.Address) {
|
||||||
j.append(selfDestructChange{account: addr})
|
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{
|
j.append(storageChange{
|
||||||
account: addr,
|
account: addr,
|
||||||
key: key,
|
key: key,
|
||||||
prevvalue: prev,
|
prevvalue: prev,
|
||||||
|
newvalue: newval,
|
||||||
origvalue: origin,
|
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{
|
j.append(codeChange{
|
||||||
account: address,
|
account: address,
|
||||||
prevCode: prevCode,
|
prevCode: prevCode,
|
||||||
|
newCode: newCode,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -336,11 +467,13 @@ type (
|
||||||
account common.Address
|
account common.Address
|
||||||
key common.Hash
|
key common.Hash
|
||||||
prevvalue common.Hash
|
prevvalue common.Hash
|
||||||
|
newvalue common.Hash
|
||||||
origvalue common.Hash
|
origvalue common.Hash
|
||||||
}
|
}
|
||||||
codeChange struct {
|
codeChange struct {
|
||||||
account common.Address
|
account common.Address
|
||||||
prevCode []byte
|
prevCode []byte
|
||||||
|
newCode []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
// Changes to other state values.
|
// Changes to other state values.
|
||||||
|
|
@ -472,6 +605,7 @@ func (ch codeChange) copy() journalEntry {
|
||||||
return codeChange{
|
return codeChange{
|
||||||
account: ch.account,
|
account: ch.account,
|
||||||
prevCode: ch.prevCode,
|
prevCode: ch.prevCode,
|
||||||
|
newCode: ch.newCode,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -488,6 +622,7 @@ func (ch storageChange) copy() journalEntry {
|
||||||
account: ch.account,
|
account: ch.account,
|
||||||
key: ch.key,
|
key: ch.key,
|
||||||
prevvalue: ch.prevvalue,
|
prevvalue: ch.prevvalue,
|
||||||
|
newvalue: ch.newvalue,
|
||||||
origvalue: ch.origvalue,
|
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
|
// TestJournalCopyAndReset checks that the bookkeeping for closed-child ranges
|
||||||
// participates in journal.copy (deep-copied, not aliased) and journal.reset
|
// participates in journal.copy (deep-copied, not aliased) and journal.reset
|
||||||
// (cleared along with everything else).
|
// (cleared along with everything else).
|
||||||
|
|
|
||||||
|
|
@ -233,7 +233,7 @@ func (s *stateObject) SetState(key, value common.Hash) common.Hash {
|
||||||
return prev
|
return prev
|
||||||
}
|
}
|
||||||
// New value is different, update and journal the change
|
// 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)
|
s.setState(key, value, origin)
|
||||||
return prev
|
return prev
|
||||||
}
|
}
|
||||||
|
|
@ -588,7 +588,7 @@ func (s *stateObject) CodeSize() int {
|
||||||
|
|
||||||
func (s *stateObject) SetCode(codeHash common.Hash, code []byte) (prev []byte) {
|
func (s *stateObject) SetCode(codeHash common.Hash, code []byte) (prev []byte) {
|
||||||
prev = slices.Clone(s.code)
|
prev = slices.Clone(s.code)
|
||||||
s.db.journal.setCode(s.address, prev)
|
s.db.journal.setCode(s.address, prev, code)
|
||||||
s.setCode(codeHash, code)
|
s.setCode(codeHash, code)
|
||||||
return prev
|
return prev
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -749,8 +749,14 @@ func (s *StateDB) Snapshot() int {
|
||||||
}
|
}
|
||||||
|
|
||||||
// RevertToSnapshot reverts all state changes made since the given revision.
|
// 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
|
// 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
|
// 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
|
// closed children. revid must identify the topmost open snapshot (i.e. frames
|
||||||
// must be closed in LIFO order). It panics otherwise.
|
// 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.
|
// 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)
|
s.inner.Prepare(rules, sender, coinbase, dest, precompiles, txAccesses)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *hookedStateDB) RevertToSnapshot(i int) {
|
func (s *hookedStateDB) RevertToSnapshot(i int) int {
|
||||||
s.inner.RevertToSnapshot(i)
|
return s.inner.RevertToSnapshot(i)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *hookedStateDB) CloseSnapshot(i int) {
|
func (s *hookedStateDB) CloseSnapshot(i int) int {
|
||||||
s.inner.CloseSnapshot(i)
|
return s.inner.CloseSnapshot(i)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *hookedStateDB) Snapshot() int {
|
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
|
// 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
|
// parent frame so the parent can later iterate its own entries while
|
||||||
// skipping over closed children. Snapshots must be closed in LIFO order.
|
// 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
|
// RevertToSnapshot rolls back all state changes within the current frame
|
||||||
// and discards the corresponding journal entries.
|
// 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)
|
AddLog(*types.Log)
|
||||||
LogsForBurnAccounts() []*types.Log
|
LogsForBurnAccounts() []*types.Log
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue