refactor(core): semantic journalling #28880 (#2081)

This is a follow-up to #29520, and a preparatory PR to a more thorough
change in the journalling system.

This PR hides the journal-implementation details away, so that the
statedb invokes methods like `JournalCreate`, instead of explicitly
appending journal-events in a list. This means that it's up to the
journal whether to implement it as a sequence of events or
aggregate/merge events.

This PR also makes it so that management of valid snapshots is moved
inside the journal, exposed via the methods `Snapshot() int` and
`RevertToSnapshot(revid int, s *StateDB)`.

JournalSetCode journals the setting of code: it is implicit that the
previous values were "no code" and emptyCodeHash. Therefore, we can
simplify the setCode journal.

The self-destruct journalling is a bit strange: we allow the
selfdestruct operation to be journalled several times. This makes it so
that we also are forced to store whether the account was already
destructed.

What we can do instead, is to only journal the first destruction, and
after that only journal balance-changes, but not journal the
selfdestruct itself.

This simplifies the journalling, so that internals about state
management does not leak into the journal-API.

Preimages were, for some reason, integrated into the journal management,
despite not being a consensus-critical data structure. This PR undoes
that.

---------

Co-authored-by: Martin HS <martin@swende.se>
Co-authored-by: Gary Rong <garyrong0905@gmail.com>
This commit is contained in:
Daniel Liu 2026-03-07 20:12:10 +08:00 committed by GitHub
parent 973efe6482
commit e02bc0723a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 148 additions and 104 deletions

View file

