From 1e71c60b5ccac6c58e40ef3271c75e093d6126c9 Mon Sep 17 00:00:00 2001 From: setunapo Date: Tue, 14 Dec 2021 17:27:53 +0800 Subject: [PATCH 01/16] Parallel: Kick off for BEP-130: Parallel Transaction Execution. Add a new interface StateProcessor.ProcessParallel(...), it is a copy of Process(...) right now. This patch is a placeholder, we will implement BEP-130 based on it. --- core/state_processor.go | 68 +++++++++++++++++++++++++++++++++++++++++ core/types.go | 3 ++ 2 files changed, 71 insertions(+) diff --git a/core/state_processor.go b/core/state_processor.go index cdaaf1e643..63e4d4a83d 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -447,6 +447,74 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg return statedb, receipts, allLogs, *usedGas, nil } +func (p *StateProcessor) ProcessParallel(block *types.Block, statedb *state.StateDB, cfg vm.Config) (*state.StateDB, types.Receipts, []*types.Log, uint64, error) { + var ( + usedGas = new(uint64) + header = block.Header() + allLogs []*types.Log + gp = new(GasPool).AddGas(block.GasLimit()) + ) + signer := types.MakeSigner(p.bc.chainConfig, block.Number()) + statedb.TryPreload(block, signer) + var receipts = make([]*types.Receipt, 0) + // Mutate the block and state according to any hard-fork specs + if p.config.DAOForkSupport && p.config.DAOForkBlock != nil && p.config.DAOForkBlock.Cmp(block.Number()) == 0 { + misc.ApplyDAOHardFork(statedb) + } + // Handle upgrade build-in system contract code + systemcontracts.UpgradeBuildInSystemContract(p.config, block.Number(), statedb) + + blockContext := NewEVMBlockContext(header, p.bc, nil) + vmenv := vm.NewEVM(blockContext, vm.TxContext{}, statedb, p.config, cfg) + + txNum := len(block.Transactions()) + // Iterate over and process the individual transactions + posa, isPoSA := p.engine.(consensus.PoSA) + commonTxs := make([]*types.Transaction, 0, txNum) + + // initilise bloom processors + bloomProcessors := NewAsyncReceiptBloomGenerator(txNum) + statedb.MarkFullProcessed() + + // usually do have two tx, one for validator set contract, another for system reward contract. + systemTxs := make([]*types.Transaction, 0, 2) + for i, tx := range block.Transactions() { + if isPoSA { + if isSystemTx, err := posa.IsSystemTransaction(tx, block.Header()); err != nil { + return statedb, nil, nil, 0, err + } else if isSystemTx { + systemTxs = append(systemTxs, tx) + continue + } + } + + msg, err := tx.AsMessage(signer) + if err != nil { + return statedb, nil, nil, 0, err + } + statedb.Prepare(tx.Hash(), block.Hash(), i) + receipt, err := applyTransaction(msg, p.config, p.bc, nil, gp, statedb, header, tx, usedGas, vmenv, bloomProcessors) + if err != nil { + return statedb, nil, nil, 0, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err) + } + + commonTxs = append(commonTxs, tx) + receipts = append(receipts, receipt) + } + bloomProcessors.Close() + + // Finalize the block, applying any consensus engine specific extras (e.g. block rewards) + err := p.engine.Finalize(p.bc, header, statedb, &commonTxs, block.Uncles(), &receipts, &systemTxs, usedGas) + if err != nil { + return statedb, receipts, allLogs, *usedGas, err + } + for _, receipt := range receipts { + allLogs = append(allLogs, receipt.Logs...) + } + + return statedb, receipts, allLogs, *usedGas, nil +} + func applyTransaction(msg types.Message, config *params.ChainConfig, bc ChainContext, author *common.Address, gp *GasPool, statedb *state.StateDB, header *types.Header, tx *types.Transaction, usedGas *uint64, evm *vm.EVM, receiptProcessors ...ReceiptProcessor) (*types.Receipt, error) { // Create a new context to be used in the EVM environment. txContext := NewEVMTxContext(msg) diff --git a/core/types.go b/core/types.go index c9061233e6..6ead2938cc 100644 --- a/core/types.go +++ b/core/types.go @@ -50,4 +50,7 @@ type Processor interface { // the transaction messages using the statedb and applying any rewards to both // the processor (coinbase) and any included uncles. Process(block *types.Block, statedb *state.StateDB, cfg vm.Config) (*state.StateDB, types.Receipts, []*types.Log, uint64, error) + + // ProcessParallel will implement BEP-130, run transactions concurrently. + ProcessParallel(block *types.Block, statedb *state.StateDB, cfg vm.Config) (*state.StateDB, types.Receipts, []*types.Log, uint64, error) } From 356605ea60726bff319aea6542a5747d9d25df40 Mon Sep 17 00:00:00 2001 From: setunapo Date: Tue, 14 Dec 2021 19:32:07 +0800 Subject: [PATCH 02/16] Parallel: implement modules && workflow ** modules of init, slot executer and dispatcher BEP 130 parallel transaction execution will maintain a tx execution routine pool, a configured number of slot(routine) to execution transactions. Init is executed once on startup and will create the routine pool. Slot executer is the place to execute transactions. The dispacther is the module that will dispatch transaction to the right slot. ** workflow: Stage Apply, Conflict Detector, Slot, Gas... > two stages of applyTransaction For sequential execution, applyTransaction will do transaction execution and result finalization. > Conflict detector We will check the parallel execution result for each transaction. If there is a confliction, the result can not be committed, redo will be scheduled to update its StateDB and re-run For parallel execution, the execution result may not be reliable(conflict), use try-rerun policy, the transaction could be executed more than once to get the correct result. Once the result is confirm, we will finalize it to StateDB. Balance, KV, Account Create&Suicide... will be checked And conflict window is important for conflict check. > Slot StateDB Each slot will have a StateDB to execute transaction in slot. The world state changes are stored in this StateDB and merged to the main StateDB when transaction result is confirmed. SlotState.slotdbChan is the current execute TX's slotDB. And only dirty state object are allowed to merge back, otherwise, there is a race condition of merge outdated stateobject back. ** others gas pool, transaction gas, gas fee reward to system address evm instance, receipt CumulativeGasUsed & Log Index, contract creation, slot state, parallel routine safety: 1.only dispatcher can access main stateDB 2.slotDB will be created and merged to stateDB in dispatch goroutine. ** workflow 2: CopyForSlot, redesign dispatch, slot StateDB reuse & several bugfix > simplifiy statedb copy with CopyForSlot only copy dirtied state objects delete prefetcher ** redesign dispatch, slot StateDB reuse... > dispatch enhance remove atomic idle, curExec... replace by pendingExec for slot. >slot StateDB reuse It will try to reuse the latest merged slotDB in the same slot. If reuse failed(conflict), it will try to update to the latest world state and redo. The reuse SlotDB will the same BaseTxIndex, since its world state was sync when it was created based on that txIndex Conflict check can skip current slot now. it is more aggressive to reuse SlotDB for idle dispatch not only pending Txs but also the idle dispatched Txs try to reuse SlotDB now. ** others state change no needs to store value add "--parallel" startup options Parallel is not enabled by default. To enable it, just add a simple flag to geth: --parallel To config parallel execute parameter: --parallel.num 20 --parallel.queuesize 30 "--parallel.num" is the number of parallel slot to execute Tx, by default it is CPUNum-1 "--parallel.queuesize" is the maxpending queue size for each slot, by default it is 10 For example: ./build/bin/geth --parallel ./build/bin/geth --parallel --parallel.num 10 ./build/bin/geth --parallel --parallel.num 20 --parallel.queuesize 30 ** several BugFix 1.system address balance conflict We take system address as a special address, since each transaction will pay gas fee to it. Parallel execution reset its balance in slotDB, if a transaction try to access its balance, it will receive 0. If the contract needs the real system address balance, we will schedule a redo with real system address balance One transaction that accessed system address: https://bscscan.com/tx/0xcd69755be1d2f55af259441ff5ee2f312830b8539899e82488a21e85bc121a2a 2.fork caused by address state changed and read in same block 3.test case error 4.statedb.Copy should initialize parallel elements 5.do merge for snapshot --- cmd/geth/main.go | 3 + cmd/utils/flags.go | 24 ++ core/blockchain.go | 12 +- core/state/journal.go | 6 +- core/state/state_object.go | 10 + core/state/statedb.go | 531 ++++++++++++++++++++++++++++--- core/state_processor.go | 623 ++++++++++++++++++++++++++++++++++++- go.mod | 1 + metrics/exp/exp.go | 4 + 9 files changed, 1159 insertions(+), 55 deletions(-) diff --git a/cmd/geth/main.go b/cmd/geth/main.go index e0f29bce77..acea1fd10d 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -163,6 +163,9 @@ var ( utils.GpoMaxGasPriceFlag, utils.EWASMInterpreterFlag, utils.EVMInterpreterFlag, + utils.ParallelTxFlag, + utils.ParallelTxNumFlag, + utils.ParallelTxQueueSizeFlag, utils.MinerNotifyFullFlag, configFileFlag, utils.CatalystFlag, diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 85d1cef887..d4b59a9c27 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -802,6 +802,20 @@ var ( Usage: "External EVM configuration (default = built-in interpreter)", Value: "", } + ParallelTxFlag = cli.BoolFlag{ + Name: "parallel", + Usage: "Enable the experimental parallel transaction execution mode (default = false)", + } + ParallelTxNumFlag = cli.IntFlag{ + Name: "parallel.num", + Usage: "Number of slot for transaction execution, only valid in parallel mode (default = CPUNum - 1)", + Value: core.ParallelExecNum, + } + ParallelTxQueueSizeFlag = cli.IntFlag{ + Name: "parallel.queuesize", + Usage: "Max number of Tx that can be queued to a slot, only valid in parallel mode (default = 10)", + Value: core.MaxPendingQueueSize, + } // Init network InitNetworkSize = cli.IntFlag{ @@ -1322,6 +1336,16 @@ func SetNodeConfig(ctx *cli.Context, cfg *node.Config) { if ctx.GlobalIsSet(InsecureUnlockAllowedFlag.Name) { cfg.InsecureUnlockAllowed = ctx.GlobalBool(InsecureUnlockAllowedFlag.Name) } + if ctx.GlobalIsSet(ParallelTxFlag.Name) { + core.ParallelTxMode = true + } + if ctx.GlobalIsSet(ParallelTxNumFlag.Name) { + core.ParallelExecNum = ctx.GlobalInt(ParallelTxNumFlag.Name) + } + if ctx.GlobalIsSet(ParallelTxQueueSizeFlag.Name) { + core.MaxPendingQueueSize = ctx.GlobalInt(ParallelTxQueueSizeFlag.Name) + } + } func setSmartCard(ctx *cli.Context, cfg *node.Config) { diff --git a/core/blockchain.go b/core/blockchain.go index eeaf1b7e0a..3c542be4e6 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -79,6 +79,7 @@ var ( errInsertionInterrupted = errors.New("insertion is interrupted") errStateRootVerificationFailed = errors.New("state root verification failed") + ParallelTxMode = false // parallel transaction execution ) const ( @@ -2128,7 +2129,16 @@ func (bc *BlockChain) insertChain(chain types.Blocks, verifySeals bool) (int, er statedb.EnablePipeCommit() } statedb.SetExpectedStateRoot(block.Root()) - statedb, receipts, logs, usedGas, err := bc.processor.Process(block, statedb, bc.vmConfig) + + var receipts types.Receipts + var logs []*types.Log + var usedGas uint64 + if ParallelTxMode { + statedb, receipts, logs, usedGas, err = bc.processor.ProcessParallel(block, statedb, bc.vmConfig) + } else { + statedb, receipts, logs, usedGas, err = bc.processor.Process(block, statedb, bc.vmConfig) + } + atomic.StoreUint32(&followupInterrupt, 1) activeState = statedb if err != nil { diff --git a/core/state/journal.go b/core/state/journal.go index d86823c2ca..d727819375 100644 --- a/core/state/journal.go +++ b/core/state/journal.go @@ -142,7 +142,11 @@ type ( ) func (ch createObjectChange) revert(s *StateDB) { - delete(s.stateObjects, *ch.account) + if s.isSlotDB { + delete(s.dirtiedStateObjectsInSlot, *ch.account) + } else { + s.stateObjects.Delete(*ch.account) + } delete(s.stateObjectsDirty, *ch.account) } diff --git a/core/state/state_object.go b/core/state/state_object.go index b40a8a2f85..57b330ecad 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -38,6 +38,8 @@ func (c Code) String() string { return string(c) //strings.Join(Disassemble(c), " ") } +type StorageKeys map[common.Hash]struct{} + type Storage map[common.Hash]common.Hash func (s Storage) String() (str string) { @@ -531,6 +533,14 @@ func (s *StateObject) deepCopy(db *StateDB) *StateObject { return stateObject } +func (s *StateObject) deepCopyForSlot(db *StateDB) *StateObject { + s.db = db + if s.trie != nil { + s.trie = db.db.CopyTrie(s.trie) + } + return s +} + // // Attribute accessors // diff --git a/core/state/statedb.go b/core/state/statedb.go index ade1b5804f..72527d39e8 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -68,6 +68,24 @@ func (n *proofList) Delete(key []byte) error { panic("not supported") } +type StateKeys map[common.Hash]struct{} + +type StateObjectSyncMap struct { + sync.Map +} + +func (s *StateObjectSyncMap) LoadStateObject(addr common.Address) (*StateObject, bool) { + stateObject, ok := s.Load(addr) + if !ok { + return nil, ok + } + return stateObject.(*StateObject), ok +} + +func (s *StateObjectSyncMap) StoreStateObject(addr common.Address, stateObject *StateObject) { + s.Store(addr, stateObject) +} + // StateDB structs within the ethereum protocol are used to store anything // within the merkle trie. StateDBs take care of caching and storing // nested states. It's the general query interface to retrieve: @@ -98,12 +116,37 @@ type StateDB struct { snapStorage map[common.Address]map[string][]byte // This map holds 'live' objects, which will get modified while processing a state transition. - stateObjects map[common.Address]*StateObject + stateObjects *StateObjectSyncMap stateObjectsPending map[common.Address]struct{} // State objects finalized but not yet written to the trie stateObjectsDirty map[common.Address]struct{} // State objects modified in the current execution storagePool *StoragePool // sharedPool to store L1 originStorage of stateObjects writeOnSharedStorage bool // Write to the shared origin storage of a stateObject while reading from the underlying storage layer. + // parallel start + isSlotDB bool + baseTxIndex int // slotDB is created base on this tx index. + SlotIndex int // debug purpose, will be removed + dirtiedStateObjectsInSlot map[common.Address]*StateObject + // for conflict check + balanceChangedInSlot map[common.Address]struct{} // the address's balance has been changed + balanceReadsInSlot map[common.Address]struct{} // the address's balance has been read and used. + codeReadInSlot map[common.Address]struct{} + codeChangeInSlot map[common.Address]struct{} + stateReadsInSlot map[common.Address]StateKeys + stateChangedInSlot map[common.Address]StateKeys // no need record value + // Actions such as SetCode, Suicide will change address's state. + // Later call like Exist(), Empty(), HasSuicided() depond on the address's state. + addrStateReadInSlot map[common.Address]struct{} + addrStateChangeInSlot map[common.Address]struct{} + stateObjectSuicided map[common.Address]struct{} + // Transaction will pay gas fee to system address. + // Parallel execution will clear system address's balance at first, in order to maintain transaction's + // gas fee value. Normal transaction will access system address twice, otherwise it means the transaction + // needs real system address's balance, the transaction will be marked redo with keepSystemAddressBalance = true + systemAddress common.Address + systemAddressCount int + keepSystemAddressBalance bool + // DB error. // State objects are used by the consensus core and VM which are // unable to deal with database-level errors. Any error that occurs @@ -160,18 +203,71 @@ func NewWithSharedPool(root common.Hash, db Database, snaps *snapshot.Tree) (*St return statedb, nil } +// With parallel, each execute slot would have its own stateDB. +// NewSlotDB creates a new slot stateDB base on the provided stateDB. +func NewSlotDB(db *StateDB, systemAddr common.Address, txIndex int, keepSystem bool) *StateDB { + slotDB := db.CopyForSlot() + slotDB.originalRoot = db.originalRoot + slotDB.baseTxIndex = txIndex + slotDB.systemAddress = systemAddr + slotDB.systemAddressCount = 0 + slotDB.keepSystemAddressBalance = keepSystem + + // clear the slotDB's validator's balance first + // for slotDB, systemAddr's value is the tx's gas fee + if !keepSystem { + slotDB.SetBalance(systemAddr, big.NewInt(0)) + } + + return slotDB +} + +// to avoid new slotDB for each Tx, slotDB should be valid and merged +func ReUseSlotDB(slotDB *StateDB, keepSystem bool) *StateDB { + if !keepSystem { + slotDB.SetBalance(slotDB.systemAddress, big.NewInt(0)) + } + slotDB.logs = make(map[common.Hash][]*types.Log, defaultNumOfSlots) + slotDB.logSize = 0 + slotDB.systemAddressCount = 0 + slotDB.keepSystemAddressBalance = keepSystem + slotDB.stateObjectSuicided = make(map[common.Address]struct{}, defaultNumOfSlots) + slotDB.codeReadInSlot = make(map[common.Address]struct{}, defaultNumOfSlots) + slotDB.codeChangeInSlot = make(map[common.Address]struct{}, defaultNumOfSlots) + slotDB.stateChangedInSlot = make(map[common.Address]StateKeys, defaultNumOfSlots) + slotDB.stateReadsInSlot = make(map[common.Address]StateKeys, defaultNumOfSlots) + slotDB.balanceChangedInSlot = make(map[common.Address]struct{}, defaultNumOfSlots) + slotDB.balanceReadsInSlot = make(map[common.Address]struct{}, defaultNumOfSlots) + slotDB.addrStateReadInSlot = make(map[common.Address]struct{}, defaultNumOfSlots) + slotDB.addrStateChangeInSlot = make(map[common.Address]struct{}, defaultNumOfSlots) + + slotDB.stateObjectsDirty = make(map[common.Address]struct{}, defaultNumOfSlots) + slotDB.stateObjectsPending = make(map[common.Address]struct{}, defaultNumOfSlots) + + return slotDB +} + func newStateDB(root common.Hash, db Database, snaps *snapshot.Tree) (*StateDB, error) { sdb := &StateDB{ - db: db, - originalRoot: root, - snaps: snaps, - stateObjects: make(map[common.Address]*StateObject, defaultNumOfSlots), - stateObjectsPending: make(map[common.Address]struct{}, defaultNumOfSlots), - stateObjectsDirty: make(map[common.Address]struct{}, defaultNumOfSlots), - logs: make(map[common.Hash][]*types.Log, defaultNumOfSlots), - preimages: make(map[common.Hash][]byte), - journal: newJournal(), - hasher: crypto.NewKeccakState(), + db: db, + originalRoot: root, + snaps: snaps, + stateObjects: &StateObjectSyncMap{}, + stateObjectSuicided: make(map[common.Address]struct{}, defaultNumOfSlots), + codeReadInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), + codeChangeInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), + stateChangedInSlot: make(map[common.Address]StateKeys, defaultNumOfSlots), + stateReadsInSlot: make(map[common.Address]StateKeys, defaultNumOfSlots), + balanceChangedInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), + balanceReadsInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), + addrStateReadInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), + addrStateChangeInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), + stateObjectsPending: make(map[common.Address]struct{}, defaultNumOfSlots), + stateObjectsDirty: make(map[common.Address]struct{}, defaultNumOfSlots), + logs: make(map[common.Hash][]*types.Log, defaultNumOfSlots), + preimages: make(map[common.Hash][]byte), + journal: newJournal(), + hasher: crypto.NewKeccakState(), } if sdb.snaps != nil { if sdb.snap = sdb.snaps.Snapshot(root); sdb.snap != nil { @@ -195,6 +291,119 @@ func (s *StateDB) EnableWriteOnSharedStorage() { s.writeOnSharedStorage = true } +func (s *StateDB) getStateObjectFromStateObjects(addr common.Address) (*StateObject, bool) { + if s.isSlotDB { + obj, ok := s.dirtiedStateObjectsInSlot[addr] + if ok { + return obj, ok + } + } + return s.stateObjects.LoadStateObject(addr) +} + +// MergeSlotDB is for Parallel TX, when the TX is finalized(dirty -> pending) +// A bit similar to StateDB.Copy(), +// mainly copy stateObjects, since slotDB has been finalized. +// return: objSuicided, stateChanges, balanceChanges, codeChanges +func (s *StateDB) MergeSlotDB(slotDb *StateDB, slotReceipt *types.Receipt) (map[common.Address]struct{}, map[common.Address]StateKeys, map[common.Address]struct{}, map[common.Address]struct{}, map[common.Address]struct{}) { + // receipt.Logs with unified log Index within a block + // align slotDB's logs Index to the block stateDB's logSize + for _, l := range slotReceipt.Logs { + l.Index += s.logSize + } + s.logSize += slotDb.logSize + + // before merge, do validator reward first: AddBalance to consensus.SystemAddress + // object of SystemAddress is take care specially + systemAddress := slotDb.systemAddress + if slotDb.keepSystemAddressBalance { + s.SetBalance(systemAddress, slotDb.GetBalance(systemAddress)) + } else { + s.AddBalance(systemAddress, slotDb.GetBalance(systemAddress)) + } + + // only merge dirty objects + for addr := range slotDb.stateObjectsDirty { + if _, exist := s.stateObjectsDirty[addr]; !exist { + s.stateObjectsDirty[addr] = struct{}{} + } + + if addr == systemAddress { + continue + } + + // stateObjects: KV, balance, nonce... + if obj, ok := slotDb.getStateObjectFromStateObjects(addr); ok { + s.stateObjects.StoreStateObject(addr, obj.deepCopyForSlot(s)) + } + } + + for addr := range slotDb.stateObjectsPending { + if _, exist := s.stateObjectsPending[addr]; !exist { + s.stateObjectsPending[addr] = struct{}{} + } + } + + for addr, obj := range slotDb.dirtiedStateObjectsInSlot { + if addr == systemAddress { + continue + } + + if _, exist := s.stateObjects.LoadStateObject(addr); !exist { + s.stateObjects.StoreStateObject(addr, obj.deepCopyForSlot(s)) + } + } + + // slotDb.logs: logs will be kept in receipts, no need to do merge + + // Fixed: preimages should be merged not overwrite + for hash, preimage := range slotDb.preimages { + s.preimages[hash] = preimage + } + // Fixed: accessList should be merged not overwrite + if s.accessList != nil { + s.accessList = slotDb.accessList.Copy() + } + if slotDb.snaps != nil { + for k, v := range slotDb.snapDestructs { + s.snapDestructs[k] = v + } + for k, v := range slotDb.snapAccounts { + s.snapAccounts[k] = v + } + for k, v := range slotDb.snapStorage { + temp := make(map[string][]byte) + for kk, vv := range v { + temp[kk] = vv + } + s.snapStorage[k] = temp + } + } + + objectSuicided := make(map[common.Address]struct{}, len(slotDb.stateObjectSuicided)) + for addr := range slotDb.stateObjectSuicided { + objectSuicided[addr] = struct{}{} + } + stateChanges := make(map[common.Address]StateKeys, len(slotDb.stateChangedInSlot)) // must be a deep copy, since + for addr, storage := range slotDb.stateChangedInSlot { + stateChanges[addr] = storage + } + balanceChanges := make(map[common.Address]struct{}, len(slotDb.balanceChangedInSlot)) // must be a deep copy, since + for addr := range slotDb.balanceChangedInSlot { + balanceChanges[addr] = struct{}{} + } + codeChanges := make(map[common.Address]struct{}, len(slotDb.codeChangeInSlot)) + for addr := range slotDb.codeChangeInSlot { + codeChanges[addr] = struct{}{} + } + addrStateChanges := make(map[common.Address]struct{}, len(slotDb.addrStateChangeInSlot)) + for addr := range slotDb.addrStateChangeInSlot { + addrStateChanges[addr] = struct{}{} + } + + return objectSuicided, stateChanges, balanceChanges, codeChanges, addrStateChanges +} + // StartPrefetcher initializes a new trie prefetcher to pull in nodes from the // state trie concurrently while the state is mutated so that when we reach the // commit phase, most of the needed data is already hot. @@ -345,6 +554,9 @@ func (s *StateDB) SubRefund(gas uint64) { // Exist reports whether the given account address exists in the state. // Notably this also returns true for suicided accounts. func (s *StateDB) Exist(addr common.Address) bool { + if s.isSlotDB { + s.addrStateReadInSlot[addr] = struct{}{} + } return s.getStateObject(addr) != nil } @@ -352,11 +564,20 @@ func (s *StateDB) Exist(addr common.Address) bool { // or empty according to the EIP161 specification (balance = nonce = code = 0) func (s *StateDB) Empty(addr common.Address) bool { so := s.getStateObject(addr) + if s.isSlotDB { + s.addrStateReadInSlot[addr] = struct{}{} + } return so == nil || so.empty() } // GetBalance retrieves the balance from the given address or 0 if object not found func (s *StateDB) GetBalance(addr common.Address) *big.Int { + if s.isSlotDB { + s.balanceReadsInSlot[addr] = struct{}{} + if addr == s.systemAddress { + s.systemAddressCount++ + } + } stateObject := s.getStateObject(addr) if stateObject != nil { return stateObject.Balance() @@ -383,7 +604,35 @@ func (s *StateDB) BlockHash() common.Hash { return s.bhash } +// BaseTxIndex returns the tx index that slot db based. +func (s *StateDB) BaseTxIndex() int { + return s.baseTxIndex +} + +func (s *StateDB) CodeReadInSlot() map[common.Address]struct{} { + return s.codeReadInSlot +} + +func (s *StateDB) AddressReadInSlot() map[common.Address]struct{} { + return s.addrStateReadInSlot +} + +func (s *StateDB) StateReadsInSlot() map[common.Address]StateKeys { + return s.stateReadsInSlot +} + +func (s *StateDB) BalanceReadsInSlot() map[common.Address]struct{} { + return s.balanceReadsInSlot +} +func (s *StateDB) SystemAddressRedo() bool { + return s.systemAddressCount > 2 +} + func (s *StateDB) GetCode(addr common.Address) []byte { + if s.isSlotDB { + s.codeReadInSlot[addr] = struct{}{} + } + stateObject := s.getStateObject(addr) if stateObject != nil { return stateObject.Code(s.db) @@ -392,6 +641,10 @@ func (s *StateDB) GetCode(addr common.Address) []byte { } func (s *StateDB) GetCodeSize(addr common.Address) int { + if s.isSlotDB { + s.codeReadInSlot[addr] = struct{}{} // code size is part of code + } + stateObject := s.getStateObject(addr) if stateObject != nil { return stateObject.CodeSize(s.db) @@ -400,6 +653,10 @@ func (s *StateDB) GetCodeSize(addr common.Address) int { } func (s *StateDB) GetCodeHash(addr common.Address) common.Hash { + if s.isSlotDB { + s.codeReadInSlot[addr] = struct{}{} // code hash is part of code + } + stateObject := s.getStateObject(addr) if stateObject == nil { return common.Hash{} @@ -409,6 +666,13 @@ func (s *StateDB) GetCodeHash(addr common.Address) common.Hash { // GetState retrieves a value from the given account's storage trie. func (s *StateDB) GetState(addr common.Address, hash common.Hash) common.Hash { + if s.isSlotDB { + if s.stateReadsInSlot[addr] == nil { + s.stateReadsInSlot[addr] = make(map[common.Hash]struct{}, defaultNumOfSlots) + } + s.stateReadsInSlot[addr][hash] = struct{}{} + } + stateObject := s.getStateObject(addr) if stateObject != nil { return stateObject.GetState(s.db, hash) @@ -481,6 +745,10 @@ func (s *StateDB) StorageTrie(addr common.Address) Trie { func (s *StateDB) HasSuicided(addr common.Address) bool { stateObject := s.getStateObject(addr) + + if s.isSlotDB { + s.addrStateReadInSlot[addr] = struct{}{} // address suicided. + } if stateObject != nil { return stateObject.suicided } @@ -493,45 +761,134 @@ func (s *StateDB) HasSuicided(addr common.Address) bool { // AddBalance adds amount to the account associated with addr. func (s *StateDB) AddBalance(addr common.Address, amount *big.Int) { + if s.isSlotDB { + // just in case other tx creates this account, we will miss this if we only add this account when found + s.balanceChangedInSlot[addr] = struct{}{} + s.balanceReadsInSlot[addr] = struct{}{} // add balance will perform a read operation first + if addr == s.systemAddress { + s.systemAddressCount++ + } + } + stateObject := s.GetOrNewStateObject(addr) if stateObject != nil { - stateObject.AddBalance(amount) + if s.isSlotDB { + if _, ok := s.dirtiedStateObjectsInSlot[addr]; !ok { + newStateObject := stateObject.deepCopy(s) + newStateObject.AddBalance(amount) + s.dirtiedStateObjectsInSlot[addr] = newStateObject + } else { + stateObject.AddBalance(amount) + } + } else { + stateObject.AddBalance(amount) + } } } // SubBalance subtracts amount from the account associated with addr. func (s *StateDB) SubBalance(addr common.Address, amount *big.Int) { + if s.isSlotDB { + // just in case other tx creates this account, we will miss this if we only add this account when found + s.balanceChangedInSlot[addr] = struct{}{} + s.balanceReadsInSlot[addr] = struct{}{} + if addr == s.systemAddress { + s.systemAddressCount++ + } + } + stateObject := s.GetOrNewStateObject(addr) if stateObject != nil { - stateObject.SubBalance(amount) + if s.isSlotDB { + if _, ok := s.dirtiedStateObjectsInSlot[addr]; !ok { + newStateObject := stateObject.deepCopy(s) + newStateObject.SubBalance(amount) + s.dirtiedStateObjectsInSlot[addr] = newStateObject + } else { + stateObject.SubBalance(amount) + } + } else { + stateObject.SubBalance(amount) + } } } func (s *StateDB) SetBalance(addr common.Address, amount *big.Int) { stateObject := s.GetOrNewStateObject(addr) if stateObject != nil { - stateObject.SetBalance(amount) + if s.isSlotDB { + if _, ok := s.dirtiedStateObjectsInSlot[addr]; !ok { + newStateObject := stateObject.deepCopy(s) + newStateObject.SetBalance(amount) + s.dirtiedStateObjectsInSlot[addr] = newStateObject + } else { + stateObject.SetBalance(amount) + } + s.balanceChangedInSlot[addr] = struct{}{} + if addr == s.systemAddress { + s.systemAddressCount++ + } + } else { + stateObject.SetBalance(amount) + } } } func (s *StateDB) SetNonce(addr common.Address, nonce uint64) { stateObject := s.GetOrNewStateObject(addr) if stateObject != nil { - stateObject.SetNonce(nonce) + if s.isSlotDB { + if _, ok := s.dirtiedStateObjectsInSlot[addr]; !ok { + newStateObject := stateObject.deepCopy(s) + newStateObject.SetNonce(nonce) + s.dirtiedStateObjectsInSlot[addr] = newStateObject + } else { + stateObject.SetNonce(nonce) + } + } else { + stateObject.SetNonce(nonce) + } } } func (s *StateDB) SetCode(addr common.Address, code []byte) { stateObject := s.GetOrNewStateObject(addr) if stateObject != nil { - stateObject.SetCode(crypto.Keccak256Hash(code), code) + if s.isSlotDB { + if _, ok := s.dirtiedStateObjectsInSlot[addr]; !ok { + newStateObject := stateObject.deepCopy(s) + newStateObject.SetCode(crypto.Keccak256Hash(code), code) + s.dirtiedStateObjectsInSlot[addr] = newStateObject + } else { + stateObject.SetCode(crypto.Keccak256Hash(code), code) + } + + s.codeChangeInSlot[addr] = struct{}{} + } else { + stateObject.SetCode(crypto.Keccak256Hash(code), code) + } } } func (s *StateDB) SetState(addr common.Address, key, value common.Hash) { stateObject := s.GetOrNewStateObject(addr) if stateObject != nil { - stateObject.SetState(s.db, key, value) + if s.isSlotDB { + if _, ok := s.dirtiedStateObjectsInSlot[addr]; !ok { + newStateObject := stateObject.deepCopy(s) + newStateObject.SetState(s.db, key, value) + s.dirtiedStateObjectsInSlot[addr] = newStateObject + } else { + stateObject.SetState(s.db, key, value) + } + + if s.stateChangedInSlot[addr] == nil { + s.stateChangedInSlot[addr] = make(StateKeys, defaultNumOfSlots) + } + s.stateChangedInSlot[addr][key] = struct{}{} + } else { + stateObject.SetState(s.db, key, value) + } } } @@ -551,9 +908,16 @@ func (s *StateDB) SetStorage(addr common.Address, storage map[common.Hash]common // getStateObject will return a non-nil account after Suicide. func (s *StateDB) Suicide(addr common.Address) bool { stateObject := s.getStateObject(addr) + // fixme: should add read stateobject record if stateObject == nil { + log.Warn("StateDB Suicide stateObject not found", "slot", s.SlotIndex, "addr", addr) return false } + if s.isSlotDB { + s.stateObjectSuicided[addr] = struct{}{} + s.addrStateChangeInSlot[addr] = struct{}{} // address suicided. + } + s.journal.append(suicideChange{ account: &addr, prev: stateObject.suicided, @@ -619,7 +983,7 @@ func (s *StateDB) getStateObject(addr common.Address) *StateObject { // destructed object instead of wiping all knowledge about the state object. func (s *StateDB) getDeletedStateObject(addr common.Address) *StateObject { // Prefer live objects if any is available - if obj := s.stateObjects[addr]; obj != nil { + if obj, _ := s.getStateObjectFromStateObjects(addr); obj != nil { return obj } // If no live objects are available, attempt to use snapshots @@ -684,7 +1048,11 @@ func (s *StateDB) getDeletedStateObject(addr common.Address) *StateObject { } func (s *StateDB) SetStateObject(object *StateObject) { - s.stateObjects[object.Address()] = object + if s.isSlotDB { + s.dirtiedStateObjectsInSlot[object.Address()] = object + } else { + s.stateObjects.StoreStateObject(object.Address(), object) + } } // GetOrNewStateObject retrieves a state object or create a new state object if nil. @@ -692,6 +1060,9 @@ func (s *StateDB) GetOrNewStateObject(addr common.Address) *StateObject { stateObject := s.getStateObject(addr) if stateObject == nil { stateObject, _ = s.createObject(addr) + if s.isSlotDB { + s.addrStateChangeInSlot[addr] = struct{}{} // address created. + } } return stateObject } @@ -736,18 +1107,21 @@ func (s *StateDB) CreateAccount(addr common.Address) { newObj, prev := s.createObject(addr) if prev != nil { newObj.setBalance(prev.data.Balance) + } else if s.isSlotDB { + s.addrStateChangeInSlot[addr] = struct{}{} // new account created } + } -func (db *StateDB) ForEachStorage(addr common.Address, cb func(key, value common.Hash) bool) error { - so := db.getStateObject(addr) +func (s *StateDB) ForEachStorage(addr common.Address, cb func(key, value common.Hash) bool) error { + so := s.getStateObject(addr) if so == nil { return nil } - it := trie.NewIterator(so.getTrie(db.db).NodeIterator(nil)) + it := trie.NewIterator(so.getTrie(s.db).NodeIterator(nil)) for it.Next() { - key := common.BytesToHash(db.trie.GetKey(it.Key)) + key := common.BytesToHash(s.trie.GetKey(it.Key)) if value, dirty := so.dirtyStorage[key]; dirty { if !cb(key, value) { return nil @@ -775,7 +1149,7 @@ func (s *StateDB) Copy() *StateDB { state := &StateDB{ db: s.db, trie: s.db.CopyTrie(s.trie), - stateObjects: make(map[common.Address]*StateObject, len(s.journal.dirties)), + stateObjects: &StateObjectSyncMap{}, stateObjectsPending: make(map[common.Address]struct{}, len(s.stateObjectsPending)), stateObjectsDirty: make(map[common.Address]struct{}, len(s.journal.dirties)), storagePool: s.storagePool, @@ -785,6 +1159,18 @@ func (s *StateDB) Copy() *StateDB { preimages: make(map[common.Hash][]byte, len(s.preimages)), journal: newJournal(), hasher: crypto.NewKeccakState(), + + isSlotDB: false, + stateObjectSuicided: make(map[common.Address]struct{}, defaultNumOfSlots), + codeReadInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), + codeChangeInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), + stateChangedInSlot: make(map[common.Address]StateKeys, defaultNumOfSlots), + stateReadsInSlot: make(map[common.Address]StateKeys, defaultNumOfSlots), + balanceChangedInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), + balanceReadsInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), + addrStateReadInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), + addrStateChangeInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), + dirtiedStateObjectsInSlot: make(map[common.Address]*StateObject, defaultNumOfSlots), } // Copy the dirty states, logs, and preimages for addr := range s.journal.dirties { @@ -792,11 +1178,11 @@ func (s *StateDB) Copy() *StateDB { // and in the Finalise-method, there is a case where an object is in the journal but not // in the stateObjects: OOG after touch on ripeMD prior to Byzantium. Thus, we need to check for // nil - if object, exist := s.stateObjects[addr]; exist { + if object, exist := s.getStateObjectFromStateObjects(addr); exist { // Even though the original object is dirty, we are not copying the journal, // so we need to make sure that anyside effect the journal would have caused // during a commit (or similar op) is already applied to the copy. - state.stateObjects[addr] = object.deepCopy(state) + state.stateObjects.StoreStateObject(addr, object.deepCopy(state)) state.stateObjectsDirty[addr] = struct{}{} // Mark the copy dirty to force internal (code/state) commits state.stateObjectsPending[addr] = struct{}{} // Mark the copy pending to force external (account) commits @@ -806,14 +1192,16 @@ func (s *StateDB) Copy() *StateDB { // loop above will be a no-op, since the copy's journal is empty. // Thus, here we iterate over stateObjects, to enable copies of copies for addr := range s.stateObjectsPending { - if _, exist := state.stateObjects[addr]; !exist { - state.stateObjects[addr] = s.stateObjects[addr].deepCopy(state) + if _, exist := state.getStateObjectFromStateObjects(addr); !exist { + object, _ := s.getStateObjectFromStateObjects(addr) + state.stateObjects.StoreStateObject(addr, object.deepCopy(state)) } state.stateObjectsPending[addr] = struct{}{} } for addr := range s.stateObjectsDirty { - if _, exist := state.stateObjects[addr]; !exist { - state.stateObjects[addr] = s.stateObjects[addr].deepCopy(state) + if _, exist := state.getStateObjectFromStateObjects(addr); !exist { + object, _ := s.getStateObjectFromStateObjects(addr) + state.stateObjects.StoreStateObject(addr, object.deepCopy(state)) } state.stateObjectsDirty[addr] = struct{}{} } @@ -871,6 +1259,69 @@ func (s *StateDB) Copy() *StateDB { return state } +func (s *StateDB) CopyForSlot() *StateDB { + // Copy all the basic fields, initialize the memory ones + state := &StateDB{ + db: s.db, + trie: s.db.CopyTrie(s.trie), + stateObjects: s.stateObjects, + stateObjectsPending: make(map[common.Address]struct{}, defaultNumOfSlots), + stateObjectsDirty: make(map[common.Address]struct{}, defaultNumOfSlots), + refund: s.refund, + logs: make(map[common.Hash][]*types.Log, defaultNumOfSlots), + logSize: 0, + preimages: make(map[common.Hash][]byte, len(s.preimages)), + journal: newJournal(), + hasher: crypto.NewKeccakState(), + snapDestructs: make(map[common.Address]struct{}), + snapAccounts: make(map[common.Address][]byte), + snapStorage: make(map[common.Address]map[string][]byte), + stateObjectSuicided: make(map[common.Address]struct{}, defaultNumOfSlots), + codeReadInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), + codeChangeInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), + stateChangedInSlot: make(map[common.Address]StateKeys, defaultNumOfSlots), + stateReadsInSlot: make(map[common.Address]StateKeys, defaultNumOfSlots), + balanceChangedInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), + balanceReadsInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), + addrStateReadInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), + addrStateChangeInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), + + isSlotDB: true, + dirtiedStateObjectsInSlot: make(map[common.Address]*StateObject, defaultNumOfSlots), + } + + for hash, preimage := range s.preimages { + state.preimages[hash] = preimage + } + + if s.snaps != nil { + // In order for the miner to be able to use and make additions + // to the snapshot tree, we need to copy that aswell. + // Otherwise, any block mined by ourselves will cause gaps in the tree, + // and force the miner to operate trie-backed only + state.snaps = s.snaps + state.snap = s.snap + // deep copy needed + state.snapDestructs = make(map[common.Address]struct{}) + for k, v := range s.snapDestructs { + state.snapDestructs[k] = v + } + state.snapAccounts = make(map[common.Address][]byte) + for k, v := range s.snapAccounts { + state.snapAccounts[k] = v + } + state.snapStorage = make(map[common.Address]map[string][]byte) + for k, v := range s.snapStorage { + temp := make(map[string][]byte) + for kk, vv := range v { + temp[kk] = vv + } + state.snapStorage[k] = temp + } + } + return state +} + // Snapshot returns an identifier for the current revision of the state. func (s *StateDB) Snapshot() int { id := s.nextRevisionId @@ -917,7 +1368,7 @@ func (s *StateDB) WaitPipeVerification() error { func (s *StateDB) Finalise(deleteEmptyObjects bool) { addressesToPrefetch := make([][]byte, 0, len(s.journal.dirties)) for addr := range s.journal.dirties { - obj, exist := s.stateObjects[addr] + obj, exist := s.getStateObjectFromStateObjects(addr) if !exist { // ripeMD is 'touched' at block 1714175, in tx 0x1237f737031e40bcde4a8b7e717b2d15e3ecadfe49bb1bbc71ee9deb09c6fcf2 // That tx goes out of gas, and although the notion of 'touched' does not exist there, the @@ -987,21 +1438,24 @@ func (s *StateDB) CorrectAccountsRoot(blockRoot common.Hash) { return } if accounts, err := snapshot.Accounts(); err == nil && accounts != nil { - for _, obj := range s.stateObjects { + s.stateObjects.Range(func(addr, objItf interface{}) bool { + obj := objItf.(*StateObject) if !obj.deleted && !obj.rootCorrected && obj.data.Root == dummyRoot { if account, exist := accounts[crypto.Keccak256Hash(obj.address[:])]; exist && len(account.Root) != 0 { obj.data.Root = common.BytesToHash(account.Root) obj.rootCorrected = true } } - } + return true + }) + } } //PopulateSnapAccountAndStorage tries to populate required accounts and storages for pipecommit func (s *StateDB) PopulateSnapAccountAndStorage() { for addr := range s.stateObjectsPending { - if obj := s.stateObjects[addr]; !obj.deleted { + if obj, _ := s.getStateObjectFromStateObjects(addr); !obj.deleted { if s.snap != nil && !obj.deleted { root := obj.data.Root storageChanged := s.populateSnapStorage(obj) @@ -1068,11 +1522,10 @@ func (s *StateDB) AccountsIntermediateRoot() { // first, giving the account prefeches just a few more milliseconds of time // to pull useful data from disk. for addr := range s.stateObjectsPending { - if obj := s.stateObjects[addr]; !obj.deleted { + if obj, _ := s.getStateObjectFromStateObjects(addr); !obj.deleted { wg.Add(1) tasks <- func() { obj.updateRoot(s.db) - // If state snapshotting is active, cache the data til commit. Note, this // update mechanism is not symmetric to the deletion, because whereas it is // enough to track account updates at commit time, deletions need tracking @@ -1132,7 +1585,7 @@ func (s *StateDB) StateIntermediateRoot() common.Hash { usedAddrs := make([][]byte, 0, len(s.stateObjectsPending)) for addr := range s.stateObjectsPending { - if obj := s.stateObjects[addr]; obj.deleted { + if obj, _ := s.getStateObjectFromStateObjects(addr); obj.deleted { s.deleteStateObject(obj) } else { s.updateStateObject(obj) @@ -1373,7 +1826,7 @@ func (s *StateDB) Commit(failPostCommitFunc func(), postCommitFuncs ...func() er } for addr := range s.stateObjectsDirty { - if obj := s.stateObjects[addr]; !obj.deleted { + if obj, _ := s.getStateObjectFromStateObjects(addr); !obj.deleted { // Write any contract code associated with the state object tasks <- func() { // Write any storage changes in the state object to its storage trie @@ -1441,7 +1894,7 @@ func (s *StateDB) Commit(failPostCommitFunc func(), postCommitFuncs ...func() er func() error { codeWriter := s.db.TrieDB().DiskDB().NewBatch() for addr := range s.stateObjectsDirty { - if obj := s.stateObjects[addr]; !obj.deleted { + if obj, _ := s.getStateObjectFromStateObjects(addr); !obj.deleted { if obj.code != nil && obj.dirtyCode { rawdb.WriteCode(codeWriter, common.BytesToHash(obj.CodeHash()), obj.code) obj.dirtyCode = false diff --git a/core/state_processor.go b/core/state_processor.go index 63e4d4a83d..f2c04e4e5f 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -22,6 +22,7 @@ import ( "fmt" "math/big" "math/rand" + "runtime" "sync" "time" @@ -46,8 +47,12 @@ const ( recentTime = 1024 * 3 recentDiffLayerTimeout = 5 farDiffLayerTimeout = 2 + reuseSlotDB = false // parallel slot's pending Txs will reuse the latest slotDB ) +var MaxPendingQueueSize = 10 // parallel slot's maximum number of pending Txs +var ParallelExecNum = runtime.NumCPU() - 1 // leave a CPU to dispatcher + // StateProcessor is a basic Processor, which takes care of transitioning // state from one point to another. // @@ -56,6 +61,12 @@ type StateProcessor struct { config *params.ChainConfig // Chain configuration options bc *BlockChain // Canonical block chain engine consensus.Engine // Consensus engine used for block rewards + + // add for parallel execute + paraInitialized bool // todo: should use atomic value + paraTxResultChan chan *ParallelTxResult // to notify dispatcher that a tx is done + slotState []*SlotState // idle, or pending messages + mergedTxIndex int // the latest finalized tx index } // NewStateProcessor initialises a new StateProcessor. @@ -401,7 +412,7 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg posa, isPoSA := p.engine.(consensus.PoSA) commonTxs := make([]*types.Transaction, 0, txNum) - // initilise bloom processors + // initialise bloom processors bloomProcessors := NewAsyncReceiptBloomGenerator(txNum) statedb.MarkFullProcessed() @@ -447,7 +458,485 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg return statedb, receipts, allLogs, *usedGas, nil } +type MergedTxInfo struct { + slotDB *state.StateDB // used for SlotDb reuse only, otherwise, it can be discarded + StateObjectSuicided map[common.Address]struct{} + StateChangeSet map[common.Address]state.StateKeys + BalanceChangeSet map[common.Address]struct{} + CodeChangeSet map[common.Address]struct{} + AddrStateChangeSet map[common.Address]struct{} + txIndex int +} + +type SlotState struct { + tailTxReq *ParallelTxRequest // tail pending Tx of the slot, should be accessed on dispatcher only. + pendingExec chan *ParallelTxRequest + // slot needs to keep the historical stateDB for conflict check + // each finalized DB should match a TX index + mergedTxInfo []MergedTxInfo + slotdbChan chan *state.StateDB // dispatch will create and send this slotDB to slot + // conflict check uses conflict window + // conflict check will check all state changes from (cfWindowStart + 1) to the previous Tx +} + +type ParallelTxResult struct { + redo bool // for redo, dispatch will wait new tx result + updateSlotDB bool // for redo and pendingExec, slot needs new slotDB, + reuseSlotDB bool // will try to reuse latest finalized slotDB + keepSystem bool // for redo, should keep system address's balance + txIndex int + slotIndex int // slot index + err error // to describe error message? + tx *types.Transaction + txReq *ParallelTxRequest + receipt *types.Receipt + slotDB *state.StateDB +} + +type ParallelTxRequest struct { + txIndex int + tx *types.Transaction + slotDB *state.StateDB + gp *GasPool + msg types.Message + block *types.Block + vmConfig vm.Config + bloomProcessors *AsyncReceiptBloomGenerator + usedGas *uint64 + waitTxChan chan int // "int" represents the tx index + curTxChan chan int // "int" represents the tx index +} + +// if any state in readDb is updated in writeDb, then it has state conflict +func (p *StateProcessor) hasStateConflict(readDb *state.StateDB, mergedInfo MergedTxInfo) bool { + // check KV change + reads := readDb.StateReadsInSlot() + writes := mergedInfo.StateChangeSet + if len(reads) != 0 && len(writes) != 0 { + for readAddr, readKeys := range reads { + if _, exist := mergedInfo.StateObjectSuicided[readAddr]; exist { + log.Debug("hasStateConflict read suicide object", "addr", readAddr) + return true + } + if writeKeys, ok := writes[readAddr]; ok { + // readAddr exist + for writeKey := range writeKeys { + // same addr and same key, mark conflicted + if _, ok := readKeys[writeKey]; ok { + log.Info("hasStateConflict state conflict", "addr", readAddr, "key", writeKey) + return true + } + } + } + } + } + // check balance change + balanceReads := readDb.BalanceReadsInSlot() + balanceWrite := mergedInfo.BalanceChangeSet + if len(balanceReads) != 0 && len(balanceWrite) != 0 { + for readAddr := range balanceReads { + if _, exist := mergedInfo.StateObjectSuicided[readAddr]; exist { + log.Debug("hasStateConflict read suicide balance", "addr", readAddr) + return true + } + if _, ok := balanceWrite[readAddr]; ok { + if readAddr == consensus.SystemAddress { + log.Info("hasStateConflict skip specical system address's balance check") + continue + } + log.Info("hasStateConflict balance conflict", "addr", readAddr) + return true + } + } + } + + // check code change + codeReads := readDb.CodeReadInSlot() + codeWrite := mergedInfo.CodeChangeSet + if len(codeReads) != 0 && len(codeWrite) != 0 { + for readAddr := range codeReads { + if _, exist := mergedInfo.StateObjectSuicided[readAddr]; exist { + log.Debug("hasStateConflict read suicide code", "addr", readAddr) + return true + } + if _, ok := codeWrite[readAddr]; ok { + log.Debug("hasStateConflict code conflict", "addr", readAddr) + return true + } + } + } + + // check address state change: create, suicide... + addrReads := readDb.AddressReadInSlot() + addrWrite := mergedInfo.AddrStateChangeSet + if len(addrReads) != 0 && len(addrWrite) != 0 { + for readAddr := range addrReads { + if _, ok := addrWrite[readAddr]; ok { + log.Info("hasStateConflict address state conflict", "addr", readAddr) + return true + } + } + } + + return false +} + +// for parallel execute, we put contracts of same address in a slot, +// since these txs probably would have conflicts +func (p *StateProcessor) queueToSameAddress(execMsg *ParallelTxRequest) bool { + txToAddr := execMsg.tx.To() + if txToAddr == nil { + return false + } + for i, slot := range p.slotState { + if slot.tailTxReq == nil { // this slot is idle + // log.Debug("queueToSameAddress skip idle slot.") + continue + } + + // To() == nil means contract creation, won't queue to such slot. + if slot.tailTxReq.tx.To() == nil { + // log.Debug("queueToSameAddress, slot's To address is nil", "slotIndex", i) + continue + } + // same to address, put it on slot's pending list. + if *txToAddr == *slot.tailTxReq.tx.To() { + select { + case slot.pendingExec <- execMsg: + slot.tailTxReq = execMsg + log.Debug("queueToSameAddress", "slotIndex", i, "txIndex", execMsg.txIndex) + return true + default: + log.Debug("queueToSameAddress but queue is full", "slotIndex", i, "txIndex", execMsg.txIndex) + return false + } + } + } + return false +} + +// if there is idle slot, dispatch the msg to the first idle slot +func (p *StateProcessor) dispatchToIdleSlot(statedb *state.StateDB, txReq *ParallelTxRequest) bool { + for i, slot := range p.slotState { + if slot.tailTxReq == nil { + // for idle slot, we have to create a SlotDB for it. + if len(slot.mergedTxInfo) == 0 { + txReq.slotDB = state.NewSlotDB(statedb, consensus.SystemAddress, p.mergedTxIndex, false) + } + log.Debug("dispatchToIdleSlot", "slotIndex", i, "txIndex", txReq.txIndex) + slot.tailTxReq = txReq + slot.pendingExec <- txReq + return true + } + } + return false +} + +// wait until the next Tx is executed and its result is merged to the main stateDB +func (p *StateProcessor) waitUntilNextTxDone(statedb *state.StateDB) *ParallelTxResult { + var result *ParallelTxResult + for { + result = <-p.paraTxResultChan + // slot may request new slotDB, if it think its slotDB is outdated + // such as: + // tx in pendingExec, previous tx in same queue is likely "damaged" the slotDB + // tx redo for confict + // tx stage 1 failed, nonce out of order... + if result.updateSlotDB { + // the target slot is waiting for new slotDB + slotState := p.slotState[result.slotIndex] + var slotDB *state.StateDB + if result.reuseSlotDB { + // for reuse, len(slotState.mergedTxInfo) must >= 1 + lastSlotDB := slotState.mergedTxInfo[len(slotState.mergedTxInfo)-1].slotDB + slotDB = state.ReUseSlotDB(lastSlotDB, result.keepSystem) + } else { + slotDB = state.NewSlotDB(statedb, consensus.SystemAddress, p.mergedTxIndex, result.keepSystem) + } + slotState.slotdbChan <- slotDB + continue + } + if result.redo { + // wait result of redo + continue + } + // ok, the tx result is valid and can be merged + break + } + resultSlotIndex := result.slotIndex + resultTxIndex := result.txIndex + resultSlotState := p.slotState[resultSlotIndex] + if resultSlotState.tailTxReq.txIndex == resultTxIndex { + log.Debug("ProcessParallel slot is idle", "slotIndex", resultSlotIndex) + resultSlotState.tailTxReq = nil + } + + // merge slotDB to parent stateDB + log.Info("ProcessParallel a tx is done, merge to block stateDB", + "resultSlotIndex", resultSlotIndex, "resultTxIndex", resultTxIndex) + objSuicided, stateChanges, balanceChanges, codeChanges, addrChanges := statedb.MergeSlotDB(result.slotDB, result.receipt) + // slot's mergedTxInfo is updated by dispatcher, while consumed by slot. + // It is safe, since write and read is in sequential, do write -> notify -> read + // it is not good, but work right now. + + resultSlotState.mergedTxInfo = append(resultSlotState.mergedTxInfo, MergedTxInfo{result.slotDB, objSuicided, stateChanges, balanceChanges, codeChanges, addrChanges, resultTxIndex}) + + if resultTxIndex != p.mergedTxIndex+1 { + log.Warn("ProcessParallel tx result out of order", "resultTxIndex", resultTxIndex, + "p.mergedTxIndex", p.mergedTxIndex) + panic("ProcessParallel tx result out of order") + } + p.mergedTxIndex = resultTxIndex + // notify the following Tx, it is merged, what if no wait or next tx is in same slot? + result.txReq.curTxChan <- resultTxIndex + return result +} + +func (p *StateProcessor) execInParallelSlot(slotIndex int, txReq *ParallelTxRequest) *ParallelTxResult { + txIndex := txReq.txIndex + tx := txReq.tx + slotDB := txReq.slotDB + slotDB.SlotIndex = slotIndex + gp := txReq.gp // goroutine unsafe + msg := txReq.msg + block := txReq.block + header := block.Header() + cfg := txReq.vmConfig + bloomProcessors := txReq.bloomProcessors + + blockContext := NewEVMBlockContext(header, p.bc, nil) // fixme: share blockContext within a block? + vmenv := vm.NewEVM(blockContext, vm.TxContext{}, slotDB, p.config, cfg) + + var receipt *types.Receipt + var result *ExecutionResult + var err error + var evm *vm.EVM + + // fixme: to optimize, reuse the slotDB + slotDB.Prepare(tx.Hash(), block.Hash(), txIndex) + log.Debug("execInParallelSlot enter", "slotIndex", slotIndex, "txIndex", txIndex, "slotDB.baseTxIndex", slotDB.BaseTxIndex()) + + slotGasLimit := gp.Gas() + gpSlot := new(GasPool).AddGas(slotGasLimit) // each slot would use its own gas pool, and will do gaslimit check later + evm, result, err = applyTransactionStageExecution(msg, gpSlot, slotDB, vmenv) + log.Debug("execInParallelSlot Stage Execution done", "slotIndex", slotIndex, "txIndex", txIndex, "slotDB.baseTxIndex", slotDB.BaseTxIndex()) + + // wait until the previous tx is finalized. + if txReq.waitTxChan != nil { + log.Info("execInParallelSlot wait previous Tx done", "my slotIndex", slotIndex, "txIndex", txIndex) + waitTxIndex := <-txReq.waitTxChan + if waitTxIndex != txIndex-1 { + log.Error("execInParallelSlot wait tx index mismatch", "expect", txIndex-1, "actual", waitTxIndex) + panic(fmt.Sprintf("wait tx index mismatch expect:%d, actual:%d", txIndex-1, waitTxIndex)) + } + } + + // in parallel, tx can run into trouble + // for example: err="nonce too high" + // in this case, we will do re-run. + if err != nil { + log.Debug("Stage Execution err", "slotIndex", slotIndex, "txIndex", txIndex, + "current slotDB.baseTxIndex", slotDB.BaseTxIndex(), "err", err) + redoResult := &ParallelTxResult{ + redo: true, + updateSlotDB: true, + reuseSlotDB: false, + txIndex: txIndex, + slotIndex: slotIndex, + tx: tx, + txReq: txReq, + receipt: receipt, + err: err, + } + p.paraTxResultChan <- redoResult + slotDB = <-p.slotState[slotIndex].slotdbChan + slotDB.SlotIndex = slotIndex + slotDB.Prepare(tx.Hash(), block.Hash(), txIndex) + // vmenv.Reset(vm.TxContext{}, slotDB) + log.Debug("Stage Execution get new slotdb to redo", "slotIndex", slotIndex, + "txIndex", txIndex, "new slotDB.baseTxIndex", slotDB.BaseTxIndex()) + slotGasLimit = gp.Gas() + gpSlot = new(GasPool).AddGas(slotGasLimit) + evm, result, err = applyTransactionStageExecution(msg, gpSlot, slotDB, vmenv) + if err != nil { + panic(fmt.Sprintf("Stage Execution redo, error %v", err)) + } + } + + // fixme: + // parallel mode can not precheck, + // precheck should be replace by postCheck when previous Tx is finalized + + // do conflict detect + hasConflict := false + systemAddrConflict := false + + log.Debug("execInParallelSlot Tx Stage1 done, do conflict check", "slotIndex", slotIndex, "txIndex", txIndex) + if slotDB.SystemAddressRedo() { + hasConflict = true + systemAddrConflict = true + } else { + for index := 0; index < ParallelExecNum; index++ { + // can skip current slot now, since slotDB is always after current slot's merged DB + // ** idle: all previous Txs are merged, it will create a new SlotDB + // ** queued: it will request updateSlotDB, dispatcher will create or reuse a SlotDB after previous Tx results are merged + if index == slotIndex { + continue + } + + // check all finalizedDb from current slot's + for _, mergedInfo := range p.slotState[index].mergedTxInfo { + if mergedInfo.txIndex <= slotDB.BaseTxIndex() { + // log.Info("skip finalized DB which is out of the conflict window", "finDb.txIndex", finDb.txIndex, "slotDB.baseTxIndex", slotDB.baseTxIndex) + continue + } + if p.hasStateConflict(slotDB, mergedInfo) { + log.Debug("execInParallelSlot Stage Execution conflict", "slotIndex", slotIndex, + "txIndex", txIndex, " conflict slot", index, "slotDB.baseTxIndex", slotDB.BaseTxIndex()) + hasConflict = true + break + } + } + if hasConflict { + break + } + } + } + + if hasConflict { + // re-run should not have conflict, since it has the latest world state. + redoResult := &ParallelTxResult{ + redo: true, + updateSlotDB: true, + reuseSlotDB: false, // for conflict, we do not reuse + keepSystem: systemAddrConflict, + txIndex: txIndex, + slotIndex: slotIndex, + tx: tx, + txReq: txReq, + receipt: receipt, + err: err, + } + p.paraTxResultChan <- redoResult + slotDB = <-p.slotState[slotIndex].slotdbChan + slotDB.SlotIndex = slotIndex + slotDB.Prepare(tx.Hash(), block.Hash(), txIndex) + // vmenv.Reset(vm.TxContext{}, slotDB) + slotGasLimit = gp.Gas() + gpSlot = new(GasPool).AddGas(slotGasLimit) + evm, result, err = applyTransactionStageExecution(msg, gpSlot, slotDB, vmenv) + if err != nil { + panic(fmt.Sprintf("Stage Execution conflict redo, error %v", err)) + } + } + + // goroutine unsafe operation will be handled from here for safety + gasConsumed := slotGasLimit - gpSlot.Gas() + if gasConsumed != result.UsedGas { + log.Error("execInParallelSlot gasConsumed != result.UsedGas mismatch", + "gasConsumed", gasConsumed, "result.UsedGas", result.UsedGas) + panic(fmt.Sprintf("gas consume mismatch, consumed:%d, result.UsedGas:%d", gasConsumed, result.UsedGas)) + } + + if err := gp.SubGas(gasConsumed); err != nil { + log.Error("gas limit reached", "gasConsumed", gasConsumed, "gp", gp.Gas()) + panic(fmt.Sprintf("gas limit reached, gasConsumed:%d, gp.Gas():%d", gasConsumed, gp.Gas())) + } + + log.Debug("execInParallelSlot ok to finalize this TX", + "slotIndex", slotIndex, "txIndex", txIndex, "result.UsedGas", result.UsedGas, "txReq.usedGas", *txReq.usedGas) + // ok, time to do finalize, stage2 should not be parallel + receipt, err = applyTransactionStageFinalization(evm, result, msg, p.config, slotDB, header, tx, txReq.usedGas, bloomProcessors) + + if result.Err != nil { + // if Tx is reverted, all its state change will be discarded + log.Debug("execInParallelSlot TX reverted?", "slotIndex", slotIndex, "txIndex", txIndex, "result.Err", result.Err) + } + return &ParallelTxResult{ + redo: false, + updateSlotDB: false, + txIndex: txIndex, + slotIndex: slotIndex, + tx: tx, + txReq: txReq, + receipt: receipt, + slotDB: slotDB, + err: err, + } +} + +func (p *StateProcessor) runSlotLoop(slotIndex int) { + curSlot := p.slotState[slotIndex] + for { + // log.Info("parallel slot waiting", "slotIndex:", slotIndex) + // wait for new TxReq + txReq := <-curSlot.pendingExec + // receive a dispatched message + log.Debug("SlotLoop received a new TxReq", "slotIndex:", slotIndex, "txIndex", txReq.txIndex) + + // SlotDB create rational: + // ** for a dispatched tx, + // the slot should be idle, it is better to create a new SlotDB, since new Tx is not related to previous Tx + // ** for a queued tx, + // the previous SlotDB could be reused, since it is likely can be used + // reuse could avoid NewSlotDB cost, which could be costable when StateDB is full of state object + // if the previous SlotDB is + if txReq.slotDB == nil { + // for queued Tx, txReq.slotDB is nil, reuse slot's latest merged SlotDB + result := &ParallelTxResult{ + redo: false, + updateSlotDB: true, + reuseSlotDB: reuseSlotDB, + slotIndex: slotIndex, + err: nil, + } + p.paraTxResultChan <- result + txReq.slotDB = <-curSlot.slotdbChan + } + result := p.execInParallelSlot(slotIndex, txReq) + log.Debug("SlotLoop the TxReq is done", "slotIndex:", slotIndex, "err", result.err) + p.paraTxResultChan <- result + } +} + +// clear slotState for each block. +func (p *StateProcessor) resetSlotState() { + p.mergedTxIndex = -1 + for _, slotState := range p.slotState { + slotState.tailTxReq = nil + slotState.mergedTxInfo = make([]MergedTxInfo, 0) + } +} + +func (p *StateProcessor) InitParallelOnce() { + // to create and start the execution slot goroutines + if p.paraInitialized { + return + } + + p.paraTxResultChan = make(chan *ParallelTxResult, ParallelExecNum) // fixme: use blocked chan? + p.slotState = make([]*SlotState, ParallelExecNum) + + wg := sync.WaitGroup{} // make sure all goroutines are created and started + for i := 0; i < ParallelExecNum; i++ { + p.slotState[i] = new(SlotState) + p.slotState[i].slotdbChan = make(chan *state.StateDB, 1) + p.slotState[i].pendingExec = make(chan *ParallelTxRequest, MaxPendingQueueSize) + + wg.Add(1) + // start the slot's goroutine + go func(slotIndex int) { + wg.Done() + p.runSlotLoop(slotIndex) // this loop will be permanent live + log.Error("runSlotLoop exit!", "slotIndex", slotIndex) + }(i) + } + wg.Wait() + p.paraInitialized = true +} + func (p *StateProcessor) ProcessParallel(block *types.Block, statedb *state.StateDB, cfg vm.Config) (*state.StateDB, types.Receipts, []*types.Log, uint64, error) { + p.InitParallelOnce() var ( usedGas = new(uint64) header = block.Header() @@ -455,7 +944,6 @@ func (p *StateProcessor) ProcessParallel(block *types.Block, statedb *state.Stat gp = new(GasPool).AddGas(block.GasLimit()) ) signer := types.MakeSigner(p.bc.chainConfig, block.Number()) - statedb.TryPreload(block, signer) var receipts = make([]*types.Receipt, 0) // Mutate the block and state according to any hard-fork specs if p.config.DAOForkSupport && p.config.DAOForkBlock != nil && p.config.DAOForkBlock.Cmp(block.Number()) == 0 { @@ -464,20 +952,22 @@ func (p *StateProcessor) ProcessParallel(block *types.Block, statedb *state.Stat // Handle upgrade build-in system contract code systemcontracts.UpgradeBuildInSystemContract(p.config, block.Number(), statedb) - blockContext := NewEVMBlockContext(header, p.bc, nil) - vmenv := vm.NewEVM(blockContext, vm.TxContext{}, statedb, p.config, cfg) - txNum := len(block.Transactions()) + if txNum > 0 { + log.Info("ProcessParallel", "block num", block.Number(), "txNum", txNum) + p.resetSlotState() + } // Iterate over and process the individual transactions posa, isPoSA := p.engine.(consensus.PoSA) commonTxs := make([]*types.Transaction, 0, txNum) - // initilise bloom processors + // initialise bloom processors bloomProcessors := NewAsyncReceiptBloomGenerator(txNum) statedb.MarkFullProcessed() // usually do have two tx, one for validator set contract, another for system reward contract. systemTxs := make([]*types.Transaction, 0, 2) + var waitTxChan, curTxChan chan int for i, tx := range block.Transactions() { if isPoSA { if isSystemTx, err := posa.IsSystemTransaction(tx, block.Header()); err != nil { @@ -488,21 +978,75 @@ func (p *StateProcessor) ProcessParallel(block *types.Block, statedb *state.Stat } } - msg, err := tx.AsMessage(signer) + msg, err := tx.AsMessage(signer) // fixme: move it into slot. if err != nil { return statedb, nil, nil, 0, err } - statedb.Prepare(tx.Hash(), block.Hash(), i) - receipt, err := applyTransaction(msg, p.config, p.bc, nil, gp, statedb, header, tx, usedGas, vmenv, bloomProcessors) - if err != nil { - return statedb, nil, nil, 0, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err) + + // parallel start, wrap an exec message, which will be dispatched to a slot + waitTxChan = curTxChan // can be nil, if this is the tx of first batch, otherwise, it is previous Tx's wait channel + curTxChan = make(chan int, 1) + + txReq := &ParallelTxRequest{ + txIndex: i, + tx: tx, + slotDB: nil, + gp: gp, + msg: msg, + block: block, + vmConfig: cfg, + bloomProcessors: bloomProcessors, + usedGas: usedGas, + waitTxChan: waitTxChan, + curTxChan: curTxChan, } - commonTxs = append(commonTxs, tx) - receipts = append(receipts, receipt) + // fixme: to optimize the for { for {} } loop code style + for { + // if p.queueToSameAddress(txReq) { + // log.Info("ProcessParallel queue to same slot", "txIndex", txReq.txIndex) + // continue + // } + + // if idle slot available, just dispatch and process next tx. + if p.dispatchToIdleSlot(statedb, txReq) { + // log.Info("ProcessParallel dispatch to idle slot", "txIndex", txReq.txIndex) + break + } + log.Debug("ProcessParallel no slot avaiable, wait", "txIndex", txReq.txIndex) + // no idle slot, wait until a tx is executed and merged. + result := p.waitUntilNextTxDone(statedb) + + // update tx result + if result.err != nil { + log.Warn("ProcessParallel a failed tx", "resultSlotIndex", result.slotIndex, + "resultTxIndex", result.txIndex, "result.err", result.err) + return statedb, nil, nil, 0, fmt.Errorf("could not apply tx %d [%v]: %w", result.txIndex, result.tx.Hash().Hex(), result.err) + } + commonTxs = append(commonTxs, result.tx) + receipts = append(receipts, result.receipt) + } } - bloomProcessors.Close() + // wait until all tx request are done + for len(commonTxs)+len(systemTxs) < txNum { + result := p.waitUntilNextTxDone(statedb) + // update tx result + if result.err != nil { + log.Warn("ProcessParallel a failed tx", "resultSlotIndex", result.slotIndex, + "resultTxIndex", result.txIndex, "result.err", result.err) + return statedb, nil, nil, 0, fmt.Errorf("could not apply tx %d [%v]: %w", result.txIndex, result.tx.Hash().Hex(), result.err) + } + commonTxs = append(commonTxs, result.tx) + receipts = append(receipts, result.receipt) + } + + bloomProcessors.Close() + if txNum > 0 { + log.Info("ProcessParallel tx all done", "block", header.Number, "usedGas", *usedGas, + "len(commonTxs)", len(commonTxs), "len(receipts)", len(receipts), + "len(systemTxs)", len(systemTxs)) + } // Finalize the block, applying any consensus engine specific extras (e.g. block rewards) err := p.engine.Finalize(p.bc, header, statedb, &commonTxs, block.Uncles(), &receipts, &systemTxs, usedGas) if err != nil { @@ -562,6 +1106,57 @@ func applyTransaction(msg types.Message, config *params.ChainConfig, bc ChainCon return receipt, err } +func applyTransactionStageExecution(msg types.Message, gp *GasPool, statedb *state.StateDB, evm *vm.EVM) (*vm.EVM, *ExecutionResult, error) { + // Create a new context to be used in the EVM environment. + txContext := NewEVMTxContext(msg) + evm.Reset(txContext, statedb) + + // Apply the transaction to the current state (included in the env). + result, err := ApplyMessage(evm, msg, gp) + if err != nil { + return nil, nil, err + } + + return evm, result, err +} + +func applyTransactionStageFinalization(evm *vm.EVM, result *ExecutionResult, msg types.Message, config *params.ChainConfig, statedb *state.StateDB, header *types.Header, tx *types.Transaction, usedGas *uint64, receiptProcessors ...ReceiptProcessor) (*types.Receipt, error) { + // Update the state with pending changes. + var root []byte + if config.IsByzantium(header.Number) { + statedb.Finalise(true) + } else { + root = statedb.IntermediateRoot(config.IsEIP158(header.Number)).Bytes() + } + *usedGas += result.UsedGas + + // Create a new receipt for the transaction, storing the intermediate root and gas used + // by the tx. + receipt := &types.Receipt{Type: tx.Type(), PostState: root, CumulativeGasUsed: *usedGas} + if result.Failed() { + receipt.Status = types.ReceiptStatusFailed + } else { + receipt.Status = types.ReceiptStatusSuccessful + } + receipt.TxHash = tx.Hash() + receipt.GasUsed = result.UsedGas + + // If the transaction created a contract, store the creation address in the receipt. + if msg.To() == nil { + receipt.ContractAddress = crypto.CreateAddress(evm.TxContext.Origin, tx.Nonce()) + } + + // Set the receipt logs and create the bloom filter. + receipt.Logs = statedb.GetLogs(tx.Hash()) + receipt.BlockHash = statedb.BlockHash() + receipt.BlockNumber = header.Number + receipt.TransactionIndex = uint(statedb.TxIndex()) + for _, receiptProcessor := range receiptProcessors { + receiptProcessor.Apply(receipt) + } + return receipt, nil +} + // ApplyTransaction attempts to apply a transaction to the given state database // and uses the input parameters for its environment. It returns the receipt // for the transaction, gas used and an error if the transaction failed, diff --git a/go.mod b/go.mod index c58a45dfd2..41b8a28076 100644 --- a/go.mod +++ b/go.mod @@ -58,6 +58,7 @@ require ( github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c github.com/panjf2000/ants/v2 v2.4.5 github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7 + github.com/prometheus/client_golang v1.0.0 github.com/prometheus/tsdb v0.7.1 github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563 // indirect github.com/rjeczalik/notify v0.9.1 diff --git a/metrics/exp/exp.go b/metrics/exp/exp.go index 3ebe8cc68a..563a55bf65 100644 --- a/metrics/exp/exp.go +++ b/metrics/exp/exp.go @@ -8,6 +8,8 @@ import ( "net/http" "sync" + "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/metrics/prometheus" @@ -44,6 +46,7 @@ func Exp(r metrics.Registry) { // http.HandleFunc("/debug/vars", e.expHandler) // haven't found an elegant way, so just use a different endpoint http.Handle("/debug/metrics", h) + http.Handle("/debug/metrics/go_prometheus", promhttp.Handler()) http.Handle("/debug/metrics/prometheus", prometheus.Handler(r)) } @@ -58,6 +61,7 @@ func ExpHandler(r metrics.Registry) http.Handler { func Setup(address string) { m := http.NewServeMux() m.Handle("/debug/metrics", ExpHandler(metrics.DefaultRegistry)) + m.Handle("/debug/metrics/go_prometheus", promhttp.Handler()) m.Handle("/debug/metrics/prometheus", prometheus.Handler(metrics.DefaultRegistry)) log.Info("Starting metrics server", "addr", fmt.Sprintf("http://%s/debug/metrics", address)) go func() { From c15483f18c8d07b1f406bcd1cd2f1516873a352d Mon Sep 17 00:00:00 2001 From: setunapo Date: Tue, 15 Feb 2022 11:03:35 +0800 Subject: [PATCH 03/16] Parallel: more readable code & dispatch policy & Revert & UT ** move .Process() close to .ProcessParallel() ** InitParallelOnce & preExec & postExec for code maintenance ** MergedTxInfo -> SlotChangeList & debug conflict ratio ** use ParallelState to keep all parallel statedb states. ** enable queue to same slot ** discard state change of reverted transaction And debug log refine ** add ut for statedb --- core/blockchain.go | 3 + core/state/journal.go | 4 +- core/state/statedb.go | 406 ++++++++++++++++++++---------------- core/state/statedb_test.go | 375 +++++++++++++++++++++++++++++++++ core/state_processor.go | 417 ++++++++++++++++++------------------- core/types.go | 3 +- 6 files changed, 810 insertions(+), 398 deletions(-) diff --git a/core/blockchain.go b/core/blockchain.go index 3c542be4e6..f706b6dfe1 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -311,6 +311,9 @@ func NewBlockChain(db ethdb.Database, cacheConfig *CacheConfig, chainConfig *par bc.prefetcher = NewStatePrefetcher(chainConfig, bc, engine) bc.validator = NewBlockValidator(chainConfig, bc, engine) bc.processor = NewStateProcessor(chainConfig, bc, engine) + if ParallelTxMode { + bc.processor.InitParallelOnce() + } var err error bc.hc, err = NewHeaderChain(db, chainConfig, engine, bc.insertStopped) diff --git a/core/state/journal.go b/core/state/journal.go index d727819375..20ae6a269a 100644 --- a/core/state/journal.go +++ b/core/state/journal.go @@ -142,8 +142,8 @@ type ( ) func (ch createObjectChange) revert(s *StateDB) { - if s.isSlotDB { - delete(s.dirtiedStateObjectsInSlot, *ch.account) + if s.parallel.isSlotDB { + delete(s.parallel.dirtiedStateObjectsInSlot, *ch.account) } else { s.stateObjects.Delete(*ch.account) } diff --git a/core/state/statedb.go b/core/state/statedb.go index 72527d39e8..739294bdb2 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -86,6 +86,43 @@ func (s *StateObjectSyncMap) StoreStateObject(addr common.Address, stateObject * s.Store(addr, stateObject) } +// For parallel mode only, keep the change list for later conflict detect +type SlotChangeList struct { + SlotDB *StateDB // used for SlotDb reuse only, otherwise, it can be discarded + TxIndex int // the tx index of change list + StateObjectSuicided map[common.Address]struct{} + StateChangeSet map[common.Address]StateKeys + BalanceChangeSet map[common.Address]struct{} + CodeChangeSet map[common.Address]struct{} + AddrStateChangeSet map[common.Address]struct{} +} + +// For parallel mode only +type ParallelState struct { + isSlotDB bool + baseTxIndex int // slotDB is created base on this tx index. + dirtiedStateObjectsInSlot map[common.Address]*StateObject + // for conflict check + balanceChangedInSlot map[common.Address]struct{} // the address's balance has been changed + balanceReadsInSlot map[common.Address]struct{} // the address's balance has been read and used. + codeReadInSlot map[common.Address]struct{} + codeChangeInSlot map[common.Address]struct{} + stateReadsInSlot map[common.Address]StateKeys + stateChangedInSlot map[common.Address]StateKeys // no need record value + // Actions such as SetCode, Suicide will change address's state. + // Later call like Exist(), Empty(), HasSuicided() depond on the address's state. + addrStateReadInSlot map[common.Address]struct{} + addrStateChangeInSlot map[common.Address]struct{} + stateObjectSuicided map[common.Address]struct{} + // Transaction will pay gas fee to system address. + // Parallel execution will clear system address's balance at first, in order to maintain transaction's + // gas fee value. Normal transaction will access system address twice, otherwise it means the transaction + // needs real system address's balance, the transaction will be marked redo with keepSystemAddressBalance = true + systemAddress common.Address + systemAddressCount int + keepSystemAddressBalance bool +} + // StateDB structs within the ethereum protocol are used to store anything // within the merkle trie. StateDBs take care of caching and storing // nested states. It's the general query interface to retrieve: @@ -120,32 +157,9 @@ type StateDB struct { stateObjectsPending map[common.Address]struct{} // State objects finalized but not yet written to the trie stateObjectsDirty map[common.Address]struct{} // State objects modified in the current execution - storagePool *StoragePool // sharedPool to store L1 originStorage of stateObjects - writeOnSharedStorage bool // Write to the shared origin storage of a stateObject while reading from the underlying storage layer. - // parallel start - isSlotDB bool - baseTxIndex int // slotDB is created base on this tx index. - SlotIndex int // debug purpose, will be removed - dirtiedStateObjectsInSlot map[common.Address]*StateObject - // for conflict check - balanceChangedInSlot map[common.Address]struct{} // the address's balance has been changed - balanceReadsInSlot map[common.Address]struct{} // the address's balance has been read and used. - codeReadInSlot map[common.Address]struct{} - codeChangeInSlot map[common.Address]struct{} - stateReadsInSlot map[common.Address]StateKeys - stateChangedInSlot map[common.Address]StateKeys // no need record value - // Actions such as SetCode, Suicide will change address's state. - // Later call like Exist(), Empty(), HasSuicided() depond on the address's state. - addrStateReadInSlot map[common.Address]struct{} - addrStateChangeInSlot map[common.Address]struct{} - stateObjectSuicided map[common.Address]struct{} - // Transaction will pay gas fee to system address. - // Parallel execution will clear system address's balance at first, in order to maintain transaction's - // gas fee value. Normal transaction will access system address twice, otherwise it means the transaction - // needs real system address's balance, the transaction will be marked redo with keepSystemAddressBalance = true - systemAddress common.Address - systemAddressCount int - keepSystemAddressBalance bool + storagePool *StoragePool // sharedPool to store L1 originStorage of stateObjects + writeOnSharedStorage bool // Write to the shared origin storage of a stateObject while reading from the underlying storage layer. + parallel ParallelState // to keep all the parallel execution elements // DB error. // State objects are used by the consensus core and VM which are @@ -208,10 +222,10 @@ func NewWithSharedPool(root common.Hash, db Database, snaps *snapshot.Tree) (*St func NewSlotDB(db *StateDB, systemAddr common.Address, txIndex int, keepSystem bool) *StateDB { slotDB := db.CopyForSlot() slotDB.originalRoot = db.originalRoot - slotDB.baseTxIndex = txIndex - slotDB.systemAddress = systemAddr - slotDB.systemAddressCount = 0 - slotDB.keepSystemAddressBalance = keepSystem + slotDB.parallel.baseTxIndex = txIndex + slotDB.parallel.systemAddress = systemAddr + slotDB.parallel.systemAddressCount = 0 + slotDB.parallel.keepSystemAddressBalance = keepSystem // clear the slotDB's validator's balance first // for slotDB, systemAddr's value is the tx's gas fee @@ -225,21 +239,21 @@ func NewSlotDB(db *StateDB, systemAddr common.Address, txIndex int, keepSystem b // to avoid new slotDB for each Tx, slotDB should be valid and merged func ReUseSlotDB(slotDB *StateDB, keepSystem bool) *StateDB { if !keepSystem { - slotDB.SetBalance(slotDB.systemAddress, big.NewInt(0)) + slotDB.SetBalance(slotDB.parallel.systemAddress, big.NewInt(0)) } slotDB.logs = make(map[common.Hash][]*types.Log, defaultNumOfSlots) slotDB.logSize = 0 - slotDB.systemAddressCount = 0 - slotDB.keepSystemAddressBalance = keepSystem - slotDB.stateObjectSuicided = make(map[common.Address]struct{}, defaultNumOfSlots) - slotDB.codeReadInSlot = make(map[common.Address]struct{}, defaultNumOfSlots) - slotDB.codeChangeInSlot = make(map[common.Address]struct{}, defaultNumOfSlots) - slotDB.stateChangedInSlot = make(map[common.Address]StateKeys, defaultNumOfSlots) - slotDB.stateReadsInSlot = make(map[common.Address]StateKeys, defaultNumOfSlots) - slotDB.balanceChangedInSlot = make(map[common.Address]struct{}, defaultNumOfSlots) - slotDB.balanceReadsInSlot = make(map[common.Address]struct{}, defaultNumOfSlots) - slotDB.addrStateReadInSlot = make(map[common.Address]struct{}, defaultNumOfSlots) - slotDB.addrStateChangeInSlot = make(map[common.Address]struct{}, defaultNumOfSlots) + slotDB.parallel.systemAddressCount = 0 + slotDB.parallel.keepSystemAddressBalance = keepSystem + slotDB.parallel.stateObjectSuicided = make(map[common.Address]struct{}, defaultNumOfSlots) + slotDB.parallel.codeReadInSlot = make(map[common.Address]struct{}, defaultNumOfSlots) + slotDB.parallel.codeChangeInSlot = make(map[common.Address]struct{}, defaultNumOfSlots) + slotDB.parallel.stateChangedInSlot = make(map[common.Address]StateKeys, defaultNumOfSlots) + slotDB.parallel.stateReadsInSlot = make(map[common.Address]StateKeys, defaultNumOfSlots) + slotDB.parallel.balanceChangedInSlot = make(map[common.Address]struct{}, defaultNumOfSlots) + slotDB.parallel.balanceReadsInSlot = make(map[common.Address]struct{}, defaultNumOfSlots) + slotDB.parallel.addrStateReadInSlot = make(map[common.Address]struct{}, defaultNumOfSlots) + slotDB.parallel.addrStateChangeInSlot = make(map[common.Address]struct{}, defaultNumOfSlots) slotDB.stateObjectsDirty = make(map[common.Address]struct{}, defaultNumOfSlots) slotDB.stateObjectsPending = make(map[common.Address]struct{}, defaultNumOfSlots) @@ -248,11 +262,9 @@ func ReUseSlotDB(slotDB *StateDB, keepSystem bool) *StateDB { } func newStateDB(root common.Hash, db Database, snaps *snapshot.Tree) (*StateDB, error) { - sdb := &StateDB{ - db: db, - originalRoot: root, - snaps: snaps, - stateObjects: &StateObjectSyncMap{}, + + parallel := ParallelState{ + isSlotDB: false, stateObjectSuicided: make(map[common.Address]struct{}, defaultNumOfSlots), codeReadInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), codeChangeInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), @@ -262,12 +274,19 @@ func newStateDB(root common.Hash, db Database, snaps *snapshot.Tree) (*StateDB, balanceReadsInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), addrStateReadInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), addrStateChangeInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), - stateObjectsPending: make(map[common.Address]struct{}, defaultNumOfSlots), - stateObjectsDirty: make(map[common.Address]struct{}, defaultNumOfSlots), - logs: make(map[common.Hash][]*types.Log, defaultNumOfSlots), - preimages: make(map[common.Hash][]byte), - journal: newJournal(), - hasher: crypto.NewKeccakState(), + } + sdb := &StateDB{ + db: db, + originalRoot: root, + snaps: snaps, + stateObjects: &StateObjectSyncMap{}, + parallel: parallel, + stateObjectsPending: make(map[common.Address]struct{}, defaultNumOfSlots), + stateObjectsDirty: make(map[common.Address]struct{}, defaultNumOfSlots), + logs: make(map[common.Hash][]*types.Log, defaultNumOfSlots), + preimages: make(map[common.Hash][]byte), + journal: newJournal(), + hasher: crypto.NewKeccakState(), } if sdb.snaps != nil { if sdb.snap = sdb.snaps.Snapshot(root); sdb.snap != nil { @@ -292,8 +311,8 @@ func (s *StateDB) EnableWriteOnSharedStorage() { } func (s *StateDB) getStateObjectFromStateObjects(addr common.Address) (*StateObject, bool) { - if s.isSlotDB { - obj, ok := s.dirtiedStateObjectsInSlot[addr] + if s.parallel.isSlotDB { + obj, ok := s.parallel.dirtiedStateObjectsInSlot[addr] if ok { return obj, ok } @@ -301,11 +320,21 @@ func (s *StateDB) getStateObjectFromStateObjects(addr common.Address) (*StateObj return s.stateObjects.LoadStateObject(addr) } +// If the transaction execution is failed, keep its read list for conflict detect +// and discard its state changed, execept its own balance change. +func (s *StateDB) RevertSlotDB(from common.Address) { + s.parallel.stateObjectSuicided = make(map[common.Address]struct{}) + s.parallel.stateChangedInSlot = make(map[common.Address]StateKeys) + s.parallel.balanceChangedInSlot = make(map[common.Address]struct{}, 1) + s.parallel.balanceChangedInSlot[from] = struct{}{} + s.parallel.addrStateChangeInSlot = make(map[common.Address]struct{}) +} + // MergeSlotDB is for Parallel TX, when the TX is finalized(dirty -> pending) // A bit similar to StateDB.Copy(), // mainly copy stateObjects, since slotDB has been finalized. -// return: objSuicided, stateChanges, balanceChanges, codeChanges -func (s *StateDB) MergeSlotDB(slotDb *StateDB, slotReceipt *types.Receipt) (map[common.Address]struct{}, map[common.Address]StateKeys, map[common.Address]struct{}, map[common.Address]struct{}, map[common.Address]struct{}) { +// return and keep the slot's change list for later conflict detect. +func (s *StateDB) MergeSlotDB(slotDb *StateDB, slotReceipt *types.Receipt, txIndex int) SlotChangeList { // receipt.Logs with unified log Index within a block // align slotDB's logs Index to the block stateDB's logSize for _, l := range slotReceipt.Logs { @@ -315,8 +344,8 @@ func (s *StateDB) MergeSlotDB(slotDb *StateDB, slotReceipt *types.Receipt) (map[ // before merge, do validator reward first: AddBalance to consensus.SystemAddress // object of SystemAddress is take care specially - systemAddress := slotDb.systemAddress - if slotDb.keepSystemAddressBalance { + systemAddress := slotDb.parallel.systemAddress + if slotDb.parallel.keepSystemAddressBalance { s.SetBalance(systemAddress, slotDb.GetBalance(systemAddress)) } else { s.AddBalance(systemAddress, slotDb.GetBalance(systemAddress)) @@ -344,7 +373,7 @@ func (s *StateDB) MergeSlotDB(slotDb *StateDB, slotReceipt *types.Receipt) (map[ } } - for addr, obj := range slotDb.dirtiedStateObjectsInSlot { + for addr, obj := range slotDb.parallel.dirtiedStateObjectsInSlot { if addr == systemAddress { continue } @@ -380,28 +409,34 @@ func (s *StateDB) MergeSlotDB(slotDb *StateDB, slotReceipt *types.Receipt) (map[ } } - objectSuicided := make(map[common.Address]struct{}, len(slotDb.stateObjectSuicided)) - for addr := range slotDb.stateObjectSuicided { - objectSuicided[addr] = struct{}{} + // we have to create a new object to store change list for conflict detect, since + // StateDB could be reused and its elements could be overwritten + changeList := SlotChangeList{ + SlotDB: slotDb, + TxIndex: txIndex, + StateObjectSuicided: make(map[common.Address]struct{}, len(slotDb.parallel.stateObjectSuicided)), + StateChangeSet: make(map[common.Address]StateKeys, len(slotDb.parallel.stateChangedInSlot)), + BalanceChangeSet: make(map[common.Address]struct{}, len(slotDb.parallel.balanceChangedInSlot)), + CodeChangeSet: make(map[common.Address]struct{}, len(slotDb.parallel.codeChangeInSlot)), + AddrStateChangeSet: make(map[common.Address]struct{}, len(slotDb.parallel.addrStateChangeInSlot)), + } + for addr := range slotDb.parallel.stateObjectSuicided { + changeList.StateObjectSuicided[addr] = struct{}{} } - stateChanges := make(map[common.Address]StateKeys, len(slotDb.stateChangedInSlot)) // must be a deep copy, since - for addr, storage := range slotDb.stateChangedInSlot { - stateChanges[addr] = storage + for addr, storage := range slotDb.parallel.stateChangedInSlot { + changeList.StateChangeSet[addr] = storage } - balanceChanges := make(map[common.Address]struct{}, len(slotDb.balanceChangedInSlot)) // must be a deep copy, since - for addr := range slotDb.balanceChangedInSlot { - balanceChanges[addr] = struct{}{} + for addr := range slotDb.parallel.balanceChangedInSlot { + changeList.BalanceChangeSet[addr] = struct{}{} } - codeChanges := make(map[common.Address]struct{}, len(slotDb.codeChangeInSlot)) - for addr := range slotDb.codeChangeInSlot { - codeChanges[addr] = struct{}{} + for addr := range slotDb.parallel.codeChangeInSlot { + changeList.CodeChangeSet[addr] = struct{}{} } - addrStateChanges := make(map[common.Address]struct{}, len(slotDb.addrStateChangeInSlot)) - for addr := range slotDb.addrStateChangeInSlot { - addrStateChanges[addr] = struct{}{} + for addr := range slotDb.parallel.addrStateChangeInSlot { + changeList.AddrStateChangeSet[addr] = struct{}{} } - return objectSuicided, stateChanges, balanceChanges, codeChanges, addrStateChanges + return changeList } // StartPrefetcher initializes a new trie prefetcher to pull in nodes from the @@ -554,8 +589,8 @@ func (s *StateDB) SubRefund(gas uint64) { // Exist reports whether the given account address exists in the state. // Notably this also returns true for suicided accounts. func (s *StateDB) Exist(addr common.Address) bool { - if s.isSlotDB { - s.addrStateReadInSlot[addr] = struct{}{} + if s.parallel.isSlotDB { + s.parallel.addrStateReadInSlot[addr] = struct{}{} } return s.getStateObject(addr) != nil } @@ -564,18 +599,18 @@ func (s *StateDB) Exist(addr common.Address) bool { // or empty according to the EIP161 specification (balance = nonce = code = 0) func (s *StateDB) Empty(addr common.Address) bool { so := s.getStateObject(addr) - if s.isSlotDB { - s.addrStateReadInSlot[addr] = struct{}{} + if s.parallel.isSlotDB { + s.parallel.addrStateReadInSlot[addr] = struct{}{} } return so == nil || so.empty() } // GetBalance retrieves the balance from the given address or 0 if object not found func (s *StateDB) GetBalance(addr common.Address) *big.Int { - if s.isSlotDB { - s.balanceReadsInSlot[addr] = struct{}{} - if addr == s.systemAddress { - s.systemAddressCount++ + if s.parallel.isSlotDB { + s.parallel.balanceReadsInSlot[addr] = struct{}{} + if addr == s.parallel.systemAddress { + s.parallel.systemAddressCount++ } } stateObject := s.getStateObject(addr) @@ -606,31 +641,31 @@ func (s *StateDB) BlockHash() common.Hash { // BaseTxIndex returns the tx index that slot db based. func (s *StateDB) BaseTxIndex() int { - return s.baseTxIndex + return s.parallel.baseTxIndex } func (s *StateDB) CodeReadInSlot() map[common.Address]struct{} { - return s.codeReadInSlot + return s.parallel.codeReadInSlot } func (s *StateDB) AddressReadInSlot() map[common.Address]struct{} { - return s.addrStateReadInSlot + return s.parallel.addrStateReadInSlot } func (s *StateDB) StateReadsInSlot() map[common.Address]StateKeys { - return s.stateReadsInSlot + return s.parallel.stateReadsInSlot } func (s *StateDB) BalanceReadsInSlot() map[common.Address]struct{} { - return s.balanceReadsInSlot + return s.parallel.balanceReadsInSlot } func (s *StateDB) SystemAddressRedo() bool { - return s.systemAddressCount > 2 + return s.parallel.systemAddressCount > 2 } func (s *StateDB) GetCode(addr common.Address) []byte { - if s.isSlotDB { - s.codeReadInSlot[addr] = struct{}{} + if s.parallel.isSlotDB { + s.parallel.codeReadInSlot[addr] = struct{}{} } stateObject := s.getStateObject(addr) @@ -641,8 +676,8 @@ func (s *StateDB) GetCode(addr common.Address) []byte { } func (s *StateDB) GetCodeSize(addr common.Address) int { - if s.isSlotDB { - s.codeReadInSlot[addr] = struct{}{} // code size is part of code + if s.parallel.isSlotDB { + s.parallel.codeReadInSlot[addr] = struct{}{} // code size is part of code } stateObject := s.getStateObject(addr) @@ -653,8 +688,8 @@ func (s *StateDB) GetCodeSize(addr common.Address) int { } func (s *StateDB) GetCodeHash(addr common.Address) common.Hash { - if s.isSlotDB { - s.codeReadInSlot[addr] = struct{}{} // code hash is part of code + if s.parallel.isSlotDB { + s.parallel.codeReadInSlot[addr] = struct{}{} // code hash is part of code } stateObject := s.getStateObject(addr) @@ -666,11 +701,11 @@ func (s *StateDB) GetCodeHash(addr common.Address) common.Hash { // GetState retrieves a value from the given account's storage trie. func (s *StateDB) GetState(addr common.Address, hash common.Hash) common.Hash { - if s.isSlotDB { - if s.stateReadsInSlot[addr] == nil { - s.stateReadsInSlot[addr] = make(map[common.Hash]struct{}, defaultNumOfSlots) + if s.parallel.isSlotDB { + if s.parallel.stateReadsInSlot[addr] == nil { + s.parallel.stateReadsInSlot[addr] = make(map[common.Hash]struct{}, defaultNumOfSlots) } - s.stateReadsInSlot[addr][hash] = struct{}{} + s.parallel.stateReadsInSlot[addr][hash] = struct{}{} } stateObject := s.getStateObject(addr) @@ -746,8 +781,8 @@ func (s *StateDB) StorageTrie(addr common.Address) Trie { func (s *StateDB) HasSuicided(addr common.Address) bool { stateObject := s.getStateObject(addr) - if s.isSlotDB { - s.addrStateReadInSlot[addr] = struct{}{} // address suicided. + if s.parallel.isSlotDB { + s.parallel.addrStateReadInSlot[addr] = struct{}{} // address suicided. } if stateObject != nil { return stateObject.suicided @@ -761,22 +796,22 @@ func (s *StateDB) HasSuicided(addr common.Address) bool { // AddBalance adds amount to the account associated with addr. func (s *StateDB) AddBalance(addr common.Address, amount *big.Int) { - if s.isSlotDB { + if s.parallel.isSlotDB { // just in case other tx creates this account, we will miss this if we only add this account when found - s.balanceChangedInSlot[addr] = struct{}{} - s.balanceReadsInSlot[addr] = struct{}{} // add balance will perform a read operation first - if addr == s.systemAddress { - s.systemAddressCount++ + s.parallel.balanceChangedInSlot[addr] = struct{}{} + s.parallel.balanceReadsInSlot[addr] = struct{}{} // add balance will perform a read operation first + if addr == s.parallel.systemAddress { + s.parallel.systemAddressCount++ } } stateObject := s.GetOrNewStateObject(addr) if stateObject != nil { - if s.isSlotDB { - if _, ok := s.dirtiedStateObjectsInSlot[addr]; !ok { + if s.parallel.isSlotDB { + if _, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; !ok { newStateObject := stateObject.deepCopy(s) newStateObject.AddBalance(amount) - s.dirtiedStateObjectsInSlot[addr] = newStateObject + s.parallel.dirtiedStateObjectsInSlot[addr] = newStateObject } else { stateObject.AddBalance(amount) } @@ -788,22 +823,22 @@ func (s *StateDB) AddBalance(addr common.Address, amount *big.Int) { // SubBalance subtracts amount from the account associated with addr. func (s *StateDB) SubBalance(addr common.Address, amount *big.Int) { - if s.isSlotDB { + if s.parallel.isSlotDB { // just in case other tx creates this account, we will miss this if we only add this account when found - s.balanceChangedInSlot[addr] = struct{}{} - s.balanceReadsInSlot[addr] = struct{}{} - if addr == s.systemAddress { - s.systemAddressCount++ + s.parallel.balanceChangedInSlot[addr] = struct{}{} + s.parallel.balanceReadsInSlot[addr] = struct{}{} + if addr == s.parallel.systemAddress { + s.parallel.systemAddressCount++ } } stateObject := s.GetOrNewStateObject(addr) if stateObject != nil { - if s.isSlotDB { - if _, ok := s.dirtiedStateObjectsInSlot[addr]; !ok { + if s.parallel.isSlotDB { + if _, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; !ok { newStateObject := stateObject.deepCopy(s) newStateObject.SubBalance(amount) - s.dirtiedStateObjectsInSlot[addr] = newStateObject + s.parallel.dirtiedStateObjectsInSlot[addr] = newStateObject } else { stateObject.SubBalance(amount) } @@ -816,17 +851,17 @@ func (s *StateDB) SubBalance(addr common.Address, amount *big.Int) { func (s *StateDB) SetBalance(addr common.Address, amount *big.Int) { stateObject := s.GetOrNewStateObject(addr) if stateObject != nil { - if s.isSlotDB { - if _, ok := s.dirtiedStateObjectsInSlot[addr]; !ok { + if s.parallel.isSlotDB { + if _, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; !ok { newStateObject := stateObject.deepCopy(s) newStateObject.SetBalance(amount) - s.dirtiedStateObjectsInSlot[addr] = newStateObject + s.parallel.dirtiedStateObjectsInSlot[addr] = newStateObject } else { stateObject.SetBalance(amount) } - s.balanceChangedInSlot[addr] = struct{}{} - if addr == s.systemAddress { - s.systemAddressCount++ + s.parallel.balanceChangedInSlot[addr] = struct{}{} + if addr == s.parallel.systemAddress { + s.parallel.systemAddressCount++ } } else { stateObject.SetBalance(amount) @@ -837,11 +872,11 @@ func (s *StateDB) SetBalance(addr common.Address, amount *big.Int) { func (s *StateDB) SetNonce(addr common.Address, nonce uint64) { stateObject := s.GetOrNewStateObject(addr) if stateObject != nil { - if s.isSlotDB { - if _, ok := s.dirtiedStateObjectsInSlot[addr]; !ok { + if s.parallel.isSlotDB { + if _, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; !ok { newStateObject := stateObject.deepCopy(s) newStateObject.SetNonce(nonce) - s.dirtiedStateObjectsInSlot[addr] = newStateObject + s.parallel.dirtiedStateObjectsInSlot[addr] = newStateObject } else { stateObject.SetNonce(nonce) } @@ -854,16 +889,16 @@ func (s *StateDB) SetNonce(addr common.Address, nonce uint64) { func (s *StateDB) SetCode(addr common.Address, code []byte) { stateObject := s.GetOrNewStateObject(addr) if stateObject != nil { - if s.isSlotDB { - if _, ok := s.dirtiedStateObjectsInSlot[addr]; !ok { + if s.parallel.isSlotDB { + if _, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; !ok { newStateObject := stateObject.deepCopy(s) newStateObject.SetCode(crypto.Keccak256Hash(code), code) - s.dirtiedStateObjectsInSlot[addr] = newStateObject + s.parallel.dirtiedStateObjectsInSlot[addr] = newStateObject } else { stateObject.SetCode(crypto.Keccak256Hash(code), code) } - s.codeChangeInSlot[addr] = struct{}{} + s.parallel.codeChangeInSlot[addr] = struct{}{} } else { stateObject.SetCode(crypto.Keccak256Hash(code), code) } @@ -873,19 +908,19 @@ func (s *StateDB) SetCode(addr common.Address, code []byte) { func (s *StateDB) SetState(addr common.Address, key, value common.Hash) { stateObject := s.GetOrNewStateObject(addr) if stateObject != nil { - if s.isSlotDB { - if _, ok := s.dirtiedStateObjectsInSlot[addr]; !ok { + if s.parallel.isSlotDB { + if _, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; !ok { newStateObject := stateObject.deepCopy(s) newStateObject.SetState(s.db, key, value) - s.dirtiedStateObjectsInSlot[addr] = newStateObject + s.parallel.dirtiedStateObjectsInSlot[addr] = newStateObject } else { stateObject.SetState(s.db, key, value) } - if s.stateChangedInSlot[addr] == nil { - s.stateChangedInSlot[addr] = make(StateKeys, defaultNumOfSlots) + if s.parallel.stateChangedInSlot[addr] == nil { + s.parallel.stateChangedInSlot[addr] = make(StateKeys, defaultNumOfSlots) } - s.stateChangedInSlot[addr][key] = struct{}{} + s.parallel.stateChangedInSlot[addr][key] = struct{}{} } else { stateObject.SetState(s.db, key, value) } @@ -910,12 +945,11 @@ func (s *StateDB) Suicide(addr common.Address) bool { stateObject := s.getStateObject(addr) // fixme: should add read stateobject record if stateObject == nil { - log.Warn("StateDB Suicide stateObject not found", "slot", s.SlotIndex, "addr", addr) return false } - if s.isSlotDB { - s.stateObjectSuicided[addr] = struct{}{} - s.addrStateChangeInSlot[addr] = struct{}{} // address suicided. + if s.parallel.isSlotDB { + s.parallel.stateObjectSuicided[addr] = struct{}{} + s.parallel.addrStateChangeInSlot[addr] = struct{}{} // address suicided. } s.journal.append(suicideChange{ @@ -1048,8 +1082,8 @@ func (s *StateDB) getDeletedStateObject(addr common.Address) *StateObject { } func (s *StateDB) SetStateObject(object *StateObject) { - if s.isSlotDB { - s.dirtiedStateObjectsInSlot[object.Address()] = object + if s.parallel.isSlotDB { + s.parallel.dirtiedStateObjectsInSlot[object.Address()] = object } else { s.stateObjects.StoreStateObject(object.Address(), object) } @@ -1060,8 +1094,8 @@ func (s *StateDB) GetOrNewStateObject(addr common.Address) *StateObject { stateObject := s.getStateObject(addr) if stateObject == nil { stateObject, _ = s.createObject(addr) - if s.isSlotDB { - s.addrStateChangeInSlot[addr] = struct{}{} // address created. + if s.parallel.isSlotDB { + s.parallel.addrStateChangeInSlot[addr] = struct{}{} // address created. } } return stateObject @@ -1107,8 +1141,8 @@ func (s *StateDB) CreateAccount(addr common.Address) { newObj, prev := s.createObject(addr) if prev != nil { newObj.setBalance(prev.data.Balance) - } else if s.isSlotDB { - s.addrStateChangeInSlot[addr] = struct{}{} // new account created + } else if s.parallel.isSlotDB { + s.parallel.addrStateChangeInSlot[addr] = struct{}{} // new account created } } @@ -1146,6 +1180,20 @@ func (s *StateDB) ForEachStorage(addr common.Address, cb func(key, value common. // Snapshots of the copied state cannot be applied to the copy. func (s *StateDB) Copy() *StateDB { // Copy all the basic fields, initialize the memory ones + parallel := ParallelState{ + isSlotDB: false, + stateObjectSuicided: make(map[common.Address]struct{}, defaultNumOfSlots), + codeReadInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), + codeChangeInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), + stateChangedInSlot: make(map[common.Address]StateKeys, defaultNumOfSlots), + stateReadsInSlot: make(map[common.Address]StateKeys, defaultNumOfSlots), + balanceChangedInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), + balanceReadsInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), + addrStateReadInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), + addrStateChangeInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), + dirtiedStateObjectsInSlot: make(map[common.Address]*StateObject, defaultNumOfSlots), + } + state := &StateDB{ db: s.db, trie: s.db.CopyTrie(s.trie), @@ -1159,18 +1207,7 @@ func (s *StateDB) Copy() *StateDB { preimages: make(map[common.Hash][]byte, len(s.preimages)), journal: newJournal(), hasher: crypto.NewKeccakState(), - - isSlotDB: false, - stateObjectSuicided: make(map[common.Address]struct{}, defaultNumOfSlots), - codeReadInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), - codeChangeInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), - stateChangedInSlot: make(map[common.Address]StateKeys, defaultNumOfSlots), - stateReadsInSlot: make(map[common.Address]StateKeys, defaultNumOfSlots), - balanceChangedInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), - balanceReadsInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), - addrStateReadInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), - addrStateChangeInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), - dirtiedStateObjectsInSlot: make(map[common.Address]*StateObject, defaultNumOfSlots), + parallel: parallel, } // Copy the dirty states, logs, and preimages for addr := range s.journal.dirties { @@ -1261,34 +1298,37 @@ func (s *StateDB) Copy() *StateDB { func (s *StateDB) CopyForSlot() *StateDB { // Copy all the basic fields, initialize the memory ones - state := &StateDB{ - db: s.db, - trie: s.db.CopyTrie(s.trie), - stateObjects: s.stateObjects, - stateObjectsPending: make(map[common.Address]struct{}, defaultNumOfSlots), - stateObjectsDirty: make(map[common.Address]struct{}, defaultNumOfSlots), - refund: s.refund, - logs: make(map[common.Hash][]*types.Log, defaultNumOfSlots), - logSize: 0, - preimages: make(map[common.Hash][]byte, len(s.preimages)), - journal: newJournal(), - hasher: crypto.NewKeccakState(), - snapDestructs: make(map[common.Address]struct{}), - snapAccounts: make(map[common.Address][]byte), - snapStorage: make(map[common.Address]map[string][]byte), - stateObjectSuicided: make(map[common.Address]struct{}, defaultNumOfSlots), - codeReadInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), - codeChangeInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), - stateChangedInSlot: make(map[common.Address]StateKeys, defaultNumOfSlots), - stateReadsInSlot: make(map[common.Address]StateKeys, defaultNumOfSlots), - balanceChangedInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), - balanceReadsInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), - addrStateReadInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), - addrStateChangeInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), + parallel := ParallelState{ + stateObjectSuicided: make(map[common.Address]struct{}, defaultNumOfSlots), + codeReadInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), + codeChangeInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), + stateChangedInSlot: make(map[common.Address]StateKeys, defaultNumOfSlots), + stateReadsInSlot: make(map[common.Address]StateKeys, defaultNumOfSlots), + balanceChangedInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), + balanceReadsInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), + addrStateReadInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), + addrStateChangeInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), isSlotDB: true, dirtiedStateObjectsInSlot: make(map[common.Address]*StateObject, defaultNumOfSlots), } + state := &StateDB{ + db: s.db, + trie: s.db.CopyTrie(s.trie), + stateObjects: s.stateObjects, + stateObjectsPending: make(map[common.Address]struct{}, defaultNumOfSlots), + stateObjectsDirty: make(map[common.Address]struct{}, defaultNumOfSlots), + refund: s.refund, + logs: make(map[common.Hash][]*types.Log, defaultNumOfSlots), + logSize: 0, + preimages: make(map[common.Hash][]byte, len(s.preimages)), + journal: newJournal(), + hasher: crypto.NewKeccakState(), + snapDestructs: make(map[common.Address]struct{}), + snapAccounts: make(map[common.Address][]byte), + snapStorage: make(map[common.Address]map[string][]byte), + parallel: parallel, + } for hash, preimage := range s.preimages { state.preimages[hash] = preimage diff --git a/core/state/statedb_test.go b/core/state/statedb_test.go index acbbf1cd2f..2fce92ae1d 100644 --- a/core/state/statedb_test.go +++ b/core/state/statedb_test.go @@ -19,6 +19,7 @@ package state import ( "bytes" "encoding/binary" + "encoding/hex" "fmt" "math" "math/big" @@ -34,6 +35,10 @@ import ( "github.com/ethereum/go-ethereum/core/types" ) +var ( + systemAddress = common.HexToAddress("0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE") +) + // Tests that updating a state trie does not leak any database writes prior to // actually committing the state. func TestUpdateLeaks(t *testing.T) { @@ -932,3 +937,373 @@ func TestStateDBAccessList(t *testing.T) { t.Fatalf("expected empty, got %d", got) } } + +func TestSuicide(t *testing.T) { + // Create an initial state with a few accounts + memDb := rawdb.NewMemoryDatabase() + db := NewDatabase(memDb) + state, _ := New(common.Hash{}, db, nil) + slotDb := NewSlotDB(state, systemAddress, 0, false) + + addr := common.BytesToAddress([]byte("so")) + slotDb.SetBalance(addr, big.NewInt(1)) + + result := slotDb.Suicide(addr) + if !result { + t.Fatalf("expected account suicide, got %v", result) + } + + if _, ok := slotDb.parallel.stateObjectSuicided[addr]; !ok { + t.Fatalf("address should exist in stateObjectSuicided") + } + + if _, ok := slotDb.parallel.addrStateChangeInSlot[addr]; !ok { + t.Fatalf("address should exist in addrStateChangeInSlot") + } + + if _, ok := slotDb.parallel.dirtiedStateObjectsInSlot[addr]; !ok { + t.Fatalf("address should exist in dirtiedStateObjectsInSlot") + } + + hasSuicide := slotDb.HasSuicided(addr) + if !hasSuicide { + t.Fatalf("address should be suicided") + } + + if _, ok := slotDb.parallel.addrStateReadInSlot[addr]; !ok { + t.Fatalf("address should exist in addrStateReadInSlot") + } +} + +func TestSetAndGetState(t *testing.T) { + memDb := rawdb.NewMemoryDatabase() + db := NewDatabase(memDb) + state, _ := New(common.Hash{}, db, nil) + slotDb := NewSlotDB(state, systemAddress, 0, false) + + addr := common.BytesToAddress([]byte("so")) + state.SetBalance(addr, big.NewInt(1)) + + slotDb.SetState(addr, common.BytesToHash([]byte("test key")), common.BytesToHash([]byte("test store"))) + + if _, ok := slotDb.parallel.dirtiedStateObjectsInSlot[addr]; !ok { + t.Fatalf("address should exist in dirtiedStateObjectsInSlot") + } + + if _, ok := slotDb.parallel.stateChangedInSlot[addr]; !ok { + t.Fatalf("address should exist in stateChangedInSlot") + } + + oldValueRead := state.GetState(addr, common.BytesToHash([]byte("test key"))) + emptyHash := common.Hash{} + if oldValueRead != emptyHash { + t.Fatalf("value read in old state should be empty") + } + + valueRead := slotDb.GetState(addr, common.BytesToHash([]byte("test key"))) + if valueRead != common.BytesToHash([]byte("test store")) { + t.Fatalf("value read should be equal to the stored value") + } + + if _, ok := slotDb.parallel.stateReadsInSlot[addr]; !ok { + t.Fatalf("address should exist in stateReadsInSlot") + } +} + +func TestSetAndGetCode(t *testing.T) { + memDb := rawdb.NewMemoryDatabase() + db := NewDatabase(memDb) + state, _ := New(common.Hash{}, db, nil) + slotDb := NewSlotDB(state, systemAddress, 0, false) + + addr := common.BytesToAddress([]byte("so")) + state.SetBalance(addr, big.NewInt(1)) + + if _, ok := slotDb.parallel.dirtiedStateObjectsInSlot[addr]; ok { + t.Fatalf("address should not exist in dirtiedStateObjectsInSlot") + } + + slotDb.SetCode(addr, []byte("test code")) + + if _, ok := slotDb.parallel.dirtiedStateObjectsInSlot[addr]; !ok { + t.Fatalf("address should exist in dirtiedStateObjectsInSlot") + } + + if _, ok := slotDb.parallel.codeChangeInSlot[addr]; !ok { + t.Fatalf("address should exist in codeChangeInSlot") + } + + codeRead := slotDb.GetCode(addr) + if string(codeRead) != "test code" { + t.Fatalf("code read should be equal to the code stored") + } + + if _, ok := slotDb.parallel.codeReadInSlot[addr]; !ok { + t.Fatalf("address should exist in codeReadInSlot") + } +} + +func TestGetCodeSize(t *testing.T) { + memDb := rawdb.NewMemoryDatabase() + db := NewDatabase(memDb) + state, _ := New(common.Hash{}, db, nil) + slotDb := NewSlotDB(state, systemAddress, 0, false) + + addr := common.BytesToAddress([]byte("so")) + state.SetBalance(addr, big.NewInt(1)) + + slotDb.SetCode(addr, []byte("test code")) + + codeSize := slotDb.GetCodeSize(addr) + if codeSize != 9 { + t.Fatalf("code size should be 9") + } + + if _, ok := slotDb.parallel.codeReadInSlot[addr]; !ok { + t.Fatalf("address should exist in codeReadInSlot") + } +} + +func TestGetCodeHash(t *testing.T) { + memDb := rawdb.NewMemoryDatabase() + db := NewDatabase(memDb) + state, _ := New(common.Hash{}, db, nil) + slotDb := NewSlotDB(state, systemAddress, 0, false) + + addr := common.BytesToAddress([]byte("so")) + state.SetBalance(addr, big.NewInt(1)) + + slotDb.SetCode(addr, []byte("test code")) + + codeSize := slotDb.GetCodeHash(addr) + print(hex.EncodeToString(codeSize[:])) + if hex.EncodeToString(codeSize[:]) != "6e73fa02f7828b28608b078b007a4023fb40453c3e102b83828a3609a94d8cbb" { + t.Fatalf("code hash should be 6e73fa02f7828b28608b078b007a4023fb40453c3e102b83828a3609a94d8cbb") + } + if _, ok := slotDb.parallel.codeReadInSlot[addr]; !ok { + t.Fatalf("address should exist in codeReadInSlot") + } +} + +func TestSetNonce(t *testing.T) { + memDb := rawdb.NewMemoryDatabase() + db := NewDatabase(memDb) + state, _ := New(common.Hash{}, db, nil) + slotDb := NewSlotDB(state, systemAddress, 0, false) + + addr := common.BytesToAddress([]byte("so")) + state.SetBalance(addr, big.NewInt(1)) + state.SetNonce(addr, 1) + + slotDb.SetNonce(addr, 2) + + oldNonce := state.GetNonce(addr) + if oldNonce != 1 { + t.Fatalf("old nonce should be 1") + } + + newNonce := slotDb.GetNonce(addr) + if newNonce != 2 { + t.Fatalf("new nonce should be 2") + } + if _, ok := slotDb.parallel.dirtiedStateObjectsInSlot[addr]; !ok { + t.Fatalf("address should exist in dirtiedStateObjectsInSlot") + } +} + +func TestSetAndGetBalance(t *testing.T) { + memDb := rawdb.NewMemoryDatabase() + db := NewDatabase(memDb) + state, _ := New(common.Hash{}, db, nil) + slotDb := NewSlotDB(state, systemAddress, 0, true) + + addr := systemAddress + state.SetBalance(addr, big.NewInt(1)) + + slotDb.SetBalance(addr, big.NewInt(2)) + + oldBalance := state.GetBalance(addr) + if oldBalance.Int64() != 1 { + t.Fatalf("old balance should be 1") + } + + if _, ok := slotDb.parallel.dirtiedStateObjectsInSlot[addr]; !ok { + t.Fatalf("address should exist in dirtiedStateObjectsInSlot") + } + + if _, ok := slotDb.parallel.balanceChangedInSlot[addr]; !ok { + t.Fatalf("address should exist in balanceChangedInSlot") + } + + if slotDb.parallel.systemAddressCount != 1 { + t.Fatalf("systemAddressCount should be 1") + } + + newBalance := slotDb.GetBalance(addr) + if newBalance.Int64() != 2 { + t.Fatalf("new nonce should be 2") + } + + if _, ok := slotDb.parallel.balanceReadsInSlot[addr]; !ok { + t.Fatalf("address should exist in balanceReadsInSlot") + } + + if slotDb.parallel.systemAddressCount != 2 { + t.Fatalf("systemAddressCount should be 1") + } +} + +func TestSubBalance(t *testing.T) { + memDb := rawdb.NewMemoryDatabase() + db := NewDatabase(memDb) + state, _ := New(common.Hash{}, db, nil) + slotDb := NewSlotDB(state, systemAddress, 0, true) + + addr := systemAddress + state.SetBalance(addr, big.NewInt(2)) + + slotDb.SubBalance(addr, big.NewInt(1)) + + oldBalance := state.GetBalance(addr) + if oldBalance.Int64() != 2 { + t.Fatalf("old balance should be 1") + } + + if _, ok := slotDb.parallel.dirtiedStateObjectsInSlot[addr]; !ok { + t.Fatalf("address should exist in dirtiedStateObjectsInSlot") + } + + if _, ok := slotDb.parallel.balanceChangedInSlot[addr]; !ok { + t.Fatalf("address should exist in balanceChangedInSlot") + } + + if _, ok := slotDb.parallel.balanceReadsInSlot[addr]; !ok { + t.Fatalf("address should exist in balanceReadsInSlot") + } + + if slotDb.parallel.systemAddressCount != 1 { + t.Fatalf("systemAddressCount should be 1") + } + + newBalance := slotDb.GetBalance(addr) + if newBalance.Int64() != 1 { + t.Fatalf("new nonce should be 2") + } +} + +func TestAddBalance(t *testing.T) { + memDb := rawdb.NewMemoryDatabase() + db := NewDatabase(memDb) + state, _ := New(common.Hash{}, db, nil) + slotDb := NewSlotDB(state, systemAddress, 0, true) + + addr := systemAddress + state.SetBalance(addr, big.NewInt(2)) + + slotDb.AddBalance(addr, big.NewInt(1)) + + oldBalance := state.GetBalance(addr) + if oldBalance.Int64() != 2 { + t.Fatalf("old balance should be 1") + } + + if _, ok := slotDb.parallel.dirtiedStateObjectsInSlot[addr]; !ok { + t.Fatalf("address should exist in dirtiedStateObjectsInSlot") + } + + if _, ok := slotDb.parallel.balanceChangedInSlot[addr]; !ok { + t.Fatalf("address should exist in balanceChangedInSlot") + } + + if _, ok := slotDb.parallel.balanceReadsInSlot[addr]; !ok { + t.Fatalf("address should exist in balanceReadsInSlot") + } + + if slotDb.parallel.systemAddressCount != 1 { + t.Fatalf("systemAddressCount should be 1") + } + + newBalance := slotDb.GetBalance(addr) + if newBalance.Int64() != 3 { + t.Fatalf("new nonce should be 2") + } +} + +func TestEmpty(t *testing.T) { + memDb := rawdb.NewMemoryDatabase() + db := NewDatabase(memDb) + state, _ := New(common.Hash{}, db, nil) + slotDb := NewSlotDB(state, systemAddress, 0, true) + + addr := systemAddress + state.SetBalance(addr, big.NewInt(2)) + + empty := slotDb.Empty(addr) + if empty { + t.Fatalf("address should exist") + } + + if _, ok := slotDb.parallel.addrStateReadInSlot[addr]; !ok { + t.Fatalf("address should exist in addrStateReadInSlot") + } +} + +func TestExist(t *testing.T) { + memDb := rawdb.NewMemoryDatabase() + db := NewDatabase(memDb) + state, _ := New(common.Hash{}, db, nil) + slotDb := NewSlotDB(state, systemAddress, 0, true) + + addr := systemAddress + state.SetBalance(addr, big.NewInt(2)) + + exist := slotDb.Exist(addr) + if !exist { + t.Fatalf("address should exist") + } + + if _, ok := slotDb.parallel.addrStateReadInSlot[addr]; !ok { + t.Fatalf("address should exist in addrStateReadInSlot") + } +} + +func TestMergeSlotDB(t *testing.T) { + memDb := rawdb.NewMemoryDatabase() + db := NewDatabase(memDb) + state, _ := New(common.Hash{}, db, nil) + oldSlotDb := NewSlotDB(state, systemAddress, 0, true) + + newSlotDb := NewSlotDB(state, systemAddress, 0, true) + + addr := systemAddress + newSlotDb.SetBalance(addr, big.NewInt(2)) + newSlotDb.SetState(addr, common.BytesToHash([]byte("test key")), common.BytesToHash([]byte("test store"))) + newSlotDb.SetCode(addr, []byte("test code")) + newSlotDb.Suicide(addr) + + changeList := oldSlotDb.MergeSlotDB(newSlotDb, &types.Receipt{}, 0) + + if _, ok := changeList.StateObjectSuicided[addr]; !ok { + t.Fatalf("address should exist in StateObjectSuicided") + } + + if _, ok := changeList.StateObjectSuicided[addr]; !ok { + t.Fatalf("address should exist in StateObjectSuicided") + } + + if _, ok := changeList.StateChangeSet[addr]; !ok { + t.Fatalf("address should exist in StateChangeSet") + } + + if _, ok := changeList.BalanceChangeSet[addr]; !ok { + t.Fatalf("address should exist in StateChangeSet") + } + + if _, ok := changeList.CodeChangeSet[addr]; !ok { + t.Fatalf("address should exist in CodeChangeSet") + } + + if _, ok := changeList.AddrStateChangeSet[addr]; !ok { + t.Fatalf("address should exist in AddrStateChangeSet") + } +} diff --git a/core/state_processor.go b/core/state_processor.go index f2c04e4e5f..9ae8d88762 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -24,6 +24,7 @@ import ( "math/rand" "runtime" "sync" + "sync/atomic" "time" "github.com/ethereum/go-ethereum/common" @@ -63,10 +64,12 @@ type StateProcessor struct { engine consensus.Engine // Consensus engine used for block rewards // add for parallel execute - paraInitialized bool // todo: should use atomic value - paraTxResultChan chan *ParallelTxResult // to notify dispatcher that a tx is done - slotState []*SlotState // idle, or pending messages - mergedTxIndex int // the latest finalized tx index + paraInitialized int32 + paraTxResultChan chan *ParallelTxResult // to notify dispatcher that a tx is done + slotState []*SlotState // idle, or pending messages + mergedTxIndex int // the latest finalized tx index + debugErrorRedoNum int + debugConflictRedoNum int } // NewStateProcessor initialises a new StateProcessor. @@ -381,100 +384,11 @@ func (p *LightStateProcessor) LightProcess(diffLayer *types.DiffLayer, block *ty return diffLayer.Receipts, allLogs, gasUsed, nil } -// Process processes the state changes according to the Ethereum rules by running -// the transaction messages using the statedb and applying any rewards to both -// the processor (coinbase) and any included uncles. -// -// Process returns the receipts and logs accumulated during the process and -// returns the amount of gas that was used in the process. If any of the -// transactions failed to execute due to insufficient gas it will return an error. -func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg vm.Config) (*state.StateDB, types.Receipts, []*types.Log, uint64, error) { - var ( - usedGas = new(uint64) - header = block.Header() - allLogs []*types.Log - gp = new(GasPool).AddGas(block.GasLimit()) - ) - signer := types.MakeSigner(p.bc.chainConfig, block.Number()) - var receipts = make([]*types.Receipt, 0) - // Mutate the block and state according to any hard-fork specs - if p.config.DAOForkSupport && p.config.DAOForkBlock != nil && p.config.DAOForkBlock.Cmp(block.Number()) == 0 { - misc.ApplyDAOHardFork(statedb) - } - // Handle upgrade build-in system contract code - systemcontracts.UpgradeBuildInSystemContract(p.config, block.Number(), statedb) - - blockContext := NewEVMBlockContext(header, p.bc, nil) - vmenv := vm.NewEVM(blockContext, vm.TxContext{}, statedb, p.config, cfg) - - txNum := len(block.Transactions()) - // Iterate over and process the individual transactions - posa, isPoSA := p.engine.(consensus.PoSA) - commonTxs := make([]*types.Transaction, 0, txNum) - - // initialise bloom processors - bloomProcessors := NewAsyncReceiptBloomGenerator(txNum) - statedb.MarkFullProcessed() - - // usually do have two tx, one for validator set contract, another for system reward contract. - systemTxs := make([]*types.Transaction, 0, 2) - for i, tx := range block.Transactions() { - if isPoSA { - if isSystemTx, err := posa.IsSystemTransaction(tx, block.Header()); err != nil { - bloomProcessors.Close() - return statedb, nil, nil, 0, err - } else if isSystemTx { - systemTxs = append(systemTxs, tx) - continue - } - } - - msg, err := tx.AsMessage(signer) - if err != nil { - bloomProcessors.Close() - return statedb, nil, nil, 0, err - } - statedb.Prepare(tx.Hash(), block.Hash(), i) - receipt, err := applyTransaction(msg, p.config, p.bc, nil, gp, statedb, header, tx, usedGas, vmenv, bloomProcessors) - if err != nil { - bloomProcessors.Close() - return statedb, nil, nil, 0, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err) - } - - commonTxs = append(commonTxs, tx) - receipts = append(receipts, receipt) - } - bloomProcessors.Close() - - // Finalize the block, applying any consensus engine specific extras (e.g. block rewards) - err := p.engine.Finalize(p.bc, header, statedb, &commonTxs, block.Uncles(), &receipts, &systemTxs, usedGas) - if err != nil { - return statedb, receipts, allLogs, *usedGas, err - } - for _, receipt := range receipts { - allLogs = append(allLogs, receipt.Logs...) - } - - return statedb, receipts, allLogs, *usedGas, nil -} - -type MergedTxInfo struct { - slotDB *state.StateDB // used for SlotDb reuse only, otherwise, it can be discarded - StateObjectSuicided map[common.Address]struct{} - StateChangeSet map[common.Address]state.StateKeys - BalanceChangeSet map[common.Address]struct{} - CodeChangeSet map[common.Address]struct{} - AddrStateChangeSet map[common.Address]struct{} - txIndex int -} - type SlotState struct { - tailTxReq *ParallelTxRequest // tail pending Tx of the slot, should be accessed on dispatcher only. - pendingExec chan *ParallelTxRequest - // slot needs to keep the historical stateDB for conflict check - // each finalized DB should match a TX index - mergedTxInfo []MergedTxInfo - slotdbChan chan *state.StateDB // dispatch will create and send this slotDB to slot + tailTxReq *ParallelTxRequest // tail pending Tx of the slot, should be accessed on dispatcher only. + pendingExec chan *ParallelTxRequest + mergedChangeList []state.SlotChangeList + slotdbChan chan *state.StateDB // dispatch will create and send this slotDB to slot // conflict check uses conflict window // conflict check will check all state changes from (cfWindowStart + 1) to the previous Tx } @@ -507,15 +421,41 @@ type ParallelTxRequest struct { curTxChan chan int // "int" represents the tx index } -// if any state in readDb is updated in writeDb, then it has state conflict -func (p *StateProcessor) hasStateConflict(readDb *state.StateDB, mergedInfo MergedTxInfo) bool { +func (p *StateProcessor) InitParallelOnce() { + // to create and start the execution slot goroutines + if !atomic.CompareAndSwapInt32(&p.paraInitialized, 0, 1) { // not swapped means already initialized. + return + } + log.Info("Parallel execution mode is used and initialized", "Parallel Num", ParallelExecNum) + p.paraTxResultChan = make(chan *ParallelTxResult, ParallelExecNum) // fixme: use blocked chan? + p.slotState = make([]*SlotState, ParallelExecNum) + + wg := sync.WaitGroup{} // make sure all goroutines are created and started + for i := 0; i < ParallelExecNum; i++ { + p.slotState[i] = new(SlotState) + p.slotState[i].slotdbChan = make(chan *state.StateDB, 1) + p.slotState[i].pendingExec = make(chan *ParallelTxRequest, MaxPendingQueueSize) + + wg.Add(1) + // start the slot's goroutine + go func(slotIndex int) { + wg.Done() + p.runSlotLoop(slotIndex) // this loop will be permanent live + log.Error("runSlotLoop exit!", "Slot", slotIndex) + }(i) + } + wg.Wait() +} + +// if any state in readDb is updated in changeList, then it has state conflict +func (p *StateProcessor) hasStateConflict(readDb *state.StateDB, changeList state.SlotChangeList) bool { // check KV change reads := readDb.StateReadsInSlot() - writes := mergedInfo.StateChangeSet + writes := changeList.StateChangeSet if len(reads) != 0 && len(writes) != 0 { for readAddr, readKeys := range reads { - if _, exist := mergedInfo.StateObjectSuicided[readAddr]; exist { - log.Debug("hasStateConflict read suicide object", "addr", readAddr) + if _, exist := changeList.StateObjectSuicided[readAddr]; exist { + log.Debug("conflict: read suicide object", "addr", readAddr) return true } if writeKeys, ok := writes[readAddr]; ok { @@ -523,7 +463,7 @@ func (p *StateProcessor) hasStateConflict(readDb *state.StateDB, mergedInfo Merg for writeKey := range writeKeys { // same addr and same key, mark conflicted if _, ok := readKeys[writeKey]; ok { - log.Info("hasStateConflict state conflict", "addr", readAddr, "key", writeKey) + log.Debug("conflict: state conflict", "addr", readAddr, "key", writeKey) return true } } @@ -532,19 +472,19 @@ func (p *StateProcessor) hasStateConflict(readDb *state.StateDB, mergedInfo Merg } // check balance change balanceReads := readDb.BalanceReadsInSlot() - balanceWrite := mergedInfo.BalanceChangeSet + balanceWrite := changeList.BalanceChangeSet if len(balanceReads) != 0 && len(balanceWrite) != 0 { for readAddr := range balanceReads { - if _, exist := mergedInfo.StateObjectSuicided[readAddr]; exist { - log.Debug("hasStateConflict read suicide balance", "addr", readAddr) + if _, exist := changeList.StateObjectSuicided[readAddr]; exist { + log.Debug("conflict: read suicide balance", "addr", readAddr) return true } if _, ok := balanceWrite[readAddr]; ok { if readAddr == consensus.SystemAddress { - log.Info("hasStateConflict skip specical system address's balance check") + log.Debug("conflict: skip specical system address's balance check") continue } - log.Info("hasStateConflict balance conflict", "addr", readAddr) + log.Debug("conflict: balance conflict", "addr", readAddr) return true } } @@ -552,15 +492,15 @@ func (p *StateProcessor) hasStateConflict(readDb *state.StateDB, mergedInfo Merg // check code change codeReads := readDb.CodeReadInSlot() - codeWrite := mergedInfo.CodeChangeSet + codeWrite := changeList.CodeChangeSet if len(codeReads) != 0 && len(codeWrite) != 0 { for readAddr := range codeReads { - if _, exist := mergedInfo.StateObjectSuicided[readAddr]; exist { - log.Debug("hasStateConflict read suicide code", "addr", readAddr) + if _, exist := changeList.StateObjectSuicided[readAddr]; exist { + log.Debug("conflict: read suicide code", "addr", readAddr) return true } if _, ok := codeWrite[readAddr]; ok { - log.Debug("hasStateConflict code conflict", "addr", readAddr) + log.Debug("conflict: code conflict", "addr", readAddr) return true } } @@ -568,11 +508,11 @@ func (p *StateProcessor) hasStateConflict(readDb *state.StateDB, mergedInfo Merg // check address state change: create, suicide... addrReads := readDb.AddressReadInSlot() - addrWrite := mergedInfo.AddrStateChangeSet + addrWrite := changeList.AddrStateChangeSet if len(addrReads) != 0 && len(addrWrite) != 0 { for readAddr := range addrReads { if _, ok := addrWrite[readAddr]; ok { - log.Info("hasStateConflict address state conflict", "addr", readAddr) + log.Debug("conflict: address state conflict", "addr", readAddr) return true } } @@ -596,7 +536,7 @@ func (p *StateProcessor) queueToSameAddress(execMsg *ParallelTxRequest) bool { // To() == nil means contract creation, won't queue to such slot. if slot.tailTxReq.tx.To() == nil { - // log.Debug("queueToSameAddress, slot's To address is nil", "slotIndex", i) + // log.Debug("queueToSameAddress, slot's To address is nil", "Slot", i) continue } // same to address, put it on slot's pending list. @@ -604,10 +544,10 @@ func (p *StateProcessor) queueToSameAddress(execMsg *ParallelTxRequest) bool { select { case slot.pendingExec <- execMsg: slot.tailTxReq = execMsg - log.Debug("queueToSameAddress", "slotIndex", i, "txIndex", execMsg.txIndex) + log.Debug("queueToSameAddress", "Slot", i, "txIndex", execMsg.txIndex) return true default: - log.Debug("queueToSameAddress but queue is full", "slotIndex", i, "txIndex", execMsg.txIndex) + log.Debug("queueToSameAddress but queue is full", "Slot", i, "txIndex", execMsg.txIndex) return false } } @@ -620,10 +560,10 @@ func (p *StateProcessor) dispatchToIdleSlot(statedb *state.StateDB, txReq *Paral for i, slot := range p.slotState { if slot.tailTxReq == nil { // for idle slot, we have to create a SlotDB for it. - if len(slot.mergedTxInfo) == 0 { + if len(slot.mergedChangeList) == 0 { txReq.slotDB = state.NewSlotDB(statedb, consensus.SystemAddress, p.mergedTxIndex, false) } - log.Debug("dispatchToIdleSlot", "slotIndex", i, "txIndex", txReq.txIndex) + log.Debug("dispatchToIdleSlot", "Slot", i, "txIndex", txReq.txIndex) slot.tailTxReq = txReq slot.pendingExec <- txReq return true @@ -647,8 +587,8 @@ func (p *StateProcessor) waitUntilNextTxDone(statedb *state.StateDB) *ParallelTx slotState := p.slotState[result.slotIndex] var slotDB *state.StateDB if result.reuseSlotDB { - // for reuse, len(slotState.mergedTxInfo) must >= 1 - lastSlotDB := slotState.mergedTxInfo[len(slotState.mergedTxInfo)-1].slotDB + // for reuse, len(slotState.mergedChangeList) must >= 1 + lastSlotDB := slotState.mergedChangeList[len(slotState.mergedChangeList)-1].SlotDB slotDB = state.ReUseSlotDB(lastSlotDB, result.keepSystem) } else { slotDB = state.NewSlotDB(statedb, consensus.SystemAddress, p.mergedTxIndex, result.keepSystem) @@ -667,19 +607,15 @@ func (p *StateProcessor) waitUntilNextTxDone(statedb *state.StateDB) *ParallelTx resultTxIndex := result.txIndex resultSlotState := p.slotState[resultSlotIndex] if resultSlotState.tailTxReq.txIndex == resultTxIndex { - log.Debug("ProcessParallel slot is idle", "slotIndex", resultSlotIndex) + log.Debug("ProcessParallel slot is idle", "Slot", resultSlotIndex) resultSlotState.tailTxReq = nil } - // merge slotDB to parent stateDB - log.Info("ProcessParallel a tx is done, merge to block stateDB", - "resultSlotIndex", resultSlotIndex, "resultTxIndex", resultTxIndex) - objSuicided, stateChanges, balanceChanges, codeChanges, addrChanges := statedb.MergeSlotDB(result.slotDB, result.receipt) - // slot's mergedTxInfo is updated by dispatcher, while consumed by slot. + // Slot's mergedChangeList is produced by dispatcher, while consumed by slot. // It is safe, since write and read is in sequential, do write -> notify -> read - // it is not good, but work right now. - - resultSlotState.mergedTxInfo = append(resultSlotState.mergedTxInfo, MergedTxInfo{result.slotDB, objSuicided, stateChanges, balanceChanges, codeChanges, addrChanges, resultTxIndex}) + // It is not good, but work right now. + changeList := statedb.MergeSlotDB(result.slotDB, result.receipt, resultTxIndex) + resultSlotState.mergedChangeList = append(resultSlotState.mergedChangeList, changeList) if resultTxIndex != p.mergedTxIndex+1 { log.Warn("ProcessParallel tx result out of order", "resultTxIndex", resultTxIndex, @@ -687,7 +623,8 @@ func (p *StateProcessor) waitUntilNextTxDone(statedb *state.StateDB) *ParallelTx panic("ProcessParallel tx result out of order") } p.mergedTxIndex = resultTxIndex - // notify the following Tx, it is merged, what if no wait or next tx is in same slot? + // notify the following Tx, it is merged, + // fixme: what if no wait or next tx is in same slot? result.txReq.curTxChan <- resultTxIndex return result } @@ -696,7 +633,6 @@ func (p *StateProcessor) execInParallelSlot(slotIndex int, txReq *ParallelTxRequ txIndex := txReq.txIndex tx := txReq.tx slotDB := txReq.slotDB - slotDB.SlotIndex = slotIndex gp := txReq.gp // goroutine unsafe msg := txReq.msg block := txReq.block @@ -714,19 +650,19 @@ func (p *StateProcessor) execInParallelSlot(slotIndex int, txReq *ParallelTxRequ // fixme: to optimize, reuse the slotDB slotDB.Prepare(tx.Hash(), block.Hash(), txIndex) - log.Debug("execInParallelSlot enter", "slotIndex", slotIndex, "txIndex", txIndex, "slotDB.baseTxIndex", slotDB.BaseTxIndex()) + log.Debug("exec In Slot", "Slot", slotIndex, "txIndex", txIndex, "slotDB.baseTxIndex", slotDB.BaseTxIndex()) slotGasLimit := gp.Gas() gpSlot := new(GasPool).AddGas(slotGasLimit) // each slot would use its own gas pool, and will do gaslimit check later evm, result, err = applyTransactionStageExecution(msg, gpSlot, slotDB, vmenv) - log.Debug("execInParallelSlot Stage Execution done", "slotIndex", slotIndex, "txIndex", txIndex, "slotDB.baseTxIndex", slotDB.BaseTxIndex()) + log.Debug("Stage Execution done", "Slot", slotIndex, "txIndex", txIndex, "slotDB.baseTxIndex", slotDB.BaseTxIndex()) // wait until the previous tx is finalized. if txReq.waitTxChan != nil { - log.Info("execInParallelSlot wait previous Tx done", "my slotIndex", slotIndex, "txIndex", txIndex) + log.Debug("Stage wait previous Tx done", "Slot", slotIndex, "txIndex", txIndex) waitTxIndex := <-txReq.waitTxChan if waitTxIndex != txIndex-1 { - log.Error("execInParallelSlot wait tx index mismatch", "expect", txIndex-1, "actual", waitTxIndex) + log.Error("Stage wait tx index mismatch", "expect", txIndex-1, "actual", waitTxIndex) panic(fmt.Sprintf("wait tx index mismatch expect:%d, actual:%d", txIndex-1, waitTxIndex)) } } @@ -735,7 +671,8 @@ func (p *StateProcessor) execInParallelSlot(slotIndex int, txReq *ParallelTxRequ // for example: err="nonce too high" // in this case, we will do re-run. if err != nil { - log.Debug("Stage Execution err", "slotIndex", slotIndex, "txIndex", txIndex, + p.debugErrorRedoNum++ + log.Debug("Stage Execution err", "Slot", slotIndex, "txIndex", txIndex, "current slotDB.baseTxIndex", slotDB.BaseTxIndex(), "err", err) redoResult := &ParallelTxResult{ redo: true, @@ -750,10 +687,9 @@ func (p *StateProcessor) execInParallelSlot(slotIndex int, txReq *ParallelTxRequ } p.paraTxResultChan <- redoResult slotDB = <-p.slotState[slotIndex].slotdbChan - slotDB.SlotIndex = slotIndex slotDB.Prepare(tx.Hash(), block.Hash(), txIndex) // vmenv.Reset(vm.TxContext{}, slotDB) - log.Debug("Stage Execution get new slotdb to redo", "slotIndex", slotIndex, + log.Debug("Stage Execution get new slotdb to redo", "Slot", slotIndex, "txIndex", txIndex, "new slotDB.baseTxIndex", slotDB.BaseTxIndex()) slotGasLimit = gp.Gas() gpSlot = new(GasPool).AddGas(slotGasLimit) @@ -771,7 +707,7 @@ func (p *StateProcessor) execInParallelSlot(slotIndex int, txReq *ParallelTxRequ hasConflict := false systemAddrConflict := false - log.Debug("execInParallelSlot Tx Stage1 done, do conflict check", "slotIndex", slotIndex, "txIndex", txIndex) + log.Debug("Stage Execution done, do conflict check", "Slot", slotIndex, "txIndex", txIndex) if slotDB.SystemAddressRedo() { hasConflict = true systemAddrConflict = true @@ -785,13 +721,13 @@ func (p *StateProcessor) execInParallelSlot(slotIndex int, txReq *ParallelTxRequ } // check all finalizedDb from current slot's - for _, mergedInfo := range p.slotState[index].mergedTxInfo { - if mergedInfo.txIndex <= slotDB.BaseTxIndex() { - // log.Info("skip finalized DB which is out of the conflict window", "finDb.txIndex", finDb.txIndex, "slotDB.baseTxIndex", slotDB.baseTxIndex) + for _, changeList := range p.slotState[index].mergedChangeList { + if changeList.TxIndex <= slotDB.BaseTxIndex() { + // log.Debug("skip finalized DB which is out of the conflict window", "finDb.txIndex", finDb.txIndex, "slotDB.baseTxIndex", slotDB.baseTxIndex) continue } - if p.hasStateConflict(slotDB, mergedInfo) { - log.Debug("execInParallelSlot Stage Execution conflict", "slotIndex", slotIndex, + if p.hasStateConflict(slotDB, changeList) { + log.Debug("Stage Execution conflict", "Slot", slotIndex, "txIndex", txIndex, " conflict slot", index, "slotDB.baseTxIndex", slotDB.BaseTxIndex()) hasConflict = true break @@ -804,6 +740,7 @@ func (p *StateProcessor) execInParallelSlot(slotIndex int, txReq *ParallelTxRequ } if hasConflict { + p.debugConflictRedoNum++ // re-run should not have conflict, since it has the latest world state. redoResult := &ParallelTxResult{ redo: true, @@ -819,7 +756,6 @@ func (p *StateProcessor) execInParallelSlot(slotIndex int, txReq *ParallelTxRequ } p.paraTxResultChan <- redoResult slotDB = <-p.slotState[slotIndex].slotdbChan - slotDB.SlotIndex = slotIndex slotDB.Prepare(tx.Hash(), block.Hash(), txIndex) // vmenv.Reset(vm.TxContext{}, slotDB) slotGasLimit = gp.Gas() @@ -833,7 +769,7 @@ func (p *StateProcessor) execInParallelSlot(slotIndex int, txReq *ParallelTxRequ // goroutine unsafe operation will be handled from here for safety gasConsumed := slotGasLimit - gpSlot.Gas() if gasConsumed != result.UsedGas { - log.Error("execInParallelSlot gasConsumed != result.UsedGas mismatch", + log.Error("gasConsumed != result.UsedGas mismatch", "gasConsumed", gasConsumed, "result.UsedGas", result.UsedGas) panic(fmt.Sprintf("gas consume mismatch, consumed:%d, result.UsedGas:%d", gasConsumed, result.UsedGas)) } @@ -843,15 +779,17 @@ func (p *StateProcessor) execInParallelSlot(slotIndex int, txReq *ParallelTxRequ panic(fmt.Sprintf("gas limit reached, gasConsumed:%d, gp.Gas():%d", gasConsumed, gp.Gas())) } - log.Debug("execInParallelSlot ok to finalize this TX", - "slotIndex", slotIndex, "txIndex", txIndex, "result.UsedGas", result.UsedGas, "txReq.usedGas", *txReq.usedGas) + log.Debug("ok to finalize this TX", + "Slot", slotIndex, "txIndex", txIndex, "result.UsedGas", result.UsedGas, "txReq.usedGas", *txReq.usedGas) // ok, time to do finalize, stage2 should not be parallel receipt, err = applyTransactionStageFinalization(evm, result, msg, p.config, slotDB, header, tx, txReq.usedGas, bloomProcessors) - if result.Err != nil { + if result.Failed() { // if Tx is reverted, all its state change will be discarded - log.Debug("execInParallelSlot TX reverted?", "slotIndex", slotIndex, "txIndex", txIndex, "result.Err", result.Err) + log.Debug("TX reverted?", "Slot", slotIndex, "txIndex", txIndex, "result.Err", result.Err) + slotDB.RevertSlotDB(msg.From()) } + return &ParallelTxResult{ redo: false, updateSlotDB: false, @@ -868,11 +806,11 @@ func (p *StateProcessor) execInParallelSlot(slotIndex int, txReq *ParallelTxRequ func (p *StateProcessor) runSlotLoop(slotIndex int) { curSlot := p.slotState[slotIndex] for { - // log.Info("parallel slot waiting", "slotIndex:", slotIndex) + // log.Info("parallel slot waiting", "Slot", slotIndex) // wait for new TxReq txReq := <-curSlot.pendingExec // receive a dispatched message - log.Debug("SlotLoop received a new TxReq", "slotIndex:", slotIndex, "txIndex", txReq.txIndex) + log.Debug("SlotLoop received a new TxReq", "Slot", slotIndex, "txIndex", txReq.txIndex) // SlotDB create rational: // ** for a dispatched tx, @@ -894,83 +832,142 @@ func (p *StateProcessor) runSlotLoop(slotIndex int) { txReq.slotDB = <-curSlot.slotdbChan } result := p.execInParallelSlot(slotIndex, txReq) - log.Debug("SlotLoop the TxReq is done", "slotIndex:", slotIndex, "err", result.err) + log.Debug("SlotLoop the TxReq is done", "Slot", slotIndex, "err", result.err) p.paraTxResultChan <- result } } // clear slotState for each block. -func (p *StateProcessor) resetSlotState() { +func (p *StateProcessor) resetParallelState(txNum int) { + if txNum == 0 { + return + } p.mergedTxIndex = -1 + p.debugErrorRedoNum = 0 + p.debugConflictRedoNum = 0 + for _, slotState := range p.slotState { slotState.tailTxReq = nil - slotState.mergedTxInfo = make([]MergedTxInfo, 0) + slotState.mergedChangeList = make([]state.SlotChangeList, 0) } } -func (p *StateProcessor) InitParallelOnce() { - // to create and start the execution slot goroutines - if p.paraInitialized { - return +// Before transactions are executed, do shared preparation for Process() & ProcessParallel() +func (p *StateProcessor) preExecute(block *types.Block, statedb *state.StateDB, cfg vm.Config, parallel bool) (types.Signer, *vm.EVM, *AsyncReceiptBloomGenerator) { + signer := types.MakeSigner(p.bc.chainConfig, block.Number()) + // Mutate the block and state according to any hard-fork specs + if p.config.DAOForkSupport && p.config.DAOForkBlock != nil && p.config.DAOForkBlock.Cmp(block.Number()) == 0 { + misc.ApplyDAOHardFork(statedb) } + // Handle upgrade build-in system contract code + systemcontracts.UpgradeBuildInSystemContract(p.config, block.Number(), statedb) - p.paraTxResultChan = make(chan *ParallelTxResult, ParallelExecNum) // fixme: use blocked chan? - p.slotState = make([]*SlotState, ParallelExecNum) + blockContext := NewEVMBlockContext(block.Header(), p.bc, nil) + // with parallel mode, vmenv will be created inside of slot + var vmenv *vm.EVM + if !parallel { + vmenv = vm.NewEVM(blockContext, vm.TxContext{}, statedb, p.config, cfg) + } - wg := sync.WaitGroup{} // make sure all goroutines are created and started - for i := 0; i < ParallelExecNum; i++ { - p.slotState[i] = new(SlotState) - p.slotState[i].slotdbChan = make(chan *state.StateDB, 1) - p.slotState[i].pendingExec = make(chan *ParallelTxRequest, MaxPendingQueueSize) + // initialise bloom processors + bloomProcessors := NewAsyncReceiptBloomGenerator(len(block.Transactions())) + statedb.MarkFullProcessed() - wg.Add(1) - // start the slot's goroutine - go func(slotIndex int) { - wg.Done() - p.runSlotLoop(slotIndex) // this loop will be permanent live - log.Error("runSlotLoop exit!", "slotIndex", slotIndex) - }(i) + return signer, vmenv, bloomProcessors +} + +func (p *StateProcessor) postExecute(block *types.Block, statedb *state.StateDB, commonTxs *[]*types.Transaction, + receipts *[]*types.Receipt, systemTxs *[]*types.Transaction, usedGas *uint64, bloomProcessors *AsyncReceiptBloomGenerator) ([]*types.Log, error) { + var allLogs []*types.Log + + bloomProcessors.Close() + + // Finalize the block, applying any consensus engine specific extras (e.g. block rewards) + err := p.engine.Finalize(p.bc, block.Header(), statedb, commonTxs, block.Uncles(), receipts, systemTxs, usedGas) + if err != nil { + return allLogs, err } - wg.Wait() - p.paraInitialized = true + for _, receipt := range *receipts { + allLogs = append(allLogs, receipt.Logs...) + } + return allLogs, nil } -func (p *StateProcessor) ProcessParallel(block *types.Block, statedb *state.StateDB, cfg vm.Config) (*state.StateDB, types.Receipts, []*types.Log, uint64, error) { - p.InitParallelOnce() +// Process processes the state changes according to the Ethereum rules by running +// the transaction messages using the statedb and applying any rewards to both +// the processor (coinbase) and any included uncles. +// +// Process returns the receipts and logs accumulated during the process and +// returns the amount of gas that was used in the process. If any of the +// transactions failed to execute due to insufficient gas it will return an error. +func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg vm.Config) (*state.StateDB, types.Receipts, []*types.Log, uint64, error) { var ( usedGas = new(uint64) header = block.Header() - allLogs []*types.Log gp = new(GasPool).AddGas(block.GasLimit()) ) - signer := types.MakeSigner(p.bc.chainConfig, block.Number()) var receipts = make([]*types.Receipt, 0) - // Mutate the block and state according to any hard-fork specs - if p.config.DAOForkSupport && p.config.DAOForkBlock != nil && p.config.DAOForkBlock.Cmp(block.Number()) == 0 { - misc.ApplyDAOHardFork(statedb) + txNum := len(block.Transactions()) + commonTxs := make([]*types.Transaction, 0, txNum) + // Iterate over and process the individual transactions + posa, isPoSA := p.engine.(consensus.PoSA) + // usually do have two tx, one for validator set contract, another for system reward contract. + systemTxs := make([]*types.Transaction, 0, 2) + + signer, vmenv, bloomProcessors := p.preExecute(block, statedb, cfg, false) + for i, tx := range block.Transactions() { + if isPoSA { + if isSystemTx, err := posa.IsSystemTransaction(tx, block.Header()); err != nil { + bloomProcessors.Close() + return statedb, nil, nil, 0, err + } else if isSystemTx { + systemTxs = append(systemTxs, tx) + continue + } + } + + msg, err := tx.AsMessage(signer) + if err != nil { + bloomProcessors.Close() + return statedb, nil, nil, 0, err + } + statedb.Prepare(tx.Hash(), block.Hash(), i) + receipt, err := applyTransaction(msg, p.config, p.bc, nil, gp, statedb, header, tx, usedGas, vmenv, bloomProcessors) + if err != nil { + bloomProcessors.Close() + return statedb, nil, nil, 0, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err) + } + + commonTxs = append(commonTxs, tx) + receipts = append(receipts, receipt) } - // Handle upgrade build-in system contract code - systemcontracts.UpgradeBuildInSystemContract(p.config, block.Number(), statedb) + allLogs, err := p.postExecute(block, statedb, &commonTxs, &receipts, &systemTxs, usedGas, bloomProcessors) + return statedb, receipts, allLogs, *usedGas, err +} + +func (p *StateProcessor) ProcessParallel(block *types.Block, statedb *state.StateDB, cfg vm.Config) (*state.StateDB, types.Receipts, []*types.Log, uint64, error) { + var ( + usedGas = new(uint64) + header = block.Header() + gp = new(GasPool).AddGas(block.GasLimit()) + ) + var receipts = make([]*types.Receipt, 0) txNum := len(block.Transactions()) - if txNum > 0 { - log.Info("ProcessParallel", "block num", block.Number(), "txNum", txNum) - p.resetSlotState() - } + p.resetParallelState(txNum) + // Iterate over and process the individual transactions posa, isPoSA := p.engine.(consensus.PoSA) commonTxs := make([]*types.Transaction, 0, txNum) - - // initialise bloom processors - bloomProcessors := NewAsyncReceiptBloomGenerator(txNum) - statedb.MarkFullProcessed() - // usually do have two tx, one for validator set contract, another for system reward contract. systemTxs := make([]*types.Transaction, 0, 2) + + signer, _, bloomProcessors := p.preExecute(block, statedb, cfg, true) var waitTxChan, curTxChan chan int for i, tx := range block.Transactions() { if isPoSA { if isSystemTx, err := posa.IsSystemTransaction(tx, block.Header()); err != nil { + bloomProcessors.Close() return statedb, nil, nil, 0, err } else if isSystemTx { systemTxs = append(systemTxs, tx) @@ -980,6 +977,7 @@ func (p *StateProcessor) ProcessParallel(block *types.Block, statedb *state.Stat msg, err := tx.AsMessage(signer) // fixme: move it into slot. if err != nil { + bloomProcessors.Close() return statedb, nil, nil, 0, err } @@ -1003,10 +1001,9 @@ func (p *StateProcessor) ProcessParallel(block *types.Block, statedb *state.Stat // fixme: to optimize the for { for {} } loop code style for { - // if p.queueToSameAddress(txReq) { - // log.Info("ProcessParallel queue to same slot", "txIndex", txReq.txIndex) - // continue - // } + if p.queueToSameAddress(txReq) { + break + } // if idle slot available, just dispatch and process next tx. if p.dispatchToIdleSlot(statedb, txReq) { @@ -1021,6 +1018,7 @@ func (p *StateProcessor) ProcessParallel(block *types.Block, statedb *state.Stat if result.err != nil { log.Warn("ProcessParallel a failed tx", "resultSlotIndex", result.slotIndex, "resultTxIndex", result.txIndex, "result.err", result.err) + bloomProcessors.Close() return statedb, nil, nil, 0, fmt.Errorf("could not apply tx %d [%v]: %w", result.txIndex, result.tx.Hash().Hex(), result.err) } commonTxs = append(commonTxs, result.tx) @@ -1041,22 +1039,17 @@ func (p *StateProcessor) ProcessParallel(block *types.Block, statedb *state.Stat receipts = append(receipts, result.receipt) } - bloomProcessors.Close() - if txNum > 0 { + // len(commonTxs) could be 0, such as: https://bscscan.com/block/14580486 + if len(commonTxs) > 0 { log.Info("ProcessParallel tx all done", "block", header.Number, "usedGas", *usedGas, - "len(commonTxs)", len(commonTxs), "len(receipts)", len(receipts), - "len(systemTxs)", len(systemTxs)) + "txNum", txNum, + "len(commonTxs)", len(commonTxs), + "debugErrorRedoNum", p.debugErrorRedoNum, + "debugConflictRedoNum", p.debugConflictRedoNum, + "redo rate(%)", 100*(p.debugErrorRedoNum+p.debugConflictRedoNum)/len(commonTxs)) } - // Finalize the block, applying any consensus engine specific extras (e.g. block rewards) - err := p.engine.Finalize(p.bc, header, statedb, &commonTxs, block.Uncles(), &receipts, &systemTxs, usedGas) - if err != nil { - return statedb, receipts, allLogs, *usedGas, err - } - for _, receipt := range receipts { - allLogs = append(allLogs, receipt.Logs...) - } - - return statedb, receipts, allLogs, *usedGas, nil + allLogs, err := p.postExecute(block, statedb, &commonTxs, &receipts, &systemTxs, usedGas, bloomProcessors) + return statedb, receipts, allLogs, *usedGas, err } func applyTransaction(msg types.Message, config *params.ChainConfig, bc ChainContext, author *common.Address, gp *GasPool, statedb *state.StateDB, header *types.Header, tx *types.Transaction, usedGas *uint64, evm *vm.EVM, receiptProcessors ...ReceiptProcessor) (*types.Receipt, error) { diff --git a/core/types.go b/core/types.go index 6ead2938cc..82966e809d 100644 --- a/core/types.go +++ b/core/types.go @@ -51,6 +51,7 @@ type Processor interface { // the processor (coinbase) and any included uncles. Process(block *types.Block, statedb *state.StateDB, cfg vm.Config) (*state.StateDB, types.Receipts, []*types.Log, uint64, error) - // ProcessParallel will implement BEP-130, run transactions concurrently. + // Implement BEP-130: Parallel Transaction Execution. + InitParallelOnce() ProcessParallel(block *types.Block, statedb *state.StateDB, cfg vm.Config) (*state.StateDB, types.Receipts, []*types.Log, uint64, error) } From aa3953c9012ead008844017a6847843044f6ee35 Mon Sep 17 00:00:00 2001 From: setunapo Date: Thu, 17 Feb 2022 18:16:12 +0800 Subject: [PATCH 04/16] Parallel: dispatch, queueSize, slot DB prefetch, disable cache prefetch for parallel this patch has 3 changes: 1.change default queuesize to 20, since 10 could be not enough and will cause more conflicts 2.enable slot DB trie prefetch, use the prefetch of main state DB. 3.disable transaction cache prefetch when parallel is enabled since in parallel mode CPU resource could be limitted, and paralle has its own piped transaction execution 4.change dispatch policy ** queue based on from address ** queue based on to address, try next slot if current is full Since from address is used to make dispatch policy, the pending transactions in a slot could have several different To address, so we will compare the To address of every pending transactions. --- cmd/utils/flags.go | 4 +- core/blockchain.go | 13 +++--- core/state/statedb.go | 8 ++++ core/state_processor.go | 92 ++++++++++++++++++++++++++++------------- 4 files changed, 81 insertions(+), 36 deletions(-) diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index d4b59a9c27..90e12f5404 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -808,12 +808,12 @@ var ( } ParallelTxNumFlag = cli.IntFlag{ Name: "parallel.num", - Usage: "Number of slot for transaction execution, only valid in parallel mode (default = CPUNum - 1)", + Usage: "Number of slot for transaction execution, only valid in parallel mode (default: CPUNum - 1)", Value: core.ParallelExecNum, } ParallelTxQueueSizeFlag = cli.IntFlag{ Name: "parallel.queuesize", - Usage: "Max number of Tx that can be queued to a slot, only valid in parallel mode (default = 10)", + Usage: "Max number of Tx that can be queued to a slot, only valid in parallel mode", Value: core.MaxPendingQueueSize, } diff --git a/core/blockchain.go b/core/blockchain.go index f706b6dfe1..0af6232e7a 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -2120,11 +2120,14 @@ func (bc *BlockChain) insertChain(chain types.Blocks, verifySeals bool) (int, er statedb.StartPrefetcher("chain") var followupInterrupt uint32 // For diff sync, it may fallback to full sync, so we still do prefetch - if len(block.Transactions()) >= prefetchTxNumber { - throwaway := statedb.Copy() - go func(start time.Time, followup *types.Block, throwaway *state.StateDB, interrupt *uint32) { - bc.prefetcher.Prefetch(followup, throwaway, bc.vmConfig, &followupInterrupt) - }(time.Now(), block, throwaway, &followupInterrupt) + // parallel mode has a pipeline, similar to this prefetch, to save CPU we disable this prefetch for parallel + if !ParallelTxMode { + if len(block.Transactions()) >= prefetchTxNumber { + throwaway := statedb.Copy() + go func(start time.Time, followup *types.Block, throwaway *state.StateDB, interrupt *uint32) { + bc.prefetcher.Prefetch(followup, throwaway, bc.vmConfig, &followupInterrupt) + }(time.Now(), block, throwaway, &followupInterrupt) + } } //Process block using the parent state as reference point substart := time.Now() diff --git a/core/state/statedb.go b/core/state/statedb.go index 739294bdb2..5ea38c8585 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -443,6 +443,9 @@ func (s *StateDB) MergeSlotDB(slotDb *StateDB, slotReceipt *types.Receipt, txInd // state trie concurrently while the state is mutated so that when we reach the // commit phase, most of the needed data is already hot. func (s *StateDB) StartPrefetcher(namespace string) { + if s.parallel.isSlotDB { + log.Warn("StartPrefetcher should not be called by slot DB") + } s.prefetcherLock.Lock() defer s.prefetcherLock.Unlock() if s.prefetcher != nil { @@ -457,6 +460,9 @@ func (s *StateDB) StartPrefetcher(namespace string) { // StopPrefetcher terminates a running prefetcher and reports any leftover stats // from the gathered metrics. func (s *StateDB) StopPrefetcher() { + if s.parallel.isSlotDB { + log.Warn("StopPrefetcher should not be called by slot DB") + } s.prefetcherLock.Lock() defer s.prefetcherLock.Unlock() if s.prefetcher != nil { @@ -1358,6 +1364,8 @@ func (s *StateDB) CopyForSlot() *StateDB { } state.snapStorage[k] = temp } + // slot will shared main stateDB's prefetcher + state.prefetcher = s.prefetcher } return state } diff --git a/core/state_processor.go b/core/state_processor.go index 9ae8d88762..7467599e48 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -51,7 +51,7 @@ const ( reuseSlotDB = false // parallel slot's pending Txs will reuse the latest slotDB ) -var MaxPendingQueueSize = 10 // parallel slot's maximum number of pending Txs +var MaxPendingQueueSize = 20 // parallel slot's maximum number of pending Txs var ParallelExecNum = runtime.NumCPU() - 1 // leave a CPU to dispatcher // StateProcessor is a basic Processor, which takes care of transitioning @@ -386,7 +386,8 @@ func (p *LightStateProcessor) LightProcess(diffLayer *types.DiffLayer, block *ty type SlotState struct { tailTxReq *ParallelTxRequest // tail pending Tx of the slot, should be accessed on dispatcher only. - pendingExec chan *ParallelTxRequest + pendingTxReqChan chan *ParallelTxRequest + pendingTxReqList []*ParallelTxRequest // maintained by dispatcher for dispatch policy mergedChangeList []state.SlotChangeList slotdbChan chan *state.StateDB // dispatch will create and send this slotDB to slot // conflict check uses conflict window @@ -395,7 +396,7 @@ type SlotState struct { type ParallelTxResult struct { redo bool // for redo, dispatch will wait new tx result - updateSlotDB bool // for redo and pendingExec, slot needs new slotDB, + updateSlotDB bool // for redo and pending tx quest, slot needs new slotDB, reuseSlotDB bool // will try to reuse latest finalized slotDB keepSystem bool // for redo, should keep system address's balance txIndex int @@ -434,7 +435,7 @@ func (p *StateProcessor) InitParallelOnce() { for i := 0; i < ParallelExecNum; i++ { p.slotState[i] = new(SlotState) p.slotState[i].slotdbChan = make(chan *state.StateDB, 1) - p.slotState[i].pendingExec = make(chan *ParallelTxRequest, MaxPendingQueueSize) + p.slotState[i].pendingTxReqChan = make(chan *ParallelTxRequest, MaxPendingQueueSize) wg.Add(1) // start the slot's goroutine @@ -523,32 +524,60 @@ func (p *StateProcessor) hasStateConflict(readDb *state.StateDB, changeList stat // for parallel execute, we put contracts of same address in a slot, // since these txs probably would have conflicts -func (p *StateProcessor) queueToSameAddress(execMsg *ParallelTxRequest) bool { - txToAddr := execMsg.tx.To() +func (p *StateProcessor) queueSameToAddress(txReq *ParallelTxRequest) bool { + txToAddr := txReq.tx.To() + // To() == nil means contract creation, no same To address if txToAddr == nil { return false } for i, slot := range p.slotState { if slot.tailTxReq == nil { // this slot is idle - // log.Debug("queueToSameAddress skip idle slot.") continue } + for _, pending := range slot.pendingTxReqList { + // To() == nil means contract creation, skip it. + if pending.tx.To() == nil { + continue + } + // same to address, put it on slot's pending list. + if *txToAddr == *pending.tx.To() { + select { + case slot.pendingTxReqChan <- txReq: + slot.tailTxReq = txReq + slot.pendingTxReqList = append(slot.pendingTxReqList, txReq) + log.Debug("queue same To address", "Slot", i, "txIndex", txReq.txIndex) + return true + default: + log.Debug("queue same To address, but queue is full", "Slot", i, "txIndex", txReq.txIndex) + break // try next slot + } + } + } + } + return false +} - // To() == nil means contract creation, won't queue to such slot. - if slot.tailTxReq.tx.To() == nil { - // log.Debug("queueToSameAddress, slot's To address is nil", "Slot", i) +// for parallel execute, we put contracts of same address in a slot, +// since these txs probably would have conflicts +func (p *StateProcessor) queueSameFromAddress(txReq *ParallelTxRequest) bool { + txFromAddr := txReq.msg.From() + for i, slot := range p.slotState { + if slot.tailTxReq == nil { // this slot is idle continue } - // same to address, put it on slot's pending list. - if *txToAddr == *slot.tailTxReq.tx.To() { - select { - case slot.pendingExec <- execMsg: - slot.tailTxReq = execMsg - log.Debug("queueToSameAddress", "Slot", i, "txIndex", execMsg.txIndex) - return true - default: - log.Debug("queueToSameAddress but queue is full", "Slot", i, "txIndex", execMsg.txIndex) - return false + for _, pending := range slot.pendingTxReqList { + // same from address, put it on slot's pending list. + if txFromAddr == pending.msg.From() { + select { + case slot.pendingTxReqChan <- txReq: + slot.tailTxReq = txReq + slot.pendingTxReqList = append(slot.pendingTxReqList, txReq) + log.Debug("queue same From address", "Slot", i, "txIndex", txReq.txIndex) + return true + default: + log.Debug("queue same From address, but queue is full", "Slot", i, "txIndex", txReq.txIndex) + break // try next slot + } } } } @@ -565,7 +594,8 @@ func (p *StateProcessor) dispatchToIdleSlot(statedb *state.StateDB, txReq *Paral } log.Debug("dispatchToIdleSlot", "Slot", i, "txIndex", txReq.txIndex) slot.tailTxReq = txReq - slot.pendingExec <- txReq + slot.pendingTxReqList = append(slot.pendingTxReqList, txReq) + slot.pendingTxReqChan <- txReq return true } } @@ -579,7 +609,7 @@ func (p *StateProcessor) waitUntilNextTxDone(statedb *state.StateDB) *ParallelTx result = <-p.paraTxResultChan // slot may request new slotDB, if it think its slotDB is outdated // such as: - // tx in pendingExec, previous tx in same queue is likely "damaged" the slotDB + // tx in pending tx request, previous tx in same queue is likely "damaged" the slotDB // tx redo for confict // tx stage 1 failed, nonce out of order... if result.updateSlotDB { @@ -606,6 +636,7 @@ func (p *StateProcessor) waitUntilNextTxDone(statedb *state.StateDB) *ParallelTx resultSlotIndex := result.slotIndex resultTxIndex := result.txIndex resultSlotState := p.slotState[resultSlotIndex] + resultSlotState.pendingTxReqList = resultSlotState.pendingTxReqList[1:] if resultSlotState.tailTxReq.txIndex == resultTxIndex { log.Debug("ProcessParallel slot is idle", "Slot", resultSlotIndex) resultSlotState.tailTxReq = nil @@ -808,7 +839,7 @@ func (p *StateProcessor) runSlotLoop(slotIndex int) { for { // log.Info("parallel slot waiting", "Slot", slotIndex) // wait for new TxReq - txReq := <-curSlot.pendingExec + txReq := <-curSlot.pendingTxReqChan // receive a dispatched message log.Debug("SlotLoop received a new TxReq", "Slot", slotIndex, "txIndex", txReq.txIndex) @@ -837,7 +868,7 @@ func (p *StateProcessor) runSlotLoop(slotIndex int) { } } -// clear slotState for each block. +// clear slot state for each block. func (p *StateProcessor) resetParallelState(txNum int) { if txNum == 0 { return @@ -846,9 +877,10 @@ func (p *StateProcessor) resetParallelState(txNum int) { p.debugErrorRedoNum = 0 p.debugConflictRedoNum = 0 - for _, slotState := range p.slotState { - slotState.tailTxReq = nil - slotState.mergedChangeList = make([]state.SlotChangeList, 0) + for _, slot := range p.slotState { + slot.tailTxReq = nil + slot.mergedChangeList = make([]state.SlotChangeList, 0) + slot.pendingTxReqList = make([]*ParallelTxRequest, 0) } } @@ -1001,10 +1033,12 @@ func (p *StateProcessor) ProcessParallel(block *types.Block, statedb *state.Stat // fixme: to optimize the for { for {} } loop code style for { - if p.queueToSameAddress(txReq) { + if p.queueSameToAddress(txReq) { + break + } + if p.queueSameFromAddress(txReq) { break } - // if idle slot available, just dispatch and process next tx. if p.dispatchToIdleSlot(statedb, txReq) { // log.Info("ProcessParallel dispatch to idle slot", "txIndex", txReq.txIndex) From 9477bc8a97affd486bdb7a3d4161e24073bef268 Mon Sep 17 00:00:00 2001 From: lunarblock Date: Wed, 23 Feb 2022 15:08:32 +0800 Subject: [PATCH 05/16] Parallel: implement COW(Copy-On-Write) ** use sync map for the stateObjects in parallel ** others fix a SlotDB reuse bug & enable it delete unnecessary parallel initialize for none slot DB. --- core/blockchain.go | 1 + core/state/journal.go | 2 +- core/state/state_object.go | 5 +- core/state/statedb.go | 123 ++++++++++++++++++++++--------------- core/state/statedb_test.go | 24 ++++++++ core/state_processor.go | 10 +-- 6 files changed, 109 insertions(+), 56 deletions(-) diff --git a/core/blockchain.go b/core/blockchain.go index 0af6232e7a..46a420ca94 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -2114,6 +2114,7 @@ func (bc *BlockChain) insertChain(chain types.Blocks, verifySeals bool) (int, er if err != nil { return it.index, err } + bc.updateHighestVerifiedHeader(block.Header()) // Enable prefetching to pull in trie node paths while processing transactions diff --git a/core/state/journal.go b/core/state/journal.go index 20ae6a269a..487e79a57d 100644 --- a/core/state/journal.go +++ b/core/state/journal.go @@ -145,7 +145,7 @@ func (ch createObjectChange) revert(s *StateDB) { if s.parallel.isSlotDB { delete(s.parallel.dirtiedStateObjectsInSlot, *ch.account) } else { - s.stateObjects.Delete(*ch.account) + s.deleteStateObjectFromStateDB(*ch.account) } delete(s.stateObjectsDirty, *ch.account) } diff --git a/core/state/state_object.go b/core/state/state_object.go index 57b330ecad..7631382ae7 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -533,11 +533,10 @@ func (s *StateObject) deepCopy(db *StateDB) *StateObject { return stateObject } +// fixme: this is ownership transfer, to be optimized by state object merge. +// we can leave the ownership to slot and it can be reused. func (s *StateObject) deepCopyForSlot(db *StateDB) *StateObject { s.db = db - if s.trie != nil { - s.trie = db.db.CopyTrie(s.trie) - } return s } diff --git a/core/state/statedb.go b/core/state/statedb.go index 5ea38c8585..3ef0b306e4 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -86,6 +86,34 @@ func (s *StateObjectSyncMap) StoreStateObject(addr common.Address, stateObject * s.Store(addr, stateObject) } +// loadStateObjectFromStateDB is the entry for loading state object from stateObjects in StateDB or stateObjects in parallel +func (s *StateDB) loadStateObjectFromStateDB(addr common.Address) (*StateObject, bool) { + if s.isParallel { + return s.parallel.stateObjects.LoadStateObject(addr) + } else { + obj, ok := s.stateObjects[addr] + return obj, ok + } +} + +// storeStateObjectToStateDB is the entry for storing state object to stateObjects in StateDB or stateObjects in parallel +func (s *StateDB) storeStateObjectToStateDB(addr common.Address, stateObject *StateObject) { + if s.isParallel { + s.parallel.stateObjects.Store(addr, stateObject) + } else { + s.stateObjects[addr] = stateObject + } +} + +// deleteStateObjectFromStateDB is the entry for deleting state object to stateObjects in StateDB or stateObjects in parallel +func (s *StateDB) deleteStateObjectFromStateDB(addr common.Address) { + if s.isParallel { + s.parallel.stateObjects.Delete(addr) + } else { + delete(s.stateObjects, addr) + } +} + // For parallel mode only, keep the change list for later conflict detect type SlotChangeList struct { SlotDB *StateDB // used for SlotDb reuse only, otherwise, it can be discarded @@ -99,7 +127,14 @@ type SlotChangeList struct { // For parallel mode only type ParallelState struct { - isSlotDB bool + isSlotDB bool // isSlotDB denotes StateDB is used in slot + + // stateObjects holds the state objects in the base slot db + // the reason for using stateObjects instead of stateObjects on the outside is + // we need a thread safe map to hold state objects since there are many slots will read + // state objects from this and in the same time we will change this when merging slot db to the base slot db + stateObjects *StateObjectSyncMap + baseTxIndex int // slotDB is created base on this tx index. dirtiedStateObjectsInSlot map[common.Address]*StateObject // for conflict check @@ -153,12 +188,13 @@ type StateDB struct { snapStorage map[common.Address]map[string][]byte // This map holds 'live' objects, which will get modified while processing a state transition. - stateObjects *StateObjectSyncMap + stateObjects map[common.Address]*StateObject stateObjectsPending map[common.Address]struct{} // State objects finalized but not yet written to the trie stateObjectsDirty map[common.Address]struct{} // State objects modified in the current execution - storagePool *StoragePool // sharedPool to store L1 originStorage of stateObjects - writeOnSharedStorage bool // Write to the shared origin storage of a stateObject while reading from the underlying storage layer. + storagePool *StoragePool // sharedPool to store L1 originStorage of stateObjects + writeOnSharedStorage bool // Write to the shared origin storage of a stateObject while reading from the underlying storage layer. + isParallel bool parallel ParallelState // to keep all the parallel execution elements // DB error. @@ -217,8 +253,8 @@ func NewWithSharedPool(root common.Hash, db Database, snaps *snapshot.Tree) (*St return statedb, nil } -// With parallel, each execute slot would have its own stateDB. // NewSlotDB creates a new slot stateDB base on the provided stateDB. +// With parallel, each execute slot would have its own stateDB. func NewSlotDB(db *StateDB, systemAddr common.Address, txIndex int, keepSystem bool) *StateDB { slotDB := db.CopyForSlot() slotDB.originalRoot = db.originalRoot @@ -238,9 +274,6 @@ func NewSlotDB(db *StateDB, systemAddr common.Address, txIndex int, keepSystem b // to avoid new slotDB for each Tx, slotDB should be valid and merged func ReUseSlotDB(slotDB *StateDB, keepSystem bool) *StateDB { - if !keepSystem { - slotDB.SetBalance(slotDB.parallel.systemAddress, big.NewInt(0)) - } slotDB.logs = make(map[common.Hash][]*types.Log, defaultNumOfSlots) slotDB.logSize = 0 slotDB.parallel.systemAddressCount = 0 @@ -255,31 +288,28 @@ func ReUseSlotDB(slotDB *StateDB, keepSystem bool) *StateDB { slotDB.parallel.addrStateReadInSlot = make(map[common.Address]struct{}, defaultNumOfSlots) slotDB.parallel.addrStateChangeInSlot = make(map[common.Address]struct{}, defaultNumOfSlots) + // Previous *StateObject in slot db has been transfered to dispatcher now. + // Slot could no longer use these *StateObject, do clear. + slotDB.parallel.dirtiedStateObjectsInSlot = make(map[common.Address]*StateObject, defaultNumOfSlots) + slotDB.stateObjectsDirty = make(map[common.Address]struct{}, defaultNumOfSlots) slotDB.stateObjectsPending = make(map[common.Address]struct{}, defaultNumOfSlots) + if !keepSystem { + slotDB.SetBalance(slotDB.parallel.systemAddress, big.NewInt(0)) + } return slotDB } func newStateDB(root common.Hash, db Database, snaps *snapshot.Tree) (*StateDB, error) { - parallel := ParallelState{ - isSlotDB: false, - stateObjectSuicided: make(map[common.Address]struct{}, defaultNumOfSlots), - codeReadInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), - codeChangeInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), - stateChangedInSlot: make(map[common.Address]StateKeys, defaultNumOfSlots), - stateReadsInSlot: make(map[common.Address]StateKeys, defaultNumOfSlots), - balanceChangedInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), - balanceReadsInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), - addrStateReadInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), - addrStateChangeInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), + isSlotDB: false, } sdb := &StateDB{ db: db, originalRoot: root, snaps: snaps, - stateObjects: &StateObjectSyncMap{}, + stateObjects: make(map[common.Address]*StateObject, defaultNumOfSlots), parallel: parallel, stateObjectsPending: make(map[common.Address]struct{}, defaultNumOfSlots), stateObjectsDirty: make(map[common.Address]struct{}, defaultNumOfSlots), @@ -317,7 +347,7 @@ func (s *StateDB) getStateObjectFromStateObjects(addr common.Address) (*StateObj return obj, ok } } - return s.stateObjects.LoadStateObject(addr) + return s.loadStateObjectFromStateDB(addr) } // If the transaction execution is failed, keep its read list for conflict detect @@ -330,6 +360,11 @@ func (s *StateDB) RevertSlotDB(from common.Address) { s.parallel.addrStateChangeInSlot = make(map[common.Address]struct{}) } +func (s *StateDB) PrepareForParallel() { + s.isParallel = true + s.parallel.stateObjects = &StateObjectSyncMap{} +} + // MergeSlotDB is for Parallel TX, when the TX is finalized(dirty -> pending) // A bit similar to StateDB.Copy(), // mainly copy stateObjects, since slotDB has been finalized. @@ -363,7 +398,7 @@ func (s *StateDB) MergeSlotDB(slotDb *StateDB, slotReceipt *types.Receipt, txInd // stateObjects: KV, balance, nonce... if obj, ok := slotDb.getStateObjectFromStateObjects(addr); ok { - s.stateObjects.StoreStateObject(addr, obj.deepCopyForSlot(s)) + s.storeStateObjectToStateDB(addr, obj.deepCopyForSlot(s)) } } @@ -378,8 +413,8 @@ func (s *StateDB) MergeSlotDB(slotDb *StateDB, slotReceipt *types.Receipt, txInd continue } - if _, exist := s.stateObjects.LoadStateObject(addr); !exist { - s.stateObjects.StoreStateObject(addr, obj.deepCopyForSlot(s)) + if _, exist := s.loadStateObjectFromStateDB(addr); !exist { + s.storeStateObjectToStateDB(addr, obj.deepCopyForSlot(s)) } } @@ -436,6 +471,8 @@ func (s *StateDB) MergeSlotDB(slotDb *StateDB, slotReceipt *types.Receipt, txInd changeList.AddrStateChangeSet[addr] = struct{}{} } + // the slot DB's is valid now, move baseTxIndex forward, since it could be reused. + slotDb.parallel.baseTxIndex = txIndex return changeList } @@ -1091,7 +1128,7 @@ func (s *StateDB) SetStateObject(object *StateObject) { if s.parallel.isSlotDB { s.parallel.dirtiedStateObjectsInSlot[object.Address()] = object } else { - s.stateObjects.StoreStateObject(object.Address(), object) + s.storeStateObjectToStateDB(object.Address(), object) } } @@ -1187,23 +1224,13 @@ func (s *StateDB) ForEachStorage(addr common.Address, cb func(key, value common. func (s *StateDB) Copy() *StateDB { // Copy all the basic fields, initialize the memory ones parallel := ParallelState{ - isSlotDB: false, - stateObjectSuicided: make(map[common.Address]struct{}, defaultNumOfSlots), - codeReadInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), - codeChangeInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), - stateChangedInSlot: make(map[common.Address]StateKeys, defaultNumOfSlots), - stateReadsInSlot: make(map[common.Address]StateKeys, defaultNumOfSlots), - balanceChangedInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), - balanceReadsInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), - addrStateReadInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), - addrStateChangeInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), - dirtiedStateObjectsInSlot: make(map[common.Address]*StateObject, defaultNumOfSlots), + isSlotDB: false, } state := &StateDB{ db: s.db, trie: s.db.CopyTrie(s.trie), - stateObjects: &StateObjectSyncMap{}, + stateObjects: make(map[common.Address]*StateObject, len(s.journal.dirties)), stateObjectsPending: make(map[common.Address]struct{}, len(s.stateObjectsPending)), stateObjectsDirty: make(map[common.Address]struct{}, len(s.journal.dirties)), storagePool: s.storagePool, @@ -1225,7 +1252,7 @@ func (s *StateDB) Copy() *StateDB { // Even though the original object is dirty, we are not copying the journal, // so we need to make sure that anyside effect the journal would have caused // during a commit (or similar op) is already applied to the copy. - state.stateObjects.StoreStateObject(addr, object.deepCopy(state)) + state.storeStateObjectToStateDB(addr, object.deepCopy(state)) state.stateObjectsDirty[addr] = struct{}{} // Mark the copy dirty to force internal (code/state) commits state.stateObjectsPending[addr] = struct{}{} // Mark the copy pending to force external (account) commits @@ -1237,14 +1264,14 @@ func (s *StateDB) Copy() *StateDB { for addr := range s.stateObjectsPending { if _, exist := state.getStateObjectFromStateObjects(addr); !exist { object, _ := s.getStateObjectFromStateObjects(addr) - state.stateObjects.StoreStateObject(addr, object.deepCopy(state)) + state.storeStateObjectToStateDB(addr, object.deepCopy(state)) } state.stateObjectsPending[addr] = struct{}{} } for addr := range s.stateObjectsDirty { if _, exist := state.getStateObjectFromStateObjects(addr); !exist { object, _ := s.getStateObjectFromStateObjects(addr) - state.stateObjects.StoreStateObject(addr, object.deepCopy(state)) + state.storeStateObjectToStateDB(addr, object.deepCopy(state)) } state.stateObjectsDirty[addr] = struct{}{} } @@ -1304,8 +1331,11 @@ func (s *StateDB) Copy() *StateDB { func (s *StateDB) CopyForSlot() *StateDB { // Copy all the basic fields, initialize the memory ones - parallel := ParallelState{ + isSlotDB: true, + // Share base slot db's stateObjects + // It is a SyncMap, only readable to slot, not writable + stateObjects: s.parallel.stateObjects, stateObjectSuicided: make(map[common.Address]struct{}, defaultNumOfSlots), codeReadInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), codeChangeInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), @@ -1315,13 +1345,12 @@ func (s *StateDB) CopyForSlot() *StateDB { balanceReadsInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), addrStateReadInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), addrStateChangeInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), - isSlotDB: true, dirtiedStateObjectsInSlot: make(map[common.Address]*StateObject, defaultNumOfSlots), } state := &StateDB{ db: s.db, trie: s.db.CopyTrie(s.trie), - stateObjects: s.stateObjects, + stateObjects: make(map[common.Address]*StateObject, defaultNumOfSlots), stateObjectsPending: make(map[common.Address]struct{}, defaultNumOfSlots), stateObjectsDirty: make(map[common.Address]struct{}, defaultNumOfSlots), refund: s.refund, @@ -1333,6 +1362,7 @@ func (s *StateDB) CopyForSlot() *StateDB { snapDestructs: make(map[common.Address]struct{}), snapAccounts: make(map[common.Address][]byte), snapStorage: make(map[common.Address]map[string][]byte), + isParallel: true, parallel: parallel, } @@ -1486,17 +1516,14 @@ func (s *StateDB) CorrectAccountsRoot(blockRoot common.Hash) { return } if accounts, err := snapshot.Accounts(); err == nil && accounts != nil { - s.stateObjects.Range(func(addr, objItf interface{}) bool { - obj := objItf.(*StateObject) + for _, obj := range s.stateObjects { if !obj.deleted && !obj.rootCorrected && obj.data.Root == dummyRoot { if account, exist := accounts[crypto.Keccak256Hash(obj.address[:])]; exist && len(account.Root) != 0 { obj.data.Root = common.BytesToHash(account.Root) obj.rootCorrected = true } } - return true - }) - + } } } diff --git a/core/state/statedb_test.go b/core/state/statedb_test.go index 2fce92ae1d..2841381963 100644 --- a/core/state/statedb_test.go +++ b/core/state/statedb_test.go @@ -943,6 +943,8 @@ func TestSuicide(t *testing.T) { memDb := rawdb.NewMemoryDatabase() db := NewDatabase(memDb) state, _ := New(common.Hash{}, db, nil) + state.PrepareForParallel() + slotDb := NewSlotDB(state, systemAddress, 0, false) addr := common.BytesToAddress([]byte("so")) @@ -979,6 +981,8 @@ func TestSetAndGetState(t *testing.T) { memDb := rawdb.NewMemoryDatabase() db := NewDatabase(memDb) state, _ := New(common.Hash{}, db, nil) + state.PrepareForParallel() + slotDb := NewSlotDB(state, systemAddress, 0, false) addr := common.BytesToAddress([]byte("so")) @@ -1014,6 +1018,8 @@ func TestSetAndGetCode(t *testing.T) { memDb := rawdb.NewMemoryDatabase() db := NewDatabase(memDb) state, _ := New(common.Hash{}, db, nil) + state.PrepareForParallel() + slotDb := NewSlotDB(state, systemAddress, 0, false) addr := common.BytesToAddress([]byte("so")) @@ -1047,6 +1053,8 @@ func TestGetCodeSize(t *testing.T) { memDb := rawdb.NewMemoryDatabase() db := NewDatabase(memDb) state, _ := New(common.Hash{}, db, nil) + state.PrepareForParallel() + slotDb := NewSlotDB(state, systemAddress, 0, false) addr := common.BytesToAddress([]byte("so")) @@ -1068,6 +1076,8 @@ func TestGetCodeHash(t *testing.T) { memDb := rawdb.NewMemoryDatabase() db := NewDatabase(memDb) state, _ := New(common.Hash{}, db, nil) + state.PrepareForParallel() + slotDb := NewSlotDB(state, systemAddress, 0, false) addr := common.BytesToAddress([]byte("so")) @@ -1089,6 +1099,8 @@ func TestSetNonce(t *testing.T) { memDb := rawdb.NewMemoryDatabase() db := NewDatabase(memDb) state, _ := New(common.Hash{}, db, nil) + state.PrepareForParallel() + slotDb := NewSlotDB(state, systemAddress, 0, false) addr := common.BytesToAddress([]byte("so")) @@ -1115,6 +1127,8 @@ func TestSetAndGetBalance(t *testing.T) { memDb := rawdb.NewMemoryDatabase() db := NewDatabase(memDb) state, _ := New(common.Hash{}, db, nil) + state.PrepareForParallel() + slotDb := NewSlotDB(state, systemAddress, 0, true) addr := systemAddress @@ -1157,6 +1171,8 @@ func TestSubBalance(t *testing.T) { memDb := rawdb.NewMemoryDatabase() db := NewDatabase(memDb) state, _ := New(common.Hash{}, db, nil) + state.PrepareForParallel() + slotDb := NewSlotDB(state, systemAddress, 0, true) addr := systemAddress @@ -1195,6 +1211,8 @@ func TestAddBalance(t *testing.T) { memDb := rawdb.NewMemoryDatabase() db := NewDatabase(memDb) state, _ := New(common.Hash{}, db, nil) + state.PrepareForParallel() + slotDb := NewSlotDB(state, systemAddress, 0, true) addr := systemAddress @@ -1233,6 +1251,8 @@ func TestEmpty(t *testing.T) { memDb := rawdb.NewMemoryDatabase() db := NewDatabase(memDb) state, _ := New(common.Hash{}, db, nil) + state.PrepareForParallel() + slotDb := NewSlotDB(state, systemAddress, 0, true) addr := systemAddress @@ -1252,6 +1272,8 @@ func TestExist(t *testing.T) { memDb := rawdb.NewMemoryDatabase() db := NewDatabase(memDb) state, _ := New(common.Hash{}, db, nil) + state.PrepareForParallel() + slotDb := NewSlotDB(state, systemAddress, 0, true) addr := systemAddress @@ -1271,6 +1293,8 @@ func TestMergeSlotDB(t *testing.T) { memDb := rawdb.NewMemoryDatabase() db := NewDatabase(memDb) state, _ := New(common.Hash{}, db, nil) + state.PrepareForParallel() + oldSlotDb := NewSlotDB(state, systemAddress, 0, true) newSlotDb := NewSlotDB(state, systemAddress, 0, true) diff --git a/core/state_processor.go b/core/state_processor.go index 7467599e48..b9de96d272 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -48,7 +48,7 @@ const ( recentTime = 1024 * 3 recentDiffLayerTimeout = 5 farDiffLayerTimeout = 2 - reuseSlotDB = false // parallel slot's pending Txs will reuse the latest slotDB + reuseSlotDB = true // reuse could save state object copy cost ) var MaxPendingQueueSize = 20 // parallel slot's maximum number of pending Txs @@ -588,8 +588,8 @@ func (p *StateProcessor) queueSameFromAddress(txReq *ParallelTxRequest) bool { func (p *StateProcessor) dispatchToIdleSlot(statedb *state.StateDB, txReq *ParallelTxRequest) bool { for i, slot := range p.slotState { if slot.tailTxReq == nil { - // for idle slot, we have to create a SlotDB for it. if len(slot.mergedChangeList) == 0 { + // first transaction of a slot, there is no usable SlotDB, have to create one for it. txReq.slotDB = state.NewSlotDB(statedb, consensus.SystemAddress, p.mergedTxIndex, false) } log.Debug("dispatchToIdleSlot", "Slot", i, "txIndex", txReq.txIndex) @@ -869,7 +869,7 @@ func (p *StateProcessor) runSlotLoop(slotIndex int) { } // clear slot state for each block. -func (p *StateProcessor) resetParallelState(txNum int) { +func (p *StateProcessor) resetParallelState(txNum int, statedb *state.StateDB) { if txNum == 0 { return } @@ -877,6 +877,8 @@ func (p *StateProcessor) resetParallelState(txNum int) { p.debugErrorRedoNum = 0 p.debugConflictRedoNum = 0 + statedb.PrepareForParallel() + for _, slot := range p.slotState { slot.tailTxReq = nil slot.mergedChangeList = make([]state.SlotChangeList, 0) @@ -986,7 +988,7 @@ func (p *StateProcessor) ProcessParallel(block *types.Block, statedb *state.Stat ) var receipts = make([]*types.Receipt, 0) txNum := len(block.Transactions()) - p.resetParallelState(txNum) + p.resetParallelState(txNum, statedb) // Iterate over and process the individual transactions posa, isPoSA := p.engine.(consensus.PoSA) From fe4b302f1657e36f17ea4cc07aa47ee4c6177f46 Mon Sep 17 00:00:00 2001 From: setunapo Date: Mon, 28 Feb 2022 15:31:33 +0800 Subject: [PATCH 06/16] Parallel: several bugfixs: state merge, suicide fixup, conflict detect, prefetch, fork This is a complicated patch, to do some fixup ** fix MergeSlotDB Since copy-on-write is used, transaction will do StateObject deepCopy before it writes the state; All the dirty state changed will be recorded in this copied one first, the ownership will be transfered to main StateDB on merge. It has a potential race condition that the simple ownership transfer may discard other state changes by other concurrent transactions. When copy-on-write is used, we should do StateObject merge. ** fix Suicide Suicide has an address state read operation. And it also needs do copy-on-write, to avoid damage main StateDB's state object. ** fix conflict detect If state read is not zero, should do conflict detect with addr state change first. Do conflict detect even with current slot, if we use copy-on-write and slotDB reuse, same slot could has race conditon of conflict. ** disable prefetch on slotDB trie prefetch should be started on main DB on Merge ** Add/Sub zero balance, Set State These are void operation, optimized to reduce conflict rate. Simple test show, conflict rate dropped from ~25% -> 12% **fix a fork on block 15,338,563 It a nonce conflict caused by opcode: opCreate & opCreate2 Generally, the nonce is advanced by 1 for the transaction sender; But opCreate & opCreate2 will try to create a new contract, the caller will advance its nonce too. It makes the nonce conflict detect more complicated: as nonce is a fundamental part of an account, as long as it has been changed, we mark the address as StateChanged, any concurrent access to it will be considered as conflicted. --- core/state/state_object.go | 11 +- core/state/statedb.go | 260 +++++++++++++++++++++++++++++-------- core/state_processor.go | 95 ++++++++------ core/vm/evm.go | 1 + core/vm/interface.go | 1 + 5 files changed, 272 insertions(+), 96 deletions(-) diff --git a/core/state/state_object.go b/core/state/state_object.go index 7631382ae7..ccf0b8266a 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -533,11 +533,12 @@ func (s *StateObject) deepCopy(db *StateDB) *StateObject { return stateObject } -// fixme: this is ownership transfer, to be optimized by state object merge. -// we can leave the ownership to slot and it can be reused. -func (s *StateObject) deepCopyForSlot(db *StateDB) *StateObject { - s.db = db - return s +func (s *StateObject) MergeSlotObject(db Database, dirtyObjs *StateObject, keys StateKeys) { + for key := range keys { + // better to do s.GetState(db, key) to load originStorage for this key? + // since originStorage was in dirtyObjs, but it works even originStorage miss the state object. + s.SetState(db, key, dirtyObjs.GetState(db, key)) + } } // diff --git a/core/state/statedb.go b/core/state/statedb.go index 3ef0b306e4..db32bccd65 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -123,6 +123,7 @@ type SlotChangeList struct { BalanceChangeSet map[common.Address]struct{} CodeChangeSet map[common.Address]struct{} AddrStateChangeSet map[common.Address]struct{} + NonceAdvancedSet map[common.Address]struct{} } // For parallel mode only @@ -149,6 +150,7 @@ type ParallelState struct { addrStateReadInSlot map[common.Address]struct{} addrStateChangeInSlot map[common.Address]struct{} stateObjectSuicided map[common.Address]struct{} + nonceAdvanced map[common.Address]struct{} // Transaction will pay gas fee to system address. // Parallel execution will clear system address's balance at first, in order to maintain transaction's // gas fee value. Normal transaction will access system address twice, otherwise it means the transaction @@ -255,10 +257,11 @@ func NewWithSharedPool(root common.Hash, db Database, snaps *snapshot.Tree) (*St // NewSlotDB creates a new slot stateDB base on the provided stateDB. // With parallel, each execute slot would have its own stateDB. -func NewSlotDB(db *StateDB, systemAddr common.Address, txIndex int, keepSystem bool) *StateDB { +func NewSlotDB(db *StateDB, systemAddr common.Address, baseTxIndex int, keepSystem bool) *StateDB { + log.Debug("NewSlotDB", "baseTxIndex", baseTxIndex) slotDB := db.CopyForSlot() slotDB.originalRoot = db.originalRoot - slotDB.parallel.baseTxIndex = txIndex + slotDB.parallel.baseTxIndex = baseTxIndex slotDB.parallel.systemAddress = systemAddr slotDB.parallel.systemAddressCount = 0 slotDB.parallel.keepSystemAddressBalance = keepSystem @@ -274,6 +277,9 @@ func NewSlotDB(db *StateDB, systemAddr common.Address, txIndex int, keepSystem b // to avoid new slotDB for each Tx, slotDB should be valid and merged func ReUseSlotDB(slotDB *StateDB, keepSystem bool) *StateDB { + log.Debug("ReUseSlotDB", "baseTxIndex", slotDB.parallel.baseTxIndex, + "keepSystem", keepSystem, "refund", slotDB.refund, + "len(journal.entries)", len(slotDB.journal.entries)) slotDB.logs = make(map[common.Hash][]*types.Log, defaultNumOfSlots) slotDB.logSize = 0 slotDB.parallel.systemAddressCount = 0 @@ -287,6 +293,7 @@ func ReUseSlotDB(slotDB *StateDB, keepSystem bool) *StateDB { slotDB.parallel.balanceReadsInSlot = make(map[common.Address]struct{}, defaultNumOfSlots) slotDB.parallel.addrStateReadInSlot = make(map[common.Address]struct{}, defaultNumOfSlots) slotDB.parallel.addrStateChangeInSlot = make(map[common.Address]struct{}, defaultNumOfSlots) + slotDB.parallel.nonceAdvanced = make(map[common.Address]struct{}, defaultNumOfSlots) // Previous *StateObject in slot db has been transfered to dispatcher now. // Slot could no longer use these *StateObject, do clear. @@ -295,6 +302,10 @@ func ReUseSlotDB(slotDB *StateDB, keepSystem bool) *StateDB { slotDB.stateObjectsDirty = make(map[common.Address]struct{}, defaultNumOfSlots) slotDB.stateObjectsPending = make(map[common.Address]struct{}, defaultNumOfSlots) + // slotDB.snapDestructs = make(map[common.Address]struct{}) + // slotDB.snapAccounts = make(map[common.Address][]byte) + // slotDB.snapStorage = make(map[common.Address]map[string][]byte) + if !keepSystem { slotDB.SetBalance(slotDB.parallel.systemAddress, big.NewInt(0)) } @@ -353,11 +364,14 @@ func (s *StateDB) getStateObjectFromStateObjects(addr common.Address) (*StateObj // If the transaction execution is failed, keep its read list for conflict detect // and discard its state changed, execept its own balance change. func (s *StateDB) RevertSlotDB(from common.Address) { + log.Debug("RevertSlotDB", "addr", from, "txIndex", s.txIndex) s.parallel.stateObjectSuicided = make(map[common.Address]struct{}) s.parallel.stateChangedInSlot = make(map[common.Address]StateKeys) s.parallel.balanceChangedInSlot = make(map[common.Address]struct{}, 1) s.parallel.balanceChangedInSlot[from] = struct{}{} s.parallel.addrStateChangeInSlot = make(map[common.Address]struct{}) + s.parallel.nonceAdvanced = make(map[common.Address]struct{}) + } func (s *StateDB) PrepareForParallel() { @@ -391,14 +405,95 @@ func (s *StateDB) MergeSlotDB(slotDb *StateDB, slotReceipt *types.Receipt, txInd if _, exist := s.stateObjectsDirty[addr]; !exist { s.stateObjectsDirty[addr] = struct{}{} } - + // system address is EOA account, it should have no storage change if addr == systemAddress { continue } // stateObjects: KV, balance, nonce... - if obj, ok := slotDb.getStateObjectFromStateObjects(addr); ok { - s.storeStateObjectToStateDB(addr, obj.deepCopyForSlot(s)) + dirtyObj, ok := slotDb.getStateObjectFromStateObjects(addr) + if !ok { + panic(fmt.Sprintf("MergeSlotDB dirty object not exist! (txIndex: %d, addr: %s)", slotDb.txIndex, addr.String())) + } + mainObj, exist := s.loadStateObjectFromStateDB(addr) + + log.Debug("MergeSlotDB", "txIndex", slotDb.txIndex, "addr", addr, + "exist", exist, "dirtyObj.deleted", dirtyObj.deleted) + if !exist { + // addr not exist on main DB, do ownership transfer + dirtyObj.db = s + dirtyObj.finalise(true) // prefetch on dispatcher + s.storeStateObjectToStateDB(addr, dirtyObj) + delete(slotDb.parallel.dirtiedStateObjectsInSlot, addr) // transfer ownership + } else { + // addr already in main DB, do merge: balance, KV, code, State(create, suicide) + // can not do copy or ownership transfer directly, since dirtyObj could have outdated + // data(may be update within the conflict window) + + // This is debug log for add balance with 0, can be removed + if dirtyObj.deleted && dirtyObj.empty() && !mainObj.empty() { + if _, exist := slotDb.parallel.balanceChangedInSlot[addr]; !exist { + // add(0) could trigger empty delete + // note: what if add/sub with 0 result, it is also a fake delete? + // it is ok, since none-zero add will produce a read record, should conflict + log.Warn("MergeSlotDB empty deleted", "txIndex", slotDb.txIndex, "addr", addr) + } + } + + // Do deepCopy a temporary *StateObject for safety, + // since slot could read the address, dispatch should avoid overwrite the StateObject directly + // otherwise, it could crash for: concurrent map iteration and map write + var newMainObj *StateObject + if _, suicided := slotDb.parallel.stateObjectSuicided[addr]; suicided { + if !dirtyObj.suicided { + // debug purpose, can be removed + log.Warn("MergeSlotDB suicide and recreated", "txIndex", slotDb.txIndex, "addr", addr) + } + } + + if dirtyObj.deleted { + log.Debug("MergeSlotDB state object merge: Suicide") + if !dirtyObj.suicided && !dirtyObj.empty() { + // none suicide object, should be empty delete + log.Error("MergeSlotDB none suicide deleted, should be empty", "txIndex", slotDb.txIndex, "addr", addr) + } + // suicided object will keep its worldstate(useless), and will be deleted from trie on block commit + newMainObj = dirtyObj.deepCopy(s) + delete(s.snapAccounts, addr) + delete(s.snapStorage, addr) + } else if _, created := slotDb.parallel.addrStateChangeInSlot[addr]; created { + log.Debug("MergeSlotDB state object merge: addr state change") + // there are 2 kinds of object creation: + // 1.createObject: AddBalance,SetState to an unexist or emptyDeleted address. + // 2.CreateAccount: like DAO the fork, regenerate a account carry its balance without KV + // can not merge, do ownership transafer + dirtyObj.db = s + newMainObj = dirtyObj + delete(slotDb.parallel.dirtiedStateObjectsInSlot, addr) // transfer ownership + } else { + newMainObj = mainObj.deepCopy(s) + // do merge: balance, KV, code... + if _, balanced := slotDb.parallel.balanceChangedInSlot[addr]; balanced { + log.Debug("MergeSlotDB state object merge: state merge: balance", + "newMainObj.Balance()", newMainObj.Balance(), + "dirtyObj.Balance()", dirtyObj.Balance()) + newMainObj.SetBalance(dirtyObj.Balance()) + } + if _, coded := slotDb.parallel.codeChangeInSlot[addr]; coded { + log.Debug("MergeSlotDB state object merge: state merge: code") + newMainObj.code = dirtyObj.code + newMainObj.data.CodeHash = dirtyObj.data.CodeHash + newMainObj.dirtyCode = true + } + if keys, stated := slotDb.parallel.stateChangedInSlot[addr]; stated { + newMainObj.MergeSlotObject(s.db, dirtyObj, keys) + } + // dirtyObj.Nonce() should not be less than newMainObj + newMainObj.setNonce(dirtyObj.Nonce()) + } + newMainObj.finalise(true) // prefetch on dispatcher + // update the object + s.storeStateObjectToStateDB(addr, newMainObj) } } @@ -408,30 +503,17 @@ func (s *StateDB) MergeSlotDB(slotDb *StateDB, slotReceipt *types.Receipt, txInd } } - for addr, obj := range slotDb.parallel.dirtiedStateObjectsInSlot { - if addr == systemAddress { - continue - } - - if _, exist := s.loadStateObjectFromStateDB(addr); !exist { - s.storeStateObjectToStateDB(addr, obj.deepCopyForSlot(s)) - } - } - // slotDb.logs: logs will be kept in receipts, no need to do merge - // Fixed: preimages should be merged not overwrite for hash, preimage := range slotDb.preimages { s.preimages[hash] = preimage } - // Fixed: accessList should be merged not overwrite if s.accessList != nil { + // fixme: accessList is not enabled yet, should use merged rather than overwrite s.accessList = slotDb.accessList.Copy() } + if slotDb.snaps != nil { - for k, v := range slotDb.snapDestructs { - s.snapDestructs[k] = v - } for k, v := range slotDb.snapAccounts { s.snapAccounts[k] = v } @@ -442,6 +524,18 @@ func (s *StateDB) MergeSlotDB(slotDb *StateDB, slotReceipt *types.Receipt, txInd } s.snapStorage[k] = temp } + for k := range slotDb.snapDestructs { + obj, _ := s.loadStateObjectFromStateDB(k) + if !obj.suicided && !obj.empty() { + // There could be a race condition for parallel transaction execution + // One add balance 0 to an empty address, it will delete it(delete empty is enabled,). + // While another concurrent transaction could add a none-zero balance to it, make it not empty + // Right now, we mark a read operation to avoid this race condition + // We hope to remove this read record, since it could reduce conflict rate. + log.Warn("MergeSlotDB state object to be deleted is not empty, ", "addr", k) + } + s.snapDestructs[k] = struct{}{} + } } // we have to create a new object to store change list for conflict detect, since @@ -454,6 +548,7 @@ func (s *StateDB) MergeSlotDB(slotDb *StateDB, slotReceipt *types.Receipt, txInd BalanceChangeSet: make(map[common.Address]struct{}, len(slotDb.parallel.balanceChangedInSlot)), CodeChangeSet: make(map[common.Address]struct{}, len(slotDb.parallel.codeChangeInSlot)), AddrStateChangeSet: make(map[common.Address]struct{}, len(slotDb.parallel.addrStateChangeInSlot)), + NonceAdvancedSet: make(map[common.Address]struct{}, len(slotDb.parallel.nonceAdvanced)), } for addr := range slotDb.parallel.stateObjectSuicided { changeList.StateObjectSuicided[addr] = struct{}{} @@ -470,6 +565,9 @@ func (s *StateDB) MergeSlotDB(slotDb *StateDB, slotReceipt *types.Receipt, txInd for addr := range slotDb.parallel.addrStateChangeInSlot { changeList.AddrStateChangeSet[addr] = struct{}{} } + for addr := range slotDb.parallel.nonceAdvanced { + changeList.NonceAdvancedSet[addr] = struct{}{} + } // the slot DB's is valid now, move baseTxIndex forward, since it could be reused. slotDb.parallel.baseTxIndex = txIndex @@ -632,9 +730,6 @@ func (s *StateDB) SubRefund(gas uint64) { // Exist reports whether the given account address exists in the state. // Notably this also returns true for suicided accounts. func (s *StateDB) Exist(addr common.Address) bool { - if s.parallel.isSlotDB { - s.parallel.addrStateReadInSlot[addr] = struct{}{} - } return s.getStateObject(addr) != nil } @@ -642,9 +737,6 @@ func (s *StateDB) Exist(addr common.Address) bool { // or empty according to the EIP161 specification (balance = nonce = code = 0) func (s *StateDB) Empty(addr common.Address) bool { so := s.getStateObject(addr) - if s.parallel.isSlotDB { - s.parallel.addrStateReadInSlot[addr] = struct{}{} - } return so == nil || so.empty() } @@ -668,7 +760,6 @@ func (s *StateDB) GetNonce(addr common.Address) uint64 { if stateObject != nil { return stateObject.Nonce() } - return 0 } @@ -797,6 +888,13 @@ func (s *StateDB) GetStorageProofByHash(a common.Address, key common.Hash) ([][] // GetCommittedState retrieves a value from the given account's committed storage trie. func (s *StateDB) GetCommittedState(addr common.Address, hash common.Hash) common.Hash { + if s.parallel.isSlotDB { + if s.parallel.stateReadsInSlot[addr] == nil { + s.parallel.stateReadsInSlot[addr] = make(map[common.Hash]struct{}, defaultNumOfSlots) + } + s.parallel.stateReadsInSlot[addr][hash] = struct{}{} + } + stateObject := s.getStateObject(addr) if stateObject != nil { return stateObject.GetCommittedState(s.db, hash) @@ -823,10 +921,6 @@ func (s *StateDB) StorageTrie(addr common.Address) Trie { func (s *StateDB) HasSuicided(addr common.Address) bool { stateObject := s.getStateObject(addr) - - if s.parallel.isSlotDB { - s.parallel.addrStateReadInSlot[addr] = struct{}{} // address suicided. - } if stateObject != nil { return stateObject.suicided } @@ -840,9 +934,13 @@ func (s *StateDB) HasSuicided(addr common.Address) bool { // AddBalance adds amount to the account associated with addr. func (s *StateDB) AddBalance(addr common.Address, amount *big.Int) { if s.parallel.isSlotDB { - // just in case other tx creates this account, we will miss this if we only add this account when found - s.parallel.balanceChangedInSlot[addr] = struct{}{} - s.parallel.balanceReadsInSlot[addr] = struct{}{} // add balance will perform a read operation first + // just in case other tx creates this account, + // we will miss this if we only add this account when found + if amount.Sign() != 0 { + s.parallel.balanceChangedInSlot[addr] = struct{}{} + } + // add balance will perform a read operation first, empty object will be deleted + s.parallel.balanceReadsInSlot[addr] = struct{}{} if addr == s.parallel.systemAddress { s.parallel.systemAddressCount++ } @@ -867,9 +965,13 @@ func (s *StateDB) AddBalance(addr common.Address, amount *big.Int) { // SubBalance subtracts amount from the account associated with addr. func (s *StateDB) SubBalance(addr common.Address, amount *big.Int) { if s.parallel.isSlotDB { - // just in case other tx creates this account, we will miss this if we only add this account when found - s.parallel.balanceChangedInSlot[addr] = struct{}{} - s.parallel.balanceReadsInSlot[addr] = struct{}{} + // just in case other tx creates this account, + // we will miss this if we only add this account when found + if amount.Sign() != 0 { + s.parallel.balanceChangedInSlot[addr] = struct{}{} + // unlike add, sub 0 balance will not touch empty object + s.parallel.balanceReadsInSlot[addr] = struct{}{} + } if addr == s.parallel.systemAddress { s.parallel.systemAddressCount++ } @@ -912,6 +1014,18 @@ func (s *StateDB) SetBalance(addr common.Address, amount *big.Int) { } } +// Generally sender's nonce will be increased by 1 for each transaction +// But if the contract tries to create a new contract, its nonce will be advanced +// for each opCreate or opCreate2. Nonce is key to transaction execution, once it is +// changed for contract created, the concurrent transaction will be marked invalid if +// they accessed the address. +func (s *StateDB) NonceChanged(addr common.Address) { + if s.parallel.isSlotDB { + log.Debug("NonceChanged", "txIndex", s.txIndex, "addr", addr) + s.parallel.nonceAdvanced[addr] = struct{}{} + } +} + func (s *StateDB) SetNonce(addr common.Address, nonce uint64) { stateObject := s.GetOrNewStateObject(addr) if stateObject != nil { @@ -952,6 +1066,15 @@ func (s *StateDB) SetState(addr common.Address, key, value common.Hash) { stateObject := s.GetOrNewStateObject(addr) if stateObject != nil { if s.parallel.isSlotDB { + if s.parallel.baseTxIndex+1 == s.txIndex { + // we check if state is unchanged + // only when current transaction is the next transaction to be committed + if stateObject.GetState(s.db, key) == value { + log.Debug("Skip set same state", "baseTxIndex", s.parallel.baseTxIndex, + "txIndex", s.txIndex) + return + } + } if _, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; !ok { newStateObject := stateObject.deepCopy(s) newStateObject.SetState(s.db, key, value) @@ -986,22 +1109,37 @@ func (s *StateDB) SetStorage(addr common.Address, storage map[common.Hash]common // getStateObject will return a non-nil account after Suicide. func (s *StateDB) Suicide(addr common.Address) bool { stateObject := s.getStateObject(addr) - // fixme: should add read stateobject record + + if s.parallel.isSlotDB { + s.parallel.addrStateReadInSlot[addr] = struct{}{} + } if stateObject == nil { return false } - if s.parallel.isSlotDB { - s.parallel.stateObjectSuicided[addr] = struct{}{} - s.parallel.addrStateChangeInSlot[addr] = struct{}{} // address suicided. - } s.journal.append(suicideChange{ account: &addr, prev: stateObject.suicided, prevbalance: new(big.Int).Set(stateObject.Balance()), }) - stateObject.markSuicided() - stateObject.data.Balance = new(big.Int) + + if s.parallel.isSlotDB { + s.parallel.stateObjectSuicided[addr] = struct{}{} + s.parallel.addrStateChangeInSlot[addr] = struct{}{} + if _, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; !ok { + // do copy-on-write for suicide "write" + newStateObject := stateObject.deepCopy(s) + newStateObject.markSuicided() + newStateObject.data.Balance = new(big.Int) + s.parallel.dirtiedStateObjectsInSlot[addr] = newStateObject + } else { + stateObject.markSuicided() + stateObject.data.Balance = new(big.Int) + } + } else { + stateObject.markSuicided() + stateObject.data.Balance = new(big.Int) + } return true } @@ -1048,6 +1186,10 @@ func (s *StateDB) deleteStateObject(obj *StateObject) { // the object is not found or was deleted in this execution context. If you need // to differentiate between non-existent/just-deleted, use getDeletedStateObject. func (s *StateDB) getStateObject(addr common.Address) *StateObject { + if s.parallel.isSlotDB { + s.parallel.addrStateReadInSlot[addr] = struct{}{} + } + if obj := s.getDeletedStateObject(addr); obj != nil && !obj.deleted { return obj } @@ -1137,9 +1279,6 @@ func (s *StateDB) GetOrNewStateObject(addr common.Address) *StateObject { stateObject := s.getStateObject(addr) if stateObject == nil { stateObject, _ = s.createObject(addr) - if s.parallel.isSlotDB { - s.parallel.addrStateChangeInSlot[addr] = struct{}{} // address created. - } } return stateObject } @@ -1147,12 +1286,19 @@ func (s *StateDB) GetOrNewStateObject(addr common.Address) *StateObject { // createObject creates a new state object. If there is an existing account with // the given address, it is overwritten and returned as the second return value. func (s *StateDB) createObject(addr common.Address) (newobj, prev *StateObject) { + if s.parallel.isSlotDB { + s.parallel.addrStateReadInSlot[addr] = struct{}{} // fixme: may not necessary + s.parallel.addrStateChangeInSlot[addr] = struct{}{} // address created. + } + prev = s.getDeletedStateObject(addr) // Note, prev might have been deleted, we need that! var prevdestruct bool if s.snap != nil && prev != nil { _, prevdestruct = s.snapDestructs[prev.address] if !prevdestruct { + // createObject for deleted object is ok, + // it will destroy the previous trie node and update with the new object on block commit s.snapDestructs[prev.address] = struct{}{} } } @@ -1183,11 +1329,12 @@ func (s *StateDB) createObject(addr common.Address) (newobj, prev *StateObject) func (s *StateDB) CreateAccount(addr common.Address) { newObj, prev := s.createObject(addr) if prev != nil { - newObj.setBalance(prev.data.Balance) - } else if s.parallel.isSlotDB { - s.parallel.addrStateChangeInSlot[addr] = struct{}{} // new account created + newObj.setBalance(prev.data.Balance) // this read + } + if s.parallel.isSlotDB { + s.parallel.balanceReadsInSlot[addr] = struct{}{} + s.parallel.dirtiedStateObjectsInSlot[addr] = newObj } - } func (s *StateDB) ForEachStorage(addr common.Address, cb func(key, value common.Hash) bool) error { @@ -1332,7 +1479,6 @@ func (s *StateDB) Copy() *StateDB { func (s *StateDB) CopyForSlot() *StateDB { // Copy all the basic fields, initialize the memory ones parallel := ParallelState{ - isSlotDB: true, // Share base slot db's stateObjects // It is a SyncMap, only readable to slot, not writable stateObjects: s.parallel.stateObjects, @@ -1345,6 +1491,8 @@ func (s *StateDB) CopyForSlot() *StateDB { balanceReadsInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), addrStateReadInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), addrStateChangeInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), + nonceAdvanced: make(map[common.Address]struct{}, defaultNumOfSlots), + isSlotDB: true, dirtiedStateObjectsInSlot: make(map[common.Address]*StateObject, defaultNumOfSlots), } state := &StateDB{ @@ -1394,8 +1542,9 @@ func (s *StateDB) CopyForSlot() *StateDB { } state.snapStorage[k] = temp } - // slot will shared main stateDB's prefetcher - state.prefetcher = s.prefetcher + // trie prefetch should be done by dispacther on StateObject Merge, + // disable it in parallel slot + // state.prefetcher = s.prefetcher } return state } @@ -1457,6 +1606,9 @@ func (s *StateDB) Finalise(deleteEmptyObjects bool) { continue } if obj.suicided || (deleteEmptyObjects && obj.empty()) { + if s.parallel.isSlotDB { + s.parallel.addrStateChangeInSlot[addr] = struct{}{} // empty an StateObject is a state change + } obj.deleted = true // If state snapshotting is active, also mark the destruction there. diff --git a/core/state_processor.go b/core/state_processor.go index b9de96d272..7f28b5d44e 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -453,19 +453,21 @@ func (p *StateProcessor) hasStateConflict(readDb *state.StateDB, changeList stat // check KV change reads := readDb.StateReadsInSlot() writes := changeList.StateChangeSet - if len(reads) != 0 && len(writes) != 0 { + if len(reads) != 0 { for readAddr, readKeys := range reads { - if _, exist := changeList.StateObjectSuicided[readAddr]; exist { - log.Debug("conflict: read suicide object", "addr", readAddr) + if _, exist := changeList.AddrStateChangeSet[readAddr]; exist { + log.Debug("conflict: read addr changed state", "addr", readAddr) return true } - if writeKeys, ok := writes[readAddr]; ok { - // readAddr exist - for writeKey := range writeKeys { - // same addr and same key, mark conflicted - if _, ok := readKeys[writeKey]; ok { - log.Debug("conflict: state conflict", "addr", readAddr, "key", writeKey) - return true + if len(writes) != 0 { + if writeKeys, ok := writes[readAddr]; ok { + // readAddr exist + for writeKey := range writeKeys { + // same addr and same key, mark conflicted + if _, ok := readKeys[writeKey]; ok { + log.Debug("conflict: state conflict", "addr", readAddr, "key", writeKey) + return true + } } } } @@ -474,19 +476,22 @@ func (p *StateProcessor) hasStateConflict(readDb *state.StateDB, changeList stat // check balance change balanceReads := readDb.BalanceReadsInSlot() balanceWrite := changeList.BalanceChangeSet - if len(balanceReads) != 0 && len(balanceWrite) != 0 { + if len(balanceReads) != 0 { for readAddr := range balanceReads { - if _, exist := changeList.StateObjectSuicided[readAddr]; exist { - log.Debug("conflict: read suicide balance", "addr", readAddr) + if _, exist := changeList.AddrStateChangeSet[readAddr]; exist { + // txIndex = 0, would create StateObject for SystemAddress + log.Debug("conflict: read addr changed balance", "addr", readAddr) return true } - if _, ok := balanceWrite[readAddr]; ok { - if readAddr == consensus.SystemAddress { - log.Debug("conflict: skip specical system address's balance check") - continue + if len(balanceWrite) != 0 { + if _, ok := balanceWrite[readAddr]; ok { + if readAddr == consensus.SystemAddress { + // log.Debug("conflict: skip specical system address's balance check") + continue + } + log.Debug("conflict: balance conflict", "addr", readAddr) + return true } - log.Debug("conflict: balance conflict", "addr", readAddr) - return true } } } @@ -494,15 +499,17 @@ func (p *StateProcessor) hasStateConflict(readDb *state.StateDB, changeList stat // check code change codeReads := readDb.CodeReadInSlot() codeWrite := changeList.CodeChangeSet - if len(codeReads) != 0 && len(codeWrite) != 0 { + if len(codeReads) != 0 { for readAddr := range codeReads { - if _, exist := changeList.StateObjectSuicided[readAddr]; exist { - log.Debug("conflict: read suicide code", "addr", readAddr) + if _, exist := changeList.AddrStateChangeSet[readAddr]; exist { + log.Debug("conflict: read addr changed code", "addr", readAddr) return true } - if _, ok := codeWrite[readAddr]; ok { - log.Debug("conflict: code conflict", "addr", readAddr) - return true + if len(codeWrite) != 0 { + if _, ok := codeWrite[readAddr]; ok { + log.Debug("conflict: code conflict", "addr", readAddr) + return true + } } } } @@ -510,11 +517,22 @@ func (p *StateProcessor) hasStateConflict(readDb *state.StateDB, changeList stat // check address state change: create, suicide... addrReads := readDb.AddressReadInSlot() addrWrite := changeList.AddrStateChangeSet - if len(addrReads) != 0 && len(addrWrite) != 0 { - for readAddr := range addrReads { - if _, ok := addrWrite[readAddr]; ok { - log.Debug("conflict: address state conflict", "addr", readAddr) - return true + nonceWrite := changeList.NonceAdvancedSet + if len(addrReads) != 0 { + if len(addrWrite) != 0 { + for readAddr := range addrReads { + if _, ok := addrWrite[readAddr]; ok { + log.Debug("conflict: address state conflict", "addr", readAddr) + return true + } + } + } + if len(nonceWrite) != 0 { + for readAddr := range addrReads { + if _, ok := nonceWrite[readAddr]; ok { + log.Debug("conflict: address nonce conflict", "addr", readAddr) + return true + } } } } @@ -747,9 +765,11 @@ func (p *StateProcessor) execInParallelSlot(slotIndex int, txReq *ParallelTxRequ // can skip current slot now, since slotDB is always after current slot's merged DB // ** idle: all previous Txs are merged, it will create a new SlotDB // ** queued: it will request updateSlotDB, dispatcher will create or reuse a SlotDB after previous Tx results are merged - if index == slotIndex { - continue - } + + // with copy-on-write, can not skip current slot + // if index == slotIndex { + // continue + // } // check all finalizedDb from current slot's for _, changeList := range p.slotState[index].mergedChangeList { @@ -759,7 +779,8 @@ func (p *StateProcessor) execInParallelSlot(slotIndex int, txReq *ParallelTxRequ } if p.hasStateConflict(slotDB, changeList) { log.Debug("Stage Execution conflict", "Slot", slotIndex, - "txIndex", txIndex, " conflict slot", index, "slotDB.baseTxIndex", slotDB.BaseTxIndex()) + "txIndex", txIndex, " conflict slot", index, "slotDB.baseTxIndex", slotDB.BaseTxIndex(), + "conflict txIndex", changeList.TxIndex) hasConflict = true break } @@ -1080,9 +1101,9 @@ func (p *StateProcessor) ProcessParallel(block *types.Block, statedb *state.Stat log.Info("ProcessParallel tx all done", "block", header.Number, "usedGas", *usedGas, "txNum", txNum, "len(commonTxs)", len(commonTxs), - "debugErrorRedoNum", p.debugErrorRedoNum, - "debugConflictRedoNum", p.debugConflictRedoNum, - "redo rate(%)", 100*(p.debugErrorRedoNum+p.debugConflictRedoNum)/len(commonTxs)) + "errorNum", p.debugErrorRedoNum, + "conflictNum", p.debugConflictRedoNum, + "redoRate(%)", 100*(p.debugErrorRedoNum+p.debugConflictRedoNum)/len(commonTxs)) } allLogs, err := p.postExecute(block, statedb, &commonTxs, &receipts, &systemTxs, usedGas, bloomProcessors) return statedb, receipts, allLogs, *usedGas, err diff --git a/core/vm/evm.go b/core/vm/evm.go index 53e2e8797b..c7c8e0596c 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -475,6 +475,7 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, } nonce := evm.StateDB.GetNonce(caller.Address()) evm.StateDB.SetNonce(caller.Address(), nonce+1) + evm.StateDB.NonceChanged(caller.Address()) // We add this to the access list _before_ taking a snapshot. Even if the creation fails, // the access-list change should not be rolled back if evm.chainRules.IsBerlin { diff --git a/core/vm/interface.go b/core/vm/interface.go index ad9b05d666..c3d99aaa76 100644 --- a/core/vm/interface.go +++ b/core/vm/interface.go @@ -31,6 +31,7 @@ type StateDB interface { AddBalance(common.Address, *big.Int) GetBalance(common.Address) *big.Int + NonceChanged(common.Address) GetNonce(common.Address) uint64 SetNonce(common.Address, uint64) From dd1ce37fb19ce01bc2e734cb22e92aed712d3f82 Mon Sep 17 00:00:00 2001 From: setunapo Date: Thu, 3 Mar 2022 19:45:40 +0800 Subject: [PATCH 07/16] Parallel: conflict optimize, remove SlotDB reuse, trie prefetch ** optimize conflict for AddBalance(0) Add balance with 0 did nothing, but it will do an empty() check, and add a touch event. Add on transaction finalize, the touch event will check if the StateObject is empty, do empty delete if it is. This patch is to take the empty check as a state check, if the addr state has not been changed(create, suicide, empty delete), then empty check is reliable. ** optimize conflict for system address ** some code improvement & lint fixup & refactor for params ** remove reuse SlotDB Reuse SlotDB was added to reduce copy of StateObject, in order to mitigate the Go GC problem. And COW(Copy-On-Write) is used to address the GC problem too. With COW enabled, reuse can be removed as it has limitted benefits now and add more complexity. ** fix trie prefetch on dispatcher Trie prefetch will be scheduled on object finalize. With parallel, we should schedule trie prefetch on dispatcher, since the TriePrefetcher is not safe for concurrent access and it is created & stopped on dispatcher routine. But object.finalize on slot cleared its dirtyStorage, which broken the later trie prefetch on dispatcher when do MergeSlotDB. --- core/state/statedb.go | 289 +++++++++++++++---------------------- core/state/statedb_test.go | 68 ++++----- core/state_processor.go | 134 +++++++---------- 3 files changed, 203 insertions(+), 288 deletions(-) diff --git a/core/state/statedb.go b/core/state/statedb.go index db32bccd65..696667cd7b 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -116,14 +116,13 @@ func (s *StateDB) deleteStateObjectFromStateDB(addr common.Address) { // For parallel mode only, keep the change list for later conflict detect type SlotChangeList struct { - SlotDB *StateDB // used for SlotDb reuse only, otherwise, it can be discarded - TxIndex int // the tx index of change list + TxIndex int // the tx index of change list StateObjectSuicided map[common.Address]struct{} StateChangeSet map[common.Address]StateKeys BalanceChangeSet map[common.Address]struct{} CodeChangeSet map[common.Address]struct{} AddrStateChangeSet map[common.Address]struct{} - NonceAdvancedSet map[common.Address]struct{} + NonceChangeSet map[common.Address]struct{} } // For parallel mode only @@ -139,24 +138,24 @@ type ParallelState struct { baseTxIndex int // slotDB is created base on this tx index. dirtiedStateObjectsInSlot map[common.Address]*StateObject // for conflict check - balanceChangedInSlot map[common.Address]struct{} // the address's balance has been changed + balanceChangesInSlot map[common.Address]struct{} // the address's balance has been changed balanceReadsInSlot map[common.Address]struct{} // the address's balance has been read and used. - codeReadInSlot map[common.Address]struct{} - codeChangeInSlot map[common.Address]struct{} + codeReadsInSlot map[common.Address]struct{} + codeChangesInSlot map[common.Address]struct{} stateReadsInSlot map[common.Address]StateKeys - stateChangedInSlot map[common.Address]StateKeys // no need record value + stateChangesInSlot map[common.Address]StateKeys // no need record value // Actions such as SetCode, Suicide will change address's state. // Later call like Exist(), Empty(), HasSuicided() depond on the address's state. - addrStateReadInSlot map[common.Address]struct{} - addrStateChangeInSlot map[common.Address]struct{} - stateObjectSuicided map[common.Address]struct{} - nonceAdvanced map[common.Address]struct{} + addrStateReadsInSlot map[common.Address]struct{} + addrStateChangesInSlot map[common.Address]struct{} + stateObjectsSuicidedInSlot map[common.Address]struct{} + nonceChangesInSlot map[common.Address]struct{} // Transaction will pay gas fee to system address. // Parallel execution will clear system address's balance at first, in order to maintain transaction's // gas fee value. Normal transaction will access system address twice, otherwise it means the transaction // needs real system address's balance, the transaction will be marked redo with keepSystemAddressBalance = true systemAddress common.Address - systemAddressCount int + systemAddressOpsCount int keepSystemAddressBalance bool } @@ -263,7 +262,7 @@ func NewSlotDB(db *StateDB, systemAddr common.Address, baseTxIndex int, keepSyst slotDB.originalRoot = db.originalRoot slotDB.parallel.baseTxIndex = baseTxIndex slotDB.parallel.systemAddress = systemAddr - slotDB.parallel.systemAddressCount = 0 + slotDB.parallel.systemAddressOpsCount = 0 slotDB.parallel.keepSystemAddressBalance = keepSystem // clear the slotDB's validator's balance first @@ -275,53 +274,13 @@ func NewSlotDB(db *StateDB, systemAddr common.Address, baseTxIndex int, keepSyst return slotDB } -// to avoid new slotDB for each Tx, slotDB should be valid and merged -func ReUseSlotDB(slotDB *StateDB, keepSystem bool) *StateDB { - log.Debug("ReUseSlotDB", "baseTxIndex", slotDB.parallel.baseTxIndex, - "keepSystem", keepSystem, "refund", slotDB.refund, - "len(journal.entries)", len(slotDB.journal.entries)) - slotDB.logs = make(map[common.Hash][]*types.Log, defaultNumOfSlots) - slotDB.logSize = 0 - slotDB.parallel.systemAddressCount = 0 - slotDB.parallel.keepSystemAddressBalance = keepSystem - slotDB.parallel.stateObjectSuicided = make(map[common.Address]struct{}, defaultNumOfSlots) - slotDB.parallel.codeReadInSlot = make(map[common.Address]struct{}, defaultNumOfSlots) - slotDB.parallel.codeChangeInSlot = make(map[common.Address]struct{}, defaultNumOfSlots) - slotDB.parallel.stateChangedInSlot = make(map[common.Address]StateKeys, defaultNumOfSlots) - slotDB.parallel.stateReadsInSlot = make(map[common.Address]StateKeys, defaultNumOfSlots) - slotDB.parallel.balanceChangedInSlot = make(map[common.Address]struct{}, defaultNumOfSlots) - slotDB.parallel.balanceReadsInSlot = make(map[common.Address]struct{}, defaultNumOfSlots) - slotDB.parallel.addrStateReadInSlot = make(map[common.Address]struct{}, defaultNumOfSlots) - slotDB.parallel.addrStateChangeInSlot = make(map[common.Address]struct{}, defaultNumOfSlots) - slotDB.parallel.nonceAdvanced = make(map[common.Address]struct{}, defaultNumOfSlots) - - // Previous *StateObject in slot db has been transfered to dispatcher now. - // Slot could no longer use these *StateObject, do clear. - slotDB.parallel.dirtiedStateObjectsInSlot = make(map[common.Address]*StateObject, defaultNumOfSlots) - - slotDB.stateObjectsDirty = make(map[common.Address]struct{}, defaultNumOfSlots) - slotDB.stateObjectsPending = make(map[common.Address]struct{}, defaultNumOfSlots) - - // slotDB.snapDestructs = make(map[common.Address]struct{}) - // slotDB.snapAccounts = make(map[common.Address][]byte) - // slotDB.snapStorage = make(map[common.Address]map[string][]byte) - - if !keepSystem { - slotDB.SetBalance(slotDB.parallel.systemAddress, big.NewInt(0)) - } - return slotDB -} - func newStateDB(root common.Hash, db Database, snaps *snapshot.Tree) (*StateDB, error) { - parallel := ParallelState{ - isSlotDB: false, - } sdb := &StateDB{ db: db, originalRoot: root, snaps: snaps, stateObjects: make(map[common.Address]*StateObject, defaultNumOfSlots), - parallel: parallel, + parallel: ParallelState{}, stateObjectsPending: make(map[common.Address]struct{}, defaultNumOfSlots), stateObjectsDirty: make(map[common.Address]struct{}, defaultNumOfSlots), logs: make(map[common.Hash][]*types.Log, defaultNumOfSlots), @@ -365,15 +324,15 @@ func (s *StateDB) getStateObjectFromStateObjects(addr common.Address) (*StateObj // and discard its state changed, execept its own balance change. func (s *StateDB) RevertSlotDB(from common.Address) { log.Debug("RevertSlotDB", "addr", from, "txIndex", s.txIndex) - s.parallel.stateObjectSuicided = make(map[common.Address]struct{}) - s.parallel.stateChangedInSlot = make(map[common.Address]StateKeys) - s.parallel.balanceChangedInSlot = make(map[common.Address]struct{}, 1) - s.parallel.balanceChangedInSlot[from] = struct{}{} - s.parallel.addrStateChangeInSlot = make(map[common.Address]struct{}) - s.parallel.nonceAdvanced = make(map[common.Address]struct{}) - + s.parallel.stateObjectsSuicidedInSlot = make(map[common.Address]struct{}) + s.parallel.stateChangesInSlot = make(map[common.Address]StateKeys) + s.parallel.balanceChangesInSlot = make(map[common.Address]struct{}, 1) + s.parallel.balanceChangesInSlot[from] = struct{}{} + s.parallel.addrStateChangesInSlot = make(map[common.Address]struct{}) + s.parallel.nonceChangesInSlot = make(map[common.Address]struct{}) } +// PrepareForParallel prepares for state db to be used in parallel process. func (s *StateDB) PrepareForParallel() { s.isParallel = true s.parallel.stateObjects = &StateObjectSyncMap{} @@ -401,6 +360,7 @@ func (s *StateDB) MergeSlotDB(slotDb *StateDB, slotReceipt *types.Receipt, txInd } // only merge dirty objects + addressesToPrefetch := make([][]byte, 0, len(slotDb.stateObjectsDirty)) for addr := range slotDb.stateObjectsDirty { if _, exist := s.stateObjectsDirty[addr]; !exist { s.stateObjectsDirty[addr] = struct{}{} @@ -422,7 +382,7 @@ func (s *StateDB) MergeSlotDB(slotDb *StateDB, slotReceipt *types.Receipt, txInd if !exist { // addr not exist on main DB, do ownership transfer dirtyObj.db = s - dirtyObj.finalise(true) // prefetch on dispatcher + dirtyObj.finalise(true) // true: prefetch on dispatcher s.storeStateObjectToStateDB(addr, dirtyObj) delete(slotDb.parallel.dirtiedStateObjectsInSlot, addr) // transfer ownership } else { @@ -430,71 +390,59 @@ func (s *StateDB) MergeSlotDB(slotDb *StateDB, slotReceipt *types.Receipt, txInd // can not do copy or ownership transfer directly, since dirtyObj could have outdated // data(may be update within the conflict window) - // This is debug log for add balance with 0, can be removed - if dirtyObj.deleted && dirtyObj.empty() && !mainObj.empty() { - if _, exist := slotDb.parallel.balanceChangedInSlot[addr]; !exist { - // add(0) could trigger empty delete - // note: what if add/sub with 0 result, it is also a fake delete? - // it is ok, since none-zero add will produce a read record, should conflict - log.Warn("MergeSlotDB empty deleted", "txIndex", slotDb.txIndex, "addr", addr) - } - } - // Do deepCopy a temporary *StateObject for safety, // since slot could read the address, dispatch should avoid overwrite the StateObject directly // otherwise, it could crash for: concurrent map iteration and map write var newMainObj *StateObject - if _, suicided := slotDb.parallel.stateObjectSuicided[addr]; suicided { - if !dirtyObj.suicided { - // debug purpose, can be removed - log.Warn("MergeSlotDB suicide and recreated", "txIndex", slotDb.txIndex, "addr", addr) - } - } - - if dirtyObj.deleted { - log.Debug("MergeSlotDB state object merge: Suicide") - if !dirtyObj.suicided && !dirtyObj.empty() { - // none suicide object, should be empty delete - log.Error("MergeSlotDB none suicide deleted, should be empty", "txIndex", slotDb.txIndex, "addr", addr) - } - // suicided object will keep its worldstate(useless), and will be deleted from trie on block commit - newMainObj = dirtyObj.deepCopy(s) - delete(s.snapAccounts, addr) - delete(s.snapStorage, addr) - } else if _, created := slotDb.parallel.addrStateChangeInSlot[addr]; created { + if _, created := slotDb.parallel.addrStateChangesInSlot[addr]; created { + // there are 3 kinds of state change: + // 1.Suicide + // 2.Empty Delete + // 3.createObject + // a.AddBalance,SetState to an unexist or deleted(suicide, empty delete) address. + // b.CreateAccount: like DAO the fork, regenerate a account carry its balance without KV + // For these state change, do ownership transafer for efficiency: log.Debug("MergeSlotDB state object merge: addr state change") - // there are 2 kinds of object creation: - // 1.createObject: AddBalance,SetState to an unexist or emptyDeleted address. - // 2.CreateAccount: like DAO the fork, regenerate a account carry its balance without KV - // can not merge, do ownership transafer dirtyObj.db = s newMainObj = dirtyObj delete(slotDb.parallel.dirtiedStateObjectsInSlot, addr) // transfer ownership + if dirtyObj.deleted { + // remove the addr from snapAccounts&snapStorage only when object is deleted. + // "deleted" is not equal to "snapDestructs", since createObject() will add an addr for + // snapDestructs to destroy previous object, while it will keep the addr in snapAccounts & snapAccounts + delete(s.snapAccounts, addr) + delete(s.snapStorage, addr) + } } else { - newMainObj = mainObj.deepCopy(s) // do merge: balance, KV, code... - if _, balanced := slotDb.parallel.balanceChangedInSlot[addr]; balanced { + newMainObj = mainObj.deepCopy(s) + if _, balanced := slotDb.parallel.balanceChangesInSlot[addr]; balanced { log.Debug("MergeSlotDB state object merge: state merge: balance", "newMainObj.Balance()", newMainObj.Balance(), "dirtyObj.Balance()", dirtyObj.Balance()) newMainObj.SetBalance(dirtyObj.Balance()) } - if _, coded := slotDb.parallel.codeChangeInSlot[addr]; coded { + if _, coded := slotDb.parallel.codeChangesInSlot[addr]; coded { log.Debug("MergeSlotDB state object merge: state merge: code") newMainObj.code = dirtyObj.code newMainObj.data.CodeHash = dirtyObj.data.CodeHash newMainObj.dirtyCode = true } - if keys, stated := slotDb.parallel.stateChangedInSlot[addr]; stated { + if keys, stated := slotDb.parallel.stateChangesInSlot[addr]; stated { newMainObj.MergeSlotObject(s.db, dirtyObj, keys) } // dirtyObj.Nonce() should not be less than newMainObj newMainObj.setNonce(dirtyObj.Nonce()) } - newMainObj.finalise(true) // prefetch on dispatcher + newMainObj.finalise(true) // true: prefetch on dispatcher // update the object s.storeStateObjectToStateDB(addr, newMainObj) } + addressesToPrefetch = append(addressesToPrefetch, common.CopyBytes(addr[:])) // Copy needed for closure + } + + if s.prefetcher != nil && len(addressesToPrefetch) > 0 { + s.prefetcher.prefetch(s.originalRoot, addressesToPrefetch, emptyAddr) // prefetch for trie node of account } for addr := range slotDb.stateObjectsPending { @@ -514,6 +462,13 @@ func (s *StateDB) MergeSlotDB(slotDb *StateDB, slotReceipt *types.Receipt, txInd } if slotDb.snaps != nil { + for k := range slotDb.snapDestructs { + // There could be a race condition for parallel transaction execution + // One add balance 0 to an empty address, it will delete it(delete empty is enabled,). + // While another concurrent transaction could add a none-zero balance to it, make it not empty + // We fixed it by add a addr state read record for add balance 0 + s.snapDestructs[k] = struct{}{} + } for k, v := range slotDb.snapAccounts { s.snapAccounts[k] = v } @@ -524,49 +479,36 @@ func (s *StateDB) MergeSlotDB(slotDb *StateDB, slotReceipt *types.Receipt, txInd } s.snapStorage[k] = temp } - for k := range slotDb.snapDestructs { - obj, _ := s.loadStateObjectFromStateDB(k) - if !obj.suicided && !obj.empty() { - // There could be a race condition for parallel transaction execution - // One add balance 0 to an empty address, it will delete it(delete empty is enabled,). - // While another concurrent transaction could add a none-zero balance to it, make it not empty - // Right now, we mark a read operation to avoid this race condition - // We hope to remove this read record, since it could reduce conflict rate. - log.Warn("MergeSlotDB state object to be deleted is not empty, ", "addr", k) - } - s.snapDestructs[k] = struct{}{} - } } // we have to create a new object to store change list for conflict detect, since // StateDB could be reused and its elements could be overwritten changeList := SlotChangeList{ - SlotDB: slotDb, TxIndex: txIndex, - StateObjectSuicided: make(map[common.Address]struct{}, len(slotDb.parallel.stateObjectSuicided)), - StateChangeSet: make(map[common.Address]StateKeys, len(slotDb.parallel.stateChangedInSlot)), - BalanceChangeSet: make(map[common.Address]struct{}, len(slotDb.parallel.balanceChangedInSlot)), - CodeChangeSet: make(map[common.Address]struct{}, len(slotDb.parallel.codeChangeInSlot)), - AddrStateChangeSet: make(map[common.Address]struct{}, len(slotDb.parallel.addrStateChangeInSlot)), - NonceAdvancedSet: make(map[common.Address]struct{}, len(slotDb.parallel.nonceAdvanced)), - } - for addr := range slotDb.parallel.stateObjectSuicided { + StateObjectSuicided: make(map[common.Address]struct{}, len(slotDb.parallel.stateObjectsSuicidedInSlot)), + StateChangeSet: make(map[common.Address]StateKeys, len(slotDb.parallel.stateChangesInSlot)), + BalanceChangeSet: make(map[common.Address]struct{}, len(slotDb.parallel.balanceChangesInSlot)), + CodeChangeSet: make(map[common.Address]struct{}, len(slotDb.parallel.codeChangesInSlot)), + AddrStateChangeSet: make(map[common.Address]struct{}, len(slotDb.parallel.addrStateChangesInSlot)), + NonceChangeSet: make(map[common.Address]struct{}, len(slotDb.parallel.nonceChangesInSlot)), + } + for addr := range slotDb.parallel.stateObjectsSuicidedInSlot { changeList.StateObjectSuicided[addr] = struct{}{} } - for addr, storage := range slotDb.parallel.stateChangedInSlot { + for addr, storage := range slotDb.parallel.stateChangesInSlot { changeList.StateChangeSet[addr] = storage } - for addr := range slotDb.parallel.balanceChangedInSlot { + for addr := range slotDb.parallel.balanceChangesInSlot { changeList.BalanceChangeSet[addr] = struct{}{} } - for addr := range slotDb.parallel.codeChangeInSlot { + for addr := range slotDb.parallel.codeChangesInSlot { changeList.CodeChangeSet[addr] = struct{}{} } - for addr := range slotDb.parallel.addrStateChangeInSlot { + for addr := range slotDb.parallel.addrStateChangesInSlot { changeList.AddrStateChangeSet[addr] = struct{}{} } - for addr := range slotDb.parallel.nonceAdvanced { - changeList.NonceAdvancedSet[addr] = struct{}{} + for addr := range slotDb.parallel.nonceChangesInSlot { + changeList.NonceChangeSet[addr] = struct{}{} } // the slot DB's is valid now, move baseTxIndex forward, since it could be reused. @@ -745,7 +687,7 @@ func (s *StateDB) GetBalance(addr common.Address) *big.Int { if s.parallel.isSlotDB { s.parallel.balanceReadsInSlot[addr] = struct{}{} if addr == s.parallel.systemAddress { - s.parallel.systemAddressCount++ + s.parallel.systemAddressOpsCount++ } } stateObject := s.getStateObject(addr) @@ -778,12 +720,12 @@ func (s *StateDB) BaseTxIndex() int { return s.parallel.baseTxIndex } -func (s *StateDB) CodeReadInSlot() map[common.Address]struct{} { - return s.parallel.codeReadInSlot +func (s *StateDB) CodeReadsInSlot() map[common.Address]struct{} { + return s.parallel.codeReadsInSlot } -func (s *StateDB) AddressReadInSlot() map[common.Address]struct{} { - return s.parallel.addrStateReadInSlot +func (s *StateDB) AddressReadsInSlot() map[common.Address]struct{} { + return s.parallel.addrStateReadsInSlot } func (s *StateDB) StateReadsInSlot() map[common.Address]StateKeys { @@ -794,12 +736,12 @@ func (s *StateDB) BalanceReadsInSlot() map[common.Address]struct{} { return s.parallel.balanceReadsInSlot } func (s *StateDB) SystemAddressRedo() bool { - return s.parallel.systemAddressCount > 2 + return s.parallel.systemAddressOpsCount > 2 } func (s *StateDB) GetCode(addr common.Address) []byte { if s.parallel.isSlotDB { - s.parallel.codeReadInSlot[addr] = struct{}{} + s.parallel.codeReadsInSlot[addr] = struct{}{} } stateObject := s.getStateObject(addr) @@ -811,7 +753,7 @@ func (s *StateDB) GetCode(addr common.Address) []byte { func (s *StateDB) GetCodeSize(addr common.Address) int { if s.parallel.isSlotDB { - s.parallel.codeReadInSlot[addr] = struct{}{} // code size is part of code + s.parallel.codeReadsInSlot[addr] = struct{}{} // code size is part of code } stateObject := s.getStateObject(addr) @@ -823,7 +765,7 @@ func (s *StateDB) GetCodeSize(addr common.Address) int { func (s *StateDB) GetCodeHash(addr common.Address) common.Hash { if s.parallel.isSlotDB { - s.parallel.codeReadInSlot[addr] = struct{}{} // code hash is part of code + s.parallel.codeReadsInSlot[addr] = struct{}{} // code hash is part of code } stateObject := s.getStateObject(addr) @@ -937,12 +879,16 @@ func (s *StateDB) AddBalance(addr common.Address, amount *big.Int) { // just in case other tx creates this account, // we will miss this if we only add this account when found if amount.Sign() != 0 { - s.parallel.balanceChangedInSlot[addr] = struct{}{} + s.parallel.balanceChangesInSlot[addr] = struct{}{} + // add balance will perform a read operation first, empty object will be deleted + s.parallel.balanceReadsInSlot[addr] = struct{}{} + } else { + // if amount == 0, no balance change, but there is still an empty check. + // take this empty check as addr state read(create, suicide, empty delete) + s.parallel.addrStateReadsInSlot[addr] = struct{}{} } - // add balance will perform a read operation first, empty object will be deleted - s.parallel.balanceReadsInSlot[addr] = struct{}{} if addr == s.parallel.systemAddress { - s.parallel.systemAddressCount++ + s.parallel.systemAddressOpsCount++ } } @@ -968,12 +914,12 @@ func (s *StateDB) SubBalance(addr common.Address, amount *big.Int) { // just in case other tx creates this account, // we will miss this if we only add this account when found if amount.Sign() != 0 { - s.parallel.balanceChangedInSlot[addr] = struct{}{} + s.parallel.balanceChangesInSlot[addr] = struct{}{} // unlike add, sub 0 balance will not touch empty object s.parallel.balanceReadsInSlot[addr] = struct{}{} } if addr == s.parallel.systemAddress { - s.parallel.systemAddressCount++ + s.parallel.systemAddressOpsCount++ } } @@ -1004,9 +950,9 @@ func (s *StateDB) SetBalance(addr common.Address, amount *big.Int) { } else { stateObject.SetBalance(amount) } - s.parallel.balanceChangedInSlot[addr] = struct{}{} + s.parallel.balanceChangesInSlot[addr] = struct{}{} if addr == s.parallel.systemAddress { - s.parallel.systemAddressCount++ + s.parallel.systemAddressOpsCount++ } } else { stateObject.SetBalance(amount) @@ -1022,7 +968,7 @@ func (s *StateDB) SetBalance(addr common.Address, amount *big.Int) { func (s *StateDB) NonceChanged(addr common.Address) { if s.parallel.isSlotDB { log.Debug("NonceChanged", "txIndex", s.txIndex, "addr", addr) - s.parallel.nonceAdvanced[addr] = struct{}{} + s.parallel.nonceChangesInSlot[addr] = struct{}{} } } @@ -1055,7 +1001,7 @@ func (s *StateDB) SetCode(addr common.Address, code []byte) { stateObject.SetCode(crypto.Keccak256Hash(code), code) } - s.parallel.codeChangeInSlot[addr] = struct{}{} + s.parallel.codeChangesInSlot[addr] = struct{}{} } else { stateObject.SetCode(crypto.Keccak256Hash(code), code) } @@ -1083,10 +1029,10 @@ func (s *StateDB) SetState(addr common.Address, key, value common.Hash) { stateObject.SetState(s.db, key, value) } - if s.parallel.stateChangedInSlot[addr] == nil { - s.parallel.stateChangedInSlot[addr] = make(StateKeys, defaultNumOfSlots) + if s.parallel.stateChangesInSlot[addr] == nil { + s.parallel.stateChangesInSlot[addr] = make(StateKeys, defaultNumOfSlots) } - s.parallel.stateChangedInSlot[addr][key] = struct{}{} + s.parallel.stateChangesInSlot[addr][key] = struct{}{} } else { stateObject.SetState(s.db, key, value) } @@ -1111,7 +1057,7 @@ func (s *StateDB) Suicide(addr common.Address) bool { stateObject := s.getStateObject(addr) if s.parallel.isSlotDB { - s.parallel.addrStateReadInSlot[addr] = struct{}{} + s.parallel.addrStateReadsInSlot[addr] = struct{}{} } if stateObject == nil { return false @@ -1124,8 +1070,8 @@ func (s *StateDB) Suicide(addr common.Address) bool { }) if s.parallel.isSlotDB { - s.parallel.stateObjectSuicided[addr] = struct{}{} - s.parallel.addrStateChangeInSlot[addr] = struct{}{} + s.parallel.stateObjectsSuicidedInSlot[addr] = struct{}{} + s.parallel.addrStateChangesInSlot[addr] = struct{}{} if _, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; !ok { // do copy-on-write for suicide "write" newStateObject := stateObject.deepCopy(s) @@ -1187,7 +1133,7 @@ func (s *StateDB) deleteStateObject(obj *StateObject) { // to differentiate between non-existent/just-deleted, use getDeletedStateObject. func (s *StateDB) getStateObject(addr common.Address) *StateObject { if s.parallel.isSlotDB { - s.parallel.addrStateReadInSlot[addr] = struct{}{} + s.parallel.addrStateReadsInSlot[addr] = struct{}{} } if obj := s.getDeletedStateObject(addr); obj != nil && !obj.deleted { @@ -1287,8 +1233,8 @@ func (s *StateDB) GetOrNewStateObject(addr common.Address) *StateObject { // the given address, it is overwritten and returned as the second return value. func (s *StateDB) createObject(addr common.Address) (newobj, prev *StateObject) { if s.parallel.isSlotDB { - s.parallel.addrStateReadInSlot[addr] = struct{}{} // fixme: may not necessary - s.parallel.addrStateChangeInSlot[addr] = struct{}{} // address created. + s.parallel.addrStateReadsInSlot[addr] = struct{}{} // fixme: may not necessary + s.parallel.addrStateChangesInSlot[addr] = struct{}{} // address created. } prev = s.getDeletedStateObject(addr) // Note, prev might have been deleted, we need that! @@ -1481,19 +1427,19 @@ func (s *StateDB) CopyForSlot() *StateDB { parallel := ParallelState{ // Share base slot db's stateObjects // It is a SyncMap, only readable to slot, not writable - stateObjects: s.parallel.stateObjects, - stateObjectSuicided: make(map[common.Address]struct{}, defaultNumOfSlots), - codeReadInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), - codeChangeInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), - stateChangedInSlot: make(map[common.Address]StateKeys, defaultNumOfSlots), - stateReadsInSlot: make(map[common.Address]StateKeys, defaultNumOfSlots), - balanceChangedInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), - balanceReadsInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), - addrStateReadInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), - addrStateChangeInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), - nonceAdvanced: make(map[common.Address]struct{}, defaultNumOfSlots), - isSlotDB: true, - dirtiedStateObjectsInSlot: make(map[common.Address]*StateObject, defaultNumOfSlots), + stateObjects: s.parallel.stateObjects, + stateObjectsSuicidedInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), + codeReadsInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), + codeChangesInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), + stateChangesInSlot: make(map[common.Address]StateKeys, defaultNumOfSlots), + stateReadsInSlot: make(map[common.Address]StateKeys, defaultNumOfSlots), + balanceChangesInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), + balanceReadsInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), + addrStateReadsInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), + addrStateChangesInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), + nonceChangesInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), + isSlotDB: true, + dirtiedStateObjectsInSlot: make(map[common.Address]*StateObject, defaultNumOfSlots), } state := &StateDB{ db: s.db, @@ -1607,7 +1553,7 @@ func (s *StateDB) Finalise(deleteEmptyObjects bool) { } if obj.suicided || (deleteEmptyObjects && obj.empty()) { if s.parallel.isSlotDB { - s.parallel.addrStateChangeInSlot[addr] = struct{}{} // empty an StateObject is a state change + s.parallel.addrStateChangesInSlot[addr] = struct{}{} // empty an StateObject is a state change } obj.deleted = true @@ -1621,7 +1567,12 @@ func (s *StateDB) Finalise(deleteEmptyObjects bool) { delete(s.snapStorage, obj.address) // Clear out any previously updated storage data (may be recreated via a ressurrect) } } else { - obj.finalise(true) // Prefetch slots in the background + // 1.none parallel mode, we do obj.finalise(true) as normal + // 2.with parallel mode, we do obj.finalise(true) on dispatcher, not on slot routine + // obj.finalise(true) will clear its dirtyStorage, will make prefetch broken. + if !s.isParallel || !s.parallel.isSlotDB { + obj.finalise(true) // Prefetch slots in the background + } } if _, exist := s.stateObjectsPending[addr]; !exist { s.stateObjectsPending[addr] = struct{}{} diff --git a/core/state/statedb_test.go b/core/state/statedb_test.go index 2841381963..5554375560 100644 --- a/core/state/statedb_test.go +++ b/core/state/statedb_test.go @@ -955,12 +955,12 @@ func TestSuicide(t *testing.T) { t.Fatalf("expected account suicide, got %v", result) } - if _, ok := slotDb.parallel.stateObjectSuicided[addr]; !ok { - t.Fatalf("address should exist in stateObjectSuicided") + if _, ok := slotDb.parallel.stateObjectsSuicidedInSlot[addr]; !ok { + t.Fatalf("address should exist in stateObjectsSuicidedInSlot") } - if _, ok := slotDb.parallel.addrStateChangeInSlot[addr]; !ok { - t.Fatalf("address should exist in addrStateChangeInSlot") + if _, ok := slotDb.parallel.addrStateChangesInSlot[addr]; !ok { + t.Fatalf("address should exist in addrStateChangesInSlot") } if _, ok := slotDb.parallel.dirtiedStateObjectsInSlot[addr]; !ok { @@ -972,8 +972,8 @@ func TestSuicide(t *testing.T) { t.Fatalf("address should be suicided") } - if _, ok := slotDb.parallel.addrStateReadInSlot[addr]; !ok { - t.Fatalf("address should exist in addrStateReadInSlot") + if _, ok := slotDb.parallel.addrStateReadsInSlot[addr]; !ok { + t.Fatalf("address should exist in addrStateReadsInSlot") } } @@ -994,8 +994,8 @@ func TestSetAndGetState(t *testing.T) { t.Fatalf("address should exist in dirtiedStateObjectsInSlot") } - if _, ok := slotDb.parallel.stateChangedInSlot[addr]; !ok { - t.Fatalf("address should exist in stateChangedInSlot") + if _, ok := slotDb.parallel.stateChangesInSlot[addr]; !ok { + t.Fatalf("address should exist in stateChangesInSlot") } oldValueRead := state.GetState(addr, common.BytesToHash([]byte("test key"))) @@ -1035,8 +1035,8 @@ func TestSetAndGetCode(t *testing.T) { t.Fatalf("address should exist in dirtiedStateObjectsInSlot") } - if _, ok := slotDb.parallel.codeChangeInSlot[addr]; !ok { - t.Fatalf("address should exist in codeChangeInSlot") + if _, ok := slotDb.parallel.codeChangesInSlot[addr]; !ok { + t.Fatalf("address should exist in codeChangesInSlot") } codeRead := slotDb.GetCode(addr) @@ -1044,8 +1044,8 @@ func TestSetAndGetCode(t *testing.T) { t.Fatalf("code read should be equal to the code stored") } - if _, ok := slotDb.parallel.codeReadInSlot[addr]; !ok { - t.Fatalf("address should exist in codeReadInSlot") + if _, ok := slotDb.parallel.codeReadsInSlot[addr]; !ok { + t.Fatalf("address should exist in codeReadsInSlot") } } @@ -1067,8 +1067,8 @@ func TestGetCodeSize(t *testing.T) { t.Fatalf("code size should be 9") } - if _, ok := slotDb.parallel.codeReadInSlot[addr]; !ok { - t.Fatalf("address should exist in codeReadInSlot") + if _, ok := slotDb.parallel.codeReadsInSlot[addr]; !ok { + t.Fatalf("address should exist in codeReadsInSlot") } } @@ -1090,8 +1090,8 @@ func TestGetCodeHash(t *testing.T) { if hex.EncodeToString(codeSize[:]) != "6e73fa02f7828b28608b078b007a4023fb40453c3e102b83828a3609a94d8cbb" { t.Fatalf("code hash should be 6e73fa02f7828b28608b078b007a4023fb40453c3e102b83828a3609a94d8cbb") } - if _, ok := slotDb.parallel.codeReadInSlot[addr]; !ok { - t.Fatalf("address should exist in codeReadInSlot") + if _, ok := slotDb.parallel.codeReadsInSlot[addr]; !ok { + t.Fatalf("address should exist in codeReadsInSlot") } } @@ -1145,12 +1145,12 @@ func TestSetAndGetBalance(t *testing.T) { t.Fatalf("address should exist in dirtiedStateObjectsInSlot") } - if _, ok := slotDb.parallel.balanceChangedInSlot[addr]; !ok { - t.Fatalf("address should exist in balanceChangedInSlot") + if _, ok := slotDb.parallel.balanceChangesInSlot[addr]; !ok { + t.Fatalf("address should exist in balanceChangesInSlot") } - if slotDb.parallel.systemAddressCount != 1 { - t.Fatalf("systemAddressCount should be 1") + if slotDb.parallel.systemAddressOpsCount != 1 { + t.Fatalf("systemAddressOpsCount should be 1") } newBalance := slotDb.GetBalance(addr) @@ -1162,8 +1162,8 @@ func TestSetAndGetBalance(t *testing.T) { t.Fatalf("address should exist in balanceReadsInSlot") } - if slotDb.parallel.systemAddressCount != 2 { - t.Fatalf("systemAddressCount should be 1") + if slotDb.parallel.systemAddressOpsCount != 2 { + t.Fatalf("systemAddressOpsCount should be 1") } } @@ -1189,16 +1189,16 @@ func TestSubBalance(t *testing.T) { t.Fatalf("address should exist in dirtiedStateObjectsInSlot") } - if _, ok := slotDb.parallel.balanceChangedInSlot[addr]; !ok { - t.Fatalf("address should exist in balanceChangedInSlot") + if _, ok := slotDb.parallel.balanceChangesInSlot[addr]; !ok { + t.Fatalf("address should exist in balanceChangesInSlot") } if _, ok := slotDb.parallel.balanceReadsInSlot[addr]; !ok { t.Fatalf("address should exist in balanceReadsInSlot") } - if slotDb.parallel.systemAddressCount != 1 { - t.Fatalf("systemAddressCount should be 1") + if slotDb.parallel.systemAddressOpsCount != 1 { + t.Fatalf("systemAddressOpsCount should be 1") } newBalance := slotDb.GetBalance(addr) @@ -1229,16 +1229,16 @@ func TestAddBalance(t *testing.T) { t.Fatalf("address should exist in dirtiedStateObjectsInSlot") } - if _, ok := slotDb.parallel.balanceChangedInSlot[addr]; !ok { - t.Fatalf("address should exist in balanceChangedInSlot") + if _, ok := slotDb.parallel.balanceChangesInSlot[addr]; !ok { + t.Fatalf("address should exist in balanceChangesInSlot") } if _, ok := slotDb.parallel.balanceReadsInSlot[addr]; !ok { t.Fatalf("address should exist in balanceReadsInSlot") } - if slotDb.parallel.systemAddressCount != 1 { - t.Fatalf("systemAddressCount should be 1") + if slotDb.parallel.systemAddressOpsCount != 1 { + t.Fatalf("systemAddressOpsCount should be 1") } newBalance := slotDb.GetBalance(addr) @@ -1263,8 +1263,8 @@ func TestEmpty(t *testing.T) { t.Fatalf("address should exist") } - if _, ok := slotDb.parallel.addrStateReadInSlot[addr]; !ok { - t.Fatalf("address should exist in addrStateReadInSlot") + if _, ok := slotDb.parallel.addrStateReadsInSlot[addr]; !ok { + t.Fatalf("address should exist in addrStateReadsInSlot") } } @@ -1284,8 +1284,8 @@ func TestExist(t *testing.T) { t.Fatalf("address should exist") } - if _, ok := slotDb.parallel.addrStateReadInSlot[addr]; !ok { - t.Fatalf("address should exist in addrStateReadInSlot") + if _, ok := slotDb.parallel.addrStateReadsInSlot[addr]; !ok { + t.Fatalf("address should exist in addrStateReadsInSlot") } } diff --git a/core/state_processor.go b/core/state_processor.go index 7f28b5d44e..af6a752f8b 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -48,7 +48,6 @@ const ( recentTime = 1024 * 3 recentDiffLayerTimeout = 5 farDiffLayerTimeout = 2 - reuseSlotDB = true // reuse could save state object copy cost ) var MaxPendingQueueSize = 20 // parallel slot's maximum number of pending Txs @@ -397,7 +396,6 @@ type SlotState struct { type ParallelTxResult struct { redo bool // for redo, dispatch will wait new tx result updateSlotDB bool // for redo and pending tx quest, slot needs new slotDB, - reuseSlotDB bool // will try to reuse latest finalized slotDB keepSystem bool // for redo, should keep system address's balance txIndex int slotIndex int // slot index @@ -453,22 +451,18 @@ func (p *StateProcessor) hasStateConflict(readDb *state.StateDB, changeList stat // check KV change reads := readDb.StateReadsInSlot() writes := changeList.StateChangeSet - if len(reads) != 0 { - for readAddr, readKeys := range reads { - if _, exist := changeList.AddrStateChangeSet[readAddr]; exist { - log.Debug("conflict: read addr changed state", "addr", readAddr) - return true - } - if len(writes) != 0 { - if writeKeys, ok := writes[readAddr]; ok { - // readAddr exist - for writeKey := range writeKeys { - // same addr and same key, mark conflicted - if _, ok := readKeys[writeKey]; ok { - log.Debug("conflict: state conflict", "addr", readAddr, "key", writeKey) - return true - } - } + for readAddr, readKeys := range reads { + if _, exist := changeList.AddrStateChangeSet[readAddr]; exist { + log.Debug("conflict: read addr changed state", "addr", readAddr) + return true + } + if writeKeys, ok := writes[readAddr]; ok { + // readAddr exist + for writeKey := range writeKeys { + // same addr and same key, mark conflicted + if _, ok := readKeys[writeKey]; ok { + log.Debug("conflict: state conflict", "addr", readAddr, "key", writeKey) + return true } } } @@ -476,64 +470,53 @@ func (p *StateProcessor) hasStateConflict(readDb *state.StateDB, changeList stat // check balance change balanceReads := readDb.BalanceReadsInSlot() balanceWrite := changeList.BalanceChangeSet - if len(balanceReads) != 0 { - for readAddr := range balanceReads { - if _, exist := changeList.AddrStateChangeSet[readAddr]; exist { - // txIndex = 0, would create StateObject for SystemAddress + for readAddr := range balanceReads { + if _, exist := changeList.AddrStateChangeSet[readAddr]; exist { + // SystemAddress is special, SystemAddressRedo() is prepared for it. + // Since txIndex = 0 will create StateObject for SystemAddress, skip its state change check + if readAddr != consensus.SystemAddress { log.Debug("conflict: read addr changed balance", "addr", readAddr) return true } - if len(balanceWrite) != 0 { - if _, ok := balanceWrite[readAddr]; ok { - if readAddr == consensus.SystemAddress { - // log.Debug("conflict: skip specical system address's balance check") - continue - } - log.Debug("conflict: balance conflict", "addr", readAddr) - return true - } + } + if _, ok := balanceWrite[readAddr]; ok { + if readAddr != consensus.SystemAddress { + log.Debug("conflict: balance conflict", "addr", readAddr) + return true } } } // check code change - codeReads := readDb.CodeReadInSlot() + codeReads := readDb.CodeReadsInSlot() codeWrite := changeList.CodeChangeSet - if len(codeReads) != 0 { - for readAddr := range codeReads { - if _, exist := changeList.AddrStateChangeSet[readAddr]; exist { - log.Debug("conflict: read addr changed code", "addr", readAddr) - return true - } - if len(codeWrite) != 0 { - if _, ok := codeWrite[readAddr]; ok { - log.Debug("conflict: code conflict", "addr", readAddr) - return true - } - } + for readAddr := range codeReads { + if _, exist := changeList.AddrStateChangeSet[readAddr]; exist { + log.Debug("conflict: read addr changed code", "addr", readAddr) + return true + } + if _, ok := codeWrite[readAddr]; ok { + log.Debug("conflict: code conflict", "addr", readAddr) + return true } } // check address state change: create, suicide... - addrReads := readDb.AddressReadInSlot() + addrReads := readDb.AddressReadsInSlot() addrWrite := changeList.AddrStateChangeSet - nonceWrite := changeList.NonceAdvancedSet - if len(addrReads) != 0 { - if len(addrWrite) != 0 { - for readAddr := range addrReads { - if _, ok := addrWrite[readAddr]; ok { - log.Debug("conflict: address state conflict", "addr", readAddr) - return true - } + nonceWrite := changeList.NonceChangeSet + for readAddr := range addrReads { + if _, ok := addrWrite[readAddr]; ok { + // SystemAddress is special, SystemAddressRedo() is prepared for it. + // Since txIndex = 0 will create StateObject for SystemAddress, skip its state change check + if readAddr != consensus.SystemAddress { + log.Debug("conflict: address state conflict", "addr", readAddr) + return true } } - if len(nonceWrite) != 0 { - for readAddr := range addrReads { - if _, ok := nonceWrite[readAddr]; ok { - log.Debug("conflict: address nonce conflict", "addr", readAddr) - return true - } - } + if _, ok := nonceWrite[readAddr]; ok { + log.Debug("conflict: address nonce conflict", "addr", readAddr) + return true } } @@ -633,14 +616,7 @@ func (p *StateProcessor) waitUntilNextTxDone(statedb *state.StateDB) *ParallelTx if result.updateSlotDB { // the target slot is waiting for new slotDB slotState := p.slotState[result.slotIndex] - var slotDB *state.StateDB - if result.reuseSlotDB { - // for reuse, len(slotState.mergedChangeList) must >= 1 - lastSlotDB := slotState.mergedChangeList[len(slotState.mergedChangeList)-1].SlotDB - slotDB = state.ReUseSlotDB(lastSlotDB, result.keepSystem) - } else { - slotDB = state.NewSlotDB(statedb, consensus.SystemAddress, p.mergedTxIndex, result.keepSystem) - } + slotDB := state.NewSlotDB(statedb, consensus.SystemAddress, p.mergedTxIndex, result.keepSystem) slotState.slotdbChan <- slotDB continue } @@ -697,7 +673,6 @@ func (p *StateProcessor) execInParallelSlot(slotIndex int, txReq *ParallelTxRequ var err error var evm *vm.EVM - // fixme: to optimize, reuse the slotDB slotDB.Prepare(tx.Hash(), block.Hash(), txIndex) log.Debug("exec In Slot", "Slot", slotIndex, "txIndex", txIndex, "slotDB.baseTxIndex", slotDB.BaseTxIndex()) @@ -726,7 +701,6 @@ func (p *StateProcessor) execInParallelSlot(slotIndex int, txReq *ParallelTxRequ redoResult := &ParallelTxResult{ redo: true, updateSlotDB: true, - reuseSlotDB: false, txIndex: txIndex, slotIndex: slotIndex, tx: tx, @@ -762,14 +736,9 @@ func (p *StateProcessor) execInParallelSlot(slotIndex int, txReq *ParallelTxRequ systemAddrConflict = true } else { for index := 0; index < ParallelExecNum; index++ { - // can skip current slot now, since slotDB is always after current slot's merged DB - // ** idle: all previous Txs are merged, it will create a new SlotDB - // ** queued: it will request updateSlotDB, dispatcher will create or reuse a SlotDB after previous Tx results are merged - - // with copy-on-write, can not skip current slot - // if index == slotIndex { - // continue - // } + if index == slotIndex { + continue + } // check all finalizedDb from current slot's for _, changeList := range p.slotState[index].mergedChangeList { @@ -797,7 +766,6 @@ func (p *StateProcessor) execInParallelSlot(slotIndex int, txReq *ParallelTxRequ redoResult := &ParallelTxResult{ redo: true, updateSlotDB: true, - reuseSlotDB: false, // for conflict, we do not reuse keepSystem: systemAddrConflict, txIndex: txIndex, slotIndex: slotIndex, @@ -868,15 +836,11 @@ func (p *StateProcessor) runSlotLoop(slotIndex int) { // ** for a dispatched tx, // the slot should be idle, it is better to create a new SlotDB, since new Tx is not related to previous Tx // ** for a queued tx, - // the previous SlotDB could be reused, since it is likely can be used - // reuse could avoid NewSlotDB cost, which could be costable when StateDB is full of state object - // if the previous SlotDB is + // it is better to create a new SlotDB, since COW is used. if txReq.slotDB == nil { - // for queued Tx, txReq.slotDB is nil, reuse slot's latest merged SlotDB result := &ParallelTxResult{ redo: false, updateSlotDB: true, - reuseSlotDB: reuseSlotDB, slotIndex: slotIndex, err: nil, } @@ -1067,7 +1031,7 @@ func (p *StateProcessor) ProcessParallel(block *types.Block, statedb *state.Stat // log.Info("ProcessParallel dispatch to idle slot", "txIndex", txReq.txIndex) break } - log.Debug("ProcessParallel no slot avaiable, wait", "txIndex", txReq.txIndex) + log.Debug("ProcessParallel no slot available, wait", "txIndex", txReq.txIndex) // no idle slot, wait until a tx is executed and merged. result := p.waitUntilNextTxDone(statedb) From 6d22e98d0d32f46bd4c94763e130e4aa5aee691e Mon Sep 17 00:00:00 2001 From: setunapo Date: Thu, 10 Mar 2022 16:54:42 +0800 Subject: [PATCH 08/16] Parallel: handle fixup & code review & enhancement No fundamental change, some improvements, include: ** Add a new type ParallelStateProcessor; ** move Parallel Config to BlockChain ** more precious ParallelNum set ** Add EnableParallelProcessor() ** remove panic() ** remove useless: redo flag, ** change waitChan from `chan int` to `chan struct {}` and communicate by close() ** dispatch policy: queue `from` ahead of `to` ** pre-allocate allLogs ** disable parallel processor is snapshot is not enabled ** others: rename... --- cmd/utils/flags.go | 46 ++-- core/blockchain.go | 44 ++-- core/state/journal.go | 2 +- core/state/state_object.go | 2 - core/state/statedb.go | 283 ++++++++++------------- core/state_processor.go | 450 +++++++++++++++++-------------------- core/types.go | 4 - eth/backend.go | 2 + eth/ethconfig/config.go | 5 +- 9 files changed, 390 insertions(+), 448 deletions(-) diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 90e12f5404..641f8a3140 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -26,6 +26,7 @@ import ( "math/big" "os" "path/filepath" + "runtime" godebug "runtime/debug" "strconv" "strings" @@ -804,17 +805,16 @@ var ( } ParallelTxFlag = cli.BoolFlag{ Name: "parallel", - Usage: "Enable the experimental parallel transaction execution mode (default = false)", + Usage: "Enable the experimental parallel transaction execution mode, only valid in full sync mode (default = false)", } ParallelTxNumFlag = cli.IntFlag{ Name: "parallel.num", - Usage: "Number of slot for transaction execution, only valid in parallel mode (default: CPUNum - 1)", - Value: core.ParallelExecNum, + Usage: "Number of slot for transaction execution, only valid in parallel mode (runtime calculated, no fixed default value)", } ParallelTxQueueSizeFlag = cli.IntFlag{ Name: "parallel.queuesize", - Usage: "Max number of Tx that can be queued to a slot, only valid in parallel mode", - Value: core.MaxPendingQueueSize, + Usage: "Max number of Tx that can be queued to a slot, only valid in parallel mode (advanced option)", + Value: 20, } // Init network @@ -1336,16 +1336,6 @@ func SetNodeConfig(ctx *cli.Context, cfg *node.Config) { if ctx.GlobalIsSet(InsecureUnlockAllowedFlag.Name) { cfg.InsecureUnlockAllowed = ctx.GlobalBool(InsecureUnlockAllowedFlag.Name) } - if ctx.GlobalIsSet(ParallelTxFlag.Name) { - core.ParallelTxMode = true - } - if ctx.GlobalIsSet(ParallelTxNumFlag.Name) { - core.ParallelExecNum = ctx.GlobalInt(ParallelTxNumFlag.Name) - } - if ctx.GlobalIsSet(ParallelTxQueueSizeFlag.Name) { - core.MaxPendingQueueSize = ctx.GlobalInt(ParallelTxQueueSizeFlag.Name) - } - } func setSmartCard(ctx *cli.Context, cfg *node.Config) { @@ -1666,6 +1656,32 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) { if ctx.GlobalIsSet(RangeLimitFlag.Name) { cfg.RangeLimit = ctx.GlobalBool(RangeLimitFlag.Name) } + if ctx.GlobalIsSet(ParallelTxFlag.Name) { + cfg.ParallelTxMode = ctx.GlobalBool(ParallelTxFlag.Name) + // The best prallel num will be tuned later, we do a simple parallel num set here + numCpu := runtime.NumCPU() + var parallelNum int + if ctx.GlobalIsSet(ParallelTxNumFlag.Name) { + // first of all, we use "--parallel.num", but "--parallel.num 0" is not allowed + parallelNum = ctx.GlobalInt(ParallelTxNumFlag.Name) + if parallelNum < 1 { + parallelNum = 1 + } + } else if numCpu == 1 { + parallelNum = 1 // single CPU core + } else if numCpu < 10 { + parallelNum = numCpu - 1 + } else { + parallelNum = 8 // we found concurrency 8 is slightly better than 15 + } + cfg.ParallelTxNum = parallelNum + // set up queue size, it is an advanced option + if ctx.GlobalIsSet(ParallelTxQueueSizeFlag.Name) { + cfg.ParallelTxQueueSize = ctx.GlobalInt(ParallelTxQueueSizeFlag.Name) + } else { + cfg.ParallelTxQueueSize = 20 // default queue size, will be optimized + } + } // Read the value from the flag no matter if it's set or not. cfg.Preimages = ctx.GlobalBool(CachePreimagesFlag.Name) if cfg.NoPruning && !cfg.Preimages { diff --git a/core/blockchain.go b/core/blockchain.go index 46a420ca94..ff09aeb4f5 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -79,7 +79,6 @@ var ( errInsertionInterrupted = errors.New("insertion is interrupted") errStateRootVerificationFailed = errors.New("state root verification failed") - ParallelTxMode = false // parallel transaction execution ) const ( @@ -241,12 +240,13 @@ type BlockChain struct { running int32 // 0 if chain is running, 1 when stopped procInterrupt int32 // interrupt signaler for block processing - engine consensus.Engine - prefetcher Prefetcher - validator Validator // Block and state validator interface - processor Processor // Block transaction processor interface - vmConfig vm.Config - pipeCommit bool + engine consensus.Engine + prefetcher Prefetcher + validator Validator // Block and state validator interface + processor Processor // Block transaction processor interface + vmConfig vm.Config + pipeCommit bool + parallelExecution bool shouldPreserve func(*types.Block) bool // Function used to determine whether should preserve the given block. terminateInsert func(common.Hash, uint64) bool // Testing hook used to terminate ancient receipt chain insertion. @@ -311,9 +311,6 @@ func NewBlockChain(db ethdb.Database, cacheConfig *CacheConfig, chainConfig *par bc.prefetcher = NewStatePrefetcher(chainConfig, bc, engine) bc.validator = NewBlockValidator(chainConfig, bc, engine) bc.processor = NewStateProcessor(chainConfig, bc, engine) - if ParallelTxMode { - bc.processor.InitParallelOnce() - } var err error bc.hc, err = NewHeaderChain(db, chainConfig, engine, bc.insertStopped) @@ -2114,7 +2111,6 @@ func (bc *BlockChain) insertChain(chain types.Blocks, verifySeals bool) (int, er if err != nil { return it.index, err } - bc.updateHighestVerifiedHeader(block.Header()) // Enable prefetching to pull in trie node paths while processing transactions @@ -2122,7 +2118,7 @@ func (bc *BlockChain) insertChain(chain types.Blocks, verifySeals bool) (int, er var followupInterrupt uint32 // For diff sync, it may fallback to full sync, so we still do prefetch // parallel mode has a pipeline, similar to this prefetch, to save CPU we disable this prefetch for parallel - if !ParallelTxMode { + if !bc.parallelExecution { if len(block.Transactions()) >= prefetchTxNumber { throwaway := statedb.Copy() go func(start time.Time, followup *types.Block, throwaway *state.StateDB, interrupt *uint32) { @@ -2136,16 +2132,7 @@ func (bc *BlockChain) insertChain(chain types.Blocks, verifySeals bool) (int, er statedb.EnablePipeCommit() } statedb.SetExpectedStateRoot(block.Root()) - - var receipts types.Receipts - var logs []*types.Log - var usedGas uint64 - if ParallelTxMode { - statedb, receipts, logs, usedGas, err = bc.processor.ProcessParallel(block, statedb, bc.vmConfig) - } else { - statedb, receipts, logs, usedGas, err = bc.processor.Process(block, statedb, bc.vmConfig) - } - + statedb, receipts, logs, usedGas, err := bc.processor.Process(block, statedb, bc.vmConfig) atomic.StoreUint32(&followupInterrupt, 1) activeState = statedb if err != nil { @@ -3122,3 +3109,16 @@ func EnablePersistDiff(limit uint64) BlockChainOption { return chain } } + +func EnableParallelProcessor(parallelNum int, queueSize int) BlockChainOption { + return func(chain *BlockChain) *BlockChain { + if chain.snaps == nil { + // disable parallel processor if snapshot is not enabled to avoid concurrent issue for SecureTrie + log.Info("parallel processor is not enabled since snapshot is not enabled") + return chain + } + chain.parallelExecution = true + chain.processor = NewParallelStateProcessor(chain.Config(), chain, chain.engine, parallelNum, queueSize) + return chain + } +} diff --git a/core/state/journal.go b/core/state/journal.go index 487e79a57d..b3a2956f75 100644 --- a/core/state/journal.go +++ b/core/state/journal.go @@ -145,7 +145,7 @@ func (ch createObjectChange) revert(s *StateDB) { if s.parallel.isSlotDB { delete(s.parallel.dirtiedStateObjectsInSlot, *ch.account) } else { - s.deleteStateObjectFromStateDB(*ch.account) + s.deleteStateObj(*ch.account) } delete(s.stateObjectsDirty, *ch.account) } diff --git a/core/state/state_object.go b/core/state/state_object.go index ccf0b8266a..ca79da0a74 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -38,8 +38,6 @@ func (c Code) String() string { return string(c) //strings.Join(Disassemble(c), " ") } -type StorageKeys map[common.Hash]struct{} - type Storage map[common.Hash]common.Hash func (s Storage) String() (str string) { diff --git a/core/state/statedb.go b/core/state/statedb.go index 696667cd7b..4c28ae9bdf 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -86,18 +86,17 @@ func (s *StateObjectSyncMap) StoreStateObject(addr common.Address, stateObject * s.Store(addr, stateObject) } -// loadStateObjectFromStateDB is the entry for loading state object from stateObjects in StateDB or stateObjects in parallel -func (s *StateDB) loadStateObjectFromStateDB(addr common.Address) (*StateObject, bool) { +// loadStateObj is the entry for loading state object from stateObjects in StateDB or stateObjects in parallel +func (s *StateDB) loadStateObj(addr common.Address) (*StateObject, bool) { if s.isParallel { return s.parallel.stateObjects.LoadStateObject(addr) - } else { - obj, ok := s.stateObjects[addr] - return obj, ok } + obj, ok := s.stateObjects[addr] + return obj, ok } -// storeStateObjectToStateDB is the entry for storing state object to stateObjects in StateDB or stateObjects in parallel -func (s *StateDB) storeStateObjectToStateDB(addr common.Address, stateObject *StateObject) { +// storeStateObj is the entry for storing state object to stateObjects in StateDB or stateObjects in parallel +func (s *StateDB) storeStateObj(addr common.Address, stateObject *StateObject) { if s.isParallel { s.parallel.stateObjects.Store(addr, stateObject) } else { @@ -105,8 +104,8 @@ func (s *StateDB) storeStateObjectToStateDB(addr common.Address, stateObject *St } } -// deleteStateObjectFromStateDB is the entry for deleting state object to stateObjects in StateDB or stateObjects in parallel -func (s *StateDB) deleteStateObjectFromStateDB(addr common.Address) { +// deleteStateObj is the entry for deleting state object to stateObjects in StateDB or stateObjects in parallel +func (s *StateDB) deleteStateObj(addr common.Address) { if s.isParallel { s.parallel.stateObjects.Delete(addr) } else { @@ -116,7 +115,7 @@ func (s *StateDB) deleteStateObjectFromStateDB(addr common.Address) { // For parallel mode only, keep the change list for later conflict detect type SlotChangeList struct { - TxIndex int // the tx index of change list + TxIndex int StateObjectSuicided map[common.Address]struct{} StateChangeSet map[common.Address]StateKeys BalanceChangeSet map[common.Address]struct{} @@ -132,7 +131,8 @@ type ParallelState struct { // stateObjects holds the state objects in the base slot db // the reason for using stateObjects instead of stateObjects on the outside is // we need a thread safe map to hold state objects since there are many slots will read - // state objects from this and in the same time we will change this when merging slot db to the base slot db + // state objects from it; + // And we will merge all the changes made by the concurrent slot into it. stateObjects *StateObjectSyncMap baseTxIndex int // slotDB is created base on this tx index. @@ -145,7 +145,7 @@ type ParallelState struct { stateReadsInSlot map[common.Address]StateKeys stateChangesInSlot map[common.Address]StateKeys // no need record value // Actions such as SetCode, Suicide will change address's state. - // Later call like Exist(), Empty(), HasSuicided() depond on the address's state. + // Later call like Exist(), Empty(), HasSuicided() depend on the address's state. addrStateReadsInSlot map[common.Address]struct{} addrStateChangesInSlot map[common.Address]struct{} stateObjectsSuicidedInSlot map[common.Address]struct{} @@ -254,10 +254,9 @@ func NewWithSharedPool(root common.Hash, db Database, snaps *snapshot.Tree) (*St return statedb, nil } -// NewSlotDB creates a new slot stateDB base on the provided stateDB. -// With parallel, each execute slot would have its own stateDB. +// NewSlotDB creates a new State DB based on the provided StateDB. +// With parallel, each execution slot would have its own StateDB. func NewSlotDB(db *StateDB, systemAddr common.Address, baseTxIndex int, keepSystem bool) *StateDB { - log.Debug("NewSlotDB", "baseTxIndex", baseTxIndex) slotDB := db.CopyForSlot() slotDB.originalRoot = db.originalRoot slotDB.parallel.baseTxIndex = baseTxIndex @@ -265,8 +264,12 @@ func NewSlotDB(db *StateDB, systemAddr common.Address, baseTxIndex int, keepSyst slotDB.parallel.systemAddressOpsCount = 0 slotDB.parallel.keepSystemAddressBalance = keepSystem - // clear the slotDB's validator's balance first - // for slotDB, systemAddr's value is the tx's gas fee + // All transactions will pay gas fee to the systemAddr at the end, this address is + // deemed to conflict, we handle it specially, clear it now and set it back to the main + // StateDB later; + // But there are transactions that will try to read systemAddr's balance, such as: + // https://bscscan.com/tx/0xcd69755be1d2f55af259441ff5ee2f312830b8539899e82488a21e85bc121a2a. + // It will trigger transaction redo and keepSystem will be marked as true. if !keepSystem { slotDB.SetBalance(systemAddr, big.NewInt(0)) } @@ -317,13 +320,12 @@ func (s *StateDB) getStateObjectFromStateObjects(addr common.Address) (*StateObj return obj, ok } } - return s.loadStateObjectFromStateDB(addr) + return s.loadStateObj(addr) } -// If the transaction execution is failed, keep its read list for conflict detect -// and discard its state changed, execept its own balance change. +// RevertSlotDB keep its read list for conflict detect and discard its state changes except its own balance change, +// if the transaction execution is reverted, func (s *StateDB) RevertSlotDB(from common.Address) { - log.Debug("RevertSlotDB", "addr", from, "txIndex", s.txIndex) s.parallel.stateObjectsSuicidedInSlot = make(map[common.Address]struct{}) s.parallel.stateChangesInSlot = make(map[common.Address]StateKeys) s.parallel.balanceChangesInSlot = make(map[common.Address]struct{}, 1) @@ -332,26 +334,25 @@ func (s *StateDB) RevertSlotDB(from common.Address) { s.parallel.nonceChangesInSlot = make(map[common.Address]struct{}) } -// PrepareForParallel prepares for state db to be used in parallel process. +// PrepareForParallel prepares for state db to be used in parallel execution mode. func (s *StateDB) PrepareForParallel() { s.isParallel = true s.parallel.stateObjects = &StateObjectSyncMap{} } -// MergeSlotDB is for Parallel TX, when the TX is finalized(dirty -> pending) -// A bit similar to StateDB.Copy(), -// mainly copy stateObjects, since slotDB has been finalized. -// return and keep the slot's change list for later conflict detect. +// MergeSlotDB is for Parallel execution mode, when the transaction has been +// finalized(dirty -> pending) on execution slot, the execution results should be +// merged back to the main StateDB. +// And it will return and keep the slot's change list for later conflict detect. func (s *StateDB) MergeSlotDB(slotDb *StateDB, slotReceipt *types.Receipt, txIndex int) SlotChangeList { - // receipt.Logs with unified log Index within a block - // align slotDB's logs Index to the block stateDB's logSize + // receipt.Logs use unified log index within a block + // align slotDB's log index to the block stateDB's logSize for _, l := range slotReceipt.Logs { l.Index += s.logSize } s.logSize += slotDb.logSize - // before merge, do validator reward first: AddBalance to consensus.SystemAddress - // object of SystemAddress is take care specially + // before merge, pay the gas fee first: AddBalance to consensus.SystemAddress systemAddress := slotDb.parallel.systemAddress if slotDb.parallel.keepSystemAddressBalance { s.SetBalance(systemAddress, slotDb.GetBalance(systemAddress)) @@ -373,26 +374,21 @@ func (s *StateDB) MergeSlotDB(slotDb *StateDB, slotReceipt *types.Receipt, txInd // stateObjects: KV, balance, nonce... dirtyObj, ok := slotDb.getStateObjectFromStateObjects(addr) if !ok { - panic(fmt.Sprintf("MergeSlotDB dirty object not exist! (txIndex: %d, addr: %s)", slotDb.txIndex, addr.String())) + log.Error("parallel merge, but dirty object not exist!", "txIndex:", slotDb.txIndex, "addr", addr) + continue } - mainObj, exist := s.loadStateObjectFromStateDB(addr) - - log.Debug("MergeSlotDB", "txIndex", slotDb.txIndex, "addr", addr, - "exist", exist, "dirtyObj.deleted", dirtyObj.deleted) + mainObj, exist := s.loadStateObj(addr) if !exist { // addr not exist on main DB, do ownership transfer dirtyObj.db = s dirtyObj.finalise(true) // true: prefetch on dispatcher - s.storeStateObjectToStateDB(addr, dirtyObj) + s.storeStateObj(addr, dirtyObj) delete(slotDb.parallel.dirtiedStateObjectsInSlot, addr) // transfer ownership } else { // addr already in main DB, do merge: balance, KV, code, State(create, suicide) // can not do copy or ownership transfer directly, since dirtyObj could have outdated // data(may be update within the conflict window) - // Do deepCopy a temporary *StateObject for safety, - // since slot could read the address, dispatch should avoid overwrite the StateObject directly - // otherwise, it could crash for: concurrent map iteration and map write var newMainObj *StateObject if _, created := slotDb.parallel.addrStateChangesInSlot[addr]; created { // there are 3 kinds of state change: @@ -414,21 +410,24 @@ func (s *StateDB) MergeSlotDB(slotDb *StateDB, slotReceipt *types.Receipt, txInd delete(s.snapStorage, addr) } } else { - // do merge: balance, KV, code... + // deepCopy a temporary *StateObject for safety, since slot could read the address, + // dispatch should avoid overwrite the StateObject directly otherwise, it could + // crash for: concurrent map iteration and map write newMainObj = mainObj.deepCopy(s) if _, balanced := slotDb.parallel.balanceChangesInSlot[addr]; balanced { - log.Debug("MergeSlotDB state object merge: state merge: balance", + log.Debug("merge state object: Balance", "newMainObj.Balance()", newMainObj.Balance(), "dirtyObj.Balance()", dirtyObj.Balance()) newMainObj.SetBalance(dirtyObj.Balance()) } if _, coded := slotDb.parallel.codeChangesInSlot[addr]; coded { - log.Debug("MergeSlotDB state object merge: state merge: code") + log.Debug("merge state object: Code") newMainObj.code = dirtyObj.code newMainObj.data.CodeHash = dirtyObj.data.CodeHash newMainObj.dirtyCode = true } if keys, stated := slotDb.parallel.stateChangesInSlot[addr]; stated { + log.Debug("merge state object: KV") newMainObj.MergeSlotObject(s.db, dirtyObj, keys) } // dirtyObj.Nonce() should not be less than newMainObj @@ -436,7 +435,7 @@ func (s *StateDB) MergeSlotDB(slotDb *StateDB, slotReceipt *types.Receipt, txInd } newMainObj.finalise(true) // true: prefetch on dispatcher // update the object - s.storeStateObjectToStateDB(addr, newMainObj) + s.storeStateObj(addr, newMainObj) } addressesToPrefetch = append(addressesToPrefetch, common.CopyBytes(addr[:])) // Copy needed for closure } @@ -457,62 +456,44 @@ func (s *StateDB) MergeSlotDB(slotDb *StateDB, slotReceipt *types.Receipt, txInd s.preimages[hash] = preimage } if s.accessList != nil { - // fixme: accessList is not enabled yet, should use merged rather than overwrite + // fixme: accessList is not enabled yet, but it should use merge rather than overwrite Copy s.accessList = slotDb.accessList.Copy() } if slotDb.snaps != nil { for k := range slotDb.snapDestructs { // There could be a race condition for parallel transaction execution - // One add balance 0 to an empty address, it will delete it(delete empty is enabled,). + // One transaction add balance 0 to an empty address, will delete it(delete empty is enabled). // While another concurrent transaction could add a none-zero balance to it, make it not empty // We fixed it by add a addr state read record for add balance 0 s.snapDestructs[k] = struct{}{} } - for k, v := range slotDb.snapAccounts { - s.snapAccounts[k] = v - } - for k, v := range slotDb.snapStorage { - temp := make(map[string][]byte) - for kk, vv := range v { - temp[kk] = vv - } - s.snapStorage[k] = temp - } + + // slotDb.snapAccounts should be empty, comment out and to be deleted later + // for k, v := range slotDb.snapAccounts { + // s.snapAccounts[k] = v + // } + // slotDb.snapStorage should be empty, comment out and to be deleted later + // for k, v := range slotDb.snapStorage { + // temp := make(map[string][]byte) + // for kk, vv := range v { + // temp[kk] = vv + // } + // s.snapStorage[k] = temp + // } } - // we have to create a new object to store change list for conflict detect, since - // StateDB could be reused and its elements could be overwritten + // to create a new object to store change list for conflict detect, + // since slot db reuse is disabled, we do not need to do copy. changeList := SlotChangeList{ TxIndex: txIndex, - StateObjectSuicided: make(map[common.Address]struct{}, len(slotDb.parallel.stateObjectsSuicidedInSlot)), - StateChangeSet: make(map[common.Address]StateKeys, len(slotDb.parallel.stateChangesInSlot)), - BalanceChangeSet: make(map[common.Address]struct{}, len(slotDb.parallel.balanceChangesInSlot)), - CodeChangeSet: make(map[common.Address]struct{}, len(slotDb.parallel.codeChangesInSlot)), - AddrStateChangeSet: make(map[common.Address]struct{}, len(slotDb.parallel.addrStateChangesInSlot)), - NonceChangeSet: make(map[common.Address]struct{}, len(slotDb.parallel.nonceChangesInSlot)), + StateObjectSuicided: slotDb.parallel.stateObjectsSuicidedInSlot, + StateChangeSet: slotDb.parallel.stateChangesInSlot, + BalanceChangeSet: slotDb.parallel.balanceChangesInSlot, + CodeChangeSet: slotDb.parallel.codeChangesInSlot, + AddrStateChangeSet: slotDb.parallel.addrStateChangesInSlot, + NonceChangeSet: slotDb.parallel.nonceChangesInSlot, } - for addr := range slotDb.parallel.stateObjectsSuicidedInSlot { - changeList.StateObjectSuicided[addr] = struct{}{} - } - for addr, storage := range slotDb.parallel.stateChangesInSlot { - changeList.StateChangeSet[addr] = storage - } - for addr := range slotDb.parallel.balanceChangesInSlot { - changeList.BalanceChangeSet[addr] = struct{}{} - } - for addr := range slotDb.parallel.codeChangesInSlot { - changeList.CodeChangeSet[addr] = struct{}{} - } - for addr := range slotDb.parallel.addrStateChangesInSlot { - changeList.AddrStateChangeSet[addr] = struct{}{} - } - for addr := range slotDb.parallel.nonceChangesInSlot { - changeList.NonceChangeSet[addr] = struct{}{} - } - - // the slot DB's is valid now, move baseTxIndex forward, since it could be reused. - slotDb.parallel.baseTxIndex = txIndex return changeList } @@ -520,9 +501,6 @@ func (s *StateDB) MergeSlotDB(slotDb *StateDB, slotReceipt *types.Receipt, txInd // state trie concurrently while the state is mutated so that when we reach the // commit phase, most of the needed data is already hot. func (s *StateDB) StartPrefetcher(namespace string) { - if s.parallel.isSlotDB { - log.Warn("StartPrefetcher should not be called by slot DB") - } s.prefetcherLock.Lock() defer s.prefetcherLock.Unlock() if s.prefetcher != nil { @@ -537,9 +515,6 @@ func (s *StateDB) StartPrefetcher(namespace string) { // StopPrefetcher terminates a running prefetcher and reports any leftover stats // from the gathered metrics. func (s *StateDB) StopPrefetcher() { - if s.parallel.isSlotDB { - log.Warn("StopPrefetcher should not be called by slot DB") - } s.prefetcherLock.Lock() defer s.prefetcherLock.Unlock() if s.prefetcher != nil { @@ -735,6 +710,12 @@ func (s *StateDB) StateReadsInSlot() map[common.Address]StateKeys { func (s *StateDB) BalanceReadsInSlot() map[common.Address]struct{} { return s.parallel.balanceReadsInSlot } + +// For most of the transactions, systemAddressOpsCount should be 2: +// one for SetBalance(0) on NewSlotDB() +// the other is for AddBalance(GasFee) at the end. +// (systemAddressOpsCount > 2) means the transaction tries to access systemAddress, in +// this case, we should redo and keep its balance on NewSlotDB() func (s *StateDB) SystemAddressRedo() bool { return s.parallel.systemAddressOpsCount > 2 } @@ -876,11 +857,9 @@ func (s *StateDB) HasSuicided(addr common.Address) bool { // AddBalance adds amount to the account associated with addr. func (s *StateDB) AddBalance(addr common.Address, amount *big.Int) { if s.parallel.isSlotDB { - // just in case other tx creates this account, - // we will miss this if we only add this account when found if amount.Sign() != 0 { s.parallel.balanceChangesInSlot[addr] = struct{}{} - // add balance will perform a read operation first, empty object will be deleted + // add balance will perform a read operation first s.parallel.balanceReadsInSlot[addr] = struct{}{} } else { // if amount == 0, no balance change, but there is still an empty check. @@ -899,20 +878,16 @@ func (s *StateDB) AddBalance(addr common.Address, amount *big.Int) { newStateObject := stateObject.deepCopy(s) newStateObject.AddBalance(amount) s.parallel.dirtiedStateObjectsInSlot[addr] = newStateObject - } else { - stateObject.AddBalance(amount) + return } - } else { - stateObject.AddBalance(amount) } + stateObject.AddBalance(amount) } } // SubBalance subtracts amount from the account associated with addr. func (s *StateDB) SubBalance(addr common.Address, amount *big.Int) { if s.parallel.isSlotDB { - // just in case other tx creates this account, - // we will miss this if we only add this account when found if amount.Sign() != 0 { s.parallel.balanceChangesInSlot[addr] = struct{}{} // unlike add, sub 0 balance will not touch empty object @@ -930,12 +905,10 @@ func (s *StateDB) SubBalance(addr common.Address, amount *big.Int) { newStateObject := stateObject.deepCopy(s) newStateObject.SubBalance(amount) s.parallel.dirtiedStateObjectsInSlot[addr] = newStateObject - } else { - stateObject.SubBalance(amount) + return } - } else { - stateObject.SubBalance(amount) } + stateObject.SubBalance(amount) } } @@ -943,20 +916,19 @@ func (s *StateDB) SetBalance(addr common.Address, amount *big.Int) { stateObject := s.GetOrNewStateObject(addr) if stateObject != nil { if s.parallel.isSlotDB { + s.parallel.balanceChangesInSlot[addr] = struct{}{} + if addr == s.parallel.systemAddress { + s.parallel.systemAddressOpsCount++ + } + if _, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; !ok { newStateObject := stateObject.deepCopy(s) newStateObject.SetBalance(amount) s.parallel.dirtiedStateObjectsInSlot[addr] = newStateObject - } else { - stateObject.SetBalance(amount) + return } - s.parallel.balanceChangesInSlot[addr] = struct{}{} - if addr == s.parallel.systemAddress { - s.parallel.systemAddressOpsCount++ - } - } else { - stateObject.SetBalance(amount) } + stateObject.SetBalance(amount) } } @@ -980,12 +952,10 @@ func (s *StateDB) SetNonce(addr common.Address, nonce uint64) { newStateObject := stateObject.deepCopy(s) newStateObject.SetNonce(nonce) s.parallel.dirtiedStateObjectsInSlot[addr] = newStateObject - } else { - stateObject.SetNonce(nonce) + return } - } else { - stateObject.SetNonce(nonce) } + stateObject.SetNonce(nonce) } } @@ -993,18 +963,16 @@ func (s *StateDB) SetCode(addr common.Address, code []byte) { stateObject := s.GetOrNewStateObject(addr) if stateObject != nil { if s.parallel.isSlotDB { + s.parallel.codeChangesInSlot[addr] = struct{}{} + if _, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; !ok { newStateObject := stateObject.deepCopy(s) newStateObject.SetCode(crypto.Keccak256Hash(code), code) s.parallel.dirtiedStateObjectsInSlot[addr] = newStateObject - } else { - stateObject.SetCode(crypto.Keccak256Hash(code), code) + return } - - s.parallel.codeChangesInSlot[addr] = struct{}{} - } else { - stateObject.SetCode(crypto.Keccak256Hash(code), code) } + stateObject.SetCode(crypto.Keccak256Hash(code), code) } } @@ -1021,21 +989,20 @@ func (s *StateDB) SetState(addr common.Address, key, value common.Hash) { return } } - if _, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; !ok { - newStateObject := stateObject.deepCopy(s) - newStateObject.SetState(s.db, key, value) - s.parallel.dirtiedStateObjectsInSlot[addr] = newStateObject - } else { - stateObject.SetState(s.db, key, value) - } if s.parallel.stateChangesInSlot[addr] == nil { s.parallel.stateChangesInSlot[addr] = make(StateKeys, defaultNumOfSlots) } s.parallel.stateChangesInSlot[addr][key] = struct{}{} - } else { - stateObject.SetState(s.db, key, value) + + if _, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; !ok { + newStateObject := stateObject.deepCopy(s) + newStateObject.SetState(s.db, key, value) + s.parallel.dirtiedStateObjectsInSlot[addr] = newStateObject + return + } } + stateObject.SetState(s.db, key, value) } } @@ -1055,10 +1022,6 @@ func (s *StateDB) SetStorage(addr common.Address, storage map[common.Hash]common // getStateObject will return a non-nil account after Suicide. func (s *StateDB) Suicide(addr common.Address) bool { stateObject := s.getStateObject(addr) - - if s.parallel.isSlotDB { - s.parallel.addrStateReadsInSlot[addr] = struct{}{} - } if stateObject == nil { return false } @@ -1078,15 +1041,12 @@ func (s *StateDB) Suicide(addr common.Address) bool { newStateObject.markSuicided() newStateObject.data.Balance = new(big.Int) s.parallel.dirtiedStateObjectsInSlot[addr] = newStateObject - } else { - stateObject.markSuicided() - stateObject.data.Balance = new(big.Int) + return true } - } else { - stateObject.markSuicided() - stateObject.data.Balance = new(big.Int) } + stateObject.markSuicided() + stateObject.data.Balance = new(big.Int) return true } @@ -1216,7 +1176,7 @@ func (s *StateDB) SetStateObject(object *StateObject) { if s.parallel.isSlotDB { s.parallel.dirtiedStateObjectsInSlot[object.Address()] = object } else { - s.storeStateObjectToStateDB(object.Address(), object) + s.storeStateObj(object.Address(), object) } } @@ -1233,8 +1193,8 @@ func (s *StateDB) GetOrNewStateObject(addr common.Address) *StateObject { // the given address, it is overwritten and returned as the second return value. func (s *StateDB) createObject(addr common.Address) (newobj, prev *StateObject) { if s.parallel.isSlotDB { - s.parallel.addrStateReadsInSlot[addr] = struct{}{} // fixme: may not necessary - s.parallel.addrStateChangesInSlot[addr] = struct{}{} // address created. + s.parallel.addrStateReadsInSlot[addr] = struct{}{} // will try to get the previous object. + s.parallel.addrStateChangesInSlot[addr] = struct{}{} } prev = s.getDeletedStateObject(addr) // Note, prev might have been deleted, we need that! @@ -1243,8 +1203,8 @@ func (s *StateDB) createObject(addr common.Address) (newobj, prev *StateObject) if s.snap != nil && prev != nil { _, prevdestruct = s.snapDestructs[prev.address] if !prevdestruct { - // createObject for deleted object is ok, - // it will destroy the previous trie node and update with the new object on block commit + // createObject for deleted object will destroy the previous trie node first + // and update the trie tree with the new object on block commit. s.snapDestructs[prev.address] = struct{}{} } } @@ -1275,10 +1235,10 @@ func (s *StateDB) createObject(addr common.Address) (newobj, prev *StateObject) func (s *StateDB) CreateAccount(addr common.Address) { newObj, prev := s.createObject(addr) if prev != nil { - newObj.setBalance(prev.data.Balance) // this read + newObj.setBalance(prev.data.Balance) } if s.parallel.isSlotDB { - s.parallel.balanceReadsInSlot[addr] = struct{}{} + s.parallel.balanceReadsInSlot[addr] = struct{}{} // read the balance of previous object s.parallel.dirtiedStateObjectsInSlot[addr] = newObj } } @@ -1316,10 +1276,6 @@ func (s *StateDB) ForEachStorage(addr common.Address, cb func(key, value common. // Snapshots of the copied state cannot be applied to the copy. func (s *StateDB) Copy() *StateDB { // Copy all the basic fields, initialize the memory ones - parallel := ParallelState{ - isSlotDB: false, - } - state := &StateDB{ db: s.db, trie: s.db.CopyTrie(s.trie), @@ -1333,7 +1289,7 @@ func (s *StateDB) Copy() *StateDB { preimages: make(map[common.Hash][]byte, len(s.preimages)), journal: newJournal(), hasher: crypto.NewKeccakState(), - parallel: parallel, + parallel: ParallelState{}, } // Copy the dirty states, logs, and preimages for addr := range s.journal.dirties { @@ -1345,7 +1301,7 @@ func (s *StateDB) Copy() *StateDB { // Even though the original object is dirty, we are not copying the journal, // so we need to make sure that anyside effect the journal would have caused // during a commit (or similar op) is already applied to the copy. - state.storeStateObjectToStateDB(addr, object.deepCopy(state)) + state.storeStateObj(addr, object.deepCopy(state)) state.stateObjectsDirty[addr] = struct{}{} // Mark the copy dirty to force internal (code/state) commits state.stateObjectsPending[addr] = struct{}{} // Mark the copy pending to force external (account) commits @@ -1357,14 +1313,14 @@ func (s *StateDB) Copy() *StateDB { for addr := range s.stateObjectsPending { if _, exist := state.getStateObjectFromStateObjects(addr); !exist { object, _ := s.getStateObjectFromStateObjects(addr) - state.storeStateObjectToStateDB(addr, object.deepCopy(state)) + state.storeStateObj(addr, object.deepCopy(state)) } state.stateObjectsPending[addr] = struct{}{} } for addr := range s.stateObjectsDirty { if _, exist := state.getStateObjectFromStateObjects(addr); !exist { object, _ := s.getStateObjectFromStateObjects(addr) - state.storeStateObjectToStateDB(addr, object.deepCopy(state)) + state.storeStateObj(addr, object.deepCopy(state)) } state.stateObjectsDirty[addr] = struct{}{} } @@ -1422,32 +1378,32 @@ func (s *StateDB) Copy() *StateDB { return state } +// Copy all the basic fields, initialize the memory ones func (s *StateDB) CopyForSlot() *StateDB { - // Copy all the basic fields, initialize the memory ones parallel := ParallelState{ - // Share base slot db's stateObjects + // use base(dispatcher) slot db's stateObjects. // It is a SyncMap, only readable to slot, not writable stateObjects: s.parallel.stateObjects, - stateObjectsSuicidedInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), + stateObjectsSuicidedInSlot: make(map[common.Address]struct{}, 10), codeReadsInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), - codeChangesInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), + codeChangesInSlot: make(map[common.Address]struct{}, 10), stateChangesInSlot: make(map[common.Address]StateKeys, defaultNumOfSlots), stateReadsInSlot: make(map[common.Address]StateKeys, defaultNumOfSlots), balanceChangesInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), balanceReadsInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), addrStateReadsInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), - addrStateChangesInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), - nonceChangesInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), + addrStateChangesInSlot: make(map[common.Address]struct{}, 10), + nonceChangesInSlot: make(map[common.Address]struct{}, 10), isSlotDB: true, dirtiedStateObjectsInSlot: make(map[common.Address]*StateObject, defaultNumOfSlots), } state := &StateDB{ db: s.db, trie: s.db.CopyTrie(s.trie), - stateObjects: make(map[common.Address]*StateObject, defaultNumOfSlots), + stateObjects: make(map[common.Address]*StateObject), // replaced by parallel.stateObjects in parallel mode stateObjectsPending: make(map[common.Address]struct{}, defaultNumOfSlots), stateObjectsDirty: make(map[common.Address]struct{}, defaultNumOfSlots), - refund: s.refund, + refund: s.refund, // should be 0 logs: make(map[common.Hash][]*types.Log, defaultNumOfSlots), logSize: 0, preimages: make(map[common.Hash][]byte, len(s.preimages)), @@ -1476,6 +1432,7 @@ func (s *StateDB) CopyForSlot() *StateDB { for k, v := range s.snapDestructs { state.snapDestructs[k] = v } + // state.snapAccounts = make(map[common.Address][]byte) for k, v := range s.snapAccounts { state.snapAccounts[k] = v diff --git a/core/state_processor.go b/core/state_processor.go index af6a752f8b..1f56cd44a2 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -24,7 +24,6 @@ import ( "math/rand" "runtime" "sync" - "sync/atomic" "time" "github.com/ethereum/go-ethereum/common" @@ -50,9 +49,6 @@ const ( farDiffLayerTimeout = 2 ) -var MaxPendingQueueSize = 20 // parallel slot's maximum number of pending Txs -var ParallelExecNum = runtime.NumCPU() - 1 // leave a CPU to dispatcher - // StateProcessor is a basic Processor, which takes care of transitioning // state from one point to another. // @@ -61,17 +57,8 @@ type StateProcessor struct { config *params.ChainConfig // Chain configuration options bc *BlockChain // Canonical block chain engine consensus.Engine // Consensus engine used for block rewards - - // add for parallel execute - paraInitialized int32 - paraTxResultChan chan *ParallelTxResult // to notify dispatcher that a tx is done - slotState []*SlotState // idle, or pending messages - mergedTxIndex int // the latest finalized tx index - debugErrorRedoNum int - debugConflictRedoNum int } -// NewStateProcessor initialises a new StateProcessor. func NewStateProcessor(config *params.ChainConfig, bc *BlockChain, engine consensus.Engine) *StateProcessor { return &StateProcessor{ config: config, @@ -80,6 +67,28 @@ func NewStateProcessor(config *params.ChainConfig, bc *BlockChain, engine consen } } +// add for parallel executions +type ParallelStateProcessor struct { + StateProcessor + parallelNum int // leave a CPU to dispatcher + queueSize int // parallel slot's maximum number of pending Txs + txResultChan chan *ParallelTxResult // to notify dispatcher that a tx is done + slotState []*SlotState // idle, or pending messages + mergedTxIndex int // the latest finalized tx index + debugErrorRedoNum int + debugConflictRedoNum int +} + +func NewParallelStateProcessor(config *params.ChainConfig, bc *BlockChain, engine consensus.Engine, parallelNum int, queueSize int) *ParallelStateProcessor { + processor := &ParallelStateProcessor{ + StateProcessor: *NewStateProcessor(config, bc, engine), + parallelNum: parallelNum, + queueSize: queueSize, + } + processor.init() + return processor +} + type LightStateProcessor struct { check int64 StateProcessor @@ -389,65 +398,55 @@ type SlotState struct { pendingTxReqList []*ParallelTxRequest // maintained by dispatcher for dispatch policy mergedChangeList []state.SlotChangeList slotdbChan chan *state.StateDB // dispatch will create and send this slotDB to slot - // conflict check uses conflict window - // conflict check will check all state changes from (cfWindowStart + 1) to the previous Tx } type ParallelTxResult struct { - redo bool // for redo, dispatch will wait new tx result - updateSlotDB bool // for redo and pending tx quest, slot needs new slotDB, - keepSystem bool // for redo, should keep system address's balance - txIndex int + updateSlotDB bool // for redo and pending tx quest, slot needs new slotDB, + keepSystem bool // for redo, should keep system address's balance slotIndex int // slot index err error // to describe error message? - tx *types.Transaction txReq *ParallelTxRequest receipt *types.Receipt - slotDB *state.StateDB + slotDB *state.StateDB // if updated, it is not equal to txReq.slotDB } type ParallelTxRequest struct { - txIndex int - tx *types.Transaction - slotDB *state.StateDB - gp *GasPool - msg types.Message - block *types.Block - vmConfig vm.Config - bloomProcessors *AsyncReceiptBloomGenerator - usedGas *uint64 - waitTxChan chan int // "int" represents the tx index - curTxChan chan int // "int" represents the tx index + txIndex int + tx *types.Transaction + slotDB *state.StateDB + gasLimit uint64 + msg types.Message + block *types.Block + vmConfig vm.Config + bloomProcessor *AsyncReceiptBloomGenerator + usedGas *uint64 + waitTxChan chan struct{} + curTxChan chan struct{} } -func (p *StateProcessor) InitParallelOnce() { - // to create and start the execution slot goroutines - if !atomic.CompareAndSwapInt32(&p.paraInitialized, 0, 1) { // not swapped means already initialized. - return - } - log.Info("Parallel execution mode is used and initialized", "Parallel Num", ParallelExecNum) - p.paraTxResultChan = make(chan *ParallelTxResult, ParallelExecNum) // fixme: use blocked chan? - p.slotState = make([]*SlotState, ParallelExecNum) - - wg := sync.WaitGroup{} // make sure all goroutines are created and started - for i := 0; i < ParallelExecNum; i++ { - p.slotState[i] = new(SlotState) - p.slotState[i].slotdbChan = make(chan *state.StateDB, 1) - p.slotState[i].pendingTxReqChan = make(chan *ParallelTxRequest, MaxPendingQueueSize) - - wg.Add(1) +// to create and start the execution slot goroutines +func (p *ParallelStateProcessor) init() { + log.Info("Parallel execution mode is enabled", "Parallel Num", p.parallelNum, + "CPUNum", runtime.NumCPU(), + "QueueSize", p.queueSize) + p.txResultChan = make(chan *ParallelTxResult, p.parallelNum) + p.slotState = make([]*SlotState, p.parallelNum) + + for i := 0; i < p.parallelNum; i++ { + p.slotState[i] = &SlotState{ + slotdbChan: make(chan *state.StateDB, 1), + pendingTxReqChan: make(chan *ParallelTxRequest, p.queueSize), + } // start the slot's goroutine go func(slotIndex int) { - wg.Done() p.runSlotLoop(slotIndex) // this loop will be permanent live - log.Error("runSlotLoop exit!", "Slot", slotIndex) }(i) } - wg.Wait() } -// if any state in readDb is updated in changeList, then it has state conflict -func (p *StateProcessor) hasStateConflict(readDb *state.StateDB, changeList state.SlotChangeList) bool { +// conflict check uses conflict window, it will check all state changes from (cfWindowStart + 1) +// to the previous Tx, if any state in readDb is updated in changeList, then it is conflicted +func (p *ParallelStateProcessor) hasStateConflict(readDb *state.StateDB, changeList state.SlotChangeList) bool { // check KV change reads := readDb.StateReadsInSlot() writes := changeList.StateChangeSet @@ -525,7 +524,7 @@ func (p *StateProcessor) hasStateConflict(readDb *state.StateDB, changeList stat // for parallel execute, we put contracts of same address in a slot, // since these txs probably would have conflicts -func (p *StateProcessor) queueSameToAddress(txReq *ParallelTxRequest) bool { +func (p *ParallelStateProcessor) queueSameToAddress(txReq *ParallelTxRequest) bool { txToAddr := txReq.tx.To() // To() == nil means contract creation, no same To address if txToAddr == nil { @@ -560,7 +559,7 @@ func (p *StateProcessor) queueSameToAddress(txReq *ParallelTxRequest) bool { // for parallel execute, we put contracts of same address in a slot, // since these txs probably would have conflicts -func (p *StateProcessor) queueSameFromAddress(txReq *ParallelTxRequest) bool { +func (p *ParallelStateProcessor) queueSameFromAddress(txReq *ParallelTxRequest) bool { txFromAddr := txReq.msg.From() for i, slot := range p.slotState { if slot.tailTxReq == nil { // this slot is idle @@ -586,7 +585,7 @@ func (p *StateProcessor) queueSameFromAddress(txReq *ParallelTxRequest) bool { } // if there is idle slot, dispatch the msg to the first idle slot -func (p *StateProcessor) dispatchToIdleSlot(statedb *state.StateDB, txReq *ParallelTxRequest) bool { +func (p *ParallelStateProcessor) dispatchToIdleSlot(statedb *state.StateDB, txReq *ParallelTxRequest) bool { for i, slot := range p.slotState { if slot.tailTxReq == nil { if len(slot.mergedChangeList) == 0 { @@ -604,14 +603,14 @@ func (p *StateProcessor) dispatchToIdleSlot(statedb *state.StateDB, txReq *Paral } // wait until the next Tx is executed and its result is merged to the main stateDB -func (p *StateProcessor) waitUntilNextTxDone(statedb *state.StateDB) *ParallelTxResult { +func (p *ParallelStateProcessor) waitUntilNextTxDone(statedb *state.StateDB, gp *GasPool) *ParallelTxResult { var result *ParallelTxResult for { - result = <-p.paraTxResultChan - // slot may request new slotDB, if it think its slotDB is outdated + result = <-p.txResultChan + // slot may request new slotDB, if slotDB is outdated // such as: // tx in pending tx request, previous tx in same queue is likely "damaged" the slotDB - // tx redo for confict + // tx redo for conflict // tx stage 1 failed, nonce out of order... if result.updateSlotDB { // the target slot is waiting for new slotDB @@ -620,15 +619,17 @@ func (p *StateProcessor) waitUntilNextTxDone(statedb *state.StateDB) *ParallelTx slotState.slotdbChan <- slotDB continue } - if result.redo { - // wait result of redo - continue - } // ok, the tx result is valid and can be merged break } + + if err := gp.SubGas(result.receipt.GasUsed); err != nil { + log.Error("gas limit reached", "block", result.txReq.block.Number(), + "txIndex", result.txReq.txIndex, "GasUsed", result.receipt.GasUsed, "gp.Gas", gp.Gas()) + } + resultSlotIndex := result.slotIndex - resultTxIndex := result.txIndex + resultTxIndex := result.txReq.txIndex resultSlotState := p.slotState[resultSlotIndex] resultSlotState.pendingTxReqList = resultSlotState.pendingTxReqList[1:] if resultSlotState.tailTxReq.txIndex == resultTxIndex { @@ -643,29 +644,28 @@ func (p *StateProcessor) waitUntilNextTxDone(statedb *state.StateDB) *ParallelTx resultSlotState.mergedChangeList = append(resultSlotState.mergedChangeList, changeList) if resultTxIndex != p.mergedTxIndex+1 { - log.Warn("ProcessParallel tx result out of order", "resultTxIndex", resultTxIndex, + log.Error("ProcessParallel tx result out of order", "resultTxIndex", resultTxIndex, "p.mergedTxIndex", p.mergedTxIndex) - panic("ProcessParallel tx result out of order") } p.mergedTxIndex = resultTxIndex // notify the following Tx, it is merged, - // fixme: what if no wait or next tx is in same slot? - result.txReq.curTxChan <- resultTxIndex + // todo(optimize): if next tx is in same slot, it do not need to wait; save this channel cost. + close(result.txReq.curTxChan) return result } -func (p *StateProcessor) execInParallelSlot(slotIndex int, txReq *ParallelTxRequest) *ParallelTxResult { +func (p *ParallelStateProcessor) execInSlot(slotIndex int, txReq *ParallelTxRequest) *ParallelTxResult { txIndex := txReq.txIndex tx := txReq.tx slotDB := txReq.slotDB - gp := txReq.gp // goroutine unsafe + slotGasLimit := txReq.gasLimit // not accurate, but it is ok for block import. msg := txReq.msg block := txReq.block header := block.Header() cfg := txReq.vmConfig - bloomProcessors := txReq.bloomProcessors + bloomProcessor := txReq.bloomProcessor - blockContext := NewEVMBlockContext(header, p.bc, nil) // fixme: share blockContext within a block? + blockContext := NewEVMBlockContext(header, p.bc, nil) // can share blockContext within a block for efficiency vmenv := vm.NewEVM(blockContext, vm.TxContext{}, slotDB, p.config, cfg) var receipt *types.Receipt @@ -676,7 +676,6 @@ func (p *StateProcessor) execInParallelSlot(slotIndex int, txReq *ParallelTxRequ slotDB.Prepare(tx.Hash(), block.Hash(), txIndex) log.Debug("exec In Slot", "Slot", slotIndex, "txIndex", txIndex, "slotDB.baseTxIndex", slotDB.BaseTxIndex()) - slotGasLimit := gp.Gas() gpSlot := new(GasPool).AddGas(slotGasLimit) // each slot would use its own gas pool, and will do gaslimit check later evm, result, err = applyTransactionStageExecution(msg, gpSlot, slotDB, vmenv) log.Debug("Stage Execution done", "Slot", slotIndex, "txIndex", txIndex, "slotDB.baseTxIndex", slotDB.BaseTxIndex()) @@ -684,58 +683,43 @@ func (p *StateProcessor) execInParallelSlot(slotIndex int, txReq *ParallelTxRequ // wait until the previous tx is finalized. if txReq.waitTxChan != nil { log.Debug("Stage wait previous Tx done", "Slot", slotIndex, "txIndex", txIndex) - waitTxIndex := <-txReq.waitTxChan - if waitTxIndex != txIndex-1 { - log.Error("Stage wait tx index mismatch", "expect", txIndex-1, "actual", waitTxIndex) - panic(fmt.Sprintf("wait tx index mismatch expect:%d, actual:%d", txIndex-1, waitTxIndex)) - } + <-txReq.waitTxChan // close the channel } - // in parallel, tx can run into trouble - // for example: err="nonce too high" - // in this case, we will do re-run. + // in parallel mode, tx can run into trouble, for example: err="nonce too high" + // in these cases, we will wait and re-run. if err != nil { p.debugErrorRedoNum++ log.Debug("Stage Execution err", "Slot", slotIndex, "txIndex", txIndex, "current slotDB.baseTxIndex", slotDB.BaseTxIndex(), "err", err) redoResult := &ParallelTxResult{ - redo: true, updateSlotDB: true, - txIndex: txIndex, slotIndex: slotIndex, - tx: tx, txReq: txReq, receipt: receipt, err: err, } - p.paraTxResultChan <- redoResult + p.txResultChan <- redoResult slotDB = <-p.slotState[slotIndex].slotdbChan slotDB.Prepare(tx.Hash(), block.Hash(), txIndex) - // vmenv.Reset(vm.TxContext{}, slotDB) log.Debug("Stage Execution get new slotdb to redo", "Slot", slotIndex, "txIndex", txIndex, "new slotDB.baseTxIndex", slotDB.BaseTxIndex()) - slotGasLimit = gp.Gas() gpSlot = new(GasPool).AddGas(slotGasLimit) evm, result, err = applyTransactionStageExecution(msg, gpSlot, slotDB, vmenv) if err != nil { - panic(fmt.Sprintf("Stage Execution redo, error %v", err)) + log.Error("Stage Execution redo, error", err) } } - // fixme: - // parallel mode can not precheck, - // precheck should be replace by postCheck when previous Tx is finalized - // do conflict detect hasConflict := false systemAddrConflict := false - log.Debug("Stage Execution done, do conflict check", "Slot", slotIndex, "txIndex", txIndex) if slotDB.SystemAddressRedo() { hasConflict = true systemAddrConflict = true } else { - for index := 0; index < ParallelExecNum; index++ { + for index := 0; index < p.parallelNum; index++ { if index == slotIndex { continue } @@ -743,7 +727,6 @@ func (p *StateProcessor) execInParallelSlot(slotIndex int, txReq *ParallelTxRequ // check all finalizedDb from current slot's for _, changeList := range p.slotState[index].mergedChangeList { if changeList.TxIndex <= slotDB.BaseTxIndex() { - // log.Debug("skip finalized DB which is out of the conflict window", "finDb.txIndex", finDb.txIndex, "slotDB.baseTxIndex", slotDB.baseTxIndex) continue } if p.hasStateConflict(slotDB, changeList) { @@ -764,25 +747,20 @@ func (p *StateProcessor) execInParallelSlot(slotIndex int, txReq *ParallelTxRequ p.debugConflictRedoNum++ // re-run should not have conflict, since it has the latest world state. redoResult := &ParallelTxResult{ - redo: true, updateSlotDB: true, keepSystem: systemAddrConflict, - txIndex: txIndex, slotIndex: slotIndex, - tx: tx, txReq: txReq, receipt: receipt, err: err, } - p.paraTxResultChan <- redoResult + p.txResultChan <- redoResult slotDB = <-p.slotState[slotIndex].slotdbChan slotDB.Prepare(tx.Hash(), block.Hash(), txIndex) - // vmenv.Reset(vm.TxContext{}, slotDB) - slotGasLimit = gp.Gas() gpSlot = new(GasPool).AddGas(slotGasLimit) evm, result, err = applyTransactionStageExecution(msg, gpSlot, slotDB, vmenv) if err != nil { - panic(fmt.Sprintf("Stage Execution conflict redo, error %v", err)) + log.Error("Stage Execution conflict redo, error", err) } } @@ -791,18 +769,12 @@ func (p *StateProcessor) execInParallelSlot(slotIndex int, txReq *ParallelTxRequ if gasConsumed != result.UsedGas { log.Error("gasConsumed != result.UsedGas mismatch", "gasConsumed", gasConsumed, "result.UsedGas", result.UsedGas) - panic(fmt.Sprintf("gas consume mismatch, consumed:%d, result.UsedGas:%d", gasConsumed, result.UsedGas)) - } - - if err := gp.SubGas(gasConsumed); err != nil { - log.Error("gas limit reached", "gasConsumed", gasConsumed, "gp", gp.Gas()) - panic(fmt.Sprintf("gas limit reached, gasConsumed:%d, gp.Gas():%d", gasConsumed, gp.Gas())) } log.Debug("ok to finalize this TX", "Slot", slotIndex, "txIndex", txIndex, "result.UsedGas", result.UsedGas, "txReq.usedGas", *txReq.usedGas) // ok, time to do finalize, stage2 should not be parallel - receipt, err = applyTransactionStageFinalization(evm, result, msg, p.config, slotDB, header, tx, txReq.usedGas, bloomProcessors) + receipt, err = applyTransactionStageFinalization(evm, result, msg, p.config, slotDB, header, tx, txReq.usedGas, bloomProcessor) if result.Failed() { // if Tx is reverted, all its state change will be discarded @@ -811,11 +783,8 @@ func (p *StateProcessor) execInParallelSlot(slotIndex int, txReq *ParallelTxRequ } return &ParallelTxResult{ - redo: false, updateSlotDB: false, - txIndex: txIndex, slotIndex: slotIndex, - tx: tx, txReq: txReq, receipt: receipt, slotDB: slotDB, @@ -823,10 +792,9 @@ func (p *StateProcessor) execInParallelSlot(slotIndex int, txReq *ParallelTxRequ } } -func (p *StateProcessor) runSlotLoop(slotIndex int) { +func (p *ParallelStateProcessor) runSlotLoop(slotIndex int) { curSlot := p.slotState[slotIndex] for { - // log.Info("parallel slot waiting", "Slot", slotIndex) // wait for new TxReq txReq := <-curSlot.pendingTxReqChan // receive a dispatched message @@ -839,22 +807,21 @@ func (p *StateProcessor) runSlotLoop(slotIndex int) { // it is better to create a new SlotDB, since COW is used. if txReq.slotDB == nil { result := &ParallelTxResult{ - redo: false, updateSlotDB: true, slotIndex: slotIndex, err: nil, } - p.paraTxResultChan <- result + p.txResultChan <- result txReq.slotDB = <-curSlot.slotdbChan } - result := p.execInParallelSlot(slotIndex, txReq) + result := p.execInSlot(slotIndex, txReq) log.Debug("SlotLoop the TxReq is done", "Slot", slotIndex, "err", result.err) - p.paraTxResultChan <- result + p.txResultChan <- result } } // clear slot state for each block. -func (p *StateProcessor) resetParallelState(txNum int, statedb *state.StateDB) { +func (p *ParallelStateProcessor) resetState(txNum int, statedb *state.StateDB) { if txNum == 0 { return } @@ -871,6 +838,117 @@ func (p *StateProcessor) resetParallelState(txNum int, statedb *state.StateDB) { } } +// Implement BEP-130: Parallel Transaction Execution. +func (p *ParallelStateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg vm.Config) (*state.StateDB, types.Receipts, []*types.Log, uint64, error) { + var ( + usedGas = new(uint64) + header = block.Header() + gp = new(GasPool).AddGas(block.GasLimit()) + ) + var receipts = make([]*types.Receipt, 0) + txNum := len(block.Transactions()) + p.resetState(txNum, statedb) + + // Iterate over and process the individual transactions + posa, isPoSA := p.engine.(consensus.PoSA) + commonTxs := make([]*types.Transaction, 0, txNum) + // usually do have two tx, one for validator set contract, another for system reward contract. + systemTxs := make([]*types.Transaction, 0, 2) + + signer, _, bloomProcessor := p.preExecute(block, statedb, cfg, true) + var waitTxChan, curTxChan chan struct{} + for i, tx := range block.Transactions() { + if isPoSA { + if isSystemTx, err := posa.IsSystemTransaction(tx, block.Header()); err != nil { + bloomProcessor.Close() + return statedb, nil, nil, 0, err + } else if isSystemTx { + systemTxs = append(systemTxs, tx) + continue + } + } + + // can be moved it into slot for efficiency, but signer is not concurrent safe + msg, err := tx.AsMessage(signer) + if err != nil { + bloomProcessor.Close() + return statedb, nil, nil, 0, err + } + + // parallel start, wrap an exec message, which will be dispatched to a slot + waitTxChan = curTxChan // can be nil, if this is the tx of first batch, otherwise, it is previous Tx's wait channel + curTxChan = make(chan struct{}, 1) + + txReq := &ParallelTxRequest{ + txIndex: i, + tx: tx, + slotDB: nil, + gasLimit: gp.Gas(), + msg: msg, + block: block, + vmConfig: cfg, + bloomProcessor: bloomProcessor, + usedGas: usedGas, + waitTxChan: waitTxChan, + curTxChan: curTxChan, + } + + // to optimize the for { for {} } loop code style? it is ok right now. + for { + if p.queueSameFromAddress(txReq) { + break + } + + if p.queueSameToAddress(txReq) { + break + } + // if idle slot available, just dispatch and process next tx. + if p.dispatchToIdleSlot(statedb, txReq) { + break + } + log.Debug("ProcessParallel no slot available, wait", "txIndex", txReq.txIndex) + // no idle slot, wait until a tx is executed and merged. + result := p.waitUntilNextTxDone(statedb, gp) + + // update tx result + if result.err != nil { + log.Warn("ProcessParallel a failed tx", "resultSlotIndex", result.slotIndex, + "resultTxIndex", result.txReq.txIndex, "result.err", result.err) + bloomProcessor.Close() + return statedb, nil, nil, 0, fmt.Errorf("could not apply tx %d [%v]: %w", result.txReq.txIndex, result.txReq.tx.Hash().Hex(), result.err) + } + + commonTxs = append(commonTxs, result.txReq.tx) + receipts = append(receipts, result.receipt) + } + } + + // wait until all tx request are done + for len(commonTxs)+len(systemTxs) < txNum { + result := p.waitUntilNextTxDone(statedb, gp) + // update tx result + if result.err != nil { + log.Warn("ProcessParallel a failed tx", "resultSlotIndex", result.slotIndex, + "resultTxIndex", result.txReq.txIndex, "result.err", result.err) + return statedb, nil, nil, 0, fmt.Errorf("could not apply tx %d [%v]: %w", result.txReq.txIndex, result.txReq.tx.Hash().Hex(), result.err) + } + commonTxs = append(commonTxs, result.txReq.tx) + receipts = append(receipts, result.receipt) + } + + // len(commonTxs) could be 0, such as: https://bscscan.com/block/14580486 + if len(commonTxs) > 0 { + log.Info("ProcessParallel tx all done", "block", header.Number, "usedGas", *usedGas, + "txNum", txNum, + "len(commonTxs)", len(commonTxs), + "errorNum", p.debugErrorRedoNum, + "conflictNum", p.debugConflictRedoNum, + "redoRate(%)", 100*(p.debugErrorRedoNum+p.debugConflictRedoNum)/len(commonTxs)) + } + allLogs, err := p.postExecute(block, statedb, &commonTxs, &receipts, &systemTxs, usedGas, bloomProcessor) + return statedb, receipts, allLogs, *usedGas, err +} + // Before transactions are executed, do shared preparation for Process() & ProcessParallel() func (p *StateProcessor) preExecute(block *types.Block, statedb *state.StateDB, cfg vm.Config, parallel bool) (types.Signer, *vm.EVM, *AsyncReceiptBloomGenerator) { signer := types.MakeSigner(p.bc.chainConfig, block.Number()) @@ -881,25 +959,25 @@ func (p *StateProcessor) preExecute(block *types.Block, statedb *state.StateDB, // Handle upgrade build-in system contract code systemcontracts.UpgradeBuildInSystemContract(p.config, block.Number(), statedb) - blockContext := NewEVMBlockContext(block.Header(), p.bc, nil) // with parallel mode, vmenv will be created inside of slot var vmenv *vm.EVM if !parallel { + blockContext := NewEVMBlockContext(block.Header(), p.bc, nil) vmenv = vm.NewEVM(blockContext, vm.TxContext{}, statedb, p.config, cfg) } // initialise bloom processors - bloomProcessors := NewAsyncReceiptBloomGenerator(len(block.Transactions())) + bloomProcessor := NewAsyncReceiptBloomGenerator(len(block.Transactions())) statedb.MarkFullProcessed() - return signer, vmenv, bloomProcessors + return signer, vmenv, bloomProcessor } func (p *StateProcessor) postExecute(block *types.Block, statedb *state.StateDB, commonTxs *[]*types.Transaction, - receipts *[]*types.Receipt, systemTxs *[]*types.Transaction, usedGas *uint64, bloomProcessors *AsyncReceiptBloomGenerator) ([]*types.Log, error) { - var allLogs []*types.Log + receipts *[]*types.Receipt, systemTxs *[]*types.Transaction, usedGas *uint64, bloomProcessor *AsyncReceiptBloomGenerator) ([]*types.Log, error) { + allLogs := make([]*types.Log, 0, len(*receipts)) - bloomProcessors.Close() + bloomProcessor.Close() // Finalize the block, applying any consensus engine specific extras (e.g. block rewards) err := p.engine.Finalize(p.bc, block.Header(), statedb, commonTxs, block.Uncles(), receipts, systemTxs, usedGas) @@ -933,11 +1011,11 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg // usually do have two tx, one for validator set contract, another for system reward contract. systemTxs := make([]*types.Transaction, 0, 2) - signer, vmenv, bloomProcessors := p.preExecute(block, statedb, cfg, false) + signer, vmenv, bloomProcessor := p.preExecute(block, statedb, cfg, false) for i, tx := range block.Transactions() { if isPoSA { if isSystemTx, err := posa.IsSystemTransaction(tx, block.Header()); err != nil { - bloomProcessors.Close() + bloomProcessor.Close() return statedb, nil, nil, 0, err } else if isSystemTx { systemTxs = append(systemTxs, tx) @@ -947,13 +1025,13 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg msg, err := tx.AsMessage(signer) if err != nil { - bloomProcessors.Close() + bloomProcessor.Close() return statedb, nil, nil, 0, err } statedb.Prepare(tx.Hash(), block.Hash(), i) - receipt, err := applyTransaction(msg, p.config, p.bc, nil, gp, statedb, header, tx, usedGas, vmenv, bloomProcessors) + receipt, err := applyTransaction(msg, p.config, p.bc, nil, gp, statedb, header, tx, usedGas, vmenv, bloomProcessor) if err != nil { - bloomProcessors.Close() + bloomProcessor.Close() return statedb, nil, nil, 0, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err) } @@ -961,115 +1039,7 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg receipts = append(receipts, receipt) } - allLogs, err := p.postExecute(block, statedb, &commonTxs, &receipts, &systemTxs, usedGas, bloomProcessors) - return statedb, receipts, allLogs, *usedGas, err -} - -func (p *StateProcessor) ProcessParallel(block *types.Block, statedb *state.StateDB, cfg vm.Config) (*state.StateDB, types.Receipts, []*types.Log, uint64, error) { - var ( - usedGas = new(uint64) - header = block.Header() - gp = new(GasPool).AddGas(block.GasLimit()) - ) - var receipts = make([]*types.Receipt, 0) - txNum := len(block.Transactions()) - p.resetParallelState(txNum, statedb) - - // Iterate over and process the individual transactions - posa, isPoSA := p.engine.(consensus.PoSA) - commonTxs := make([]*types.Transaction, 0, txNum) - // usually do have two tx, one for validator set contract, another for system reward contract. - systemTxs := make([]*types.Transaction, 0, 2) - - signer, _, bloomProcessors := p.preExecute(block, statedb, cfg, true) - var waitTxChan, curTxChan chan int - for i, tx := range block.Transactions() { - if isPoSA { - if isSystemTx, err := posa.IsSystemTransaction(tx, block.Header()); err != nil { - bloomProcessors.Close() - return statedb, nil, nil, 0, err - } else if isSystemTx { - systemTxs = append(systemTxs, tx) - continue - } - } - - msg, err := tx.AsMessage(signer) // fixme: move it into slot. - if err != nil { - bloomProcessors.Close() - return statedb, nil, nil, 0, err - } - - // parallel start, wrap an exec message, which will be dispatched to a slot - waitTxChan = curTxChan // can be nil, if this is the tx of first batch, otherwise, it is previous Tx's wait channel - curTxChan = make(chan int, 1) - - txReq := &ParallelTxRequest{ - txIndex: i, - tx: tx, - slotDB: nil, - gp: gp, - msg: msg, - block: block, - vmConfig: cfg, - bloomProcessors: bloomProcessors, - usedGas: usedGas, - waitTxChan: waitTxChan, - curTxChan: curTxChan, - } - - // fixme: to optimize the for { for {} } loop code style - for { - if p.queueSameToAddress(txReq) { - break - } - if p.queueSameFromAddress(txReq) { - break - } - // if idle slot available, just dispatch and process next tx. - if p.dispatchToIdleSlot(statedb, txReq) { - // log.Info("ProcessParallel dispatch to idle slot", "txIndex", txReq.txIndex) - break - } - log.Debug("ProcessParallel no slot available, wait", "txIndex", txReq.txIndex) - // no idle slot, wait until a tx is executed and merged. - result := p.waitUntilNextTxDone(statedb) - - // update tx result - if result.err != nil { - log.Warn("ProcessParallel a failed tx", "resultSlotIndex", result.slotIndex, - "resultTxIndex", result.txIndex, "result.err", result.err) - bloomProcessors.Close() - return statedb, nil, nil, 0, fmt.Errorf("could not apply tx %d [%v]: %w", result.txIndex, result.tx.Hash().Hex(), result.err) - } - commonTxs = append(commonTxs, result.tx) - receipts = append(receipts, result.receipt) - } - } - - // wait until all tx request are done - for len(commonTxs)+len(systemTxs) < txNum { - result := p.waitUntilNextTxDone(statedb) - // update tx result - if result.err != nil { - log.Warn("ProcessParallel a failed tx", "resultSlotIndex", result.slotIndex, - "resultTxIndex", result.txIndex, "result.err", result.err) - return statedb, nil, nil, 0, fmt.Errorf("could not apply tx %d [%v]: %w", result.txIndex, result.tx.Hash().Hex(), result.err) - } - commonTxs = append(commonTxs, result.tx) - receipts = append(receipts, result.receipt) - } - - // len(commonTxs) could be 0, such as: https://bscscan.com/block/14580486 - if len(commonTxs) > 0 { - log.Info("ProcessParallel tx all done", "block", header.Number, "usedGas", *usedGas, - "txNum", txNum, - "len(commonTxs)", len(commonTxs), - "errorNum", p.debugErrorRedoNum, - "conflictNum", p.debugConflictRedoNum, - "redoRate(%)", 100*(p.debugErrorRedoNum+p.debugConflictRedoNum)/len(commonTxs)) - } - allLogs, err := p.postExecute(block, statedb, &commonTxs, &receipts, &systemTxs, usedGas, bloomProcessors) + allLogs, err := p.postExecute(block, statedb, &commonTxs, &receipts, &systemTxs, usedGas, bloomProcessor) return statedb, receipts, allLogs, *usedGas, err } diff --git a/core/types.go b/core/types.go index 82966e809d..c9061233e6 100644 --- a/core/types.go +++ b/core/types.go @@ -50,8 +50,4 @@ type Processor interface { // the transaction messages using the statedb and applying any rewards to both // the processor (coinbase) and any included uncles. Process(block *types.Block, statedb *state.StateDB, cfg vm.Config) (*state.StateDB, types.Receipts, []*types.Log, uint64, error) - - // Implement BEP-130: Parallel Transaction Execution. - InitParallelOnce() - ProcessParallel(block *types.Block, statedb *state.StateDB, cfg vm.Config) (*state.StateDB, types.Receipts, []*types.Log, uint64, error) } diff --git a/eth/backend.go b/eth/backend.go index 3f782ff6a8..4bac6bf733 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -203,6 +203,8 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { // TODO diffsync performance is not as expected, disable it when pipecommit is enabled for now if config.DiffSync && !config.PipeCommit { bcOps = append(bcOps, core.EnableLightProcessor) + } else if config.ParallelTxMode { + bcOps = append(bcOps, core.EnableParallelProcessor(config.ParallelTxNum, config.ParallelTxQueueSize)) } if config.PipeCommit { bcOps = append(bcOps, core.EnablePipelineCommit) diff --git a/eth/ethconfig/config.go b/eth/ethconfig/config.go index 09baad1e1c..94998acd6c 100644 --- a/eth/ethconfig/config.go +++ b/eth/ethconfig/config.go @@ -135,10 +135,13 @@ type Config struct { NoPruning bool // Whether to disable pruning and flush everything to disk DirectBroadcast bool - DisableSnapProtocol bool //Whether disable snap protocol + DisableSnapProtocol bool // Whether disable snap protocol DiffSync bool // Whether support diff sync PipeCommit bool RangeLimit bool + ParallelTxMode bool // Whether to execute transaction in parallel mode when do full sync + ParallelTxNum int // Number of slot for transaction execution + ParallelTxQueueSize int // Max number of Tx that can be queued to a slot TxLookupLimit uint64 `toml:",omitempty"` // The maximum number of blocks from head whose tx indices are reserved. From 27e324f5a1697854b6e0a09051bdcea430325b82 Mon Sep 17 00:00:00 2001 From: setunapo Date: Tue, 12 Apr 2022 11:02:02 +0800 Subject: [PATCH 09/16] [WIP] the Implementaion of Parallel EVM 2.0 1.features of 2.0: ** Streaming Pipeline ** Implement universal unconfirmed state db reference, try best to get account object state. ** New conflict detect, check based on what it has read. ** Do parallel KV conflict check for large KV read ** new Interface StateDBer and ParallelStateDB ** shared memory pool for parallel objects ** use map in sequential mode and sync.map in parallel mode for concurrent StateObject access ** replace DeepCopy by LightCopy to avoid redundant memory copy of StateObject ** do trie prefetch in advance ** dispatcher 2.0 Static Dispatch & Dynamic Dispatch Stolen mode for TxReq when a slot finished its static dispatched tasks RealTime result confirm in Stage2, when most if the tx have been executed at least once Make it configurable 2.Handle of corner case: ** don't panic if there is anything wrong reading state ** handle system address, skip its balance check ** handle WBNB contract to reduce conflict rate by balance make up WBNB balance makeup by GetBalanceOpCode & depth add a lock to fix WBNB make up concurrent crash add a new interface GetBalanceOpCode --- core/state/dump.go | 2 +- core/state/interface.go | 82 ++ core/state/journal.go | 65 +- core/state/state_object.go | 287 +++- core/state/state_test.go | 53 +- core/state/statedb.go | 2682 ++++++++++++++++++++++++++++-------- core/state_processor.go | 938 +++++++------ core/vm/evm.go | 2 +- core/vm/instructions.go | 2 +- core/vm/interface.go | 3 +- 10 files changed, 3055 insertions(+), 1061 deletions(-) create mode 100644 core/state/interface.go diff --git a/core/state/dump.go b/core/state/dump.go index b25da714fd..55f4c7754d 100644 --- a/core/state/dump.go +++ b/core/state/dump.go @@ -138,7 +138,7 @@ func (s *StateDB) DumpToCollector(c DumpCollector, excludeCode, excludeStorage, account.SecureKey = it.Key } addr := common.BytesToAddress(addrBytes) - obj := newObject(s, addr, data) + obj := newObject(s, s.isParallel, addr, data) if !excludeCode { account.Code = common.Bytes2Hex(obj.Code(s.db)) } diff --git a/core/state/interface.go b/core/state/interface.go new file mode 100644 index 0000000000..2362ac828b --- /dev/null +++ b/core/state/interface.go @@ -0,0 +1,82 @@ +// Copyright 2016 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum 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 go-ethereum 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 go-ethereum library. If not, see . + +package state + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" +) + +// StateDBer is copied from vm/interface.go +// It is used by StateObject & Journal right now, to abstract StateDB & ParallelStateDB +type StateDBer interface { + getBaseStateDB() *StateDB + getStateObject(common.Address) *StateObject // only accessible for journal + storeStateObj(common.Address, *StateObject) // only accessible for journal + + CreateAccount(common.Address) + + SubBalance(common.Address, *big.Int) + AddBalance(common.Address, *big.Int) + GetBalance(common.Address) *big.Int + + GetNonce(common.Address) uint64 + SetNonce(common.Address, uint64) + + GetCodeHash(common.Address) common.Hash + GetCode(common.Address) []byte + SetCode(common.Address, []byte) + GetCodeSize(common.Address) int + + AddRefund(uint64) + SubRefund(uint64) + GetRefund() uint64 + + GetCommittedState(common.Address, common.Hash) common.Hash + GetState(common.Address, common.Hash) common.Hash + SetState(common.Address, common.Hash, common.Hash) + + Suicide(common.Address) bool + HasSuicided(common.Address) bool + + // Exist reports whether the given account exists in state. + // Notably this should also return true for suicided accounts. + Exist(common.Address) bool + // Empty returns whether the given account is empty. Empty + // is defined according to EIP161 (balance = nonce = code = 0). + Empty(common.Address) bool + + PrepareAccessList(sender common.Address, dest *common.Address, precompiles []common.Address, txAccesses types.AccessList) + AddressInAccessList(addr common.Address) bool + SlotInAccessList(addr common.Address, slot common.Hash) (addressOk bool, slotOk bool) + // AddAddressToAccessList adds the given address to the access list. This operation is safe to perform + // even if the feature/fork is not active yet + AddAddressToAccessList(addr common.Address) + // AddSlotToAccessList adds the given (address,slot) to the access list. This operation is safe to perform + // even if the feature/fork is not active yet + AddSlotToAccessList(addr common.Address, slot common.Hash) + + RevertToSnapshot(int) + Snapshot() int + + AddLog(*types.Log) + AddPreimage(common.Hash, []byte) + + ForEachStorage(common.Address, func(common.Hash, common.Hash) bool) error +} diff --git a/core/state/journal.go b/core/state/journal.go index b3a2956f75..dbb552c142 100644 --- a/core/state/journal.go +++ b/core/state/journal.go @@ -26,7 +26,7 @@ import ( // reverted on demand. type journalEntry interface { // revert undoes the changes introduced by this journal entry. - revert(*StateDB) + revert(StateDBer) // dirtied returns the Ethereum address modified by this journal entry. dirtied() *common.Address @@ -58,10 +58,10 @@ func (j *journal) append(entry journalEntry) { // revert undoes a batch of journalled modifications along with any reverted // dirty handling too. -func (j *journal) revert(statedb *StateDB, snapshot int) { +func (j *journal) revert(dber StateDBer, snapshot int) { for i := len(j.entries) - 1; i >= snapshot; i-- { // Undo the changes made by the operation - j.entries[i].revert(statedb) + j.entries[i].revert(dber) // Drop any dirty tracking induced by the change if addr := j.entries[i].dirtied(); addr != nil { @@ -141,9 +141,15 @@ type ( } ) -func (ch createObjectChange) revert(s *StateDB) { +func (ch createObjectChange) revert(dber StateDBer) { + s := dber.getBaseStateDB() if s.parallel.isSlotDB { delete(s.parallel.dirtiedStateObjectsInSlot, *ch.account) + delete(s.parallel.addrStateChangesInSlot, *ch.account) + delete(s.parallel.nonceChangesInSlot, *ch.account) + delete(s.parallel.balanceChangesInSlot, *ch.account) + delete(s.parallel.codeChangesInSlot, *ch.account) + delete(s.parallel.kvChangesInSlot, *ch.account) } else { s.deleteStateObj(*ch.account) } @@ -154,10 +160,19 @@ func (ch createObjectChange) dirtied() *common.Address { return ch.account } -func (ch resetObjectChange) revert(s *StateDB) { - s.SetStateObject(ch.prev) +func (ch resetObjectChange) revert(dber StateDBer) { + s := dber.getBaseStateDB() + if s.parallel.isSlotDB { + // ch.prev must be from dirtiedStateObjectsInSlot, put it back + s.parallel.dirtiedStateObjectsInSlot[ch.prev.address] = ch.prev + } else { + // ch.prev was got from main DB, put it back to main DB. + s.storeStateObj(ch.prev.address, ch.prev) + } if !ch.prevdestruct && s.snap != nil { + s.snapParallelLock.Lock() delete(s.snapDestructs, ch.prev.address) + s.snapParallelLock.Unlock() } } @@ -165,8 +180,8 @@ func (ch resetObjectChange) dirtied() *common.Address { return nil } -func (ch suicideChange) revert(s *StateDB) { - obj := s.getStateObject(*ch.account) +func (ch suicideChange) revert(dber StateDBer) { + obj := dber.getStateObject(*ch.account) if obj != nil { obj.suicided = ch.prev obj.setBalance(ch.prevbalance) @@ -179,46 +194,47 @@ func (ch suicideChange) dirtied() *common.Address { var ripemd = common.HexToAddress("0000000000000000000000000000000000000003") -func (ch touchChange) revert(s *StateDB) { +func (ch touchChange) revert(dber StateDBer) { } func (ch touchChange) dirtied() *common.Address { return ch.account } -func (ch balanceChange) revert(s *StateDB) { - s.getStateObject(*ch.account).setBalance(ch.prev) +func (ch balanceChange) revert(dber StateDBer) { + dber.getStateObject(*ch.account).setBalance(ch.prev) } func (ch balanceChange) dirtied() *common.Address { return ch.account } -func (ch nonceChange) revert(s *StateDB) { - s.getStateObject(*ch.account).setNonce(ch.prev) +func (ch nonceChange) revert(dber StateDBer) { + dber.getStateObject(*ch.account).setNonce(ch.prev) } func (ch nonceChange) dirtied() *common.Address { return ch.account } -func (ch codeChange) revert(s *StateDB) { - s.getStateObject(*ch.account).setCode(common.BytesToHash(ch.prevhash), ch.prevcode) +func (ch codeChange) revert(dber StateDBer) { + dber.getStateObject(*ch.account).setCode(common.BytesToHash(ch.prevhash), ch.prevcode) } func (ch codeChange) dirtied() *common.Address { return ch.account } -func (ch storageChange) revert(s *StateDB) { - s.getStateObject(*ch.account).setState(ch.key, ch.prevalue) +func (ch storageChange) revert(dber StateDBer) { + dber.getStateObject(*ch.account).setState(ch.key, ch.prevalue) } func (ch storageChange) dirtied() *common.Address { return ch.account } -func (ch refundChange) revert(s *StateDB) { +func (ch refundChange) revert(dber StateDBer) { + s := dber.getBaseStateDB() s.refund = ch.prev } @@ -226,7 +242,9 @@ func (ch refundChange) dirtied() *common.Address { return nil } -func (ch addLogChange) revert(s *StateDB) { +func (ch addLogChange) revert(dber StateDBer) { + s := dber.getBaseStateDB() + logs := s.logs[ch.txhash] if len(logs) == 1 { delete(s.logs, ch.txhash) @@ -240,7 +258,8 @@ func (ch addLogChange) dirtied() *common.Address { return nil } -func (ch addPreimageChange) revert(s *StateDB) { +func (ch addPreimageChange) revert(dber StateDBer) { + s := dber.getBaseStateDB() delete(s.preimages, ch.hash) } @@ -248,7 +267,8 @@ func (ch addPreimageChange) dirtied() *common.Address { return nil } -func (ch accessListAddAccountChange) revert(s *StateDB) { +func (ch accessListAddAccountChange) revert(dber StateDBer) { + s := dber.getBaseStateDB() /* One important invariant here, is that whenever a (addr, slot) is added, if the addr is not already present, the add causes two journal entries: @@ -267,7 +287,8 @@ func (ch accessListAddAccountChange) dirtied() *common.Address { return nil } -func (ch accessListAddSlotChange) revert(s *StateDB) { +func (ch accessListAddSlotChange) revert(dber StateDBer) { + s := dber.getBaseStateDB() if s.accessList != nil { s.accessList.DeleteSlot(*ch.address, *ch.slot) } diff --git a/core/state/state_object.go b/core/state/state_object.go index ca79da0a74..fed9ce31b1 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -38,9 +38,18 @@ func (c Code) String() string { return string(c) //strings.Join(Disassemble(c), " ") } -type Storage map[common.Hash]common.Hash +type Storage interface { + String() string + GetValue(hash common.Hash) (common.Hash, bool) + StoreValue(hash common.Hash, value common.Hash) + Length() (length int) + Copy() Storage + Range(func(key, value interface{}) bool) +} + +type StorageMap map[common.Hash]common.Hash -func (s Storage) String() (str string) { +func (s StorageMap) String() (str string) { for key, value := range s { str += fmt.Sprintf("%X : %X\n", key, value) } @@ -48,8 +57,8 @@ func (s Storage) String() (str string) { return } -func (s Storage) Copy() Storage { - cpy := make(Storage) +func (s StorageMap) Copy() Storage { + cpy := make(StorageMap) for key, value := range s { cpy[key] = value } @@ -57,6 +66,79 @@ func (s Storage) Copy() Storage { return cpy } +func (s StorageMap) GetValue(hash common.Hash) (common.Hash, bool) { + value, ok := s[hash] + return value, ok +} + +func (s StorageMap) StoreValue(hash common.Hash, value common.Hash) { + s[hash] = value +} + +func (s StorageMap) Length() int { + return len(s) +} + +func (s StorageMap) Range(f func(hash, value interface{}) bool) { + for k, v := range s { + result := f(k, v) + if !result { + return + } + } +} + +type StorageSyncMap struct { + sync.Map +} + +func (s *StorageSyncMap) String() (str string) { + s.Range(func(key, value interface{}) bool { + str += fmt.Sprintf("%X : %X\n", key, value) + return true + }) + + return +} + +func (s *StorageSyncMap) GetValue(hash common.Hash) (common.Hash, bool) { + value, ok := s.Load(hash) + if !ok { + return common.Hash{}, ok + } + + return value.(common.Hash), ok +} + +func (s *StorageSyncMap) StoreValue(hash common.Hash, value common.Hash) { + s.Store(hash, value) +} + +func (s *StorageSyncMap) Length() (length int) { + s.Range(func(key, value interface{}) bool { + length++ + return true + }) + return length +} + +func (s *StorageSyncMap) Copy() Storage { + cpy := StorageSyncMap{} + s.Range(func(key, value interface{}) bool { + cpy.Store(key, value) + return true + }) + + return &cpy +} + +func newStorage(isParallel bool) Storage { + if isParallel { + return &StorageSyncMap{} + } + return make(StorageMap) +} + // StateObject represents an Ethereum account which is being modified. // // The usage pattern is as follows: @@ -68,6 +150,7 @@ type StateObject struct { addrHash common.Hash // hash of ethereum address of the account data Account db *StateDB + dbItf StateDBer rootCorrected bool // To indicate whether the root has been corrected in pipecommit mode // DB error. @@ -81,12 +164,12 @@ type StateObject struct { trie Trie // storage trie, which becomes non-nil on first access code Code // contract bytecode, which gets set when code is loaded - sharedOriginStorage *sync.Map // Point to the entry of the stateObject in sharedPool + isParallel bool // isParallel indicates this state object is used in parallel mode + sharedOriginStorage *sync.Map // Storage cache of original entries to dedup rewrites, reset for every transaction originStorage Storage // Storage cache of original entries to dedup rewrites, reset for every transaction - - pendingStorage Storage // Storage entries that need to be flushed to disk, at the end of an entire block - dirtyStorage Storage // Storage entries that have been modified in the current transaction execution - fakeStorage Storage // Fake storage which constructed by caller for debugging purpose. + pendingStorage Storage // Storage entries that need to be flushed to disk, at the end of an entire block + dirtyStorage Storage // Storage entries that have been modified in the current transaction execution + fakeStorage Storage // Fake storage which constructed by caller for debugging purpose. // Cache flags. // When an object is marked suicided it will be delete from the trie @@ -101,7 +184,51 @@ type StateObject struct { // empty returns whether the account is considered empty. func (s *StateObject) empty() bool { - return s.data.Nonce == 0 && s.data.Balance.Sign() == 0 && bytes.Equal(s.data.CodeHash, emptyCodeHash) + // return s.data.Nonce == 0 && s.data.Balance.Sign() == 0 && bytes.Equal(s.data.CodeHash, emptyCodeHash) + + // 0426, leave some notation, empty() works so far + // empty() has 3 use cases: + // 1.StateDB.Empty(), to empty check + // A: It is ok, we have handled it in Empty(), to make sure nonce, balance, codeHash are solid + // 2:AddBalance 0, empty check for touch event + // empty() will add a touch event. + // if we misjudge it, the touch event could be lost, which make address not deleted. // fixme + // 3.Finalise(), to do empty delete + // the address should be dirtied or touched + // if it nonce dirtied, it is ok, since nonce is monotonically increasing, won't be zero + // if balance is dirtied, balance could be zero, we should refer solid nonce & codeHash // fixme + // if codeHash is dirtied, it is ok, since code will not be updated. + // if suicide, it is ok + // if object is new created, it is ok + // if CreateAccout, recreate the address, it is ok. + + // Slot 0 tx 0: AddBalance(100) to addr_1, => addr_1: balance = 100, nonce = 0, code is empty + // Slot 1 tx 1: addr_1 Transfer 99.9979 with GasFee 0.0021, => addr_1: balance = 0, nonce = 1, code is empty + // notice: balance transfer cost 21,000 gas, with gasPrice = 100Gwei, GasFee will be 0.0021 + // Slot 0 tx 2: add balance 0 to addr_1(empty check for touch event), + // the object was lightCopied from tx 0, + + // in parallel mode, we should not check empty by raw nonce, balance, codeHash any more, + // since it could be invalid. + // e.g., AddBalance() to an address, we will do lightCopy to get a new StateObject, we did balance fixup to + // make sure object's Balance is reliable. But we did not fixup nonce or code, we only do nonce or codehash + // fixup on need, that's when we wanna to update the nonce or codehash. + // So nonce, blance + // Before the block is processed, addr_1 account: nonce = 0, emptyCodeHash, balance = 100 + // Slot 0 tx 0: no access to addr_1 + // Slot 1 tx 1: sub balance 100, it is empty and deleted + // Slot 0 tx 2: GetNonce, lightCopy based on main DB(balance = 100) , not empty + // return s.db.GetNonce(s.address) == 0 && s.db.GetBalance(s.address).Sign() == 0 && bytes.Equal(s.db.GetCodeHash(s.address).Bytes(), emptyCodeHash) + + if s.dbItf.GetBalance(s.address).Sign() != 0 { // check balance first, since it is most likely not zero + return false + } + if s.dbItf.GetNonce(s.address) != 0 { + return false + } + codeHash := s.dbItf.GetCodeHash(s.address) + return bytes.Equal(codeHash.Bytes(), emptyCodeHash) // code is empty, the object is empty + } // Account is the Ethereum consensus representation of accounts. @@ -114,9 +241,10 @@ type Account struct { } // newObject creates a state object. -func newObject(db *StateDB, address common.Address, data Account) *StateObject { +func newObject(dbItf StateDBer, isParallel bool, address common.Address, data Account) *StateObject { + db := dbItf.getBaseStateDB() if data.Balance == nil { - data.Balance = new(big.Int) + data.Balance = new(big.Int) // todo: why not common.Big0? } if data.CodeHash == nil { data.CodeHash = emptyCodeHash @@ -132,13 +260,15 @@ func newObject(db *StateDB, address common.Address, data Account) *StateObject { return &StateObject{ db: db, + dbItf: dbItf, address: address, addrHash: crypto.Keccak256Hash(address[:]), data: data, + isParallel: isParallel, sharedOriginStorage: storageMap, - originStorage: make(Storage), - pendingStorage: make(Storage), - dirtyStorage: make(Storage), + originStorage: newStorage(isParallel), + dirtyStorage: newStorage(isParallel), + pendingStorage: newStorage(isParallel), } } @@ -194,10 +324,11 @@ func (s *StateObject) getTrie(db Database) Trie { func (s *StateObject) GetState(db Database, key common.Hash) common.Hash { // If the fake storage is set, only lookup the state here(in the debugging mode) if s.fakeStorage != nil { - return s.fakeStorage[key] + fakeValue, _ := s.fakeStorage.GetValue(key) + return fakeValue } // If we have a dirty value for this state entry, return it - value, dirty := s.dirtyStorage[key] + value, dirty := s.dirtyStorage.GetValue(key) if dirty { return value } @@ -206,7 +337,7 @@ func (s *StateObject) GetState(db Database, key common.Hash) common.Hash { } func (s *StateObject) getOriginStorage(key common.Hash) (common.Hash, bool) { - if value, cached := s.originStorage[key]; cached { + if value, cached := s.originStorage.GetValue(key); cached { return value, true } // if L1 cache miss, try to get it from shared pool @@ -215,7 +346,7 @@ func (s *StateObject) getOriginStorage(key common.Hash) (common.Hash, bool) { if !ok { return common.Hash{}, false } - s.originStorage[key] = val.(common.Hash) + s.originStorage.StoreValue(key, val.(common.Hash)) return val.(common.Hash), true } return common.Hash{}, false @@ -225,17 +356,18 @@ func (s *StateObject) setOriginStorage(key common.Hash, value common.Hash) { if s.db.writeOnSharedStorage && s.sharedOriginStorage != nil { s.sharedOriginStorage.Store(key, value) } - s.originStorage[key] = value + s.originStorage.StoreValue(key, value) } // GetCommittedState retrieves a value from the committed account storage trie. func (s *StateObject) GetCommittedState(db Database, key common.Hash) common.Hash { // If the fake storage is set, only lookup the state here(in the debugging mode) if s.fakeStorage != nil { - return s.fakeStorage[key] + fakeValue, _ := s.fakeStorage.GetValue(key) + return fakeValue } // If we have a pending write or clean cached, return that - if value, pending := s.pendingStorage[key]; pending { + if value, pending := s.pendingStorage.GetValue(key); pending { return value } @@ -269,9 +401,12 @@ func (s *StateObject) GetCommittedState(db Database, key common.Hash) common.Has // 1) resurrect happened, and new slot values were set -- those should // have been handles via pendingStorage above. // 2) we don't have new values, and can deliver empty response back - if _, destructed := s.db.snapDestructs[s.address]; destructed { + s.db.snapParallelLock.RLock() + if _, destructed := s.db.snapDestructs[s.address]; destructed { // fixme: use sync.Map, instead of RWMutex? + s.db.snapParallelLock.RUnlock() return common.Hash{} } + s.db.snapParallelLock.RUnlock() enc, err = s.db.snap.Storage(s.addrHash, crypto.Keccak256Hash(key.Bytes())) } // If snapshot unavailable or reading from it failed, load from the database @@ -306,11 +441,18 @@ func (s *StateObject) GetCommittedState(db Database, key common.Hash) common.Has func (s *StateObject) SetState(db Database, key, value common.Hash) { // If the fake storage is set, put the temporary state update here. if s.fakeStorage != nil { - s.fakeStorage[key] = value + s.fakeStorage.StoreValue(key, value) return } // If the new value is the same as old, don't set - prev := s.GetState(db, key) + // In parallel mode, it has to get from StateDB, in case: + // a.the Slot did not set the key before and try to set it to `val_1` + // b.Unconfirmed DB has set the key to `val_2` + // c.if we use StateObject.GetState, and the key load from the main DB is `val_1` + // this `SetState could be skipped` + // d.Finally, the key's value will be `val_2`, while it should be `val_1` + // such as: https://bscscan.com/txs?block=2491181 + prev := s.dbItf.GetState(s.address, key) // fixme: if it is for journal, may not necessary, we can remove this change record if prev == value { return } @@ -320,6 +462,10 @@ func (s *StateObject) SetState(db Database, key, value common.Hash) { key: key, prevalue: prev, }) + if s.db.parallel.isSlotDB { + s.db.parallel.kvChangesInSlot[s.address][key] = struct{}{} // should be moved to here, after `s.db.GetState()` + } + s.setState(key, value) } @@ -332,29 +478,33 @@ func (s *StateObject) SetState(db Database, key, value common.Hash) { func (s *StateObject) SetStorage(storage map[common.Hash]common.Hash) { // Allocate fake storage if it's nil. if s.fakeStorage == nil { - s.fakeStorage = make(Storage) + s.fakeStorage = newStorage(s.isParallel) } for key, value := range storage { - s.fakeStorage[key] = value + s.fakeStorage.StoreValue(key, value) } // Don't bother journal since this function should only be used for // debugging and the `fake` storage won't be committed to database. } func (s *StateObject) setState(key, value common.Hash) { - s.dirtyStorage[key] = value + s.dirtyStorage.StoreValue(key, value) } // finalise moves all dirty storage slots into the pending area to be hashed or // committed later. It is invoked at the end of every transaction. func (s *StateObject) finalise(prefetch bool) { - slotsToPrefetch := make([][]byte, 0, len(s.dirtyStorage)) - for key, value := range s.dirtyStorage { - s.pendingStorage[key] = value - if value != s.originStorage[key] { - slotsToPrefetch = append(slotsToPrefetch, common.CopyBytes(key[:])) // Copy needed for closure + slotsToPrefetch := make([][]byte, 0, s.dirtyStorage.Length()) + s.dirtyStorage.Range(func(key, value interface{}) bool { + s.pendingStorage.StoreValue(key.(common.Hash), value.(common.Hash)) + + originalValue, _ := s.originStorage.GetValue(key.(common.Hash)) + if value.(common.Hash) != originalValue { + originalKey := key.(common.Hash) + slotsToPrefetch = append(slotsToPrefetch, common.CopyBytes(originalKey[:])) // Copy needed for closure } - } + return true + }) // The account root need to be updated before prefetch, otherwise the account root is empty if s.db.pipeCommit && s.data.Root == dummyRoot && !s.rootCorrected && s.db.snap.AccountsCorrected() { @@ -369,8 +519,8 @@ func (s *StateObject) finalise(prefetch bool) { if s.db.prefetcher != nil && prefetch && len(slotsToPrefetch) > 0 && s.data.Root != emptyRoot && s.data.Root != dummyRoot { s.db.prefetcher.prefetch(s.data.Root, slotsToPrefetch, s.addrHash) } - if len(s.dirtyStorage) > 0 { - s.dirtyStorage = make(Storage) + if s.dirtyStorage.Length() > 0 { + s.dirtyStorage = newStorage(s.isParallel) } } @@ -379,7 +529,7 @@ func (s *StateObject) finalise(prefetch bool) { func (s *StateObject) updateTrie(db Database) Trie { // Make sure all dirty slots are finalized into the pending storage area s.finalise(false) // Don't prefetch any more, pull directly if need be - if len(s.pendingStorage) == 0 { + if s.pendingStorage.Length() == 0 { return s.trie } // Track the amount of time wasted on updating the storage trie @@ -395,20 +545,26 @@ func (s *StateObject) updateTrie(db Database) Trie { // Insert all the pending updates into the trie tr := s.getTrie(db) - usedStorage := make([][]byte, 0, len(s.pendingStorage)) - for key, value := range s.pendingStorage { + usedStorage := make([][]byte, 0, s.pendingStorage.Length()) + s.pendingStorage.Range(func(k, v interface{}) bool { + key := k.(common.Hash) + value := v.(common.Hash) + // Skip noop changes, persist actual changes - if value == s.originStorage[key] { - continue + originalValue, _ := s.originStorage.GetValue(k.(common.Hash)) + if v.(common.Hash) == originalValue { + return true } - s.originStorage[key] = value - var v []byte + + s.setOriginStorage(key, value) + + var vs []byte if (value == common.Hash{}) { s.setError(tr.TryDelete(key[:])) } else { // Encoding []byte cannot fail, ok to ignore the error. - v, _ = rlp.EncodeToBytes(common.TrimLeftZeroes(value[:])) - s.setError(tr.TryUpdate(key[:], v)) + vs, _ = rlp.EncodeToBytes(common.TrimLeftZeroes(value[:])) + s.setError(tr.TryUpdate(key[:], vs)) } // If state snapshotting is active, cache the data til commit if s.db.snap != nil { @@ -420,16 +576,18 @@ func (s *StateObject) updateTrie(db Database) Trie { s.db.snapStorage[s.address] = storage } } - storage[string(key[:])] = v // v will be nil if value is 0x00 + storage[string(key[:])] = vs // vs will be nil if value is 0x00 s.db.snapMux.Unlock() } usedStorage = append(usedStorage, common.CopyBytes(key[:])) // Copy needed for closure - } + return true + }) + if s.db.prefetcher != nil { s.db.prefetcher.used(s.data.Root, usedStorage) } - if len(s.pendingStorage) > 0 { - s.pendingStorage = make(Storage) + if s.pendingStorage.Length() > 0 { + s.pendingStorage = newStorage(s.isParallel) } return tr } @@ -504,7 +662,8 @@ func (s *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), + prev: new(big.Int).Set(s.data.Balance), // prevBalance, + // prev: prevBalance, }) s.setBalance(amount) } @@ -516,8 +675,21 @@ func (s *StateObject) setBalance(amount *big.Int) { // Return the gas back to the origin. Used by the Virtual machine or Closures func (s *StateObject) ReturnGas(gas *big.Int) {} +func (s *StateObject) lightCopy(db *ParallelStateDB) *StateObject { + stateObject := newObject(db, s.isParallel, s.address, s.data) + if s.trie != nil { + // fixme: no need to copy trie for light copy, since light copied object won't access trie DB + stateObject.trie = db.db.CopyTrie(s.trie) + } + stateObject.code = s.code + stateObject.suicided = false // should be false + stateObject.dirtyCode = s.dirtyCode // it is not used in slot, but keep it is ok + stateObject.deleted = false // should be false + return stateObject +} + func (s *StateObject) deepCopy(db *StateDB) *StateObject { - stateObject := newObject(db, s.address, s.data) + stateObject := newObject(db, s.isParallel, s.address, s.data) if s.trie != nil { stateObject.trie = db.db.CopyTrie(s.trie) } @@ -533,9 +705,11 @@ func (s *StateObject) deepCopy(db *StateDB) *StateObject { func (s *StateObject) MergeSlotObject(db Database, dirtyObjs *StateObject, keys StateKeys) { for key := range keys { - // better to do s.GetState(db, key) to load originStorage for this key? - // since originStorage was in dirtyObjs, but it works even originStorage miss the state object. - s.SetState(db, key, dirtyObjs.GetState(db, key)) + // In parallel mode, always GetState by StateDB, not by StateObject directly, + // since it the KV could exist in unconfirmed DB. + // But here, it should be ok, since the KV should be changed and valid in the SlotDB, + // s.SetState(db, key, dirtyObjs.GetState(db, key)) + s.setState(key, dirtyObjs.GetState(db, key)) } } @@ -582,7 +756,7 @@ func (s *StateObject) CodeSize(db Database) int { } func (s *StateObject) SetCode(codeHash common.Hash, code []byte) { - prevcode := s.Code(s.db.db) + prevcode := s.dbItf.GetCode(s.address) s.db.journal.append(codeChange{ account: &s.address, prevhash: s.CodeHash(), @@ -598,9 +772,10 @@ func (s *StateObject) setCode(codeHash common.Hash, code []byte) { } func (s *StateObject) SetNonce(nonce uint64) { + prevNonce := s.dbItf.GetNonce(s.address) s.db.journal.append(nonceChange{ account: &s.address, - prev: s.data.Nonce, + prev: prevNonce, }) s.setNonce(nonce) } diff --git a/core/state/state_test.go b/core/state/state_test.go index 4be9ae8ce3..a8417b13e7 100644 --- a/core/state/state_test.go +++ b/core/state/state_test.go @@ -227,30 +227,47 @@ func compareStateObjects(so0, so1 *StateObject, t *testing.T) { t.Fatalf("Code mismatch: have %v, want %v", so0.code, so1.code) } - if len(so1.dirtyStorage) != len(so0.dirtyStorage) { - t.Errorf("Dirty storage size mismatch: have %d, want %d", len(so1.dirtyStorage), len(so0.dirtyStorage)) + if so1.dirtyStorage.Length() != so0.dirtyStorage.Length() { + t.Errorf("Dirty storage size mismatch: have %d, want %d", so1.dirtyStorage.Length(), so0.dirtyStorage.Length()) } - for k, v := range so1.dirtyStorage { - if so0.dirtyStorage[k] != v { - t.Errorf("Dirty storage key %x mismatch: have %v, want %v", k, so0.dirtyStorage[k], v) + + so1.dirtyStorage.Range(func(key, value interface{}) bool { + k, v := key.(common.Hash), value.(common.Hash) + + if tmpV, _ := so0.dirtyStorage.GetValue(k); tmpV != v { + t.Errorf("Dirty storage key %x mismatch: have %v, want %v", k, tmpV.String(), v) } - } - for k, v := range so0.dirtyStorage { - if so1.dirtyStorage[k] != v { + return true + }) + + so0.dirtyStorage.Range(func(key, value interface{}) bool { + k, v := key.(common.Hash), value.(common.Hash) + + if tmpV, _ := so1.dirtyStorage.GetValue(k); tmpV != v { t.Errorf("Dirty storage key %x mismatch: have %v, want none.", k, v) } + return true + }) + + if so1.originStorage.Length() != so0.originStorage.Length() { + t.Errorf("Origin storage size mismatch: have %d, want %d", so1.originStorage.Length(), so0.originStorage.Length()) } - if len(so1.originStorage) != len(so0.originStorage) { - t.Errorf("Origin storage size mismatch: have %d, want %d", len(so1.originStorage), len(so0.originStorage)) - } - for k, v := range so1.originStorage { - if so0.originStorage[k] != v { - t.Errorf("Origin storage key %x mismatch: have %v, want %v", k, so0.originStorage[k], v) + + so1.originStorage.Range(func(key, value interface{}) bool { + k, v := key.(common.Hash), value.(common.Hash) + + if tmpV, _ := so0.originStorage.GetValue(k); tmpV != v { + t.Errorf("Origin storage key %x mismatch: have %v, want %v", k, tmpV, v) } - } - for k, v := range so0.originStorage { - if so1.originStorage[k] != v { + return true + }) + + so0.originStorage.Range(func(key, value interface{}) bool { + k, v := key.(common.Hash), value.(common.Hash) + + if tmpV, _ := so1.originStorage.GetValue(k); tmpV != v { t.Errorf("Origin storage key %x mismatch: have %v, want none.", k, v) } - } + return true + }) } diff --git a/core/state/statedb.go b/core/state/statedb.go index 4c28ae9bdf..b9f81c044e 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -18,6 +18,7 @@ package state import ( + "bytes" "errors" "fmt" "math/big" @@ -47,6 +48,7 @@ type revision struct { } var ( + once sync.Once // emptyRoot is the known root hash of an empty trie. emptyRoot = common.HexToHash("56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421") @@ -55,6 +57,18 @@ var ( dummyRoot = crypto.Keccak256Hash([]byte("dummy_account_root")) emptyAddr = crypto.Keccak256Hash(common.Address{}.Bytes()) + + // https://bscscan.com/address/0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c + WBNBAddress = common.HexToAddress("0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c") + // EVM use big-endian mode, so as the MethodID + WBNBAddress_deposit = []byte{0xd0, 0xe3, 0x0d, 0xb0} // "0xd0e30db0": Keccak-256("deposit()") + WBNBAddress_withdraw = []byte{0x2e, 0x1a, 0x7d, 0x4d} // "0x2e1a7d4d": Keccak-256("withdraw(uint256)") + WBNBAddress_totalSupply = []byte{0x18, 0x16, 0x0d, 0xdd} // "0x18160ddd": Keccak-256("totalSupply()") + WBNBAddress_approve = []byte{0x09, 0x5e, 0xa7, 0xb3} // "0x095ea7b3": Keccak-256("approve(address,uint256)") + WBNBAddress_transfer = []byte{0xa9, 0x05, 0x9c, 0xbb} // "0xa9059cbb": Keccak-256("transfer(address,uint256)") + WBNBAddress_transferFrom = []byte{0x23, 0xb8, 0x72, 0xdd} // "0x23b872dd": Keccak-256("transferFrom(address,address,uint256)") + // unknown WBNB interface 1: {0xDD, 0x62,0xED, 0x3E} in block: 14,248,627 + // unknown WBNB interface 2: {0x70, 0xa0,0x82, 0x31} in block: 14,249,300 ) type proofList [][]byte @@ -98,7 +112,11 @@ func (s *StateDB) loadStateObj(addr common.Address) (*StateObject, bool) { // storeStateObj is the entry for storing state object to stateObjects in StateDB or stateObjects in parallel func (s *StateDB) storeStateObj(addr common.Address, stateObject *StateObject) { if s.isParallel { + // When a state object is stored into s.parallel.stateObjects, + // it belongs to base StateDB, it is confirmed and valid. + stateObject.db.storeParallelLock.Lock() s.parallel.stateObjects.Store(addr, stateObject) + stateObject.db.storeParallelLock.Unlock() } else { s.stateObjects[addr] = stateObject } @@ -113,21 +131,10 @@ func (s *StateDB) deleteStateObj(addr common.Address) { } } -// For parallel mode only, keep the change list for later conflict detect -type SlotChangeList struct { - TxIndex int - StateObjectSuicided map[common.Address]struct{} - StateChangeSet map[common.Address]StateKeys - BalanceChangeSet map[common.Address]struct{} - CodeChangeSet map[common.Address]struct{} - AddrStateChangeSet map[common.Address]struct{} - NonceChangeSet map[common.Address]struct{} -} - // For parallel mode only type ParallelState struct { - isSlotDB bool // isSlotDB denotes StateDB is used in slot - + isSlotDB bool // denotes StateDB is used in slot, we will try to remove it + SlotIndex int // fixme: to be removed // stateObjects holds the state objects in the base slot db // the reason for using stateObjects instead of stateObjects on the outside is // we need a thread safe map to hold state objects since there are many slots will read @@ -135,21 +142,34 @@ type ParallelState struct { // And we will merge all the changes made by the concurrent slot into it. stateObjects *StateObjectSyncMap - baseTxIndex int // slotDB is created base on this tx index. + baseStateDB *StateDB // for parallel mode, there will be a base StateDB in dispatcher routine. + baseTxIndex int // slotDB is created base on this tx index. dirtiedStateObjectsInSlot map[common.Address]*StateObject - // for conflict check + unconfirmedDBs *sync.Map /*map[int]*ParallelStateDB*/ // do unconfirmed reference in same slot. + + // we will record the read detail for conflict check and + // the changed addr or key for object merge, the changed detail can be acheived from the dirty object + nonceChangesInSlot map[common.Address]struct{} + nonceReadsInSlot map[common.Address]uint64 balanceChangesInSlot map[common.Address]struct{} // the address's balance has been changed - balanceReadsInSlot map[common.Address]struct{} // the address's balance has been read and used. - codeReadsInSlot map[common.Address]struct{} - codeChangesInSlot map[common.Address]struct{} - stateReadsInSlot map[common.Address]StateKeys - stateChangesInSlot map[common.Address]StateKeys // no need record value + balanceReadsInSlot map[common.Address]*big.Int // the address's balance has been read and used. + // codeSize can be derived based on code, but codeHash can not directly derived based on code + // - codeSize is 0 for address not exist or empty code + // - codeHash is `common.Hash{}` for address not exist, emptyCodeHash(`Keccak256Hash(nil)`) for empty code + // so we use codeReadsInSlot & codeHashReadsInSlot to keep code and codeHash, codeSize is derived from code + codeReadsInSlot map[common.Address][]byte // empty if address not exist or no code in this address + codeHashReadsInSlot map[common.Address]common.Hash + codeChangesInSlot map[common.Address]struct{} + kvReadsInSlot map[common.Address]Storage + kvChangesInSlot map[common.Address]StateKeys // value will be kept in dirtiedStateObjectsInSlot // Actions such as SetCode, Suicide will change address's state. // Later call like Exist(), Empty(), HasSuicided() depend on the address's state. - addrStateReadsInSlot map[common.Address]struct{} - addrStateChangesInSlot map[common.Address]struct{} - stateObjectsSuicidedInSlot map[common.Address]struct{} - nonceChangesInSlot map[common.Address]struct{} + addrStateReadsInSlot map[common.Address]bool // true: exist, false: not exist or deleted + addrStateChangesInSlot map[common.Address]bool // true: created, false: deleted + + addrSnapDestructsReadsInSlot map[common.Address]bool + // addrSnapDestructsChangesInSlot map[common.Address]struct{} // no use to get from unconfirmed DB for efficiency + // Transaction will pay gas fee to system address. // Parallel execution will clear system address's balance at first, in order to maintain transaction's // gas fee value. Normal transaction will access system address twice, otherwise it means the transaction @@ -157,6 +177,9 @@ type ParallelState struct { systemAddress common.Address systemAddressOpsCount int keepSystemAddressBalance bool + + // we may need to redo for some specific reasons, like we read the wrong state and need to panic in sequential mode in SubRefund + needsRedo bool } // StateDB structs within the ethereum protocol are used to store anything @@ -181,12 +204,14 @@ type StateDB struct { fullProcessed bool pipeCommit bool - snapMux sync.Mutex - snaps *snapshot.Tree - snap snapshot.Snapshot - snapDestructs map[common.Address]struct{} - snapAccounts map[common.Address][]byte - snapStorage map[common.Address]map[string][]byte + snapMux sync.Mutex + snaps *snapshot.Tree + snap snapshot.Snapshot + storeParallelLock sync.RWMutex + snapParallelLock sync.RWMutex // for parallel mode, for main StateDB, slot will read snapshot, while processor will write. + snapDestructs map[common.Address]struct{} + snapAccounts map[common.Address][]byte + snapStorage map[common.Address]map[string][]byte // This map holds 'live' objects, which will get modified while processing a state transition. stateObjects map[common.Address]*StateObject @@ -254,38 +279,18 @@ func NewWithSharedPool(root common.Hash, db Database, snaps *snapshot.Tree) (*St return statedb, nil } -// NewSlotDB creates a new State DB based on the provided StateDB. -// With parallel, each execution slot would have its own StateDB. -func NewSlotDB(db *StateDB, systemAddr common.Address, baseTxIndex int, keepSystem bool) *StateDB { - slotDB := db.CopyForSlot() - slotDB.originalRoot = db.originalRoot - slotDB.parallel.baseTxIndex = baseTxIndex - slotDB.parallel.systemAddress = systemAddr - slotDB.parallel.systemAddressOpsCount = 0 - slotDB.parallel.keepSystemAddressBalance = keepSystem - - // All transactions will pay gas fee to the systemAddr at the end, this address is - // deemed to conflict, we handle it specially, clear it now and set it back to the main - // StateDB later; - // But there are transactions that will try to read systemAddr's balance, such as: - // https://bscscan.com/tx/0xcd69755be1d2f55af259441ff5ee2f312830b8539899e82488a21e85bc121a2a. - // It will trigger transaction redo and keepSystem will be marked as true. - if !keepSystem { - slotDB.SetBalance(systemAddr, big.NewInt(0)) - } - - return slotDB -} - func newStateDB(root common.Hash, db Database, snaps *snapshot.Tree) (*StateDB, error) { sdb := &StateDB{ - db: db, - originalRoot: root, - snaps: snaps, - stateObjects: make(map[common.Address]*StateObject, defaultNumOfSlots), - parallel: ParallelState{}, + db: db, + originalRoot: root, + snaps: snaps, + stateObjects: make(map[common.Address]*StateObject, defaultNumOfSlots), + parallel: ParallelState{ + SlotIndex: -1, + }, stateObjectsPending: make(map[common.Address]struct{}, defaultNumOfSlots), stateObjectsDirty: make(map[common.Address]struct{}, defaultNumOfSlots), + txIndex: -1, logs: make(map[common.Hash][]*types.Log, defaultNumOfSlots), preimages: make(map[common.Hash][]byte), journal: newJournal(), @@ -306,6 +311,7 @@ func newStateDB(root common.Hash, db Database, snaps *snapshot.Tree) (*StateDB, return nil, err } sdb.trie = tr + sdb.EnableWriteOnSharedStorage() // fixme:remove when s.originStorage[key] is enabled return sdb, nil } @@ -313,188 +319,12 @@ func (s *StateDB) EnableWriteOnSharedStorage() { s.writeOnSharedStorage = true } -func (s *StateDB) getStateObjectFromStateObjects(addr common.Address) (*StateObject, bool) { - if s.parallel.isSlotDB { - obj, ok := s.parallel.dirtiedStateObjectsInSlot[addr] - if ok { - return obj, ok - } - } - return s.loadStateObj(addr) -} - -// RevertSlotDB keep its read list for conflict detect and discard its state changes except its own balance change, -// if the transaction execution is reverted, -func (s *StateDB) RevertSlotDB(from common.Address) { - s.parallel.stateObjectsSuicidedInSlot = make(map[common.Address]struct{}) - s.parallel.stateChangesInSlot = make(map[common.Address]StateKeys) - s.parallel.balanceChangesInSlot = make(map[common.Address]struct{}, 1) - s.parallel.balanceChangesInSlot[from] = struct{}{} - s.parallel.addrStateChangesInSlot = make(map[common.Address]struct{}) - s.parallel.nonceChangesInSlot = make(map[common.Address]struct{}) -} - -// PrepareForParallel prepares for state db to be used in parallel execution mode. -func (s *StateDB) PrepareForParallel() { - s.isParallel = true - s.parallel.stateObjects = &StateObjectSyncMap{} +func (s *StateDB) getBaseStateDB() *StateDB { + return s } -// MergeSlotDB is for Parallel execution mode, when the transaction has been -// finalized(dirty -> pending) on execution slot, the execution results should be -// merged back to the main StateDB. -// And it will return and keep the slot's change list for later conflict detect. -func (s *StateDB) MergeSlotDB(slotDb *StateDB, slotReceipt *types.Receipt, txIndex int) SlotChangeList { - // receipt.Logs use unified log index within a block - // align slotDB's log index to the block stateDB's logSize - for _, l := range slotReceipt.Logs { - l.Index += s.logSize - } - s.logSize += slotDb.logSize - - // before merge, pay the gas fee first: AddBalance to consensus.SystemAddress - systemAddress := slotDb.parallel.systemAddress - if slotDb.parallel.keepSystemAddressBalance { - s.SetBalance(systemAddress, slotDb.GetBalance(systemAddress)) - } else { - s.AddBalance(systemAddress, slotDb.GetBalance(systemAddress)) - } - - // only merge dirty objects - addressesToPrefetch := make([][]byte, 0, len(slotDb.stateObjectsDirty)) - for addr := range slotDb.stateObjectsDirty { - if _, exist := s.stateObjectsDirty[addr]; !exist { - s.stateObjectsDirty[addr] = struct{}{} - } - // system address is EOA account, it should have no storage change - if addr == systemAddress { - continue - } - - // stateObjects: KV, balance, nonce... - dirtyObj, ok := slotDb.getStateObjectFromStateObjects(addr) - if !ok { - log.Error("parallel merge, but dirty object not exist!", "txIndex:", slotDb.txIndex, "addr", addr) - continue - } - mainObj, exist := s.loadStateObj(addr) - if !exist { - // addr not exist on main DB, do ownership transfer - dirtyObj.db = s - dirtyObj.finalise(true) // true: prefetch on dispatcher - s.storeStateObj(addr, dirtyObj) - delete(slotDb.parallel.dirtiedStateObjectsInSlot, addr) // transfer ownership - } else { - // addr already in main DB, do merge: balance, KV, code, State(create, suicide) - // can not do copy or ownership transfer directly, since dirtyObj could have outdated - // data(may be update within the conflict window) - - var newMainObj *StateObject - if _, created := slotDb.parallel.addrStateChangesInSlot[addr]; created { - // there are 3 kinds of state change: - // 1.Suicide - // 2.Empty Delete - // 3.createObject - // a.AddBalance,SetState to an unexist or deleted(suicide, empty delete) address. - // b.CreateAccount: like DAO the fork, regenerate a account carry its balance without KV - // For these state change, do ownership transafer for efficiency: - log.Debug("MergeSlotDB state object merge: addr state change") - dirtyObj.db = s - newMainObj = dirtyObj - delete(slotDb.parallel.dirtiedStateObjectsInSlot, addr) // transfer ownership - if dirtyObj.deleted { - // remove the addr from snapAccounts&snapStorage only when object is deleted. - // "deleted" is not equal to "snapDestructs", since createObject() will add an addr for - // snapDestructs to destroy previous object, while it will keep the addr in snapAccounts & snapAccounts - delete(s.snapAccounts, addr) - delete(s.snapStorage, addr) - } - } else { - // deepCopy a temporary *StateObject for safety, since slot could read the address, - // dispatch should avoid overwrite the StateObject directly otherwise, it could - // crash for: concurrent map iteration and map write - newMainObj = mainObj.deepCopy(s) - if _, balanced := slotDb.parallel.balanceChangesInSlot[addr]; balanced { - log.Debug("merge state object: Balance", - "newMainObj.Balance()", newMainObj.Balance(), - "dirtyObj.Balance()", dirtyObj.Balance()) - newMainObj.SetBalance(dirtyObj.Balance()) - } - if _, coded := slotDb.parallel.codeChangesInSlot[addr]; coded { - log.Debug("merge state object: Code") - newMainObj.code = dirtyObj.code - newMainObj.data.CodeHash = dirtyObj.data.CodeHash - newMainObj.dirtyCode = true - } - if keys, stated := slotDb.parallel.stateChangesInSlot[addr]; stated { - log.Debug("merge state object: KV") - newMainObj.MergeSlotObject(s.db, dirtyObj, keys) - } - // dirtyObj.Nonce() should not be less than newMainObj - newMainObj.setNonce(dirtyObj.Nonce()) - } - newMainObj.finalise(true) // true: prefetch on dispatcher - // update the object - s.storeStateObj(addr, newMainObj) - } - addressesToPrefetch = append(addressesToPrefetch, common.CopyBytes(addr[:])) // Copy needed for closure - } - - if s.prefetcher != nil && len(addressesToPrefetch) > 0 { - s.prefetcher.prefetch(s.originalRoot, addressesToPrefetch, emptyAddr) // prefetch for trie node of account - } - - for addr := range slotDb.stateObjectsPending { - if _, exist := s.stateObjectsPending[addr]; !exist { - s.stateObjectsPending[addr] = struct{}{} - } - } - - // slotDb.logs: logs will be kept in receipts, no need to do merge - - for hash, preimage := range slotDb.preimages { - s.preimages[hash] = preimage - } - if s.accessList != nil { - // fixme: accessList is not enabled yet, but it should use merge rather than overwrite Copy - s.accessList = slotDb.accessList.Copy() - } - - if slotDb.snaps != nil { - for k := range slotDb.snapDestructs { - // There could be a race condition for parallel transaction execution - // One transaction add balance 0 to an empty address, will delete it(delete empty is enabled). - // While another concurrent transaction could add a none-zero balance to it, make it not empty - // We fixed it by add a addr state read record for add balance 0 - s.snapDestructs[k] = struct{}{} - } - - // slotDb.snapAccounts should be empty, comment out and to be deleted later - // for k, v := range slotDb.snapAccounts { - // s.snapAccounts[k] = v - // } - // slotDb.snapStorage should be empty, comment out and to be deleted later - // for k, v := range slotDb.snapStorage { - // temp := make(map[string][]byte) - // for kk, vv := range v { - // temp[kk] = vv - // } - // s.snapStorage[k] = temp - // } - } - - // to create a new object to store change list for conflict detect, - // since slot db reuse is disabled, we do not need to do copy. - changeList := SlotChangeList{ - TxIndex: txIndex, - StateObjectSuicided: slotDb.parallel.stateObjectsSuicidedInSlot, - StateChangeSet: slotDb.parallel.stateChangesInSlot, - BalanceChangeSet: slotDb.parallel.balanceChangesInSlot, - CodeChangeSet: slotDb.parallel.codeChangesInSlot, - AddrStateChangeSet: slotDb.parallel.addrStateChangesInSlot, - NonceChangeSet: slotDb.parallel.nonceChangesInSlot, - } - return changeList +func (s *StateDB) getStateObjectFromStateObjects(addr common.Address) (*StateObject, bool) { + return s.loadStateObj(addr) } // StartPrefetcher initializes a new trie prefetcher to pull in nodes from the @@ -647,37 +477,41 @@ func (s *StateDB) SubRefund(gas uint64) { // Exist reports whether the given account address exists in the state. // Notably this also returns true for suicided accounts. func (s *StateDB) Exist(addr common.Address) bool { - return s.getStateObject(addr) != nil + exist := s.getStateObject(addr) != nil + return exist } // Empty returns whether the state object is either non-existent // or empty according to the EIP161 specification (balance = nonce = code = 0) func (s *StateDB) Empty(addr common.Address) bool { so := s.getStateObject(addr) - return so == nil || so.empty() + empty := (so == nil || so.empty()) + return empty } // GetBalance retrieves the balance from the given address or 0 if object not found +// GetFrom the dirty list => from unconfirmed DB => get from main stateDB func (s *StateDB) GetBalance(addr common.Address) *big.Int { - if s.parallel.isSlotDB { - s.parallel.balanceReadsInSlot[addr] = struct{}{} - if addr == s.parallel.systemAddress { - s.parallel.systemAddressOpsCount++ - } - } + balance := common.Big0 stateObject := s.getStateObject(addr) if stateObject != nil { - return stateObject.Balance() + balance = stateObject.Balance() } - return common.Big0 + return balance +} + +func (s *StateDB) GetBalanceOpCode(addr common.Address) *big.Int { + return s.GetBalance(addr) } func (s *StateDB) GetNonce(addr common.Address) uint64 { + var nonce uint64 = 0 stateObject := s.getStateObject(addr) if stateObject != nil { - return stateObject.Nonce() + nonce = stateObject.Nonce() } - return 0 + + return nonce } // TxIndex returns the current transaction index set by Prepare. @@ -695,81 +529,45 @@ func (s *StateDB) BaseTxIndex() int { return s.parallel.baseTxIndex } -func (s *StateDB) CodeReadsInSlot() map[common.Address]struct{} { - return s.parallel.codeReadsInSlot -} - -func (s *StateDB) AddressReadsInSlot() map[common.Address]struct{} { - return s.parallel.addrStateReadsInSlot -} - -func (s *StateDB) StateReadsInSlot() map[common.Address]StateKeys { - return s.parallel.stateReadsInSlot -} - -func (s *StateDB) BalanceReadsInSlot() map[common.Address]struct{} { - return s.parallel.balanceReadsInSlot -} - -// For most of the transactions, systemAddressOpsCount should be 2: -// one for SetBalance(0) on NewSlotDB() -// the other is for AddBalance(GasFee) at the end. -// (systemAddressOpsCount > 2) means the transaction tries to access systemAddress, in -// this case, we should redo and keep its balance on NewSlotDB() -func (s *StateDB) SystemAddressRedo() bool { - return s.parallel.systemAddressOpsCount > 2 -} - func (s *StateDB) GetCode(addr common.Address) []byte { - if s.parallel.isSlotDB { - s.parallel.codeReadsInSlot[addr] = struct{}{} - } - stateObject := s.getStateObject(addr) + var code []byte if stateObject != nil { - return stateObject.Code(s.db) + code = stateObject.Code(s.db) } - return nil + return code } func (s *StateDB) GetCodeSize(addr common.Address) int { - if s.parallel.isSlotDB { - s.parallel.codeReadsInSlot[addr] = struct{}{} // code size is part of code - } - + var codeSize int = 0 stateObject := s.getStateObject(addr) if stateObject != nil { - return stateObject.CodeSize(s.db) + codeSize = stateObject.CodeSize(s.db) } - return 0 + return codeSize } +// return value of GetCodeHash: +// - common.Hash{}: the address does not exist +// - emptyCodeHash: the address exist, but code is empty +// - others: the address exist, and code is not empty func (s *StateDB) GetCodeHash(addr common.Address) common.Hash { - if s.parallel.isSlotDB { - s.parallel.codeReadsInSlot[addr] = struct{}{} // code hash is part of code - } - stateObject := s.getStateObject(addr) - if stateObject == nil { - return common.Hash{} + codeHash := common.Hash{} + if stateObject != nil { + codeHash = common.BytesToHash(stateObject.CodeHash()) } - return common.BytesToHash(stateObject.CodeHash()) + return codeHash } // GetState retrieves a value from the given account's storage trie. func (s *StateDB) GetState(addr common.Address, hash common.Hash) common.Hash { - if s.parallel.isSlotDB { - if s.parallel.stateReadsInSlot[addr] == nil { - s.parallel.stateReadsInSlot[addr] = make(map[common.Hash]struct{}, defaultNumOfSlots) - } - s.parallel.stateReadsInSlot[addr][hash] = struct{}{} - } - stateObject := s.getStateObject(addr) + val := common.Hash{} if stateObject != nil { - return stateObject.GetState(s.db, hash) + val = stateObject.GetState(s.db, hash) } - return common.Hash{} + return val } // GetProof returns the Merkle proof for a given account. @@ -811,18 +609,12 @@ func (s *StateDB) GetStorageProofByHash(a common.Address, key common.Hash) ([][] // GetCommittedState retrieves a value from the given account's committed storage trie. func (s *StateDB) GetCommittedState(addr common.Address, hash common.Hash) common.Hash { - if s.parallel.isSlotDB { - if s.parallel.stateReadsInSlot[addr] == nil { - s.parallel.stateReadsInSlot[addr] = make(map[common.Hash]struct{}, defaultNumOfSlots) - } - s.parallel.stateReadsInSlot[addr][hash] = struct{}{} - } - stateObject := s.getStateObject(addr) + val := common.Hash{} if stateObject != nil { - return stateObject.GetCommittedState(s.db, hash) + val = stateObject.GetCommittedState(s.db, hash) } - return common.Hash{} + return val } // Database retrieves the low level database supporting the lower level trie ops. @@ -856,58 +648,16 @@ func (s *StateDB) HasSuicided(addr common.Address) bool { // AddBalance adds amount to the account associated with addr. func (s *StateDB) AddBalance(addr common.Address, amount *big.Int) { - if s.parallel.isSlotDB { - if amount.Sign() != 0 { - s.parallel.balanceChangesInSlot[addr] = struct{}{} - // add balance will perform a read operation first - s.parallel.balanceReadsInSlot[addr] = struct{}{} - } else { - // if amount == 0, no balance change, but there is still an empty check. - // take this empty check as addr state read(create, suicide, empty delete) - s.parallel.addrStateReadsInSlot[addr] = struct{}{} - } - if addr == s.parallel.systemAddress { - s.parallel.systemAddressOpsCount++ - } - } - stateObject := s.GetOrNewStateObject(addr) if stateObject != nil { - if s.parallel.isSlotDB { - if _, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; !ok { - newStateObject := stateObject.deepCopy(s) - newStateObject.AddBalance(amount) - s.parallel.dirtiedStateObjectsInSlot[addr] = newStateObject - return - } - } stateObject.AddBalance(amount) } } // SubBalance subtracts amount from the account associated with addr. func (s *StateDB) SubBalance(addr common.Address, amount *big.Int) { - if s.parallel.isSlotDB { - if amount.Sign() != 0 { - s.parallel.balanceChangesInSlot[addr] = struct{}{} - // unlike add, sub 0 balance will not touch empty object - s.parallel.balanceReadsInSlot[addr] = struct{}{} - } - if addr == s.parallel.systemAddress { - s.parallel.systemAddressOpsCount++ - } - } - stateObject := s.GetOrNewStateObject(addr) if stateObject != nil { - if s.parallel.isSlotDB { - if _, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; !ok { - newStateObject := stateObject.deepCopy(s) - newStateObject.SubBalance(amount) - s.parallel.dirtiedStateObjectsInSlot[addr] = newStateObject - return - } - } stateObject.SubBalance(amount) } } @@ -915,46 +665,13 @@ func (s *StateDB) SubBalance(addr common.Address, amount *big.Int) { func (s *StateDB) SetBalance(addr common.Address, amount *big.Int) { stateObject := s.GetOrNewStateObject(addr) if stateObject != nil { - if s.parallel.isSlotDB { - s.parallel.balanceChangesInSlot[addr] = struct{}{} - if addr == s.parallel.systemAddress { - s.parallel.systemAddressOpsCount++ - } - - if _, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; !ok { - newStateObject := stateObject.deepCopy(s) - newStateObject.SetBalance(amount) - s.parallel.dirtiedStateObjectsInSlot[addr] = newStateObject - return - } - } stateObject.SetBalance(amount) } } -// Generally sender's nonce will be increased by 1 for each transaction -// But if the contract tries to create a new contract, its nonce will be advanced -// for each opCreate or opCreate2. Nonce is key to transaction execution, once it is -// changed for contract created, the concurrent transaction will be marked invalid if -// they accessed the address. -func (s *StateDB) NonceChanged(addr common.Address) { - if s.parallel.isSlotDB { - log.Debug("NonceChanged", "txIndex", s.txIndex, "addr", addr) - s.parallel.nonceChangesInSlot[addr] = struct{}{} - } -} - func (s *StateDB) SetNonce(addr common.Address, nonce uint64) { stateObject := s.GetOrNewStateObject(addr) if stateObject != nil { - if s.parallel.isSlotDB { - if _, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; !ok { - newStateObject := stateObject.deepCopy(s) - newStateObject.SetNonce(nonce) - s.parallel.dirtiedStateObjectsInSlot[addr] = newStateObject - return - } - } stateObject.SetNonce(nonce) } } @@ -962,46 +679,14 @@ func (s *StateDB) SetNonce(addr common.Address, nonce uint64) { func (s *StateDB) SetCode(addr common.Address, code []byte) { stateObject := s.GetOrNewStateObject(addr) if stateObject != nil { - if s.parallel.isSlotDB { - s.parallel.codeChangesInSlot[addr] = struct{}{} - - if _, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; !ok { - newStateObject := stateObject.deepCopy(s) - newStateObject.SetCode(crypto.Keccak256Hash(code), code) - s.parallel.dirtiedStateObjectsInSlot[addr] = newStateObject - return - } - } - stateObject.SetCode(crypto.Keccak256Hash(code), code) + codeHash := crypto.Keccak256Hash(code) + stateObject.SetCode(codeHash, code) } } func (s *StateDB) SetState(addr common.Address, key, value common.Hash) { stateObject := s.GetOrNewStateObject(addr) if stateObject != nil { - if s.parallel.isSlotDB { - if s.parallel.baseTxIndex+1 == s.txIndex { - // we check if state is unchanged - // only when current transaction is the next transaction to be committed - if stateObject.GetState(s.db, key) == value { - log.Debug("Skip set same state", "baseTxIndex", s.parallel.baseTxIndex, - "txIndex", s.txIndex) - return - } - } - - if s.parallel.stateChangesInSlot[addr] == nil { - s.parallel.stateChangesInSlot[addr] = make(StateKeys, defaultNumOfSlots) - } - s.parallel.stateChangesInSlot[addr][key] = struct{}{} - - if _, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; !ok { - newStateObject := stateObject.deepCopy(s) - newStateObject.SetState(s.db, key, value) - s.parallel.dirtiedStateObjectsInSlot[addr] = newStateObject - return - } - } stateObject.SetState(s.db, key, value) } } @@ -1009,7 +694,7 @@ func (s *StateDB) SetState(addr common.Address, key, value common.Hash) { // SetStorage replaces the entire storage for the specified account with given // storage. This function should only be used for debugging. func (s *StateDB) SetStorage(addr common.Address, storage map[common.Hash]common.Hash) { - stateObject := s.GetOrNewStateObject(addr) + stateObject := s.GetOrNewStateObject(addr) // fixme: parallel mode? if stateObject != nil { stateObject.SetStorage(storage) } @@ -1021,30 +706,22 @@ func (s *StateDB) SetStorage(addr common.Address, storage map[common.Hash]common // The account's state object is still available until the state is committed, // getStateObject will return a non-nil account after Suicide. func (s *StateDB) Suicide(addr common.Address) bool { - stateObject := s.getStateObject(addr) + var stateObject *StateObject if stateObject == nil { - return false + // 3.Try to get from main StateDB + stateObject = s.getStateObject(addr) + if stateObject == nil { + log.Error("Suicide addr not exist", "txIndex", s.txIndex, "addr", addr) + return false + } } s.journal.append(suicideChange{ account: &addr, - prev: stateObject.suicided, - prevbalance: new(big.Int).Set(stateObject.Balance()), + prev: stateObject.suicided, // todo: must be false? + prevbalance: new(big.Int).Set(s.GetBalance(addr)), }) - if s.parallel.isSlotDB { - s.parallel.stateObjectsSuicidedInSlot[addr] = struct{}{} - s.parallel.addrStateChangesInSlot[addr] = struct{}{} - if _, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; !ok { - // do copy-on-write for suicide "write" - newStateObject := stateObject.deepCopy(s) - newStateObject.markSuicided() - newStateObject.data.Balance = new(big.Int) - s.parallel.dirtiedStateObjectsInSlot[addr] = newStateObject - return true - } - } - stateObject.markSuicided() stateObject.data.Balance = new(big.Int) return true @@ -1092,30 +769,15 @@ func (s *StateDB) deleteStateObject(obj *StateObject) { // the object is not found or was deleted in this execution context. If you need // to differentiate between non-existent/just-deleted, use getDeletedStateObject. func (s *StateDB) getStateObject(addr common.Address) *StateObject { - if s.parallel.isSlotDB { - s.parallel.addrStateReadsInSlot[addr] = struct{}{} - } - if obj := s.getDeletedStateObject(addr); obj != nil && !obj.deleted { return obj } return nil } -// getDeletedStateObject is similar to getStateObject, but instead of returning -// nil for a deleted state object, it returns the actual object with the deleted -// flag set. This is needed by the state journal to revert to the correct s- -// destructed object instead of wiping all knowledge about the state object. -func (s *StateDB) getDeletedStateObject(addr common.Address) *StateObject { - // Prefer live objects if any is available - if obj, _ := s.getStateObjectFromStateObjects(addr); obj != nil { - return obj - } +func (s *StateDB) getStateObjectFromSnapshotOrTrie(addr common.Address) (data *Account, ok bool) { + var err error // If no live objects are available, attempt to use snapshots - var ( - data *Account - err error - ) if s.snap != nil { if metrics.EnabledExpensive { defer func(start time.Time) { s.SnapshotAccountReads += time.Since(start) }(time.Now()) @@ -1123,7 +785,7 @@ func (s *StateDB) getDeletedStateObject(addr common.Address) *StateObject { var acc *snapshot.Account if acc, err = s.snap.Account(crypto.HashData(s.hasher, addr.Bytes())); err == nil { if acc == nil { - return nil + return nil, false } data = &Account{ Nonce: acc.Nonce, @@ -1145,7 +807,7 @@ func (s *StateDB) getDeletedStateObject(addr common.Address) *StateObject { tr, err := s.db.OpenTrie(s.originalRoot) if err != nil { s.setError(fmt.Errorf("failed to open trie tree")) - return nil + return nil, false } s.trie = tr } @@ -1155,71 +817,99 @@ func (s *StateDB) getDeletedStateObject(addr common.Address) *StateObject { enc, err := s.trie.TryGet(addr.Bytes()) if err != nil { s.setError(fmt.Errorf("getDeleteStateObject (%x) error: %v", addr.Bytes(), err)) - return nil + return nil, false } if len(enc) == 0 { - return nil + return nil, false } data = new(Account) if err := rlp.DecodeBytes(enc, data); err != nil { log.Error("Failed to decode state object", "addr", addr, "err", err) - return nil + return nil, false } } - // Insert into the live set - obj := newObject(s, addr, *data) - s.SetStateObject(obj) - return obj + return data, true } -func (s *StateDB) SetStateObject(object *StateObject) { - if s.parallel.isSlotDB { - s.parallel.dirtiedStateObjectsInSlot[object.Address()] = object - } else { - s.storeStateObj(object.Address(), object) +// getDeletedStateObject is similar to getStateObject, but instead of returning +// nil for a deleted state object, it returns the actual object with the deleted +// flag set. This is needed by the state journal to revert to the correct s- +// destructed object instead of wiping all knowledge about the state object. +func (s *StateDB) getDeletedStateObject(addr common.Address) *StateObject { + // Prefer live objects if any is available + if obj, _ := s.getStateObjectFromStateObjects(addr); obj != nil { + return obj + } + data, ok := s.getStateObjectFromSnapshotOrTrie(addr) + if !ok { + return nil } + // Insert into the live set + // if obj, ok := s.loadStateObj(addr); ok { + // fixme: concurrent not safe, merge could update it... + // return obj + //} + obj := newObject(s, s.isParallel, addr, *data) + s.storeStateObj(addr, obj) + return obj } +// func (s *StateDB) SetStateObject(object *StateObject) { +// s.storeStateObj(object.Address(), object) +// } + // GetOrNewStateObject retrieves a state object or create a new state object if nil. +// dirtyInSlot -> Unconfirmed DB -> main DB -> snapshot, no? create one func (s *StateDB) GetOrNewStateObject(addr common.Address) *StateObject { - stateObject := s.getStateObject(addr) + var stateObject *StateObject = nil if stateObject == nil { - stateObject, _ = s.createObject(addr) + stateObject = s.getStateObject(addr) + } + if stateObject == nil || stateObject.deleted || stateObject.suicided { + stateObject = s.createObject(addr) } return stateObject } // createObject creates a new state object. If there is an existing account with // the given address, it is overwritten and returned as the second return value. -func (s *StateDB) createObject(addr common.Address) (newobj, prev *StateObject) { - if s.parallel.isSlotDB { - s.parallel.addrStateReadsInSlot[addr] = struct{}{} // will try to get the previous object. - s.parallel.addrStateChangesInSlot[addr] = struct{}{} - } - - prev = s.getDeletedStateObject(addr) // Note, prev might have been deleted, we need that! +// prev is used for CreateAccount to get its balance +// Parallel mode: +// if prev in dirty: revert is ok +// if prev in unconfirmed DB: addr state read record, revert should not put it back +// if prev in main DB: addr state read record, revert should not put it back +// if pre no exist: addr state read record, + +// `prev` is used to handle revert, to recover with the `prev` object +// In Parallel mode, we only need to recover to `prev` in SlotDB, +// a.if it is not in SlotDB, `revert` will remove it from the SlotDB +// b.if it is exist in SlotDB, `revert` will recover to the `prev` in SlotDB +// c.as `snapDestructs` it is the same +func (s *StateDB) createObject(addr common.Address) (newobj *StateObject) { + prev := s.getDeletedStateObject(addr) // Note, prev might have been deleted, we need that! var prevdestruct bool + if s.snap != nil && prev != nil { + s.snapParallelLock.Lock() // fixme: with new dispatch policy, the ending Tx could runing, while the block have processed. _, prevdestruct = s.snapDestructs[prev.address] if !prevdestruct { - // createObject for deleted object will destroy the previous trie node first - // and update the trie tree with the new object on block commit. + // To destroy the previous trie node first and update the trie tree + // with the new object on block commit. s.snapDestructs[prev.address] = struct{}{} } + s.snapParallelLock.Unlock() } - newobj = newObject(s, addr, Account{}) + newobj = newObject(s, s.isParallel, addr, Account{}) newobj.setNonce(0) // sets the object to dirty if prev == nil { s.journal.append(createObjectChange{account: &addr}) } else { s.journal.append(resetObjectChange{prev: prev, prevdestruct: prevdestruct}) } - s.SetStateObject(newobj) - if prev != nil && !prev.deleted { - return newobj, prev - } - return newobj, nil + + s.storeStateObj(addr, newobj) + return newobj } // CreateAccount explicitly creates a state object. If a state object with the address @@ -1233,14 +923,12 @@ func (s *StateDB) createObject(addr common.Address) (newobj, prev *StateObject) // // Carrying over the balance ensures that Ether doesn't disappear. func (s *StateDB) CreateAccount(addr common.Address) { - newObj, prev := s.createObject(addr) - if prev != nil { - newObj.setBalance(prev.data.Balance) - } - if s.parallel.isSlotDB { - s.parallel.balanceReadsInSlot[addr] = struct{}{} // read the balance of previous object - s.parallel.dirtiedStateObjectsInSlot[addr] = newObj - } + // no matter it is got from dirty, unconfirmed or main DB + // if addr not exist, preBalance will be common.Big0, it is same as new(big.Int) which + // is the value newObject(), + preBalance := s.GetBalance(addr) + newObj := s.createObject(addr) + newObj.setBalance(new(big.Int).Set(preBalance)) // new big.Int for newObj } func (s *StateDB) ForEachStorage(addr common.Address, cb func(key, value common.Hash) bool) error { @@ -1252,7 +940,7 @@ func (s *StateDB) ForEachStorage(addr common.Address, cb func(key, value common. for it.Next() { key := common.BytesToHash(s.trie.GetKey(it.Key)) - if value, dirty := so.dirtyStorage[key]; dirty { + if value, dirty := so.dirtyStorage.GetValue(key); dirty { if !cb(key, value) { return nil } @@ -1378,47 +1066,216 @@ func (s *StateDB) Copy() *StateDB { return state } -// Copy all the basic fields, initialize the memory ones -func (s *StateDB) CopyForSlot() *StateDB { - parallel := ParallelState{ - // use base(dispatcher) slot db's stateObjects. - // It is a SyncMap, only readable to slot, not writable - stateObjects: s.parallel.stateObjects, - stateObjectsSuicidedInSlot: make(map[common.Address]struct{}, 10), - codeReadsInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), - codeChangesInSlot: make(map[common.Address]struct{}, 10), - stateChangesInSlot: make(map[common.Address]StateKeys, defaultNumOfSlots), - stateReadsInSlot: make(map[common.Address]StateKeys, defaultNumOfSlots), - balanceChangesInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), - balanceReadsInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), - addrStateReadsInSlot: make(map[common.Address]struct{}, defaultNumOfSlots), - addrStateChangesInSlot: make(map[common.Address]struct{}, 10), - nonceChangesInSlot: make(map[common.Address]struct{}, 10), - isSlotDB: true, - dirtiedStateObjectsInSlot: make(map[common.Address]*StateObject, defaultNumOfSlots), +var journalPool = sync.Pool{ + New: func() interface{} { + return &journal{ + dirties: make(map[common.Address]int, defaultNumOfSlots), + entries: make([]journalEntry, 0, defaultNumOfSlots), + } + }, +} + +var addressToStructPool = sync.Pool{ + New: func() interface{} { return make(map[common.Address]struct{}, defaultNumOfSlots) }, +} + +var addressToStateKeysPool = sync.Pool{ + New: func() interface{} { return make(map[common.Address]StateKeys, defaultNumOfSlots) }, +} + +var addressToStoragePool = sync.Pool{ + New: func() interface{} { return make(map[common.Address]Storage, defaultNumOfSlots) }, +} + +var addressToStateObjectsPool = sync.Pool{ + New: func() interface{} { return make(map[common.Address]*StateObject, defaultNumOfSlots) }, +} + +var balancePool = sync.Pool{ + New: func() interface{} { return make(map[common.Address]*big.Int, defaultNumOfSlots) }, +} + +var addressToHashPool = sync.Pool{ + New: func() interface{} { return make(map[common.Address]common.Hash, defaultNumOfSlots) }, +} + +var addressToBytesPool = sync.Pool{ + New: func() interface{} { return make(map[common.Address][]byte, defaultNumOfSlots) }, +} + +var addressToBoolPool = sync.Pool{ + New: func() interface{} { return make(map[common.Address]bool, defaultNumOfSlots) }, +} + +var addressToUintPool = sync.Pool{ + New: func() interface{} { return make(map[common.Address]uint64, defaultNumOfSlots) }, +} + +var snapStoragePool = sync.Pool{ + New: func() interface{} { return make(map[common.Address]map[string][]byte, defaultNumOfSlots) }, +} + +var snapStorageValuePool = sync.Pool{ + New: func() interface{} { return make(map[string][]byte, defaultNumOfSlots) }, +} + +var logsPool = sync.Pool{ + New: func() interface{} { return make(map[common.Hash][]*types.Log, defaultNumOfSlots) }, +} + +func (s *StateDB) PutSyncPool() { + for key := range s.parallel.codeReadsInSlot { + delete(s.parallel.codeReadsInSlot, key) } - state := &StateDB{ - db: s.db, - trie: s.db.CopyTrie(s.trie), - stateObjects: make(map[common.Address]*StateObject), // replaced by parallel.stateObjects in parallel mode - stateObjectsPending: make(map[common.Address]struct{}, defaultNumOfSlots), - stateObjectsDirty: make(map[common.Address]struct{}, defaultNumOfSlots), - refund: s.refund, // should be 0 - logs: make(map[common.Hash][]*types.Log, defaultNumOfSlots), - logSize: 0, - preimages: make(map[common.Hash][]byte, len(s.preimages)), - journal: newJournal(), - hasher: crypto.NewKeccakState(), - snapDestructs: make(map[common.Address]struct{}), - snapAccounts: make(map[common.Address][]byte), - snapStorage: make(map[common.Address]map[string][]byte), - isParallel: true, - parallel: parallel, + addressToStructPool.Put(s.parallel.codeReadsInSlot) + + for key := range s.parallel.codeHashReadsInSlot { + delete(s.parallel.codeHashReadsInSlot, key) } + addressToHashPool.Put(s.parallel.codeHashReadsInSlot) - for hash, preimage := range s.preimages { - state.preimages[hash] = preimage + for key := range s.parallel.codeChangesInSlot { + delete(s.parallel.codeChangesInSlot, key) + } + addressToStructPool.Put(s.parallel.codeChangesInSlot) + + for key := range s.parallel.kvChangesInSlot { + delete(s.parallel.kvChangesInSlot, key) + } + addressToStateKeysPool.Put(s.parallel.kvChangesInSlot) + + for key := range s.parallel.kvReadsInSlot { + delete(s.parallel.kvReadsInSlot, key) + } + addressToStoragePool.Put(s.parallel.kvReadsInSlot) + + for key := range s.parallel.balanceChangesInSlot { + delete(s.parallel.balanceChangesInSlot, key) + } + addressToStructPool.Put(s.parallel.balanceChangesInSlot) + + for key := range s.parallel.balanceReadsInSlot { + delete(s.parallel.balanceReadsInSlot, key) + } + balancePool.Put(s.parallel.balanceReadsInSlot) + + for key := range s.parallel.addrStateReadsInSlot { + delete(s.parallel.addrStateReadsInSlot, key) + } + addressToBoolPool.Put(s.parallel.addrStateReadsInSlot) + + for key := range s.parallel.addrStateChangesInSlot { + delete(s.parallel.addrStateChangesInSlot, key) + } + addressToBoolPool.Put(s.parallel.addrStateChangesInSlot) + + for key := range s.parallel.nonceChangesInSlot { + delete(s.parallel.nonceChangesInSlot, key) + } + addressToStructPool.Put(s.parallel.nonceChangesInSlot) + + for key := range s.parallel.nonceReadsInSlot { + delete(s.parallel.nonceReadsInSlot, key) + } + addressToUintPool.Put(s.parallel.nonceReadsInSlot) + + for key := range s.parallel.addrSnapDestructsReadsInSlot { + delete(s.parallel.addrSnapDestructsReadsInSlot, key) + } + addressToBoolPool.Put(s.parallel.addrSnapDestructsReadsInSlot) + + for key := range s.parallel.dirtiedStateObjectsInSlot { + delete(s.parallel.dirtiedStateObjectsInSlot, key) + } + addressToStateObjectsPool.Put(s.parallel.dirtiedStateObjectsInSlot) + + for key := range s.stateObjectsPending { + delete(s.stateObjectsPending, key) + } + addressToStructPool.Put(s.stateObjectsPending) + + for key := range s.stateObjectsDirty { + delete(s.stateObjectsDirty, key) + } + addressToStructPool.Put(s.stateObjectsDirty) + + for key := range s.logs { + delete(s.logs, key) + } + logsPool.Put(s.logs) + + for key := range s.journal.dirties { + delete(s.journal.dirties, key) + } + s.journal.entries = s.journal.entries[:0] + journalPool.Put(s.journal) + + for key := range s.snapDestructs { + delete(s.snapDestructs, key) + } + addressToStructPool.Put(s.snapDestructs) + + for key := range s.snapAccounts { + delete(s.snapAccounts, key) + } + addressToBytesPool.Put(s.snapAccounts) + + for key, storage := range s.snapStorage { + for key := range storage { + delete(storage, key) + } + snapStorageValuePool.Put(storage) + delete(s.snapStorage, key) + } + snapStoragePool.Put(s.snapStorage) +} + +// CopyForSlot copy all the basic fields, initialize the memory ones +func (s *StateDB) CopyForSlot() *ParallelStateDB { + parallel := ParallelState{ + // use base(dispatcher) slot db's stateObjects. + // It is a SyncMap, only readable to slot, not writable + stateObjects: s.parallel.stateObjects, + codeReadsInSlot: addressToBytesPool.Get().(map[common.Address][]byte), + codeHashReadsInSlot: addressToHashPool.Get().(map[common.Address]common.Hash), + codeChangesInSlot: addressToStructPool.Get().(map[common.Address]struct{}), + kvChangesInSlot: addressToStateKeysPool.Get().(map[common.Address]StateKeys), + kvReadsInSlot: addressToStoragePool.Get().(map[common.Address]Storage), + balanceChangesInSlot: addressToStructPool.Get().(map[common.Address]struct{}), + balanceReadsInSlot: balancePool.Get().(map[common.Address]*big.Int), + addrStateReadsInSlot: addressToBoolPool.Get().(map[common.Address]bool), + addrStateChangesInSlot: addressToBoolPool.Get().(map[common.Address]bool), + nonceChangesInSlot: addressToStructPool.Get().(map[common.Address]struct{}), + nonceReadsInSlot: addressToUintPool.Get().(map[common.Address]uint64), + addrSnapDestructsReadsInSlot: addressToBoolPool.Get().(map[common.Address]bool), + isSlotDB: true, + dirtiedStateObjectsInSlot: addressToStateObjectsPool.Get().(map[common.Address]*StateObject), + } + state := &ParallelStateDB{ + StateDB: StateDB{ + db: s.db, + trie: nil, // Parallel StateDB can not access trie, since it is concurrent safe. + stateObjects: make(map[common.Address]*StateObject), // replaced by parallel.stateObjects in parallel mode + stateObjectsPending: addressToStructPool.Get().(map[common.Address]struct{}), + stateObjectsDirty: addressToStructPool.Get().(map[common.Address]struct{}), + refund: 0, // should be 0 + logs: logsPool.Get().(map[common.Hash][]*types.Log), + logSize: 0, + preimages: make(map[common.Hash][]byte, len(s.preimages)), + journal: journalPool.Get().(*journal), + hasher: crypto.NewKeccakState(), + isParallel: true, + parallel: parallel, + }, + wbnbMakeUp: true, + // wbnbBalanceAccessed: 0, + // wbnbBalanceAccessedExpected: 0, + balanceUpdateDepth: 0, } + // no need to copy preimages, comment out and remove later + // for hash, preimage := range s.preimages { + // state.preimages[hash] = preimage + // } if s.snaps != nil { // In order for the miner to be able to use and make additions @@ -1428,27 +1285,33 @@ func (s *StateDB) CopyForSlot() *StateDB { state.snaps = s.snaps state.snap = s.snap // deep copy needed - state.snapDestructs = make(map[common.Address]struct{}) + state.snapDestructs = addressToStructPool.Get().(map[common.Address]struct{}) + s.snapParallelLock.RLock() for k, v := range s.snapDestructs { state.snapDestructs[k] = v } - // - state.snapAccounts = make(map[common.Address][]byte) - for k, v := range s.snapAccounts { - state.snapAccounts[k] = v - } - state.snapStorage = make(map[common.Address]map[string][]byte) - for k, v := range s.snapStorage { - temp := make(map[string][]byte) - for kk, vv := range v { - temp[kk] = vv - } - state.snapStorage[k] = temp - } + s.snapParallelLock.RUnlock() + // snapAccounts is useless in SlotDB, comment out and remove later + // state.snapAccounts = make(map[common.Address][]byte) // snapAccountPool.Get().(map[common.Address][]byte) + // for k, v := range s.snapAccounts { + // state.snapAccounts[k] = v + // } + + // snapStorage is useless in SlotDB either, it is updated on updateTrie, which is validation phase to update the snapshot of a finalized block. + // state.snapStorage = snapStoragePool.Get().(map[common.Address]map[string][]byte) + // for k, v := range s.snapStorage { + // temp := snapStorageValuePool.Get().(map[string][]byte) + // for kk, vv := range v { + // temp[kk] = vv + // } + // state.snapStorage[k] = temp + // } + // trie prefetch should be done by dispacther on StateObject Merge, // disable it in parallel slot // state.prefetcher = s.prefetcher } + return state } @@ -1495,10 +1358,22 @@ func (s *StateDB) WaitPipeVerification() error { // Finalise finalises the state by removing the s destructed objects and clears // the journal as well as the refunds. Finalise, however, will not push any updates // into the tries just yet. Only IntermediateRoot or Commit will do that. -func (s *StateDB) Finalise(deleteEmptyObjects bool) { +func (s *StateDB) Finalise(deleteEmptyObjects bool) { // fixme: concurrent safe... addressesToPrefetch := make([][]byte, 0, len(s.journal.dirties)) for addr := range s.journal.dirties { - obj, exist := s.getStateObjectFromStateObjects(addr) + var obj *StateObject + var exist bool + if s.parallel.isSlotDB { + obj = s.parallel.dirtiedStateObjectsInSlot[addr] + if obj != nil { + exist = true + } else { + log.Error("StateDB Finalise dirty addr not in dirtiedStateObjectsInSlot", + "addr", addr) + } + } else { + obj, exist = s.getStateObjectFromStateObjects(addr) + } if !exist { // ripeMD is 'touched' at block 1714175, in tx 0x1237f737031e40bcde4a8b7e717b2d15e3ecadfe49bb1bbc71ee9deb09c6fcf2 // That tx goes out of gas, and although the notion of 'touched' does not exist there, the @@ -1510,7 +1385,7 @@ func (s *StateDB) Finalise(deleteEmptyObjects bool) { } if obj.suicided || (deleteEmptyObjects && obj.empty()) { if s.parallel.isSlotDB { - s.parallel.addrStateChangesInSlot[addr] = struct{}{} // empty an StateObject is a state change + s.parallel.addrStateChangesInSlot[addr] = false // false: deleted } obj.deleted = true @@ -1519,9 +1394,11 @@ func (s *StateDB) Finalise(deleteEmptyObjects bool) { // transactions within the same block might self destruct and then // ressurrect an account; but the snapshotter needs both events. if s.snap != nil { + s.snapParallelLock.Lock() s.snapDestructs[obj.address] = struct{}{} // We need to maintain account deletions explicitly (will remain set indefinitely) - delete(s.snapAccounts, obj.address) // Clear out any previously updated account data (may be recreated via a ressurrect) - delete(s.snapStorage, obj.address) // Clear out any previously updated storage data (may be recreated via a ressurrect) + s.snapParallelLock.Unlock() + delete(s.snapAccounts, obj.address) // Clear out any previously updated account data (may be recreated via a ressurrect) + delete(s.snapStorage, obj.address) // Clear out any previously updated storage data (may be recreated via a ressurrect) } } else { // 1.none parallel mode, we do obj.finalise(true) as normal @@ -1605,14 +1482,17 @@ func (s *StateDB) PopulateSnapAccountAndStorage() { //populateSnapStorage tries to populate required storages for pipecommit, and returns a flag to indicate whether the storage root changed or not func (s *StateDB) populateSnapStorage(obj *StateObject) bool { - for key, value := range obj.dirtyStorage { - obj.pendingStorage[key] = value - } - if len(obj.pendingStorage) == 0 { + obj.dirtyStorage.Range(func(key, value interface{}) bool { + obj.pendingStorage.StoreValue(key.(common.Hash), value.(common.Hash)) + return true + }) + if obj.pendingStorage.Length() == 0 { return false } var storage map[string][]byte - for key, value := range obj.pendingStorage { + obj.pendingStorage.Range(func(keyItf, valueItf interface{}) bool { + key := keyItf.(common.Hash) + value := valueItf.(common.Hash) var v []byte if (value != common.Hash{}) { // Encoding []byte cannot fail, ok to ignore the error. @@ -1629,16 +1509,17 @@ func (s *StateDB) populateSnapStorage(obj *StateObject) bool { } storage[string(key[:])] = v // v will be nil if value is 0x00 } - } + return true + }) return true } func (s *StateDB) AccountsIntermediateRoot() { - tasks := make(chan func()) + tasks := make(chan func()) // use buffer chan? finishCh := make(chan struct{}) defer close(finishCh) wg := sync.WaitGroup{} - for i := 0; i < runtime.NumCPU(); i++ { + for i := 0; i < runtime.NumCPU(); i++ { // more the cpu num since there are async IO operation go func() { for { select { @@ -2254,3 +2135,1684 @@ func (s *StateDB) GetDirtyAccounts() []common.Address { func (s *StateDB) GetStorage(address common.Address) *sync.Map { return s.storagePool.getStorage(address) } + +// PrepareForParallel prepares for state db to be used in parallel execution mode. +func (s *StateDB) PrepareForParallel() { + s.isParallel = true + s.parallel.stateObjects = &StateObjectSyncMap{} +} + +func (s *StateDB) AddrPrefetch(slotDb *ParallelStateDB) { + addressesToPrefetch := make([][]byte, 0, len(slotDb.parallel.dirtiedStateObjectsInSlot)) + for addr, obj := range slotDb.parallel.dirtiedStateObjectsInSlot { + addressesToPrefetch = append(addressesToPrefetch, common.CopyBytes(addr[:])) // Copy needed for closure + if obj.deleted { + continue + } + // copied from obj.finalise(true) + slotsToPrefetch := make([][]byte, 0, obj.dirtyStorage.Length()) + obj.dirtyStorage.Range(func(key, value interface{}) bool { + originalValue, _ := obj.originStorage.GetValue(key.(common.Hash)) + if value.(common.Hash) != originalValue { + originalKey := key.(common.Hash) + slotsToPrefetch = append(slotsToPrefetch, common.CopyBytes(originalKey[:])) // Copy needed for closure + } + return true + }) + if s.prefetcher != nil && len(slotsToPrefetch) > 0 && obj.data.Root != emptyRoot { + s.prefetcher.prefetch(obj.data.Root, slotsToPrefetch, obj.addrHash) + } + } + + if s.prefetcher != nil && len(addressesToPrefetch) > 0 { + // log.Info("AddrPrefetch", "slotDb.TxIndex", slotDb.TxIndex(), + // "len(addressesToPrefetch)", len(slotDb.parallel.addressesToPrefetch)) + s.prefetcher.prefetch(s.originalRoot, addressesToPrefetch, emptyAddr) + } +} + +// MergeSlotDB is for Parallel execution mode, when the transaction has been +// finalized(dirty -> pending) on execution slot, the execution results should be +// merged back to the main StateDB. +// And it will return and keep the slot's change list for later conflict detect. +func (s *StateDB) MergeSlotDB(slotDb *ParallelStateDB, slotReceipt *types.Receipt, txIndex int) { + // receipt.Logs use unified log index within a block + // align slotDB's log index to the block stateDB's logSize + for _, l := range slotReceipt.Logs { + l.Index += s.logSize + } + s.logSize += slotDb.logSize + + // before merge, pay the gas fee first: AddBalance to consensus.SystemAddress + systemAddress := slotDb.parallel.systemAddress + if slotDb.parallel.keepSystemAddressBalance { + s.SetBalance(systemAddress, slotDb.GetBalance(systemAddress)) + } else { + s.AddBalance(systemAddress, slotDb.GetBalance(systemAddress)) + } + // system address is EOA account, it should have no storage change + delete(slotDb.stateObjectsDirty, systemAddress) + // only merge dirty objects + addressesToPrefetch := make([][]byte, 0, len(slotDb.stateObjectsDirty)) + for addr := range slotDb.stateObjectsDirty { + if _, exist := s.stateObjectsDirty[addr]; !exist { + s.stateObjectsDirty[addr] = struct{}{} + } + + // stateObjects: KV, balance, nonce... + dirtyObj, ok := slotDb.parallel.dirtiedStateObjectsInSlot[addr] + if !ok { + log.Error("parallel merge, but dirty object not exist!", "SlotIndex", slotDb.parallel.SlotIndex, "txIndex:", slotDb.txIndex, "addr", addr) + continue + } + mainObj, exist := s.loadStateObj(addr) + if !exist { // fixme: it is also state change + // addr not exist on main DB, do ownership transfer + // dirtyObj.db = s + // dirtyObj.finalise(true) // true: prefetch on dispatcher + mainObj = dirtyObj.deepCopy(s) + if addr == WBNBAddress && slotDb.wbnbMakeUpBalance != nil { + mainObj.setBalance(slotDb.wbnbMakeUpBalance) + } + mainObj.finalise(true) + s.storeStateObj(addr, mainObj) + // fixme: should not delete, would cause unconfirmed DB incorrect? + // delete(slotDb.parallel.dirtiedStateObjectsInSlot, addr) // transfer ownership, fixme: shared read? + if dirtyObj.deleted { + // remove the addr from snapAccounts&snapStorage only when object is deleted. + // "deleted" is not equal to "snapDestructs", since createObject() will add an addr for + // snapDestructs to destroy previous object, while it will keep the addr in snapAccounts & snapAccounts + delete(s.snapAccounts, addr) + delete(s.snapStorage, addr) + } + } else { + // addr already in main DB, do merge: balance, KV, code, State(create, suicide) + // can not do copy or ownership transfer directly, since dirtyObj could have outdated + // data(may be updated within the conflict window) + + var newMainObj = mainObj // we don't need to copy the object since the storages are thread safe + if _, ok := slotDb.parallel.addrStateChangesInSlot[addr]; ok { + // there are 3 kinds of state change: + // 1.Suicide + // 2.Empty Delete + // 3.createObject + // a.AddBalance,SetState to an unexist or deleted(suicide, empty delete) address. + // b.CreateAccount: like DAO the fork, regenerate a account carry its balance without KV + // For these state change, do ownership transafer for efficiency: + // dirtyObj.db = s + // newMainObj = dirtyObj + newMainObj = dirtyObj.deepCopy(s) + // should not delete, would cause unconfirmed DB incorrect. + // delete(slotDb.parallel.dirtiedStateObjectsInSlot, addr) // transfer ownership, fixme: shared read? + if dirtyObj.deleted { + // remove the addr from snapAccounts&snapStorage only when object is deleted. + // "deleted" is not equal to "snapDestructs", since createObject() will add an addr for + // snapDestructs to destroy previous object, while it will keep the addr in snapAccounts & snapAccounts + delete(s.snapAccounts, addr) + delete(s.snapStorage, addr) + } + } else { + // deepCopy a temporary *StateObject for safety, since slot could read the address, + // dispatch should avoid overwrite the StateObject directly otherwise, it could + // crash for: concurrent map iteration and map write + + if _, balanced := slotDb.parallel.balanceChangesInSlot[addr]; balanced { + newMainObj.setBalance(dirtyObj.Balance()) + } + if _, coded := slotDb.parallel.codeChangesInSlot[addr]; coded { + newMainObj.code = dirtyObj.code + newMainObj.data.CodeHash = dirtyObj.data.CodeHash + newMainObj.dirtyCode = true + } + if keys, stated := slotDb.parallel.kvChangesInSlot[addr]; stated { + newMainObj.MergeSlotObject(s.db, dirtyObj, keys) + } + if _, nonced := slotDb.parallel.nonceChangesInSlot[addr]; nonced { + // dirtyObj.Nonce() should not be less than newMainObj + newMainObj.setNonce(dirtyObj.Nonce()) + } + } + if addr == WBNBAddress && slotDb.wbnbMakeUpBalance != nil { + newMainObj.setBalance(slotDb.wbnbMakeUpBalance) + } + newMainObj.finalise(true) // true: prefetch on dispatcher + // update the object + s.storeStateObj(addr, newMainObj) + } + addressesToPrefetch = append(addressesToPrefetch, common.CopyBytes(addr[:])) // Copy needed for closure + } + + if s.prefetcher != nil && len(addressesToPrefetch) > 0 { + // log.Info("MergeSlotDB", "len(addressesToPrefetch)", len(addressesToPrefetch)) + s.prefetcher.prefetch(s.originalRoot, addressesToPrefetch, emptyAddr) // prefetch for trie node of account + } + + for addr := range slotDb.stateObjectsPending { + if _, exist := s.stateObjectsPending[addr]; !exist { + s.stateObjectsPending[addr] = struct{}{} + } + } + + // slotDb.logs: logs will be kept in receipts, no need to do merge + + for hash, preimage := range slotDb.preimages { + s.preimages[hash] = preimage + } + if s.accessList != nil { + // fixme: accessList is not enabled yet, but it should use merge rather than overwrite Copy + s.accessList = slotDb.accessList.Copy() + } + + if slotDb.snaps != nil { + for k := range slotDb.snapDestructs { + // There could be a race condition for parallel transaction execution + // One transaction add balance 0 to an empty address, will delete it(delete empty is enabled). + // While another concurrent transaction could add a none-zero balance to it, make it not empty + // We fixed it by add a addr state read record for add balance 0 + s.snapParallelLock.Lock() + s.snapDestructs[k] = struct{}{} + s.snapParallelLock.Unlock() + } + + // slotDb.snapAccounts should be empty, comment out and to be deleted later + // for k, v := range slotDb.snapAccounts { + // s.snapAccounts[k] = v + // } + // slotDb.snapStorage should be empty, comment out and to be deleted later + // for k, v := range slotDb.snapStorage { + // temp := make(map[string][]byte) + // for kk, vv := range v { + // temp[kk] = vv + // } + // s.snapStorage[k] = temp + // } + } + s.txIndex = txIndex +} + +func (s *StateDB) ParallelMakeUp(addr common.Address, input []byte) { + // do nothing, this API is for parallel mode +} + +type ParallelStateDB struct { + StateDB + wbnbMakeUp bool // default true, we can not do WBNB make up only when supported API call is received. + // wbnbBalanceAccessed int // how many times the WBNB's balance is acccessed, i.e. `GetBalance`, `AddBalance`, `SubBalance`, `SetBalance` + // wbnbBalanceAccessedExpected int // how many times the WBNB contract is called. + // wbnbMakeUpLock sync.RWMutex // we may make up WBNB's balanace of the unconfirmed DB, while other slot read it. + // wbnbContractCalled int // how many times the WBNB contract is called. + balanceUpdateDepth int + wbnbMakeUpBalance *big.Int +} + +// NewSlotDB creates a new State DB based on the provided StateDB. +// With parallel, each execution slot would have its own StateDB. +func NewSlotDB(db *StateDB, systemAddr common.Address, txIndex int, baseTxIndex int, keepSystem bool, + unconfirmedDBs *sync.Map /*map[int]*ParallelStateDB*/) *ParallelStateDB { + slotDB := db.CopyForSlot() + slotDB.txIndex = txIndex + slotDB.originalRoot = db.originalRoot + slotDB.parallel.baseStateDB = db + slotDB.parallel.baseTxIndex = baseTxIndex + slotDB.parallel.systemAddress = systemAddr + slotDB.parallel.systemAddressOpsCount = 0 + slotDB.parallel.keepSystemAddressBalance = keepSystem + slotDB.storagePool = NewStoragePool() + slotDB.EnableWriteOnSharedStorage() + slotDB.parallel.unconfirmedDBs = unconfirmedDBs + + // All transactions will pay gas fee to the systemAddr at the end, this address is + // deemed to conflict, we handle it specially, clear it now and set it back to the main + // StateDB later; + // But there are transactions that will try to read systemAddr's balance, such as: + // https://bscscan.com/tx/0xcd69755be1d2f55af259441ff5ee2f312830b8539899e82488a21e85bc121a2a. + // It will trigger transaction redo and keepSystem will be marked as true. + if !keepSystem { + slotDB.SetBalance(systemAddr, big.NewInt(0)) + } + + return slotDB +} + +// RevertSlotDB keep the Read list for conflict detect, +// discard all state changes except: +// - nonce and balance of from address +// - balance of system address: will be used on merge to update SystemAddress's balance +func (s *ParallelStateDB) RevertSlotDB(from common.Address) { + s.parallel.kvChangesInSlot = make(map[common.Address]StateKeys) + + // balance := s.parallel.balanceChangesInSlot[from] + s.parallel.nonceChangesInSlot = make(map[common.Address]struct{}) + s.parallel.balanceChangesInSlot = make(map[common.Address]struct{}, 1) + s.parallel.addrStateChangesInSlot = make(map[common.Address]bool) // 0: created, 1: deleted + + selfStateObject := s.parallel.dirtiedStateObjectsInSlot[from] + systemAddress := s.parallel.systemAddress + systemStateObject := s.parallel.dirtiedStateObjectsInSlot[systemAddress] + s.parallel.dirtiedStateObjectsInSlot = make(map[common.Address]*StateObject, 2) + // keep these elements + s.parallel.dirtiedStateObjectsInSlot[from] = selfStateObject + s.parallel.dirtiedStateObjectsInSlot[systemAddress] = systemStateObject + s.parallel.balanceChangesInSlot[from] = struct{}{} + s.parallel.balanceChangesInSlot[systemAddress] = struct{}{} + s.parallel.nonceChangesInSlot[from] = struct{}{} +} + +func (s *ParallelStateDB) getBaseStateDB() *StateDB { + return &s.StateDB +} + +func (s *ParallelStateDB) SetSlotIndex(index int) { + s.parallel.SlotIndex = index +} + +// for parallel execution mode, try to get dirty StateObject in slot first. +// it is mainly used by journal revert right now. +func (s *ParallelStateDB) getStateObject(addr common.Address) *StateObject { + if obj, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; ok { + return obj + } + // can not call s.StateDB.getStateObject(), since `newObject` need ParallelStateDB as the interface + return s.getStateObjectNoSlot(addr) +} + +func (s *ParallelStateDB) storeStateObj(addr common.Address, stateObject *StateObject) { + // When a state object is stored into s.parallel.stateObjects, + // it belongs to base StateDB, it is confirmed and valid. + stateObject.db = s.parallel.baseStateDB + stateObject.dbItf = s.parallel.baseStateDB + // the object could be create in SlotDB, if it got the object from DB and + // update it to the shared `s.parallel.stateObjects`` + stateObject.db.storeParallelLock.Lock() + if _, ok := s.parallel.stateObjects.Load(addr); !ok { + s.parallel.stateObjects.Store(addr, stateObject) + } + stateObject.db.storeParallelLock.Unlock() +} + +func (s *ParallelStateDB) getStateObjectNoSlot(addr common.Address) *StateObject { + if obj := s.getDeletedStateObject(addr); obj != nil && !obj.deleted { + return obj + } + return nil +} + +// createObject creates a new state object. If there is an existing account with +// the given address, it is overwritten and returned as the second return value. + +// prev is used for CreateAccount to get its balance +// Parallel mode: +// if prev in dirty: revert is ok +// if prev in unconfirmed DB: addr state read record, revert should not put it back +// if prev in main DB: addr state read record, revert should not put it back +// if pre no exist: addr state read record, + +// `prev` is used to handle revert, to recover with the `prev` object +// In Parallel mode, we only need to recover to `prev` in SlotDB, +// a.if it is not in SlotDB, `revert` will remove it from the SlotDB +// b.if it is exist in SlotDB, `revert` will recover to the `prev` in SlotDB +// c.as `snapDestructs` it is the same +func (s *ParallelStateDB) createObject(addr common.Address) (newobj *StateObject) { + // do not get from unconfirmed DB, since it will has problem on revert + prev := s.parallel.dirtiedStateObjectsInSlot[addr] + + var prevdestruct bool + + if s.snap != nil && prev != nil { + s.snapParallelLock.Lock() + _, prevdestruct = s.snapDestructs[prev.address] // fixme, record the snapshot read for create Account + s.parallel.addrSnapDestructsReadsInSlot[addr] = prevdestruct + if !prevdestruct { + // To destroy the previous trie node first and update the trie tree + // with the new object on block commit. + s.snapDestructs[prev.address] = struct{}{} + } + s.snapParallelLock.Lock() + + } + newobj = newObject(s, s.isParallel, addr, Account{}) + newobj.setNonce(0) // sets the object to dirty + if prev == nil { + s.journal.append(createObjectChange{account: &addr}) + } else { + s.journal.append(resetObjectChange{prev: prev, prevdestruct: prevdestruct}) + } + + // s.parallel.dirtiedStateObjectsInSlot[addr] = newobj // would change the bahavior of AddBalance... + s.parallel.addrStateChangesInSlot[addr] = true // the object sis created + s.parallel.nonceChangesInSlot[addr] = struct{}{} + s.parallel.balanceChangesInSlot[addr] = struct{}{} + s.parallel.codeChangesInSlot[addr] = struct{}{} + // notice: all the KVs are cleared if any + s.parallel.kvChangesInSlot[addr] = make(StateKeys) + return newobj +} + +// getDeletedStateObject is similar to getStateObject, but instead of returning +// nil for a deleted state object, it returns the actual object with the deleted +// flag set. This is needed by the state journal to revert to the correct s- +// destructed object instead of wiping all knowledge about the state object. +func (s *ParallelStateDB) getDeletedStateObject(addr common.Address) *StateObject { + // Prefer live objects if any is available + if obj, _ := s.getStateObjectFromStateObjects(addr); obj != nil { + return obj + } + data, ok := s.getStateObjectFromSnapshotOrTrie(addr) + if !ok { + return nil + } + // Insert into the live set + // if obj, ok := s.loadStateObj(addr); ok { + // fixme: concurrent not safe, merge could update it... + // return obj + // } + // this is why we have to use a seperate getDeletedStateObject for ParallelStateDB + // `s` has to be the ParallelStateDB + obj := newObject(s, s.isParallel, addr, *data) + s.storeStateObj(addr, obj) + // s.SetStateObject(obj) + return obj +} + +// GetOrNewStateObject retrieves a state object or create a new state object if nil. +// dirtyInSlot -> Unconfirmed DB -> main DB -> snapshot, no? create one +func (s *ParallelStateDB) GetOrNewStateObject(addr common.Address) *StateObject { + var stateObject *StateObject = nil + exist := true + if stateObject, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; ok { + return stateObject + } + stateObject, _ = s.getStateObjectFromUnconfirmedDB(addr) + + if stateObject == nil { + stateObject = s.getStateObjectNoSlot(addr) // try to get from base db + } + if stateObject == nil || stateObject.deleted || stateObject.suicided { + stateObject = s.createObject(addr) + exist = false + } + + s.parallel.addrStateReadsInSlot[addr] = exist // true: exist, false: not exist + return stateObject +} + +// Exist reports whether the given account address exists in the state. +// Notably this also returns true for suicided accounts. +func (s *ParallelStateDB) Exist(addr common.Address) bool { + // 1.Try to get from dirty + if obj, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; ok { + // dirty object should not be deleted, since deleted is only flagged on finalise + // and if it is suicided in contract call, suicide is taken as exist until it is finalised + // todo: add a check here, to be removed later + if obj.deleted || obj.suicided { + log.Error("Exist in dirty, but marked as deleted or suicided", + "txIndex", s.txIndex, "baseTxIndex:", s.parallel.baseTxIndex) + } + return true + } + // 2.Try to get from uncomfirmed & main DB + // 2.1 Already read before + if exist, ok := s.parallel.addrStateReadsInSlot[addr]; ok { + return exist + } + // 2.2 Try to get from unconfirmed DB if exist + if exist, ok := s.getAddrStateFromUnconfirmedDB(addr); ok { + s.parallel.addrStateReadsInSlot[addr] = exist // update and cache + return exist + } + + // 3.Try to get from main StateDB + exist := s.getStateObjectNoSlot(addr) != nil + s.parallel.addrStateReadsInSlot[addr] = exist // update and cache + return exist +} + +// Empty returns whether the state object is either non-existent +// or empty according to the EIP161 specification (balance = nonce = code = 0) +func (s *ParallelStateDB) Empty(addr common.Address) bool { + // 1.Try to get from dirty + if obj, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; ok { + // dirty object is light copied and fixup on need, + // empty could be wrong, except it is created with this TX + if _, ok := s.parallel.addrStateChangesInSlot[addr]; ok { + return obj.empty() + } + // so we have to check it manually + // empty means: Nonce == 0 && Balance == 0 && CodeHash == emptyCodeHash + if s.GetBalance(addr).Sign() != 0 { // check balance first, since it is most likely not zero + return false + } + if s.GetNonce(addr) != 0 { + return false + } + codeHash := s.GetCodeHash(addr) + return bytes.Equal(codeHash.Bytes(), emptyCodeHash) // code is empty, the object is empty + } + // 2.Try to get from uncomfirmed & main DB + // 2.1 Already read before + if exist, ok := s.parallel.addrStateReadsInSlot[addr]; ok { + // exist means not empty + return !exist + } + // 2.2 Try to get from unconfirmed DB if exist + if exist, ok := s.getAddrStateFromUnconfirmedDB(addr); ok { + s.parallel.addrStateReadsInSlot[addr] = exist // update and cache + return !exist + } + + so := s.getStateObjectNoSlot(addr) + empty := (so == nil || so.empty()) + s.parallel.addrStateReadsInSlot[addr] = !empty // update and cache + return empty +} + +// GetBalance retrieves the balance from the given address or 0 if object not found +// GetFrom the dirty list => from unconfirmed DB => get from main stateDB +func (s *ParallelStateDB) GetBalance(addr common.Address) *big.Int { + if addr == s.parallel.systemAddress { + s.parallel.systemAddressOpsCount++ + } + // 1.Try to get from dirty + if _, ok := s.parallel.balanceChangesInSlot[addr]; ok { + if obj, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; ok { + // on balance fixup, addr may not exist in dirtiedStateObjectsInSlot + // we intend to fixup balance based on unconfirmed DB or main DB + return obj.Balance() + } + } + // 2.Try to get from uncomfirmed DB or main DB + // 2.1 Already read before + if balance, ok := s.parallel.balanceReadsInSlot[addr]; ok { + return balance + } + // 2.2 Try to get from unconfirmed DB if exist + if balance := s.getBalanceFromUnconfirmedDB(addr); balance != nil { + s.parallel.balanceReadsInSlot[addr] = balance + return balance + } + + // 3. Try to get from main StateObejct + balance := common.Big0 + stateObject := s.getStateObjectNoSlot(addr) + if stateObject != nil { + balance = stateObject.Balance() + } + s.parallel.balanceReadsInSlot[addr] = balance + return balance +} + +func (s *ParallelStateDB) GetBalanceOpCode(addr common.Address) *big.Int { + if addr == WBNBAddress { + // s.wbnbBalanceAccessed++ + s.wbnbMakeUp = false + // log.Debug("GetBalanceOpCode for WBNB", "txIndex", s.TxIndex()) + } + return s.GetBalance(addr) +} + +func (s *ParallelStateDB) GetNonce(addr common.Address) uint64 { + // 1.Try to get from dirty + if _, ok := s.parallel.nonceChangesInSlot[addr]; ok { + if obj, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; ok { + // on nonce fixup, addr may not exist in dirtiedStateObjectsInSlot + // we intend to fixup nonce based on unconfirmed DB or main DB + return obj.Nonce() + } + } + // 2.Try to get from uncomfirmed DB or main DB + // 2.1 Already read before + if nonce, ok := s.parallel.nonceReadsInSlot[addr]; ok { + return nonce + } + // 2.2 Try to get from unconfirmed DB if exist + if nonce, ok := s.getNonceFromUnconfirmedDB(addr); ok { + s.parallel.nonceReadsInSlot[addr] = nonce + return nonce + } + + // 3.Try to get from main StateDB + var nonce uint64 = 0 + stateObject := s.getStateObjectNoSlot(addr) + if stateObject != nil { + nonce = stateObject.Nonce() + } + s.parallel.nonceReadsInSlot[addr] = nonce + return nonce +} + +func (s *ParallelStateDB) GetCode(addr common.Address) []byte { + // 1.Try to get from dirty + if _, ok := s.parallel.codeChangesInSlot[addr]; ok { + if obj, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; ok { + // on code fixup, addr may not exist in dirtiedStateObjectsInSlot + // we intend to fixup code based on unconfirmed DB or main DB + code := obj.Code(s.db) + return code + } + } + // 2.Try to get from uncomfirmed DB or main DB + // 2.1 Already read before + if code, ok := s.parallel.codeReadsInSlot[addr]; ok { + return code + } + // 2.2 Try to get from unconfirmed DB if exist + if code, ok := s.getCodeFromUnconfirmedDB(addr); ok { + s.parallel.codeReadsInSlot[addr] = code + return code + } + + // 3. Try to get from main StateObejct + stateObject := s.getStateObjectNoSlot(addr) + var code []byte + if stateObject != nil { + code = stateObject.Code(s.db) + } + s.parallel.codeReadsInSlot[addr] = code + return code +} + +func (s *ParallelStateDB) GetCodeSize(addr common.Address) int { + // 1.Try to get from dirty + if _, ok := s.parallel.codeChangesInSlot[addr]; ok { + if obj, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; ok { + // on code fixup, addr may not exist in dirtiedStateObjectsInSlot + // we intend to fixup code based on unconfirmed DB or main DB + return obj.CodeSize(s.db) + } + } + // 2.Try to get from uncomfirmed DB or main DB + // 2.1 Already read before + if code, ok := s.parallel.codeReadsInSlot[addr]; ok { + return len(code) // len(nil) is 0 too + } + // 2.2 Try to get from unconfirmed DB if exist + if code, ok := s.getCodeFromUnconfirmedDB(addr); ok { + s.parallel.codeReadsInSlot[addr] = code + return len(code) // len(nil) is 0 too + } + + // 3. Try to get from main StateObejct + var codeSize int = 0 + var code []byte + stateObject := s.getStateObjectNoSlot(addr) + + if stateObject != nil { + code = stateObject.Code(s.db) + codeSize = stateObject.CodeSize(s.db) + } + s.parallel.codeReadsInSlot[addr] = code + return codeSize +} + +// return value of GetCodeHash: +// - common.Hash{}: the address does not exist +// - emptyCodeHash: the address exist, but code is empty +// - others: the address exist, and code is not empty +func (s *ParallelStateDB) GetCodeHash(addr common.Address) common.Hash { + // 1.Try to get from dirty + if _, ok := s.parallel.codeChangesInSlot[addr]; ok { + if obj, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; ok { + // on code fixup, addr may not exist in dirtiedStateObjectsInSlot + // we intend to fixup balance based on unconfirmed DB or main DB + return common.BytesToHash(obj.CodeHash()) + } + } + // 2.Try to get from uncomfirmed DB or main DB + // 2.1 Already read before + if codeHash, ok := s.parallel.codeHashReadsInSlot[addr]; ok { + return codeHash + } + // 2.2 Try to get from unconfirmed DB if exist + if codeHash, ok := s.getCodeHashFromUnconfirmedDB(addr); ok { + s.parallel.codeHashReadsInSlot[addr] = codeHash + return codeHash + } + // 3. Try to get from main StateObejct + stateObject := s.getStateObjectNoSlot(addr) + codeHash := common.Hash{} + if stateObject != nil { + codeHash = common.BytesToHash(stateObject.CodeHash()) + } + s.parallel.codeHashReadsInSlot[addr] = codeHash + return codeHash +} + +// GetState retrieves a value from the given account's storage trie. +// For parallel mode wih, get from the state in order: +// -> self dirty, both Slot & MainProcessor +// -> pending of self: Slot on merge +// -> pending of unconfirmed DB +// -> pending of main StateDB +// -> origin +func (s *ParallelStateDB) GetState(addr common.Address, hash common.Hash) common.Hash { + // 1.Try to get from dirty + if exist, ok := s.parallel.addrStateChangesInSlot[addr]; ok { + if !exist { + return common.Hash{} + } + obj := s.parallel.dirtiedStateObjectsInSlot[addr] // addr must exist in dirtiedStateObjectsInSlot + return obj.GetState(s.db, hash) + } + if keys, ok := s.parallel.kvChangesInSlot[addr]; ok { + if _, ok := keys[hash]; ok { + obj := s.parallel.dirtiedStateObjectsInSlot[addr] // addr must exist in dirtiedStateObjectsInSlot + return obj.GetState(s.db, hash) + } + } + // 2.Try to get from uncomfirmed DB or main DB + // 2.1 Already read before + if storage, ok := s.parallel.kvReadsInSlot[addr]; ok { + if val, ok := storage.GetValue(hash); ok { + return val + } + } + // 2.2 Try to get from unconfirmed DB if exist + if val, ok := s.getKVFromUnconfirmedDB(addr, hash); ok { + if s.parallel.kvReadsInSlot[addr] == nil { + s.parallel.kvReadsInSlot[addr] = newStorage(false) + } + s.parallel.kvReadsInSlot[addr].StoreValue(hash, val) // update cache + return val + } + + // 3.Get from main StateDB + stateObject := s.getStateObjectNoSlot(addr) + val := common.Hash{} + if stateObject != nil { + val = stateObject.GetState(s.db, hash) + } + if s.parallel.kvReadsInSlot[addr] == nil { + s.parallel.kvReadsInSlot[addr] = newStorage(false) + } + s.parallel.kvReadsInSlot[addr].StoreValue(hash, val) // update cache + return val +} + +// GetCommittedState retrieves a value from the given account's committed storage trie. +func (s *ParallelStateDB) GetCommittedState(addr common.Address, hash common.Hash) common.Hash { + // 1.No need to get from pending of itself even on merge, since stateobject in SlotDB won't do finalise + // 2.Try to get from uncomfirmed DB or main DB + // KVs in unconfirmed DB can be seen as pending storage + // KVs in main DB are merged from SlotDB and has done finalise() on merge, can be seen as pending storage too. + // 2.1 Already read before + if storage, ok := s.parallel.kvReadsInSlot[addr]; ok { + if val, ok := storage.GetValue(hash); ok { + return val + } + } + // 2.2 Try to get from unconfirmed DB if exist + if val, ok := s.getKVFromUnconfirmedDB(addr, hash); ok { + if s.parallel.kvReadsInSlot[addr] == nil { + s.parallel.kvReadsInSlot[addr] = newStorage(false) + } + s.parallel.kvReadsInSlot[addr].StoreValue(hash, val) // update cache + return val + } + + // 3. Try to get from main DB + stateObject := s.getStateObjectNoSlot(addr) + val := common.Hash{} + if stateObject != nil { + val = stateObject.GetCommittedState(s.db, hash) + } + if s.parallel.kvReadsInSlot[addr] == nil { + s.parallel.kvReadsInSlot[addr] = newStorage(false) + } + s.parallel.kvReadsInSlot[addr].StoreValue(hash, val) // update cache + return val +} + +func (s *ParallelStateDB) HasSuicided(addr common.Address) bool { + // 1.Try to get from dirty + if obj, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; ok { + return obj.suicided + } + // 2.Try to get from uncomfirmed + if exist, ok := s.getAddrStateFromUnconfirmedDB(addr); ok { + return !exist + } + + stateObject := s.getStateObjectNoSlot(addr) + if stateObject != nil { + return stateObject.suicided + } + return false +} + +// AddBalance adds amount to the account associated with addr. +func (s *ParallelStateDB) AddBalance(addr common.Address, amount *big.Int) { + // add balance will perform a read operation first + // s.parallel.balanceReadsInSlot[addr] = struct{}{} // fixme: to make the the balance valid, since unconfirmed would refer it. + // if amount.Sign() == 0 { + // if amount == 0, no balance change, but there is still an empty check. + // take this empty check as addr state read(create, suicide, empty delete) + // s.parallel.addrStateReadsInSlot[addr] = struct{}{} + // } + s.balanceUpdateDepth++ + defer func() { + s.balanceUpdateDepth-- + }() + stateObject := s.GetOrNewStateObject(addr) + if stateObject != nil { + if addr == s.parallel.systemAddress { + s.parallel.systemAddressOpsCount++ + } + //else if addr == WBNBAddress { + // s.wbnbBalanceAccessed++ + //} + // if amount.Sign() != 0 { // todo: to reenable it + if _, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; !ok { + newStateObject := stateObject.lightCopy(s) // light copy from main DB + // do balance fixup from the confirmed DB, it could be more reliable than main DB + balance := s.GetBalance(addr) + newStateObject.setBalance(balance) + // s.parallel.balanceReadsInSlot[addr] = newStateObject.Balance() // could read from main DB or unconfirmed DB + newStateObject.AddBalance(amount) + s.parallel.dirtiedStateObjectsInSlot[addr] = newStateObject + s.parallel.balanceChangesInSlot[addr] = struct{}{} + return + } + // already dirty, make sure the balance if fixed up + // if stateObject.Balance() + if addr != s.parallel.systemAddress { + balance := s.GetBalance(addr) + if stateObject.Balance().Cmp(balance) != 0 { + log.Warn("AddBalance in dirty, but balance has not do fixup", "txIndex", s.txIndex, "addr", addr, + "stateObject.Balance()", stateObject.Balance(), "s.GetBalance(addr)", balance) + stateObject.setBalance(balance) + } + } + + stateObject.AddBalance(amount) + s.parallel.balanceChangesInSlot[addr] = struct{}{} + } +} + +// SubBalance subtracts amount from the account associated with addr. +func (s *ParallelStateDB) SubBalance(addr common.Address, amount *big.Int) { + // if amount.Sign() != 0 { + // unlike add, sub 0 balance will not touch empty object + // s.parallel.balanceReadsInSlot[addr] = struct{}{} + // } + s.balanceUpdateDepth++ + defer func() { + s.balanceUpdateDepth-- + }() + + stateObject := s.GetOrNewStateObject(addr) + if stateObject != nil { + if addr == s.parallel.systemAddress { + s.parallel.systemAddressOpsCount++ + } + // else if addr == WBNBAddress { + // s.wbnbBalanceAccessed++ + // } + + // if amount.Sign() != 0 { // todo: to reenable it + if _, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; !ok { + newStateObject := stateObject.lightCopy(s) // light copy from main DB + // do balance fixup from the confirmed DB, it could be more reliable than main DB + balance := s.GetBalance(addr) + newStateObject.setBalance(balance) + // s.parallel.balanceReadsInSlot[addr] = newStateObject.Balance() + newStateObject.SubBalance(amount) + s.parallel.balanceChangesInSlot[addr] = struct{}{} + s.parallel.dirtiedStateObjectsInSlot[addr] = newStateObject + return + } + // already dirty, make sure the balance if fixed + // if stateObject.Balance() + if addr != s.parallel.systemAddress { + balance := s.GetBalance(addr) + if stateObject.Balance().Cmp(balance) != 0 { + log.Warn("SubBalance in dirty, but balance is incorrect", "txIndex", s.txIndex, "addr", addr, + "stateObject.Balance()", stateObject.Balance(), "s.GetBalance(addr)", balance) + stateObject.setBalance(balance) + } + } + + stateObject.SubBalance(amount) + s.parallel.balanceChangesInSlot[addr] = struct{}{} + } +} + +func (s *ParallelStateDB) SetBalance(addr common.Address, amount *big.Int) { + s.balanceUpdateDepth++ + defer func() { + s.balanceUpdateDepth-- + }() + + stateObject := s.GetOrNewStateObject(addr) + if stateObject != nil { + if addr == s.parallel.systemAddress { + s.parallel.systemAddressOpsCount++ + } + // else if addr == WBNBAddress { + // s.wbnbBalanceAccessed++ + // } + if _, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; !ok { + newStateObject := stateObject.lightCopy(s) + // update balance for revert, in case child contract is revertted, + // it should revert to the previous balance + balance := s.GetBalance(addr) + newStateObject.setBalance(balance) + newStateObject.SetBalance(amount) + s.parallel.balanceChangesInSlot[addr] = struct{}{} + s.parallel.dirtiedStateObjectsInSlot[addr] = newStateObject + return + } + // do balance fixup + if addr != s.parallel.systemAddress { + balance := s.GetBalance(addr) + stateObject.setBalance(balance) + } + stateObject.SetBalance(amount) + s.parallel.balanceChangesInSlot[addr] = struct{}{} + } +} + +func (s *ParallelStateDB) SetNonce(addr common.Address, nonce uint64) { + stateObject := s.GetOrNewStateObject(addr) + if stateObject != nil { + if _, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; !ok { + newStateObject := stateObject.lightCopy(s) + noncePre := s.GetNonce(addr) + newStateObject.setNonce(noncePre) // nonce fixup + newStateObject.SetNonce(nonce) + s.parallel.nonceChangesInSlot[addr] = struct{}{} + s.parallel.dirtiedStateObjectsInSlot[addr] = newStateObject + return + } + noncePre := s.GetNonce(addr) + stateObject.setNonce(noncePre) // nonce fixup + + stateObject.SetNonce(nonce) + s.parallel.nonceChangesInSlot[addr] = struct{}{} + } +} + +func (s *ParallelStateDB) SetCode(addr common.Address, code []byte) { + stateObject := s.GetOrNewStateObject(addr) + if stateObject != nil { + codeHash := crypto.Keccak256Hash(code) + if _, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; !ok { + newStateObject := stateObject.lightCopy(s) + codePre := s.GetCode(addr) // code fixup + codeHashPre := crypto.Keccak256Hash(codePre) + newStateObject.setCode(codeHashPre, codePre) + + newStateObject.SetCode(codeHash, code) + s.parallel.dirtiedStateObjectsInSlot[addr] = newStateObject + s.parallel.codeChangesInSlot[addr] = struct{}{} + return + } + codePre := s.GetCode(addr) // code fixup + codeHashPre := crypto.Keccak256Hash(codePre) + stateObject.setCode(codeHashPre, codePre) + + stateObject.SetCode(codeHash, code) + s.parallel.codeChangesInSlot[addr] = struct{}{} + } +} + +func (s *ParallelStateDB) SetState(addr common.Address, key, value common.Hash) { + stateObject := s.GetOrNewStateObject(addr) // attention: if StateObject's lightCopy, its storage is only a part of the full storage, + if stateObject != nil { + if s.parallel.baseTxIndex+1 == s.txIndex { + // we check if state is unchanged + // only when current transaction is the next transaction to be committed + // fixme: there is a bug, block: 14,962,284, + // stateObject is in dirty (light copy), but the key is in mainStateDB + // stateObject dirty -> committed, will skip mainStateDB dirty + if s.GetState(addr, key) == value { + log.Debug("Skip set same state", "baseTxIndex", s.parallel.baseTxIndex, + "txIndex", s.txIndex, "addr", addr, + "key", key, "value", value) + return + } + } + + if s.parallel.kvChangesInSlot[addr] == nil { + s.parallel.kvChangesInSlot[addr] = make(StateKeys) // make(Storage, defaultNumOfSlots) + } + + if _, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; !ok { + newStateObject := stateObject.lightCopy(s) + newStateObject.SetState(s.db, key, value) + s.parallel.dirtiedStateObjectsInSlot[addr] = newStateObject + return + } + // do State Update + stateObject.SetState(s.db, key, value) + } +} + +// Suicide marks the given account as suicided. +// This clears the account balance. +// +// The account's state object is still available until the state is committed, +// getStateObject will return a non-nil account after Suicide. +func (s *ParallelStateDB) Suicide(addr common.Address) bool { + var stateObject *StateObject + // 1.Try to get from dirty, it could be suicided inside of contract call + stateObject = s.parallel.dirtiedStateObjectsInSlot[addr] + if stateObject == nil { + // 2.Try to get from uncomfirmed, if deleted return false, since the address does not exist + if obj, ok := s.getStateObjectFromUnconfirmedDB(addr); ok { + stateObject = obj + s.parallel.addrStateReadsInSlot[addr] = !stateObject.deleted // true: exist, false: deleted + if stateObject.deleted { + log.Error("Suicide addr alreay deleted in confirmed DB", "txIndex", s.txIndex, "addr", addr) + return false + } + } + } + + if stateObject == nil { + // 3.Try to get from main StateDB + stateObject = s.getStateObjectNoSlot(addr) + if stateObject == nil { + s.parallel.addrStateReadsInSlot[addr] = false // true: exist, false: deleted + log.Error("Suicide addr not exist", "txIndex", s.txIndex, "addr", addr) + return false + } + s.parallel.addrStateReadsInSlot[addr] = true // true: exist, false: deleted + } + + s.journal.append(suicideChange{ + account: &addr, + prev: stateObject.suicided, // todo: must be false? + prevbalance: new(big.Int).Set(s.GetBalance(addr)), + }) + + if _, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; !ok { + // do copy-on-write for suicide "write" + newStateObject := stateObject.lightCopy(s) + newStateObject.markSuicided() + newStateObject.data.Balance = new(big.Int) + s.parallel.dirtiedStateObjectsInSlot[addr] = newStateObject + s.parallel.addrStateChangesInSlot[addr] = false // false: the address does not exist any more, + // s.parallel.nonceChangesInSlot[addr] = struct{}{} + s.parallel.balanceChangesInSlot[addr] = struct{}{} + s.parallel.codeChangesInSlot[addr] = struct{}{} + // s.parallel.kvChangesInSlot[addr] = make(StateKeys) // all key changes are discarded + return true + } + s.parallel.addrStateChangesInSlot[addr] = false // false: the address does not exist any more, + s.parallel.balanceChangesInSlot[addr] = struct{}{} + s.parallel.codeChangesInSlot[addr] = struct{}{} + + stateObject.markSuicided() + stateObject.data.Balance = new(big.Int) + return true +} + +// CreateAccount explicitly creates a state object. If a state object with the address +// already exists the balance is carried over to the new account. +// +// CreateAccount is called during the EVM CREATE operation. The situation might arise that +// a contract does the following: +// +// 1. sends funds to sha(account ++ (nonce + 1)) +// 2. tx_create(sha(account ++ nonce)) (note that this gets the address of 1) +// +// Carrying over the balance ensures that Ether doesn't disappear. +func (s *ParallelStateDB) CreateAccount(addr common.Address) { + // no matter it is got from dirty, unconfirmed or main DB + // if addr not exist, preBalance will be common.Big0, it is same as new(big.Int) which + // is the value newObject(), + preBalance := s.GetBalance(addr) // parallel balance read will be recorded inside of GetBalance + newObj := s.createObject(addr) + newObj.setBalance(new(big.Int).Set(preBalance)) // new big.Int for newObj +} + +// RevertToSnapshot reverts all state changes made since the given revision. +func (s *ParallelStateDB) 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] +} + +// AddRefund adds gas to the refund counter +// journal.append will use ParallelState for revert +func (s *ParallelStateDB) AddRefund(gas uint64) { // fixme: not needed + s.journal.append(refundChange{prev: 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 *ParallelStateDB) SubRefund(gas uint64) { // fixme: not needed + s.journal.append(refundChange{prev: s.refund}) + if gas > s.refund { + // we don't need to panic here if we read the wrong state in parallelm mode + // we just need to redo this transaction + log.Info(fmt.Sprintf("Refund counter below zero (gas: %d > refund: %d)", gas, s.refund), "tx", s.thash.String()) + s.parallel.needsRedo = true + return + } + s.refund -= gas +} + +// For Parallel Execution Mode, it can be seen as Penetrated Access: +// ------------------------------------------------------- +// | BaseTxIndex | Unconfirmed Txs... | Current TxIndex | +// ------------------------------------------------------- +// Access from the unconfirmed DB with range&priority: txIndex -1(previous tx) -> baseTxIndex + 1 +func (s *ParallelStateDB) getBalanceFromUnconfirmedDB(addr common.Address) *big.Int { + if addr == s.parallel.systemAddress { + // never get systemaddress from unconfirmed DB + return nil + } + + for i := s.txIndex - 1; i > s.parallel.baseStateDB.txIndex; i-- { + db_, ok := s.parallel.unconfirmedDBs.Load(i) + if !ok { + continue + } + db := db_.(*ParallelStateDB) + // 1.Refer the state of address, exist or not in dirtiedStateObjectsInSlot + balanceHit := false + if _, exist := db.parallel.addrStateChangesInSlot[addr]; exist { + balanceHit = true + } + if _, exist := db.parallel.balanceChangesInSlot[addr]; exist { // only changed balance is reliable + balanceHit = true + } + if !balanceHit { + continue + } + obj := db.parallel.dirtiedStateObjectsInSlot[addr] + balance := obj.Balance() + if obj.deleted { + balance = common.Big0 + } + return balance + + } + return nil +} + +// Similar to getBalanceFromUnconfirmedDB +func (s *ParallelStateDB) getNonceFromUnconfirmedDB(addr common.Address) (uint64, bool) { + if addr == s.parallel.systemAddress { + // never get systemaddress from unconfirmed DB + return 0, false + } + + for i := s.txIndex - 1; i > s.parallel.baseStateDB.txIndex; i-- { + db_, ok := s.parallel.unconfirmedDBs.Load(i) + if !ok { + continue + } + db := db_.(*ParallelStateDB) + + nonceHit := false + if _, ok := db.parallel.addrStateChangesInSlot[addr]; ok { + nonceHit = true + } else if _, ok := db.parallel.nonceChangesInSlot[addr]; ok { + nonceHit = true + } + if !nonceHit { + // nonce refer not hit, try next unconfirmedDb + continue + } + // nonce hit, return the nonce + obj := db.parallel.dirtiedStateObjectsInSlot[addr] + if obj == nil { + // could not exist, if it is changed but reverted + // fixme: revert should remove the change record + log.Debug("Get nonce from UnconfirmedDB, changed but object not exist, ", + "txIndex", s.txIndex, "referred txIndex", i, "addr", addr) + continue + } + nonce := obj.Nonce() + // deleted object with nonce == 0 + if obj.deleted { + nonce = 0 + } + return nonce, true + } + return 0, false +} + +// Similar to getBalanceFromUnconfirmedDB +// It is not only for code, but also codeHash and codeSize, we return the *StateObject for convienence. +func (s *ParallelStateDB) getCodeFromUnconfirmedDB(addr common.Address) ([]byte, bool) { + if addr == s.parallel.systemAddress { + // never get systemaddress from unconfirmed DB + return nil, false + } + + for i := s.txIndex - 1; i > s.parallel.baseStateDB.txIndex; i-- { + db_, ok := s.parallel.unconfirmedDBs.Load(i) + if !ok { + continue + } + db := db_.(*ParallelStateDB) + + codeHit := false + if _, exist := db.parallel.addrStateChangesInSlot[addr]; exist { + codeHit = true + } + if _, exist := db.parallel.codeChangesInSlot[addr]; exist { + codeHit = true + } + if !codeHit { + // try next unconfirmedDb + continue + } + obj := db.parallel.dirtiedStateObjectsInSlot[addr] + if obj == nil { + // could not exist, if it is changed but reverted + // fixme: revert should remove the change record + log.Debug("Get code from UnconfirmedDB, changed but object not exist, ", + "txIndex", s.txIndex, "referred txIndex", i, "addr", addr) + continue + } + code := obj.Code(s.db) + if obj.deleted { + code = nil + } + return code, true + + } + return nil, false +} + +// Similar to getCodeFromUnconfirmedDB +// but differ when address is deleted or not exist +func (s *ParallelStateDB) getCodeHashFromUnconfirmedDB(addr common.Address) (common.Hash, bool) { + if addr == s.parallel.systemAddress { + // never get systemaddress from unconfirmed DB + return common.Hash{}, false + } + + for i := s.txIndex - 1; i > s.parallel.baseStateDB.txIndex; i-- { + db_, ok := s.parallel.unconfirmedDBs.Load(i) + if !ok { + continue + } + db := db_.(*ParallelStateDB) + hashHit := false + if _, exist := db.parallel.addrStateChangesInSlot[addr]; exist { + hashHit = true + } + if _, exist := db.parallel.codeChangesInSlot[addr]; exist { + hashHit = true + } + if !hashHit { + // try next unconfirmedDb + continue + } + obj := db.parallel.dirtiedStateObjectsInSlot[addr] + if obj == nil { + // could not exist, if it is changed but reverted + // fixme: revert should remove the change record + log.Debug("Get codeHash from UnconfirmedDB, changed but object not exist, ", + "txIndex", s.txIndex, "referred txIndex", i, "addr", addr) + continue + } + codeHash := common.Hash{} + if !obj.deleted { + codeHash = common.BytesToHash(obj.CodeHash()) + } + return codeHash, true + } + return common.Hash{}, false +} + +// Similar to getCodeFromUnconfirmedDB +// It is for address state check of: Exist(), Empty() and HasSuicided() +// Since the unconfirmed DB should have done Finalise() with `deleteEmptyObjects = true` +// If the dirty address is empty or suicided, it will be marked as deleted, so we only need to return `deleted` or not. +func (s *ParallelStateDB) getAddrStateFromUnconfirmedDB(addr common.Address) (bool, bool) { + if addr == s.parallel.systemAddress { + // never get systemaddress from unconfirmed DB + return false, false + } + + // check the unconfirmed DB with range: baseTxIndex -> txIndex -1(previous tx) + for i := s.txIndex - 1; i > s.parallel.baseStateDB.txIndex; i-- { + db_, ok := s.parallel.unconfirmedDBs.Load(i) + if !ok { + continue + } + db := db_.(*ParallelStateDB) + if exist, ok := db.parallel.addrStateChangesInSlot[addr]; ok { + if _, ok := db.parallel.dirtiedStateObjectsInSlot[addr]; !ok { + // could not exist, if it is changed but reverted + // fixme: revert should remove the change record + log.Debug("Get addr State from UnconfirmedDB, changed but object not exist, ", + "txIndex", s.txIndex, "referred txIndex", i, "addr", addr) + continue + } + + return exist, true + } + } + return false, false +} + +func (s *ParallelStateDB) getKVFromUnconfirmedDB(addr common.Address, key common.Hash) (common.Hash, bool) { + // check the unconfirmed DB with range: baseTxIndex -> txIndex -1(previous tx) + for i := s.txIndex - 1; i > s.parallel.baseStateDB.txIndex; i-- { + db_, ok := s.parallel.unconfirmedDBs.Load(i) + if !ok { + continue + } + db := db_.(*ParallelStateDB) + if _, ok := db.parallel.kvChangesInSlot[addr]; ok { + obj := db.parallel.dirtiedStateObjectsInSlot[addr] + if val, exist := obj.dirtyStorage.GetValue(key); exist { + return val, true + } + } + } + return common.Hash{}, false +} + +func (s *ParallelStateDB) getStateObjectFromUnconfirmedDB(addr common.Address) (*StateObject, bool) { + // check the unconfirmed DB with range: baseTxIndex -> txIndex -1(previous tx) + for i := s.txIndex - 1; i > s.parallel.baseStateDB.txIndex; i-- { + db_, ok := s.parallel.unconfirmedDBs.Load(i) + if !ok { + continue + } + db := db_.(*ParallelStateDB) + if obj, ok := db.parallel.dirtiedStateObjectsInSlot[addr]; ok { + return obj, true + } + } + return nil, false +} + +type KvCheckUnit struct { + addr common.Address + key common.Hash + val common.Hash +} +type KvCheckMessage struct { + slotDB *ParallelStateDB + isStage2 bool + kvUnit KvCheckUnit +} + +func hasKvConflict(slotDB *ParallelStateDB, addr common.Address, key common.Hash, val common.Hash, isStage2 bool) bool { + mainDB := slotDB.parallel.baseStateDB + + if isStage2 { // update slotDB's unconfirmed DB list and try + if valUnconfirm, ok := slotDB.getKVFromUnconfirmedDB(addr, key); ok { + if !bytes.Equal(val.Bytes(), valUnconfirm.Bytes()) { + log.Debug("IsSlotDBReadsValid KV read is invalid in unconfirmed", "addr", addr, + "valSlot", val, "valUnconfirm", valUnconfirm, + "SlotIndex", slotDB.parallel.SlotIndex, + "txIndex", slotDB.txIndex, "baseTxIndex", slotDB.parallel.baseTxIndex) + return true + } + } + } + valMain := mainDB.GetState(addr, key) + if !bytes.Equal(val.Bytes(), valMain.Bytes()) { + log.Debug("hasKvConflict is invalid", "addr", addr, + "key", key, "valSlot", val, + "valMain", valMain, "SlotIndex", slotDB.parallel.SlotIndex, + "txIndex", slotDB.txIndex, "baseTxIndex", slotDB.parallel.baseTxIndex) + return true // return false, Range will be terminated. + } + return false +} + +var checkReqCh chan KvCheckMessage +var checkResCh chan bool + +func StartKvCheckLoop() { + // start routines to do conflict check + checkReqCh = make(chan KvCheckMessage, 200) + checkResCh = make(chan bool, 10) + for i := 0; i < runtime.NumCPU(); i++ { + go func() { + for { + kvEle1 := <-checkReqCh + checkResCh <- hasKvConflict(kvEle1.slotDB, kvEle1.kvUnit.addr, + kvEle1.kvUnit.key, kvEle1.kvUnit.val, kvEle1.isStage2) + } + }() + } +} + +// in stage2, we do unconfirmed conflict detect +func (s *ParallelStateDB) IsParallelReadsValid(isStage2 bool, mergedTxIndex int) bool { + once.Do(func() { + StartKvCheckLoop() + }) + slotDB := s + mainDB := slotDB.parallel.baseStateDB + // for nonce + for addr, nonceSlot := range slotDB.parallel.nonceReadsInSlot { + if isStage2 { // update slotDB's unconfirmed DB list and try + if nonceUnconfirm, ok := slotDB.getNonceFromUnconfirmedDB(addr); ok { + if nonceSlot != nonceUnconfirm { + log.Debug("IsSlotDBReadsValid nonce read is invalid in unconfirmed", "addr", addr, + "nonceSlot", nonceSlot, "nonceUnconfirm", nonceUnconfirm, "SlotIndex", slotDB.parallel.SlotIndex, + "txIndex", slotDB.txIndex, "baseTxIndex", slotDB.parallel.baseTxIndex) + return false + } + } + } + nonceMain := mainDB.GetNonce(addr) + if nonceSlot != nonceMain { + log.Debug("IsSlotDBReadsValid nonce read is invalid", "addr", addr, + "nonceSlot", nonceSlot, "nonceMain", nonceMain, "SlotIndex", slotDB.parallel.SlotIndex, + "txIndex", slotDB.txIndex, "baseTxIndex", slotDB.parallel.baseTxIndex) + return false + } + } + // balance + for addr, balanceSlot := range slotDB.parallel.balanceReadsInSlot { + if isStage2 { // update slotDB's unconfirmed DB list and try + if balanceUnconfirm := slotDB.getBalanceFromUnconfirmedDB(addr); balanceUnconfirm != nil { + if balanceSlot.Cmp(balanceUnconfirm) == 0 { + continue + } + if addr == WBNBAddress && slotDB.WBNBMakeUp() { + log.Debug("IsSlotDBReadsValid skip makeup for WBNB in stage 2", + "SlotIndex", slotDB.parallel.SlotIndex, "txIndex", slotDB.txIndex) + continue // stage2 will skip WBNB check, no balance makeup + } + return false + } + } + + if addr != slotDB.parallel.systemAddress { // skip balance check for system address + balanceMain := mainDB.GetBalance(addr) + if balanceSlot.Cmp(balanceMain) != 0 { + if addr == WBNBAddress && slotDB.WBNBMakeUp() { // WBNB balance make up + if isStage2 { + log.Debug("IsSlotDBReadsValid skip makeup for WBNB in stage 2", + "SlotIndex", slotDB.parallel.SlotIndex, "txIndex", slotDB.txIndex) + continue // stage2 will skip WBNB check, no balance makeup + } + if _, ok := s.parallel.balanceChangesInSlot[addr]; !ok { + // balance unchanged, no need to make up + log.Debug("IsSlotDBReadsValid WBNB balance no makeup since it is not changed ", + "SlotIndex", slotDB.parallel.SlotIndex, "txIndex", slotDB.txIndex, + "updated WBNB balance", slotDB.GetBalance(addr)) + continue + } + balanceDelta := new(big.Int).Sub(balanceMain, balanceSlot) + slotDB.wbnbMakeUpBalance = new(big.Int).Add(slotDB.GetBalance(addr), balanceDelta) + /* + if _, exist := slotDB.stateObjectsPending[addr]; !exist { + slotDB.stateObjectsPending[addr] = struct{}{} + } + if _, exist := slotDB.stateObjectsDirty[addr]; !exist { + // only read, but never change WBNB's balance or state + // log.Warn("IsSlotDBReadsValid balance makeup for WBNB, but it is not in dirty", + // "SlotIndex", slotDB.parallel.SlotIndex, "txIndex", slotDB.txIndex) + slotDB.stateObjectsDirty[addr] = struct{}{} + } + */ + log.Debug("IsSlotDBReadsValid balance makeup for WBNB", + "SlotIndex", slotDB.parallel.SlotIndex, "txIndex", slotDB.txIndex, + "updated WBNB balance", slotDB.GetBalance(addr)) + continue + } + + log.Debug("IsSlotDBReadsValid balance read is invalid", "addr", addr, + "balanceSlot", balanceSlot, "balanceMain", balanceMain, "SlotIndex", slotDB.parallel.SlotIndex, + "txIndex", slotDB.txIndex, "baseTxIndex", slotDB.parallel.baseTxIndex) + return false + } + } + } + // check KV + var units []KvCheckUnit // todo: pre-allocate to make it faster + for addr, read := range slotDB.parallel.kvReadsInSlot { + read.Range(func(keySlot, valSlot interface{}) bool { + units = append(units, KvCheckUnit{addr, keySlot.(common.Hash), valSlot.(common.Hash)}) + return true + }) + } + readLen := len(units) + if readLen < 8 || isStage2 { + for _, unit := range units { + if hasKvConflict(slotDB, unit.addr, unit.key, unit.val, isStage2) { + return false + } + } + } else { + msgHandledNum := 0 + msgSendNum := 0 + for _, unit := range units { + for { // make sure the unit is consumed + consumed := false + select { + case conflict := <-checkResCh: // consume result if checkReqCh is blocked + msgHandledNum++ + if conflict { + // make sure all request are handled or discarded + for { + if msgHandledNum == msgSendNum { + break + } + select { + case <-checkReqCh: + msgHandledNum++ + case <-checkResCh: + msgHandledNum++ + } + } + return false + } + case checkReqCh <- KvCheckMessage{slotDB, isStage2, unit}: + msgSendNum++ + consumed = true + } + if consumed { + break + } + } + } + for { + if msgHandledNum == readLen { + break + } + conflict := <-checkResCh + msgHandledNum++ + if conflict { + // make sure all request are handled or discarded + for { + if msgHandledNum == msgSendNum { + break + } + select { + case <-checkReqCh: + msgHandledNum++ + case <-checkResCh: + msgHandledNum++ + } + } + return false + } + } + } + if isStage2 { // stage2 skip check code, or state, since they are likely unchanged. + return true + } + + // check code + for addr, codeSlot := range slotDB.parallel.codeReadsInSlot { + codeMain := mainDB.GetCode(addr) + if !bytes.Equal(codeSlot, codeMain) { + log.Debug("IsSlotDBReadsValid code read is invalid", "addr", addr, + "len codeSlot", len(codeSlot), "len codeMain", len(codeMain), "SlotIndex", slotDB.parallel.SlotIndex, + "txIndex", slotDB.txIndex, "baseTxIndex", slotDB.parallel.baseTxIndex) + return false + } + } + // check codeHash + for addr, codeHashSlot := range slotDB.parallel.codeHashReadsInSlot { + codeHashMain := mainDB.GetCodeHash(addr) + if !bytes.Equal(codeHashSlot.Bytes(), codeHashMain.Bytes()) { + log.Debug("IsSlotDBReadsValid codehash read is invalid", "addr", addr, + "codeHashSlot", codeHashSlot, "codeHashMain", codeHashMain, "SlotIndex", slotDB.parallel.SlotIndex, + "txIndex", slotDB.txIndex, "baseTxIndex", slotDB.parallel.baseTxIndex) + return false + } + } + // addr state check + for addr, stateSlot := range slotDB.parallel.addrStateReadsInSlot { + stateMain := false // addr not exist + if mainDB.getStateObject(addr) != nil { + stateMain = true // addr exist in main DB + } + if stateSlot != stateMain { + // skip addr state check for system address + if addr != slotDB.parallel.systemAddress { + log.Debug("IsSlotDBReadsValid addrState read invalid(true: exist, false: not exist)", + "addr", addr, "stateSlot", stateSlot, "stateMain", stateMain, + "SlotIndex", slotDB.parallel.SlotIndex, + "txIndex", slotDB.txIndex, "baseTxIndex", slotDB.parallel.baseTxIndex) + return false + } + } + } + // snapshot destructs check + for addr, destructRead := range slotDB.parallel.addrSnapDestructsReadsInSlot { + mainObj := mainDB.getStateObject(addr) + if mainObj == nil { + log.Debug("IsSlotDBReadsValid snapshot destructs read invalid, address should exist", + "addr", addr, "destruct", destructRead, + "SlotIndex", slotDB.parallel.SlotIndex, + "txIndex", slotDB.txIndex, "baseTxIndex", slotDB.parallel.baseTxIndex) + return false + } + slotDB.snapParallelLock.RLock() // fixme: this lock is not needed + _, destructMain := mainDB.snapDestructs[addr] // addr not exist + slotDB.snapParallelLock.RUnlock() + if destructRead != destructMain { + log.Debug("IsSlotDBReadsValid snapshot destructs read invalid", + "addr", addr, "destructRead", destructRead, "destructMain", destructMain, + "SlotIndex", slotDB.parallel.SlotIndex, + "txIndex", slotDB.txIndex, "baseTxIndex", slotDB.parallel.baseTxIndex) + return false + } + } + + return true +} + +// For most of the transactions, systemAddressOpsCount should be 3: +// one for SetBalance(0) on NewSlotDB() +// the other is for AddBalance(GasFee) at the end. +// (systemAddressOpsCount > 3) means the transaction tries to access systemAddress, in +// this case, we should redo and keep its balance on NewSlotDB() +// for example: +// https://bscscan.com/tx/0xe469f1f948de90e9508f96da59a96ed84b818e71432ca11c5176eb60eb66671b +func (s *ParallelStateDB) SystemAddressRedo() bool { + if s.parallel.systemAddressOpsCount > 4 { + log.Info("SystemAddressRedo", "SlotIndex", s.parallel.SlotIndex, + "txIndex", s.txIndex, + "systemAddressOpsCount", s.parallel.systemAddressOpsCount) + return true + } + return false +} + +// NeedsRedo returns true if there is any clear reason that we need to redo this transaction +func (s *ParallelStateDB) NeedsRedo() bool { + return s.parallel.needsRedo +} + +/** + * WBNB makeup is allowed when WBNB'balance is only accessed through contract Call. + * If it is accessed not through contract all, e.g., by `address.balance`, `address.transfer(amount)`, + * we can not do balance make up. + */ +/* +fixme: not work... wbnbBalanceAccessedExpected is not correct... +dumped log: +wbnbBalanceAccessed=3 wbnbBalanceAccessedExpected=0 +wbnbBalanceAccessed=9 wbnbBalanceAccessedExpected=4 +wbnbBalanceAccessed=9 wbnbBalanceAccessedExpected=4 +wbnbBalanceAccessed=12 wbnbBalanceAccessedExpected=2 +wbnbBalanceAccessed=10 wbnbBalanceAccessedExpected=2 +wbnbBalanceAccessed=12 wbnbBalanceAccessedExpected=4 +wbnbBalanceAccessed=9 wbnbBalanceAccessedExpected=4 +wbnbBalanceAccessed=13 wbnbBalanceAccessedExpected=2 +wbnbBalanceAccessed=7 wbnbBalanceAccessedExpected=2 +wbnbBalanceAccessed=9 wbnbBalanceAccessedExpected=4 +wbnbBalanceAccessed=9 wbnbBalanceAccessedExpected=4 +wbnbBalanceAccessed=9 wbnbBalanceAccessedExpected=4 +wbnbBalanceAccessed=9 wbnbBalanceAccessedExpected=4 +wbnbBalanceAccessed=9 wbnbBalanceAccessedExpected=4 +wbnbBalanceAccessed=9 wbnbBalanceAccessedExpected=4 +*/ +func (s *ParallelStateDB) WBNBMakeUp() bool { + return s.wbnbMakeUp +} + +func (s *ParallelStateDB) ParallelMakeUp(addr common.Address, input []byte) { + if addr == WBNBAddress { + if len(input) < 4 { + // should never less than 4 + // log.Warn("ParallelMakeUp for WBNB input size invalid", "input size", len(input), "input", input) + s.wbnbMakeUp = false + return + } + methodId := input[:4] + if bytes.Equal(methodId, WBNBAddress_deposit) { + // log.Debug("ParallelMakeUp for WBNB deposit", "input size", len(input), "input", input) + // s.wbnbBalanceAccessedExpected += 2 // AddBalance() + return + } + if bytes.Equal(methodId, WBNBAddress_withdraw) { + // log.Debug("ParallelMakeUp for WBNB withdraw", "input size", len(input), "input", input) + // ** If from's balance is not enough, it will revert ==> +2, only AddBalance() + // ** if from's balance is enough, ==> +4, AddBalance(), SubBalance() for transfer + // attention, WBNB contract's balance should always sufficient + // s.wbnbBalanceAccessedExpected += 4 + + // as noted above, withdraw's access depends revert or not. + // we have to hack RevertToSnapshot to get the really access count, disable right now. + // s.wbnbMakeUp = false + return + } + if bytes.Equal(methodId, WBNBAddress_approve) { + // log.Debug("ParallelMakeUp for WBNB approve", "input size", len(input), "input", input) + // s.wbnbBalanceAccessedExpected += 2 + return + } + if bytes.Equal(methodId, WBNBAddress_transfer) { + // log.Debug("ParallelMakeUp for WBNB transfer", "input size", len(input), "input", input) + // This is WBNB token transfer, not balance transfer + // s.wbnbBalanceAccessedExpected += 2 + return + } + if bytes.Equal(methodId, WBNBAddress_transferFrom) { + // log.Debug("ParallelMakeUp for WBNB transferFrom", "input size", len(input), "input", input) + // This is WBNB token transfer, not balance transfer + // s.wbnbBalanceAccessedExpected += 2 + return + } + // if bytes.Equal(methodId, WBNBAddress_totalSupply) { + // log.Debug("ParallelMakeUp for WBNB, not for totalSupply", "input size", len(input), "input", input) + // s.wbnbMakeUp = false // can not makeup + // return + // } + + // log.Warn("ParallelMakeUp for WBNB unknown method id", "input size", len(input), "input", input) + s.wbnbMakeUp = false + } + +} diff --git a/core/state_processor.go b/core/state_processor.go index 1f56cd44a2..ead132ab17 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -24,6 +24,7 @@ import ( "math/rand" "runtime" "sync" + "sync/atomic" "time" "github.com/ethereum/go-ethereum/common" @@ -47,6 +48,14 @@ const ( recentTime = 1024 * 3 recentDiffLayerTimeout = 5 farDiffLayerTimeout = 2 + maxUnitSize = 10 + + parallelPrimarySlot = 0 + parallelShadowlot = 1 + + stage2CheckNumber = 30 // not fixed, use decrease? + stage2RedoNumber = 10 + stage2AheadNum = 3 // ? ) // StateProcessor is a basic Processor, which takes care of transitioning @@ -70,13 +79,28 @@ func NewStateProcessor(config *params.ChainConfig, bc *BlockChain, engine consen // add for parallel executions type ParallelStateProcessor struct { StateProcessor - parallelNum int // leave a CPU to dispatcher - queueSize int // parallel slot's maximum number of pending Txs - txResultChan chan *ParallelTxResult // to notify dispatcher that a tx is done - slotState []*SlotState // idle, or pending messages - mergedTxIndex int // the latest finalized tx index - debugErrorRedoNum int - debugConflictRedoNum int + parallelNum int // leave a CPU to dispatcher + queueSize int // parallel slot's maximum number of pending Txs + // pendingConfirmChan chan *ParallelTxResult + pendingConfirmResults map[int][]*ParallelTxResult // tx could be executed several times, with several result to check + txResultChan chan *ParallelTxResult // to notify dispatcher that a tx is done + // txReqAccountSorted map[common.Address][]*ParallelTxRequest // fixme: *ParallelTxRequest => ParallelTxRequest? + slotState []*SlotState // idle, or pending messages + mergedTxIndex int // the latest finalized tx index, fixme: use Atomic + slotDBsToRelease []*state.ParallelStateDB + debugConflictRedoNum int + unconfirmedResults *sync.Map // this is for stage2 confirm, since pendingConfirmResults can not be access in stage2 loop + unconfirmedDBs *sync.Map + stopSlotChan chan int // fixme: use struct{}{}, to make sure all slot are idle + stopConfirmChan chan struct{} // fixme: use struct{}{}, to make sure all slot are idle + confirmStage2Chan chan int + stopConfirmStage2Chan chan struct{} + allTxReqs []*ParallelTxRequest + txReqExecuteRecord map[int]int // for each the execute count of each Tx + txReqExecuteCount int + inConfirmStage2 bool + targetStage2Count int // when executed txNUM reach it, enter stage2 RT confirm + nextStage2TxIndex int } func NewParallelStateProcessor(config *params.ChainConfig, bc *BlockChain, engine consensus.Engine, parallelNum int, queueSize int) *ParallelStateProcessor { @@ -393,35 +417,50 @@ func (p *LightStateProcessor) LightProcess(diffLayer *types.DiffLayer, block *ty } type SlotState struct { - tailTxReq *ParallelTxRequest // tail pending Tx of the slot, should be accessed on dispatcher only. - pendingTxReqChan chan *ParallelTxRequest - pendingTxReqList []*ParallelTxRequest // maintained by dispatcher for dispatch policy - mergedChangeList []state.SlotChangeList - slotdbChan chan *state.StateDB // dispatch will create and send this slotDB to slot + pendingTxReqList []*ParallelTxRequest + primaryWakeUpChan chan *ParallelTxRequest + shadowWakeUpChan chan *ParallelTxRequest + primaryStopChan chan struct{} + shadowStopChan chan struct{} + + slotDBChan chan *state.ParallelStateDB // to update SlotDB + activatedType int32 // 0: primary slot, 1: shadow slot } type ParallelTxResult struct { - updateSlotDB bool // for redo and pending tx quest, slot needs new slotDB, - keepSystem bool // for redo, should keep system address's balance - slotIndex int // slot index - err error // to describe error message? - txReq *ParallelTxRequest - receipt *types.Receipt - slotDB *state.StateDB // if updated, it is not equal to txReq.slotDB + executedIndex int // the TxReq can be executed several time, increase index for each execution + updateSlotDB bool // for redo and pending tx quest, slot needs new slotDB, + // keepSystem bool // for redo, should keep system address's balance + slotIndex int // slot index + err error // to describe error message? + txReq *ParallelTxRequest + receipt *types.Receipt + slotDB *state.ParallelStateDB // if updated, it is not equal to txReq.slotDB + gpSlot *GasPool + evm *vm.EVM + result *ExecutionResult } type ParallelTxRequest struct { - txIndex int - tx *types.Transaction - slotDB *state.StateDB + txIndex int + // baseTxIndex int + baseStateDB *state.StateDB + staticSlotIndex int // static dispatched id + tx *types.Transaction + // slotDB *state.ParallelStateDB gasLimit uint64 msg types.Message block *types.Block vmConfig vm.Config bloomProcessor *AsyncReceiptBloomGenerator usedGas *uint64 - waitTxChan chan struct{} - curTxChan chan struct{} + curTxChan chan int + systemAddrRedo bool + // runnable 0: runnable + // runnable 1: it has been executed, but results has not been confirmed + // runnable 2: all confirmed, + runnable int32 + executedNum int } // to create and start the execution slot goroutines @@ -429,413 +468,482 @@ func (p *ParallelStateProcessor) init() { log.Info("Parallel execution mode is enabled", "Parallel Num", p.parallelNum, "CPUNum", runtime.NumCPU(), "QueueSize", p.queueSize) - p.txResultChan = make(chan *ParallelTxResult, p.parallelNum) - p.slotState = make([]*SlotState, p.parallelNum) + // In extreme case, parallelNum*2 are requiring updateStateDB, + // confirmLoop is deliverring the valid result or asking for AddrPrefetch. + p.txResultChan = make(chan *ParallelTxResult, 200) + p.stopSlotChan = make(chan int, 1) + p.stopConfirmChan = make(chan struct{}, 1) + p.stopConfirmStage2Chan = make(chan struct{}, 1) + p.slotState = make([]*SlotState, p.parallelNum) for i := 0; i < p.parallelNum; i++ { p.slotState[i] = &SlotState{ - slotdbChan: make(chan *state.StateDB, 1), - pendingTxReqChan: make(chan *ParallelTxRequest, p.queueSize), + slotDBChan: make(chan *state.ParallelStateDB, 1), + primaryWakeUpChan: make(chan *ParallelTxRequest, 1), + shadowWakeUpChan: make(chan *ParallelTxRequest, 1), + primaryStopChan: make(chan struct{}, 1), + shadowStopChan: make(chan struct{}, 1), } - // start the slot's goroutine + // start the primary slot's goroutine + go func(slotIndex int) { + p.runSlotLoop(slotIndex, parallelPrimarySlot) // this loop will be permanent live + }(i) + + // start the shadow slot. + // It is back up of the primary slot to make sure transaction can be redo ASAP, + // since the primary slot could be busy at executing another transaction go func(slotIndex int) { - p.runSlotLoop(slotIndex) // this loop will be permanent live + p.runSlotLoop(slotIndex, 1) // this loop will be permanent live }(i) + } + + // p.pendingConfirmChan = make(chan *ParallelTxResult, 400) + //go func() { + // p.runConfirmLoop() // this loop will be permanent live + //}() + p.confirmStage2Chan = make(chan int, 10) + go func() { + p.runConfirmStage2Loop() // this loop will be permanent live + }() } -// conflict check uses conflict window, it will check all state changes from (cfWindowStart + 1) -// to the previous Tx, if any state in readDb is updated in changeList, then it is conflicted -func (p *ParallelStateProcessor) hasStateConflict(readDb *state.StateDB, changeList state.SlotChangeList) bool { - // check KV change - reads := readDb.StateReadsInSlot() - writes := changeList.StateChangeSet - for readAddr, readKeys := range reads { - if _, exist := changeList.AddrStateChangeSet[readAddr]; exist { - log.Debug("conflict: read addr changed state", "addr", readAddr) - return true - } - if writeKeys, ok := writes[readAddr]; ok { - // readAddr exist - for writeKey := range writeKeys { - // same addr and same key, mark conflicted - if _, ok := readKeys[writeKey]; ok { - log.Debug("conflict: state conflict", "addr", readAddr, "key", writeKey) - return true - } - } - } +// clear slot state for each block. +func (p *ParallelStateProcessor) resetState(txNum int, statedb *state.StateDB) { + if txNum == 0 { + return } - // check balance change - balanceReads := readDb.BalanceReadsInSlot() - balanceWrite := changeList.BalanceChangeSet - for readAddr := range balanceReads { - if _, exist := changeList.AddrStateChangeSet[readAddr]; exist { - // SystemAddress is special, SystemAddressRedo() is prepared for it. - // Since txIndex = 0 will create StateObject for SystemAddress, skip its state change check - if readAddr != consensus.SystemAddress { - log.Debug("conflict: read addr changed balance", "addr", readAddr) - return true - } + p.mergedTxIndex = -1 + p.debugConflictRedoNum = 0 + p.inConfirmStage2 = false + // p.txReqAccountSorted = make(map[common.Address][]*ParallelTxRequest) // fixme: to be reused? + + statedb.PrepareForParallel() + p.allTxReqs = make([]*ParallelTxRequest, 0) + p.slotDBsToRelease = make([]*state.ParallelStateDB, 0, txNum) + + stateDBsToRelease := p.slotDBsToRelease + go func() { + for _, slotDB := range stateDBsToRelease { + slotDB.PutSyncPool() } - if _, ok := balanceWrite[readAddr]; ok { - if readAddr != consensus.SystemAddress { - log.Debug("conflict: balance conflict", "addr", readAddr) - return true + }() + for _, slot := range p.slotState { + slot.pendingTxReqList = make([]*ParallelTxRequest, 0) + slot.activatedType = 0 + } + p.unconfirmedResults = new(sync.Map) // make(map[int]*state.ParallelStateDB) + p.unconfirmedDBs = new(sync.Map) // make(map[int]*state.ParallelStateDB) + p.pendingConfirmResults = make(map[int][]*ParallelTxResult, 200) + p.txReqExecuteRecord = make(map[int]int, 200) + p.txReqExecuteCount = 0 + p.nextStage2TxIndex = 0 +} + +// Benefits of StaticDispatch: +// ** try best to make Txs with same From() in same slot +// ** reduce IPC cost by dispatch in Unit +// ** make sure same From in same slot +// ** try to make it balanced, queue to the most hungry slot for new Address +func (p *ParallelStateProcessor) doStaticDispatch(mainStatedb *state.StateDB, txReqs []*ParallelTxRequest) { + fromSlotMap := make(map[common.Address]int, 100) + toSlotMap := make(map[common.Address]int, 100) + for _, txReq := range txReqs { + var slotIndex int = -1 + if i, ok := fromSlotMap[txReq.msg.From()]; ok { + // first: same From are all in same slot + slotIndex = i + } else if txReq.msg.To() != nil { + // To Address, with txIndex sorted, could be in different slot. + // fixme: Create will move to hungry slot + if i, ok := toSlotMap[*txReq.msg.To()]; ok { + slotIndex = i } } - } - // check code change - codeReads := readDb.CodeReadsInSlot() - codeWrite := changeList.CodeChangeSet - for readAddr := range codeReads { - if _, exist := changeList.AddrStateChangeSet[readAddr]; exist { - log.Debug("conflict: read addr changed code", "addr", readAddr) - return true + // not found, dispatch to most hungry slot + if slotIndex == -1 { + var workload int = len(p.slotState[0].pendingTxReqList) + slotIndex = 0 + for i, slot := range p.slotState { // can start from index 1 + if len(slot.pendingTxReqList) < workload { + slotIndex = i + workload = len(slot.pendingTxReqList) + } + } } - if _, ok := codeWrite[readAddr]; ok { - log.Debug("conflict: code conflict", "addr", readAddr) - return true + // update + fromSlotMap[txReq.msg.From()] = slotIndex + if txReq.msg.To() != nil { + toSlotMap[*txReq.msg.To()] = slotIndex } + + slot := p.slotState[slotIndex] + txReq.staticSlotIndex = slotIndex // txreq is better to be executed in this slot + slot.pendingTxReqList = append(slot.pendingTxReqList, txReq) } +} - // check address state change: create, suicide... - addrReads := readDb.AddressReadsInSlot() - addrWrite := changeList.AddrStateChangeSet - nonceWrite := changeList.NonceChangeSet - for readAddr := range addrReads { - if _, ok := addrWrite[readAddr]; ok { - // SystemAddress is special, SystemAddressRedo() is prepared for it. - // Since txIndex = 0 will create StateObject for SystemAddress, skip its state change check - if readAddr != consensus.SystemAddress { - log.Debug("conflict: address state conflict", "addr", readAddr) - return true - } +// do conflict detect +func (p *ParallelStateProcessor) hasConflict(txResult *ParallelTxResult, isStage2 bool) bool { + slotDB := txResult.slotDB + if txResult.err != nil { + return true + } else if slotDB.SystemAddressRedo() { + if !isStage2 { + // for system addr redo, it has to wait until it's turn to keep the system address balance + txResult.txReq.systemAddrRedo = true } - if _, ok := nonceWrite[readAddr]; ok { - log.Debug("conflict: address nonce conflict", "addr", readAddr) + return true + } else if slotDB.NeedsRedo() { + // if this is any reason that indicates this transaction needs to redo, skip the conflict check + return true + } else { + // to check if what the slot db read is correct. + if !slotDB.IsParallelReadsValid(isStage2, p.mergedTxIndex) { return true } } - return false } -// for parallel execute, we put contracts of same address in a slot, -// since these txs probably would have conflicts -func (p *ParallelStateProcessor) queueSameToAddress(txReq *ParallelTxRequest) bool { - txToAddr := txReq.tx.To() - // To() == nil means contract creation, no same To address - if txToAddr == nil { - return false - } - for i, slot := range p.slotState { - if slot.tailTxReq == nil { // this slot is idle - continue +func (p *ParallelStateProcessor) switchSlot(slotIndex int) { + slot := p.slotState[slotIndex] + if atomic.CompareAndSwapInt32(&slot.activatedType, 0, 1) { + // switch from normal to shadow slot + if len(slot.shadowWakeUpChan) == 0 { + slot.shadowWakeUpChan <- nil // only notify when target once } - for _, pending := range slot.pendingTxReqList { - // To() == nil means contract creation, skip it. - if pending.tx.To() == nil { - continue - } - // same to address, put it on slot's pending list. - if *txToAddr == *pending.tx.To() { - select { - case slot.pendingTxReqChan <- txReq: - slot.tailTxReq = txReq - slot.pendingTxReqList = append(slot.pendingTxReqList, txReq) - log.Debug("queue same To address", "Slot", i, "txIndex", txReq.txIndex) - return true - default: - log.Debug("queue same To address, but queue is full", "Slot", i, "txIndex", txReq.txIndex) - break // try next slot - } - } + } else if atomic.CompareAndSwapInt32(&slot.activatedType, 1, 0) { + // switch from shadow to normal slot + if len(slot.primaryWakeUpChan) == 0 { + slot.primaryWakeUpChan <- nil // only notify when target once } } - return false } -// for parallel execute, we put contracts of same address in a slot, -// since these txs probably would have conflicts -func (p *ParallelStateProcessor) queueSameFromAddress(txReq *ParallelTxRequest) bool { - txFromAddr := txReq.msg.From() - for i, slot := range p.slotState { - if slot.tailTxReq == nil { // this slot is idle - continue - } - for _, pending := range slot.pendingTxReqList { - // same from address, put it on slot's pending list. - if txFromAddr == pending.msg.From() { - select { - case slot.pendingTxReqChan <- txReq: - slot.tailTxReq = txReq - slot.pendingTxReqList = append(slot.pendingTxReqList, txReq) - log.Debug("queue same From address", "Slot", i, "txIndex", txReq.txIndex) - return true - default: - log.Debug("queue same From address, but queue is full", "Slot", i, "txIndex", txReq.txIndex) - break // try next slot - } - } +func (p *ParallelStateProcessor) executeInSlot(slotIndex int, txReq *ParallelTxRequest) *ParallelTxResult { + txReq.executedNum++ // fixme: atomic? + slotDB := state.NewSlotDB(txReq.baseStateDB, consensus.SystemAddress, txReq.txIndex, + p.mergedTxIndex, txReq.systemAddrRedo, p.unconfirmedDBs) + + slotDB.Prepare(txReq.tx.Hash(), txReq.block.Hash(), txReq.txIndex) + blockContext := NewEVMBlockContext(txReq.block.Header(), p.bc, nil) // can share blockContext within a block for efficiency + vmenv := vm.NewEVM(blockContext, vm.TxContext{}, slotDB, p.config, txReq.vmConfig) + // gasLimit not accurate, but it is ok for block import. + // each slot would use its own gas pool, and will do gaslimit check later + gpSlot := new(GasPool).AddGas(txReq.gasLimit) // block.GasLimit() + + evm, result, err := applyTransactionStageExecution(txReq.msg, gpSlot, slotDB, vmenv) + txResult := ParallelTxResult{ + executedIndex: txReq.executedNum, + updateSlotDB: false, + slotIndex: slotIndex, + txReq: txReq, + receipt: nil, // receipt is generated in finalize stage + slotDB: slotDB, + err: err, + gpSlot: gpSlot, + evm: evm, + result: result, + } + if err == nil { + if result.Failed() { + // if Tx is reverted, all its state change will be discarded + slotDB.RevertSlotDB(txReq.msg.From()) } + slotDB.Finalise(true) // Finalise could write s.parallel.addrStateChangesInSlot[addr], keep Read and Write in same routine to avoid crash + p.unconfirmedDBs.Store(txReq.txIndex, slotDB) + } else { + // the transaction failed at check(nonce or blanace), actually it has not been executed yet. + atomic.CompareAndSwapInt32(&txReq.runnable, 0, 1) + // the error could be caused by unconfirmed balance reference, + // the balance could insufficient to pay its gas limit, which cause it preCheck.buyGas() failed + // redo could solve it. + log.Debug("In slot execution error", "error", err, + "slotIndex", slotIndex, "txIndex", txReq.txIndex) } - return false + p.unconfirmedResults.Store(txReq.txIndex, &txResult) + return &txResult } -// if there is idle slot, dispatch the msg to the first idle slot -func (p *ParallelStateProcessor) dispatchToIdleSlot(statedb *state.StateDB, txReq *ParallelTxRequest) bool { - for i, slot := range p.slotState { - if slot.tailTxReq == nil { - if len(slot.mergedChangeList) == 0 { - // first transaction of a slot, there is no usable SlotDB, have to create one for it. - txReq.slotDB = state.NewSlotDB(statedb, consensus.SystemAddress, p.mergedTxIndex, false) - } - log.Debug("dispatchToIdleSlot", "Slot", i, "txIndex", txReq.txIndex) - slot.tailTxReq = txReq - slot.pendingTxReqList = append(slot.pendingTxReqList, txReq) - slot.pendingTxReqChan <- txReq - return true +// to confirm a serial TxResults with same txIndex +func (p *ParallelStateProcessor) toConfirmTxIndex(targetTxIndex int, isStage2 bool) *ParallelTxResult { + if isStage2 { + if targetTxIndex <= p.mergedTxIndex+1 { + // this is the one that can been merged, + // others are for likely conflict check, since it is not their tuen. + // log.Warn("to confirm in stage 2, invalid txIndex", + // "targetTxIndex", targetTxIndex, "p.mergedTxIndex", p.mergedTxIndex) + return nil } } - return false -} -// wait until the next Tx is executed and its result is merged to the main stateDB -func (p *ParallelStateProcessor) waitUntilNextTxDone(statedb *state.StateDB, gp *GasPool) *ParallelTxResult { - var result *ParallelTxResult for { - result = <-p.txResultChan - // slot may request new slotDB, if slotDB is outdated - // such as: - // tx in pending tx request, previous tx in same queue is likely "damaged" the slotDB - // tx redo for conflict - // tx stage 1 failed, nonce out of order... - if result.updateSlotDB { - // the target slot is waiting for new slotDB - slotState := p.slotState[result.slotIndex] - slotDB := state.NewSlotDB(statedb, consensus.SystemAddress, p.mergedTxIndex, result.keepSystem) - slotState.slotdbChan <- slotDB + // handle a targetTxIndex in a loop + // targetTxIndex = p.mergedTxIndex + 1 + // select a unconfirmedResult to check + var targetResult *ParallelTxResult + if isStage2 { + result, ok := p.unconfirmedResults.Load(targetTxIndex) + if !ok { + return nil + } + targetResult = result.(*ParallelTxResult) + // in stage 2, don't schedule a new redo if the TxReq is: + // a.runnable: it will be redo + // b.running: the new result will be more reliable, we skip check right now + if atomic.CompareAndSwapInt32(&targetResult.txReq.runnable, 1, 1) { + return nil + } + if targetResult.executedIndex < targetResult.txReq.executedNum { + return nil + } + } else { + results := p.pendingConfirmResults[targetTxIndex] + resultsLen := len(results) + if resultsLen == 0 { // no pending result can be verified, break and wait for incoming results + return nil + } + targetResult = results[len(results)-1] // last is the most fresh, stack based priority + p.pendingConfirmResults[targetTxIndex] = p.pendingConfirmResults[targetTxIndex][:resultsLen-1] // remove from the queue + } + + valid := p.toConfirmTxIndexResult(targetResult, isStage2) + if !valid { + staticSlotIndex := targetResult.txReq.staticSlotIndex // it is better to run the TxReq in its static dispatch slot + if isStage2 { + atomic.CompareAndSwapInt32(&targetResult.txReq.runnable, 0, 1) // needs redo + p.debugConflictRedoNum++ + // interrupt the slot's current routine, and switch to the other routine + p.switchSlot(staticSlotIndex) + return nil + } + if len(p.pendingConfirmResults[targetTxIndex]) == 0 { // this is the last result to check and it is not valid + atomic.CompareAndSwapInt32(&targetResult.txReq.runnable, 0, 1) // needs redo + p.debugConflictRedoNum++ + // interrupt its current routine, and switch to the other routine + p.switchSlot(staticSlotIndex) + return nil + } continue } - // ok, the tx result is valid and can be merged - break + if isStage2 { + // likely valid, but not sure, can not deliver + // fixme: need to handle txResult repeatedly check? + return nil + } + return targetResult } +} - if err := gp.SubGas(result.receipt.GasUsed); err != nil { - log.Error("gas limit reached", "block", result.txReq.block.Number(), - "txIndex", result.txReq.txIndex, "GasUsed", result.receipt.GasUsed, "gp.Gas", gp.Gas()) +// to confirm one txResult, return true if the result is valid +// if it is in Stage 2 it is a likely result, not 100% sure +func (p *ParallelStateProcessor) toConfirmTxIndexResult(txResult *ParallelTxResult, isStage2 bool) bool { + txReq := txResult.txReq + if p.hasConflict(txResult, isStage2) { + return false } - - resultSlotIndex := result.slotIndex - resultTxIndex := result.txReq.txIndex - resultSlotState := p.slotState[resultSlotIndex] - resultSlotState.pendingTxReqList = resultSlotState.pendingTxReqList[1:] - if resultSlotState.tailTxReq.txIndex == resultTxIndex { - log.Debug("ProcessParallel slot is idle", "Slot", resultSlotIndex) - resultSlotState.tailTxReq = nil + if isStage2 { // not its turn + return true // likely valid, not sure, not finalized right now. } - // Slot's mergedChangeList is produced by dispatcher, while consumed by slot. - // It is safe, since write and read is in sequential, do write -> notify -> read - // It is not good, but work right now. - changeList := statedb.MergeSlotDB(result.slotDB, result.receipt, resultTxIndex) - resultSlotState.mergedChangeList = append(resultSlotState.mergedChangeList, changeList) - - if resultTxIndex != p.mergedTxIndex+1 { - log.Error("ProcessParallel tx result out of order", "resultTxIndex", resultTxIndex, - "p.mergedTxIndex", p.mergedTxIndex) + // goroutine unsafe operation will be handled from here for safety + gasConsumed := txReq.gasLimit - txResult.gpSlot.Gas() + if gasConsumed != txResult.result.UsedGas { + log.Error("gasConsumed != result.UsedGas mismatch", + "gasConsumed", gasConsumed, "result.UsedGas", txResult.result.UsedGas) } - p.mergedTxIndex = resultTxIndex - // notify the following Tx, it is merged, - // todo(optimize): if next tx is in same slot, it do not need to wait; save this channel cost. - close(result.txReq.curTxChan) - return result + + // ok, time to do finalize, stage2 should not be parallel + header := txReq.block.Header() + txResult.receipt, txResult.err = applyTransactionStageFinalization(txResult.evm, txResult.result, + txReq.msg, p.config, txResult.slotDB, header, + txReq.tx, txReq.usedGas, txReq.bloomProcessor) + txResult.updateSlotDB = false + return true } -func (p *ParallelStateProcessor) execInSlot(slotIndex int, txReq *ParallelTxRequest) *ParallelTxResult { - txIndex := txReq.txIndex - tx := txReq.tx - slotDB := txReq.slotDB - slotGasLimit := txReq.gasLimit // not accurate, but it is ok for block import. - msg := txReq.msg - block := txReq.block - header := block.Header() - cfg := txReq.vmConfig - bloomProcessor := txReq.bloomProcessor - - blockContext := NewEVMBlockContext(header, p.bc, nil) // can share blockContext within a block for efficiency - vmenv := vm.NewEVM(blockContext, vm.TxContext{}, slotDB, p.config, cfg) - - var receipt *types.Receipt - var result *ExecutionResult - var err error - var evm *vm.EVM - - slotDB.Prepare(tx.Hash(), block.Hash(), txIndex) - log.Debug("exec In Slot", "Slot", slotIndex, "txIndex", txIndex, "slotDB.baseTxIndex", slotDB.BaseTxIndex()) - - gpSlot := new(GasPool).AddGas(slotGasLimit) // each slot would use its own gas pool, and will do gaslimit check later - evm, result, err = applyTransactionStageExecution(msg, gpSlot, slotDB, vmenv) - log.Debug("Stage Execution done", "Slot", slotIndex, "txIndex", txIndex, "slotDB.baseTxIndex", slotDB.BaseTxIndex()) - - // wait until the previous tx is finalized. - if txReq.waitTxChan != nil { - log.Debug("Stage wait previous Tx done", "Slot", slotIndex, "txIndex", txIndex) - <-txReq.waitTxChan // close the channel +func (p *ParallelStateProcessor) runSlotLoop(slotIndex int, slotType int32) { + curSlot := p.slotState[slotIndex] + var wakeupChan chan *ParallelTxRequest + var stopChan chan struct{} + + if slotType == parallelPrimarySlot { + wakeupChan = curSlot.primaryWakeUpChan + stopChan = curSlot.primaryStopChan + } else { + wakeupChan = curSlot.shadowWakeUpChan + stopChan = curSlot.shadowStopChan } + for { + select { + case <-stopChan: + p.stopSlotChan <- slotIndex + continue + case <-wakeupChan: + } - // in parallel mode, tx can run into trouble, for example: err="nonce too high" - // in these cases, we will wait and re-run. - if err != nil { - p.debugErrorRedoNum++ - log.Debug("Stage Execution err", "Slot", slotIndex, "txIndex", txIndex, - "current slotDB.baseTxIndex", slotDB.BaseTxIndex(), "err", err) - redoResult := &ParallelTxResult{ - updateSlotDB: true, - slotIndex: slotIndex, - txReq: txReq, - receipt: receipt, - err: err, + interrupted := false + for _, txReq := range curSlot.pendingTxReqList { + if txReq.txIndex <= p.mergedTxIndex { + continue + } + if curSlot.activatedType != slotType { // fixme: atomic compare? + interrupted = true + break + } + if !atomic.CompareAndSwapInt32(&txReq.runnable, 1, 0) { + // not swapped: txReq.runnable == 0 + continue + } + result := p.executeInSlot(slotIndex, txReq) + if result == nil { // fixme: code improve, nil means block processed, to be stopped + break + } + p.txResultChan <- result } - p.txResultChan <- redoResult - slotDB = <-p.slotState[slotIndex].slotdbChan - slotDB.Prepare(tx.Hash(), block.Hash(), txIndex) - log.Debug("Stage Execution get new slotdb to redo", "Slot", slotIndex, - "txIndex", txIndex, "new slotDB.baseTxIndex", slotDB.BaseTxIndex()) - gpSlot = new(GasPool).AddGas(slotGasLimit) - evm, result, err = applyTransactionStageExecution(msg, gpSlot, slotDB, vmenv) - if err != nil { - log.Error("Stage Execution redo, error", err) + // switched to the other slot. + if interrupted { + continue } - } - // do conflict detect - hasConflict := false - systemAddrConflict := false - log.Debug("Stage Execution done, do conflict check", "Slot", slotIndex, "txIndex", txIndex) - if slotDB.SystemAddressRedo() { - hasConflict = true - systemAddrConflict = true - } else { - for index := 0; index < p.parallelNum; index++ { - if index == slotIndex { + // txReq in this Slot have all been executed, try steal one from other slot. + // as long as the TxReq is runable, we steal it, mark it as stolen + // steal one by one + for _, stealTxReq := range p.allTxReqs { + if stealTxReq.txIndex <= p.mergedTxIndex { continue } + if curSlot.activatedType != slotType { + interrupted = true + break + } - // check all finalizedDb from current slot's - for _, changeList := range p.slotState[index].mergedChangeList { - if changeList.TxIndex <= slotDB.BaseTxIndex() { - continue - } - if p.hasStateConflict(slotDB, changeList) { - log.Debug("Stage Execution conflict", "Slot", slotIndex, - "txIndex", txIndex, " conflict slot", index, "slotDB.baseTxIndex", slotDB.BaseTxIndex(), - "conflict txIndex", changeList.TxIndex) - hasConflict = true - break - } + if !atomic.CompareAndSwapInt32(&stealTxReq.runnable, 1, 0) { + // not swapped: txReq.runnable == 0 + continue } - if hasConflict { + result := p.executeInSlot(slotIndex, stealTxReq) + if result == nil { // fixme: code improve, nil means block processed, to be stopped break } + p.txResultChan <- result } } +} - if hasConflict { - p.debugConflictRedoNum++ - // re-run should not have conflict, since it has the latest world state. - redoResult := &ParallelTxResult{ - updateSlotDB: true, - keepSystem: systemAddrConflict, - slotIndex: slotIndex, - txReq: txReq, - receipt: receipt, - err: err, +func (p *ParallelStateProcessor) runConfirmStage2Loop() { + for { + // var mergedTxIndex int + select { + case <-p.stopConfirmStage2Chan: + for len(p.confirmStage2Chan) > 0 { + <-p.confirmStage2Chan + } + p.stopSlotChan <- -1 + continue + case <-p.confirmStage2Chan: + for len(p.confirmStage2Chan) > 0 { + <-p.confirmStage2Chan // drain the chan to get the latest merged txIndex + } } - p.txResultChan <- redoResult - slotDB = <-p.slotState[slotIndex].slotdbChan - slotDB.Prepare(tx.Hash(), block.Hash(), txIndex) - gpSlot = new(GasPool).AddGas(slotGasLimit) - evm, result, err = applyTransactionStageExecution(msg, gpSlot, slotDB, vmenv) - if err != nil { - log.Error("Stage Execution conflict redo, error", err) + // stage 2,if all tx have been executed at least once, and its result has been recevied. + // in Stage 2, we will run check when merge is advanced. + // more aggressive tx result confirm, even for these Txs not in turn + // now we will be more aggressive: + // do conflcit check , as long as tx result is generated, + // if lucky, it is the Tx's turn, we will do conflict check with WBNB makeup + // otherwise, do conflict check without WBNB makeup, but we will ignor WBNB's balance conflict. + // throw these likely conflicted tx back to re-execute + startTxIndex := p.mergedTxIndex + 2 // stage 2's will start from the next target merge index + endTxIndex := startTxIndex + stage2CheckNumber + txSize := len(p.allTxReqs) + if endTxIndex > (txSize - 1) { + endTxIndex = txSize - 1 + } + log.Debug("runConfirmStage2Loop", "startTxIndex", startTxIndex, "endTxIndex", endTxIndex) + // conflictNumMark := p.debugConflictRedoNum + for txIndex := startTxIndex; txIndex < endTxIndex; txIndex++ { + p.toConfirmTxIndex(txIndex, true) + // newConflictNum := p.debugConflictRedoNum - conflictNumMark + // to avoid schedule too many redo each time. + // if newConflictNum >= stage2RedoNumber { + // break + //} + } + // make sure all slots are wake up + for i := 0; i < p.parallelNum; i++ { + p.switchSlot(i) } } - // goroutine unsafe operation will be handled from here for safety - gasConsumed := slotGasLimit - gpSlot.Gas() - if gasConsumed != result.UsedGas { - log.Error("gasConsumed != result.UsedGas mismatch", - "gasConsumed", gasConsumed, "result.UsedGas", result.UsedGas) - } - - log.Debug("ok to finalize this TX", - "Slot", slotIndex, "txIndex", txIndex, "result.UsedGas", result.UsedGas, "txReq.usedGas", *txReq.usedGas) - // ok, time to do finalize, stage2 should not be parallel - receipt, err = applyTransactionStageFinalization(evm, result, msg, p.config, slotDB, header, tx, txReq.usedGas, bloomProcessor) +} - if result.Failed() { - // if Tx is reverted, all its state change will be discarded - log.Debug("TX reverted?", "Slot", slotIndex, "txIndex", txIndex, "result.Err", result.Err) - slotDB.RevertSlotDB(msg.From()) +func (p *ParallelStateProcessor) handleTxResults() *ParallelTxResult { + log.Debug("handleTxResults", "p.mergedTxIndex", p.mergedTxIndex) + confirmedResult := p.toConfirmTxIndex(p.mergedTxIndex+1, false) + if confirmedResult == nil { + return nil } - - return &ParallelTxResult{ - updateSlotDB: false, - slotIndex: slotIndex, - txReq: txReq, - receipt: receipt, - slotDB: slotDB, - err: err, + // schedule stage 2 when new Tx has been merged, schedule once and ASAP + // stage 2,if all tx have been executed at least once, and its result has been recevied. + // in Stage 2, we will run check when main DB is advanced, i.e., new Tx result has been merged. + if p.inConfirmStage2 && p.mergedTxIndex >= p.nextStage2TxIndex { + p.nextStage2TxIndex = p.mergedTxIndex + stage2CheckNumber // fixme: more accurate one + p.confirmStage2Chan <- p.mergedTxIndex } + return confirmedResult } -func (p *ParallelStateProcessor) runSlotLoop(slotIndex int) { - curSlot := p.slotState[slotIndex] - for { - // wait for new TxReq - txReq := <-curSlot.pendingTxReqChan - // receive a dispatched message - log.Debug("SlotLoop received a new TxReq", "Slot", slotIndex, "txIndex", txReq.txIndex) - - // SlotDB create rational: - // ** for a dispatched tx, - // the slot should be idle, it is better to create a new SlotDB, since new Tx is not related to previous Tx - // ** for a queued tx, - // it is better to create a new SlotDB, since COW is used. - if txReq.slotDB == nil { - result := &ParallelTxResult{ - updateSlotDB: true, - slotIndex: slotIndex, - err: nil, - } - p.txResultChan <- result - txReq.slotDB = <-curSlot.slotdbChan - } - result := p.execInSlot(slotIndex, txReq) - log.Debug("SlotLoop the TxReq is done", "Slot", slotIndex, "err", result.err) - p.txResultChan <- result +// wait until the next Tx is executed and its result is merged to the main stateDB +func (p *ParallelStateProcessor) confirmTxResults(statedb *state.StateDB, gp *GasPool) *ParallelTxResult { + result := p.handleTxResults() + if result == nil { + return nil } -} + // ok, the tx result is valid and can be merged -// clear slot state for each block. -func (p *ParallelStateProcessor) resetState(txNum int, statedb *state.StateDB) { - if txNum == 0 { - return + if err := gp.SubGas(result.receipt.GasUsed); err != nil { + log.Error("gas limit reached", "block", result.txReq.block.Number(), + "txIndex", result.txReq.txIndex, "GasUsed", result.receipt.GasUsed, "gp.Gas", gp.Gas()) } - p.mergedTxIndex = -1 - p.debugErrorRedoNum = 0 - p.debugConflictRedoNum = 0 + resultTxIndex := result.txReq.txIndex + statedb.MergeSlotDB(result.slotDB, result.receipt, resultTxIndex) - statedb.PrepareForParallel() + if resultTxIndex != p.mergedTxIndex+1 { + log.Error("ProcessParallel tx result out of order", "resultTxIndex", resultTxIndex, + "p.mergedTxIndex", p.mergedTxIndex) + } + p.mergedTxIndex = resultTxIndex + // log.Debug("confirmTxResults result is merged", "result.slotIndex", result.slotIndex, + // "TxIndex", result.txReq.txIndex, "p.mergedTxIndex", p.mergedTxIndex) + return result +} +func (p *ParallelStateProcessor) doCleanUp() { + // 1.clean up all slot: primary and shadow, to make sure they are stopped for _, slot := range p.slotState { - slot.tailTxReq = nil - slot.mergedChangeList = make([]state.SlotChangeList, 0) - slot.pendingTxReqList = make([]*ParallelTxRequest, 0) + slot.primaryStopChan <- struct{}{} + slot.shadowStopChan <- struct{}{} + <-p.stopSlotChan + <-p.stopSlotChan + } + // 2.discard delayed txResults if any + for { + if len(p.txResultChan) > 0 { // drop prefetch addr? + <-p.txResultChan + continue + } + break } + // 3.make sure the confirm routines are stopped + // p.stopConfirmChan <- struct{}{} + // <-p.stopSlotChan + log.Debug("ProcessParallel to stop confirm routine") + p.stopConfirmStage2Chan <- struct{}{} + <-p.stopSlotChan + log.Debug("ProcessParallel stopped confirm routine") } // Implement BEP-130: Parallel Transaction Execution. @@ -848,6 +956,9 @@ func (p *ParallelStateProcessor) Process(block *types.Block, statedb *state.Stat var receipts = make([]*types.Receipt, 0) txNum := len(block.Transactions()) p.resetState(txNum, statedb) + if txNum > 0 { + log.Info("ProcessParallel", "block", header.Number, "txNum", txNum) + } // Iterate over and process the individual transactions posa, isPoSA := p.engine.(consensus.PoSA) @@ -856,7 +967,7 @@ func (p *ParallelStateProcessor) Process(block *types.Block, statedb *state.Stat systemTxs := make([]*types.Transaction, 0, 2) signer, _, bloomProcessor := p.preExecute(block, statedb, cfg, true) - var waitTxChan, curTxChan chan struct{} + // var txReqs []*ParallelTxRequest for i, tx := range block.Transactions() { if isPoSA { if isSystemTx, err := posa.IsSystemTransaction(tx, block.Header()); err != nil { @@ -869,81 +980,103 @@ func (p *ParallelStateProcessor) Process(block *types.Block, statedb *state.Stat } // can be moved it into slot for efficiency, but signer is not concurrent safe - msg, err := tx.AsMessage(signer) + // Parallel Execution 1.0&2.0 is for full sync mode, Nonce PreCheck is not necessary + // And since we will do out-of-order execution, the Nonce PreCheck could fail. + // We will disable it and leave it to Parallel 3.0 which is for validator mode + msg, err := tx.AsMessageNoNonceCheck(signer) if err != nil { bloomProcessor.Close() return statedb, nil, nil, 0, err } // parallel start, wrap an exec message, which will be dispatched to a slot - waitTxChan = curTxChan // can be nil, if this is the tx of first batch, otherwise, it is previous Tx's wait channel - curTxChan = make(chan struct{}, 1) - txReq := &ParallelTxRequest{ - txIndex: i, - tx: tx, - slotDB: nil, - gasLimit: gp.Gas(), - msg: msg, - block: block, - vmConfig: cfg, - bloomProcessor: bloomProcessor, - usedGas: usedGas, - waitTxChan: waitTxChan, - curTxChan: curTxChan, + txIndex: i, + baseStateDB: statedb, + staticSlotIndex: -1, + tx: tx, + gasLimit: block.GasLimit(), // gp.Gas(). + msg: msg, + block: block, + vmConfig: cfg, + bloomProcessor: bloomProcessor, + usedGas: usedGas, + curTxChan: make(chan int, 1), + systemAddrRedo: false, // set to true, when systemAddr access is detected. + runnable: 1, // 0: not runnable, 1: runnable + executedNum: 0, } + p.allTxReqs = append(p.allTxReqs, txReq) + } + // set up stage2 enter criteria + p.targetStage2Count = len(p.allTxReqs) + if p.targetStage2Count > 50 { + // usually, the the last Tx could be the bottleneck it could be very slow, + // so it is better for us to enter stage 2 a bit earlier + p.targetStage2Count = p.targetStage2Count - stage2AheadNum + } - // to optimize the for { for {} } loop code style? it is ok right now. - for { - if p.queueSameFromAddress(txReq) { - break - } + p.doStaticDispatch(statedb, p.allTxReqs) // todo: put txReqs in unit? + // after static dispatch, we notify the slot to work. + for _, slot := range p.slotState { + slot.primaryWakeUpChan <- nil + } + // wait until all Txs have processed. + for { + if len(commonTxs)+len(systemTxs) == txNum { + // put it ahead of chan receive to avoid waiting for empty block + break + } + + unconfirmedResult := <-p.txResultChan + unconfirmedTxIndex := unconfirmedResult.txReq.txIndex + if unconfirmedTxIndex <= p.mergedTxIndex { + log.Warn("drop merged txReq", "unconfirmedTxIndex", unconfirmedTxIndex, "p.mergedTxIndex", p.mergedTxIndex) + continue + } + p.pendingConfirmResults[unconfirmedTxIndex] = append(p.pendingConfirmResults[unconfirmedTxIndex], unconfirmedResult) + + // schedule prefetch once only when unconfirmedResult is valid + if unconfirmedResult.err == nil { + if _, ok := p.txReqExecuteRecord[unconfirmedTxIndex]; !ok { + p.txReqExecuteRecord[unconfirmedTxIndex] = 0 + p.txReqExecuteCount++ + statedb.AddrPrefetch(unconfirmedResult.slotDB) // todo: prefetch when it is not merged + // enter stage2, RT confirm + if !p.inConfirmStage2 && p.txReqExecuteCount == p.targetStage2Count { + p.inConfirmStage2 = true + } - if p.queueSameToAddress(txReq) { - break } - // if idle slot available, just dispatch and process next tx. - if p.dispatchToIdleSlot(statedb, txReq) { + p.txReqExecuteRecord[unconfirmedTxIndex]++ + } + + for { + result := p.confirmTxResults(statedb, gp) + if result == nil { break } - log.Debug("ProcessParallel no slot available, wait", "txIndex", txReq.txIndex) - // no idle slot, wait until a tx is executed and merged. - result := p.waitUntilNextTxDone(statedb, gp) - // update tx result if result.err != nil { - log.Warn("ProcessParallel a failed tx", "resultSlotIndex", result.slotIndex, + log.Error("ProcessParallel a failed tx", "resultSlotIndex", result.slotIndex, "resultTxIndex", result.txReq.txIndex, "result.err", result.err) bloomProcessor.Close() return statedb, nil, nil, 0, fmt.Errorf("could not apply tx %d [%v]: %w", result.txReq.txIndex, result.txReq.tx.Hash().Hex(), result.err) } - commonTxs = append(commonTxs, result.txReq.tx) receipts = append(receipts, result.receipt) } } - - // wait until all tx request are done - for len(commonTxs)+len(systemTxs) < txNum { - result := p.waitUntilNextTxDone(statedb, gp) - // update tx result - if result.err != nil { - log.Warn("ProcessParallel a failed tx", "resultSlotIndex", result.slotIndex, - "resultTxIndex", result.txReq.txIndex, "result.err", result.err) - return statedb, nil, nil, 0, fmt.Errorf("could not apply tx %d [%v]: %w", result.txReq.txIndex, result.txReq.tx.Hash().Hex(), result.err) - } - commonTxs = append(commonTxs, result.txReq.tx) - receipts = append(receipts, result.receipt) - } + // to do clean up when the block is processed + p.doCleanUp() // len(commonTxs) could be 0, such as: https://bscscan.com/block/14580486 if len(commonTxs) > 0 { log.Info("ProcessParallel tx all done", "block", header.Number, "usedGas", *usedGas, "txNum", txNum, "len(commonTxs)", len(commonTxs), - "errorNum", p.debugErrorRedoNum, "conflictNum", p.debugConflictRedoNum, - "redoRate(%)", 100*(p.debugErrorRedoNum+p.debugConflictRedoNum)/len(commonTxs)) + "redoRate(%)", 100*(p.debugConflictRedoNum)/len(commonTxs)) } allLogs, err := p.postExecute(block, statedb, &commonTxs, &receipts, &systemTxs, usedGas, bloomProcessor) return statedb, receipts, allLogs, *usedGas, err @@ -1005,6 +1138,9 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg ) var receipts = make([]*types.Receipt, 0) txNum := len(block.Transactions()) + if txNum > 0 { + log.Info("Process", "block", header.Number, "txNum", txNum) + } commonTxs := make([]*types.Transaction, 0, txNum) // Iterate over and process the individual transactions posa, isPoSA := p.engine.(consensus.PoSA) @@ -1090,7 +1226,7 @@ func applyTransaction(msg types.Message, config *params.ChainConfig, bc ChainCon return receipt, err } -func applyTransactionStageExecution(msg types.Message, gp *GasPool, statedb *state.StateDB, evm *vm.EVM) (*vm.EVM, *ExecutionResult, error) { +func applyTransactionStageExecution(msg types.Message, gp *GasPool, statedb *state.ParallelStateDB, evm *vm.EVM) (*vm.EVM, *ExecutionResult, error) { // Create a new context to be used in the EVM environment. txContext := NewEVMTxContext(msg) evm.Reset(txContext, statedb) @@ -1104,7 +1240,7 @@ func applyTransactionStageExecution(msg types.Message, gp *GasPool, statedb *sta return evm, result, err } -func applyTransactionStageFinalization(evm *vm.EVM, result *ExecutionResult, msg types.Message, config *params.ChainConfig, statedb *state.StateDB, header *types.Header, tx *types.Transaction, usedGas *uint64, receiptProcessors ...ReceiptProcessor) (*types.Receipt, error) { +func applyTransactionStageFinalization(evm *vm.EVM, result *ExecutionResult, msg types.Message, config *params.ChainConfig, statedb *state.ParallelStateDB, header *types.Header, tx *types.Transaction, usedGas *uint64, receiptProcessors ...ReceiptProcessor) (*types.Receipt, error) { // Update the state with pending changes. var root []byte if config.IsByzantium(header.Number) { diff --git a/core/vm/evm.go b/core/vm/evm.go index c7c8e0596c..f21df8885d 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -277,6 +277,7 @@ func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas // The depth-check is already done, and precompiles handled above contract := NewContract(caller, AccountRef(addrCopy), value, gas) contract.SetCallCode(&addrCopy, evm.StateDB.GetCodeHash(addrCopy), code) + evm.StateDB.ParallelMakeUp(addr, input) ret, err = run(evm, contract, input, false) gas = contract.Gas } @@ -475,7 +476,6 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, } nonce := evm.StateDB.GetNonce(caller.Address()) evm.StateDB.SetNonce(caller.Address(), nonce+1) - evm.StateDB.NonceChanged(caller.Address()) // We add this to the access list _before_ taking a snapshot. Even if the creation fails, // the access-list change should not be rolled back if evm.chainRules.IsBerlin { diff --git a/core/vm/instructions.go b/core/vm/instructions.go index 0ecf28d59a..ae5c7079f3 100644 --- a/core/vm/instructions.go +++ b/core/vm/instructions.go @@ -259,7 +259,7 @@ func opAddress(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([] func opBalance(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { slot := scope.Stack.peek() address := common.Address(slot.Bytes20()) - slot.SetFromBig(interpreter.evm.StateDB.GetBalance(address)) + slot.SetFromBig(interpreter.evm.StateDB.GetBalanceOpCode(address)) return nil, nil } diff --git a/core/vm/interface.go b/core/vm/interface.go index c3d99aaa76..be263002b7 100644 --- a/core/vm/interface.go +++ b/core/vm/interface.go @@ -30,8 +30,8 @@ type StateDB interface { SubBalance(common.Address, *big.Int) AddBalance(common.Address, *big.Int) GetBalance(common.Address) *big.Int + GetBalanceOpCode(common.Address) *big.Int - NonceChanged(common.Address) GetNonce(common.Address) uint64 SetNonce(common.Address, uint64) @@ -75,6 +75,7 @@ type StateDB interface { AddPreimage(common.Hash, []byte) ForEachStorage(common.Address, func(common.Hash, common.Hash) bool) error + ParallelMakeUp(addr common.Address, input []byte) } // CallContext provides a basic interface for the EVM calling conventions. The EVM From ad6765a4a59145c1d93639d641f5cb49b1d70f8c Mon Sep 17 00:00:00 2001 From: setunapo Date: Mon, 23 May 2022 14:29:11 +0800 Subject: [PATCH 10/16] code prune rd:1 --- cmd/geth/main.go | 1 - cmd/utils/flags.go | 11 ----- core/blockchain.go | 4 +- core/state_processor.go | 93 +++++++++++++++-------------------------- eth/backend.go | 2 +- eth/ethconfig/config.go | 1 - 6 files changed, 37 insertions(+), 75 deletions(-) diff --git a/cmd/geth/main.go b/cmd/geth/main.go index acea1fd10d..cafb85ac6c 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -165,7 +165,6 @@ var ( utils.EVMInterpreterFlag, utils.ParallelTxFlag, utils.ParallelTxNumFlag, - utils.ParallelTxQueueSizeFlag, utils.MinerNotifyFullFlag, configFileFlag, utils.CatalystFlag, diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 641f8a3140..5307daab57 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -811,11 +811,6 @@ var ( Name: "parallel.num", Usage: "Number of slot for transaction execution, only valid in parallel mode (runtime calculated, no fixed default value)", } - ParallelTxQueueSizeFlag = cli.IntFlag{ - Name: "parallel.queuesize", - Usage: "Max number of Tx that can be queued to a slot, only valid in parallel mode (advanced option)", - Value: 20, - } // Init network InitNetworkSize = cli.IntFlag{ @@ -1675,12 +1670,6 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) { parallelNum = 8 // we found concurrency 8 is slightly better than 15 } cfg.ParallelTxNum = parallelNum - // set up queue size, it is an advanced option - if ctx.GlobalIsSet(ParallelTxQueueSizeFlag.Name) { - cfg.ParallelTxQueueSize = ctx.GlobalInt(ParallelTxQueueSizeFlag.Name) - } else { - cfg.ParallelTxQueueSize = 20 // default queue size, will be optimized - } } // Read the value from the flag no matter if it's set or not. cfg.Preimages = ctx.GlobalBool(CachePreimagesFlag.Name) diff --git a/core/blockchain.go b/core/blockchain.go index ff09aeb4f5..134d8a304a 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -3110,7 +3110,7 @@ func EnablePersistDiff(limit uint64) BlockChainOption { } } -func EnableParallelProcessor(parallelNum int, queueSize int) BlockChainOption { +func EnableParallelProcessor(parallelNum int) BlockChainOption { return func(chain *BlockChain) *BlockChain { if chain.snaps == nil { // disable parallel processor if snapshot is not enabled to avoid concurrent issue for SecureTrie @@ -3118,7 +3118,7 @@ func EnableParallelProcessor(parallelNum int, queueSize int) BlockChainOption { return chain } chain.parallelExecution = true - chain.processor = NewParallelStateProcessor(chain.Config(), chain, chain.engine, parallelNum, queueSize) + chain.processor = NewParallelStateProcessor(chain.Config(), chain, chain.engine, parallelNum) return chain } } diff --git a/core/state_processor.go b/core/state_processor.go index ead132ab17..3174523852 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -48,14 +48,11 @@ const ( recentTime = 1024 * 3 recentDiffLayerTimeout = 5 farDiffLayerTimeout = 2 - maxUnitSize = 10 parallelPrimarySlot = 0 parallelShadowlot = 1 - - stage2CheckNumber = 30 // not fixed, use decrease? - stage2RedoNumber = 10 - stage2AheadNum = 3 // ? + stage2CheckNumber = 30 // ConfirmStage2 will check this number of transaction, to avoid too busy stage2 check + stage2AheadNum = 3 // enter ConfirmStage2 in advance to avoid waiting for Fat Tx ) // StateProcessor is a basic Processor, which takes care of transitioning @@ -79,23 +76,21 @@ func NewStateProcessor(config *params.ChainConfig, bc *BlockChain, engine consen // add for parallel executions type ParallelStateProcessor struct { StateProcessor - parallelNum int // leave a CPU to dispatcher - queueSize int // parallel slot's maximum number of pending Txs - // pendingConfirmChan chan *ParallelTxResult - pendingConfirmResults map[int][]*ParallelTxResult // tx could be executed several times, with several result to check - txResultChan chan *ParallelTxResult // to notify dispatcher that a tx is done - // txReqAccountSorted map[common.Address][]*ParallelTxRequest // fixme: *ParallelTxRequest => ParallelTxRequest? + parallelNum int // leave a CPU to dispatcher slotState []*SlotState // idle, or pending messages - mergedTxIndex int // the latest finalized tx index, fixme: use Atomic - slotDBsToRelease []*state.ParallelStateDB - debugConflictRedoNum int - unconfirmedResults *sync.Map // this is for stage2 confirm, since pendingConfirmResults can not be access in stage2 loop + allTxReqs []*ParallelTxRequest + txResultChan chan *ParallelTxResult // to notify dispatcher that a tx is done + mergedTxIndex int // the latest finalized tx index, fixme: use Atomic + pendingConfirmResults map[int][]*ParallelTxResult // tx could be executed several times, with several result to check + unconfirmedResults *sync.Map // this is for stage2 confirm, since pendingConfirmResults can not be accessed in stage2 loop unconfirmedDBs *sync.Map + slotDBsToRelease []*state.ParallelStateDB stopSlotChan chan int // fixme: use struct{}{}, to make sure all slot are idle stopConfirmChan chan struct{} // fixme: use struct{}{}, to make sure all slot are idle + debugConflictRedoNum int + // start for confirm stage2 confirmStage2Chan chan int stopConfirmStage2Chan chan struct{} - allTxReqs []*ParallelTxRequest txReqExecuteRecord map[int]int // for each the execute count of each Tx txReqExecuteCount int inConfirmStage2 bool @@ -103,11 +98,10 @@ type ParallelStateProcessor struct { nextStage2TxIndex int } -func NewParallelStateProcessor(config *params.ChainConfig, bc *BlockChain, engine consensus.Engine, parallelNum int, queueSize int) *ParallelStateProcessor { +func NewParallelStateProcessor(config *params.ChainConfig, bc *BlockChain, engine consensus.Engine, parallelNum int) *ParallelStateProcessor { processor := &ParallelStateProcessor{ StateProcessor: *NewStateProcessor(config, bc, engine), parallelNum: parallelNum, - queueSize: queueSize, } processor.init() return processor @@ -430,46 +424,37 @@ type SlotState struct { type ParallelTxResult struct { executedIndex int // the TxReq can be executed several time, increase index for each execution updateSlotDB bool // for redo and pending tx quest, slot needs new slotDB, - // keepSystem bool // for redo, should keep system address's balance - slotIndex int // slot index - err error // to describe error message? - txReq *ParallelTxRequest - receipt *types.Receipt - slotDB *state.ParallelStateDB // if updated, it is not equal to txReq.slotDB - gpSlot *GasPool - evm *vm.EVM - result *ExecutionResult + slotIndex int // slot index + txReq *ParallelTxRequest + receipt *types.Receipt + slotDB *state.ParallelStateDB // if updated, it is not equal to txReq.slotDB + gpSlot *GasPool + evm *vm.EVM + result *ExecutionResult + err error // to describe error message? } type ParallelTxRequest struct { - txIndex int - // baseTxIndex int + txIndex int baseStateDB *state.StateDB staticSlotIndex int // static dispatched id tx *types.Transaction - // slotDB *state.ParallelStateDB - gasLimit uint64 - msg types.Message - block *types.Block - vmConfig vm.Config - bloomProcessor *AsyncReceiptBloomGenerator - usedGas *uint64 - curTxChan chan int - systemAddrRedo bool - // runnable 0: runnable - // runnable 1: it has been executed, but results has not been confirmed - // runnable 2: all confirmed, - runnable int32 - executedNum int + gasLimit uint64 + msg types.Message + block *types.Block + vmConfig vm.Config + bloomProcessor *AsyncReceiptBloomGenerator + usedGas *uint64 + curTxChan chan int + systemAddrRedo bool + runnable int32 // 0: not runnable, 1: runnable + executedNum int } // to create and start the execution slot goroutines func (p *ParallelStateProcessor) init() { log.Info("Parallel execution mode is enabled", "Parallel Num", p.parallelNum, - "CPUNum", runtime.NumCPU(), - "QueueSize", p.queueSize) - // In extreme case, parallelNum*2 are requiring updateStateDB, - // confirmLoop is deliverring the valid result or asking for AddrPrefetch. + "CPUNum", runtime.NumCPU()) p.txResultChan = make(chan *ParallelTxResult, 200) p.stopSlotChan = make(chan int, 1) p.stopConfirmChan = make(chan struct{}, 1) @@ -498,10 +483,6 @@ func (p *ParallelStateProcessor) init() { } - // p.pendingConfirmChan = make(chan *ParallelTxResult, 400) - //go func() { - // p.runConfirmLoop() // this loop will be permanent live - //}() p.confirmStage2Chan = make(chan int, 10) go func() { p.runConfirmStage2Loop() // this loop will be permanent live @@ -516,7 +497,6 @@ func (p *ParallelStateProcessor) resetState(txNum int, statedb *state.StateDB) { p.mergedTxIndex = -1 p.debugConflictRedoNum = 0 p.inConfirmStage2 = false - // p.txReqAccountSorted = make(map[common.Address][]*ParallelTxRequest) // fixme: to be reused? statedb.PrepareForParallel() p.allTxReqs = make([]*ParallelTxRequest, 0) @@ -532,8 +512,8 @@ func (p *ParallelStateProcessor) resetState(txNum int, statedb *state.StateDB) { slot.pendingTxReqList = make([]*ParallelTxRequest, 0) slot.activatedType = 0 } - p.unconfirmedResults = new(sync.Map) // make(map[int]*state.ParallelStateDB) - p.unconfirmedDBs = new(sync.Map) // make(map[int]*state.ParallelStateDB) + p.unconfirmedResults = new(sync.Map) + p.unconfirmedDBs = new(sync.Map) p.pendingConfirmResults = make(map[int][]*ParallelTxResult, 200) p.txReqExecuteRecord = make(map[int]int, 200) p.txReqExecuteCount = 0 @@ -866,11 +846,6 @@ func (p *ParallelStateProcessor) runConfirmStage2Loop() { // conflictNumMark := p.debugConflictRedoNum for txIndex := startTxIndex; txIndex < endTxIndex; txIndex++ { p.toConfirmTxIndex(txIndex, true) - // newConflictNum := p.debugConflictRedoNum - conflictNumMark - // to avoid schedule too many redo each time. - // if newConflictNum >= stage2RedoNumber { - // break - //} } // make sure all slots are wake up for i := 0; i < p.parallelNum; i++ { diff --git a/eth/backend.go b/eth/backend.go index 4bac6bf733..fa0f4b580f 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -204,7 +204,7 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { if config.DiffSync && !config.PipeCommit { bcOps = append(bcOps, core.EnableLightProcessor) } else if config.ParallelTxMode { - bcOps = append(bcOps, core.EnableParallelProcessor(config.ParallelTxNum, config.ParallelTxQueueSize)) + bcOps = append(bcOps, core.EnableParallelProcessor(config.ParallelTxNum)) } if config.PipeCommit { bcOps = append(bcOps, core.EnablePipelineCommit) diff --git a/eth/ethconfig/config.go b/eth/ethconfig/config.go index 94998acd6c..14a9904351 100644 --- a/eth/ethconfig/config.go +++ b/eth/ethconfig/config.go @@ -141,7 +141,6 @@ type Config struct { RangeLimit bool ParallelTxMode bool // Whether to execute transaction in parallel mode when do full sync ParallelTxNum int // Number of slot for transaction execution - ParallelTxQueueSize int // Max number of Tx that can be queued to a slot TxLookupLimit uint64 `toml:",omitempty"` // The maximum number of blocks from head whose tx indices are reserved. From 4be068df42aaa17f5eb20e28c25b5935670d701e Mon Sep 17 00:00:00 2001 From: setunapo Date: Mon, 23 May 2022 15:54:40 +0800 Subject: [PATCH 11/16] code prune rd:2 --- core/state/state_object.go | 3 -- core/state/statedb.go | 90 ++++---------------------------------- 2 files changed, 9 insertions(+), 84 deletions(-) diff --git a/core/state/state_object.go b/core/state/state_object.go index fed9ce31b1..309e799030 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -186,7 +186,6 @@ type StateObject struct { func (s *StateObject) empty() bool { // return s.data.Nonce == 0 && s.data.Balance.Sign() == 0 && bytes.Equal(s.data.CodeHash, emptyCodeHash) - // 0426, leave some notation, empty() works so far // empty() has 3 use cases: // 1.StateDB.Empty(), to empty check // A: It is ok, we have handled it in Empty(), to make sure nonce, balance, codeHash are solid @@ -218,7 +217,6 @@ func (s *StateObject) empty() bool { // Slot 0 tx 0: no access to addr_1 // Slot 1 tx 1: sub balance 100, it is empty and deleted // Slot 0 tx 2: GetNonce, lightCopy based on main DB(balance = 100) , not empty - // return s.db.GetNonce(s.address) == 0 && s.db.GetBalance(s.address).Sign() == 0 && bytes.Equal(s.db.GetCodeHash(s.address).Bytes(), emptyCodeHash) if s.dbItf.GetBalance(s.address).Sign() != 0 { // check balance first, since it is most likely not zero return false @@ -708,7 +706,6 @@ func (s *StateObject) MergeSlotObject(db Database, dirtyObjs *StateObject, keys // In parallel mode, always GetState by StateDB, not by StateObject directly, // since it the KV could exist in unconfirmed DB. // But here, it should be ok, since the KV should be changed and valid in the SlotDB, - // s.SetState(db, key, dirtyObjs.GetState(db, key)) s.setState(key, dirtyObjs.GetState(db, key)) } } diff --git a/core/state/statedb.go b/core/state/statedb.go index b9f81c044e..cac2620820 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -168,7 +168,6 @@ type ParallelState struct { addrStateChangesInSlot map[common.Address]bool // true: created, false: deleted addrSnapDestructsReadsInSlot map[common.Address]bool - // addrSnapDestructsChangesInSlot map[common.Address]struct{} // no use to get from unconfirmed DB for efficiency // Transaction will pay gas fee to system address. // Parallel execution will clear system address's balance at first, in order to maintain transaction's @@ -311,7 +310,7 @@ func newStateDB(root common.Hash, db Database, snaps *snapshot.Tree) (*StateDB, return nil, err } sdb.trie = tr - sdb.EnableWriteOnSharedStorage() // fixme:remove when s.originStorage[key] is enabled + sdb.EnableWriteOnSharedStorage() return sdb, nil } @@ -477,20 +476,17 @@ func (s *StateDB) SubRefund(gas uint64) { // Exist reports whether the given account address exists in the state. // Notably this also returns true for suicided accounts. func (s *StateDB) Exist(addr common.Address) bool { - exist := s.getStateObject(addr) != nil - return exist + return s.getStateObject(addr) != nil } // Empty returns whether the state object is either non-existent // or empty according to the EIP161 specification (balance = nonce = code = 0) func (s *StateDB) Empty(addr common.Address) bool { so := s.getStateObject(addr) - empty := (so == nil || so.empty()) - return empty + return so == nil || so.empty() } // GetBalance retrieves the balance from the given address or 0 if object not found -// GetFrom the dirty list => from unconfirmed DB => get from main stateDB func (s *StateDB) GetBalance(addr common.Address) *big.Int { balance := common.Big0 stateObject := s.getStateObject(addr) @@ -694,7 +690,7 @@ func (s *StateDB) SetState(addr common.Address, key, value common.Hash) { // SetStorage replaces the entire storage for the specified account with given // storage. This function should only be used for debugging. func (s *StateDB) SetStorage(addr common.Address, storage map[common.Hash]common.Hash) { - stateObject := s.GetOrNewStateObject(addr) // fixme: parallel mode? + stateObject := s.GetOrNewStateObject(addr) if stateObject != nil { stateObject.SetStorage(storage) } @@ -706,14 +702,9 @@ func (s *StateDB) SetStorage(addr common.Address, storage map[common.Hash]common // The account's state object is still available until the state is committed, // getStateObject will return a non-nil account after Suicide. func (s *StateDB) Suicide(addr common.Address) bool { - var stateObject *StateObject + stateObject := s.getStateObject(addr) if stateObject == nil { - // 3.Try to get from main StateDB - stateObject = s.getStateObject(addr) - if stateObject == nil { - log.Error("Suicide addr not exist", "txIndex", s.txIndex, "addr", addr) - return false - } + return false } s.journal.append(suicideChange{ @@ -854,17 +845,10 @@ func (s *StateDB) getDeletedStateObject(addr common.Address) *StateObject { return obj } -// func (s *StateDB) SetStateObject(object *StateObject) { -// s.storeStateObj(object.Address(), object) -// } - // GetOrNewStateObject retrieves a state object or create a new state object if nil. // dirtyInSlot -> Unconfirmed DB -> main DB -> snapshot, no? create one func (s *StateDB) GetOrNewStateObject(addr common.Address) *StateObject { - var stateObject *StateObject = nil - if stateObject == nil { - stateObject = s.getStateObject(addr) - } + stateObject := s.getStateObject(addr) if stateObject == nil || stateObject.deleted || stateObject.suicided { stateObject = s.createObject(addr) } @@ -1267,9 +1251,7 @@ func (s *StateDB) CopyForSlot() *ParallelStateDB { isParallel: true, parallel: parallel, }, - wbnbMakeUp: true, - // wbnbBalanceAccessed: 0, - // wbnbBalanceAccessedExpected: 0, + wbnbMakeUp: true, balanceUpdateDepth: 0, } // no need to copy preimages, comment out and remove later @@ -2336,11 +2318,7 @@ func (s *StateDB) ParallelMakeUp(addr common.Address, input []byte) { type ParallelStateDB struct { StateDB - wbnbMakeUp bool // default true, we can not do WBNB make up only when supported API call is received. - // wbnbBalanceAccessed int // how many times the WBNB's balance is acccessed, i.e. `GetBalance`, `AddBalance`, `SubBalance`, `SetBalance` - // wbnbBalanceAccessedExpected int // how many times the WBNB contract is called. - // wbnbMakeUpLock sync.RWMutex // we may make up WBNB's balanace of the unconfirmed DB, while other slot read it. - // wbnbContractCalled int // how many times the WBNB contract is called. + wbnbMakeUp bool // default true, we can not do WBNB make up only when supported API call is received. balanceUpdateDepth int wbnbMakeUpBalance *big.Int } @@ -2510,7 +2488,6 @@ func (s *ParallelStateDB) getDeletedStateObject(addr common.Address) *StateObjec // `s` has to be the ParallelStateDB obj := newObject(s, s.isParallel, addr, *data) s.storeStateObj(addr, obj) - // s.SetStateObject(obj) return obj } @@ -2643,9 +2620,7 @@ func (s *ParallelStateDB) GetBalance(addr common.Address) *big.Int { func (s *ParallelStateDB) GetBalanceOpCode(addr common.Address) *big.Int { if addr == WBNBAddress { - // s.wbnbBalanceAccessed++ s.wbnbMakeUp = false - // log.Debug("GetBalanceOpCode for WBNB", "txIndex", s.TxIndex()) } return s.GetBalance(addr) } @@ -2897,9 +2872,6 @@ func (s *ParallelStateDB) AddBalance(addr common.Address, amount *big.Int) { if addr == s.parallel.systemAddress { s.parallel.systemAddressOpsCount++ } - //else if addr == WBNBAddress { - // s.wbnbBalanceAccessed++ - //} // if amount.Sign() != 0 { // todo: to reenable it if _, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; !ok { newStateObject := stateObject.lightCopy(s) // light copy from main DB @@ -2944,9 +2916,6 @@ func (s *ParallelStateDB) SubBalance(addr common.Address, amount *big.Int) { if addr == s.parallel.systemAddress { s.parallel.systemAddressOpsCount++ } - // else if addr == WBNBAddress { - // s.wbnbBalanceAccessed++ - // } // if amount.Sign() != 0 { // todo: to reenable it if _, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; !ok { @@ -2987,9 +2956,6 @@ func (s *ParallelStateDB) SetBalance(addr common.Address, amount *big.Int) { if addr == s.parallel.systemAddress { s.parallel.systemAddressOpsCount++ } - // else if addr == WBNBAddress { - // s.wbnbBalanceAccessed++ - // } if _, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; !ok { newStateObject := stateObject.lightCopy(s) // update balance for revert, in case child contract is revertted, @@ -3739,25 +3705,6 @@ func (s *ParallelStateDB) NeedsRedo() bool { * If it is accessed not through contract all, e.g., by `address.balance`, `address.transfer(amount)`, * we can not do balance make up. */ -/* -fixme: not work... wbnbBalanceAccessedExpected is not correct... -dumped log: -wbnbBalanceAccessed=3 wbnbBalanceAccessedExpected=0 -wbnbBalanceAccessed=9 wbnbBalanceAccessedExpected=4 -wbnbBalanceAccessed=9 wbnbBalanceAccessedExpected=4 -wbnbBalanceAccessed=12 wbnbBalanceAccessedExpected=2 -wbnbBalanceAccessed=10 wbnbBalanceAccessedExpected=2 -wbnbBalanceAccessed=12 wbnbBalanceAccessedExpected=4 -wbnbBalanceAccessed=9 wbnbBalanceAccessedExpected=4 -wbnbBalanceAccessed=13 wbnbBalanceAccessedExpected=2 -wbnbBalanceAccessed=7 wbnbBalanceAccessedExpected=2 -wbnbBalanceAccessed=9 wbnbBalanceAccessedExpected=4 -wbnbBalanceAccessed=9 wbnbBalanceAccessedExpected=4 -wbnbBalanceAccessed=9 wbnbBalanceAccessedExpected=4 -wbnbBalanceAccessed=9 wbnbBalanceAccessedExpected=4 -wbnbBalanceAccessed=9 wbnbBalanceAccessedExpected=4 -wbnbBalanceAccessed=9 wbnbBalanceAccessedExpected=4 -*/ func (s *ParallelStateDB) WBNBMakeUp() bool { return s.wbnbMakeUp } @@ -3772,37 +3719,18 @@ func (s *ParallelStateDB) ParallelMakeUp(addr common.Address, input []byte) { } methodId := input[:4] if bytes.Equal(methodId, WBNBAddress_deposit) { - // log.Debug("ParallelMakeUp for WBNB deposit", "input size", len(input), "input", input) - // s.wbnbBalanceAccessedExpected += 2 // AddBalance() return } if bytes.Equal(methodId, WBNBAddress_withdraw) { - // log.Debug("ParallelMakeUp for WBNB withdraw", "input size", len(input), "input", input) - // ** If from's balance is not enough, it will revert ==> +2, only AddBalance() - // ** if from's balance is enough, ==> +4, AddBalance(), SubBalance() for transfer - // attention, WBNB contract's balance should always sufficient - // s.wbnbBalanceAccessedExpected += 4 - - // as noted above, withdraw's access depends revert or not. - // we have to hack RevertToSnapshot to get the really access count, disable right now. - // s.wbnbMakeUp = false return } if bytes.Equal(methodId, WBNBAddress_approve) { - // log.Debug("ParallelMakeUp for WBNB approve", "input size", len(input), "input", input) - // s.wbnbBalanceAccessedExpected += 2 return } if bytes.Equal(methodId, WBNBAddress_transfer) { - // log.Debug("ParallelMakeUp for WBNB transfer", "input size", len(input), "input", input) - // This is WBNB token transfer, not balance transfer - // s.wbnbBalanceAccessedExpected += 2 return } if bytes.Equal(methodId, WBNBAddress_transferFrom) { - // log.Debug("ParallelMakeUp for WBNB transferFrom", "input size", len(input), "input", input) - // This is WBNB token transfer, not balance transfer - // s.wbnbBalanceAccessedExpected += 2 return } // if bytes.Equal(methodId, WBNBAddress_totalSupply) { From f5df794223dcb491a63fad96916f0a6530b694a5 Mon Sep 17 00:00:00 2001 From: setunapo Date: Mon, 23 May 2022 16:35:27 +0800 Subject: [PATCH 12/16] code prune rd:3 --- core/state/statedb.go | 172 ++++++++++++++++++++-------------------- core/state_processor.go | 2 +- 2 files changed, 85 insertions(+), 89 deletions(-) diff --git a/core/state/statedb.go b/core/state/statedb.go index cac2620820..dc774e6054 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -58,17 +58,7 @@ var ( emptyAddr = crypto.Keccak256Hash(common.Address{}.Bytes()) - // https://bscscan.com/address/0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c WBNBAddress = common.HexToAddress("0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c") - // EVM use big-endian mode, so as the MethodID - WBNBAddress_deposit = []byte{0xd0, 0xe3, 0x0d, 0xb0} // "0xd0e30db0": Keccak-256("deposit()") - WBNBAddress_withdraw = []byte{0x2e, 0x1a, 0x7d, 0x4d} // "0x2e1a7d4d": Keccak-256("withdraw(uint256)") - WBNBAddress_totalSupply = []byte{0x18, 0x16, 0x0d, 0xdd} // "0x18160ddd": Keccak-256("totalSupply()") - WBNBAddress_approve = []byte{0x09, 0x5e, 0xa7, 0xb3} // "0x095ea7b3": Keccak-256("approve(address,uint256)") - WBNBAddress_transfer = []byte{0xa9, 0x05, 0x9c, 0xbb} // "0xa9059cbb": Keccak-256("transfer(address,uint256)") - WBNBAddress_transferFrom = []byte{0x23, 0xb8, 0x72, 0xdd} // "0x23b872dd": Keccak-256("transferFrom(address,address,uint256)") - // unknown WBNB interface 1: {0xDD, 0x62,0xED, 0x3E} in block: 14,248,627 - // unknown WBNB interface 2: {0x70, 0xa0,0x82, 0x31} in block: 14,249,300 ) type proofList [][]byte @@ -846,10 +836,9 @@ func (s *StateDB) getDeletedStateObject(addr common.Address) *StateObject { } // GetOrNewStateObject retrieves a state object or create a new state object if nil. -// dirtyInSlot -> Unconfirmed DB -> main DB -> snapshot, no? create one func (s *StateDB) GetOrNewStateObject(addr common.Address) *StateObject { stateObject := s.getStateObject(addr) - if stateObject == nil || stateObject.deleted || stateObject.suicided { + if stateObject == nil { stateObject = s.createObject(addr) } return stateObject @@ -2156,7 +2145,6 @@ func (s *StateDB) AddrPrefetch(slotDb *ParallelStateDB) { // MergeSlotDB is for Parallel execution mode, when the transaction has been // finalized(dirty -> pending) on execution slot, the execution results should be // merged back to the main StateDB. -// And it will return and keep the slot's change list for later conflict detect. func (s *StateDB) MergeSlotDB(slotDb *ParallelStateDB, slotReceipt *types.Receipt, txIndex int) { // receipt.Logs use unified log index within a block // align slotDB's log index to the block stateDB's logSize @@ -2265,7 +2253,6 @@ func (s *StateDB) MergeSlotDB(slotDb *ParallelStateDB, slotReceipt *types.Receip } if s.prefetcher != nil && len(addressesToPrefetch) > 0 { - // log.Info("MergeSlotDB", "len(addressesToPrefetch)", len(addressesToPrefetch)) s.prefetcher.prefetch(s.originalRoot, addressesToPrefetch, emptyAddr) // prefetch for trie node of account } @@ -2316,13 +2303,67 @@ func (s *StateDB) ParallelMakeUp(addr common.Address, input []byte) { // do nothing, this API is for parallel mode } +type ParallelKvCheckUnit struct { + addr common.Address + key common.Hash + val common.Hash +} +type ParallelKvCheckMessage struct { + slotDB *ParallelStateDB + isStage2 bool + kvUnit ParallelKvCheckUnit +} + +var parallelKvCheckReqCh chan ParallelKvCheckMessage +var parallelKvCheckResCh chan bool + type ParallelStateDB struct { StateDB - wbnbMakeUp bool // default true, we can not do WBNB make up only when supported API call is received. + wbnbMakeUp bool // default true, we can not do WBNB make up if its absolute balance is used. balanceUpdateDepth int wbnbMakeUpBalance *big.Int } +func hasKvConflict(slotDB *ParallelStateDB, addr common.Address, key common.Hash, val common.Hash, isStage2 bool) bool { + mainDB := slotDB.parallel.baseStateDB + + if isStage2 { // update slotDB's unconfirmed DB list and try + if valUnconfirm, ok := slotDB.getKVFromUnconfirmedDB(addr, key); ok { + if !bytes.Equal(val.Bytes(), valUnconfirm.Bytes()) { + log.Debug("IsSlotDBReadsValid KV read is invalid in unconfirmed", "addr", addr, + "valSlot", val, "valUnconfirm", valUnconfirm, + "SlotIndex", slotDB.parallel.SlotIndex, + "txIndex", slotDB.txIndex, "baseTxIndex", slotDB.parallel.baseTxIndex) + return true + } + } + } + valMain := mainDB.GetState(addr, key) + if !bytes.Equal(val.Bytes(), valMain.Bytes()) { + log.Debug("hasKvConflict is invalid", "addr", addr, + "key", key, "valSlot", val, + "valMain", valMain, "SlotIndex", slotDB.parallel.SlotIndex, + "txIndex", slotDB.txIndex, "baseTxIndex", slotDB.parallel.baseTxIndex) + return true // return false, Range will be terminated. + } + return false +} + +// start several routines to do conflict check +func StartKvCheckLoop() { + parallelKvCheckReqCh = make(chan ParallelKvCheckMessage, 200) + parallelKvCheckResCh = make(chan bool, 10) + for i := 0; i < runtime.NumCPU(); i++ { + go func() { + for { + kvEle1 := <-parallelKvCheckReqCh + parallelKvCheckResCh <- hasKvConflict(kvEle1.slotDB, kvEle1.kvUnit.addr, + kvEle1.kvUnit.key, kvEle1.kvUnit.val, kvEle1.isStage2) + } + }() + } +} + // NewSlotDB creates a new State DB based on the provided StateDB. // With parallel, each execution slot would have its own StateDB. func NewSlotDB(db *StateDB, systemAddr common.Address, txIndex int, baseTxIndex int, keepSystem bool, @@ -2359,7 +2400,6 @@ func NewSlotDB(db *StateDB, systemAddr common.Address, txIndex int, baseTxIndex func (s *ParallelStateDB) RevertSlotDB(from common.Address) { s.parallel.kvChangesInSlot = make(map[common.Address]StateKeys) - // balance := s.parallel.balanceChangesInSlot[from] s.parallel.nonceChangesInSlot = make(map[common.Address]struct{}) s.parallel.balanceChangesInSlot = make(map[common.Address]struct{}, 1) s.parallel.addrStateChangesInSlot = make(map[common.Address]bool) // 0: created, 1: deleted @@ -2456,7 +2496,6 @@ func (s *ParallelStateDB) createObject(addr common.Address) (newobj *StateObject s.journal.append(resetObjectChange{prev: prev, prevdestruct: prevdestruct}) } - // s.parallel.dirtiedStateObjectsInSlot[addr] = newobj // would change the bahavior of AddBalance... s.parallel.addrStateChangesInSlot[addr] = true // the object sis created s.parallel.nonceChangesInSlot[addr] = struct{}{} s.parallel.balanceChangesInSlot[addr] = struct{}{} @@ -2495,15 +2534,16 @@ func (s *ParallelStateDB) getDeletedStateObject(addr common.Address) *StateObjec // dirtyInSlot -> Unconfirmed DB -> main DB -> snapshot, no? create one func (s *ParallelStateDB) GetOrNewStateObject(addr common.Address) *StateObject { var stateObject *StateObject = nil - exist := true if stateObject, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; ok { return stateObject } - stateObject, _ = s.getStateObjectFromUnconfirmedDB(addr) + stateObject, _ = s.getStateObjectFromUnconfirmedDB(addr) if stateObject == nil { stateObject = s.getStateObjectNoSlot(addr) // try to get from base db } + + exist := true if stateObject == nil || stateObject.deleted || stateObject.suicided { stateObject = s.createObject(addr) exist = false @@ -3402,60 +3442,6 @@ func (s *ParallelStateDB) getStateObjectFromUnconfirmedDB(addr common.Address) ( return nil, false } -type KvCheckUnit struct { - addr common.Address - key common.Hash - val common.Hash -} -type KvCheckMessage struct { - slotDB *ParallelStateDB - isStage2 bool - kvUnit KvCheckUnit -} - -func hasKvConflict(slotDB *ParallelStateDB, addr common.Address, key common.Hash, val common.Hash, isStage2 bool) bool { - mainDB := slotDB.parallel.baseStateDB - - if isStage2 { // update slotDB's unconfirmed DB list and try - if valUnconfirm, ok := slotDB.getKVFromUnconfirmedDB(addr, key); ok { - if !bytes.Equal(val.Bytes(), valUnconfirm.Bytes()) { - log.Debug("IsSlotDBReadsValid KV read is invalid in unconfirmed", "addr", addr, - "valSlot", val, "valUnconfirm", valUnconfirm, - "SlotIndex", slotDB.parallel.SlotIndex, - "txIndex", slotDB.txIndex, "baseTxIndex", slotDB.parallel.baseTxIndex) - return true - } - } - } - valMain := mainDB.GetState(addr, key) - if !bytes.Equal(val.Bytes(), valMain.Bytes()) { - log.Debug("hasKvConflict is invalid", "addr", addr, - "key", key, "valSlot", val, - "valMain", valMain, "SlotIndex", slotDB.parallel.SlotIndex, - "txIndex", slotDB.txIndex, "baseTxIndex", slotDB.parallel.baseTxIndex) - return true // return false, Range will be terminated. - } - return false -} - -var checkReqCh chan KvCheckMessage -var checkResCh chan bool - -func StartKvCheckLoop() { - // start routines to do conflict check - checkReqCh = make(chan KvCheckMessage, 200) - checkResCh = make(chan bool, 10) - for i := 0; i < runtime.NumCPU(); i++ { - go func() { - for { - kvEle1 := <-checkReqCh - checkResCh <- hasKvConflict(kvEle1.slotDB, kvEle1.kvUnit.addr, - kvEle1.kvUnit.key, kvEle1.kvUnit.val, kvEle1.isStage2) - } - }() - } -} - // in stage2, we do unconfirmed conflict detect func (s *ParallelStateDB) IsParallelReadsValid(isStage2 bool, mergedTxIndex int) bool { once.Do(func() { @@ -3542,10 +3528,10 @@ func (s *ParallelStateDB) IsParallelReadsValid(isStage2 bool, mergedTxIndex int) } } // check KV - var units []KvCheckUnit // todo: pre-allocate to make it faster + var units []ParallelKvCheckUnit // todo: pre-allocate to make it faster for addr, read := range slotDB.parallel.kvReadsInSlot { read.Range(func(keySlot, valSlot interface{}) bool { - units = append(units, KvCheckUnit{addr, keySlot.(common.Hash), valSlot.(common.Hash)}) + units = append(units, ParallelKvCheckUnit{addr, keySlot.(common.Hash), valSlot.(common.Hash)}) return true }) } @@ -3563,7 +3549,7 @@ func (s *ParallelStateDB) IsParallelReadsValid(isStage2 bool, mergedTxIndex int) for { // make sure the unit is consumed consumed := false select { - case conflict := <-checkResCh: // consume result if checkReqCh is blocked + case conflict := <-parallelKvCheckResCh: msgHandledNum++ if conflict { // make sure all request are handled or discarded @@ -3572,15 +3558,15 @@ func (s *ParallelStateDB) IsParallelReadsValid(isStage2 bool, mergedTxIndex int) break } select { - case <-checkReqCh: + case <-parallelKvCheckReqCh: msgHandledNum++ - case <-checkResCh: + case <-parallelKvCheckResCh: msgHandledNum++ } } return false } - case checkReqCh <- KvCheckMessage{slotDB, isStage2, unit}: + case parallelKvCheckReqCh <- ParallelKvCheckMessage{slotDB, isStage2, unit}: msgSendNum++ consumed = true } @@ -3593,7 +3579,7 @@ func (s *ParallelStateDB) IsParallelReadsValid(isStage2 bool, mergedTxIndex int) if msgHandledNum == readLen { break } - conflict := <-checkResCh + conflict := <-parallelKvCheckResCh msgHandledNum++ if conflict { // make sure all request are handled or discarded @@ -3602,9 +3588,9 @@ func (s *ParallelStateDB) IsParallelReadsValid(isStage2 bool, mergedTxIndex int) break } select { - case <-checkReqCh: + case <-parallelKvCheckReqCh: msgHandledNum++ - case <-checkResCh: + case <-parallelKvCheckResCh: msgHandledNum++ } } @@ -3717,23 +3703,33 @@ func (s *ParallelStateDB) ParallelMakeUp(addr common.Address, input []byte) { s.wbnbMakeUp = false return } + // EVM use big-endian mode, so as the MethodID + wbnbDeposit := []byte{0xd0, 0xe3, 0x0d, 0xb0} // "0xd0e30db0": Keccak-256("deposit()") + wbnbWithdraw := []byte{0x2e, 0x1a, 0x7d, 0x4d} // "0x2e1a7d4d": Keccak-256("withdraw(uint256)") + wbnbApprove := []byte{0x09, 0x5e, 0xa7, 0xb3} // "0x095ea7b3": Keccak-256("approve(address,uint256)") + wbnbTransfer := []byte{0xa9, 0x05, 0x9c, 0xbb} // "0xa9059cbb": Keccak-256("transfer(address,uint256)") + wbnbTransferFrom := []byte{0x23, 0xb8, 0x72, 0xdd} // "0x23b872dd": Keccak-256("transferFrom(address,address,uint256)") + // wbnbTotalSupply := []byte{0x18, 0x16, 0x0d, 0xdd} // "0x18160ddd": Keccak-256("totalSupply()") + // unknown WBNB interface 1: {0xDD, 0x62,0xED, 0x3E} in block: 14,248,627 + // unknown WBNB interface 2: {0x70, 0xa0,0x82, 0x31} in block: 14,249,300 + methodId := input[:4] - if bytes.Equal(methodId, WBNBAddress_deposit) { + if bytes.Equal(methodId, wbnbDeposit) { return } - if bytes.Equal(methodId, WBNBAddress_withdraw) { + if bytes.Equal(methodId, wbnbWithdraw) { return } - if bytes.Equal(methodId, WBNBAddress_approve) { + if bytes.Equal(methodId, wbnbApprove) { return } - if bytes.Equal(methodId, WBNBAddress_transfer) { + if bytes.Equal(methodId, wbnbTransfer) { return } - if bytes.Equal(methodId, WBNBAddress_transferFrom) { + if bytes.Equal(methodId, wbnbTransferFrom) { return } - // if bytes.Equal(methodId, WBNBAddress_totalSupply) { + // if bytes.Equal(methodId, wbnbTotalSupply) { // log.Debug("ParallelMakeUp for WBNB, not for totalSupply", "input size", len(input), "input", input) // s.wbnbMakeUp = false // can not makeup // return diff --git a/core/state_processor.go b/core/state_processor.go index 3174523852..989c953cb4 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -1006,7 +1006,7 @@ func (p *ParallelStateProcessor) Process(block *types.Block, statedb *state.Stat unconfirmedResult := <-p.txResultChan unconfirmedTxIndex := unconfirmedResult.txReq.txIndex if unconfirmedTxIndex <= p.mergedTxIndex { - log.Warn("drop merged txReq", "unconfirmedTxIndex", unconfirmedTxIndex, "p.mergedTxIndex", p.mergedTxIndex) + // log.Warn("drop merged txReq", "unconfirmedTxIndex", unconfirmedTxIndex, "p.mergedTxIndex", p.mergedTxIndex) continue } p.pendingConfirmResults[unconfirmedTxIndex] = append(p.pendingConfirmResults[unconfirmedTxIndex], unconfirmedResult) From 04f6fe843497a1e2c5b625ebe124aa3146a0de16 Mon Sep 17 00:00:00 2001 From: setunapo Date: Mon, 23 May 2022 17:40:06 +0800 Subject: [PATCH 13/16] code prune rd:4 --- core/state/state_object.go | 3 +-- core/state/statedb.go | 9 ++++--- core/state_processor.go | 48 ++++++++++++++------------------------ 3 files changed, 22 insertions(+), 38 deletions(-) diff --git a/core/state/state_object.go b/core/state/state_object.go index 309e799030..c8db8b25c1 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -660,8 +660,7 @@ func (s *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), // prevBalance, - // prev: prevBalance, + prev: new(big.Int).Set(s.data.Balance), }) s.setBalance(amount) } diff --git a/core/state/statedb.go b/core/state/statedb.go index dc774e6054..1fa9680838 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -48,17 +48,15 @@ type revision struct { } var ( - once sync.Once // emptyRoot is the known root hash of an empty trie. emptyRoot = common.HexToHash("56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421") - // dummyRoot is the dummy account root before corrected in pipecommit sync mode, // the value is 542e5fc2709de84248e9bce43a9c0c8943a608029001360f8ab55bf113b23d28 dummyRoot = crypto.Keccak256Hash([]byte("dummy_account_root")) - emptyAddr = crypto.Keccak256Hash(common.Address{}.Bytes()) - WBNBAddress = common.HexToAddress("0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c") + WBNBAddress = common.HexToAddress("0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c") + parallelKvOnce sync.Once ) type proofList [][]byte @@ -2658,6 +2656,7 @@ func (s *ParallelStateDB) GetBalance(addr common.Address) *big.Int { return balance } +// different from GetBalance(), it is opcode triggered func (s *ParallelStateDB) GetBalanceOpCode(addr common.Address) *big.Int { if addr == WBNBAddress { s.wbnbMakeUp = false @@ -3444,7 +3443,7 @@ func (s *ParallelStateDB) getStateObjectFromUnconfirmedDB(addr common.Address) ( // in stage2, we do unconfirmed conflict detect func (s *ParallelStateDB) IsParallelReadsValid(isStage2 bool, mergedTxIndex int) bool { - once.Do(func() { + parallelKvOnce.Do(func() { StartKvCheckLoop() }) slotDB := s diff --git a/core/state_processor.go b/core/state_processor.go index 989c953cb4..4532f9f31d 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -85,8 +85,8 @@ type ParallelStateProcessor struct { unconfirmedResults *sync.Map // this is for stage2 confirm, since pendingConfirmResults can not be accessed in stage2 loop unconfirmedDBs *sync.Map slotDBsToRelease []*state.ParallelStateDB - stopSlotChan chan int // fixme: use struct{}{}, to make sure all slot are idle - stopConfirmChan chan struct{} // fixme: use struct{}{}, to make sure all slot are idle + stopSlotChan chan struct{} + stopConfirmChan chan struct{} debugConflictRedoNum int // start for confirm stage2 confirmStage2Chan chan int @@ -412,26 +412,23 @@ func (p *LightStateProcessor) LightProcess(diffLayer *types.DiffLayer, block *ty type SlotState struct { pendingTxReqList []*ParallelTxRequest - primaryWakeUpChan chan *ParallelTxRequest - shadowWakeUpChan chan *ParallelTxRequest + primaryWakeUpChan chan struct{} + shadowWakeUpChan chan struct{} primaryStopChan chan struct{} shadowStopChan chan struct{} - - slotDBChan chan *state.ParallelStateDB // to update SlotDB - activatedType int32 // 0: primary slot, 1: shadow slot + activatedType int32 // 0: primary slot, 1: shadow slot } type ParallelTxResult struct { - executedIndex int // the TxReq can be executed several time, increase index for each execution - updateSlotDB bool // for redo and pending tx quest, slot needs new slotDB, - slotIndex int // slot index + executedIndex int // the TxReq can be executed several time, increase index for each execution + slotIndex int // slot index txReq *ParallelTxRequest receipt *types.Receipt slotDB *state.ParallelStateDB // if updated, it is not equal to txReq.slotDB gpSlot *GasPool evm *vm.EVM result *ExecutionResult - err error // to describe error message? + err error } type ParallelTxRequest struct { @@ -456,16 +453,15 @@ func (p *ParallelStateProcessor) init() { log.Info("Parallel execution mode is enabled", "Parallel Num", p.parallelNum, "CPUNum", runtime.NumCPU()) p.txResultChan = make(chan *ParallelTxResult, 200) - p.stopSlotChan = make(chan int, 1) + p.stopSlotChan = make(chan struct{}, 1) p.stopConfirmChan = make(chan struct{}, 1) p.stopConfirmStage2Chan = make(chan struct{}, 1) p.slotState = make([]*SlotState, p.parallelNum) for i := 0; i < p.parallelNum; i++ { p.slotState[i] = &SlotState{ - slotDBChan: make(chan *state.ParallelStateDB, 1), - primaryWakeUpChan: make(chan *ParallelTxRequest, 1), - shadowWakeUpChan: make(chan *ParallelTxRequest, 1), + primaryWakeUpChan: make(chan struct{}, 1), + shadowWakeUpChan: make(chan struct{}, 1), primaryStopChan: make(chan struct{}, 1), shadowStopChan: make(chan struct{}, 1), } @@ -592,12 +588,12 @@ func (p *ParallelStateProcessor) switchSlot(slotIndex int) { if atomic.CompareAndSwapInt32(&slot.activatedType, 0, 1) { // switch from normal to shadow slot if len(slot.shadowWakeUpChan) == 0 { - slot.shadowWakeUpChan <- nil // only notify when target once + slot.shadowWakeUpChan <- struct{}{} // only notify when target once } } else if atomic.CompareAndSwapInt32(&slot.activatedType, 1, 0) { // switch from shadow to normal slot if len(slot.primaryWakeUpChan) == 0 { - slot.primaryWakeUpChan <- nil // only notify when target once + slot.primaryWakeUpChan <- struct{}{} // only notify when target once } } } @@ -617,7 +613,6 @@ func (p *ParallelStateProcessor) executeInSlot(slotIndex int, txReq *ParallelTxR evm, result, err := applyTransactionStageExecution(txReq.msg, gpSlot, slotDB, vmenv) txResult := ParallelTxResult{ executedIndex: txReq.executedNum, - updateSlotDB: false, slotIndex: slotIndex, txReq: txReq, receipt: nil, // receipt is generated in finalize stage @@ -740,13 +735,12 @@ func (p *ParallelStateProcessor) toConfirmTxIndexResult(txResult *ParallelTxResu txResult.receipt, txResult.err = applyTransactionStageFinalization(txResult.evm, txResult.result, txReq.msg, p.config, txResult.slotDB, header, txReq.tx, txReq.usedGas, txReq.bloomProcessor) - txResult.updateSlotDB = false return true } func (p *ParallelStateProcessor) runSlotLoop(slotIndex int, slotType int32) { curSlot := p.slotState[slotIndex] - var wakeupChan chan *ParallelTxRequest + var wakeupChan chan struct{} var stopChan chan struct{} if slotType == parallelPrimarySlot { @@ -759,7 +753,7 @@ func (p *ParallelStateProcessor) runSlotLoop(slotIndex int, slotType int32) { for { select { case <-stopChan: - p.stopSlotChan <- slotIndex + p.stopSlotChan <- struct{}{} continue case <-wakeupChan: } @@ -821,7 +815,7 @@ func (p *ParallelStateProcessor) runConfirmStage2Loop() { for len(p.confirmStage2Chan) > 0 { <-p.confirmStage2Chan } - p.stopSlotChan <- -1 + p.stopSlotChan <- struct{}{} continue case <-p.confirmStage2Chan: for len(p.confirmStage2Chan) > 0 { @@ -913,12 +907,8 @@ func (p *ParallelStateProcessor) doCleanUp() { break } // 3.make sure the confirm routines are stopped - // p.stopConfirmChan <- struct{}{} - // <-p.stopSlotChan - log.Debug("ProcessParallel to stop confirm routine") p.stopConfirmStage2Chan <- struct{}{} <-p.stopSlotChan - log.Debug("ProcessParallel stopped confirm routine") } // Implement BEP-130: Parallel Transaction Execution. @@ -931,10 +921,6 @@ func (p *ParallelStateProcessor) Process(block *types.Block, statedb *state.Stat var receipts = make([]*types.Receipt, 0) txNum := len(block.Transactions()) p.resetState(txNum, statedb) - if txNum > 0 { - log.Info("ProcessParallel", "block", header.Number, "txNum", txNum) - } - // Iterate over and process the individual transactions posa, isPoSA := p.engine.(consensus.PoSA) commonTxs := make([]*types.Transaction, 0, txNum) @@ -994,7 +980,7 @@ func (p *ParallelStateProcessor) Process(block *types.Block, statedb *state.Stat p.doStaticDispatch(statedb, p.allTxReqs) // todo: put txReqs in unit? // after static dispatch, we notify the slot to work. for _, slot := range p.slotState { - slot.primaryWakeUpChan <- nil + slot.primaryWakeUpChan <- struct{}{} } // wait until all Txs have processed. for { From 9628a04cd9803dcc94d1c2ccdac812a69830bb02 Mon Sep 17 00:00:00 2001 From: setunapo Date: Mon, 23 May 2022 22:05:21 +0800 Subject: [PATCH 14/16] code prune rd:5 --- core/state/statedb.go | 33 ++++++----------------------- core/state_processor.go | 46 ++++++++++++++--------------------------- 2 files changed, 22 insertions(+), 57 deletions(-) diff --git a/core/state/statedb.go b/core/state/statedb.go index 1fa9680838..75c52ce15f 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -122,7 +122,7 @@ func (s *StateDB) deleteStateObj(addr common.Address) { // For parallel mode only type ParallelState struct { isSlotDB bool // denotes StateDB is used in slot, we will try to remove it - SlotIndex int // fixme: to be removed + SlotIndex int // for debug, to be removed // stateObjects holds the state objects in the base slot db // the reason for using stateObjects instead of stateObjects on the outside is // we need a thread safe map to hold state objects since there are many slots will read @@ -824,10 +824,6 @@ func (s *StateDB) getDeletedStateObject(addr common.Address) *StateObject { return nil } // Insert into the live set - // if obj, ok := s.loadStateObj(addr); ok { - // fixme: concurrent not safe, merge could update it... - // return obj - //} obj := newObject(s, s.isParallel, addr, *data) s.storeStateObj(addr, obj) return obj @@ -2476,7 +2472,7 @@ func (s *ParallelStateDB) createObject(addr common.Address) (newobj *StateObject if s.snap != nil && prev != nil { s.snapParallelLock.Lock() - _, prevdestruct = s.snapDestructs[prev.address] // fixme, record the snapshot read for create Account + _, prevdestruct = s.snapDestructs[prev.address] s.parallel.addrSnapDestructsReadsInSlot[addr] = prevdestruct if !prevdestruct { // To destroy the previous trie node first and update the trie tree @@ -2516,11 +2512,6 @@ func (s *ParallelStateDB) getDeletedStateObject(addr common.Address) *StateObjec if !ok { return nil } - // Insert into the live set - // if obj, ok := s.loadStateObj(addr); ok { - // fixme: concurrent not safe, merge could update it... - // return obj - // } // this is why we have to use a seperate getDeletedStateObject for ParallelStateDB // `s` has to be the ParallelStateDB obj := newObject(s, s.isParallel, addr, *data) @@ -2896,12 +2887,7 @@ func (s *ParallelStateDB) HasSuicided(addr common.Address) bool { // AddBalance adds amount to the account associated with addr. func (s *ParallelStateDB) AddBalance(addr common.Address, amount *big.Int) { // add balance will perform a read operation first - // s.parallel.balanceReadsInSlot[addr] = struct{}{} // fixme: to make the the balance valid, since unconfirmed would refer it. - // if amount.Sign() == 0 { // if amount == 0, no balance change, but there is still an empty check. - // take this empty check as addr state read(create, suicide, empty delete) - // s.parallel.addrStateReadsInSlot[addr] = struct{}{} - // } s.balanceUpdateDepth++ defer func() { s.balanceUpdateDepth-- @@ -2911,13 +2897,11 @@ func (s *ParallelStateDB) AddBalance(addr common.Address, amount *big.Int) { if addr == s.parallel.systemAddress { s.parallel.systemAddressOpsCount++ } - // if amount.Sign() != 0 { // todo: to reenable it if _, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; !ok { newStateObject := stateObject.lightCopy(s) // light copy from main DB // do balance fixup from the confirmed DB, it could be more reliable than main DB - balance := s.GetBalance(addr) + balance := s.GetBalance(addr) // it will record the balance read operation newStateObject.setBalance(balance) - // s.parallel.balanceReadsInSlot[addr] = newStateObject.Balance() // could read from main DB or unconfirmed DB newStateObject.AddBalance(amount) s.parallel.dirtiedStateObjectsInSlot[addr] = newStateObject s.parallel.balanceChangesInSlot[addr] = struct{}{} @@ -2941,10 +2925,7 @@ func (s *ParallelStateDB) AddBalance(addr common.Address, amount *big.Int) { // SubBalance subtracts amount from the account associated with addr. func (s *ParallelStateDB) SubBalance(addr common.Address, amount *big.Int) { - // if amount.Sign() != 0 { // unlike add, sub 0 balance will not touch empty object - // s.parallel.balanceReadsInSlot[addr] = struct{}{} - // } s.balanceUpdateDepth++ defer func() { s.balanceUpdateDepth-- @@ -2956,13 +2937,11 @@ func (s *ParallelStateDB) SubBalance(addr common.Address, amount *big.Int) { s.parallel.systemAddressOpsCount++ } - // if amount.Sign() != 0 { // todo: to reenable it if _, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; !ok { newStateObject := stateObject.lightCopy(s) // light copy from main DB // do balance fixup from the confirmed DB, it could be more reliable than main DB balance := s.GetBalance(addr) newStateObject.setBalance(balance) - // s.parallel.balanceReadsInSlot[addr] = newStateObject.Balance() newStateObject.SubBalance(amount) s.parallel.balanceChangesInSlot[addr] = struct{}{} s.parallel.dirtiedStateObjectsInSlot[addr] = newStateObject @@ -3189,14 +3168,14 @@ func (s *ParallelStateDB) RevertToSnapshot(revid int) { // AddRefund adds gas to the refund counter // journal.append will use ParallelState for revert -func (s *ParallelStateDB) AddRefund(gas uint64) { // fixme: not needed +func (s *ParallelStateDB) AddRefund(gas uint64) { // todo: not needed, can be deleted s.journal.append(refundChange{prev: 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 *ParallelStateDB) SubRefund(gas uint64) { // fixme: not needed +func (s *ParallelStateDB) SubRefund(gas uint64) { s.journal.append(refundChange{prev: s.refund}) if gas > s.refund { // we don't need to panic here if we read the wrong state in parallelm mode @@ -3442,7 +3421,7 @@ func (s *ParallelStateDB) getStateObjectFromUnconfirmedDB(addr common.Address) ( } // in stage2, we do unconfirmed conflict detect -func (s *ParallelStateDB) IsParallelReadsValid(isStage2 bool, mergedTxIndex int) bool { +func (s *ParallelStateDB) IsParallelReadsValid(isStage2 bool) bool { parallelKvOnce.Do(func() { StartKvCheckLoop() }) diff --git a/core/state_processor.go b/core/state_processor.go index 4532f9f31d..b69df9063f 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -420,8 +420,8 @@ type SlotState struct { } type ParallelTxResult struct { - executedIndex int // the TxReq can be executed several time, increase index for each execution - slotIndex int // slot index + executedIndex int32 // the TxReq can be executed several time, increase index for each execution + slotIndex int // slot index txReq *ParallelTxRequest receipt *types.Receipt slotDB *state.ParallelStateDB // if updated, it is not equal to txReq.slotDB @@ -445,7 +445,7 @@ type ParallelTxRequest struct { curTxChan chan int systemAddrRedo bool runnable int32 // 0: not runnable, 1: runnable - executedNum int + executedNum int32 } // to create and start the execution slot goroutines @@ -506,7 +506,7 @@ func (p *ParallelStateProcessor) resetState(txNum int, statedb *state.StateDB) { }() for _, slot := range p.slotState { slot.pendingTxReqList = make([]*ParallelTxRequest, 0) - slot.activatedType = 0 + slot.activatedType = parallelPrimarySlot } p.unconfirmedResults = new(sync.Map) p.unconfirmedDBs = new(sync.Map) @@ -531,7 +531,6 @@ func (p *ParallelStateProcessor) doStaticDispatch(mainStatedb *state.StateDB, tx slotIndex = i } else if txReq.msg.To() != nil { // To Address, with txIndex sorted, could be in different slot. - // fixme: Create will move to hungry slot if i, ok := toSlotMap[*txReq.msg.To()]; ok { slotIndex = i } @@ -576,7 +575,7 @@ func (p *ParallelStateProcessor) hasConflict(txResult *ParallelTxResult, isStage return true } else { // to check if what the slot db read is correct. - if !slotDB.IsParallelReadsValid(isStage2, p.mergedTxIndex) { + if !slotDB.IsParallelReadsValid(isStage2) { return true } } @@ -585,12 +584,12 @@ func (p *ParallelStateProcessor) hasConflict(txResult *ParallelTxResult, isStage func (p *ParallelStateProcessor) switchSlot(slotIndex int) { slot := p.slotState[slotIndex] - if atomic.CompareAndSwapInt32(&slot.activatedType, 0, 1) { + if atomic.CompareAndSwapInt32(&slot.activatedType, parallelPrimarySlot, parallelShadowlot) { // switch from normal to shadow slot if len(slot.shadowWakeUpChan) == 0 { slot.shadowWakeUpChan <- struct{}{} // only notify when target once } - } else if atomic.CompareAndSwapInt32(&slot.activatedType, 1, 0) { + } else if atomic.CompareAndSwapInt32(&slot.activatedType, parallelShadowlot, parallelPrimarySlot) { // switch from shadow to normal slot if len(slot.primaryWakeUpChan) == 0 { slot.primaryWakeUpChan <- struct{}{} // only notify when target once @@ -599,7 +598,7 @@ func (p *ParallelStateProcessor) switchSlot(slotIndex int) { } func (p *ParallelStateProcessor) executeInSlot(slotIndex int, txReq *ParallelTxRequest) *ParallelTxResult { - txReq.executedNum++ // fixme: atomic? + atomic.AddInt32(&txReq.executedNum, 1) slotDB := state.NewSlotDB(txReq.baseStateDB, consensus.SystemAddress, txReq.txIndex, p.mergedTxIndex, txReq.systemAddrRedo, p.unconfirmedDBs) @@ -612,7 +611,7 @@ func (p *ParallelStateProcessor) executeInSlot(slotIndex int, txReq *ParallelTxR evm, result, err := applyTransactionStageExecution(txReq.msg, gpSlot, slotDB, vmenv) txResult := ParallelTxResult{ - executedIndex: txReq.executedNum, + executedIndex: atomic.LoadInt32(&txReq.executedNum), slotIndex: slotIndex, txReq: txReq, receipt: nil, // receipt is generated in finalize stage @@ -648,16 +647,12 @@ func (p *ParallelStateProcessor) toConfirmTxIndex(targetTxIndex int, isStage2 bo if targetTxIndex <= p.mergedTxIndex+1 { // this is the one that can been merged, // others are for likely conflict check, since it is not their tuen. - // log.Warn("to confirm in stage 2, invalid txIndex", - // "targetTxIndex", targetTxIndex, "p.mergedTxIndex", p.mergedTxIndex) return nil } } for { // handle a targetTxIndex in a loop - // targetTxIndex = p.mergedTxIndex + 1 - // select a unconfirmedResult to check var targetResult *ParallelTxResult if isStage2 { result, ok := p.unconfirmedResults.Load(targetTxIndex) @@ -671,7 +666,7 @@ func (p *ParallelStateProcessor) toConfirmTxIndex(targetTxIndex int, isStage2 bo if atomic.CompareAndSwapInt32(&targetResult.txReq.runnable, 1, 1) { return nil } - if targetResult.executedIndex < targetResult.txReq.executedNum { + if targetResult.executedIndex < atomic.LoadInt32(&targetResult.txReq.executedNum) { return nil } } else { @@ -705,7 +700,6 @@ func (p *ParallelStateProcessor) toConfirmTxIndex(targetTxIndex int, isStage2 bo } if isStage2 { // likely valid, but not sure, can not deliver - // fixme: need to handle txResult repeatedly check? return nil } return targetResult @@ -763,7 +757,8 @@ func (p *ParallelStateProcessor) runSlotLoop(slotIndex int, slotType int32) { if txReq.txIndex <= p.mergedTxIndex { continue } - if curSlot.activatedType != slotType { // fixme: atomic compare? + + if atomic.LoadInt32(&curSlot.activatedType) != slotType { interrupted = true break } @@ -771,11 +766,7 @@ func (p *ParallelStateProcessor) runSlotLoop(slotIndex int, slotType int32) { // not swapped: txReq.runnable == 0 continue } - result := p.executeInSlot(slotIndex, txReq) - if result == nil { // fixme: code improve, nil means block processed, to be stopped - break - } - p.txResultChan <- result + p.txResultChan <- p.executeInSlot(slotIndex, txReq) } // switched to the other slot. if interrupted { @@ -789,7 +780,7 @@ func (p *ParallelStateProcessor) runSlotLoop(slotIndex int, slotType int32) { if stealTxReq.txIndex <= p.mergedTxIndex { continue } - if curSlot.activatedType != slotType { + if atomic.LoadInt32(&curSlot.activatedType) != slotType { interrupted = true break } @@ -798,11 +789,7 @@ func (p *ParallelStateProcessor) runSlotLoop(slotIndex int, slotType int32) { // not swapped: txReq.runnable == 0 continue } - result := p.executeInSlot(slotIndex, stealTxReq) - if result == nil { // fixme: code improve, nil means block processed, to be stopped - break - } - p.txResultChan <- result + p.txResultChan <- p.executeInSlot(slotIndex, stealTxReq) } } } @@ -850,7 +837,6 @@ func (p *ParallelStateProcessor) runConfirmStage2Loop() { } func (p *ParallelStateProcessor) handleTxResults() *ParallelTxResult { - log.Debug("handleTxResults", "p.mergedTxIndex", p.mergedTxIndex) confirmedResult := p.toConfirmTxIndex(p.mergedTxIndex+1, false) if confirmedResult == nil { return nil @@ -859,7 +845,7 @@ func (p *ParallelStateProcessor) handleTxResults() *ParallelTxResult { // stage 2,if all tx have been executed at least once, and its result has been recevied. // in Stage 2, we will run check when main DB is advanced, i.e., new Tx result has been merged. if p.inConfirmStage2 && p.mergedTxIndex >= p.nextStage2TxIndex { - p.nextStage2TxIndex = p.mergedTxIndex + stage2CheckNumber // fixme: more accurate one + p.nextStage2TxIndex = p.mergedTxIndex + stage2CheckNumber p.confirmStage2Chan <- p.mergedTxIndex } return confirmedResult From 21723fd882d11c33045d169a11db3e85768b3269 Mon Sep 17 00:00:00 2001 From: setunapo Date: Wed, 25 May 2022 15:27:05 +0800 Subject: [PATCH 15/16] code prune rd:6, for review comments --- core/state/statedb.go | 22 +++------------------- core/state_processor.go | 17 +++++++++-------- 2 files changed, 12 insertions(+), 27 deletions(-) diff --git a/core/state/statedb.go b/core/state/statedb.go index 75c52ce15f..49dc2e2419 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -1234,8 +1234,7 @@ func (s *StateDB) CopyForSlot() *ParallelStateDB { isParallel: true, parallel: parallel, }, - wbnbMakeUp: true, - balanceUpdateDepth: 0, + wbnbMakeUp: true, } // no need to copy preimages, comment out and remove later // for hash, preimage := range s.preimages { @@ -2313,9 +2312,8 @@ var parallelKvCheckResCh chan bool type ParallelStateDB struct { StateDB - wbnbMakeUp bool // default true, we can not do WBNB make up if its absolute balance is used. - balanceUpdateDepth int - wbnbMakeUpBalance *big.Int + wbnbMakeUp bool // default true, we can not do WBNB make up if its absolute balance is used. + wbnbMakeUpBalance *big.Int } func hasKvConflict(slotDB *ParallelStateDB, addr common.Address, key common.Hash, val common.Hash, isStage2 bool) bool { @@ -2888,10 +2886,6 @@ func (s *ParallelStateDB) HasSuicided(addr common.Address) bool { func (s *ParallelStateDB) AddBalance(addr common.Address, amount *big.Int) { // add balance will perform a read operation first // if amount == 0, no balance change, but there is still an empty check. - s.balanceUpdateDepth++ - defer func() { - s.balanceUpdateDepth-- - }() stateObject := s.GetOrNewStateObject(addr) if stateObject != nil { if addr == s.parallel.systemAddress { @@ -2926,11 +2920,6 @@ func (s *ParallelStateDB) AddBalance(addr common.Address, amount *big.Int) { // SubBalance subtracts amount from the account associated with addr. func (s *ParallelStateDB) SubBalance(addr common.Address, amount *big.Int) { // unlike add, sub 0 balance will not touch empty object - s.balanceUpdateDepth++ - defer func() { - s.balanceUpdateDepth-- - }() - stateObject := s.GetOrNewStateObject(addr) if stateObject != nil { if addr == s.parallel.systemAddress { @@ -2964,11 +2953,6 @@ func (s *ParallelStateDB) SubBalance(addr common.Address, amount *big.Int) { } func (s *ParallelStateDB) SetBalance(addr common.Address, amount *big.Int) { - s.balanceUpdateDepth++ - defer func() { - s.balanceUpdateDepth-- - }() - stateObject := s.GetOrNewStateObject(addr) if stateObject != nil { if addr == s.parallel.systemAddress { diff --git a/core/state_processor.go b/core/state_processor.go index b69df9063f..9df75b9c7a 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -50,7 +50,7 @@ const ( farDiffLayerTimeout = 2 parallelPrimarySlot = 0 - parallelShadowlot = 1 + parallelShadowSlot = 1 stage2CheckNumber = 30 // ConfirmStage2 will check this number of transaction, to avoid too busy stage2 check stage2AheadNum = 3 // enter ConfirmStage2 in advance to avoid waiting for Fat Tx ) @@ -474,7 +474,7 @@ func (p *ParallelStateProcessor) init() { // It is back up of the primary slot to make sure transaction can be redo ASAP, // since the primary slot could be busy at executing another transaction go func(slotIndex int) { - p.runSlotLoop(slotIndex, 1) // this loop will be permanent live + p.runSlotLoop(slotIndex, parallelShadowSlot) // this loop will be permanent live }(i) } @@ -521,7 +521,7 @@ func (p *ParallelStateProcessor) resetState(txNum int, statedb *state.StateDB) { // ** reduce IPC cost by dispatch in Unit // ** make sure same From in same slot // ** try to make it balanced, queue to the most hungry slot for new Address -func (p *ParallelStateProcessor) doStaticDispatch(mainStatedb *state.StateDB, txReqs []*ParallelTxRequest) { +func (p *ParallelStateProcessor) doStaticDispatch(txReqs []*ParallelTxRequest) { fromSlotMap := make(map[common.Address]int, 100) toSlotMap := make(map[common.Address]int, 100) for _, txReq := range txReqs { @@ -584,12 +584,12 @@ func (p *ParallelStateProcessor) hasConflict(txResult *ParallelTxResult, isStage func (p *ParallelStateProcessor) switchSlot(slotIndex int) { slot := p.slotState[slotIndex] - if atomic.CompareAndSwapInt32(&slot.activatedType, parallelPrimarySlot, parallelShadowlot) { + if atomic.CompareAndSwapInt32(&slot.activatedType, parallelPrimarySlot, parallelShadowSlot) { // switch from normal to shadow slot if len(slot.shadowWakeUpChan) == 0 { slot.shadowWakeUpChan <- struct{}{} // only notify when target once } - } else if atomic.CompareAndSwapInt32(&slot.activatedType, parallelShadowlot, parallelPrimarySlot) { + } else if atomic.CompareAndSwapInt32(&slot.activatedType, parallelShadowSlot, parallelPrimarySlot) { // switch from shadow to normal slot if len(slot.primaryWakeUpChan) == 0 { slot.primaryWakeUpChan <- struct{}{} // only notify when target once @@ -645,8 +645,8 @@ func (p *ParallelStateProcessor) executeInSlot(slotIndex int, txReq *ParallelTxR func (p *ParallelStateProcessor) toConfirmTxIndex(targetTxIndex int, isStage2 bool) *ParallelTxResult { if isStage2 { if targetTxIndex <= p.mergedTxIndex+1 { - // this is the one that can been merged, - // others are for likely conflict check, since it is not their tuen. + // `p.mergedTxIndex+1` is the one to be merged, + // in stage2, we do likely conflict check, for these not their turn. return nil } } @@ -963,7 +963,7 @@ func (p *ParallelStateProcessor) Process(block *types.Block, statedb *state.Stat p.targetStage2Count = p.targetStage2Count - stage2AheadNum } - p.doStaticDispatch(statedb, p.allTxReqs) // todo: put txReqs in unit? + p.doStaticDispatch(p.allTxReqs) // todo: put txReqs in unit? // after static dispatch, we notify the slot to work. for _, slot := range p.slotState { slot.primaryWakeUpChan <- struct{}{} @@ -1007,6 +1007,7 @@ func (p *ParallelStateProcessor) Process(block *types.Block, statedb *state.Stat if result.err != nil { log.Error("ProcessParallel a failed tx", "resultSlotIndex", result.slotIndex, "resultTxIndex", result.txReq.txIndex, "result.err", result.err) + p.doCleanUp() bloomProcessor.Close() return statedb, nil, nil, 0, fmt.Errorf("could not apply tx %d [%v]: %w", result.txReq.txIndex, result.txReq.tx.Hash().Hex(), result.err) } From e6751125741941f0198887f83884d5fecd0923bf Mon Sep 17 00:00:00 2001 From: setunapo Date: Fri, 27 May 2022 10:52:46 +0800 Subject: [PATCH 16/16] code prune rd:7, typo fixups --- core/state/state_object.go | 16 ++--- core/state/statedb.go | 134 ++++++++++++++++++------------------- core/state_processor.go | 40 ++++++----- 3 files changed, 94 insertions(+), 96 deletions(-) diff --git a/core/state/state_object.go b/core/state/state_object.go index c8db8b25c1..f6c518a83a 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -172,7 +172,7 @@ type StateObject struct { fakeStorage Storage // Fake storage which constructed by caller for debugging purpose. // Cache flags. - // When an object is marked suicided it will be delete from the trie + // When an object is marked suicided it will be deleted from the trie // during the "update" phase of the state transition. dirtyCode bool // true if the code was updated suicided bool @@ -199,7 +199,7 @@ func (s *StateObject) empty() bool { // if codeHash is dirtied, it is ok, since code will not be updated. // if suicide, it is ok // if object is new created, it is ok - // if CreateAccout, recreate the address, it is ok. + // if CreateAccount, recreate the address, it is ok. // Slot 0 tx 0: AddBalance(100) to addr_1, => addr_1: balance = 100, nonce = 0, code is empty // Slot 1 tx 1: addr_1 Transfer 99.9979 with GasFee 0.0021, => addr_1: balance = 0, nonce = 1, code is empty @@ -207,12 +207,12 @@ func (s *StateObject) empty() bool { // Slot 0 tx 2: add balance 0 to addr_1(empty check for touch event), // the object was lightCopied from tx 0, - // in parallel mode, we should not check empty by raw nonce, balance, codeHash any more, + // in parallel mode, we should not check empty by raw nonce, balance, codeHash anymore, // since it could be invalid. // e.g., AddBalance() to an address, we will do lightCopy to get a new StateObject, we did balance fixup to // make sure object's Balance is reliable. But we did not fixup nonce or code, we only do nonce or codehash // fixup on need, that's when we wanna to update the nonce or codehash. - // So nonce, blance + // So nonce, balance // Before the block is processed, addr_1 account: nonce = 0, emptyCodeHash, balance = 100 // Slot 0 tx 0: no access to addr_1 // Slot 1 tx 1: sub balance 100, it is empty and deleted @@ -330,7 +330,7 @@ func (s *StateObject) GetState(db Database, key common.Hash) common.Hash { if dirty { return value } - // Otherwise return the entry's original value + // Otherwise, return the entry's original value return s.GetCommittedState(db, key) } @@ -381,7 +381,7 @@ func (s *StateObject) GetCommittedState(db Database, key common.Hash) common.Has readStart := time.Now() if metrics.EnabledExpensive { // If the snap is 'under construction', the first lookup may fail. If that - // happens, we don't want to double-count the time elapsed. Thus this + // happens, we don't want to double-count the time elapsed. Thus, this // dance with the metering. defer func() { if meter != nil { @@ -450,7 +450,7 @@ func (s *StateObject) SetState(db Database, key, value common.Hash) { // this `SetState could be skipped` // d.Finally, the key's value will be `val_2`, while it should be `val_1` // such as: https://bscscan.com/txs?block=2491181 - prev := s.dbItf.GetState(s.address, key) // fixme: if it is for journal, may not necessary, we can remove this change record + prev := s.dbItf.GetState(s.address, key) if prev == value { return } @@ -526,7 +526,7 @@ func (s *StateObject) finalise(prefetch bool) { // It will return nil if the trie has not been loaded and no changes have been made func (s *StateObject) updateTrie(db Database) Trie { // Make sure all dirty slots are finalized into the pending storage area - s.finalise(false) // Don't prefetch any more, pull directly if need be + s.finalise(false) // Don't prefetch anymore, pull directly if need be if s.pendingStorage.Length() == 0 { return s.trie } diff --git a/core/state/statedb.go b/core/state/statedb.go index 49dc2e2419..fabb2aa3ad 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -119,7 +119,7 @@ func (s *StateDB) deleteStateObj(addr common.Address) { } } -// For parallel mode only +// ParallelState is for parallel mode only type ParallelState struct { isSlotDB bool // denotes StateDB is used in slot, we will try to remove it SlotIndex int // for debug, to be removed @@ -136,14 +136,14 @@ type ParallelState struct { unconfirmedDBs *sync.Map /*map[int]*ParallelStateDB*/ // do unconfirmed reference in same slot. // we will record the read detail for conflict check and - // the changed addr or key for object merge, the changed detail can be acheived from the dirty object + // the changed addr or key for object merge, the changed detail can be achieved from the dirty object nonceChangesInSlot map[common.Address]struct{} nonceReadsInSlot map[common.Address]uint64 balanceChangesInSlot map[common.Address]struct{} // the address's balance has been changed balanceReadsInSlot map[common.Address]*big.Int // the address's balance has been read and used. - // codeSize can be derived based on code, but codeHash can not directly derived based on code + // codeSize can be derived based on code, but codeHash can not be directly derived based on code // - codeSize is 0 for address not exist or empty code - // - codeHash is `common.Hash{}` for address not exist, emptyCodeHash(`Keccak256Hash(nil)`) for empty code + // - codeHash is `common.Hash{}` for address not exist, emptyCodeHash(`Keccak256Hash(nil)`) for empty code, // so we use codeReadsInSlot & codeHashReadsInSlot to keep code and codeHash, codeSize is derived from code codeReadsInSlot map[common.Address][]byte // empty if address not exist or no code in this address codeHashReadsInSlot map[common.Address]common.Hash @@ -256,7 +256,7 @@ func New(root common.Hash, db Database, snaps *snapshot.Tree) (*StateDB, error) return newStateDB(root, db, snaps) } -// NewWithSharedPool creates a new state with sharedStorge on layer 1.5 +// NewWithSharedPool creates a new state with sharedStorage on layer 1.5 func NewWithSharedPool(root common.Hash, db Database, snaps *snapshot.Tree) (*StateDB, error) { statedb, err := newStateDB(root, db, snaps) if err != nil { @@ -523,7 +523,7 @@ func (s *StateDB) GetCode(addr common.Address) []byte { } func (s *StateDB) GetCodeSize(addr common.Address) int { - var codeSize int = 0 + var codeSize = 0 stateObject := s.getStateObject(addr) if stateObject != nil { codeSize = stateObject.CodeSize(s.db) @@ -531,7 +531,7 @@ func (s *StateDB) GetCodeSize(addr common.Address) int { return codeSize } -// return value of GetCodeHash: +// GetCodeHash return: // - common.Hash{}: the address does not exist // - emptyCodeHash: the address exist, but code is empty // - others: the address exist, and code is not empty @@ -851,14 +851,14 @@ func (s *StateDB) GetOrNewStateObject(addr common.Address) *StateObject { // `prev` is used to handle revert, to recover with the `prev` object // In Parallel mode, we only need to recover to `prev` in SlotDB, // a.if it is not in SlotDB, `revert` will remove it from the SlotDB -// b.if it is exist in SlotDB, `revert` will recover to the `prev` in SlotDB +// b.if it is existed in SlotDB, `revert` will recover to the `prev` in SlotDB // c.as `snapDestructs` it is the same func (s *StateDB) createObject(addr common.Address) (newobj *StateObject) { prev := s.getDeletedStateObject(addr) // Note, prev might have been deleted, we need that! var prevdestruct bool if s.snap != nil && prev != nil { - s.snapParallelLock.Lock() // fixme: with new dispatch policy, the ending Tx could runing, while the block have processed. + s.snapParallelLock.Lock() // fixme: with new dispatch policy, the ending Tx could running, while the block have processed. _, prevdestruct = s.snapDestructs[prev.address] if !prevdestruct { // To destroy the previous trie node first and update the trie tree @@ -1007,7 +1007,7 @@ func (s *StateDB) Copy() *StateDB { } if s.snaps != nil { // In order for the miner to be able to use and make additions - // to the snapshot tree, we need to copy that aswell. + // to the snapshot tree, we need to copy that as well. // Otherwise, any block mined by ourselves will cause gaps in the tree, // and force the miner to operate trie-backed only state.snaps = s.snaps @@ -1243,7 +1243,7 @@ func (s *StateDB) CopyForSlot() *ParallelStateDB { if s.snaps != nil { // In order for the miner to be able to use and make additions - // to the snapshot tree, we need to copy that aswell. + // to the snapshot tree, we need to copy that as well. // Otherwise, any block mined by ourselves will cause gaps in the tree, // and force the miner to operate trie-backed only state.snaps = s.snaps @@ -1271,7 +1271,7 @@ func (s *StateDB) CopyForSlot() *ParallelStateDB { // state.snapStorage[k] = temp // } - // trie prefetch should be done by dispacther on StateObject Merge, + // trie prefetch should be done by dispatcher on StateObject Merge, // disable it in parallel slot // state.prefetcher = s.prefetcher } @@ -1355,14 +1355,14 @@ func (s *StateDB) Finalise(deleteEmptyObjects bool) { // fixme: concurrent safe. // If state snapshotting is active, also mark the destruction there. // Note, we can't do this only at the end of a block because multiple - // transactions within the same block might self destruct and then + // transactions within the same block might self-destruct and then // ressurrect an account; but the snapshotter needs both events. if s.snap != nil { s.snapParallelLock.Lock() s.snapDestructs[obj.address] = struct{}{} // We need to maintain account deletions explicitly (will remain set indefinitely) s.snapParallelLock.Unlock() - delete(s.snapAccounts, obj.address) // Clear out any previously updated account data (may be recreated via a ressurrect) - delete(s.snapStorage, obj.address) // Clear out any previously updated storage data (may be recreated via a ressurrect) + delete(s.snapAccounts, obj.address) // Clear out any previously updated account data (maybe recreated via a ressurrect) + delete(s.snapStorage, obj.address) // Clear out any previously updated storage data (maybe recreated via a ressurrect) } } else { // 1.none parallel mode, we do obj.finalise(true) as normal @@ -1499,7 +1499,7 @@ func (s *StateDB) AccountsIntermediateRoot() { // Although naively it makes sense to retrieve the account trie and then do // the contract storage and account updates sequentially, that short circuits // the account prefetcher. Instead, let's process all the storage updates - // first, giving the account prefeches just a few more milliseconds of time + // first, giving the account prefetches just a few more milliseconds of time // to pull useful data from disk. for addr := range s.stateObjectsPending { if obj, _ := s.getStateObjectFromStateObjects(addr); !obj.deleted { @@ -1601,7 +1601,7 @@ func (s *StateDB) clearJournalAndRefund() { s.journal = newJournal() s.refund = 0 } - s.validRevisions = s.validRevisions[:0] // Snapshots can be created without journal entires + s.validRevisions = s.validRevisions[:0] // Snapshots can be created without journal entries } func (s *StateDB) LightCommit() (common.Hash, *types.DiffLayer, error) { @@ -2191,7 +2191,7 @@ func (s *StateDB) MergeSlotDB(slotDb *ParallelStateDB, slotReceipt *types.Receip } else { // addr already in main DB, do merge: balance, KV, code, State(create, suicide) // can not do copy or ownership transfer directly, since dirtyObj could have outdated - // data(may be updated within the conflict window) + // data(maybe updated within the conflict window) var newMainObj = mainObj // we don't need to copy the object since the storages are thread safe if _, ok := slotDb.parallel.addrStateChangesInSlot[addr]; ok { @@ -2199,9 +2199,9 @@ func (s *StateDB) MergeSlotDB(slotDb *ParallelStateDB, slotReceipt *types.Receip // 1.Suicide // 2.Empty Delete // 3.createObject - // a.AddBalance,SetState to an unexist or deleted(suicide, empty delete) address. - // b.CreateAccount: like DAO the fork, regenerate a account carry its balance without KV - // For these state change, do ownership transafer for efficiency: + // a: AddBalance,SetState to a non-exist or deleted(suicide, empty delete) address. + // b: CreateAccount: like DAO the fork, regenerate an account carry its balance without KV + // For these state change, do ownership transfer for efficiency: // dirtyObj.db = s // newMainObj = dirtyObj newMainObj = dirtyObj.deepCopy(s) @@ -2270,7 +2270,7 @@ func (s *StateDB) MergeSlotDB(slotDb *ParallelStateDB, slotReceipt *types.Receip // There could be a race condition for parallel transaction execution // One transaction add balance 0 to an empty address, will delete it(delete empty is enabled). // While another concurrent transaction could add a none-zero balance to it, make it not empty - // We fixed it by add a addr state read record for add balance 0 + // We fixed it by add an addr state read record for add balance 0 s.snapParallelLock.Lock() s.snapDestructs[k] = struct{}{} s.snapParallelLock.Unlock() @@ -2292,7 +2292,7 @@ func (s *StateDB) MergeSlotDB(slotDb *ParallelStateDB, slotReceipt *types.Receip s.txIndex = txIndex } -func (s *StateDB) ParallelMakeUp(addr common.Address, input []byte) { +func (s *StateDB) ParallelMakeUp(common.Address, []byte) { // do nothing, this API is for parallel mode } @@ -2341,7 +2341,7 @@ func hasKvConflict(slotDB *ParallelStateDB, addr common.Address, key common.Hash return false } -// start several routines to do conflict check +// StartKvCheckLoop start several routines to do conflict check func StartKvCheckLoop() { parallelKvCheckReqCh = make(chan ParallelKvCheckMessage, 200) parallelKvCheckResCh = make(chan bool, 10) @@ -2431,7 +2431,7 @@ func (s *ParallelStateDB) storeStateObj(addr common.Address, stateObject *StateO // it belongs to base StateDB, it is confirmed and valid. stateObject.db = s.parallel.baseStateDB stateObject.dbItf = s.parallel.baseStateDB - // the object could be create in SlotDB, if it got the object from DB and + // the object could be created in SlotDB, if it got the object from DB and // update it to the shared `s.parallel.stateObjects`` stateObject.db.storeParallelLock.Lock() if _, ok := s.parallel.stateObjects.Load(addr); !ok { @@ -2460,10 +2460,10 @@ func (s *ParallelStateDB) getStateObjectNoSlot(addr common.Address) *StateObject // `prev` is used to handle revert, to recover with the `prev` object // In Parallel mode, we only need to recover to `prev` in SlotDB, // a.if it is not in SlotDB, `revert` will remove it from the SlotDB -// b.if it is exist in SlotDB, `revert` will recover to the `prev` in SlotDB +// b.if it is existed in SlotDB, `revert` will recover to the `prev` in SlotDB // c.as `snapDestructs` it is the same func (s *ParallelStateDB) createObject(addr common.Address) (newobj *StateObject) { - // do not get from unconfirmed DB, since it will has problem on revert + // do not get from unconfirmed DB, since it will have problem on revert prev := s.parallel.dirtiedStateObjectsInSlot[addr] var prevdestruct bool @@ -2510,7 +2510,7 @@ func (s *ParallelStateDB) getDeletedStateObject(addr common.Address) *StateObjec if !ok { return nil } - // this is why we have to use a seperate getDeletedStateObject for ParallelStateDB + // this is why we have to use a separate getDeletedStateObject for ParallelStateDB // `s` has to be the ParallelStateDB obj := newObject(s, s.isParallel, addr, *data) s.storeStateObj(addr, obj) @@ -2554,7 +2554,7 @@ func (s *ParallelStateDB) Exist(addr common.Address) bool { } return true } - // 2.Try to get from uncomfirmed & main DB + // 2.Try to get from unconfirmed & main DB // 2.1 Already read before if exist, ok := s.parallel.addrStateReadsInSlot[addr]; ok { return exist @@ -2592,7 +2592,7 @@ func (s *ParallelStateDB) Empty(addr common.Address) bool { codeHash := s.GetCodeHash(addr) return bytes.Equal(codeHash.Bytes(), emptyCodeHash) // code is empty, the object is empty } - // 2.Try to get from uncomfirmed & main DB + // 2.Try to get from unconfirmed & main DB // 2.1 Already read before if exist, ok := s.parallel.addrStateReadsInSlot[addr]; ok { // exist means not empty @@ -2605,7 +2605,7 @@ func (s *ParallelStateDB) Empty(addr common.Address) bool { } so := s.getStateObjectNoSlot(addr) - empty := (so == nil || so.empty()) + empty := so == nil || so.empty() s.parallel.addrStateReadsInSlot[addr] = !empty // update and cache return empty } @@ -2624,7 +2624,7 @@ func (s *ParallelStateDB) GetBalance(addr common.Address) *big.Int { return obj.Balance() } } - // 2.Try to get from uncomfirmed DB or main DB + // 2.Try to get from unconfirmed DB or main DB // 2.1 Already read before if balance, ok := s.parallel.balanceReadsInSlot[addr]; ok { return balance @@ -2635,7 +2635,7 @@ func (s *ParallelStateDB) GetBalance(addr common.Address) *big.Int { return balance } - // 3. Try to get from main StateObejct + // 3. Try to get from main StateObject balance := common.Big0 stateObject := s.getStateObjectNoSlot(addr) if stateObject != nil { @@ -2645,7 +2645,7 @@ func (s *ParallelStateDB) GetBalance(addr common.Address) *big.Int { return balance } -// different from GetBalance(), it is opcode triggered +// GetBalanceOpCode different from GetBalance(), it is opcode triggered func (s *ParallelStateDB) GetBalanceOpCode(addr common.Address) *big.Int { if addr == WBNBAddress { s.wbnbMakeUp = false @@ -2662,7 +2662,7 @@ func (s *ParallelStateDB) GetNonce(addr common.Address) uint64 { return obj.Nonce() } } - // 2.Try to get from uncomfirmed DB or main DB + // 2.Try to get from unconfirmed DB or main DB // 2.1 Already read before if nonce, ok := s.parallel.nonceReadsInSlot[addr]; ok { return nonce @@ -2693,7 +2693,7 @@ func (s *ParallelStateDB) GetCode(addr common.Address) []byte { return code } } - // 2.Try to get from uncomfirmed DB or main DB + // 2.Try to get from unconfirmed DB or main DB // 2.1 Already read before if code, ok := s.parallel.codeReadsInSlot[addr]; ok { return code @@ -2704,7 +2704,7 @@ func (s *ParallelStateDB) GetCode(addr common.Address) []byte { return code } - // 3. Try to get from main StateObejct + // 3. Try to get from main StateObject stateObject := s.getStateObjectNoSlot(addr) var code []byte if stateObject != nil { @@ -2723,7 +2723,7 @@ func (s *ParallelStateDB) GetCodeSize(addr common.Address) int { return obj.CodeSize(s.db) } } - // 2.Try to get from uncomfirmed DB or main DB + // 2.Try to get from unconfirmed DB or main DB // 2.1 Already read before if code, ok := s.parallel.codeReadsInSlot[addr]; ok { return len(code) // len(nil) is 0 too @@ -2734,8 +2734,8 @@ func (s *ParallelStateDB) GetCodeSize(addr common.Address) int { return len(code) // len(nil) is 0 too } - // 3. Try to get from main StateObejct - var codeSize int = 0 + // 3. Try to get from main StateObject + var codeSize = 0 var code []byte stateObject := s.getStateObjectNoSlot(addr) @@ -2747,7 +2747,7 @@ func (s *ParallelStateDB) GetCodeSize(addr common.Address) int { return codeSize } -// return value of GetCodeHash: +// GetCodeHash return: // - common.Hash{}: the address does not exist // - emptyCodeHash: the address exist, but code is empty // - others: the address exist, and code is not empty @@ -2760,7 +2760,7 @@ func (s *ParallelStateDB) GetCodeHash(addr common.Address) common.Hash { return common.BytesToHash(obj.CodeHash()) } } - // 2.Try to get from uncomfirmed DB or main DB + // 2.Try to get from unconfirmed DB or main DB // 2.1 Already read before if codeHash, ok := s.parallel.codeHashReadsInSlot[addr]; ok { return codeHash @@ -2770,7 +2770,7 @@ func (s *ParallelStateDB) GetCodeHash(addr common.Address) common.Hash { s.parallel.codeHashReadsInSlot[addr] = codeHash return codeHash } - // 3. Try to get from main StateObejct + // 3. Try to get from main StateObject stateObject := s.getStateObjectNoSlot(addr) codeHash := common.Hash{} if stateObject != nil { @@ -2802,7 +2802,7 @@ func (s *ParallelStateDB) GetState(addr common.Address, hash common.Hash) common return obj.GetState(s.db, hash) } } - // 2.Try to get from uncomfirmed DB or main DB + // 2.Try to get from unconfirmed DB or main DB // 2.1 Already read before if storage, ok := s.parallel.kvReadsInSlot[addr]; ok { if val, ok := storage.GetValue(hash); ok { @@ -2834,7 +2834,7 @@ func (s *ParallelStateDB) GetState(addr common.Address, hash common.Hash) common // GetCommittedState retrieves a value from the given account's committed storage trie. func (s *ParallelStateDB) GetCommittedState(addr common.Address, hash common.Hash) common.Hash { // 1.No need to get from pending of itself even on merge, since stateobject in SlotDB won't do finalise - // 2.Try to get from uncomfirmed DB or main DB + // 2.Try to get from unconfirmed DB or main DB // KVs in unconfirmed DB can be seen as pending storage // KVs in main DB are merged from SlotDB and has done finalise() on merge, can be seen as pending storage too. // 2.1 Already read before @@ -2870,7 +2870,7 @@ func (s *ParallelStateDB) HasSuicided(addr common.Address) bool { if obj, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; ok { return obj.suicided } - // 2.Try to get from uncomfirmed + // 2.Try to get from unconfirmed if exist, ok := s.getAddrStateFromUnconfirmedDB(addr); ok { return !exist } @@ -2901,8 +2901,7 @@ func (s *ParallelStateDB) AddBalance(addr common.Address, amount *big.Int) { s.parallel.balanceChangesInSlot[addr] = struct{}{} return } - // already dirty, make sure the balance if fixed up - // if stateObject.Balance() + // already dirty, make sure the balance is fixed up since it could be previously dirtied by nonce or KV... if addr != s.parallel.systemAddress { balance := s.GetBalance(addr) if stateObject.Balance().Cmp(balance) != 0 { @@ -2936,8 +2935,7 @@ func (s *ParallelStateDB) SubBalance(addr common.Address, amount *big.Int) { s.parallel.dirtiedStateObjectsInSlot[addr] = newStateObject return } - // already dirty, make sure the balance if fixed - // if stateObject.Balance() + // already dirty, make sure the balance is fixed up since it could be previously dirtied by nonce or KV... if addr != s.parallel.systemAddress { if addr != s.parallel.systemAddress { balance := s.GetBalance(addr) if stateObject.Balance().Cmp(balance) != 0 { @@ -2960,7 +2958,7 @@ func (s *ParallelStateDB) SetBalance(addr common.Address, amount *big.Int) { } if _, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; !ok { newStateObject := stateObject.lightCopy(s) - // update balance for revert, in case child contract is revertted, + // update balance for revert, in case child contract is reverted, // it should revert to the previous balance balance := s.GetBalance(addr) newStateObject.setBalance(balance) @@ -3065,12 +3063,12 @@ func (s *ParallelStateDB) Suicide(addr common.Address) bool { // 1.Try to get from dirty, it could be suicided inside of contract call stateObject = s.parallel.dirtiedStateObjectsInSlot[addr] if stateObject == nil { - // 2.Try to get from uncomfirmed, if deleted return false, since the address does not exist + // 2.Try to get from unconfirmed, if deleted return false, since the address does not exist if obj, ok := s.getStateObjectFromUnconfirmedDB(addr); ok { stateObject = obj s.parallel.addrStateReadsInSlot[addr] = !stateObject.deleted // true: exist, false: deleted if stateObject.deleted { - log.Error("Suicide addr alreay deleted in confirmed DB", "txIndex", s.txIndex, "addr", addr) + log.Error("Suicide addr already deleted in confirmed DB", "txIndex", s.txIndex, "addr", addr) return false } } @@ -3106,7 +3104,7 @@ func (s *ParallelStateDB) Suicide(addr common.Address) bool { // s.parallel.kvChangesInSlot[addr] = make(StateKeys) // all key changes are discarded return true } - s.parallel.addrStateChangesInSlot[addr] = false // false: the address does not exist any more, + s.parallel.addrStateChangesInSlot[addr] = false // false: the address does not exist anymore s.parallel.balanceChangesInSlot[addr] = struct{}{} s.parallel.codeChangesInSlot[addr] = struct{}{} @@ -3129,7 +3127,7 @@ func (s *ParallelStateDB) CreateAccount(addr common.Address) { // no matter it is got from dirty, unconfirmed or main DB // if addr not exist, preBalance will be common.Big0, it is same as new(big.Int) which // is the value newObject(), - preBalance := s.GetBalance(addr) // parallel balance read will be recorded inside of GetBalance + preBalance := s.GetBalance(addr) // parallel balance read will be recorded inside GetBalance newObj := s.createObject(addr) newObj.setBalance(new(big.Int).Set(preBalance)) // new big.Int for newObj } @@ -3162,7 +3160,7 @@ func (s *ParallelStateDB) AddRefund(gas uint64) { // todo: not needed, can be de func (s *ParallelStateDB) SubRefund(gas uint64) { s.journal.append(refundChange{prev: s.refund}) if gas > s.refund { - // we don't need to panic here if we read the wrong state in parallelm mode + // we don't need to panic here if we read the wrong state in parallel mode // we just need to redo this transaction log.Info(fmt.Sprintf("Refund counter below zero (gas: %d > refund: %d)", gas, s.refund), "tx", s.thash.String()) s.parallel.needsRedo = true @@ -3254,7 +3252,7 @@ func (s *ParallelStateDB) getNonceFromUnconfirmedDB(addr common.Address) (uint64 } // Similar to getBalanceFromUnconfirmedDB -// It is not only for code, but also codeHash and codeSize, we return the *StateObject for convienence. +// It is not only for code, but also codeHash and codeSize, we return the *StateObject for convenience. func (s *ParallelStateDB) getCodeFromUnconfirmedDB(addr common.Address) ([]byte, bool) { if addr == s.parallel.systemAddress { // never get systemaddress from unconfirmed DB @@ -3404,7 +3402,8 @@ func (s *ParallelStateDB) getStateObjectFromUnconfirmedDB(addr common.Address) ( return nil, false } -// in stage2, we do unconfirmed conflict detect +// IsParallelReadsValid If stage2 is true, it is a likely conflict check, +// to detect these potential conflict results in advance and schedule redo ASAP. func (s *ParallelStateDB) IsParallelReadsValid(isStage2 bool) bool { parallelKvOnce.Do(func() { StartKvCheckLoop() @@ -3626,11 +3625,13 @@ func (s *ParallelStateDB) IsParallelReadsValid(isStage2 bool) bool { return true } +// SystemAddressRedo // For most of the transactions, systemAddressOpsCount should be 3: -// one for SetBalance(0) on NewSlotDB() -// the other is for AddBalance(GasFee) at the end. -// (systemAddressOpsCount > 3) means the transaction tries to access systemAddress, in -// this case, we should redo and keep its balance on NewSlotDB() +// - one for SetBalance(0) on NewSlotDB() +// - the second is for AddBalance(GasFee) at the end, +// - the third is for GetBalance() which is triggered by AddBalance() +// (systemAddressOpsCount > 3) means the transaction tries to access systemAddress, in this case, the +// transaction needs the accurate systemAddress info, then it should redo and keep its balance on NewSlotDB() // for example: // https://bscscan.com/tx/0xe469f1f948de90e9508f96da59a96ed84b818e71432ca11c5176eb60eb66671b func (s *ParallelStateDB) SystemAddressRedo() bool { @@ -3648,11 +3649,10 @@ func (s *ParallelStateDB) NeedsRedo() bool { return s.parallel.needsRedo } -/** - * WBNB makeup is allowed when WBNB'balance is only accessed through contract Call. - * If it is accessed not through contract all, e.g., by `address.balance`, `address.transfer(amount)`, - * we can not do balance make up. - */ +// WBNBMakeUp +// WBNB makeup is allowed only when its balance is accessed through contract Call. +// If it is accessed not through contract all, e.g., by `address.balance`, `address.transfer(amount)`, +// we can not do balance make up. func (s *ParallelStateDB) WBNBMakeUp() bool { return s.wbnbMakeUp } diff --git a/core/state_processor.go b/core/state_processor.go index 9df75b9c7a..a947450074 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -61,7 +61,7 @@ const ( // StateProcessor implements Processor. type StateProcessor struct { config *params.ChainConfig // Chain configuration options - bc *BlockChain // Canonical block chain + bc *BlockChain // Canonical blockchain engine consensus.Engine // Consensus engine used for block rewards } @@ -73,7 +73,6 @@ func NewStateProcessor(config *params.ChainConfig, bc *BlockChain, engine consen } } -// add for parallel executions type ParallelStateProcessor struct { StateProcessor parallelNum int // leave a CPU to dispatcher @@ -91,7 +90,7 @@ type ParallelStateProcessor struct { // start for confirm stage2 confirmStage2Chan chan int stopConfirmStage2Chan chan struct{} - txReqExecuteRecord map[int]int // for each the execute count of each Tx + txReqExecuteRecord map[int]int txReqExecuteCount int inConfirmStage2 bool targetStage2Count int // when executed txNUM reach it, enter stage2 RT confirm @@ -471,7 +470,7 @@ func (p *ParallelStateProcessor) init() { }(i) // start the shadow slot. - // It is back up of the primary slot to make sure transaction can be redo ASAP, + // It is back up of the primary slot to make sure transaction can be redone ASAP, // since the primary slot could be busy at executing another transaction go func(slotIndex int) { p.runSlotLoop(slotIndex, parallelShadowSlot) // this loop will be permanent live @@ -525,7 +524,7 @@ func (p *ParallelStateProcessor) doStaticDispatch(txReqs []*ParallelTxRequest) { fromSlotMap := make(map[common.Address]int, 100) toSlotMap := make(map[common.Address]int, 100) for _, txReq := range txReqs { - var slotIndex int = -1 + var slotIndex = -1 if i, ok := fromSlotMap[txReq.msg.From()]; ok { // first: same From are all in same slot slotIndex = i @@ -538,7 +537,7 @@ func (p *ParallelStateProcessor) doStaticDispatch(txReqs []*ParallelTxRequest) { // not found, dispatch to most hungry slot if slotIndex == -1 { - var workload int = len(p.slotState[0].pendingTxReqList) + var workload = len(p.slotState[0].pendingTxReqList) slotIndex = 0 for i, slot := range p.slotState { // can start from index 1 if len(slot.pendingTxReqList) < workload { @@ -554,7 +553,7 @@ func (p *ParallelStateProcessor) doStaticDispatch(txReqs []*ParallelTxRequest) { } slot := p.slotState[slotIndex] - txReq.staticSlotIndex = slotIndex // txreq is better to be executed in this slot + txReq.staticSlotIndex = slotIndex // txReq is better to be executed in this slot slot.pendingTxReqList = append(slot.pendingTxReqList, txReq) } } @@ -606,7 +605,7 @@ func (p *ParallelStateProcessor) executeInSlot(slotIndex int, txReq *ParallelTxR blockContext := NewEVMBlockContext(txReq.block.Header(), p.bc, nil) // can share blockContext within a block for efficiency vmenv := vm.NewEVM(blockContext, vm.TxContext{}, slotDB, p.config, txReq.vmConfig) // gasLimit not accurate, but it is ok for block import. - // each slot would use its own gas pool, and will do gaslimit check later + // each slot would use its own gas pool, and will do gas limit check later gpSlot := new(GasPool).AddGas(txReq.gasLimit) // block.GasLimit() evm, result, err := applyTransactionStageExecution(txReq.msg, gpSlot, slotDB, vmenv) @@ -629,7 +628,7 @@ func (p *ParallelStateProcessor) executeInSlot(slotIndex int, txReq *ParallelTxR slotDB.Finalise(true) // Finalise could write s.parallel.addrStateChangesInSlot[addr], keep Read and Write in same routine to avoid crash p.unconfirmedDBs.Store(txReq.txIndex, slotDB) } else { - // the transaction failed at check(nonce or blanace), actually it has not been executed yet. + // the transaction failed at check(nonce or balance), actually it has not been executed yet. atomic.CompareAndSwapInt32(&txReq.runnable, 0, 1) // the error could be caused by unconfirmed balance reference, // the balance could insufficient to pay its gas limit, which cause it preCheck.buyGas() failed @@ -661,7 +660,7 @@ func (p *ParallelStateProcessor) toConfirmTxIndex(targetTxIndex int, isStage2 bo } targetResult = result.(*ParallelTxResult) // in stage 2, don't schedule a new redo if the TxReq is: - // a.runnable: it will be redo + // a.runnable: it will be redone // b.running: the new result will be more reliable, we skip check right now if atomic.CompareAndSwapInt32(&targetResult.txReq.runnable, 1, 1) { return nil @@ -672,10 +671,10 @@ func (p *ParallelStateProcessor) toConfirmTxIndex(targetTxIndex int, isStage2 bo } else { results := p.pendingConfirmResults[targetTxIndex] resultsLen := len(results) - if resultsLen == 0 { // no pending result can be verified, break and wait for incoming results + if resultsLen == 0 { // there is no pending result can be verified, break and wait for incoming results return nil } - targetResult = results[len(results)-1] // last is the most fresh, stack based priority + targetResult = results[len(results)-1] // last is the freshest, stack based priority p.pendingConfirmResults[targetTxIndex] = p.pendingConfirmResults[targetTxIndex][:resultsLen-1] // remove from the queue } @@ -689,7 +688,7 @@ func (p *ParallelStateProcessor) toConfirmTxIndex(targetTxIndex int, isStage2 bo p.switchSlot(staticSlotIndex) return nil } - if len(p.pendingConfirmResults[targetTxIndex]) == 0 { // this is the last result to check and it is not valid + if len(p.pendingConfirmResults[targetTxIndex]) == 0 { // this is the last result to check, and it is not valid atomic.CompareAndSwapInt32(&targetResult.txReq.runnable, 0, 1) // needs redo p.debugConflictRedoNum++ // interrupt its current routine, and switch to the other routine @@ -774,8 +773,7 @@ func (p *ParallelStateProcessor) runSlotLoop(slotIndex int, slotType int32) { } // txReq in this Slot have all been executed, try steal one from other slot. - // as long as the TxReq is runable, we steal it, mark it as stolen - // steal one by one + // as long as the TxReq is runnable, we steal it, mark it as stolen for _, stealTxReq := range p.allTxReqs { if stealTxReq.txIndex <= p.mergedTxIndex { continue @@ -809,13 +807,13 @@ func (p *ParallelStateProcessor) runConfirmStage2Loop() { <-p.confirmStage2Chan // drain the chan to get the latest merged txIndex } } - // stage 2,if all tx have been executed at least once, and its result has been recevied. + // stage 2,if all tx have been executed at least once, and its result has been received. // in Stage 2, we will run check when merge is advanced. // more aggressive tx result confirm, even for these Txs not in turn // now we will be more aggressive: - // do conflcit check , as long as tx result is generated, + // do conflict check , as long as tx result is generated, // if lucky, it is the Tx's turn, we will do conflict check with WBNB makeup - // otherwise, do conflict check without WBNB makeup, but we will ignor WBNB's balance conflict. + // otherwise, do conflict check without WBNB makeup, but we will ignore WBNB's balance conflict. // throw these likely conflicted tx back to re-execute startTxIndex := p.mergedTxIndex + 2 // stage 2's will start from the next target merge index endTxIndex := startTxIndex + stage2CheckNumber @@ -842,7 +840,7 @@ func (p *ParallelStateProcessor) handleTxResults() *ParallelTxResult { return nil } // schedule stage 2 when new Tx has been merged, schedule once and ASAP - // stage 2,if all tx have been executed at least once, and its result has been recevied. + // stage 2,if all tx have been executed at least once, and its result has been received. // in Stage 2, we will run check when main DB is advanced, i.e., new Tx result has been merged. if p.inConfirmStage2 && p.mergedTxIndex >= p.nextStage2TxIndex { p.nextStage2TxIndex = p.mergedTxIndex + stage2CheckNumber @@ -892,7 +890,7 @@ func (p *ParallelStateProcessor) doCleanUp() { } break } - // 3.make sure the confirm routines are stopped + // 3.make sure the confirmation routine is stopped p.stopConfirmStage2Chan <- struct{}{} <-p.stopSlotChan } @@ -958,7 +956,7 @@ func (p *ParallelStateProcessor) Process(block *types.Block, statedb *state.Stat // set up stage2 enter criteria p.targetStage2Count = len(p.allTxReqs) if p.targetStage2Count > 50 { - // usually, the the last Tx could be the bottleneck it could be very slow, + // usually, the last Tx could be the bottleneck it could be very slow, // so it is better for us to enter stage 2 a bit earlier p.targetStage2Count = p.targetStage2Count - stage2AheadNum }