diff --git a/core/state/journal.go b/core/state/journal.go index e728bead5..bdee5e00f 100644 --- a/core/state/journal.go +++ b/core/state/journal.go @@ -19,10 +19,19 @@ package state import ( "math/big" + "fmt" "github.com/CortexFoundation/CortexTheseus/common" + "github.com/CortexFoundation/CortexTheseus/core/types" "maps" + "slices" + "sort" ) +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 { @@ -42,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 create a new initialized journal. @@ -51,6 +63,40 @@ 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() { + 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) @@ -95,11 +141,90 @@ 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) { + j.append(codeChange{account: &address}) +} + +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 { @@ -114,11 +239,7 @@ type ( } selfDestructChange struct { - account *common.Address - prev bool // whether account had already self-destructed - prevbalance *big.Int - prevupload *big.Int - prevnum *big.Int + account *common.Address } // Changes to individual accounts. @@ -126,14 +247,6 @@ type ( account *common.Address prev *big.Int } - uploadChange struct { - account *common.Address - prev *big.Int - } - numChange struct { - account *common.Address - prev *big.Int - } nonceChange struct { account *common.Address prev uint64 @@ -145,8 +258,7 @@ type ( origvalue common.Hash } codeChange struct { - account *common.Address - prevcode, prevhash []byte + account *common.Address } // Changes to other state values. @@ -156,9 +268,6 @@ type ( addLogChange struct { txhash common.Hash } - addPreimageChange struct { - hash common.Hash - } touchChange struct { account *common.Address } @@ -210,10 +319,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.setUpload(ch.prevupload) - obj.setNum(ch.prevnum) + obj.selfDestructed = false } } @@ -223,11 +329,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), - prevupload: new(big.Int).Set(ch.prevupload), - prevnum: new(big.Int).Set(ch.prevnum), + account: ch.account, } } @@ -254,22 +356,6 @@ func (ch balanceChange) dirtied() *common.Address { return ch.account } -func (ch uploadChange) revert(s *StateDB) { - s.getStateObject(*ch.account).setUpload(ch.prev) -} - -func (ch uploadChange) dirtied() *common.Address { - return ch.account -} - -func (ch numChange) revert(s *StateDB) { - s.getStateObject(*ch.account).setNum(ch.prev) -} - -func (ch numChange) dirtied() *common.Address { - return ch.account -} - func (ch balanceChange) copy() journalEntry { return balanceChange{ account: ch.account, @@ -293,7 +379,7 @@ func (ch nonceChange) copy() journalEntry { } func (ch codeChange) revert(s *StateDB) { - s.getStateObject(*ch.account).setCode(common.BytesToHash(ch.prevhash), ch.prevcode) + s.getStateObject(*ch.account).setCode(types.EmptyCodeHash, nil) } func (ch codeChange) dirtied() *common.Address { @@ -301,11 +387,7 @@ func (ch codeChange) dirtied() *common.Address { } func (ch codeChange) copy() journalEntry { - return codeChange{ - account: ch.account, - prevhash: common.CopyBytes(ch.prevhash), - prevcode: common.CopyBytes(ch.prevcode), - } + return codeChange{account: ch.account} } func (ch storageChange) revert(s *StateDB) { @@ -374,20 +456,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/journal_mdl.go b/core/state/journal_mdl.go new file mode 100644 index 000000000..5fff6b0a4 --- /dev/null +++ b/core/state/journal_mdl.go @@ -0,0 +1,77 @@ +// Copyright 2023 The CortexTheseus Authors +// This file is part of the CortexFoundation library. +// +// The CortexFoundation library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The CortexFoundation library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the CortexFoundation library. If not, see . + +package state + +import ( + "github.com/CortexFoundation/CortexTheseus/common" + "math/big" +) + +type ( + uploadChange struct { + account *common.Address + prev *big.Int + } + numChange struct { + account *common.Address + prev *big.Int + } +) + +func (ch uploadChange) copy() journalEntry { + return uploadChange{ + account: ch.account, + prev: new(big.Int).Set(ch.prev), + } +} + +func (ch numChange) copy() journalEntry { + return numChange{ + account: ch.account, + prev: new(big.Int).Set(ch.prev), + } +} + +func (ch uploadChange) revert(s *StateDB) { + s.getStateObject(*ch.account).setUpload(ch.prev) +} + +func (ch uploadChange) dirtied() *common.Address { + return ch.account +} + +func (ch numChange) revert(s *StateDB) { + s.getStateObject(*ch.account).setNum(ch.prev) +} + +func (ch numChange) dirtied() *common.Address { + return ch.account +} + +func (j *journal) uploadChange(addr common.Address, previous *big.Int) { + j.append(uploadChange{ + account: &addr, + prev: new(big.Int).Set(previous), + }) +} + +func (j *journal) numChange(addr common.Address, previous *big.Int) { + j.append(numChange{ + account: &addr, + prev: new(big.Int).Set(previous), + }) +} diff --git a/core/state/state_object.go b/core/state/state_object.go index 8ab326a7f..6ba6c6dda 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -132,15 +132,8 @@ func (s *stateObject) markSelfdestructed() { s.selfDestructed = true } -func (c *stateObject) touch() { - c.db.journal.append(touchChange{ - account: &c.address, - }) - if c.address == ripemd { - // Explicitly put it in the dirty-cache, which is otherwise generated from - // flattened journals. - c.db.journal.dirty(c.address) - } +func (s *stateObject) touch() { + s.db.journal.touchChange(s.address) } // getTrie returns the associated storage trie. The trie will be opened if it's @@ -270,13 +263,7 @@ func (s *stateObject) SetState(key, value common.Hash) { return } // 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) } @@ -500,10 +487,7 @@ func (c *stateObject) SubBalance(amount *big.Int) { } func (s *stateObject) SetBalance(amount *big.Int) { - 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) } @@ -578,12 +562,7 @@ func (s *stateObject) CodeSize() int { } func (s *stateObject) SetCode(codeHash common.Hash, code []byte) { - prevcode := s.Code() - s.db.journal.append(codeChange{ - account: &s.address, - prevhash: s.CodeHash(), - prevcode: prevcode, - }) + s.db.journal.setCode(s.address) s.setCode(codeHash, code) } @@ -594,10 +573,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/state_object_mdl.go b/core/state/state_object_mdl.go index c608e7ac6..db58ab19a 100644 --- a/core/state/state_object_mdl.go +++ b/core/state/state_object_mdl.go @@ -35,10 +35,7 @@ func (s *stateObject) SubUpload(amount *big.Int) *big.Int { } func (s *stateObject) SetUpload(amount *big.Int) { - s.db.journal.append(uploadChange{ - account: &s.address, - prev: new(big.Int).Set(s.data.Upload), - }) + s.db.journal.uploadChange(s.address, s.data.Upload) s.setUpload(amount) } @@ -47,10 +44,7 @@ func (s *stateObject) setNum(num *big.Int) { } func (s *stateObject) SetNum(num *big.Int) { - s.db.journal.append(numChange{ - account: &s.address, - prev: new(big.Int).Set(s.data.Num), - }) + s.db.journal.numChange(s.address, s.data.Num) s.setNum(num) } @@ -61,20 +55,7 @@ func (s *stateObject) setUpload(amount *big.Int) { func (s *stateObject) Upload() *big.Int { return s.data.Upload } + func (s *stateObject) Num() *big.Int { return s.data.Num } - -func (ch uploadChange) copy() journalEntry { - return uploadChange{ - account: ch.account, - prev: new(big.Int).Set(ch.prev), - } -} - -func (ch numChange) copy() journalEntry { - return numChange{ - account: ch.account, - prev: new(big.Int).Set(ch.prev), - } -} diff --git a/core/state/statedb.go b/core/state/statedb.go index b02512515..2704fc39d 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -23,7 +23,6 @@ import ( "maps" "math/big" "slices" - "sort" "sync" "sync/atomic" "time" @@ -54,11 +53,6 @@ func (n *proofList) Delete(key []byte) error { // TriesInMemory represents the number of layers that are kept in RAM. const TriesInMemory = 128 -type revision struct { - id int - journalIndex int -} - type mutationType int const ( @@ -147,9 +141,7 @@ type StateDB struct { // Journal of state modifications. This is the backbone of // Snapshot and RevertToSnapshot. - journal *journal - validRevisions []revision - nextRevisionId int + journal *journal AccountReads time.Duration AccountHashes time.Duration @@ -253,7 +245,7 @@ func (s *StateDB) Error() 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) @@ -282,7 +274,6 @@ 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}) s.preimages[hash] = slices.Clone(preimage) } } @@ -294,14 +285,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)) } @@ -547,17 +538,23 @@ func (s *StateDB) SelfDestruct(addr common.Address) { if stateObject == nil { return } - s.journal.append(selfDestructChange{ - account: &addr, - prev: stateObject.selfDestructed, - prevbalance: new(big.Int).Set(stateObject.Balance()), - prevupload: new(big.Int).Set(stateObject.Upload()), - prevnum: new(big.Int).Set(stateObject.Num()), - }) - stateObject.markSelfdestructed() - stateObject.data.Balance = new(big.Int) - stateObject.data.Upload = new(big.Int) - stateObject.data.Num = new(big.Int) + // Regardless of whether it is already destructed or not, we do have to + // journal the balance-change, if we set it to zero here. + if stateObject.Balance().Sign() != 0 { + stateObject.SetBalance(new(big.Int)) + } + if stateObject.Upload().Sign() != 0 { + stateObject.SetUpload(new(big.Int)) + } + if stateObject.Num().Sign() != 0 { + stateObject.SetNum(new(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.destruct(addr) + stateObject.markSelfdestructed() + } } func (s *StateDB) Selfdestruct6780(addr common.Address) { @@ -578,11 +575,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) } @@ -726,7 +719,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) *stateObject { obj := newObject(s, addr, nil) - s.journal.append(createObjectChange{account: &addr}) + s.journal.createObject(addr) s.setStateObject(obj) return obj } @@ -748,7 +741,7 @@ func (s *StateDB) CreateContract(addr common.Address) { obj := s.getStateObject(addr) if !obj.newContract { obj.newContract = true - s.journal.append(createContractChange{account: addr}) + s.journal.createContract(addr) } } @@ -810,8 +803,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, // In order for the block producer to be able to use and make additions // to the snapshot tree, we need to copy that as well. Otherwise, any @@ -854,26 +845,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, %v, %v", revid, idx, len(s.validRevisions))) - } - 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. @@ -1032,11 +1009,8 @@ func (s *StateDB) SetTxContext(thash common.Hash, ti int) { } func (s *StateDB) clearJournalAndRefund() { - if len(s.journal.entries) > 0 { - s.journal = newJournal() - s.refund = 0 - } - s.validRevisions = s.validRevisions[:0] // Snapshots can be created without journal entries + s.journal.reset() + s.refund = 0 } // Commit writes the state to the underlying in-memory trie database. @@ -1152,7 +1126,7 @@ func (s *StateDB) Commit(block uint64, deleteEmptyObjects bool) (common.Hash, er // 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) } } @@ -1164,13 +1138,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_test.go b/core/state/statedb_test.go index 1c06aad65..00c2534cc 100644 --- a/core/state/statedb_test.go +++ b/core/state/statedb_test.go @@ -253,7 +253,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])) @@ -265,6 +265,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]))