@ -17,13 +17,21 @@
package state package state
import ( import (
"fmt"
"maps" "maps"
"math/big" "math/big"
"slices"
"sort"
"github.com/XinFinOrg/XDPoSChain/common" "github.com/XinFinOrg/XDPoSChain/common"
"github.com/XinFinOrg/XDPoSChain/crypto" "github.com/XinFinOrg/XDPoSChain/crypto"
) )
type revision struct {
id int
journalIndex 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
// reverted on demand. // reverted on demand.
type journalEntry interface { type journalEntry interface {
@ -43,6 +51,9 @@ type journalEntry interface {
type journal struct { type journal struct {
entries []journalEntry // Current changes tracked by the journal entries []journalEntry // Current changes tracked by the journal
dirties map[common.Address]int // Dirty accounts and the number of changes dirties map[common.Address]int // Dirty accounts and the number of changes
validRevisions []revision
nextRevisionId int
} }
// newJournal creates a new initialized journal. // newJournal creates a new initialized journal.
@ -52,6 +63,41 @@ func newJournal() *journal {
} }
} }
// reset clears the journal, after this operation the journal can be used anew.
// It is semantically similar to calling 'newJournal', but the underlying slices
// can be reused.
func (j *journal) reset() {
clear(j.entries)
j.entries = j.entries[:0]
j.validRevisions = j.validRevisions[:0]
clear(j.dirties)
j.nextRevisionId = 0
}
// snapshot returns an identifier for the current revision of the state.
func (j *journal) snapshot() int {
id := j.nextRevisionId
j.nextRevisionId++
j.validRevisions = append(j.validRevisions, revision{id, j.length()})
return id
}
// revertToSnapshot reverts all state changes made since the given revision.
func (j *journal) revertToSnapshot(revid int, s *StateDB) {
// 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
})
if idx == len(j.validRevisions) || j.validRevisions[idx].id != revid {
panic(fmt.Errorf("revision id %v cannot be reverted", revid))
}
snapshot := j.validRevisions[idx].journalIndex
// Replay the journal to undo changes and remove invalidated snapshots
j.revert(s, snapshot)
j.validRevisions = j.validRevisions[:idx]
}
// append inserts a new modification entry to the end of the change journal. // append inserts a new modification entry to the end of the change journal.
func (j *journal) append(entry journalEntry) { func (j *journal) append(entry journalEntry) {
j.entries = append(j.entries, entry) j.entries = append(j.entries, entry)
@ -96,15 +142,57 @@ func (j *journal) copy() *journal {
entries = append(entries, j.entries[i].copy()) entries = append(entries, j.entries[i].copy())
} }
return &journal{ return &journal{
entries: entries, entries: entries,
dirties: maps.Clone(j.dirties), dirties: maps.Clone(j.dirties),
validRevisions: slices.Clone(j.validRevisions),
nextRevisionId: j.nextRevisionId,
} }
} }
func (j *journal) logChange(txHash common.Hash) {
j.append(addLogChange{txhash: txHash})
}
func (j *journal) createObject(addr common.Address) {
j.append(createObjectChange{account: addr})
}
func (j *journal) createContract(addr common.Address) { func (j *journal) createContract(addr common.Address) {
j.append(createContractChange{account: addr}) j.append(createContractChange{account: addr})
} }
func (j *journal) destruct(addr common.Address) {
j.append(selfDestructChange{account: addr})
}
func (j *journal) storageChange(addr common.Address, key, prev, origin common.Hash) {
j.append(storageChange{
account: addr,
key: key,
prevvalue: prev,
origvalue: origin,
})
}
func (j *journal) transientStateChange(addr common.Address, key, prev common.Hash) {
j.append(transientStorageChange{
account: addr,
key: key,
prevalue: prev,
})
}
func (j *journal) refundChange(previous uint64) {
j.append(refundChange{prev: previous})
}
func (j *journal) balanceChange(addr common.Address, previous *big.Int) {
j.append(balanceChange{
account: addr,
prev: new(big.Int).Set(previous),
})
}
func (j *journal) setCode(address common.Address, prevCode []byte) { func (j *journal) setCode(address common.Address, prevCode []byte) {
j.append(codeChange{ j.append(codeChange{
account: address, account: address,
@ -112,6 +200,35 @@ func (j *journal) setCode(address common.Address, prevCode []byte) {
}) })
} }
func (j *journal) nonceChange(address common.Address, prev uint64) {
j.append(nonceChange{
account: address,
prev: prev,
})
}
func (j *journal) touchChange(address common.Address) {
j.append(touchChange{
account: address,
})
if address == ripemd {
// Explicitly put it in the dirty-cache, which is otherwise generated from
// flattened journals.
j.dirty(address)
}
}
func (j *journal) accessListAddAccount(addr common.Address) {
j.append(accessListAddAccountChange{addr})
}
func (j *journal) accessListAddSlot(addr common.Address, slot common.Hash) {
j.append(accessListAddSlotChange{
address: addr,
slot: slot,
})
}
type ( type (
// Changes to the account trie. // Changes to the account trie.
createObjectChange struct { createObjectChange struct {
@ -124,9 +241,7 @@ type (
account common.Address account common.Address
} }
selfDestructChange struct { selfDestructChange struct {
account common.Address account common.Address
prev bool // whether account had already self-destructed
prevbalance *big.Int
} }
// Changes to individual accounts. // Changes to individual accounts.
@ -156,9 +271,6 @@ type (
addLogChange struct { addLogChange struct {
txhash common.Hash txhash common.Hash
} }
addPreimageChange struct {
hash common.Hash
}
touchChange struct { touchChange struct {
account common.Address account common.Address
} }
@ -210,8 +322,7 @@ func (ch createContractChange) copy() journalEntry {
func (ch selfDestructChange) revert(s *StateDB) { func (ch selfDestructChange) revert(s *StateDB) {
obj := s.getStateObject(ch.account) obj := s.getStateObject(ch.account)
if obj != nil { if obj != nil {
obj.selfDestructed = ch.prev obj.selfDestructed = false
obj.setBalance(ch.prevbalance)
} }
} }
@ -221,9 +332,7 @@ func (ch selfDestructChange) dirtied() *common.Address {
func (ch selfDestructChange) copy() journalEntry { func (ch selfDestructChange) copy() journalEntry {
return selfDestructChange{ return selfDestructChange{
account: ch.account, account: ch.account,
prev: ch.prev,
prevbalance: new(big.Int).Set(ch.prevbalance),
} }
} }
@ -354,20 +463,6 @@ func (ch addLogChange) copy() journalEntry {
} }
} }
func (ch addPreimageChange) revert(s *StateDB) {
delete(s.preimages, ch.hash)
}
func (ch addPreimageChange) dirtied() *common.Address {
return nil
}
func (ch addPreimageChange) copy() journalEntry {
return addPreimageChange{
hash: ch.hash,
}
}
func (ch accessListAddAccountChange) revert(s *StateDB) { func (ch accessListAddAccountChange) revert(s *StateDB) {
/* /*
One important invariant here, is that whenever a (addr, slot) is added, if the One important invariant here, is that whenever a (addr, slot) is added, if the

View file

@ -107,14 +107,7 @@ func (s *stateObject) markSelfdestructed() {
} }
func (s *stateObject) touch() { func (s *stateObject) touch() {
s.db.journal.append(touchChange{ s.db.journal.touchChange(s.address)
account: s.address,
})
if s.address == ripemd {
// Explicitly put it in the dirty-cache, which is otherwise generated from
// flattened journals.
s.db.journal.dirty(s.address)
}
} }
// getTrie returns the associated storage trie. The trie will be opened // getTrie returns the associated storage trie. The trie will be opened
@ -194,12 +187,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.append(storageChange{ s.db.journal.storageChange(s.address, key, prev, origin)
account: s.address,
key: key,
prevvalue: prev,
origvalue: origin,
})
s.setState(key, value, origin) s.setState(key, value, origin)
return prev return prev
} }
@ -379,10 +367,7 @@ func (s *stateObject) AddBalance(amount *big.Int) *big.Int {
// SetBalance sets the balance for the object, and returns the previous balance. // SetBalance sets the balance for the object, and returns the previous balance.
func (s *stateObject) SetBalance(amount *big.Int) *big.Int { func (s *stateObject) SetBalance(amount *big.Int) *big.Int {
prev := new(big.Int).Set(s.data.Balance) prev := new(big.Int).Set(s.data.Balance)
s.db.journal.append(balanceChange{ s.db.journal.balanceChange(s.address, s.data.Balance)
account: s.address,
prev: new(big.Int).Set(s.data.Balance),
})
s.setBalance(amount) s.setBalance(amount)
return prev return prev
} }
@ -468,10 +453,7 @@ func (s *stateObject) setCode(codeHash common.Hash, code []byte) {
} }
func (s *stateObject) SetNonce(nonce uint64) { func (s *stateObject) SetNonce(nonce uint64) {
s.db.journal.append(nonceChange{ s.db.journal.nonceChange(s.address, s.data.Nonce)
account: s.address,
prev: s.data.Nonce,
})
s.setNonce(nonce) s.setNonce(nonce)
} }

View file

@ -22,7 +22,6 @@ import (
"maps" "maps"
"math/big" "math/big"
"slices" "slices"
"sort"
"time" "time"
"github.com/XinFinOrg/XDPoSChain/common" "github.com/XinFinOrg/XDPoSChain/common"
@ -38,11 +37,6 @@ import (
"github.com/XinFinOrg/XDPoSChain/trie/triestate" "github.com/XinFinOrg/XDPoSChain/trie/triestate"
) )
type revision struct {
id int
journalIndex int
}
type mutationType int type mutationType int
const ( const (
@ -136,9 +130,7 @@ type StateDB struct {
// Journal of state modifications. This is the backbone of // Journal of state modifications. This is the backbone of
// Snapshot and RevertToSnapshot. // Snapshot and RevertToSnapshot.
journal *journal journal *journal
validRevisions []revision
nextRevisionId int
// Measurements gathered during execution for debugging purposes // Measurements gathered during execution for debugging purposes
AccountReads time.Duration AccountReads time.Duration
@ -224,7 +216,7 @@ func (s *StateDB) Reset(root common.Hash) error {
} }
func (s *StateDB) AddLog(log *types.Log) { func (s *StateDB) AddLog(log *types.Log) {
s.journal.append(addLogChange{txhash: s.thash}) s.journal.logChange(s.thash)
log.TxHash = s.thash log.TxHash = s.thash
log.TxIndex = uint(s.txIndex) log.TxIndex = uint(s.txIndex)
@ -258,10 +250,7 @@ func (s *StateDB) Logs() []*types.Log {
// AddPreimage records a SHA3 preimage seen by the VM. // AddPreimage records a SHA3 preimage seen by the VM.
func (s *StateDB) AddPreimage(hash common.Hash, preimage []byte) { func (s *StateDB) AddPreimage(hash common.Hash, preimage []byte) {
if _, ok := s.preimages[hash]; !ok { if _, ok := s.preimages[hash]; !ok {
s.journal.append(addPreimageChange{hash: hash}) s.preimages[hash] = slices.Clone(preimage)
pi := make([]byte, len(preimage))
copy(pi, preimage)
s.preimages[hash] = pi
} }
} }
@ -272,14 +261,14 @@ func (s *StateDB) Preimages() map[common.Hash][]byte {
// AddRefund adds gas to the refund counter // AddRefund adds gas to the refund counter
func (s *StateDB) AddRefund(gas uint64) { func (s *StateDB) AddRefund(gas uint64) {
s.journal.append(refundChange{prev: s.refund}) s.journal.refundChange(s.refund)
s.refund += gas s.refund += gas
} }
// SubRefund removes gas from the refund counter. // SubRefund removes gas from the refund counter.
// This method will panic if the refund counter goes below zero // This method will panic if the refund counter goes below zero
func (s *StateDB) SubRefund(gas uint64) { func (s *StateDB) SubRefund(gas uint64) {
s.journal.append(refundChange{prev: s.refund}) s.journal.refundChange(s.refund)
if gas > s.refund { if gas > s.refund {
panic(fmt.Sprintf("Refund counter below zero (gas: %d > refund: %d)", gas, s.refund)) panic(fmt.Sprintf("Refund counter below zero (gas: %d > refund: %d)", gas, s.refund))
} }
@ -532,11 +521,7 @@ func (s *StateDB) SelfDestruct(addr common.Address) *big.Int {
// If it is already marked as self-destructed, we do not need to add it // If it is already marked as self-destructed, we do not need to add it
// for journalling a second time. // for journalling a second time.
if !stateObject.selfDestructed { if !stateObject.selfDestructed {
s.journal.append(selfDestructChange{ s.journal.destruct(addr)
account: addr,
prev: stateObject.selfDestructed,
prevbalance: prevBalance,
})
stateObject.markSelfdestructed() stateObject.markSelfdestructed()
} }
return prevBalance return prevBalance
@ -561,11 +546,7 @@ func (s *StateDB) SetTransientState(addr common.Address, key, value common.Hash)
if prev == value { if prev == value {
return return
} }
s.journal.append(transientStorageChange{ s.journal.transientStateChange(addr, key, prev)
account: addr,
key: key,
prevalue: prev,
})
s.setTransientState(addr, key, value) s.setTransientState(addr, key, value)
} }
@ -676,7 +657,7 @@ func (s *StateDB) GetOrNewStateObject(addr common.Address) *stateObject {
// existing account with the given address, otherwise it will be silently overwritten. // existing account with the given address, otherwise it will be silently overwritten.
func (s *StateDB) createObject(addr common.Address) (obj, prev *stateObject) { func (s *StateDB) createObject(addr common.Address) (obj, prev *stateObject) {
obj = newObject(s, addr, nil) obj = newObject(s, addr, nil)
s.journal.append(createObjectChange{account: addr}) s.journal.createObject(addr)
s.setStateObject(obj) s.setStateObject(obj)
return obj, nil return obj, nil
} }
@ -763,8 +744,6 @@ func (s *StateDB) Copy() *StateDB {
logSize: s.logSize, logSize: s.logSize,
preimages: maps.Clone(s.preimages), preimages: maps.Clone(s.preimages),
journal: s.journal.copy(), journal: s.journal.copy(),
validRevisions: slices.Clone(s.validRevisions),
nextRevisionId: s.nextRevisionId,
} }
// Deep copy cached state objects. // Deep copy cached state objects.
for addr, obj := range s.stateObjects { for addr, obj := range s.stateObjects {
@ -797,26 +776,12 @@ func (s *StateDB) Copy() *StateDB {
// Snapshot returns an identifier for the current revision of the state. // Snapshot returns an identifier for the current revision of the state.
func (s *StateDB) Snapshot() int { func (s *StateDB) Snapshot() int {
id := s.nextRevisionId return s.journal.snapshot()
s.nextRevisionId++
s.validRevisions = append(s.validRevisions, revision{id, s.journal.length()})
return id
} }
// 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) { func (s *StateDB) RevertToSnapshot(revid int) {
// Find the snapshot in the stack of valid snapshots. s.journal.revertToSnapshot(revid, s)
idx := sort.Search(len(s.validRevisions), func(i int) bool {
return s.validRevisions[i].id >= revid
})
if idx == len(s.validRevisions) || s.validRevisions[idx].id != revid {
panic(fmt.Errorf("revision id %v cannot be reverted", revid))
}
snapshot := s.validRevisions[idx].journalIndex
// Replay the journal to undo changes and remove invalidated snapshots
s.journal.revert(s, snapshot)
s.validRevisions = s.validRevisions[:idx]
} }
// GetRefund returns the current value of the refund counter. // GetRefund returns the current value of the refund counter.
@ -928,8 +893,7 @@ func (s *StateDB) SetTxContext(thash common.Hash, ti int) {
} }
func (s *StateDB) clearJournalAndRefund() { func (s *StateDB) clearJournalAndRefund() {
s.journal = newJournal() s.journal.reset()
s.validRevisions = s.validRevisions[:0]
s.refund = 0 s.refund = 0
} }
@ -1221,7 +1185,7 @@ func (s *StateDB) Prepare(rules params.Rules, sender, coinbase common.Address, d
// AddAddressToAccessList adds the given address to the access list // AddAddressToAccessList adds the given address to the access list
func (s *StateDB) AddAddressToAccessList(addr common.Address) { func (s *StateDB) AddAddressToAccessList(addr common.Address) {
if s.accessList.AddAddress(addr) { if s.accessList.AddAddress(addr) {
s.journal.append(accessListAddAccountChange{addr}) s.journal.accessListAddAccount(addr)
} }
} }
@ -1233,13 +1197,10 @@ func (s *StateDB) AddSlotToAccessList(addr common.Address, slot common.Hash) {
// scope of 'address' without having the 'address' become already added // scope of 'address' without having the 'address' become already added
// to the access list (via call-variant, create, etc). // to the access list (via call-variant, create, etc).
// Better safe than sorry, though // Better safe than sorry, though
s.journal.append(accessListAddAccountChange{addr}) s.journal.accessListAddAccount(addr)
} }
if slotMod { if slotMod {
s.journal.append(accessListAddSlotChange{ s.journal.accessListAddSlot(addr, slot)
address: addr,
slot: slot,
})
} }
} }

View file

@ -72,7 +72,7 @@ func newStateTestAction(addr common.Address, r *rand.Rand, index int) testAction
args: make([]int64, 1), args: make([]int64, 1),
}, },
{ {
name: "SetState", name: "SetStorage",
fn: func(a testAction, s *StateDB) { fn: func(a testAction, s *StateDB) {
var key, val common.Hash var key, val common.Hash
binary.BigEndian.PutUint16(key[:], uint16(a.args[0])) binary.BigEndian.PutUint16(key[:], uint16(a.args[0]))

View file

@ -341,7 +341,7 @@ func newTestAction(addr common.Address, r *rand.Rand) testAction {
args: make([]int64, 1), args: make([]int64, 1),
}, },
{ {
name: "SetState", name: "SetStorage",
fn: func(a testAction, s *StateDB) { fn: func(a testAction, s *StateDB) {
var key, val common.Hash var key, val common.Hash
binary.BigEndian.PutUint16(key[:], uint16(a.args[0])) binary.BigEndian.PutUint16(key[:], uint16(a.args[0]))
@ -353,6 +353,12 @@ func newTestAction(addr common.Address, r *rand.Rand) testAction {
{ {
name: "SetCode", name: "SetCode",
fn: func(a testAction, s *StateDB) { fn: func(a testAction, s *StateDB) {
// SetCode can only be performed in case the addr does
// not already hold code
if c := s.GetCode(addr); len(c) > 0 {
// no-op
return
}
code := make([]byte, 16) code := make([]byte, 16)
binary.BigEndian.PutUint64(code, uint64(a.args[0])) binary.BigEndian.PutUint64(code, uint64(a.args[0]))
binary.BigEndian.PutUint64(code[8:], uint64(a.args[1])) binary.BigEndian.PutUint64(code[8:], uint64(a.args[1]))