diff --git a/core/state/journal.go b/core/state/journal.go index 67f658abda..2dfdafbfdd 100644 --- a/core/state/journal.go +++ b/core/state/journal.go @@ -17,13 +17,21 @@ package state import ( + "fmt" "maps" "math/big" + "slices" + "sort" "github.com/XinFinOrg/XDPoSChain/common" "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 // reverted on demand. type journalEntry interface { @@ -43,6 +51,9 @@ type journalEntry interface { type journal struct { entries []journalEntry // Current changes tracked by the journal dirties map[common.Address]int // Dirty accounts and the number of changes + + validRevisions []revision + nextRevisionId int } // 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. func (j *journal) append(entry journalEntry) { j.entries = append(j.entries, entry) @@ -96,15 +142,57 @@ func (j *journal) copy() *journal { entries = append(entries, j.entries[i].copy()) } return &journal{ - entries: entries, - dirties: maps.Clone(j.dirties), + entries: entries, + 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) { 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) { j.append(codeChange{ 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 ( // Changes to the account trie. createObjectChange struct { @@ -124,9 +241,7 @@ type ( account common.Address } selfDestructChange struct { - account common.Address - prev bool // whether account had already self-destructed - prevbalance *big.Int + account common.Address } // Changes to individual accounts. @@ -156,9 +271,6 @@ type ( addLogChange struct { txhash common.Hash } - addPreimageChange struct { - hash common.Hash - } touchChange struct { account common.Address } @@ -210,8 +322,7 @@ func (ch createContractChange) copy() journalEntry { func (ch selfDestructChange) revert(s *StateDB) { obj := s.getStateObject(ch.account) if obj != nil { - obj.selfDestructed = ch.prev - obj.setBalance(ch.prevbalance) + obj.selfDestructed = false } } @@ -221,9 +332,7 @@ func (ch selfDestructChange) dirtied() *common.Address { func (ch selfDestructChange) copy() journalEntry { return selfDestructChange{ - account: ch.account, - prev: ch.prev, - prevbalance: new(big.Int).Set(ch.prevbalance), + account: ch.account, } } @@ -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) { /* One important invariant here, is that whenever a (addr, slot) is added, if the diff --git a/core/state/state_object.go b/core/state/state_object.go index 6188ac340f..3c1587e9bb 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -107,14 +107,7 @@ func (s *stateObject) markSelfdestructed() { } func (s *stateObject) touch() { - s.db.journal.append(touchChange{ - 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) - } + s.db.journal.touchChange(s.address) } // 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 } // New value is different, update and journal the change - s.db.journal.append(storageChange{ - account: s.address, - key: key, - prevvalue: prev, - origvalue: origin, - }) + s.db.journal.storageChange(s.address, key, prev, origin) s.setState(key, value, origin) 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. func (s *stateObject) SetBalance(amount *big.Int) *big.Int { prev := new(big.Int).Set(s.data.Balance) - s.db.journal.append(balanceChange{ - account: s.address, - prev: new(big.Int).Set(s.data.Balance), - }) + s.db.journal.balanceChange(s.address, s.data.Balance) s.setBalance(amount) return prev } @@ -468,10 +453,7 @@ func (s *stateObject) setCode(codeHash common.Hash, code []byte) { } func (s *stateObject) SetNonce(nonce uint64) { - s.db.journal.append(nonceChange{ - account: s.address, - prev: s.data.Nonce, - }) + s.db.journal.nonceChange(s.address, s.data.Nonce) s.setNonce(nonce) } diff --git a/core/state/statedb.go b/core/state/statedb.go index b1b282def9..ac67ac66c7 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -22,7 +22,6 @@ import ( "maps" "math/big" "slices" - "sort" "time" "github.com/XinFinOrg/XDPoSChain/common" @@ -38,11 +37,6 @@ import ( "github.com/XinFinOrg/XDPoSChain/trie/triestate" ) -type revision struct { - id int - journalIndex int -} - type mutationType int const ( @@ -136,9 +130,7 @@ type StateDB struct { // Journal of state modifications. This is the backbone of // Snapshot and RevertToSnapshot. - journal *journal - validRevisions []revision - nextRevisionId int + journal *journal // Measurements gathered during execution for debugging purposes AccountReads time.Duration @@ -224,7 +216,7 @@ func (s *StateDB) Reset(root common.Hash) error { } func (s *StateDB) AddLog(log *types.Log) { - s.journal.append(addLogChange{txhash: s.thash}) + s.journal.logChange(s.thash) log.TxHash = s.thash log.TxIndex = uint(s.txIndex) @@ -258,10 +250,7 @@ func (s *StateDB) Logs() []*types.Log { // AddPreimage records a SHA3 preimage seen by the VM. func (s *StateDB) AddPreimage(hash common.Hash, preimage []byte) { if _, ok := s.preimages[hash]; !ok { - s.journal.append(addPreimageChange{hash: hash}) - pi := make([]byte, len(preimage)) - copy(pi, preimage) - s.preimages[hash] = pi + s.preimages[hash] = slices.Clone(preimage) } } @@ -272,14 +261,14 @@ func (s *StateDB) Preimages() map[common.Hash][]byte { // AddRefund adds gas to the refund counter func (s *StateDB) AddRefund(gas uint64) { - s.journal.append(refundChange{prev: s.refund}) + s.journal.refundChange(s.refund) s.refund += gas } // SubRefund removes gas from the refund counter. // This method will panic if the refund counter goes below zero func (s *StateDB) SubRefund(gas uint64) { - s.journal.append(refundChange{prev: s.refund}) + s.journal.refundChange(s.refund) if 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 // for journalling a second time. if !stateObject.selfDestructed { - s.journal.append(selfDestructChange{ - account: addr, - prev: stateObject.selfDestructed, - prevbalance: prevBalance, - }) + s.journal.destruct(addr) stateObject.markSelfdestructed() } return prevBalance @@ -561,11 +546,7 @@ func (s *StateDB) SetTransientState(addr common.Address, key, value common.Hash) if prev == value { return } - s.journal.append(transientStorageChange{ - account: addr, - key: key, - prevalue: prev, - }) + s.journal.transientStateChange(addr, key, prev) 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. func (s *StateDB) createObject(addr common.Address) (obj, prev *stateObject) { obj = newObject(s, addr, nil) - s.journal.append(createObjectChange{account: addr}) + s.journal.createObject(addr) s.setStateObject(obj) return obj, nil } @@ -763,8 +744,6 @@ func (s *StateDB) Copy() *StateDB { logSize: s.logSize, preimages: maps.Clone(s.preimages), journal: s.journal.copy(), - validRevisions: slices.Clone(s.validRevisions), - nextRevisionId: s.nextRevisionId, } // Deep copy cached state objects. 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. func (s *StateDB) Snapshot() int { - id := s.nextRevisionId - s.nextRevisionId++ - s.validRevisions = append(s.validRevisions, revision{id, s.journal.length()}) - return id + return s.journal.snapshot() } // RevertToSnapshot reverts all state changes made since the given revision. func (s *StateDB) RevertToSnapshot(revid int) { - // Find the snapshot in the stack of valid snapshots. - 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] + s.journal.revertToSnapshot(revid, s) } // 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() { - s.journal = newJournal() - s.validRevisions = s.validRevisions[:0] + s.journal.reset() 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 func (s *StateDB) AddAddressToAccessList(addr common.Address) { 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 // to the access list (via call-variant, create, etc). // Better safe than sorry, though - s.journal.append(accessListAddAccountChange{addr}) + s.journal.accessListAddAccount(addr) } if slotMod { - s.journal.append(accessListAddSlotChange{ - address: addr, - slot: slot, - }) + s.journal.accessListAddSlot(addr, slot) } } diff --git a/core/state/statedb_fuzz_test.go b/core/state/statedb_fuzz_test.go index 1f35d6f942..f1f786f942 100644 --- a/core/state/statedb_fuzz_test.go +++ b/core/state/statedb_fuzz_test.go @@ -72,7 +72,7 @@ func newStateTestAction(addr common.Address, r *rand.Rand, index int) testAction args: make([]int64, 1), }, { - name: "SetState", + name: "SetStorage", fn: func(a testAction, s *StateDB) { var key, val common.Hash binary.BigEndian.PutUint16(key[:], uint16(a.args[0])) diff --git a/core/state/statedb_test.go b/core/state/statedb_test.go index f77bbdaa61..5b3d1fa5ba 100644 --- a/core/state/statedb_test.go +++ b/core/state/statedb_test.go @@ -341,7 +341,7 @@ func newTestAction(addr common.Address, r *rand.Rand) testAction { args: make([]int64, 1), }, { - name: "SetState", + name: "SetStorage", fn: func(a testAction, s *StateDB) { var key, val common.Hash binary.BigEndian.PutUint16(key[:], uint16(a.args[0])) @@ -353,6 +353,12 @@ func newTestAction(addr common.Address, r *rand.Rand) testAction { { name: "SetCode", 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) binary.BigEndian.PutUint64(code, uint64(a.args[0])) binary.BigEndian.PutUint64(code[8:], uint64(a.args[1]))