From b1ba97c4f544e5f71a3531c8e8253b86532f3dc8 Mon Sep 17 00:00:00 2001 From: Jerry Date: Tue, 5 Jul 2022 17:32:05 -0700 Subject: [PATCH 01/38] Create MVHashMap and use it StateDB --- core/blockstm/mvhashmap.go | 218 +++++++++++++++++++ core/blockstm/txio.go | 75 +++++++ core/state/journal.go | 8 + core/state/statedb.go | 436 ++++++++++++++++++++++++++++++------- core/state/statedb_test.go | 424 ++++++++++++++++++++++++++++++++++++ go.mod | 2 + go.sum | 2 + 7 files changed, 1085 insertions(+), 80 deletions(-) create mode 100644 core/blockstm/mvhashmap.go create mode 100644 core/blockstm/txio.go diff --git a/core/blockstm/mvhashmap.go b/core/blockstm/mvhashmap.go new file mode 100644 index 0000000000..ff9f1a6f9d --- /dev/null +++ b/core/blockstm/mvhashmap.go @@ -0,0 +1,218 @@ +package blockstm + +import ( + "fmt" + "sync" + + "github.com/emirpasic/gods/maps/treemap" +) + +const FlagDone = 0 +const FlagEstimate = 1 + +type MVHashMap struct { + rw sync.RWMutex + m map[string]*TxnIndexCells // TODO: might want a more efficient key representation +} + +func MakeMVHashMap() *MVHashMap { + return &MVHashMap{ + rw: sync.RWMutex{}, + m: make(map[string]*TxnIndexCells), + } +} + +type WriteCell struct { + flag uint + incarnation int + data interface{} +} + +type TxnIndexCells struct { + rw sync.RWMutex + tm *treemap.Map +} + +type Version struct { + TxnIndex int + Incarnation int +} + +func (mv *MVHashMap) getKeyCells(k []byte, fNoKey func(kenc string) *TxnIndexCells) (cells *TxnIndexCells) { + kenc := string(k) + + var ok bool + + mv.rw.RLock() + cells, ok = mv.m[kenc] + mv.rw.RUnlock() + + if !ok { + cells = fNoKey(kenc) + } + + return +} + +func (mv *MVHashMap) Write(k []byte, v Version, data interface{}) { + cells := mv.getKeyCells(k, func(kenc string) (cells *TxnIndexCells) { + n := &TxnIndexCells{ + rw: sync.RWMutex{}, + tm: treemap.NewWithIntComparator(), + } + var ok bool + mv.rw.Lock() + if cells, ok = mv.m[kenc]; !ok { + mv.m[kenc] = n + cells = n + } + mv.rw.Unlock() + return + }) + + // TODO: could probably have a scheme where this only generally requires a read lock since any given transaction transaction + // should only have one incarnation executing at a time... + cells.rw.Lock() + defer cells.rw.Unlock() + ci, ok := cells.tm.Get(v.TxnIndex) + + if ok { + if ci.(*WriteCell).incarnation > v.Incarnation { + panic(fmt.Errorf("existing transaction value does not have lower incarnation: %v, %v", + string(k), v.TxnIndex)) + } else if ci.(*WriteCell).flag == FlagEstimate { + println("marking previous estimate as done tx", v.TxnIndex, v.Incarnation) + } + + ci.(*WriteCell).flag = FlagDone + ci.(*WriteCell).incarnation = v.Incarnation + ci.(*WriteCell).data = data + } else { + cells.tm.Put(v.TxnIndex, &WriteCell{ + flag: FlagDone, + incarnation: v.Incarnation, + data: data, + }) + } +} + +func (mv *MVHashMap) MarkEstimate(k []byte, txIdx int) { + cells := mv.getKeyCells(k, func(_ string) *TxnIndexCells { + panic(fmt.Errorf("path must already exist")) + }) + + cells.rw.RLock() + if ci, ok := cells.tm.Get(txIdx); !ok { + panic("should not happen - cell should be present for path") + } else { + ci.(*WriteCell).flag = FlagEstimate + } + cells.rw.RUnlock() +} + +func (mv *MVHashMap) Delete(k []byte, txIdx int) { + cells := mv.getKeyCells(k, func(_ string) *TxnIndexCells { + panic(fmt.Errorf("path must already exist")) + }) + + cells.rw.Lock() + defer cells.rw.Unlock() + cells.tm.Remove(txIdx) +} + +const ( + MVReadResultDone = 0 + MVReadResultDependency = 1 + MVReadResultNone = 2 +) + +type MVReadResult struct { + depIdx int + incarnation int + value interface{} +} + +func (res *MVReadResult) DepIdx() int { + return res.depIdx +} + +func (res *MVReadResult) Incarnation() int { + return res.incarnation +} + +func (res *MVReadResult) Value() interface{} { + return res.value +} + +func (mvr MVReadResult) Status() int { + if mvr.depIdx != -1 { + if mvr.incarnation == -1 { + return MVReadResultDependency + } else { + return MVReadResultDone + } + } + + return MVReadResultNone +} + +func (mv *MVHashMap) Read(k []byte, txIdx int) (res MVReadResult) { + res.depIdx = -1 + res.incarnation = -1 + + cells := mv.getKeyCells(k, func(_ string) *TxnIndexCells { + return nil + }) + if cells == nil { + return + } + + cells.rw.RLock() + defer cells.rw.RUnlock() + + if fk, fv := cells.tm.Floor(txIdx - 1); fk != nil && fv != nil { + c := fv.(*WriteCell) + switch c.flag { + case FlagEstimate: + res.depIdx = fk.(int) + res.value = c.data + case FlagDone: + { + res.depIdx = fk.(int) + res.incarnation = c.incarnation + res.value = c.data + } + default: + panic(fmt.Errorf("should not happen - unknown flag value")) + } + } + + return +} + +func ValidateVersion(txIdx int, lastInputOutput *TxnInputOutput, versionedData *MVHashMap) (valid bool) { + valid = true + + for _, rd := range lastInputOutput.readSet(txIdx) { + mvResult := versionedData.Read(rd.Path, txIdx) + switch mvResult.Status() { + case MVReadResultDone: + valid = rd.Kind == ReadKindMap && rd.V == Version{ + TxnIndex: mvResult.depIdx, + Incarnation: mvResult.incarnation, + } + case MVReadResultDependency: + valid = false + case MVReadResultNone: + valid = rd.Kind == ReadKindStorage // feels like an assertion? + default: + panic(fmt.Errorf("should not happen - undefined mv read status: %ver", mvResult.Status())) + } + + if !valid { + break + } + } + + return +} diff --git a/core/blockstm/txio.go b/core/blockstm/txio.go new file mode 100644 index 0000000000..4325277c1d --- /dev/null +++ b/core/blockstm/txio.go @@ -0,0 +1,75 @@ +//nolint: unused +package blockstm + +import "encoding/base64" + +const ( + ReadKindMap = 0 + ReadKindStorage = 1 +) + +type ReadDescriptor struct { + Path []byte + Kind int + V Version +} + +type WriteDescriptor struct { + Path []byte + V Version + Val interface{} +} + +type TxnInput []ReadDescriptor +type TxnOutput []WriteDescriptor + +// hasNewWrite: returns true if the current set has a new write compared to the input +func (txo TxnOutput) hasNewWrite(cmpSet []WriteDescriptor) bool { + if len(txo) == 0 { + return false + } else if len(cmpSet) == 0 || len(txo) > len(cmpSet) { + return true + } + + cmpMap := map[string]bool{base64.StdEncoding.EncodeToString(cmpSet[0].Path): true} + + for i := 1; i < len(cmpSet); i++ { + cmpMap[base64.StdEncoding.EncodeToString(cmpSet[i].Path)] = true + } + + for _, v := range txo { + if !cmpMap[base64.StdEncoding.EncodeToString(v.Path)] { + return true + } + } + + return false +} + +type TxnInputOutput struct { + inputs []TxnInput + outputs []TxnOutput +} + +func (io *TxnInputOutput) readSet(txnIdx int) []ReadDescriptor { + return io.inputs[txnIdx] +} + +func (io *TxnInputOutput) writeSet(txnIdx int) []WriteDescriptor { + return io.outputs[txnIdx] +} + +func MakeTxnInputOutput(numTx int) *TxnInputOutput { + return &TxnInputOutput{ + inputs: make([]TxnInput, numTx), + outputs: make([]TxnOutput, numTx), + } +} + +func (io *TxnInputOutput) recordRead(txId int, input []ReadDescriptor) { + io.inputs[txId] = input +} + +func (io *TxnInputOutput) recordWrite(txId int, output []WriteDescriptor) { + io.outputs[txId] = output +} diff --git a/core/state/journal.go b/core/state/journal.go index 57a692dc7f..57393cbcf4 100644 --- a/core/state/journal.go +++ b/core/state/journal.go @@ -143,6 +143,7 @@ type ( func (ch createObjectChange) revert(s *StateDB) { delete(s.stateObjects, *ch.account) delete(s.stateObjectsDirty, *ch.account) + MVWrite(s, ch.account.Bytes()) } func (ch createObjectChange) dirtied() *common.Address { @@ -151,6 +152,7 @@ func (ch createObjectChange) dirtied() *common.Address { func (ch resetObjectChange) revert(s *StateDB) { s.setStateObject(ch.prev) + MVWrite(s, ch.prev.address.Bytes()) if !ch.prevdestruct && s.snap != nil { delete(s.snapDestructs, ch.prev.addrHash) } @@ -165,6 +167,8 @@ func (ch suicideChange) revert(s *StateDB) { if obj != nil { obj.suicided = ch.prev obj.setBalance(ch.prevbalance) + MVWrite(s, subPath(ch.account.Bytes(), suicidePath)) + MVWrite(s, subPath(ch.account.Bytes(), balancePath)) } } @@ -183,6 +187,7 @@ func (ch touchChange) dirtied() *common.Address { func (ch balanceChange) revert(s *StateDB) { s.getStateObject(*ch.account).setBalance(ch.prev) + MVWrite(s, subPath(ch.account.Bytes(), balancePath)) } func (ch balanceChange) dirtied() *common.Address { @@ -191,6 +196,7 @@ func (ch balanceChange) dirtied() *common.Address { func (ch nonceChange) revert(s *StateDB) { s.getStateObject(*ch.account).setNonce(ch.prev) + MVWrite(s, subPath(ch.account.Bytes(), noncePath)) } func (ch nonceChange) dirtied() *common.Address { @@ -199,6 +205,7 @@ func (ch nonceChange) dirtied() *common.Address { func (ch codeChange) revert(s *StateDB) { s.getStateObject(*ch.account).setCode(common.BytesToHash(ch.prevhash), ch.prevcode) + MVWrite(s, subPath(ch.account.Bytes(), codePath)) } func (ch codeChange) dirtied() *common.Address { @@ -207,6 +214,7 @@ func (ch codeChange) dirtied() *common.Address { func (ch storageChange) revert(s *StateDB) { s.getStateObject(*ch.account).setState(ch.key, ch.prevalue) + MVWrite(s, append(ch.account.Bytes(), ch.key.Bytes()...)) } func (ch storageChange) dirtied() *common.Address { diff --git a/core/state/statedb.go b/core/state/statedb.go index c236a79b5a..2d6250994c 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -25,6 +25,7 @@ import ( "time" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/blockstm" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/state/snapshot" "github.com/ethereum/go-ethereum/core/types" @@ -79,6 +80,12 @@ 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 + // Block-stm related fields + mvHashmap *blockstm.MVHashMap + incarnation int + readMap map[string]blockstm.ReadDescriptor + writeMap map[string]blockstm.WriteDescriptor + // 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 @@ -154,6 +161,159 @@ func New(root common.Hash, db Database, snaps *snapshot.Tree) (*StateDB, error) return sdb, nil } +func NewWithMVHashmap(root common.Hash, db Database, snaps *snapshot.Tree, mvhm *blockstm.MVHashMap) (*StateDB, error) { + if sdb, err := New(root, db, snaps); err != nil { + return nil, err + } else { + sdb.mvHashmap = mvhm + return sdb, nil + } +} + +func (sdb *StateDB) SetMVHashmap(mvhm *blockstm.MVHashMap) { + sdb.mvHashmap = mvhm +} + +func (s *StateDB) MVWriteList() []blockstm.WriteDescriptor { + writes := make([]blockstm.WriteDescriptor, 0, len(s.writeMap)) + + for _, v := range s.writeMap { + writes = append(writes, v) + } + + return writes +} + +func (s *StateDB) MVReadList() []blockstm.ReadDescriptor { + reads := make([]blockstm.ReadDescriptor, 0, len(s.readMap)) + + for _, v := range s.readMap { + reads = append(reads, v) + } + + return reads +} + +func (s *StateDB) ensureReadMap() { + if s.readMap == nil { + s.readMap = make(map[string]blockstm.ReadDescriptor) + } +} + +func (s *StateDB) ensureWriteMap() { + if s.writeMap == nil { + s.writeMap = make(map[string]blockstm.WriteDescriptor) + } +} + +func MVRead[T any](s *StateDB, k []byte, defaultV T, readStorage func(s *StateDB) T) (v T) { + if s.mvHashmap == nil { + return readStorage(s) + } + + s.ensureReadMap() + + if s.writeMap != nil { + if _, ok := s.writeMap[string(k)]; ok { + return readStorage(s) + } + } + + res := s.mvHashmap.Read(k, s.txIndex) + + var rd blockstm.ReadDescriptor + + rd.V = blockstm.Version{ + TxnIndex: res.DepIdx(), + Incarnation: res.Incarnation(), + } + + rd.Path = k + + switch res.Status() { + case blockstm.MVReadResultDone: + { + v = readStorage(res.Value().(*StateDB)) + rd.Kind = blockstm.ReadKindMap + } + case blockstm.MVReadResultDependency: + { + return defaultV + } + case blockstm.MVReadResultNone: + { + v = readStorage(s) + rd.Kind = blockstm.ReadKindStorage + } + default: + return defaultV + } + + mk := string(k) + // TODO: I assume we don't want to overwrite an existing read because this could - for example - change a storage + // read to map if the same value is read multiple times. + if _, ok := s.readMap[mk]; !ok { + s.readMap[mk] = rd + } + + return +} + +func MVWrite(s *StateDB, k []byte) { + if s.mvHashmap != nil { + s.ensureWriteMap() + s.mvHashmap.Write(k, s.Version(), s) + s.writeMap[string(k)] = blockstm.WriteDescriptor{ + Path: k, + V: s.Version(), + Val: s, + } + } +} + +func MVWritten(s *StateDB, k []byte) bool { + if s.mvHashmap == nil || s.writeMap == nil { + return false + } + + _, ok := s.writeMap[string(k)] + + return ok +} + +func (sw *StateDB) ApplyMVWriteSet(writes []blockstm.WriteDescriptor) { + for i := range writes { + path := writes[i].Path + sr := writes[i].Val.(*StateDB) + + keyLength := len(path) + + if keyLength == common.AddressLength { + sw.GetOrNewStateObject(common.BytesToAddress(path)) + } else if keyLength == (common.AddressLength + common.HashLength) { + addr := common.BytesToAddress(path[:common.AddressLength]) + subPath := common.BytesToHash(path[common.AddressLength:]) + sw.SetState(addr, subPath, sr.GetState(addr, subPath)) + } else { + addr := common.BytesToAddress(path[:common.AddressLength]) + switch path[keyLength-1] { + case balancePath: + sw.SetBalance(addr, sr.GetBalance(addr)) + case noncePath: + sw.SetNonce(addr, sr.GetNonce(addr)) + case codePath: + sw.SetCode(addr, sr.GetCode(addr)) + case suicidePath: + if suicided := sr.HasSuicided(addr); suicided { + sw.Suicide(addr) + } + default: + panic(fmt.Errorf("unknown key type: %d", path[keyLength-1])) + } + } + } +} + // 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. @@ -257,22 +417,48 @@ func (s *StateDB) Empty(addr common.Address) bool { return so == nil || so.empty() } +// Create a unique path for special fields (e.g. balance, code) in a state object. +func subPath(prefix []byte, s uint8) []byte { + path := append(prefix, common.Hash{}.Bytes()...) // append a full empty hash to avoid collision with storage state + path = append(path, s) // append the special field identifier + + return path +} + +const balancePath = 1 +const noncePath = 2 +const codePath = 3 +const suicidePath = 4 + // GetBalance retrieves the balance from the given address or 0 if object not found func (s *StateDB) GetBalance(addr common.Address) *big.Int { - stateObject := s.getStateObject(addr) - if stateObject != nil { - return stateObject.Balance() + if s.getStateObject(addr) == nil { + return common.Big0 } - return common.Big0 + + return MVRead(s, subPath(addr.Bytes(), balancePath), common.Big0, func(s *StateDB) *big.Int { + stateObject := s.getStateObject(addr) + if stateObject != nil { + return stateObject.Balance() + } + + return common.Big0 + }) } func (s *StateDB) GetNonce(addr common.Address) uint64 { - stateObject := s.getStateObject(addr) - if stateObject != nil { - return stateObject.Nonce() + if s.getStateObject(addr) == nil { + return 0 } - return 0 + return MVRead(s, subPath(addr.Bytes(), noncePath), 0, func(s *StateDB) uint64 { + stateObject := s.getStateObject(addr) + if stateObject != nil { + return stateObject.Nonce() + } + + return 0 + }) } // TxIndex returns the current transaction index set by Prepare. @@ -280,37 +466,68 @@ func (s *StateDB) TxIndex() int { return s.txIndex } -func (s *StateDB) GetCode(addr common.Address) []byte { - stateObject := s.getStateObject(addr) - if stateObject != nil { - return stateObject.Code(s.db) +func (s *StateDB) Version() blockstm.Version { + return blockstm.Version{ + TxnIndex: s.txIndex, + Incarnation: s.incarnation, } - return nil +} + +func (s *StateDB) GetCode(addr common.Address) []byte { + if s.getStateObject(addr) == nil { + return nil + } + + return MVRead(s, subPath(addr.Bytes(), codePath), nil, func(s *StateDB) []byte { + stateObject := s.getStateObject(addr) + if stateObject != nil { + return stateObject.Code(s.db) + } + return nil + }) } func (s *StateDB) GetCodeSize(addr common.Address) int { - stateObject := s.getStateObject(addr) - if stateObject != nil { - return stateObject.CodeSize(s.db) + if s.getStateObject(addr) == nil { + return 0 } - return 0 + + return MVRead(s, subPath(addr.Bytes(), codePath), 0, func(s *StateDB) int { + stateObject := s.getStateObject(addr) + if stateObject != nil { + return stateObject.CodeSize(s.db) + } + return 0 + }) } func (s *StateDB) GetCodeHash(addr common.Address) common.Hash { - stateObject := s.getStateObject(addr) - if stateObject == nil { + if s.getStateObject(addr) == nil { return common.Hash{} } - return common.BytesToHash(stateObject.CodeHash()) + + return MVRead(s, subPath(addr.Bytes(), codePath), common.Hash{}, func(s *StateDB) common.Hash { + stateObject := s.getStateObject(addr) + if stateObject == nil { + return common.Hash{} + } + return common.BytesToHash(stateObject.CodeHash()) + }) } // GetState retrieves a value from the given account's storage trie. func (s *StateDB) GetState(addr common.Address, hash common.Hash) common.Hash { - stateObject := s.getStateObject(addr) - if stateObject != nil { - return stateObject.GetState(s.db, hash) + if s.getStateObject(addr) == nil { + return common.Hash{} } - return common.Hash{} + + return MVRead(s, append(addr.Bytes(), hash.Bytes()...), common.Hash{}, func(s *StateDB) common.Hash { + stateObject := s.getStateObject(addr) + if stateObject != nil { + return stateObject.GetState(s.db, hash) + } + return common.Hash{} + }) } // GetProof returns the Merkle proof for a given account. @@ -338,11 +555,17 @@ func (s *StateDB) GetStorageProof(a common.Address, key common.Hash) ([][]byte, // GetCommittedState retrieves a value from the given account's committed storage trie. func (s *StateDB) GetCommittedState(addr common.Address, hash common.Hash) common.Hash { - stateObject := s.getStateObject(addr) - if stateObject != nil { - return stateObject.GetCommittedState(s.db, hash) + if s.getStateObject(addr) == nil { + return common.Hash{} } - return common.Hash{} + + return MVRead(s, append(addr.Bytes(), hash.Bytes()...), common.Hash{}, func(s *StateDB) common.Hash { + stateObject := s.getStateObject(addr) + if stateObject != nil { + return stateObject.GetCommittedState(s.db, hash) + } + return common.Hash{} + }) } // Database retrieves the low level database supporting the lower level trie ops. @@ -363,11 +586,17 @@ func (s *StateDB) StorageTrie(addr common.Address) Trie { } func (s *StateDB) HasSuicided(addr common.Address) bool { - stateObject := s.getStateObject(addr) - if stateObject != nil { - return stateObject.suicided + if s.getStateObject(addr) == nil { + return false } - return false + + return MVRead(s, subPath(addr.Bytes(), suicidePath), false, func(s *StateDB) bool { + stateObject := s.getStateObject(addr) + if stateObject != nil { + return stateObject.suicided + } + return false + }) } /* @@ -378,7 +607,9 @@ func (s *StateDB) HasSuicided(addr common.Address) bool { func (s *StateDB) AddBalance(addr common.Address, amount *big.Int) { stateObject := s.GetOrNewStateObject(addr) if stateObject != nil { + stateObject = s.mvRecordWritten(stateObject) stateObject.AddBalance(amount) + MVWrite(s, subPath(addr.Bytes(), balancePath)) } } @@ -386,35 +617,45 @@ func (s *StateDB) AddBalance(addr common.Address, amount *big.Int) { func (s *StateDB) SubBalance(addr common.Address, amount *big.Int) { stateObject := s.GetOrNewStateObject(addr) if stateObject != nil { + stateObject = s.mvRecordWritten(stateObject) stateObject.SubBalance(amount) + MVWrite(s, subPath(addr.Bytes(), balancePath)) } } func (s *StateDB) SetBalance(addr common.Address, amount *big.Int) { stateObject := s.GetOrNewStateObject(addr) if stateObject != nil { + stateObject = s.mvRecordWritten(stateObject) stateObject.SetBalance(amount) + MVWrite(s, subPath(addr.Bytes(), balancePath)) } } func (s *StateDB) SetNonce(addr common.Address, nonce uint64) { stateObject := s.GetOrNewStateObject(addr) if stateObject != nil { + stateObject = s.mvRecordWritten(stateObject) stateObject.SetNonce(nonce) + MVWrite(s, subPath(addr.Bytes(), noncePath)) } } func (s *StateDB) SetCode(addr common.Address, code []byte) { stateObject := s.GetOrNewStateObject(addr) if stateObject != nil { + stateObject = s.mvRecordWritten(stateObject) stateObject.SetCode(crypto.Keccak256Hash(code), code) + MVWrite(s, subPath(addr.Bytes(), codePath)) } } func (s *StateDB) SetState(addr common.Address, key, value common.Hash) { stateObject := s.GetOrNewStateObject(addr) if stateObject != nil { + stateObject = s.mvRecordWritten(stateObject) stateObject.SetState(s.db, key, value) + MVWrite(s, append(addr.Bytes(), key.Bytes()...)) } } @@ -437,6 +678,8 @@ func (s *StateDB) Suicide(addr common.Address) bool { if stateObject == nil { return false } + + stateObject = s.mvRecordWritten(stateObject) s.journal.append(suicideChange{ account: &addr, prev: stateObject.suicided, @@ -445,6 +688,9 @@ func (s *StateDB) Suicide(addr common.Address) bool { stateObject.markSuicided() stateObject.data.Balance = new(big.Int) + MVWrite(s, subPath(addr.Bytes(), suicidePath)) + MVWrite(s, subPath(addr.Bytes(), balancePath)) + return true } @@ -501,60 +747,62 @@ func (s *StateDB) getStateObject(addr common.Address) *stateObject { // 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.stateObjects[addr]; obj != nil { - return obj - } - // If no live objects are available, attempt to use snapshots - var data *types.StateAccount - if s.snap != nil { - start := time.Now() - acc, err := s.snap.Account(crypto.HashData(s.hasher, addr.Bytes())) - if metrics.EnabledExpensive { - s.SnapshotAccountReads += time.Since(start) + return MVRead(s, addr.Bytes(), nil, func(s *StateDB) *stateObject { + // Prefer live objects if any is available + if obj := s.stateObjects[addr]; obj != nil { + return obj } - if err == nil { - if acc == nil { + // If no live objects are available, attempt to use snapshots + var data *types.StateAccount + if s.snap != nil { // nolint + start := time.Now() + acc, err := s.snap.Account(crypto.HashData(s.hasher, addr.Bytes())) + if metrics.EnabledExpensive { + s.SnapshotAccountReads += time.Since(start) + } + if err == nil { + if acc == nil { + return nil + } + data = &types.StateAccount{ + Nonce: acc.Nonce, + Balance: acc.Balance, + CodeHash: acc.CodeHash, + Root: common.BytesToHash(acc.Root), + } + if len(data.CodeHash) == 0 { + data.CodeHash = emptyCodeHash + } + if data.Root == (common.Hash{}) { + data.Root = emptyRoot + } + } + } + // If snapshot unavailable or reading from it failed, load from the database + if data == nil { + start := time.Now() + enc, err := s.trie.TryGet(addr.Bytes()) + if metrics.EnabledExpensive { + s.AccountReads += time.Since(start) + } + if err != nil { + s.setError(fmt.Errorf("getDeleteStateObject (%x) error: %v", addr.Bytes(), err)) return nil } - data = &types.StateAccount{ - Nonce: acc.Nonce, - Balance: acc.Balance, - CodeHash: acc.CodeHash, - Root: common.BytesToHash(acc.Root), + if len(enc) == 0 { + return nil } - if len(data.CodeHash) == 0 { - data.CodeHash = emptyCodeHash - } - if data.Root == (common.Hash{}) { - data.Root = emptyRoot + data = new(types.StateAccount) + if err := rlp.DecodeBytes(enc, data); err != nil { + log.Error("Failed to decode state object", "addr", addr, "err", err) + return nil } } - } - // If snapshot unavailable or reading from it failed, load from the database - if data == nil { - start := time.Now() - enc, err := s.trie.TryGet(addr.Bytes()) - if metrics.EnabledExpensive { - s.AccountReads += time.Since(start) - } - if err != nil { - s.setError(fmt.Errorf("getDeleteStateObject (%x) error: %v", addr.Bytes(), err)) - return nil - } - if len(enc) == 0 { - return nil - } - data = new(types.StateAccount) - if err := rlp.DecodeBytes(enc, data); err != nil { - log.Error("Failed to decode state object", "addr", addr, "err", err) - return nil - } - } - // Insert into the live set - obj := newObject(s, addr, *data) - s.setStateObject(obj) - return obj + // Insert into the live set + obj := newObject(s, addr, *data) + s.setStateObject(obj) + return obj + }) } func (s *StateDB) setStateObject(object *stateObject) { @@ -570,6 +818,28 @@ func (s *StateDB) GetOrNewStateObject(addr common.Address) *stateObject { return stateObject } +// mvRecordWritten checks whether a state object is already present in the current MV writeMap. +// If yes, it returns the object directly. +// If not, it clones the object and inserts it into the writeMap before returning it. +func (s *StateDB) mvRecordWritten(object *stateObject) *stateObject { + if s.mvHashmap == nil { + return object + } + + addrPath := object.Address().Bytes() + + if MVWritten(s, addrPath) { + return object + } + + // Deepcopy is needed to ensure that objects are not written by multiple transactions at the same time, because + // the input state object can come from a different transaction. + s.setStateObject(object.deepCopy(s)) + MVWrite(s, addrPath) + + return s.stateObjects[object.Address()] +} + // 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) { @@ -589,6 +859,7 @@ func (s *StateDB) createObject(addr common.Address) (newobj, prev *stateObject) s.journal.append(resetObjectChange{prev: prev, prevdestruct: prevdestruct}) } s.setStateObject(newobj) + MVWrite(s, addr.Bytes()) if prev != nil && !prev.deleted { return newobj, prev } @@ -609,6 +880,7 @@ func (s *StateDB) CreateAccount(addr common.Address) { newObj, prev := s.createObject(addr) if prev != nil { newObj.setBalance(prev.data.Balance) + MVWrite(s, subPath(addr.Bytes(), balancePath)) } } @@ -738,6 +1010,10 @@ func (s *StateDB) Copy() *StateDB { state.snapStorage[k] = temp } } + + if s.mvHashmap != nil { + state.mvHashmap = s.mvHashmap + } return state } diff --git a/core/state/statedb_test.go b/core/state/statedb_test.go index e9576d4dc4..c1f9f04812 100644 --- a/core/state/statedb_test.go +++ b/core/state/statedb_test.go @@ -29,7 +29,10 @@ import ( "testing" "testing/quick" + "github.com/stretchr/testify/assert" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/blockstm" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/types" ) @@ -488,6 +491,427 @@ func TestTouchDelete(t *testing.T) { } } +func TestMVHashMapReadWriteDelete(t *testing.T) { + t.Parallel() + + db := NewDatabase(rawdb.NewMemoryDatabase()) + mvhm := blockstm.MakeMVHashMap() + s, _ := NewWithMVHashmap(common.Hash{}, db, nil, mvhm) + + states := []*StateDB{s} + + // Create copies of the original state for each transition + for i := 1; i <= 4; i++ { + sCopy := s.Copy() + sCopy.txIndex = i + states = append(states, sCopy) + } + + addr := common.HexToAddress("0x01") + key := common.HexToHash("0x01") + val := common.HexToHash("0x01") + balance := new(big.Int).SetUint64(uint64(100)) + + // Tx0 read + v := states[0].GetState(addr, key) + + assert.Equal(t, common.Hash{}, v) + + // Tx1 write + states[1].GetOrNewStateObject(addr) + states[1].SetState(addr, key, val) + states[1].SetBalance(addr, balance) + + // Tx1 read + v = states[2].GetState(addr, key) + b := states[2].GetBalance(addr) + + assert.Equal(t, val, v) + assert.Equal(t, balance, b) + + // Tx2 read + v = states[2].GetState(addr, key) + b = states[2].GetBalance(addr) + + assert.Equal(t, val, v) + assert.Equal(t, balance, b) + + // Tx3 delete + states[3].Suicide(addr) + + // Within Tx 3, the state should not change before finalize + v = states[3].GetState(addr, key) + assert.Equal(t, val, v) + + // After finalizing Tx 3, the state will change + states[3].Finalise(false) + v = states[3].GetState(addr, key) + assert.Equal(t, common.Hash{}, v) + + // Tx4 read + v = states[4].GetState(addr, key) + b = states[4].GetBalance(addr) + + assert.Equal(t, common.Hash{}, v) + assert.Equal(t, common.Big0, b) +} + +func TestMVHashMapRevert(t *testing.T) { + t.Parallel() + + db := NewDatabase(rawdb.NewMemoryDatabase()) + mvhm := blockstm.MakeMVHashMap() + s, _ := NewWithMVHashmap(common.Hash{}, db, nil, mvhm) + + states := []*StateDB{s} + + // Create copies of the original state for each transition + for i := 1; i <= 4; i++ { + sCopy := s.Copy() + sCopy.txIndex = i + states = append(states, sCopy) + } + + addr := common.HexToAddress("0x01") + key := common.HexToHash("0x01") + val := common.HexToHash("0x01") + balance := new(big.Int).SetUint64(uint64(100)) + + // Tx0 write + states[0].GetOrNewStateObject(addr) + states[0].SetState(addr, key, val) + states[0].SetBalance(addr, balance) + + // Tx1 perform some ops and then revert + snapshot := states[1].Snapshot() + states[1].AddBalance(addr, new(big.Int).SetUint64(uint64(100))) + states[1].SetState(addr, key, common.HexToHash("0x02")) + v := states[1].GetState(addr, key) + b := states[1].GetBalance(addr) + assert.Equal(t, new(big.Int).SetUint64(uint64(200)), b) + assert.Equal(t, common.HexToHash("0x02"), v) + + states[1].Suicide(addr) + + states[1].RevertToSnapshot(snapshot) + + v = states[1].GetState(addr, key) + b = states[1].GetBalance(addr) + + assert.Equal(t, val, v) + assert.Equal(t, balance, b) + states[1].Finalise(false) + + // Tx2 check the state and balance + v = states[2].GetState(addr, key) + b = states[2].GetBalance(addr) + + assert.Equal(t, val, v) + assert.Equal(t, balance, b) +} + +func TestMVHashMapMarkEstimate(t *testing.T) { + t.Parallel() + + db := NewDatabase(rawdb.NewMemoryDatabase()) + mvhm := blockstm.MakeMVHashMap() + s, _ := NewWithMVHashmap(common.Hash{}, db, nil, mvhm) + + states := []*StateDB{s} + + // Create copies of the original state for each transition + for i := 1; i <= 4; i++ { + sCopy := s.Copy() + sCopy.txIndex = i + states = append(states, sCopy) + } + + addr := common.HexToAddress("0x01") + key := common.HexToHash("0x01") + val := common.HexToHash("0x01") + balance := new(big.Int).SetUint64(uint64(100)) + + // Tx0 read + v := states[0].GetState(addr, key) + assert.Equal(t, common.Hash{}, v) + + // Tx0 write + states[0].SetState(addr, key, val) + v = states[0].GetState(addr, key) + assert.Equal(t, val, v) + + // Tx1 write + states[1].GetOrNewStateObject(addr) + states[1].SetState(addr, key, val) + states[1].SetBalance(addr, balance) + + // Tx2 read + v = states[2].GetState(addr, key) + b := states[2].GetBalance(addr) + + assert.Equal(t, val, v) + assert.Equal(t, balance, b) + + // Tx1 mark estimate + for _, v := range states[1].writeMap { + mvhm.MarkEstimate(v.Path, 1) + } + + // Tx2 read again should get default (empty) vals because its dependency Tx1 is marked as estimate + v = states[2].GetState(addr, key) + b = states[2].GetBalance(addr) + + assert.Equal(t, common.Hash{}, v) + assert.Equal(t, common.Big0, b) + + // Tx1 read again should get Tx0 vals + v = states[1].GetState(addr, key) + assert.Equal(t, val, v) +} + +func TestMVHashMapOverwrite(t *testing.T) { + t.Parallel() + + db := NewDatabase(rawdb.NewMemoryDatabase()) + mvhm := blockstm.MakeMVHashMap() + s, _ := NewWithMVHashmap(common.Hash{}, db, nil, mvhm) + + states := []*StateDB{s} + + // Create copies of the original state for each transition + for i := 1; i <= 4; i++ { + sCopy := s.Copy() + sCopy.txIndex = i + states = append(states, sCopy) + } + + addr := common.HexToAddress("0x01") + key := common.HexToHash("0x01") + val1 := common.HexToHash("0x01") + balance1 := new(big.Int).SetUint64(uint64(100)) + val2 := common.HexToHash("0x02") + balance2 := new(big.Int).SetUint64(uint64(200)) + + // Tx0 write + states[0].GetOrNewStateObject(addr) + states[0].SetState(addr, key, val1) + states[0].SetBalance(addr, balance1) + + // Tx1 write + states[1].SetState(addr, key, val2) + states[1].SetBalance(addr, balance2) + v := states[1].GetState(addr, key) + b := states[1].GetBalance(addr) + + assert.Equal(t, val2, v) + assert.Equal(t, balance2, b) + + // Tx2 read should get Tx1's value + v = states[2].GetState(addr, key) + b = states[2].GetBalance(addr) + + assert.Equal(t, val2, v) + assert.Equal(t, balance2, b) + + // Tx1 delete + for _, v := range states[1].writeMap { + mvhm.Delete(v.Path, 1) + + states[1].writeMap = nil + } + + // Tx2 read should get Tx0's value + v = states[2].GetState(addr, key) + b = states[2].GetBalance(addr) + + assert.Equal(t, val1, v) + assert.Equal(t, balance1, b) + + // Tx1 read should get Tx0's value + v = states[1].GetState(addr, key) + b = states[1].GetBalance(addr) + + assert.Equal(t, val1, v) + assert.Equal(t, balance1, b) + + // Tx0 delete + for _, v := range states[0].writeMap { + mvhm.Delete(v.Path, 0) + + states[0].writeMap = nil + } + + // Tx2 read again should get default vals + v = states[2].GetState(addr, key) + b = states[2].GetBalance(addr) + + assert.Equal(t, common.Hash{}, v) + assert.Equal(t, common.Big0, b) +} + +func TestMVHashMapWriteNoConflict(t *testing.T) { + t.Parallel() + + db := NewDatabase(rawdb.NewMemoryDatabase()) + mvhm := blockstm.MakeMVHashMap() + s, _ := NewWithMVHashmap(common.Hash{}, db, nil, mvhm) + + states := []*StateDB{s} + + // Create copies of the original state for each transition + for i := 1; i <= 4; i++ { + sCopy := s.Copy() + sCopy.txIndex = i + states = append(states, sCopy) + } + + addr := common.HexToAddress("0x01") + key1 := common.HexToHash("0x01") + key2 := common.HexToHash("0x02") + val1 := common.HexToHash("0x01") + balance1 := new(big.Int).SetUint64(uint64(100)) + val2 := common.HexToHash("0x02") + + // Tx0 write + states[0].GetOrNewStateObject(addr) + + // Tx2 write + states[2].SetState(addr, key2, val2) + + // Tx1 write + tx1Snapshot := states[1].Snapshot() + states[1].SetState(addr, key1, val1) + states[1].SetBalance(addr, balance1) + + // Tx1 read + assert.Equal(t, val1, states[1].GetState(addr, key1)) + assert.Equal(t, balance1, states[1].GetBalance(addr)) + // Tx1 should see empty value in key2 + assert.Equal(t, common.Hash{}, states[1].GetState(addr, key2)) + + // Tx2 read + assert.Equal(t, val2, states[2].GetState(addr, key2)) + // Tx2 should see values written by Tx1 + assert.Equal(t, val1, states[2].GetState(addr, key1)) + assert.Equal(t, balance1, states[2].GetBalance(addr)) + + // Tx3 read + assert.Equal(t, val1, states[3].GetState(addr, key1)) + assert.Equal(t, val2, states[3].GetState(addr, key2)) + assert.Equal(t, balance1, states[3].GetBalance(addr)) + + // Tx2 delete + for _, v := range states[2].writeMap { + mvhm.Delete(v.Path, 2) + + states[2].writeMap = nil + } + + assert.Equal(t, val1, states[3].GetState(addr, key1)) + assert.Equal(t, balance1, states[3].GetBalance(addr)) + assert.Equal(t, common.Hash{}, states[3].GetState(addr, key2)) + + // Tx1 revert + states[1].RevertToSnapshot(tx1Snapshot) + + assert.Equal(t, common.Hash{}, states[3].GetState(addr, key1)) + assert.Equal(t, common.Hash{}, states[3].GetState(addr, key2)) + assert.Equal(t, common.Big0, states[3].GetBalance(addr)) + + // Tx1 delete + for _, v := range states[1].writeMap { + mvhm.Delete(v.Path, 1) + + states[1].writeMap = nil + } + + assert.Equal(t, common.Hash{}, states[3].GetState(addr, key1)) + assert.Equal(t, common.Hash{}, states[3].GetState(addr, key2)) + assert.Equal(t, common.Big0, states[3].GetBalance(addr)) +} + +func TestApplyMVWriteSet(t *testing.T) { + t.Parallel() + + db := NewDatabase(rawdb.NewMemoryDatabase()) + mvhm := blockstm.MakeMVHashMap() + s, _ := NewWithMVHashmap(common.Hash{}, db, nil, mvhm) + + sClean := s.Copy() + sClean.mvHashmap = nil + + sSingleProcess := sClean.Copy() + + states := []*StateDB{s} + + // Create copies of the original state for each transition + for i := 1; i <= 4; i++ { + sCopy := s.Copy() + sCopy.txIndex = i + states = append(states, sCopy) + } + + addr1 := common.HexToAddress("0x01") + addr2 := common.HexToAddress("0x02") + key1 := common.HexToHash("0x01") + key2 := common.HexToHash("0x02") + val1 := common.HexToHash("0x01") + balance1 := new(big.Int).SetUint64(uint64(100)) + val2 := common.HexToHash("0x02") + balance2 := new(big.Int).SetUint64(uint64(200)) + code := []byte{1, 2, 3} + + // Tx0 write + states[0].SetState(addr1, key1, val1) + states[0].SetBalance(addr1, balance1) + states[0].SetState(addr2, key2, val2) + + sSingleProcess.SetState(addr1, key1, val1) + sSingleProcess.SetBalance(addr1, balance1) + sSingleProcess.SetState(addr2, key2, val2) + + sClean.ApplyMVWriteSet(states[0].MVWriteList()) + + assert.Equal(t, sSingleProcess.IntermediateRoot(false), sClean.IntermediateRoot(false)) + + // Tx1 write + states[1].SetState(addr1, key2, val2) + states[1].SetBalance(addr1, balance2) + states[1].SetNonce(addr1, 1) + + sSingleProcess.SetState(addr1, key2, val2) + sSingleProcess.SetBalance(addr1, balance2) + sSingleProcess.SetNonce(addr1, 1) + + sClean.ApplyMVWriteSet(states[1].MVWriteList()) + + assert.Equal(t, sSingleProcess.IntermediateRoot(false), sClean.IntermediateRoot(false)) + + // Tx2 write + states[2].SetState(addr1, key1, val2) + states[2].SetBalance(addr1, balance2) + states[2].SetNonce(addr1, 2) + + sSingleProcess.SetState(addr1, key1, val2) + sSingleProcess.SetBalance(addr1, balance2) + sSingleProcess.SetNonce(addr1, 2) + + sClean.ApplyMVWriteSet(states[2].MVWriteList()) + + assert.Equal(t, sSingleProcess.IntermediateRoot(false), sClean.IntermediateRoot(false)) + + // Tx3 write + states[3].Suicide(addr2) + states[3].SetCode(addr1, code) + + sSingleProcess.Suicide(addr2) + sSingleProcess.SetCode(addr1, code) + + sClean.ApplyMVWriteSet(states[3].MVWriteList()) + + assert.Equal(t, sSingleProcess.IntermediateRoot(false), sClean.IntermediateRoot(false)) +} + // TestCopyOfCopy tests that modified objects are carried over to the copy, and the copy of the copy. // See https://github.com/ethereum/go-ethereum/pull/15225#issuecomment-380191512 func TestCopyOfCopy(t *testing.T) { diff --git a/go.mod b/go.mod index 7a643a251c..fa21583ce2 100644 --- a/go.mod +++ b/go.mod @@ -105,6 +105,8 @@ require ( github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect github.com/deepmap/oapi-codegen v1.8.2 // indirect github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91 // indirect + github.com/emirpasic/gods v1.18.1 + github.com/go-kit/kit v0.9.0 // indirect github.com/go-logfmt/logfmt v0.5.0 // indirect github.com/go-ole/go-ole v1.2.1 // indirect github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect diff --git a/go.sum b/go.sum index dc419821c6..6d28e061ef 100644 --- a/go.sum +++ b/go.sum @@ -144,6 +144,8 @@ github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA github.com/eclipse/paho.mqtt.golang v1.2.0/go.mod h1:H9keYFcgq3Qr5OUJm/JZI/i6U7joQ8SYLhZwfeOo6Ts= github.com/edsrzf/mmap-go v1.0.0 h1:CEBF7HpRnUCSJgGUb5h1Gm7e3VkmVDrR8lvWVLtrOFw= github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= From bbcc6dd0ad4ac75241eb429ebe46e33ddb2b4878 Mon Sep 17 00:00:00 2001 From: Jerry Date: Sat, 16 Jul 2022 16:54:02 -0700 Subject: [PATCH 02/38] Parallel state processor --- core/blockstm/executor.go | 241 +++++++++++++++++++++++++++++++ core/blockstm/mvhashmap.go | 12 +- core/blockstm/status.go | 163 +++++++++++++++++++++ core/blockstm/txio.go | 22 ++- core/parallel_state_processor.go | 231 +++++++++++++++++++++++++++++ core/state/statedb.go | 62 +++++++- core/state/statedb_test.go | 25 ++++ 7 files changed, 742 insertions(+), 14 deletions(-) create mode 100644 core/blockstm/executor.go create mode 100644 core/blockstm/status.go create mode 100644 core/parallel_state_processor.go diff --git a/core/blockstm/executor.go b/core/blockstm/executor.go new file mode 100644 index 0000000000..cb5019bbb2 --- /dev/null +++ b/core/blockstm/executor.go @@ -0,0 +1,241 @@ +package blockstm + +import ( + "fmt" + + "github.com/ethereum/go-ethereum/log" +) + +type ExecResult struct { + err error + ver Version + txIn TxnInput + txOut TxnOutput + txAllOut TxnOutput +} + +type ExecTask interface { + Execute(mvh *MVHashMap, incarnation int) error + MVReadList() []ReadDescriptor + MVWriteList() []WriteDescriptor + MVFullWriteList() []WriteDescriptor +} + +type ExecVersionView struct { + ver Version + et ExecTask + mvh *MVHashMap +} + +func (ev *ExecVersionView) Execute() (er ExecResult) { + er.ver = ev.ver + if er.err = ev.et.Execute(ev.mvh, ev.ver.Incarnation); er.err != nil { + log.Debug("blockstm executed task failed", "Tx index", ev.ver.TxnIndex, "incarnation", ev.ver.Incarnation, "err", er.err) + return + } + + er.txIn = ev.et.MVReadList() + er.txOut = ev.et.MVWriteList() + er.txAllOut = ev.et.MVFullWriteList() + log.Debug("blockstm executed task", "Tx index", ev.ver.TxnIndex, "incarnation", ev.ver.Incarnation, "err", er.err) + + return +} + +var ErrExecAbort = fmt.Errorf("execution aborted with dependency") + +const numGoProcs = 4 + +// nolint: gocognit +func ExecuteParallel(tasks []ExecTask) (lastTxIO *TxnInputOutput, err error) { + if len(tasks) == 0 { + return MakeTxnInputOutput(len(tasks)), nil + } + + chTasks := make(chan ExecVersionView, len(tasks)) + chResults := make(chan ExecResult, len(tasks)) + chDone := make(chan bool) + + var cntExec, cntSuccess, cntAbort, cntTotalValidations, cntValidationFail int + + for i := 0; i < numGoProcs; i++ { + go func(procNum int, t chan ExecVersionView) { + Loop: + for { + select { + case task := <-t: + { + res := task.Execute() + chResults <- res + } + case <-chDone: + break Loop + } + } + log.Debug("blockstm", "proc done", procNum) // TODO: logging ... + }(i, chTasks) + } + + mvh := MakeMVHashMap() + + execTasks := makeStatusManager(len(tasks)) + validateTasks := makeStatusManager(0) + + // bootstrap execution + for x := 0; x < numGoProcs; x++ { + tx := execTasks.takeNextPending() + if tx != -1 { + cntExec++ + + log.Debug("blockstm", "bootstrap: proc", x, "executing task", tx) + chTasks <- ExecVersionView{ver: Version{tx, 0}, et: tasks[tx], mvh: mvh} + } + } + + lastTxIO = MakeTxnInputOutput(len(tasks)) + txIncarnations := make([]int, len(tasks)) + + diagExecSuccess := make([]int, len(tasks)) + diagExecAbort := make([]int, len(tasks)) + + for { + res := <-chResults + switch res.err { + case nil: + { + mvh.FlushMVWriteSet(res.txAllOut) + lastTxIO.recordRead(res.ver.TxnIndex, res.txIn) + if res.ver.Incarnation == 0 { + lastTxIO.recordWrite(res.ver.TxnIndex, res.txOut) + lastTxIO.recordAllWrite(res.ver.TxnIndex, res.txAllOut) + } else { + if res.txAllOut.hasNewWrite(lastTxIO.AllWriteSet(res.ver.TxnIndex)) { + log.Debug("blockstm", "Revalidate completed txs greater than current tx: ", res.ver.TxnIndex) + validateTasks.pushPendingSet(execTasks.getRevalidationRange(res.ver.TxnIndex)) + } + + prevWrite := lastTxIO.AllWriteSet(res.ver.TxnIndex) + + // Remove entries that were previously written but are no longer written + + cmpMap := make(map[string]bool) + + for _, w := range res.txAllOut { + cmpMap[string(w.Path)] = true + } + + for _, v := range prevWrite { + if _, ok := cmpMap[string(v.Path)]; !ok { + mvh.Delete(v.Path, res.ver.TxnIndex) + } + } + + lastTxIO.recordWrite(res.ver.TxnIndex, res.txOut) + lastTxIO.recordAllWrite(res.ver.TxnIndex, res.txAllOut) + } + validateTasks.pushPending(res.ver.TxnIndex) + execTasks.markComplete(res.ver.TxnIndex) + if diagExecSuccess[res.ver.TxnIndex] > 0 && diagExecAbort[res.ver.TxnIndex] == 0 { + log.Debug("blockstm", "got multiple successful execution w/o abort?", "Tx", res.ver.TxnIndex, "incarnation", res.ver.Incarnation) + } + diagExecSuccess[res.ver.TxnIndex]++ + cntSuccess++ + } + case ErrExecAbort: + { + // bit of a subtle / tricky bug here. this adds the tx back to pending ... + execTasks.revertInProgress(res.ver.TxnIndex) + // ... but the incarnation needs to be bumped + txIncarnations[res.ver.TxnIndex]++ + diagExecAbort[res.ver.TxnIndex]++ + cntAbort++ + } + default: + { + err = res.err + break + } + } + + // if we got more work, queue one up... + nextTx := execTasks.takeNextPending() + if nextTx != -1 { + cntExec++ + chTasks <- ExecVersionView{ver: Version{nextTx, txIncarnations[nextTx]}, et: tasks[nextTx], mvh: mvh} + } + + // do validations ... + maxComplete := execTasks.maxAllComplete() + + const validationIncrement = 2 + + cntValidate := validateTasks.countPending() + // if we're currently done with all execution tasks then let's validate everything; otherwise do one increment ... + if execTasks.countComplete() != len(tasks) && cntValidate > validationIncrement { + cntValidate = validationIncrement + } + + var toValidate []int + + for i := 0; i < cntValidate; i++ { + if validateTasks.minPending() <= maxComplete { + toValidate = append(toValidate, validateTasks.takeNextPending()) + } else { + break + } + } + + for i := 0; i < len(toValidate); i++ { + cntTotalValidations++ + + tx := toValidate[i] + log.Debug("blockstm", "validating task", tx) + + if ValidateVersion(tx, lastTxIO, mvh) { + log.Debug("blockstm", "* completed validation task", tx) + validateTasks.markComplete(tx) + } else { + log.Debug("blockstm", "* validation task FAILED", tx) + cntValidationFail++ + diagExecAbort[tx]++ + for _, v := range lastTxIO.AllWriteSet(tx) { + mvh.MarkEstimate(v.Path, tx) + } + // 'create validation tasks for all transactions > tx ...' + validateTasks.pushPendingSet(execTasks.getRevalidationRange(tx + 1)) + validateTasks.clearInProgress(tx) // clear in progress - pending will be added again once new incarnation executes + if execTasks.checkPending(tx) { + // println() // have to think about this ... + } else { + execTasks.pushPending(tx) + execTasks.clearComplete(tx) + txIncarnations[tx]++ + } + } + } + + // if we didn't queue work previously, do check again so we keep making progress ... + if nextTx == -1 { + nextTx = execTasks.takeNextPending() + if nextTx != -1 { + cntExec++ + + log.Debug("blockstm", "# tx queued up", nextTx) + chTasks <- ExecVersionView{ver: Version{nextTx, txIncarnations[nextTx]}, et: tasks[nextTx], mvh: mvh} + } + } + + if validateTasks.countComplete() == len(tasks) && execTasks.countComplete() == len(tasks) { + log.Debug("blockstm exec summary", "execs", cntExec, "success", cntSuccess, "aborts", cntAbort, "validations", cntTotalValidations, "failures", cntValidationFail) + break + } + } + + for i := 0; i < numGoProcs; i++ { + chDone <- true + } + close(chTasks) + close(chResults) + + return +} diff --git a/core/blockstm/mvhashmap.go b/core/blockstm/mvhashmap.go index ff9f1a6f9d..52a5487b5d 100644 --- a/core/blockstm/mvhashmap.go +++ b/core/blockstm/mvhashmap.go @@ -5,6 +5,8 @@ import ( "sync" "github.com/emirpasic/gods/maps/treemap" + + "github.com/ethereum/go-ethereum/log" ) const FlagDone = 0 @@ -81,7 +83,7 @@ func (mv *MVHashMap) Write(k []byte, v Version, data interface{}) { panic(fmt.Errorf("existing transaction value does not have lower incarnation: %v, %v", string(k), v.TxnIndex)) } else if ci.(*WriteCell).flag == FlagEstimate { - println("marking previous estimate as done tx", v.TxnIndex, v.Incarnation) + log.Debug("mvhashmap marking previous estimate as done", "tx index", v.TxnIndex, "incarnation", v.Incarnation) } ci.(*WriteCell).flag = FlagDone @@ -190,10 +192,16 @@ func (mv *MVHashMap) Read(k []byte, txIdx int) (res MVReadResult) { return } +func (mv *MVHashMap) FlushMVWriteSet(writes []WriteDescriptor) { + for _, v := range writes { + mv.Write(v.Path, v.V, v.Val) + } +} + func ValidateVersion(txIdx int, lastInputOutput *TxnInputOutput, versionedData *MVHashMap) (valid bool) { valid = true - for _, rd := range lastInputOutput.readSet(txIdx) { + for _, rd := range lastInputOutput.ReadSet(txIdx) { mvResult := versionedData.Read(rd.Path, txIdx) switch mvResult.Status() { case MVReadResultDone: diff --git a/core/blockstm/status.go b/core/blockstm/status.go new file mode 100644 index 0000000000..759abf63eb --- /dev/null +++ b/core/blockstm/status.go @@ -0,0 +1,163 @@ +package blockstm + +import ( + "fmt" + "sort" +) + +func makeStatusManager(numTasks int) (t taskStatusManager) { + t.pending = make([]int, numTasks) + for i := 0; i < numTasks; i++ { + t.pending[i] = i + } + + return +} + +type taskStatusManager struct { + pending []int + inProgress []int + complete []int +} + +func insertInList(l []int, v int) []int { + if len(l) == 0 || v > l[len(l)-1] { + return append(l, v) + } else { + x := sort.SearchInts(l, v) + if x < len(l) && l[x] == v { + // already in list + return l + } + a := append(l[:x+1], l[x:]...) + a[x] = v + return a + } +} + +func (m *taskStatusManager) takeNextPending() int { + if len(m.pending) == 0 { + return -1 + } + + x := m.pending[0] + m.pending = m.pending[1:] + m.inProgress = insertInList(m.inProgress, x) + + return x +} + +func hasNoGap(l []int) bool { + return l[0]+len(l) == l[len(l)-1]+1 +} + +func (m taskStatusManager) maxAllComplete() int { + if len(m.complete) == 0 || m.complete[0] != 0 { + return -1 + } else if m.complete[len(m.complete)-1] == len(m.complete)-1 { + return m.complete[len(m.complete)-1] + } else { + for i := len(m.complete) - 2; i >= 0; i-- { + if hasNoGap(m.complete[:i+1]) { + return m.complete[i] + } + } + } + + return -1 +} + +func (m *taskStatusManager) pushPending(tx int) { + m.pending = insertInList(m.pending, tx) +} + +func removeFromList(l []int, v int, expect bool) []int { + x := sort.SearchInts(l, v) + if x == -1 || l[x] != v { + if expect { + panic(fmt.Errorf("should not happen - element expected in list")) + } + + return l + } + + switch x { + case 0: + return l[1:] + case len(l) - 1: + return l[:len(l)-1] + default: + return append(l[:x], l[x+1:]...) + } +} + +func (m *taskStatusManager) markComplete(tx int) { + m.inProgress = removeFromList(m.inProgress, tx, true) + m.complete = insertInList(m.complete, tx) +} + +func (m *taskStatusManager) minPending() int { + if len(m.pending) == 0 { + return -1 + } else { + return m.pending[0] + } +} + +func (m *taskStatusManager) countComplete() int { + return len(m.complete) +} + +func (m *taskStatusManager) revertInProgress(tx int) { + m.inProgress = removeFromList(m.inProgress, tx, true) + m.pending = insertInList(m.pending, tx) +} + +func (m *taskStatusManager) clearInProgress(tx int) { + m.inProgress = removeFromList(m.inProgress, tx, true) +} + +func (m *taskStatusManager) countPending() int { + return len(m.pending) +} + +func (m *taskStatusManager) checkInProgress(tx int) bool { + x := sort.SearchInts(m.inProgress, tx) + if x < len(m.inProgress) && m.inProgress[x] == tx { + return true + } + + return false +} + +func (m *taskStatusManager) checkPending(tx int) bool { + x := sort.SearchInts(m.pending, tx) + if x < len(m.pending) && m.pending[x] == tx { + return true + } + + return false +} + +// getRevalidationRange: this range will be all tasks from tx (inclusive) that are not currently in progress up to the +// 'all complete' limit +func (m *taskStatusManager) getRevalidationRange(txFrom int) (ret []int) { + max := m.maxAllComplete() // haven't learned to trust compilers :) + for x := txFrom; x <= max; x++ { + if !m.checkInProgress(x) { + ret = append(ret, x) + } + } + + return +} + +func (m *taskStatusManager) pushPendingSet(set []int) { + for _, v := range set { + m.pushPending(v) + } +} + +func (m *taskStatusManager) clearComplete(tx int) { + m.complete = removeFromList(m.complete, tx, false) +} diff --git a/core/blockstm/txio.go b/core/blockstm/txio.go index 4325277c1d..7716197acd 100644 --- a/core/blockstm/txio.go +++ b/core/blockstm/txio.go @@ -47,22 +47,28 @@ func (txo TxnOutput) hasNewWrite(cmpSet []WriteDescriptor) bool { } type TxnInputOutput struct { - inputs []TxnInput - outputs []TxnOutput + inputs []TxnInput + outputs []TxnOutput // write sets that should be checked during validation + allOutputs []TxnOutput // entire write sets in MVHashMap. allOutputs should always be a parent set of outputs } -func (io *TxnInputOutput) readSet(txnIdx int) []ReadDescriptor { +func (io *TxnInputOutput) ReadSet(txnIdx int) []ReadDescriptor { return io.inputs[txnIdx] } -func (io *TxnInputOutput) writeSet(txnIdx int) []WriteDescriptor { +func (io *TxnInputOutput) WriteSet(txnIdx int) []WriteDescriptor { return io.outputs[txnIdx] } +func (io *TxnInputOutput) AllWriteSet(txnIdx int) []WriteDescriptor { + return io.allOutputs[txnIdx] +} + func MakeTxnInputOutput(numTx int) *TxnInputOutput { return &TxnInputOutput{ - inputs: make([]TxnInput, numTx), - outputs: make([]TxnOutput, numTx), + inputs: make([]TxnInput, numTx), + outputs: make([]TxnOutput, numTx), + allOutputs: make([]TxnOutput, numTx), } } @@ -73,3 +79,7 @@ func (io *TxnInputOutput) recordRead(txId int, input []ReadDescriptor) { func (io *TxnInputOutput) recordWrite(txId int, output []WriteDescriptor) { io.outputs[txId] = output } + +func (io *TxnInputOutput) recordAllWrite(txId int, output []WriteDescriptor) { + io.allOutputs[txId] = output +} diff --git a/core/parallel_state_processor.go b/core/parallel_state_processor.go new file mode 100644 index 0000000000..c2d6647f65 --- /dev/null +++ b/core/parallel_state_processor.go @@ -0,0 +1,231 @@ +// Copyright 2015 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 core + +import ( + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus" + "github.com/ethereum/go-ethereum/consensus/misc" + "github.com/ethereum/go-ethereum/core/blockstm" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/params" +) + +// StateProcessor is a basic Processor, which takes care of transitioning +// state from one point to another. +// +// StateProcessor implements Processor. +type ParallelStateProcessor struct { + config *params.ChainConfig // Chain configuration options + bc *BlockChain // Canonical block chain + engine consensus.Engine // Consensus engine used for block rewards +} + +// NewStateProcessor initialises a new StateProcessor. +func NewParallelStateProcessor(config *params.ChainConfig, bc *BlockChain, engine consensus.Engine) *ParallelStateProcessor { + return &ParallelStateProcessor{ + config: config, + bc: bc, + engine: engine, + } +} + +type ExecutionTask struct { + msg types.Message + config *params.ChainConfig + + gasLimit uint64 + blockNumber *big.Int + blockHash common.Hash + blockContext vm.BlockContext + tx *types.Transaction + index int + statedb *state.StateDB // State database that stores the modified values after tx execution. + cleanStateDB *state.StateDB // A clean copy of the initial statedb. It should not be modified. + evmConfig vm.Config + result *ExecutionResult +} + +func (task *ExecutionTask) Execute(mvh *blockstm.MVHashMap, incarnation int) (err error) { + task.statedb = task.cleanStateDB.Copy() + task.statedb.Prepare(task.tx.Hash(), task.index) + task.statedb.SetMVHashmap(mvh) + task.statedb.SetIncarnation(incarnation) + + evm := vm.NewEVM(task.blockContext, vm.TxContext{}, task.statedb, task.config, task.evmConfig) + + // Create a new context to be used in the EVM environment. + txContext := NewEVMTxContext(task.msg) + evm.Reset(txContext, task.statedb) + + defer func() { + if r := recover(); r != nil { + // In some pre-matured executions, EVM will panic. Recover from panic and retry the execution. + log.Debug("Recovered from EVM failure. Error:\n", r) + + err = blockstm.ErrExecAbort + + return + } + }() + + // Apply the transaction to the current state (included in the env). + result, err := ApplyMessage(evm, task.msg, new(GasPool).AddGas(task.gasLimit)) + + if task.statedb.HadInvalidRead() || err != nil { + err = blockstm.ErrExecAbort + return + } + + task.statedb.Finalise(false) + + task.result = result + + return +} + +func (task *ExecutionTask) MVReadList() []blockstm.ReadDescriptor { + return task.statedb.MVReadList() +} + +func (task *ExecutionTask) MVWriteList() []blockstm.WriteDescriptor { + return task.statedb.MVWriteList() +} + +func (task *ExecutionTask) MVFullWriteList() []blockstm.WriteDescriptor { + return task.statedb.MVFullWriteList() +} + +// 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 *ParallelStateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg vm.Config) (types.Receipts, []*types.Log, uint64, error) { + var ( + receipts types.Receipts + header = block.Header() + blockHash = block.Hash() + blockNumber = block.Number() + allLogs []*types.Log + usedGas = new(uint64) + ) + // 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) + } + + tasks := make([]blockstm.ExecTask, 0, len(block.Transactions())) + + // Iterate over and process the individual transactions + for i, tx := range block.Transactions() { + msg, err := tx.AsMessage(types.MakeSigner(p.config, header.Number), header.BaseFee) + if err != nil { + log.Error("error creating message", "err", err) + return nil, nil, 0, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err) + } + + cleansdb := statedb.Copy() + + task := &ExecutionTask{ + msg: msg, + config: p.config, + gasLimit: block.GasLimit(), + blockNumber: blockNumber, + blockHash: blockHash, + tx: tx, + index: i, + cleanStateDB: cleansdb, + blockContext: NewEVMBlockContext(header, p.bc, nil), + } + + tasks = append(tasks, task) + } + + _, err := blockstm.ExecuteParallel(tasks) + + if err != nil { + log.Error("blockstm error executing block", "err", err) + return nil, nil, 0, err + } + + for _, task := range tasks { + task := task.(*ExecutionTask) + statedb.Prepare(task.tx.Hash(), task.index) + statedb.ApplyMVWriteSet(task.MVWriteList()) + + for _, l := range task.statedb.GetLogs(task.tx.Hash(), blockHash) { + statedb.AddLog(l) + } + + for k, v := range task.statedb.Preimages() { + statedb.AddPreimage(k, v) + } + + // Update the state with pending changes. + var root []byte + + if p.config.IsByzantium(blockNumber) { + statedb.Finalise(true) + } else { + root = statedb.IntermediateRoot(p.config.IsEIP158(blockNumber)).Bytes() + } + + *usedGas += task.result.UsedGas + + // Create a new receipt for the transaction, storing the intermediate root and gas used + // by the tx. + receipt := &types.Receipt{Type: task.tx.Type(), PostState: root, CumulativeGasUsed: *usedGas} + if task.result.Failed() { + receipt.Status = types.ReceiptStatusFailed + } else { + receipt.Status = types.ReceiptStatusSuccessful + } + + receipt.TxHash = task.tx.Hash() + receipt.GasUsed = task.result.UsedGas + + // If the transaction created a contract, store the creation address in the receipt. + if task.msg.To() == nil { + receipt.ContractAddress = crypto.CreateAddress(task.msg.From(), task.tx.Nonce()) + } + + // Set the receipt logs and create the bloom filter. + receipt.Logs = statedb.GetLogs(task.tx.Hash(), blockHash) + receipt.Bloom = types.CreateBloom(types.Receipts{receipt}) + receipt.BlockHash = blockHash + receipt.BlockNumber = blockNumber + receipt.TransactionIndex = uint(statedb.TxIndex()) + + receipts = append(receipts, receipt) + allLogs = append(allLogs, receipt.Logs...) + } + + // Finalize the block, applying any consensus engine specific extras (e.g. block rewards) + p.engine.Finalize(p.bc, header, statedb, block.Transactions(), block.Uncles()) + + return receipts, allLogs, *usedGas, nil +} diff --git a/core/state/statedb.go b/core/state/statedb.go index 2d6250994c..1b7af5dda3 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -81,10 +81,12 @@ type StateDB struct { stateObjectsDirty map[common.Address]struct{} // State objects modified in the current execution // Block-stm related fields - mvHashmap *blockstm.MVHashMap - incarnation int - readMap map[string]blockstm.ReadDescriptor - writeMap map[string]blockstm.WriteDescriptor + mvHashmap *blockstm.MVHashMap + incarnation int + readMap map[string]blockstm.ReadDescriptor + writeMap map[string]blockstm.WriteDescriptor + newStateObjects map[common.Address]struct{} + invalidRead bool // DB error. // State objects are used by the consensus core and VM which are @@ -145,6 +147,7 @@ func New(root common.Hash, db Database, snaps *snapshot.Tree) (*StateDB, error) stateObjects: make(map[common.Address]*stateObject), stateObjectsPending: make(map[common.Address]struct{}), stateObjectsDirty: make(map[common.Address]struct{}), + newStateObjects: make(map[common.Address]struct{}), logs: make(map[common.Hash][]*types.Log), preimages: make(map[common.Hash][]byte), journal: newJournal(), @@ -177,6 +180,20 @@ func (sdb *StateDB) SetMVHashmap(mvhm *blockstm.MVHashMap) { func (s *StateDB) MVWriteList() []blockstm.WriteDescriptor { writes := make([]blockstm.WriteDescriptor, 0, len(s.writeMap)) + for _, v := range s.writeMap { + if len(v.Path) != common.AddressLength { + writes = append(writes, v) + } else if _, ok := s.newStateObjects[common.BytesToAddress(v.Path)]; ok { + writes = append(writes, v) + } + } + + return writes +} + +func (s *StateDB) MVFullWriteList() []blockstm.WriteDescriptor { + writes := make([]blockstm.WriteDescriptor, 0, len(s.writeMap)) + for _, v := range s.writeMap { writes = append(writes, v) } @@ -206,6 +223,14 @@ func (s *StateDB) ensureWriteMap() { } } +func (s *StateDB) HadInvalidRead() bool { + return s.invalidRead +} + +func (s *StateDB) SetIncarnation(inc int) { + s.incarnation = inc +} + func MVRead[T any](s *StateDB, k []byte, defaultV T, readStorage func(s *StateDB) T) (v T) { if s.mvHashmap == nil { return readStorage(s) @@ -238,6 +263,7 @@ func MVRead[T any](s *StateDB, k []byte, defaultV T, readStorage func(s *StateDB } case blockstm.MVReadResultDependency: { + s.invalidRead = true return defaultV } case blockstm.MVReadResultNone: @@ -262,7 +288,6 @@ func MVRead[T any](s *StateDB, k []byte, defaultV T, readStorage func(s *StateDB func MVWrite(s *StateDB, k []byte) { if s.mvHashmap != nil { s.ensureWriteMap() - s.mvHashmap.Write(k, s.Version(), s) s.writeMap[string(k)] = blockstm.WriteDescriptor{ Path: k, V: s.Version(), @@ -281,6 +306,15 @@ func MVWritten(s *StateDB, k []byte) bool { return ok } +// Apply entries in the write set to MVHashMap. Note that this function does not clear the write set. +func (s *StateDB) FlushMVWriteSet() { + if s.mvHashmap != nil && s.writeMap != nil { + s.mvHashmap.FlushMVWriteSet(s.MVFullWriteList()) + } +} + +// Apply entries in a given write set to StateDB. Note that this function does not change MVHashMap nor write set +// of the current StateDB. func (sw *StateDB) ApplyMVWriteSet(writes []blockstm.WriteDescriptor) { for i := range writes { path := writes[i].Path @@ -304,7 +338,8 @@ func (sw *StateDB) ApplyMVWriteSet(writes []blockstm.WriteDescriptor) { case codePath: sw.SetCode(addr, sr.GetCode(addr)) case suicidePath: - if suicided := sr.HasSuicided(addr); suicided { + stateObject := sr.getDeletedStateObject(addr) + if stateObject != nil && stateObject.deleted { sw.Suicide(addr) } default: @@ -606,6 +641,12 @@ 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) { stateObject := s.GetOrNewStateObject(addr) + + if s.mvHashmap != nil { + // ensure a read balance operation is recorded in mvHashmap + s.GetBalance(addr) + } + if stateObject != nil { stateObject = s.mvRecordWritten(stateObject) stateObject.AddBalance(amount) @@ -616,6 +657,12 @@ 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) { stateObject := s.GetOrNewStateObject(addr) + + if s.mvHashmap != nil { + // ensure a read balance operation is recorded in mvHashmap + s.GetBalance(addr) + } + if stateObject != nil { stateObject = s.mvRecordWritten(stateObject) stateObject.SubBalance(amount) @@ -859,6 +906,8 @@ func (s *StateDB) createObject(addr common.Address) (newobj, prev *stateObject) s.journal.append(resetObjectChange{prev: prev, prevdestruct: prevdestruct}) } s.setStateObject(newobj) + s.newStateObjects[addr] = struct{}{} + MVWrite(s, addr.Bytes()) if prev != nil && !prev.deleted { return newobj, prev @@ -923,6 +972,7 @@ func (s *StateDB) Copy() *StateDB { 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)), + newStateObjects: make(map[common.Address]struct{}, len(s.newStateObjects)), refund: s.refund, logs: make(map[common.Hash][]*types.Log, len(s.logs)), logSize: s.logSize, diff --git a/core/state/statedb_test.go b/core/state/statedb_test.go index c1f9f04812..1fd1f5477c 100644 --- a/core/state/statedb_test.go +++ b/core/state/statedb_test.go @@ -521,6 +521,7 @@ func TestMVHashMapReadWriteDelete(t *testing.T) { states[1].GetOrNewStateObject(addr) states[1].SetState(addr, key, val) states[1].SetBalance(addr, balance) + states[1].FlushMVWriteSet() // Tx1 read v = states[2].GetState(addr, key) @@ -547,6 +548,7 @@ func TestMVHashMapReadWriteDelete(t *testing.T) { states[3].Finalise(false) v = states[3].GetState(addr, key) assert.Equal(t, common.Hash{}, v) + states[3].FlushMVWriteSet() // Tx4 read v = states[4].GetState(addr, key) @@ -581,6 +583,7 @@ func TestMVHashMapRevert(t *testing.T) { states[0].GetOrNewStateObject(addr) states[0].SetState(addr, key, val) states[0].SetBalance(addr, balance) + states[0].FlushMVWriteSet() // Tx1 perform some ops and then revert snapshot := states[1].Snapshot() @@ -601,6 +604,7 @@ func TestMVHashMapRevert(t *testing.T) { assert.Equal(t, val, v) assert.Equal(t, balance, b) states[1].Finalise(false) + states[1].FlushMVWriteSet() // Tx2 check the state and balance v = states[2].GetState(addr, key) @@ -639,11 +643,13 @@ func TestMVHashMapMarkEstimate(t *testing.T) { states[0].SetState(addr, key, val) v = states[0].GetState(addr, key) assert.Equal(t, val, v) + states[0].FlushMVWriteSet() // Tx1 write states[1].GetOrNewStateObject(addr) states[1].SetState(addr, key, val) states[1].SetBalance(addr, balance) + states[1].FlushMVWriteSet() // Tx2 read v = states[2].GetState(addr, key) @@ -696,12 +702,14 @@ func TestMVHashMapOverwrite(t *testing.T) { states[0].GetOrNewStateObject(addr) states[0].SetState(addr, key, val1) states[0].SetBalance(addr, balance1) + states[0].FlushMVWriteSet() // Tx1 write states[1].SetState(addr, key, val2) states[1].SetBalance(addr, balance2) v := states[1].GetState(addr, key) b := states[1].GetBalance(addr) + states[1].FlushMVWriteSet() assert.Equal(t, val2, v) assert.Equal(t, balance2, b) @@ -774,14 +782,17 @@ func TestMVHashMapWriteNoConflict(t *testing.T) { // Tx0 write states[0].GetOrNewStateObject(addr) + states[0].FlushMVWriteSet() // Tx2 write states[2].SetState(addr, key2, val2) + states[2].FlushMVWriteSet() // Tx1 write tx1Snapshot := states[1].Snapshot() states[1].SetState(addr, key1, val1) states[1].SetBalance(addr, balance1) + states[1].FlushMVWriteSet() // Tx1 read assert.Equal(t, val1, states[1].GetState(addr, key1)) @@ -813,6 +824,7 @@ func TestMVHashMapWriteNoConflict(t *testing.T) { // Tx1 revert states[1].RevertToSnapshot(tx1Snapshot) + states[1].FlushMVWriteSet() assert.Equal(t, common.Hash{}, states[3].GetState(addr, key1)) assert.Equal(t, common.Hash{}, states[3].GetState(addr, key2)) @@ -853,6 +865,7 @@ func TestApplyMVWriteSet(t *testing.T) { addr1 := common.HexToAddress("0x01") addr2 := common.HexToAddress("0x02") + addr3 := common.HexToAddress("0x03") key1 := common.HexToHash("0x01") key2 := common.HexToHash("0x02") val1 := common.HexToHash("0x01") @@ -862,13 +875,19 @@ func TestApplyMVWriteSet(t *testing.T) { code := []byte{1, 2, 3} // Tx0 write + states[0].GetOrNewStateObject(addr1) states[0].SetState(addr1, key1, val1) states[0].SetBalance(addr1, balance1) states[0].SetState(addr2, key2, val2) + states[0].GetOrNewStateObject(addr3) + states[0].Finalise(false) + states[0].FlushMVWriteSet() + sSingleProcess.GetOrNewStateObject(addr1) sSingleProcess.SetState(addr1, key1, val1) sSingleProcess.SetBalance(addr1, balance1) sSingleProcess.SetState(addr2, key2, val2) + sSingleProcess.GetOrNewStateObject(addr3) sClean.ApplyMVWriteSet(states[0].MVWriteList()) @@ -878,6 +897,8 @@ func TestApplyMVWriteSet(t *testing.T) { states[1].SetState(addr1, key2, val2) states[1].SetBalance(addr1, balance2) states[1].SetNonce(addr1, 1) + states[1].Finalise(false) + states[1].FlushMVWriteSet() sSingleProcess.SetState(addr1, key2, val2) sSingleProcess.SetBalance(addr1, balance2) @@ -891,6 +912,8 @@ func TestApplyMVWriteSet(t *testing.T) { states[2].SetState(addr1, key1, val2) states[2].SetBalance(addr1, balance2) states[2].SetNonce(addr1, 2) + states[2].Finalise(false) + states[2].FlushMVWriteSet() sSingleProcess.SetState(addr1, key1, val2) sSingleProcess.SetBalance(addr1, balance2) @@ -903,6 +926,8 @@ func TestApplyMVWriteSet(t *testing.T) { // Tx3 write states[3].Suicide(addr2) states[3].SetCode(addr1, code) + states[3].Finalise(false) + states[3].FlushMVWriteSet() sSingleProcess.Suicide(addr2) sSingleProcess.SetCode(addr1, code) From 61accb021a7f3fc1e69a5c9de27bf35f6919df2e Mon Sep 17 00:00:00 2001 From: Jerry Date: Tue, 26 Jul 2022 11:36:33 -0700 Subject: [PATCH 03/38] Move fee burning and tipping out of state transition to reduce read/write dependencies between transactions --- core/parallel_state_processor.go | 87 ++++++++++++++++++++++++-------- core/state_transition.go | 87 +++++++++++++++++++++++--------- 2 files changed, 127 insertions(+), 47 deletions(-) diff --git a/core/parallel_state_processor.go b/core/parallel_state_processor.go index c2d6647f65..dd0e9dc0a5 100644 --- a/core/parallel_state_processor.go +++ b/core/parallel_state_processor.go @@ -55,16 +55,17 @@ type ExecutionTask struct { msg types.Message config *params.ChainConfig - gasLimit uint64 - blockNumber *big.Int - blockHash common.Hash - blockContext vm.BlockContext - tx *types.Transaction - index int - statedb *state.StateDB // State database that stores the modified values after tx execution. - cleanStateDB *state.StateDB // A clean copy of the initial statedb. It should not be modified. - evmConfig vm.Config - result *ExecutionResult + gasLimit uint64 + blockNumber *big.Int + blockHash common.Hash + blockContext vm.BlockContext + tx *types.Transaction + index int + statedb *state.StateDB // State database that stores the modified values after tx execution. + cleanStateDB *state.StateDB // A clean copy of the initial statedb. It should not be modified. + evmConfig vm.Config + result *ExecutionResult + shouldDelayFeeCal *bool } func (task *ExecutionTask) Execute(mvh *blockstm.MVHashMap, incarnation int) (err error) { @@ -91,7 +92,11 @@ func (task *ExecutionTask) Execute(mvh *blockstm.MVHashMap, incarnation int) (er }() // Apply the transaction to the current state (included in the env). - result, err := ApplyMessage(evm, task.msg, new(GasPool).AddGas(task.gasLimit)) + if *task.shouldDelayFeeCal { + task.result, err = ApplyMessageNoFeeBurnOrTip(evm, task.msg, new(GasPool).AddGas(task.gasLimit)) + } else { + task.result, err = ApplyMessage(evm, task.msg, new(GasPool).AddGas(task.gasLimit)) + } if task.statedb.HadInvalidRead() || err != nil { err = blockstm.ErrExecAbort @@ -100,8 +105,6 @@ func (task *ExecutionTask) Execute(mvh *blockstm.MVHashMap, incarnation int) (er task.statedb.Finalise(false) - task.result = result - return } @@ -140,6 +143,8 @@ func (p *ParallelStateProcessor) Process(block *types.Block, statedb *state.Stat tasks := make([]blockstm.ExecTask, 0, len(block.Transactions())) + shouldDelayFeeCal := true + // Iterate over and process the individual transactions for i, tx := range block.Transactions() { msg, err := tx.AsMessage(types.MakeSigner(p.config, header.Number), header.BaseFee) @@ -148,18 +153,26 @@ func (p *ParallelStateProcessor) Process(block *types.Block, statedb *state.Stat return nil, nil, 0, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err) } + bc := NewEVMBlockContext(header, p.bc, nil) + cleansdb := statedb.Copy() + if msg.From() == bc.Coinbase { + shouldDelayFeeCal = false + } + task := &ExecutionTask{ - msg: msg, - config: p.config, - gasLimit: block.GasLimit(), - blockNumber: blockNumber, - blockHash: blockHash, - tx: tx, - index: i, - cleanStateDB: cleansdb, - blockContext: NewEVMBlockContext(header, p.bc, nil), + msg: msg, + config: p.config, + gasLimit: block.GasLimit(), + blockNumber: blockNumber, + blockHash: blockHash, + tx: tx, + index: i, + cleanStateDB: cleansdb, + blockContext: bc, + evmConfig: cfg, + shouldDelayFeeCal: &shouldDelayFeeCal, } tasks = append(tasks, task) @@ -172,15 +185,45 @@ func (p *ParallelStateProcessor) Process(block *types.Block, statedb *state.Stat return nil, nil, 0, err } + london := p.config.IsLondon(blockNumber) + for _, task := range tasks { task := task.(*ExecutionTask) statedb.Prepare(task.tx.Hash(), task.index) + + coinbaseBalance := statedb.GetBalance(task.blockContext.Coinbase) + statedb.ApplyMVWriteSet(task.MVWriteList()) for _, l := range task.statedb.GetLogs(task.tx.Hash(), blockHash) { statedb.AddLog(l) } + if shouldDelayFeeCal { + if london { + statedb.AddBalance(task.result.BurntContractAddress, task.result.FeeBurnt) + } + + statedb.AddBalance(task.blockContext.Coinbase, task.result.FeeTipped) + output1 := new(big.Int).SetBytes(task.result.senderInitBalance.Bytes()) + output2 := new(big.Int).SetBytes(coinbaseBalance.Bytes()) + + // Deprecating transfer log and will be removed in future fork. PLEASE DO NOT USE this transfer log going forward. Parameters won't get updated as expected going forward with EIP1559 + // add transfer log + AddFeeTransferLog( + statedb, + + task.msg.From(), + task.blockContext.Coinbase, + + task.result.FeeTipped, + task.result.senderInitBalance, + coinbaseBalance, + output1.Sub(output1, task.result.FeeTipped), + output2.Add(output2, task.result.FeeTipped), + ) + } + for k, v := range task.statedb.Preimages() { statedb.AddPreimage(k, v) } diff --git a/core/state_transition.go b/core/state_transition.go index 3fc5a635e9..dee8ef271f 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -62,6 +62,11 @@ type StateTransition struct { data []byte state vm.StateDB evm *vm.EVM + + // If true, fee burning and tipping won't happen during transition. Instead, their values will be included in the + // ExecutionResult, which caller can use the values to update the balance of burner and coinbase account. + // This is useful during parallel state transition, where the common account read/write should be minimized. + noFeeBurnAndTip bool } // Message represents a message sent to a contract. @@ -84,9 +89,13 @@ type Message interface { // ExecutionResult includes all output after executing given evm // message no matter the execution itself is successful or not. type ExecutionResult struct { - UsedGas uint64 // Total used gas but include the refunded gas - Err error // Any error encountered during the execution(listed in core/vm/errors.go) - ReturnData []byte // Returned data from evm(function result or data supplied with revert opcode) + UsedGas uint64 // Total used gas but include the refunded gas + Err error // Any error encountered during the execution(listed in core/vm/errors.go) + ReturnData []byte // Returned data from evm(function result or data supplied with revert opcode) + senderInitBalance *big.Int + FeeBurnt *big.Int + BurntContractAddress common.Address + FeeTipped *big.Int } // Unwrap returns the internal evm error which allows us for further @@ -183,6 +192,13 @@ func ApplyMessage(evm *vm.EVM, msg Message, gp *GasPool) (*ExecutionResult, erro return NewStateTransition(evm, msg, gp).TransitionDb() } +func ApplyMessageNoFeeBurnOrTip(evm *vm.EVM, msg Message, gp *GasPool) (*ExecutionResult, error) { + st := NewStateTransition(evm, msg, gp) + st.noFeeBurnAndTip = true + + return st.TransitionDb() +} + // to returns the recipient of the message. func (st *StateTransition) to() common.Address { if st.msg == nil || st.msg.To() == nil /* contract creation */ { @@ -276,7 +292,12 @@ func (st *StateTransition) preCheck() error { // nil evm execution result. func (st *StateTransition) TransitionDb() (*ExecutionResult, error) { input1 := st.state.GetBalance(st.msg.From()) - input2 := st.state.GetBalance(st.evm.Context.Coinbase) + + var input2 *big.Int + + if !st.noFeeBurnAndTip { + input2 = st.state.GetBalance(st.evm.Context.Coinbase) + } // First check this message satisfies all consensus rules before // applying the message. The rules include these clauses @@ -342,34 +363,50 @@ func (st *StateTransition) TransitionDb() (*ExecutionResult, error) { effectiveTip = cmath.BigMin(st.gasTipCap, new(big.Int).Sub(st.gasFeeCap, st.evm.Context.BaseFee)) } amount := new(big.Int).Mul(new(big.Int).SetUint64(st.gasUsed()), effectiveTip) + + var burnAmount *big.Int + + var burntContractAddress common.Address + if london { - burntContractAddress := common.HexToAddress(st.evm.ChainConfig().Bor.CalculateBurntContract(st.evm.Context.BlockNumber.Uint64())) - burnAmount := new(big.Int).Mul(new(big.Int).SetUint64(st.gasUsed()), st.evm.Context.BaseFee) - st.state.AddBalance(burntContractAddress, burnAmount) + burntContractAddress = common.HexToAddress(st.evm.ChainConfig().Bor.CalculateBurntContract(st.evm.Context.BlockNumber.Uint64())) + burnAmount = new(big.Int).Mul(new(big.Int).SetUint64(st.gasUsed()), st.evm.Context.BaseFee) + + if !st.noFeeBurnAndTip { + st.state.AddBalance(burntContractAddress, burnAmount) + } } - st.state.AddBalance(st.evm.Context.Coinbase, amount) - output1 := new(big.Int).SetBytes(input1.Bytes()) - output2 := new(big.Int).SetBytes(input2.Bytes()) - // Deprecating transfer log and will be removed in future fork. PLEASE DO NOT USE this transfer log going forward. Parameters won't get updated as expected going forward with EIP1559 - // add transfer log - AddFeeTransferLog( - st.state, + if !st.noFeeBurnAndTip { + st.state.AddBalance(st.evm.Context.Coinbase, amount) - msg.From(), - st.evm.Context.Coinbase, + output1 := new(big.Int).SetBytes(input1.Bytes()) + output2 := new(big.Int).SetBytes(input2.Bytes()) - amount, - input1, - input2, - output1.Sub(output1, amount), - output2.Add(output2, amount), - ) + // Deprecating transfer log and will be removed in future fork. PLEASE DO NOT USE this transfer log going forward. Parameters won't get updated as expected going forward with EIP1559 + // add transfer log + AddFeeTransferLog( + st.state, + + msg.From(), + st.evm.Context.Coinbase, + + amount, + input1, + input2, + output1.Sub(output1, amount), + output2.Add(output2, amount), + ) + } return &ExecutionResult{ - UsedGas: st.gasUsed(), - Err: vmerr, - ReturnData: ret, + UsedGas: st.gasUsed(), + Err: vmerr, + ReturnData: ret, + senderInitBalance: input1, + FeeBurnt: burnAmount, + BurntContractAddress: burntContractAddress, + FeeTipped: amount, }, nil } From ab3ebebccac6d7c0534eb4286fca5949e9f95d66 Mon Sep 17 00:00:00 2001 From: Jerry Date: Wed, 27 Jul 2022 19:35:23 +0000 Subject: [PATCH 04/38] Re-execute parallel tasks when there is a read in coinbase or burn address --- core/parallel_state_processor.go | 41 +++++++++++++++++++++++--------- core/state/statedb.go | 4 ++++ 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/core/parallel_state_processor.go b/core/parallel_state_processor.go index dd0e9dc0a5..5fa7a4d56e 100644 --- a/core/parallel_state_processor.go +++ b/core/parallel_state_processor.go @@ -55,17 +55,18 @@ type ExecutionTask struct { msg types.Message config *params.ChainConfig - gasLimit uint64 - blockNumber *big.Int - blockHash common.Hash - blockContext vm.BlockContext - tx *types.Transaction - index int - statedb *state.StateDB // State database that stores the modified values after tx execution. - cleanStateDB *state.StateDB // A clean copy of the initial statedb. It should not be modified. - evmConfig vm.Config - result *ExecutionResult - shouldDelayFeeCal *bool + gasLimit uint64 + blockNumber *big.Int + blockHash common.Hash + blockContext vm.BlockContext + tx *types.Transaction + index int + statedb *state.StateDB // State database that stores the modified values after tx execution. + cleanStateDB *state.StateDB // A clean copy of the initial statedb. It should not be modified. + evmConfig vm.Config + result *ExecutionResult + shouldDelayFeeCal *bool + shouldRerunWithoutFeeDelay bool } func (task *ExecutionTask) Execute(mvh *blockstm.MVHashMap, incarnation int) (err error) { @@ -94,6 +95,14 @@ func (task *ExecutionTask) Execute(mvh *blockstm.MVHashMap, incarnation int) (er // Apply the transaction to the current state (included in the env). if *task.shouldDelayFeeCal { task.result, err = ApplyMessageNoFeeBurnOrTip(evm, task.msg, new(GasPool).AddGas(task.gasLimit)) + + if _, ok := task.statedb.MVReadMap()[string(task.blockContext.Coinbase.Bytes())]; ok { + task.shouldRerunWithoutFeeDelay = true + } + + if _, ok := task.statedb.MVReadMap()[string(task.result.BurntContractAddress.Bytes())]; ok { + task.shouldRerunWithoutFeeDelay = true + } } else { task.result, err = ApplyMessage(evm, task.msg, new(GasPool).AddGas(task.gasLimit)) } @@ -180,6 +189,16 @@ func (p *ParallelStateProcessor) Process(block *types.Block, statedb *state.Stat _, err := blockstm.ExecuteParallel(tasks) + for _, task := range tasks { + task := task.(*ExecutionTask) + if task.shouldRerunWithoutFeeDelay { + shouldDelayFeeCal = false + _, err = blockstm.ExecuteParallel(tasks) + + break + } + } + if err != nil { log.Error("blockstm error executing block", "err", err) return nil, nil, 0, err diff --git a/core/state/statedb.go b/core/state/statedb.go index 1b7af5dda3..bfc6821d2a 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -201,6 +201,10 @@ func (s *StateDB) MVFullWriteList() []blockstm.WriteDescriptor { return writes } +func (s *StateDB) MVReadMap() map[string]blockstm.ReadDescriptor { + return s.readMap +} + func (s *StateDB) MVReadList() []blockstm.ReadDescriptor { reads := make([]blockstm.ReadDescriptor, 0, len(s.readMap)) From f7bd7ca66b332b2c97c69b2c5312fcfba852f9c6 Mon Sep 17 00:00:00 2001 From: Pratik Patil Date: Fri, 19 Aug 2022 10:32:55 +0530 Subject: [PATCH 05/38] Txn prioritizer implemented using mutex map (#487) * basic txn prioritizer implemented using mutex map * Re-execute parallel tasks when there is a read in coinbase or burn address * Re-execute parallel tasks when there is a read in coinbase or burn address * using *sync.RWMutex{} in mutexMap Co-authored-by: Jerry --- core/blockstm/executor.go | 34 ++++++++++++++++++++++++-------- core/parallel_state_processor.go | 6 ++++++ 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/core/blockstm/executor.go b/core/blockstm/executor.go index cb5019bbb2..de63bcf7bf 100644 --- a/core/blockstm/executor.go +++ b/core/blockstm/executor.go @@ -2,7 +2,9 @@ package blockstm import ( "fmt" + "sync" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/log" ) @@ -19,12 +21,14 @@ type ExecTask interface { MVReadList() []ReadDescriptor MVWriteList() []WriteDescriptor MVFullWriteList() []WriteDescriptor + Sender() common.Address } type ExecVersionView struct { - ver Version - et ExecTask - mvh *MVHashMap + ver Version + et ExecTask + mvh *MVHashMap + sender common.Address } func (ev *ExecVersionView) Execute() (er ExecResult) { @@ -55,6 +59,13 @@ func ExecuteParallel(tasks []ExecTask) (lastTxIO *TxnInputOutput, err error) { chTasks := make(chan ExecVersionView, len(tasks)) chResults := make(chan ExecResult, len(tasks)) chDone := make(chan bool) + mutMap := map[common.Address]*sync.RWMutex{} + + for _, t := range tasks { + if _, ok := mutMap[t.Sender()]; !ok { + mutMap[t.Sender()] = &sync.RWMutex{} + } + } var cntExec, cntSuccess, cntAbort, cntTotalValidations, cntValidationFail int @@ -65,8 +76,15 @@ func ExecuteParallel(tasks []ExecTask) (lastTxIO *TxnInputOutput, err error) { select { case task := <-t: { - res := task.Execute() - chResults <- res + m := mutMap[task.sender] + if !m.TryLock() { + // why not this? -> chTasks <- task + t <- task + } else { + res := task.Execute() + chResults <- res + m.Unlock() + } } case <-chDone: break Loop @@ -88,7 +106,7 @@ func ExecuteParallel(tasks []ExecTask) (lastTxIO *TxnInputOutput, err error) { cntExec++ log.Debug("blockstm", "bootstrap: proc", x, "executing task", tx) - chTasks <- ExecVersionView{ver: Version{tx, 0}, et: tasks[tx], mvh: mvh} + chTasks <- ExecVersionView{ver: Version{tx, 0}, et: tasks[tx], mvh: mvh, sender: tasks[tx].Sender()} } } @@ -161,7 +179,7 @@ func ExecuteParallel(tasks []ExecTask) (lastTxIO *TxnInputOutput, err error) { nextTx := execTasks.takeNextPending() if nextTx != -1 { cntExec++ - chTasks <- ExecVersionView{ver: Version{nextTx, txIncarnations[nextTx]}, et: tasks[nextTx], mvh: mvh} + chTasks <- ExecVersionView{ver: Version{nextTx, txIncarnations[nextTx]}, et: tasks[nextTx], mvh: mvh, sender: tasks[nextTx].Sender()} } // do validations ... @@ -221,7 +239,7 @@ func ExecuteParallel(tasks []ExecTask) (lastTxIO *TxnInputOutput, err error) { cntExec++ log.Debug("blockstm", "# tx queued up", nextTx) - chTasks <- ExecVersionView{ver: Version{nextTx, txIncarnations[nextTx]}, et: tasks[nextTx], mvh: mvh} + chTasks <- ExecVersionView{ver: Version{nextTx, txIncarnations[nextTx]}, et: tasks[nextTx], mvh: mvh, sender: tasks[nextTx].Sender()} } } diff --git a/core/parallel_state_processor.go b/core/parallel_state_processor.go index 5fa7a4d56e..4ed3c9161d 100644 --- a/core/parallel_state_processor.go +++ b/core/parallel_state_processor.go @@ -67,6 +67,7 @@ type ExecutionTask struct { result *ExecutionResult shouldDelayFeeCal *bool shouldRerunWithoutFeeDelay bool + sender common.Address } func (task *ExecutionTask) Execute(mvh *blockstm.MVHashMap, incarnation int) (err error) { @@ -129,6 +130,10 @@ func (task *ExecutionTask) MVFullWriteList() []blockstm.WriteDescriptor { return task.statedb.MVFullWriteList() } +func (task *ExecutionTask) Sender() common.Address { + return task.sender +} + // 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. @@ -182,6 +187,7 @@ func (p *ParallelStateProcessor) Process(block *types.Block, statedb *state.Stat blockContext: bc, evmConfig: cfg, shouldDelayFeeCal: &shouldDelayFeeCal, + sender: msg.From(), } tasks = append(tasks, task) From 4507b2e0579f9cd0883b80cb0f89b2fcea60cb25 Mon Sep 17 00:00:00 2001 From: Pratik Patil Date: Fri, 19 Aug 2022 14:47:58 +0530 Subject: [PATCH 06/38] added getReadMap and getWriteMap (#473) --- core/parallel_state_processor.go | 4 +- core/state/statedb.go | 55 ++++++++++++++ core/state_transition.go | 4 +- eth/tracers/api.go | 120 +++++++++++++++++++++++++++++-- eth/tracers/api_test.go | 77 ++++++++++++++++++++ 5 files changed, 250 insertions(+), 10 deletions(-) diff --git a/core/parallel_state_processor.go b/core/parallel_state_processor.go index 4ed3c9161d..f4a971bd5b 100644 --- a/core/parallel_state_processor.go +++ b/core/parallel_state_processor.go @@ -230,7 +230,7 @@ func (p *ParallelStateProcessor) Process(block *types.Block, statedb *state.Stat } statedb.AddBalance(task.blockContext.Coinbase, task.result.FeeTipped) - output1 := new(big.Int).SetBytes(task.result.senderInitBalance.Bytes()) + output1 := new(big.Int).SetBytes(task.result.SenderInitBalance.Bytes()) output2 := new(big.Int).SetBytes(coinbaseBalance.Bytes()) // Deprecating transfer log and will be removed in future fork. PLEASE DO NOT USE this transfer log going forward. Parameters won't get updated as expected going forward with EIP1559 @@ -242,7 +242,7 @@ func (p *ParallelStateProcessor) Process(block *types.Block, statedb *state.Stat task.blockContext.Coinbase, task.result.FeeTipped, - task.result.senderInitBalance, + task.result.SenderInitBalance, coinbaseBalance, output1.Sub(output1, task.result.FeeTipped), output2.Add(output2, task.result.FeeTipped), diff --git a/core/state/statedb.go b/core/state/statedb.go index bfc6821d2a..ce2a6e72d3 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -353,6 +353,61 @@ func (sw *StateDB) ApplyMVWriteSet(writes []blockstm.WriteDescriptor) { } } +type DumpStruct struct { + TxIdx int + TxInc int + VerIdx int + VerInc int + Path []byte + Op string +} + +// get readMap Dump of format: "TxIdx, Inc, Path, Read" +func (s *StateDB) GetReadMapDump() []DumpStruct { + readList := s.MVReadList() + res := make([]DumpStruct, 0, len(readList)) + + for _, val := range readList { + temp := &DumpStruct{ + TxIdx: s.txIndex, + TxInc: s.incarnation, + VerIdx: val.V.TxnIndex, + VerInc: val.V.Incarnation, + Path: val.Path, + Op: "Read\n", + } + res = append(res, *temp) + } + + return res +} + +// get writeMap Dump of format: "TxIdx, Inc, Path, Write" +func (s *StateDB) GetWriteMapDump() []DumpStruct { + writeList := s.MVReadList() + res := make([]DumpStruct, 0, len(writeList)) + + for _, val := range writeList { + temp := &DumpStruct{ + TxIdx: s.txIndex, + TxInc: s.incarnation, + VerIdx: val.V.TxnIndex, + VerInc: val.V.Incarnation, + Path: val.Path, + Op: "Write\n", + } + res = append(res, *temp) + } + + return res +} + +// add empty MVHashMap to StateDB +func (s *StateDB) AddEmptyMVHashMap() { + mvh := blockstm.MakeMVHashMap() + s.mvHashmap = mvh +} + // 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. diff --git a/core/state_transition.go b/core/state_transition.go index dee8ef271f..44b18d578c 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -92,7 +92,7 @@ type ExecutionResult struct { UsedGas uint64 // Total used gas but include the refunded gas Err error // Any error encountered during the execution(listed in core/vm/errors.go) ReturnData []byte // Returned data from evm(function result or data supplied with revert opcode) - senderInitBalance *big.Int + SenderInitBalance *big.Int FeeBurnt *big.Int BurntContractAddress common.Address FeeTipped *big.Int @@ -403,7 +403,7 @@ func (st *StateTransition) TransitionDb() (*ExecutionResult, error) { UsedGas: st.gasUsed(), Err: vmerr, ReturnData: ret, - senderInitBalance: input1, + SenderInitBalance: input1, FeeBurnt: burnAmount, BurntContractAddress: burntContractAddress, FeeTipped: amount, diff --git a/eth/tracers/api.go b/eth/tracers/api.go index 08c17601e4..b3361932ac 100644 --- a/eth/tracers/api.go +++ b/eth/tracers/api.go @@ -20,10 +20,13 @@ import ( "bufio" "bytes" "context" + "encoding/hex" "errors" "fmt" "io/ioutil" + "math/big" "os" + "path/filepath" "runtime" "sync" "time" @@ -61,6 +64,10 @@ const ( // For non-archive nodes, this limit _will_ be overblown, as disk-backed tries // will only be found every ~15K blocks or so. defaultTracechainMemLimit = common.StorageSize(500 * 1024 * 1024) + + defaultPath = string(".") + + defaultIOFlag = false ) // Backend interface provides the common API services (that are provided by @@ -170,6 +177,8 @@ type TraceConfig struct { Tracer *string Timeout *string Reexec *uint64 + Path *string + IOFlag *bool } // TraceCallConfig is the config for traceCall API. It holds one more @@ -567,18 +576,37 @@ func (api *API) traceBlock(ctx context.Context, block *types.Block, config *Trac if block.NumberU64() == 0 { return nil, errors.New("genesis is not traceable") } + parent, err := api.blockByNumberAndHash(ctx, rpc.BlockNumber(block.NumberU64()-1), block.ParentHash()) if err != nil { return nil, err } + reexec := defaultTraceReexec if config != nil && config.Reexec != nil { reexec = *config.Reexec } + + path := defaultPath + if config != nil && config.Path != nil { + path = *config.Path + } + + ioflag := defaultIOFlag + if config != nil && config.IOFlag != nil { + ioflag = *config.IOFlag + } + statedb, err := api.backend.StateAtBlock(ctx, parent, reexec, nil, true, false) if err != nil { return nil, err } + + // create and add empty mvHashMap in statedb as StateAtBlock does not have mvHashmap in it. + if ioflag { + statedb.AddEmptyMVHashMap() + } + // Execute all the transaction contained within the block concurrently var ( signer = types.MakeSigner(api.backend.ChainConfig(), block.Number()) @@ -615,10 +643,31 @@ func (api *API) traceBlock(ctx context.Context, block *types.Block, config *Trac } }() } + + var IOdump string + + var RWstruct []state.DumpStruct + + var london bool + + if ioflag { + IOdump = "TransactionIndex, Incarnation, VersionTxIdx, VersionInc, Path, Operation\n" + RWstruct = []state.DumpStruct{} + } // Feed the transactions into the tracers and return var failed error blockCtx := core.NewEVMBlockContext(block.Header(), api.chainContext(ctx), nil) + + if ioflag { + london = api.backend.ChainConfig().IsLondon(block.Number()) + } + for i, tx := range txs { + if ioflag { + // copy of statedb + statedb = statedb.Copy() + } + // Send the trace task over for execution jobs <- &txTraceTask{statedb: statedb.Copy(), index: i} @@ -626,14 +675,73 @@ func (api *API) traceBlock(ctx context.Context, block *types.Block, config *Trac msg, _ := tx.AsMessage(signer, block.BaseFee()) statedb.Prepare(tx.Hash(), i) vmenv := vm.NewEVM(blockCtx, core.NewEVMTxContext(msg), statedb, api.backend.ChainConfig(), vm.Config{}) - if _, err := core.ApplyMessage(vmenv, msg, new(core.GasPool).AddGas(msg.Gas())); err != nil { - failed = err - break + + if !ioflag { + if _, err := core.ApplyMessage(vmenv, msg, new(core.GasPool).AddGas(msg.Gas())); err != nil { + failed = err + break + } + // Finalize the state so any modifications are written to the trie + // Only delete empty objects if EIP158/161 (a.k.a Spurious Dragon) is in effect + statedb.Finalise(vmenv.ChainConfig().IsEIP158(block.Number())) + } else { + coinbaseBalance := statedb.GetBalance(blockCtx.Coinbase) + + result, err := core.ApplyMessageNoFeeBurnOrTip(vmenv, msg, new(core.GasPool).AddGas(msg.Gas())) + + if err != nil { + failed = err + break + } + + if london { + statedb.AddBalance(result.BurntContractAddress, result.FeeBurnt) + } + + statedb.AddBalance(blockCtx.Coinbase, result.FeeTipped) + output1 := new(big.Int).SetBytes(result.SenderInitBalance.Bytes()) + output2 := new(big.Int).SetBytes(coinbaseBalance.Bytes()) + + // Deprecating transfer log and will be removed in future fork. PLEASE DO NOT USE this transfer log going forward. Parameters won't get updated as expected going forward with EIP1559 + // add transfer log + core.AddFeeTransferLog( + statedb, + + msg.From(), + blockCtx.Coinbase, + + result.FeeTipped, + result.SenderInitBalance, + coinbaseBalance, + output1.Sub(output1, result.FeeTipped), + output2.Add(output2, result.FeeTipped), + ) + + // Finalize the state so any modifications are written to the trie + // Only delete empty objects if EIP158/161 (a.k.a Spurious Dragon) is in effect + statedb.Finalise(vmenv.ChainConfig().IsEIP158(block.Number())) + statedb.FlushMVWriteSet() + + structRead := statedb.GetReadMapDump() + structWrite := statedb.GetWriteMapDump() + + RWstruct = append(RWstruct, structRead...) + RWstruct = append(RWstruct, structWrite...) } - // Finalize the state so any modifications are written to the trie - // Only delete empty objects if EIP158/161 (a.k.a Spurious Dragon) is in effect - statedb.Finalise(vmenv.ChainConfig().IsEIP158(block.Number())) } + + if ioflag { + for _, val := range RWstruct { + IOdump += fmt.Sprintf("%v , %v, %v , %v, ", val.TxIdx, val.TxInc, val.VerIdx, val.VerInc) + hex.EncodeToString(val.Path) + ", " + val.Op + } + + // make sure that the file exists and write IOdump + err = ioutil.WriteFile(filepath.Join(path, "data.csv"), []byte(fmt.Sprint(IOdump)), 0600) + if err != nil { + return nil, err + } + } + close(jobs) pend.Wait() diff --git a/eth/tracers/api_test.go b/eth/tracers/api_test.go index d2ed9c2179..4b8257756a 100644 --- a/eth/tracers/api_test.go +++ b/eth/tracers/api_test.go @@ -415,6 +415,83 @@ func TestTraceBlock(t *testing.T) { } } +func TestIOdump(t *testing.T) { + t.Parallel() + + // Initialize test accounts + accounts := newAccounts(5) + genesis := &core.Genesis{Alloc: core.GenesisAlloc{ + accounts[0].addr: {Balance: big.NewInt(params.Ether)}, + accounts[1].addr: {Balance: big.NewInt(params.Ether)}, + accounts[2].addr: {Balance: big.NewInt(params.Ether)}, + accounts[3].addr: {Balance: big.NewInt(params.Ether)}, + accounts[4].addr: {Balance: big.NewInt(params.Ether)}, + }} + genBlocks := 1 + signer := types.HomesteadSigner{} + api := NewAPI(newTestBackend(t, genBlocks, genesis, func(i int, b *core.BlockGen) { + // Transfer from account[0] to account[1], account[1] to account[2], account[2] to account[3], account[3] to account[4], account[4] to account[0] + // value: 1000 wei + // fee: 0 wei + + for j := 0; j < 5; j++ { + tx, _ := types.SignTx(types.NewTransaction(uint64(i), accounts[(j+1)%5].addr, big.NewInt(1000), params.TxGas, b.BaseFee(), nil), signer, accounts[j].key) + b.AddTx(tx) + } + })) + + ioflag := new(bool) + + *ioflag = true + + var testSuite = []struct { + blockNumber rpc.BlockNumber + config *TraceConfig + want string + expectErr error + }{ + // Trace head block + { + config: &TraceConfig{ + IOFlag: ioflag, + }, + blockNumber: rpc.BlockNumber(genBlocks), + want: `[{"result":{"gas":21000,"failed":false,"returnValue":"","structLogs":[]}},{"result":{"gas":21000,"failed":false,"returnValue":"","structLogs":[]}},{"result":{"gas":21000,"failed":false,"returnValue":"","structLogs":[]}},{"result":{"gas":21000,"failed":false,"returnValue":"","structLogs":[]}},{"result":{"gas":21000,"failed":false,"returnValue":"","structLogs":[]}}]`, + }, + } + + for i, tc := range testSuite { + result, err := api.TraceBlockByNumber(context.Background(), tc.blockNumber, tc.config) + if tc.expectErr != nil { + if err == nil { + t.Errorf("test %d, want error %v", i, tc.expectErr) + continue + } + + if !reflect.DeepEqual(err, tc.expectErr) { + t.Errorf("test %d: error mismatch, want %v, get %v", i, tc.expectErr, err) + } + + continue + } + + if err != nil { + t.Errorf("test %d, want no error, have %v", i, err) + continue + } + + have, err := json.Marshal(result) + if err != nil { + t.Errorf("Error in Marshal: %v", err) + } + + want := tc.want + if string(have) != want { + t.Errorf("test %d, result mismatch, have\n%v\n, want\n%v\n", i, string(have), want) + } + } +} + func TestTracingWithOverrides(t *testing.T) { t.Parallel() // Initialize test accounts From c36ad88aec974685c46c18d219e4a9e25c536fdd Mon Sep 17 00:00:00 2001 From: Jerry Date: Mon, 8 Aug 2022 12:38:39 -0700 Subject: [PATCH 07/38] Block-stm optimization Added tests for executor and some improvements: 1. Add a dependency map during execution. This will prevent aborted tasks from being sent for execution immedaitely after failure. 2. Change the key of MVHashMap from string to a byte array. This will reduce time to convert byte slices to strings. 3. Use sync.Map to reduce the time spent in global mutex. 4. Skip applying intermediate states. 5. Estimate dependency when an execution fails without dependency information. 6. Divide execution task queue into two separate queues. One for relatively certain transactions, and the other for speculative future transactions. 7. Setting dependencies of Txs coming from the same sender before starting parallel execution. 8. Process results in their semantic order (transaction index) instead of the order when they arrive. Replace result channel with a priority queue. --- core/blockstm/dag.go | 119 +++++++ core/blockstm/executor.go | 564 +++++++++++++++++++++++-------- core/blockstm/executor_test.go | 470 ++++++++++++++++++++++++++ core/blockstm/mvhashmap.go | 156 ++++++--- core/blockstm/status.go | 115 ++++++- core/blockstm/txio.go | 13 +- core/parallel_state_processor.go | 233 +++++++------ core/state/journal.go | 17 +- core/state/statedb.go | 146 ++++---- core/state/statedb_test.go | 17 +- go.mod | 1 + go.sum | 5 +- 12 files changed, 1473 insertions(+), 383 deletions(-) create mode 100644 core/blockstm/dag.go create mode 100644 core/blockstm/executor_test.go diff --git a/core/blockstm/dag.go b/core/blockstm/dag.go new file mode 100644 index 0000000000..8404395ec0 --- /dev/null +++ b/core/blockstm/dag.go @@ -0,0 +1,119 @@ +package blockstm + +import ( + "fmt" + "sort" + "strings" + + "github.com/heimdalr/dag" + + "github.com/ethereum/go-ethereum/log" +) + +type DAG struct { + *dag.DAG +} + +func HasReadDep(txFrom TxnOutput, txTo TxnInput) bool { + reads := make(map[Key]bool) + + for _, v := range txTo { + reads[v.Path] = true + } + + for _, rd := range txFrom { + if _, ok := reads[rd.Path]; ok { + return true + } + } + + return false +} + +func BuildDAG(deps TxnInputOutput) (d DAG) { + d = DAG{dag.NewDAG()} + ids := make(map[int]string) + + for i := len(deps.inputs) - 1; i > 0; i-- { + txTo := deps.inputs[i] + + var txToId string + + if _, ok := ids[i]; ok { + txToId = ids[i] + } else { + txToId, _ = d.AddVertex(i) + ids[i] = txToId + } + + for j := i - 1; j >= 0; j-- { + txFrom := deps.allOutputs[j] + + if HasReadDep(txFrom, txTo) { + var txFromId string + if _, ok := ids[j]; ok { + txFromId = ids[j] + } else { + txFromId, _ = d.AddVertex(j) + ids[j] = txFromId + } + + err := d.AddEdge(txFromId, txToId) + if err != nil { + log.Warn("Failed to add edge", "from", txFromId, "to", txToId, "err", err) + } + + break // once we add a 'backward' dep we can't execute before that transaction so no need to proceed + } + } + } + + return +} + +func (d DAG) Report(out func(string)) { + roots := make([]int, 0) + rootIds := make([]string, 0) + + for k, i := range d.GetRoots() { + roots = append(roots, i.(int)) + rootIds = append(rootIds, k) + } + + sort.Ints(roots) + fmt.Println(roots) + + makeStrs := func(ints []int) (ret []string) { + for _, v := range ints { + ret = append(ret, fmt.Sprint(v)) + } + + return + } + + maxDesc := 0 + maxDeps := 0 + totalDeps := 0 + + for k, v := range roots { + ids := []int{v} + desc, _ := d.GetDescendants(rootIds[k]) + + for _, i := range desc { + ids = append(ids, i.(int)) + } + + sort.Ints(ids) + out(fmt.Sprintf("(%v) %v", len(ids), strings.Join(makeStrs(ids), "->"))) + + if len(desc) > maxDesc { + maxDesc = len(desc) + } + } + + numTx := len(d.DAG.GetVertices()) + out(fmt.Sprintf("max chain length: %v of %v (%v%%)", maxDesc+1, numTx, + fmt.Sprintf("%.1f", float64(maxDesc+1)*100.0/float64(numTx)))) + out(fmt.Sprintf("max dep count: %v of %v (%v%%)", maxDeps, totalDeps, + fmt.Sprintf("%.1f", float64(maxDeps)*100.0/float64(totalDeps)))) +} diff --git a/core/blockstm/executor.go b/core/blockstm/executor.go index de63bcf7bf..b1c5770866 100644 --- a/core/blockstm/executor.go +++ b/core/blockstm/executor.go @@ -1,8 +1,11 @@ package blockstm import ( + "container/heap" "fmt" + "sort" "sync" + "time" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/log" @@ -22,6 +25,7 @@ type ExecTask interface { MVWriteList() []WriteDescriptor MVFullWriteList() []WriteDescriptor Sender() common.Address + Settle() } type ExecVersionView struct { @@ -34,186 +38,362 @@ type ExecVersionView struct { func (ev *ExecVersionView) Execute() (er ExecResult) { er.ver = ev.ver if er.err = ev.et.Execute(ev.mvh, ev.ver.Incarnation); er.err != nil { - log.Debug("blockstm executed task failed", "Tx index", ev.ver.TxnIndex, "incarnation", ev.ver.Incarnation, "err", er.err) return } er.txIn = ev.et.MVReadList() er.txOut = ev.et.MVWriteList() er.txAllOut = ev.et.MVFullWriteList() - log.Debug("blockstm executed task", "Tx index", ev.ver.TxnIndex, "incarnation", ev.ver.Incarnation, "err", er.err) return } -var ErrExecAbort = fmt.Errorf("execution aborted with dependency") +type ErrExecAbortError struct { + Dependency int +} -const numGoProcs = 4 +func (e ErrExecAbortError) Error() string { + if e.Dependency >= 0 { + return fmt.Sprintf("Execution aborted due to dependency %d", e.Dependency) + } else { + return "Execution aborted" + } +} + +type IntHeap []int + +func (h IntHeap) Len() int { return len(h) } +func (h IntHeap) Less(i, j int) bool { return h[i] < h[j] } +func (h IntHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] } + +func (h *IntHeap) Push(x any) { + // Push and Pop use pointer receivers because they modify the slice's length, + // not just its contents. + *h = append(*h, x.(int)) +} + +func (h *IntHeap) Pop() any { + old := *h + n := len(old) + x := old[n-1] + *h = old[0 : n-1] + + return x +} + +// A thread safe priority queue +type SafePriorityQueue struct { + m sync.Mutex + queue *IntHeap + data map[int]interface{} +} + +func NewSafePriorityQueue(capacity int) *SafePriorityQueue { + q := make(IntHeap, 0, capacity) + + return &SafePriorityQueue{ + m: sync.Mutex{}, + queue: &q, + data: make(map[int]interface{}, capacity), + } +} + +func (pq *SafePriorityQueue) Push(v int, d interface{}) { + pq.m.Lock() + + heap.Push(pq.queue, v) + pq.data[v] = d + + pq.m.Unlock() +} + +func (pq *SafePriorityQueue) Pop() interface{} { + pq.m.Lock() + defer pq.m.Unlock() + + v := heap.Pop(pq.queue).(int) + + return pq.data[v] +} + +func (pq *SafePriorityQueue) Len() int { + return pq.queue.Len() +} + +type ParallelExecutionResult struct { + TxIO *TxnInputOutput + Stats *[][]uint64 + Deps *DAG +} + +const numGoProcs = 2 +const numSpeculativeProcs = 16 + +// Max number of pre-validation to run per loop +const preValidateLimit = 5 + +// Max number of times a transaction (t) can be executed before its dependency is resolved to its previous tx (t-1) +const maxIncarnation = 2 // nolint: gocognit -func ExecuteParallel(tasks []ExecTask) (lastTxIO *TxnInputOutput, err error) { +// A stateless executor that executes transactions in parallel +func ExecuteParallel(tasks []ExecTask, profile bool) (ParallelExecutionResult, error) { if len(tasks) == 0 { - return MakeTxnInputOutput(len(tasks)), nil + return ParallelExecutionResult{MakeTxnInputOutput(len(tasks)), nil, nil}, nil } + // Stores the execution statistics for each task + stats := make([][]uint64, 0, len(tasks)) + statsMutex := sync.Mutex{} + + // Channel for tasks that should be prioritized chTasks := make(chan ExecVersionView, len(tasks)) - chResults := make(chan ExecResult, len(tasks)) - chDone := make(chan bool) - mutMap := map[common.Address]*sync.RWMutex{} - for _, t := range tasks { - if _, ok := mutMap[t.Sender()]; !ok { - mutMap[t.Sender()] = &sync.RWMutex{} - } + // Channel for speculative tasks + chSpeculativeTasks := make(chan struct{}, len(tasks)) + + // A priority queue that stores speculative tasks + specTaskQueue := NewSafePriorityQueue(len(tasks)) + + // Channel to signal that the result of a transaction could be written to storage + chSettle := make(chan int, len(tasks)) + + // Channel to signal that a transaction has finished executing + chResults := make(chan struct{}, len(tasks)) + + // A priority queue that stores the transaction index of results, so we can validate the results in order + resultQueue := NewSafePriorityQueue(len(tasks)) + + // A wait group to wait for all settling tasks to finish + var settleWg sync.WaitGroup + + // An integer that tracks the index of last settled transaction + lastSettled := -1 + + // For a task that runs only after all of its preceding tasks have finished and passed validation, + // its result will be absolutely valid and therefore its validation could be skipped. + // This map stores the boolean value indicating whether a task satisfy this condition ( absolutely valid). + skipCheck := make(map[int]bool) + + for i := 0; i < len(tasks); i++ { + skipCheck[i] = false } - var cntExec, cntSuccess, cntAbort, cntTotalValidations, cntValidationFail int - - for i := 0; i < numGoProcs; i++ { - go func(procNum int, t chan ExecVersionView) { - Loop: - for { - select { - case task := <-t: - { - m := mutMap[task.sender] - if !m.TryLock() { - // why not this? -> chTasks <- task - t <- task - } else { - res := task.Execute() - chResults <- res - m.Unlock() - } - } - case <-chDone: - break Loop - } - } - log.Debug("blockstm", "proc done", procNum) // TODO: logging ... - }(i, chTasks) - } - - mvh := MakeMVHashMap() - + // Execution tasks stores the state of each execution task execTasks := makeStatusManager(len(tasks)) + + // Validate tasks stores the state of each validation task validateTasks := makeStatusManager(0) - // bootstrap execution - for x := 0; x < numGoProcs; x++ { - tx := execTasks.takeNextPending() - if tx != -1 { - cntExec++ - - log.Debug("blockstm", "bootstrap: proc", x, "executing task", tx) - chTasks <- ExecVersionView{ver: Version{tx, 0}, et: tasks[tx], mvh: mvh, sender: tasks[tx].Sender()} - } - } - - lastTxIO = MakeTxnInputOutput(len(tasks)) - txIncarnations := make([]int, len(tasks)) + // Stats for debugging purposes + var cntExec, cntSuccess, cntAbort, cntTotalValidations, cntValidationFail int diagExecSuccess := make([]int, len(tasks)) diagExecAbort := make([]int, len(tasks)) - for { - res := <-chResults - switch res.err { - case nil: - { - mvh.FlushMVWriteSet(res.txAllOut) - lastTxIO.recordRead(res.ver.TxnIndex, res.txIn) - if res.ver.Incarnation == 0 { - lastTxIO.recordWrite(res.ver.TxnIndex, res.txOut) - lastTxIO.recordAllWrite(res.ver.TxnIndex, res.txAllOut) - } else { - if res.txAllOut.hasNewWrite(lastTxIO.AllWriteSet(res.ver.TxnIndex)) { - log.Debug("blockstm", "Revalidate completed txs greater than current tx: ", res.ver.TxnIndex) - validateTasks.pushPendingSet(execTasks.getRevalidationRange(res.ver.TxnIndex)) - } + // Initialize MVHashMap + mvh := MakeMVHashMap() - prevWrite := lastTxIO.AllWriteSet(res.ver.TxnIndex) + // Stores the inputs and outputs of the last incardanotion of all transactions + lastTxIO := MakeTxnInputOutput(len(tasks)) - // Remove entries that were previously written but are no longer written + // Tracks the incarnation number of each transaction + txIncarnations := make([]int, len(tasks)) - cmpMap := make(map[string]bool) + // A map that stores the estimated dependency of a transaction if it is aborted without any known dependency + estimateDeps := make(map[int][]int, len(tasks)) - for _, w := range res.txAllOut { - cmpMap[string(w.Path)] = true - } + for i := 0; i < len(tasks); i++ { + estimateDeps[i] = make([]int, 0) + } - for _, v := range prevWrite { - if _, ok := cmpMap[string(v.Path)]; !ok { - mvh.Delete(v.Path, res.ver.TxnIndex) - } - } + // A map that records whether a transaction result has been speculatively validated + preValidated := make(map[int]bool, len(tasks)) - lastTxIO.recordWrite(res.ver.TxnIndex, res.txOut) - lastTxIO.recordAllWrite(res.ver.TxnIndex, res.txAllOut) + begin := time.Now() + + workerWg := sync.WaitGroup{} + workerWg.Add(numSpeculativeProcs + numGoProcs) + + // Launch workers that execute transactions + for i := 0; i < numSpeculativeProcs+numGoProcs; i++ { + go func(procNum int) { + defer workerWg.Done() + + doWork := func(task ExecVersionView) { + start := time.Duration(0) + if profile { + start = time.Since(begin) } - validateTasks.pushPending(res.ver.TxnIndex) - execTasks.markComplete(res.ver.TxnIndex) - if diagExecSuccess[res.ver.TxnIndex] > 0 && diagExecAbort[res.ver.TxnIndex] == 0 { - log.Debug("blockstm", "got multiple successful execution w/o abort?", "Tx", res.ver.TxnIndex, "incarnation", res.ver.Incarnation) + + res := task.Execute() + + if res.err == nil { + mvh.FlushMVWriteSet(res.txAllOut) + } + + resultQueue.Push(res.ver.TxnIndex, res) + chResults <- struct{}{} + + if profile { + end := time.Since(begin) + + stat := []uint64{uint64(res.ver.TxnIndex), uint64(res.ver.Incarnation), uint64(start), uint64(end), uint64(procNum)} + + statsMutex.Lock() + stats = append(stats, stat) + statsMutex.Unlock() } - diagExecSuccess[res.ver.TxnIndex]++ - cntSuccess++ } - case ErrExecAbort: - { - // bit of a subtle / tricky bug here. this adds the tx back to pending ... - execTasks.revertInProgress(res.ver.TxnIndex) - // ... but the incarnation needs to be bumped - txIncarnations[res.ver.TxnIndex]++ - diagExecAbort[res.ver.TxnIndex]++ - cntAbort++ - } - default: - { - err = res.err - break + + if procNum < numSpeculativeProcs { + for range chSpeculativeTasks { + doWork(specTaskQueue.Pop().(ExecVersionView)) + } + } else { + for task := range chTasks { + doWork(task) + } } + }(i) + } + + // Launch a worker that settles valid transactions + settleWg.Add(len(tasks)) + + go func() { + for t := range chSettle { + tasks[t].Settle() + settleWg.Done() + } + }() + + // bootstrap first execution + tx := execTasks.takeNextPending() + if tx != -1 { + cntExec++ + + chTasks <- ExecVersionView{ver: Version{tx, 0}, et: tasks[tx], mvh: mvh, sender: tasks[tx].Sender()} + } + + // Before starting execution, going through each task to check their explicit dependencies (whether they are coming from the same account) + prevSenderTx := make(map[common.Address]int) + + for i, t := range tasks { + if tx, ok := prevSenderTx[t.Sender()]; ok { + execTasks.addDependencies(tx, i) + execTasks.clearPending(i) } - // if we got more work, queue one up... - nextTx := execTasks.takeNextPending() - if nextTx != -1 { - cntExec++ - chTasks <- ExecVersionView{ver: Version{nextTx, txIncarnations[nextTx]}, et: tasks[nextTx], mvh: mvh, sender: tasks[nextTx].Sender()} + prevSenderTx[t.Sender()] = i + } + + var res ExecResult + + var err error + + // Start main validation loop + // nolint:nestif + for range chResults { + res = resultQueue.Pop().(ExecResult) + tx := res.ver.TxnIndex + + if res.err == nil { + lastTxIO.recordRead(tx, res.txIn) + + if res.ver.Incarnation == 0 { + lastTxIO.recordWrite(tx, res.txOut) + lastTxIO.recordAllWrite(tx, res.txAllOut) + } else { + if res.txAllOut.hasNewWrite(lastTxIO.AllWriteSet(tx)) { + validateTasks.pushPendingSet(execTasks.getRevalidationRange(tx + 1)) + } + + prevWrite := lastTxIO.AllWriteSet(tx) + + // Remove entries that were previously written but are no longer written + + cmpMap := make(map[Key]bool) + + for _, w := range res.txAllOut { + cmpMap[w.Path] = true + } + + for _, v := range prevWrite { + if _, ok := cmpMap[v.Path]; !ok { + mvh.Delete(v.Path, tx) + } + } + + lastTxIO.recordWrite(tx, res.txOut) + lastTxIO.recordAllWrite(tx, res.txAllOut) + } + + validateTasks.pushPending(tx) + execTasks.markComplete(tx) + diagExecSuccess[tx]++ + cntSuccess++ + + execTasks.removeDependency(tx) + } else if execErr, ok := res.err.(ErrExecAbortError); ok { + + addedDependencies := false + + if execErr.Dependency >= 0 { + l := len(estimateDeps[tx]) + for l > 0 && estimateDeps[tx][l-1] > execErr.Dependency { + execTasks.removeDependency(estimateDeps[tx][l-1]) + estimateDeps[tx] = estimateDeps[tx][:l-1] + l-- + } + if txIncarnations[tx] < maxIncarnation { + addedDependencies = execTasks.addDependencies(execErr.Dependency, tx) + } else { + addedDependencies = execTasks.addDependencies(tx-1, tx) + } + } else { + estimate := 0 + + if len(estimateDeps[tx]) > 0 { + estimate = estimateDeps[tx][len(estimateDeps[tx])-1] + } + addedDependencies = execTasks.addDependencies(estimate, tx) + newEstimate := estimate + (estimate+tx)/2 + if newEstimate >= tx { + newEstimate = tx - 1 + } + estimateDeps[tx] = append(estimateDeps[tx], newEstimate) + } + + execTasks.clearInProgress(tx) + if !addedDependencies { + execTasks.pushPending(tx) + } + txIncarnations[tx]++ + diagExecAbort[tx]++ + cntAbort++ + } else { + err = res.err + break } // do validations ... maxComplete := execTasks.maxAllComplete() - const validationIncrement = 2 - - cntValidate := validateTasks.countPending() - // if we're currently done with all execution tasks then let's validate everything; otherwise do one increment ... - if execTasks.countComplete() != len(tasks) && cntValidate > validationIncrement { - cntValidate = validationIncrement - } - var toValidate []int - for i := 0; i < cntValidate; i++ { - if validateTasks.minPending() <= maxComplete { - toValidate = append(toValidate, validateTasks.takeNextPending()) - } else { - break - } + for validateTasks.minPending() <= maxComplete && validateTasks.minPending() >= 0 { + toValidate = append(toValidate, validateTasks.takeNextPending()) } for i := 0; i < len(toValidate); i++ { cntTotalValidations++ tx := toValidate[i] - log.Debug("blockstm", "validating task", tx) - if ValidateVersion(tx, lastTxIO, mvh) { - log.Debug("blockstm", "* completed validation task", tx) + if skipCheck[tx] || ValidateVersion(tx, lastTxIO, mvh) { validateTasks.markComplete(tx) } else { - log.Debug("blockstm", "* validation task FAILED", tx) cntValidationFail++ diagExecAbort[tx]++ for _, v := range lastTxIO.AllWriteSet(tx) { @@ -222,38 +402,138 @@ func ExecuteParallel(tasks []ExecTask) (lastTxIO *TxnInputOutput, err error) { // 'create validation tasks for all transactions > tx ...' validateTasks.pushPendingSet(execTasks.getRevalidationRange(tx + 1)) validateTasks.clearInProgress(tx) // clear in progress - pending will be added again once new incarnation executes - if execTasks.checkPending(tx) { - // println() // have to think about this ... - } else { - execTasks.pushPending(tx) - execTasks.clearComplete(tx) - txIncarnations[tx]++ + + addedDependencies := false + if txIncarnations[tx] >= maxIncarnation { + addedDependencies = execTasks.addDependencies(tx-1, tx) } + + execTasks.clearComplete(tx) + if !addedDependencies { + execTasks.pushPending(tx) + } + + preValidated[tx] = false + txIncarnations[tx]++ } } - // if we didn't queue work previously, do check again so we keep making progress ... - if nextTx == -1 { - nextTx = execTasks.takeNextPending() + preValidateCount := 0 + invalidated := []int{} + + i := sort.SearchInts(validateTasks.pending, maxComplete+1) + + for i < len(validateTasks.pending) && preValidateCount < preValidateLimit { + tx := validateTasks.pending[i] + + if !preValidated[tx] { + cntTotalValidations++ + + if !ValidateVersion(tx, lastTxIO, mvh) { + cntValidationFail++ + diagExecAbort[tx]++ + + invalidated = append(invalidated, tx) + + if execTasks.checkComplete(tx) { + execTasks.clearComplete(tx) + } + + if !execTasks.checkInProgress(tx) { + for _, v := range lastTxIO.AllWriteSet(tx) { + mvh.MarkEstimate(v.Path, tx) + } + + validateTasks.pushPendingSet(execTasks.getRevalidationRange(tx + 1)) + + addedDependencies := false + if txIncarnations[tx] >= maxIncarnation { + addedDependencies = execTasks.addDependencies(tx-1, tx) + } + + if !addedDependencies { + execTasks.pushPending(tx) + } + } + + txIncarnations[tx]++ + + preValidated[tx] = false + } else { + preValidated[tx] = true + } + preValidateCount++ + } + + i++ + } + + for _, tx := range invalidated { + validateTasks.clearPending(tx) + } + + // Settle transactions that have been validated to be correct and that won't be re-executed again + maxValidated := validateTasks.maxAllComplete() + + for lastSettled < maxValidated { + lastSettled++ + if execTasks.checkInProgress(lastSettled) || execTasks.checkPending(lastSettled) || execTasks.blockCount[lastSettled] >= 0 { + lastSettled-- + break + } + chSettle <- lastSettled + } + + if validateTasks.countComplete() == len(tasks) && execTasks.countComplete() == len(tasks) { + log.Debug("blockstm exec summary", "execs", cntExec, "success", cntSuccess, "aborts", cntAbort, "validations", cntTotalValidations, "failures", cntValidationFail, "#tasks/#execs", fmt.Sprintf("%.2f%%", float64(len(tasks))/float64(cntExec)*100)) + break + } + + // Send the next immediate pending transaction to be executed + if execTasks.minPending() != -1 && execTasks.minPending() == maxValidated+1 { + nextTx := execTasks.takeNextPending() if nextTx != -1 { cntExec++ - log.Debug("blockstm", "# tx queued up", nextTx) + skipCheck[nextTx] = true + chTasks <- ExecVersionView{ver: Version{nextTx, txIncarnations[nextTx]}, et: tasks[nextTx], mvh: mvh, sender: tasks[nextTx].Sender()} } } - if validateTasks.countComplete() == len(tasks) && execTasks.countComplete() == len(tasks) { - log.Debug("blockstm exec summary", "execs", cntExec, "success", cntSuccess, "aborts", cntAbort, "validations", cntTotalValidations, "failures", cntValidationFail) - break + // Send speculative tasks + for execTasks.peekPendingGE(maxValidated+3) != -1 || len(execTasks.inProgress) == 0 { + // We skip the next transaction to avoid the case where they all have conflicts and could not be + // scheduled for re-execution immediately even when it's their time to run, because they are already in + // speculative queue. + nextTx := execTasks.takePendingGE(maxValidated + 3) + + if nextTx == -1 { + nextTx = execTasks.takeNextPending() + } + + if nextTx != -1 { + cntExec++ + + task := ExecVersionView{ver: Version{nextTx, txIncarnations[nextTx]}, et: tasks[nextTx], mvh: mvh, sender: tasks[nextTx].Sender()} + + specTaskQueue.Push(nextTx, task) + chSpeculativeTasks <- struct{}{} + } } } - for i := 0; i < numGoProcs; i++ { - chDone <- true - } close(chTasks) + close(chSpeculativeTasks) + workerWg.Wait() close(chResults) + settleWg.Wait() + close(chSettle) - return + var dag DAG + if profile { + dag = BuildDAG(*lastTxIO) + } + + return ParallelExecutionResult{lastTxIO, &stats, &dag}, err } diff --git a/core/blockstm/executor_test.go b/core/blockstm/executor_test.go new file mode 100644 index 0000000000..47c875007b --- /dev/null +++ b/core/blockstm/executor_test.go @@ -0,0 +1,470 @@ +package blockstm + +import ( + "fmt" + "math/big" + "math/rand" + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/log" +) + +type OpType int + +const readType = 0 +const writeType = 1 +const otherType = 2 + +type Op struct { + key Key + duration time.Duration + opType OpType + val int +} + +type testExecTask struct { + txIdx int + ops []Op + readMap map[Key]ReadDescriptor + writeMap map[Key]WriteDescriptor + sender common.Address + nonce int +} + +type PathGenerator func(addr common.Address, j int, total int) Key + +type TaskRunner func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration) + +type Timer func(txIdx int, opIdx int) time.Duration + +type Sender func(int) common.Address + +func NewTestExecTask(txIdx int, ops []Op, sender common.Address, nonce int) *testExecTask { + return &testExecTask{ + txIdx: txIdx, + ops: ops, + readMap: make(map[Key]ReadDescriptor), + writeMap: make(map[Key]WriteDescriptor), + sender: sender, + nonce: nonce, + } +} + +func sleep(i time.Duration) { + start := time.Now() + for time.Since(start) < i { + } +} + +func (t *testExecTask) Execute(mvh *MVHashMap, incarnation int) error { + // Sleep for 50 microsecond to simulate setup time + sleep(time.Microsecond * 50) + + version := Version{TxnIndex: t.txIdx, Incarnation: incarnation} + + t.readMap = make(map[Key]ReadDescriptor) + t.writeMap = make(map[Key]WriteDescriptor) + + deps := -1 + + for i, op := range t.ops { + k := op.key + + switch op.opType { + case readType: + if _, ok := t.writeMap[k]; ok { + sleep(op.duration) + continue + } + + result := mvh.Read(k, t.txIdx) + + val := result.Value() + + if i == 0 && val != nil && (val.(int) != t.nonce) { + return ErrExecAbortError{} + } + + if result.Status() == MVReadResultDependency { + if result.depIdx > deps { + deps = result.depIdx + } + } + + var readKind int + + if result.Status() == MVReadResultDone { + readKind = ReadKindMap + } else if result.Status() == MVReadResultNone { + readKind = ReadKindStorage + } + + sleep(op.duration) + + t.readMap[k] = ReadDescriptor{k, readKind, Version{TxnIndex: result.depIdx, Incarnation: result.incarnation}} + case writeType: + t.writeMap[k] = WriteDescriptor{k, version, op.val} + case otherType: + sleep(op.duration) + default: + panic(fmt.Sprintf("Unknown op type: %d", op.opType)) + } + } + + if deps != -1 { + return ErrExecAbortError{deps} + } + + return nil +} + +func (t *testExecTask) MVWriteList() []WriteDescriptor { + return t.MVFullWriteList() +} + +func (t *testExecTask) MVFullWriteList() []WriteDescriptor { + writes := make([]WriteDescriptor, 0, len(t.writeMap)) + + for _, v := range t.writeMap { + writes = append(writes, v) + } + + return writes +} + +func (t *testExecTask) MVReadList() []ReadDescriptor { + reads := make([]ReadDescriptor, 0, len(t.readMap)) + + for _, v := range t.readMap { + reads = append(reads, v) + } + + return reads +} + +func (t *testExecTask) Settle() {} + +func (t *testExecTask) Sender() common.Address { + return t.sender +} + +func randTimeGenerator(min time.Duration, max time.Duration) func(txIdx int, opIdx int) time.Duration { + return func(txIdx int, opIdx int) time.Duration { + return time.Duration(rand.Int63n(int64(max-min))) + min + } +} + +func longTailTimeGenerator(min time.Duration, max time.Duration, i int, j int) func(txIdx int, opIdx int) time.Duration { + return func(txIdx int, opIdx int) time.Duration { + if txIdx%i == 0 && opIdx == j { + return max * 100 + } else { + return time.Duration(rand.Int63n(int64(max-min))) + min + } + } +} + +var randomPathGenerator = func(sender common.Address, j int, total int) Key { + return NewStateKey(sender, common.BigToHash((big.NewInt(int64(total))))) +} + +var dexPathGenerator = func(sender common.Address, j int, total int) Key { + if j == total-1 || j == 2 { + return NewSubpathKey(common.BigToAddress(big.NewInt(int64(0))), 1) + } else { + return NewSubpathKey(common.BigToAddress(big.NewInt(int64(j))), 1) + } +} + +var readTime = randTimeGenerator(4*time.Microsecond, 12*time.Microsecond) +var writeTime = randTimeGenerator(2*time.Microsecond, 6*time.Microsecond) +var nonIOTime = randTimeGenerator(1*time.Microsecond, 2*time.Microsecond) + +func taskFactory(numTask int, sender Sender, readsPerT int, writesPerT int, nonIOPerT int, pathGenerator PathGenerator, readTime Timer, writeTime Timer, nonIOTime Timer) ([]ExecTask, time.Duration) { + exec := make([]ExecTask, 0, numTask) + + var serialDuration time.Duration + + senderNonces := make(map[common.Address]int) + + for i := 0; i < numTask; i++ { + s := sender(i) + + // Set first two ops to always read and write nonce + ops := make([]Op, 0, readsPerT+writesPerT+nonIOPerT) + + ops = append(ops, Op{opType: readType, key: NewSubpathKey(s, 2), duration: readTime(i, 0), val: senderNonces[s]}) + + senderNonces[s]++ + + ops = append(ops, Op{opType: writeType, key: NewSubpathKey(s, 2), duration: writeTime(i, 1), val: senderNonces[s]}) + + for j := 0; j < readsPerT-1; j++ { + ops = append(ops, Op{opType: readType}) + } + + for j := 0; j < nonIOPerT; j++ { + ops = append(ops, Op{opType: otherType}) + } + + for j := 0; j < writesPerT-1; j++ { + ops = append(ops, Op{opType: writeType}) + } + + // shuffle ops except for the first three (read nonce, write nonce, another read) ops and last write op. + // This enables random path generator to generate deterministic paths for these "special" ops. + for j := 3; j < len(ops)-1; j++ { + k := rand.Intn(len(ops)-j-1) + j + ops[j], ops[k] = ops[k], ops[j] + } + + // Generate time and key path for each op except first two that are always read and write nonce + for j := 2; j < len(ops); j++ { + if ops[j].opType == readType { + ops[j].key = pathGenerator(s, j, len(ops)) + ops[j].duration = readTime(i, j) + } else if ops[j].opType == writeType { + ops[j].key = pathGenerator(s, j, len(ops)) + ops[j].duration = writeTime(i, j) + } else { + ops[j].duration = nonIOTime(i, j) + } + + serialDuration += ops[j].duration + } + + if ops[len(ops)-1].opType != writeType { + panic("Last op must be a write") + } + + t := NewTestExecTask(i, ops, s, senderNonces[s]-1) + exec = append(exec, t) + } + + return exec, serialDuration +} + +func testExecutorComb(t *testing.T, totalTxs []int, numReads []int, numWrites []int, numNonIO []int, taskRunner TaskRunner) { + t.Helper() + log.Root().SetHandler(log.LvlFilterHandler(log.LvlDebug, log.StreamHandler(os.Stderr, log.TerminalFormat(false)))) + + improved := 0 + total := 0 + + totalExecDuration := time.Duration(0) + totalSerialDuration := time.Duration(0) + + for _, numTx := range totalTxs { + for _, numRead := range numReads { + for _, numWrite := range numWrites { + for _, numNonIO := range numNonIO { + log.Info("Executing block", "numTx", numTx, "numRead", numRead, "numWrite", numWrite, "numNonIO", numNonIO) + execDuration, expectedSerialDuration := taskRunner(numTx, numRead, numWrite, numNonIO) + + if execDuration < expectedSerialDuration { + improved++ + } + total++ + + performance := "✅" + + if execDuration >= expectedSerialDuration { + performance = "❌" + } + + fmt.Printf("exec duration %v, serial duration %v, time reduced %v %.2f%%, %v \n", execDuration, expectedSerialDuration, expectedSerialDuration-execDuration, float64(expectedSerialDuration-execDuration)/float64(expectedSerialDuration)*100, performance) + + totalExecDuration += execDuration + totalSerialDuration += expectedSerialDuration + } + } + } + } + + fmt.Println("Improved: ", improved, "Total: ", total, "success rate: ", float64(improved)/float64(total)*100) + fmt.Printf("Total exec duration: %v, total serial duration: %v, time reduced: %v, time reduced percent: %.2f%%\n", totalExecDuration, totalSerialDuration, totalSerialDuration-totalExecDuration, float64(totalSerialDuration-totalExecDuration)/float64(totalSerialDuration)*100) +} + +func runParallel(t *testing.T, tasks []ExecTask, validation func(TxnInputOutput) bool) time.Duration { + t.Helper() + + start := time.Now() + results, _ := ExecuteParallel(tasks, false) + + txio := results.TxIO + + // Need to apply the final write set to storage + + finalWriteSet := make(map[Key]time.Duration) + + for _, task := range tasks { + task := task.(*testExecTask) + for _, op := range task.ops { + if op.opType == writeType { + finalWriteSet[op.key] = op.duration + } + } + } + + for _, v := range finalWriteSet { + sleep(v) + } + + duration := time.Since(start) + + if validation != nil { + assert.True(t, validation(*txio)) + } + + return duration +} + +func TestLessConflicts(t *testing.T) { + t.Parallel() + rand.Seed(0) + + totalTxs := []int{10, 50, 100, 200, 300} + numReads := []int{20, 100, 200} + numWrites := []int{20, 100, 200} + numNonIO := []int{100, 500} + + taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration) { + sender := func(i int) common.Address { + randomness := rand.Intn(10) + 10 + return common.BigToAddress(big.NewInt(int64(i % randomness))) + } + tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, readTime, writeTime, nonIOTime) + + return runParallel(t, tasks, nil), serialDuration + } + + testExecutorComb(t, totalTxs, numReads, numWrites, numNonIO, taskRunner) +} + +func TestAlternatingTx(t *testing.T) { + t.Parallel() + rand.Seed(0) + + totalTxs := []int{200} + numReads := []int{20} + numWrites := []int{20} + numNonIO := []int{100} + + taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration) { + sender := func(i int) common.Address { return common.BigToAddress(big.NewInt(int64(i % 2))) } + tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, readTime, writeTime, nonIOTime) + + return runParallel(t, tasks, nil), serialDuration + } + + testExecutorComb(t, totalTxs, numReads, numWrites, numNonIO, taskRunner) +} + +func TestMoreConflicts(t *testing.T) { + t.Parallel() + rand.Seed(0) + + totalTxs := []int{10, 50, 100, 200, 300} + numReads := []int{20, 100, 200} + numWrites := []int{20, 100, 200} + numNonIO := []int{100, 500} + + taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration) { + sender := func(i int) common.Address { + randomness := rand.Intn(10) + 10 + return common.BigToAddress(big.NewInt(int64(i / randomness))) + } + tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, readTime, writeTime, nonIOTime) + + return runParallel(t, tasks, nil), serialDuration + } + + testExecutorComb(t, totalTxs, numReads, numWrites, numNonIO, taskRunner) +} + +func TestRandomTx(t *testing.T) { + t.Parallel() + rand.Seed(0) + + totalTxs := []int{10, 50, 100, 200, 300} + numReads := []int{20, 100, 200} + numWrites := []int{20, 100, 200} + numNonIO := []int{100, 500} + + taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration) { + // Randomly assign this tx to one of 10 senders + sender := func(i int) common.Address { return common.BigToAddress(big.NewInt(int64(rand.Intn(10)))) } + tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, readTime, writeTime, nonIOTime) + + return runParallel(t, tasks, nil), serialDuration + } + + testExecutorComb(t, totalTxs, numReads, numWrites, numNonIO, taskRunner) +} + +func TestTxWithLongTailRead(t *testing.T) { + t.Parallel() + rand.Seed(0) + + totalTxs := []int{10, 50, 100, 200, 300} + numReads := []int{20, 100, 200} + numWrites := []int{20, 100, 200} + numNonIO := []int{100, 500} + + taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration) { + sender := func(i int) common.Address { + randomness := rand.Intn(10) + 10 + return common.BigToAddress(big.NewInt(int64(i / randomness))) + } + + longTailReadTimer := longTailTimeGenerator(4*time.Microsecond, 12*time.Microsecond, 7, 10) + + tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, longTailReadTimer, writeTime, nonIOTime) + + return runParallel(t, tasks, nil), serialDuration + } + + testExecutorComb(t, totalTxs, numReads, numWrites, numNonIO, taskRunner) +} + +func TestDexScenario(t *testing.T) { + t.Parallel() + rand.Seed(0) + + totalTxs := []int{10, 50, 100, 200, 300} + numReads := []int{20, 100, 200} + numWrites := []int{20, 100, 200} + numNonIO := []int{100, 500} + + validation := func(txio TxnInputOutput) bool { + for i, inputs := range txio.inputs { + foundDep := false + + for _, input := range inputs { + if input.V.TxnIndex == i-1 { + foundDep = true + } + } + + if !foundDep { + return false + } + } + + return true + } + + taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration) { + sender := func(i int) common.Address { return common.BigToAddress(big.NewInt(int64(i))) } + tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, dexPathGenerator, readTime, writeTime, nonIOTime) + + return runParallel(t, tasks, validation), serialDuration + } + + testExecutorComb(t, totalTxs, numReads, numWrites, numNonIO, taskRunner) +} diff --git a/core/blockstm/mvhashmap.go b/core/blockstm/mvhashmap.go index 52a5487b5d..a04fbfd6f0 100644 --- a/core/blockstm/mvhashmap.go +++ b/core/blockstm/mvhashmap.go @@ -6,22 +6,79 @@ import ( "github.com/emirpasic/gods/maps/treemap" - "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/common" ) const FlagDone = 0 const FlagEstimate = 1 +const addressType = 1 +const stateType = 2 +const subpathType = 3 + +const KeyLength = common.AddressLength + common.HashLength + 2 + +type Key [KeyLength]byte + +func (k *Key) IsAddress() bool { + return k[KeyLength-1] == addressType +} + +func (k *Key) IsState() bool { + return k[KeyLength-1] == stateType +} + +func (k *Key) IsSubpath() bool { + return k[KeyLength-1] == subpathType +} + +func (k *Key) GetAddress() common.Address { + return common.BytesToAddress(k[:common.AddressLength]) +} + +func (k *Key) GetStateKey() common.Hash { + return common.BytesToHash(k[common.AddressLength : KeyLength-2]) +} + +func (k *Key) GetSubpath() byte { + return k[KeyLength-2] +} + +func newKey(addr common.Address, hash common.Hash, subpath byte, keyType byte) Key { + var k Key + + copy(k[:common.AddressLength], addr.Bytes()) + copy(k[common.AddressLength:KeyLength-2], hash.Bytes()) + k[KeyLength-2] = subpath + k[KeyLength-1] = keyType + + return k +} + +func NewAddressKey(addr common.Address) Key { + return newKey(addr, common.Hash{}, 0, addressType) +} + +func NewStateKey(addr common.Address, hash common.Hash) Key { + k := newKey(addr, hash, 0, stateType) + if !k.IsState() { + panic(fmt.Errorf("key is not a state key")) + } + + return k +} + +func NewSubpathKey(addr common.Address, subpath byte) Key { + return newKey(addr, common.Hash{}, subpath, subpathType) +} + type MVHashMap struct { - rw sync.RWMutex - m map[string]*TxnIndexCells // TODO: might want a more efficient key representation + m sync.Map + s sync.Map } func MakeMVHashMap() *MVHashMap { - return &MVHashMap{ - rw: sync.RWMutex{}, - m: make(map[string]*TxnIndexCells), - } + return &MVHashMap{} } type WriteCell struct { @@ -40,80 +97,86 @@ type Version struct { Incarnation int } -func (mv *MVHashMap) getKeyCells(k []byte, fNoKey func(kenc string) *TxnIndexCells) (cells *TxnIndexCells) { - kenc := string(k) - - var ok bool - - mv.rw.RLock() - cells, ok = mv.m[kenc] - mv.rw.RUnlock() +func (mv *MVHashMap) getKeyCells(k Key, fNoKey func(kenc Key) *TxnIndexCells) (cells *TxnIndexCells) { + val, ok := mv.m.Load(k) if !ok { - cells = fNoKey(kenc) + cells = fNoKey(k) + } else { + cells = val.(*TxnIndexCells) } return } -func (mv *MVHashMap) Write(k []byte, v Version, data interface{}) { - cells := mv.getKeyCells(k, func(kenc string) (cells *TxnIndexCells) { +func (mv *MVHashMap) Write(k Key, v Version, data interface{}) { + cells := mv.getKeyCells(k, func(kenc Key) (cells *TxnIndexCells) { n := &TxnIndexCells{ rw: sync.RWMutex{}, tm: treemap.NewWithIntComparator(), } - var ok bool - mv.rw.Lock() - if cells, ok = mv.m[kenc]; !ok { - mv.m[kenc] = n - cells = n - } - mv.rw.Unlock() + cells = n + val, _ := mv.m.LoadOrStore(kenc, n) + cells = val.(*TxnIndexCells) return }) - // TODO: could probably have a scheme where this only generally requires a read lock since any given transaction transaction - // should only have one incarnation executing at a time... - cells.rw.Lock() - defer cells.rw.Unlock() + cells.rw.RLock() ci, ok := cells.tm.Get(v.TxnIndex) + cells.rw.RUnlock() if ok { if ci.(*WriteCell).incarnation > v.Incarnation { panic(fmt.Errorf("existing transaction value does not have lower incarnation: %v, %v", - string(k), v.TxnIndex)) - } else if ci.(*WriteCell).flag == FlagEstimate { - log.Debug("mvhashmap marking previous estimate as done", "tx index", v.TxnIndex, "incarnation", v.Incarnation) + k, v.TxnIndex)) } ci.(*WriteCell).flag = FlagDone ci.(*WriteCell).incarnation = v.Incarnation ci.(*WriteCell).data = data } else { - cells.tm.Put(v.TxnIndex, &WriteCell{ - flag: FlagDone, - incarnation: v.Incarnation, - data: data, - }) + cells.rw.Lock() + if ci, ok = cells.tm.Get(v.TxnIndex); !ok { + cells.tm.Put(v.TxnIndex, &WriteCell{ + flag: FlagDone, + incarnation: v.Incarnation, + data: data, + }) + } else { + ci.(*WriteCell).flag = FlagDone + ci.(*WriteCell).incarnation = v.Incarnation + ci.(*WriteCell).data = data + } + cells.rw.Unlock() } } -func (mv *MVHashMap) MarkEstimate(k []byte, txIdx int) { - cells := mv.getKeyCells(k, func(_ string) *TxnIndexCells { +func (mv *MVHashMap) ReadStorage(k Key, fallBack func() any) any { + data, ok := mv.s.Load(string(k[:])) + if !ok { + data = fallBack() + data, _ = mv.s.LoadOrStore(string(k[:]), data) + } + + return data +} + +func (mv *MVHashMap) MarkEstimate(k Key, txIdx int) { + cells := mv.getKeyCells(k, func(_ Key) *TxnIndexCells { panic(fmt.Errorf("path must already exist")) }) cells.rw.RLock() if ci, ok := cells.tm.Get(txIdx); !ok { - panic("should not happen - cell should be present for path") + panic(fmt.Sprintf("should not happen - cell should be present for path. TxIdx: %v, path, %x, cells keys: %v", txIdx, k, cells.tm.Keys())) } else { ci.(*WriteCell).flag = FlagEstimate } cells.rw.RUnlock() } -func (mv *MVHashMap) Delete(k []byte, txIdx int) { - cells := mv.getKeyCells(k, func(_ string) *TxnIndexCells { +func (mv *MVHashMap) Delete(k Key, txIdx int) { + cells := mv.getKeyCells(k, func(_ Key) *TxnIndexCells { panic(fmt.Errorf("path must already exist")) }) @@ -158,11 +221,11 @@ func (mvr MVReadResult) Status() int { return MVReadResultNone } -func (mv *MVHashMap) Read(k []byte, txIdx int) (res MVReadResult) { +func (mv *MVHashMap) Read(k Key, txIdx int) (res MVReadResult) { res.depIdx = -1 res.incarnation = -1 - cells := mv.getKeyCells(k, func(_ string) *TxnIndexCells { + cells := mv.getKeyCells(k, func(_ Key) *TxnIndexCells { return nil }) if cells == nil { @@ -170,9 +233,10 @@ func (mv *MVHashMap) Read(k []byte, txIdx int) (res MVReadResult) { } cells.rw.RLock() - defer cells.rw.RUnlock() + fk, fv := cells.tm.Floor(txIdx - 1) + cells.rw.RUnlock() - if fk, fv := cells.tm.Floor(txIdx - 1); fk != nil && fv != nil { + if fk != nil && fv != nil { c := fv.(*WriteCell) switch c.flag { case FlagEstimate: diff --git a/core/blockstm/status.go b/core/blockstm/status.go index 759abf63eb..f10957330c 100644 --- a/core/blockstm/status.go +++ b/core/blockstm/status.go @@ -11,6 +11,13 @@ func makeStatusManager(numTasks int) (t taskStatusManager) { t.pending[i] = i } + t.dependency = make(map[int]map[int]bool, numTasks) + t.blockCount = make(map[int]int, numTasks) + + for i := 0; i < numTasks; i++ { + t.blockCount[i] = -1 + } + return } @@ -18,6 +25,8 @@ type taskStatusManager struct { pending []int inProgress []int complete []int + dependency map[int]map[int]bool + blockCount map[int]int } func insertInList(l []int, v int) []int { @@ -47,6 +56,35 @@ func (m *taskStatusManager) takeNextPending() int { return x } +func (m *taskStatusManager) peekPendingGE(n int) int { + x := sort.SearchInts(m.pending, n) + if x >= len(m.pending) { + return -1 + } + + return m.pending[x] +} + +// Take a pending task whose transaction index is greater than or equal to the given tx index +func (m *taskStatusManager) takePendingGE(n int) int { + x := sort.SearchInts(m.pending, n) + if x >= len(m.pending) { + return -1 + } + + v := m.pending[x] + + if x < len(m.pending)-1 { + m.pending = append(m.pending[:x], m.pending[x+1:]...) + } else { + m.pending = m.pending[:x] + } + + m.inProgress = insertInList(m.inProgress, v) + + return v +} + func hasNoGap(l []int) bool { return l[0]+len(l) == l[len(l)-1]+1 } @@ -68,7 +106,11 @@ func (m taskStatusManager) maxAllComplete() int { } func (m *taskStatusManager) pushPending(tx int) { - m.pending = insertInList(m.pending, tx) + if !m.checkComplete(tx) && !m.checkInProgress(tx) { + m.pending = insertInList(m.pending, tx) + } else { + panic(fmt.Errorf("should not happen - clear complete or inProgress before pushing pending")) + } } func removeFromList(l []int, v int, expect bool) []int { @@ -108,19 +150,52 @@ func (m *taskStatusManager) countComplete() int { return len(m.complete) } -func (m *taskStatusManager) revertInProgress(tx int) { - m.inProgress = removeFromList(m.inProgress, tx, true) - m.pending = insertInList(m.pending, tx) +func (m *taskStatusManager) addDependencies(blocker int, dependent int) bool { + if blocker < 0 || blocker >= dependent { + return false + } + + curBlocker := m.blockCount[dependent] + + if curBlocker > blocker { + return true + } + + if m.checkComplete(blocker) { + // Blocking blocker has already completed + m.blockCount[dependent] = -1 + return false + } + + if _, ok := m.dependency[blocker]; !ok { + m.dependency[blocker] = make(map[int]bool) + } + + m.dependency[blocker][dependent] = true + m.blockCount[dependent] = blocker + + return true +} + +func (m *taskStatusManager) removeDependency(tx int) { + if deps, ok := m.dependency[tx]; ok && len(deps) > 0 { + for k := range deps { + if m.blockCount[k] == tx { + m.blockCount[k] = -1 + if !m.checkComplete(k) && !m.checkPending(k) && !m.checkInProgress(k) { + m.pushPending(k) + } + } + } + + delete(m.dependency, tx) + } } func (m *taskStatusManager) clearInProgress(tx int) { m.inProgress = removeFromList(m.inProgress, tx, true) } -func (m *taskStatusManager) countPending() int { - return len(m.pending) -} - func (m *taskStatusManager) checkInProgress(tx int) bool { x := sort.SearchInts(m.inProgress, tx) if x < len(m.inProgress) && m.inProgress[x] == tx { @@ -139,8 +214,18 @@ func (m *taskStatusManager) checkPending(tx int) bool { return false } +func (m *taskStatusManager) checkComplete(tx int) bool { + x := sort.SearchInts(m.complete, tx) + if x < len(m.complete) && m.complete[x] == tx { + return true + } + + return false +} + // getRevalidationRange: this range will be all tasks from tx (inclusive) that are not currently in progress up to the -// 'all complete' limit +// +// 'all complete' limit func (m *taskStatusManager) getRevalidationRange(txFrom int) (ret []int) { max := m.maxAllComplete() // haven't learned to trust compilers :) for x := txFrom; x <= max; x++ { @@ -154,10 +239,20 @@ func (m *taskStatusManager) getRevalidationRange(txFrom int) (ret []int) { func (m *taskStatusManager) pushPendingSet(set []int) { for _, v := range set { - m.pushPending(v) + if m.checkComplete(v) { + m.clearComplete(v) + } + + if !m.checkInProgress(v) { + m.pushPending(v) + } } } func (m *taskStatusManager) clearComplete(tx int) { m.complete = removeFromList(m.complete, tx, false) } + +func (m *taskStatusManager) clearPending(tx int) { + m.pending = removeFromList(m.pending, tx, false) +} diff --git a/core/blockstm/txio.go b/core/blockstm/txio.go index 7716197acd..a08cf57d22 100644 --- a/core/blockstm/txio.go +++ b/core/blockstm/txio.go @@ -1,21 +1,18 @@ -//nolint: unused package blockstm -import "encoding/base64" - const ( ReadKindMap = 0 ReadKindStorage = 1 ) type ReadDescriptor struct { - Path []byte + Path Key Kind int V Version } type WriteDescriptor struct { - Path []byte + Path Key V Version Val interface{} } @@ -31,14 +28,14 @@ func (txo TxnOutput) hasNewWrite(cmpSet []WriteDescriptor) bool { return true } - cmpMap := map[string]bool{base64.StdEncoding.EncodeToString(cmpSet[0].Path): true} + cmpMap := map[Key]bool{cmpSet[0].Path: true} for i := 1; i < len(cmpSet); i++ { - cmpMap[base64.StdEncoding.EncodeToString(cmpSet[i].Path)] = true + cmpMap[cmpSet[i].Path] = true } for _, v := range txo { - if !cmpMap[base64.StdEncoding.EncodeToString(v.Path)] { + if !cmpMap[v.Path] { return true } } diff --git a/core/parallel_state_processor.go b/core/parallel_state_processor.go index f4a971bd5b..1267ede20b 100644 --- a/core/parallel_state_processor.go +++ b/core/parallel_state_processor.go @@ -19,6 +19,7 @@ package core import ( "fmt" "math/big" + "time" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/consensus" @@ -58,16 +59,21 @@ type ExecutionTask struct { gasLimit uint64 blockNumber *big.Int blockHash common.Hash - blockContext vm.BlockContext tx *types.Transaction index int statedb *state.StateDB // State database that stores the modified values after tx execution. cleanStateDB *state.StateDB // A clean copy of the initial statedb. It should not be modified. + finalStateDB *state.StateDB // The final statedb. + header *types.Header + blockChain *BlockChain evmConfig vm.Config result *ExecutionResult shouldDelayFeeCal *bool shouldRerunWithoutFeeDelay bool sender common.Address + totalUsedGas *uint64 + receipts *types.Receipts + allLogs *[]*types.Log } func (task *ExecutionTask) Execute(mvh *blockstm.MVHashMap, incarnation int) (err error) { @@ -76,7 +82,9 @@ func (task *ExecutionTask) Execute(mvh *blockstm.MVHashMap, incarnation int) (er task.statedb.SetMVHashmap(mvh) task.statedb.SetIncarnation(incarnation) - evm := vm.NewEVM(task.blockContext, vm.TxContext{}, task.statedb, task.config, task.evmConfig) + blockContext := NewEVMBlockContext(task.header, task.blockChain, nil) + + evm := vm.NewEVM(blockContext, vm.TxContext{}, task.statedb, task.config, task.evmConfig) // Create a new context to be used in the EVM environment. txContext := NewEVMTxContext(task.msg) @@ -85,9 +93,9 @@ func (task *ExecutionTask) Execute(mvh *blockstm.MVHashMap, incarnation int) (er defer func() { if r := recover(); r != nil { // In some pre-matured executions, EVM will panic. Recover from panic and retry the execution. - log.Debug("Recovered from EVM failure. Error:\n", r) + log.Debug("Recovered from EVM failure.", "Error:", r) - err = blockstm.ErrExecAbort + err = blockstm.ErrExecAbortError{Dependency: task.statedb.DepTxIndex()} return } @@ -97,11 +105,21 @@ func (task *ExecutionTask) Execute(mvh *blockstm.MVHashMap, incarnation int) (er if *task.shouldDelayFeeCal { task.result, err = ApplyMessageNoFeeBurnOrTip(evm, task.msg, new(GasPool).AddGas(task.gasLimit)) - if _, ok := task.statedb.MVReadMap()[string(task.blockContext.Coinbase.Bytes())]; ok { + if task.result == nil || err != nil { + return blockstm.ErrExecAbortError{Dependency: task.statedb.DepTxIndex()} + } + + reads := task.statedb.MVReadMap() + + if _, ok := reads[blockstm.NewSubpathKey(blockContext.Coinbase, state.BalancePath)]; ok { + log.Info("Coinbase is in MVReadMap", "address", blockContext.Coinbase) + task.shouldRerunWithoutFeeDelay = true } - if _, ok := task.statedb.MVReadMap()[string(task.result.BurntContractAddress.Bytes())]; ok { + if _, ok := reads[blockstm.NewSubpathKey(task.result.BurntContractAddress, state.BalancePath)]; ok { + log.Info("BurntContractAddress is in MVReadMap", "address", task.result.BurntContractAddress) + task.shouldRerunWithoutFeeDelay = true } } else { @@ -109,11 +127,11 @@ func (task *ExecutionTask) Execute(mvh *blockstm.MVHashMap, incarnation int) (er } if task.statedb.HadInvalidRead() || err != nil { - err = blockstm.ErrExecAbort + err = blockstm.ErrExecAbortError{Dependency: task.statedb.DepTxIndex()} return } - task.statedb.Finalise(false) + task.statedb.Finalise(task.config.IsEIP158(task.blockNumber)) return } @@ -134,6 +152,87 @@ func (task *ExecutionTask) Sender() common.Address { return task.sender } +func (task *ExecutionTask) Settle() { + task.finalStateDB.Prepare(task.tx.Hash(), task.index) + + coinbase, _ := task.blockChain.Engine().Author(task.header) + + coinbaseBalance := task.finalStateDB.GetBalance(coinbase) + + task.finalStateDB.ApplyMVWriteSet(task.statedb.MVWriteList()) + + for _, l := range task.statedb.GetLogs(task.tx.Hash(), task.blockHash) { + task.finalStateDB.AddLog(l) + } + + if *task.shouldDelayFeeCal { + if task.config.IsLondon(task.blockNumber) { + task.finalStateDB.AddBalance(task.result.BurntContractAddress, task.result.FeeBurnt) + } + + task.finalStateDB.AddBalance(coinbase, task.result.FeeTipped) + output1 := new(big.Int).SetBytes(task.result.SenderInitBalance.Bytes()) + output2 := new(big.Int).SetBytes(coinbaseBalance.Bytes()) + + // Deprecating transfer log and will be removed in future fork. PLEASE DO NOT USE this transfer log going forward. Parameters won't get updated as expected going forward with EIP1559 + // add transfer log + AddFeeTransferLog( + task.finalStateDB, + + task.msg.From(), + coinbase, + + task.result.FeeTipped, + task.result.SenderInitBalance, + coinbaseBalance, + output1.Sub(output1, task.result.FeeTipped), + output2.Add(output2, task.result.FeeTipped), + ) + } + + for k, v := range task.statedb.Preimages() { + task.finalStateDB.AddPreimage(k, v) + } + + // Update the state with pending changes. + var root []byte + + if task.config.IsByzantium(task.blockNumber) { + task.finalStateDB.Finalise(true) + } else { + root = task.finalStateDB.IntermediateRoot(task.config.IsEIP158(task.blockNumber)).Bytes() + } + + *task.totalUsedGas += task.result.UsedGas + + // Create a new receipt for the transaction, storing the intermediate root and gas used + // by the tx. + receipt := &types.Receipt{Type: task.tx.Type(), PostState: root, CumulativeGasUsed: *task.totalUsedGas} + if task.result.Failed() { + receipt.Status = types.ReceiptStatusFailed + } else { + receipt.Status = types.ReceiptStatusSuccessful + } + + receipt.TxHash = task.tx.Hash() + receipt.GasUsed = task.result.UsedGas + + // If the transaction created a contract, store the creation address in the receipt. + if task.msg.To() == nil { + receipt.ContractAddress = crypto.CreateAddress(task.msg.From(), task.tx.Nonce()) + } + + // Set the receipt logs and create the bloom filter. + receipt.Logs = task.finalStateDB.GetLogs(task.tx.Hash(), task.blockHash) + receipt.Bloom = types.CreateBloom(types.Receipts{receipt}) + receipt.BlockHash = task.blockHash + receipt.BlockNumber = task.blockNumber + receipt.TransactionIndex = uint(task.finalStateDB.TxIndex()) + + *task.receipts = append(*task.receipts, receipt) + *task.allLogs = append(*task.allLogs, receipt.Logs...) +} + // 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. @@ -141,6 +240,7 @@ func (task *ExecutionTask) Sender() common.Address { // 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. +// nolint:gocognit func (p *ParallelStateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg vm.Config) (types.Receipts, []*types.Log, uint64, error) { var ( receipts types.Receipts @@ -150,6 +250,7 @@ func (p *ParallelStateProcessor) Process(block *types.Block, statedb *state.Stat allLogs []*types.Log usedGas = new(uint64) ) + // 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) @@ -159,6 +260,8 @@ func (p *ParallelStateProcessor) Process(block *types.Block, statedb *state.Stat shouldDelayFeeCal := true + coinbase, _ := p.bc.Engine().Author(header) + // Iterate over and process the individual transactions for i, tx := range block.Transactions() { msg, err := tx.AsMessage(types.MakeSigner(p.config, header.Number), header.BaseFee) @@ -167,11 +270,9 @@ func (p *ParallelStateProcessor) Process(block *types.Block, statedb *state.Stat return nil, nil, 0, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err) } - bc := NewEVMBlockContext(header, p.bc, nil) - cleansdb := statedb.Copy() - if msg.From() == bc.Coinbase { + if msg.From() == coinbase { shouldDelayFeeCal = false } @@ -184,22 +285,42 @@ func (p *ParallelStateProcessor) Process(block *types.Block, statedb *state.Stat tx: tx, index: i, cleanStateDB: cleansdb, - blockContext: bc, + finalStateDB: statedb, + blockChain: p.bc, + header: header, evmConfig: cfg, shouldDelayFeeCal: &shouldDelayFeeCal, sender: msg.From(), + totalUsedGas: usedGas, + receipts: &receipts, + allLogs: &allLogs, } tasks = append(tasks, task) } - _, err := blockstm.ExecuteParallel(tasks) + backupStateDB := statedb.Copy() + _, err := blockstm.ExecuteParallel(tasks, false) for _, task := range tasks { task := task.(*ExecutionTask) if task.shouldRerunWithoutFeeDelay { shouldDelayFeeCal = false - _, err = blockstm.ExecuteParallel(tasks) + *statedb = *backupStateDB + + allLogs = []*types.Log{} + receipts = types.Receipts{} + usedGas = new(uint64) + + for _, t := range tasks { + t := t.(*ExecutionTask) + t.finalStateDB = backupStateDB + t.allLogs = &allLogs + t.receipts = &receipts + t.totalUsedGas = usedGas + } + + _, err = blockstm.ExecuteParallel(tasks, false) break } @@ -210,90 +331,14 @@ func (p *ParallelStateProcessor) Process(block *types.Block, statedb *state.Stat return nil, nil, 0, err } - london := p.config.IsLondon(blockNumber) + statedb.Finalise(p.config.IsEIP158(blockNumber)) - for _, task := range tasks { - task := task.(*ExecutionTask) - statedb.Prepare(task.tx.Hash(), task.index) - - coinbaseBalance := statedb.GetBalance(task.blockContext.Coinbase) - - statedb.ApplyMVWriteSet(task.MVWriteList()) - - for _, l := range task.statedb.GetLogs(task.tx.Hash(), blockHash) { - statedb.AddLog(l) - } - - if shouldDelayFeeCal { - if london { - statedb.AddBalance(task.result.BurntContractAddress, task.result.FeeBurnt) - } - - statedb.AddBalance(task.blockContext.Coinbase, task.result.FeeTipped) - output1 := new(big.Int).SetBytes(task.result.SenderInitBalance.Bytes()) - output2 := new(big.Int).SetBytes(coinbaseBalance.Bytes()) - - // Deprecating transfer log and will be removed in future fork. PLEASE DO NOT USE this transfer log going forward. Parameters won't get updated as expected going forward with EIP1559 - // add transfer log - AddFeeTransferLog( - statedb, - - task.msg.From(), - task.blockContext.Coinbase, - - task.result.FeeTipped, - task.result.SenderInitBalance, - coinbaseBalance, - output1.Sub(output1, task.result.FeeTipped), - output2.Add(output2, task.result.FeeTipped), - ) - } - - for k, v := range task.statedb.Preimages() { - statedb.AddPreimage(k, v) - } - - // Update the state with pending changes. - var root []byte - - if p.config.IsByzantium(blockNumber) { - statedb.Finalise(true) - } else { - root = statedb.IntermediateRoot(p.config.IsEIP158(blockNumber)).Bytes() - } - - *usedGas += task.result.UsedGas - - // Create a new receipt for the transaction, storing the intermediate root and gas used - // by the tx. - receipt := &types.Receipt{Type: task.tx.Type(), PostState: root, CumulativeGasUsed: *usedGas} - if task.result.Failed() { - receipt.Status = types.ReceiptStatusFailed - } else { - receipt.Status = types.ReceiptStatusSuccessful - } - - receipt.TxHash = task.tx.Hash() - receipt.GasUsed = task.result.UsedGas - - // If the transaction created a contract, store the creation address in the receipt. - if task.msg.To() == nil { - receipt.ContractAddress = crypto.CreateAddress(task.msg.From(), task.tx.Nonce()) - } - - // Set the receipt logs and create the bloom filter. - receipt.Logs = statedb.GetLogs(task.tx.Hash(), blockHash) - receipt.Bloom = types.CreateBloom(types.Receipts{receipt}) - receipt.BlockHash = blockHash - receipt.BlockNumber = blockNumber - receipt.TransactionIndex = uint(statedb.TxIndex()) - - receipts = append(receipts, receipt) - allLogs = append(allLogs, receipt.Logs...) - } + start := time.Now() // Finalize the block, applying any consensus engine specific extras (e.g. block rewards) p.engine.Finalize(p.bc, header, statedb, block.Transactions(), block.Uncles()) + fmt.Println("Finalize time of parallel execution:", time.Since(start)) + return receipts, allLogs, *usedGas, nil } diff --git a/core/state/journal.go b/core/state/journal.go index 57393cbcf4..79a4e35422 100644 --- a/core/state/journal.go +++ b/core/state/journal.go @@ -20,6 +20,7 @@ import ( "math/big" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/blockstm" ) // journalEntry is a modification entry in the state change journal that can be @@ -143,7 +144,7 @@ type ( func (ch createObjectChange) revert(s *StateDB) { delete(s.stateObjects, *ch.account) delete(s.stateObjectsDirty, *ch.account) - MVWrite(s, ch.account.Bytes()) + MVWrite(s, blockstm.NewAddressKey(*ch.account)) } func (ch createObjectChange) dirtied() *common.Address { @@ -152,7 +153,7 @@ func (ch createObjectChange) dirtied() *common.Address { func (ch resetObjectChange) revert(s *StateDB) { s.setStateObject(ch.prev) - MVWrite(s, ch.prev.address.Bytes()) + MVWrite(s, blockstm.NewAddressKey(ch.prev.address)) if !ch.prevdestruct && s.snap != nil { delete(s.snapDestructs, ch.prev.addrHash) } @@ -167,8 +168,8 @@ func (ch suicideChange) revert(s *StateDB) { if obj != nil { obj.suicided = ch.prev obj.setBalance(ch.prevbalance) - MVWrite(s, subPath(ch.account.Bytes(), suicidePath)) - MVWrite(s, subPath(ch.account.Bytes(), balancePath)) + MVWrite(s, blockstm.NewSubpathKey(*ch.account, SuicidePath)) + MVWrite(s, blockstm.NewSubpathKey(*ch.account, BalancePath)) } } @@ -187,7 +188,7 @@ func (ch touchChange) dirtied() *common.Address { func (ch balanceChange) revert(s *StateDB) { s.getStateObject(*ch.account).setBalance(ch.prev) - MVWrite(s, subPath(ch.account.Bytes(), balancePath)) + MVWrite(s, blockstm.NewSubpathKey(*ch.account, BalancePath)) } func (ch balanceChange) dirtied() *common.Address { @@ -196,7 +197,7 @@ func (ch balanceChange) dirtied() *common.Address { func (ch nonceChange) revert(s *StateDB) { s.getStateObject(*ch.account).setNonce(ch.prev) - MVWrite(s, subPath(ch.account.Bytes(), noncePath)) + MVWrite(s, blockstm.NewSubpathKey(*ch.account, NoncePath)) } func (ch nonceChange) dirtied() *common.Address { @@ -205,7 +206,7 @@ func (ch nonceChange) dirtied() *common.Address { func (ch codeChange) revert(s *StateDB) { s.getStateObject(*ch.account).setCode(common.BytesToHash(ch.prevhash), ch.prevcode) - MVWrite(s, subPath(ch.account.Bytes(), codePath)) + MVWrite(s, blockstm.NewSubpathKey(*ch.account, CodePath)) } func (ch codeChange) dirtied() *common.Address { @@ -214,7 +215,7 @@ func (ch codeChange) dirtied() *common.Address { func (ch storageChange) revert(s *StateDB) { s.getStateObject(*ch.account).setState(ch.key, ch.prevalue) - MVWrite(s, append(ch.account.Bytes(), ch.key.Bytes()...)) + MVWrite(s, blockstm.NewStateKey(*ch.account, ch.key)) } func (ch storageChange) dirtied() *common.Address { diff --git a/core/state/statedb.go b/core/state/statedb.go index ce2a6e72d3..74546b1042 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -83,10 +83,10 @@ type StateDB struct { // Block-stm related fields mvHashmap *blockstm.MVHashMap incarnation int - readMap map[string]blockstm.ReadDescriptor - writeMap map[string]blockstm.WriteDescriptor + readMap map[blockstm.Key]blockstm.ReadDescriptor + writeMap map[blockstm.Key]blockstm.WriteDescriptor newStateObjects map[common.Address]struct{} - invalidRead bool + dep int // DB error. // State objects are used by the consensus core and VM which are @@ -169,21 +169,23 @@ func NewWithMVHashmap(root common.Hash, db Database, snaps *snapshot.Tree, mvhm return nil, err } else { sdb.mvHashmap = mvhm + sdb.dep = -1 return sdb, nil } } func (sdb *StateDB) SetMVHashmap(mvhm *blockstm.MVHashMap) { sdb.mvHashmap = mvhm + sdb.dep = -1 } func (s *StateDB) MVWriteList() []blockstm.WriteDescriptor { writes := make([]blockstm.WriteDescriptor, 0, len(s.writeMap)) for _, v := range s.writeMap { - if len(v.Path) != common.AddressLength { + if !v.Path.IsAddress() { writes = append(writes, v) - } else if _, ok := s.newStateObjects[common.BytesToAddress(v.Path)]; ok { + } else if _, ok := s.newStateObjects[common.BytesToAddress(v.Path[:common.AddressLength])]; ok { writes = append(writes, v) } } @@ -201,7 +203,7 @@ func (s *StateDB) MVFullWriteList() []blockstm.WriteDescriptor { return writes } -func (s *StateDB) MVReadMap() map[string]blockstm.ReadDescriptor { +func (s *StateDB) MVReadMap() map[blockstm.Key]blockstm.ReadDescriptor { return s.readMap } @@ -217,25 +219,33 @@ func (s *StateDB) MVReadList() []blockstm.ReadDescriptor { func (s *StateDB) ensureReadMap() { if s.readMap == nil { - s.readMap = make(map[string]blockstm.ReadDescriptor) + s.readMap = make(map[blockstm.Key]blockstm.ReadDescriptor) } } func (s *StateDB) ensureWriteMap() { if s.writeMap == nil { - s.writeMap = make(map[string]blockstm.WriteDescriptor) + s.writeMap = make(map[blockstm.Key]blockstm.WriteDescriptor) } } func (s *StateDB) HadInvalidRead() bool { - return s.invalidRead + return s.dep >= 0 +} + +func (s *StateDB) DepTxIndex() int { + return s.dep } func (s *StateDB) SetIncarnation(inc int) { s.incarnation = inc } -func MVRead[T any](s *StateDB, k []byte, defaultV T, readStorage func(s *StateDB) T) (v T) { +type StorageVal[T any] struct { + Value *T +} + +func MVRead[T any](s *StateDB, k blockstm.Key, defaultV T, readStorage func(s *StateDB) T) (v T) { if s.mvHashmap == nil { return readStorage(s) } @@ -243,7 +253,7 @@ func MVRead[T any](s *StateDB, k []byte, defaultV T, readStorage func(s *StateDB s.ensureReadMap() if s.writeMap != nil { - if _, ok := s.writeMap[string(k)]; ok { + if _, ok := s.writeMap[k]; ok { return readStorage(s) } } @@ -267,8 +277,12 @@ func MVRead[T any](s *StateDB, k []byte, defaultV T, readStorage func(s *StateDB } case blockstm.MVReadResultDependency: { - s.invalidRead = true - return defaultV + if res.DepIdx() > s.dep { + s.dep = res.DepIdx() + } + + // Return immediate to executor when we found a dependency + panic("Found dependency") } case blockstm.MVReadResultNone: { @@ -279,20 +293,19 @@ func MVRead[T any](s *StateDB, k []byte, defaultV T, readStorage func(s *StateDB return defaultV } - mk := string(k) // TODO: I assume we don't want to overwrite an existing read because this could - for example - change a storage // read to map if the same value is read multiple times. - if _, ok := s.readMap[mk]; !ok { - s.readMap[mk] = rd + if _, ok := s.readMap[k]; !ok { + s.readMap[k] = rd } return } -func MVWrite(s *StateDB, k []byte) { +func MVWrite(s *StateDB, k blockstm.Key) { if s.mvHashmap != nil { s.ensureWriteMap() - s.writeMap[string(k)] = blockstm.WriteDescriptor{ + s.writeMap[k] = blockstm.WriteDescriptor{ Path: k, V: s.Version(), Val: s, @@ -300,12 +313,12 @@ func MVWrite(s *StateDB, k []byte) { } } -func MVWritten(s *StateDB, k []byte) bool { +func MVWritten(s *StateDB, k blockstm.Key) bool { if s.mvHashmap == nil || s.writeMap == nil { return false } - _, ok := s.writeMap[string(k)] + _, ok := s.writeMap[k] return ok } @@ -324,30 +337,27 @@ func (sw *StateDB) ApplyMVWriteSet(writes []blockstm.WriteDescriptor) { path := writes[i].Path sr := writes[i].Val.(*StateDB) - keyLength := len(path) - - if keyLength == common.AddressLength { - sw.GetOrNewStateObject(common.BytesToAddress(path)) - } else if keyLength == (common.AddressLength + common.HashLength) { - addr := common.BytesToAddress(path[:common.AddressLength]) - subPath := common.BytesToHash(path[common.AddressLength:]) - sw.SetState(addr, subPath, sr.GetState(addr, subPath)) + if path.IsState() { + addr := path.GetAddress() + stateKey := path.GetStateKey() + state := sr.GetState(addr, stateKey) + sw.SetState(addr, stateKey, state) } else { - addr := common.BytesToAddress(path[:common.AddressLength]) - switch path[keyLength-1] { - case balancePath: + addr := path.GetAddress() + switch path.GetSubpath() { + case BalancePath: sw.SetBalance(addr, sr.GetBalance(addr)) - case noncePath: + case NoncePath: sw.SetNonce(addr, sr.GetNonce(addr)) - case codePath: + case CodePath: sw.SetCode(addr, sr.GetCode(addr)) - case suicidePath: + case SuicidePath: stateObject := sr.getDeletedStateObject(addr) if stateObject != nil && stateObject.deleted { sw.Suicide(addr) } default: - panic(fmt.Errorf("unknown key type: %d", path[keyLength-1])) + panic(fmt.Errorf("unknown key type: %d", path.GetSubpath())) } } } @@ -373,7 +383,7 @@ func (s *StateDB) GetReadMapDump() []DumpStruct { TxInc: s.incarnation, VerIdx: val.V.TxnIndex, VerInc: val.V.Incarnation, - Path: val.Path, + Path: val.Path[:], Op: "Read\n", } res = append(res, *temp) @@ -393,7 +403,7 @@ func (s *StateDB) GetWriteMapDump() []DumpStruct { TxInc: s.incarnation, VerIdx: val.V.TxnIndex, VerInc: val.V.Incarnation, - Path: val.Path, + Path: val.Path[:], Op: "Write\n", } res = append(res, *temp) @@ -512,17 +522,17 @@ func (s *StateDB) Empty(addr common.Address) bool { } // Create a unique path for special fields (e.g. balance, code) in a state object. -func subPath(prefix []byte, s uint8) []byte { - path := append(prefix, common.Hash{}.Bytes()...) // append a full empty hash to avoid collision with storage state - path = append(path, s) // append the special field identifier +// func subPath(prefix []byte, s uint8) [blockstm.KeyLength]byte { +// path := append(prefix, common.Hash{}.Bytes()...) // append a full empty hash to avoid collision with storage state +// path = append(path, s) // append the special field identifier - return path -} +// return path +// } -const balancePath = 1 -const noncePath = 2 -const codePath = 3 -const suicidePath = 4 +const BalancePath = 1 +const NoncePath = 2 +const CodePath = 3 +const SuicidePath = 4 // GetBalance retrieves the balance from the given address or 0 if object not found func (s *StateDB) GetBalance(addr common.Address) *big.Int { @@ -530,7 +540,7 @@ func (s *StateDB) GetBalance(addr common.Address) *big.Int { return common.Big0 } - return MVRead(s, subPath(addr.Bytes(), balancePath), common.Big0, func(s *StateDB) *big.Int { + return MVRead(s, blockstm.NewSubpathKey(addr, BalancePath), common.Big0, func(s *StateDB) *big.Int { stateObject := s.getStateObject(addr) if stateObject != nil { return stateObject.Balance() @@ -545,7 +555,7 @@ func (s *StateDB) GetNonce(addr common.Address) uint64 { return 0 } - return MVRead(s, subPath(addr.Bytes(), noncePath), 0, func(s *StateDB) uint64 { + return MVRead(s, blockstm.NewSubpathKey(addr, NoncePath), 0, func(s *StateDB) uint64 { stateObject := s.getStateObject(addr) if stateObject != nil { return stateObject.Nonce() @@ -572,7 +582,7 @@ func (s *StateDB) GetCode(addr common.Address) []byte { return nil } - return MVRead(s, subPath(addr.Bytes(), codePath), nil, func(s *StateDB) []byte { + return MVRead(s, blockstm.NewSubpathKey(addr, CodePath), nil, func(s *StateDB) []byte { stateObject := s.getStateObject(addr) if stateObject != nil { return stateObject.Code(s.db) @@ -586,7 +596,7 @@ func (s *StateDB) GetCodeSize(addr common.Address) int { return 0 } - return MVRead(s, subPath(addr.Bytes(), codePath), 0, func(s *StateDB) int { + return MVRead(s, blockstm.NewSubpathKey(addr, CodePath), 0, func(s *StateDB) int { stateObject := s.getStateObject(addr) if stateObject != nil { return stateObject.CodeSize(s.db) @@ -600,7 +610,7 @@ func (s *StateDB) GetCodeHash(addr common.Address) common.Hash { return common.Hash{} } - return MVRead(s, subPath(addr.Bytes(), codePath), common.Hash{}, func(s *StateDB) common.Hash { + return MVRead(s, blockstm.NewSubpathKey(addr, CodePath), common.Hash{}, func(s *StateDB) common.Hash { stateObject := s.getStateObject(addr) if stateObject == nil { return common.Hash{} @@ -615,7 +625,7 @@ func (s *StateDB) GetState(addr common.Address, hash common.Hash) common.Hash { return common.Hash{} } - return MVRead(s, append(addr.Bytes(), hash.Bytes()...), common.Hash{}, func(s *StateDB) common.Hash { + return MVRead(s, blockstm.NewStateKey(addr, hash), common.Hash{}, func(s *StateDB) common.Hash { stateObject := s.getStateObject(addr) if stateObject != nil { return stateObject.GetState(s.db, hash) @@ -653,7 +663,7 @@ func (s *StateDB) GetCommittedState(addr common.Address, hash common.Hash) commo return common.Hash{} } - return MVRead(s, append(addr.Bytes(), hash.Bytes()...), common.Hash{}, func(s *StateDB) common.Hash { + return MVRead(s, blockstm.NewStateKey(addr, hash), common.Hash{}, func(s *StateDB) common.Hash { stateObject := s.getStateObject(addr) if stateObject != nil { return stateObject.GetCommittedState(s.db, hash) @@ -684,7 +694,7 @@ func (s *StateDB) HasSuicided(addr common.Address) bool { return false } - return MVRead(s, subPath(addr.Bytes(), suicidePath), false, func(s *StateDB) bool { + return MVRead(s, blockstm.NewSubpathKey(addr, SuicidePath), false, func(s *StateDB) bool { stateObject := s.getStateObject(addr) if stateObject != nil { return stateObject.suicided @@ -709,7 +719,7 @@ func (s *StateDB) AddBalance(addr common.Address, amount *big.Int) { if stateObject != nil { stateObject = s.mvRecordWritten(stateObject) stateObject.AddBalance(amount) - MVWrite(s, subPath(addr.Bytes(), balancePath)) + MVWrite(s, blockstm.NewSubpathKey(addr, BalancePath)) } } @@ -725,7 +735,7 @@ func (s *StateDB) SubBalance(addr common.Address, amount *big.Int) { if stateObject != nil { stateObject = s.mvRecordWritten(stateObject) stateObject.SubBalance(amount) - MVWrite(s, subPath(addr.Bytes(), balancePath)) + MVWrite(s, blockstm.NewSubpathKey(addr, BalancePath)) } } @@ -734,7 +744,7 @@ func (s *StateDB) SetBalance(addr common.Address, amount *big.Int) { if stateObject != nil { stateObject = s.mvRecordWritten(stateObject) stateObject.SetBalance(amount) - MVWrite(s, subPath(addr.Bytes(), balancePath)) + MVWrite(s, blockstm.NewSubpathKey(addr, BalancePath)) } } @@ -743,7 +753,7 @@ func (s *StateDB) SetNonce(addr common.Address, nonce uint64) { if stateObject != nil { stateObject = s.mvRecordWritten(stateObject) stateObject.SetNonce(nonce) - MVWrite(s, subPath(addr.Bytes(), noncePath)) + MVWrite(s, blockstm.NewSubpathKey(addr, NoncePath)) } } @@ -752,7 +762,7 @@ func (s *StateDB) SetCode(addr common.Address, code []byte) { if stateObject != nil { stateObject = s.mvRecordWritten(stateObject) stateObject.SetCode(crypto.Keccak256Hash(code), code) - MVWrite(s, subPath(addr.Bytes(), codePath)) + MVWrite(s, blockstm.NewSubpathKey(addr, CodePath)) } } @@ -761,7 +771,7 @@ func (s *StateDB) SetState(addr common.Address, key, value common.Hash) { if stateObject != nil { stateObject = s.mvRecordWritten(stateObject) stateObject.SetState(s.db, key, value) - MVWrite(s, append(addr.Bytes(), key.Bytes()...)) + MVWrite(s, blockstm.NewStateKey(addr, key)) } } @@ -794,8 +804,8 @@ func (s *StateDB) Suicide(addr common.Address) bool { stateObject.markSuicided() stateObject.data.Balance = new(big.Int) - MVWrite(s, subPath(addr.Bytes(), suicidePath)) - MVWrite(s, subPath(addr.Bytes(), balancePath)) + MVWrite(s, blockstm.NewSubpathKey(addr, SuicidePath)) + MVWrite(s, blockstm.NewSubpathKey(addr, BalancePath)) return true } @@ -853,7 +863,7 @@ func (s *StateDB) getStateObject(addr common.Address) *stateObject { // 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 { - return MVRead(s, addr.Bytes(), nil, func(s *StateDB) *stateObject { + return MVRead(s, blockstm.NewAddressKey(addr), nil, func(s *StateDB) *stateObject { // Prefer live objects if any is available if obj := s.stateObjects[addr]; obj != nil { return obj @@ -932,16 +942,16 @@ func (s *StateDB) mvRecordWritten(object *stateObject) *stateObject { return object } - addrPath := object.Address().Bytes() + addrKey := blockstm.NewAddressKey(object.Address()) - if MVWritten(s, addrPath) { + if MVWritten(s, addrKey) { return object } // Deepcopy is needed to ensure that objects are not written by multiple transactions at the same time, because // the input state object can come from a different transaction. s.setStateObject(object.deepCopy(s)) - MVWrite(s, addrPath) + MVWrite(s, addrKey) return s.stateObjects[object.Address()] } @@ -967,7 +977,7 @@ func (s *StateDB) createObject(addr common.Address) (newobj, prev *stateObject) s.setStateObject(newobj) s.newStateObjects[addr] = struct{}{} - MVWrite(s, addr.Bytes()) + MVWrite(s, blockstm.NewAddressKey(addr)) if prev != nil && !prev.deleted { return newobj, prev } @@ -988,7 +998,7 @@ func (s *StateDB) CreateAccount(addr common.Address) { newObj, prev := s.createObject(addr) if prev != nil { newObj.setBalance(prev.data.Balance) - MVWrite(s, subPath(addr.Bytes(), balancePath)) + MVWrite(s, blockstm.NewSubpathKey(addr, BalancePath)) } } diff --git a/core/state/statedb_test.go b/core/state/statedb_test.go index 1fd1f5477c..c374c9256d 100644 --- a/core/state/statedb_test.go +++ b/core/state/statedb_test.go @@ -659,16 +659,21 @@ func TestMVHashMapMarkEstimate(t *testing.T) { assert.Equal(t, balance, b) // Tx1 mark estimate - for _, v := range states[1].writeMap { + for _, v := range states[1].MVWriteList() { mvhm.MarkEstimate(v.Path, 1) } - // Tx2 read again should get default (empty) vals because its dependency Tx1 is marked as estimate - v = states[2].GetState(addr, key) - b = states[2].GetBalance(addr) + defer func() { + if r := recover(); r == nil { + t.Errorf("The code did not panic") + } else { + t.Log("Recovered in f", r) + } + }() - assert.Equal(t, common.Hash{}, v) - assert.Equal(t, common.Big0, b) + // Tx2 read again should get default (empty) vals because its dependency Tx1 is marked as estimate + states[2].GetState(addr, key) + states[2].GetBalance(addr) // Tx1 read again should get Tx0 vals v = states[1].GetState(addr, key) diff --git a/go.mod b/go.mod index fa21583ce2..cf5a532d77 100644 --- a/go.mod +++ b/go.mod @@ -37,6 +37,7 @@ require ( github.com/hashicorp/go-bexpr v0.1.10 github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d github.com/hashicorp/hcl/v2 v2.10.1 + github.com/heimdalr/dag v1.2.1 github.com/holiman/bloomfilter/v2 v2.0.3 github.com/holiman/uint256 v1.2.0 github.com/huin/goupnp v1.0.3-0.20220313090229-ca81a64b4204 diff --git a/go.sum b/go.sum index 6d28e061ef..1bd56c0e36 100644 --- a/go.sum +++ b/go.sum @@ -187,8 +187,9 @@ github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5Nq github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/go-test/deep v1.0.7 h1:/VSMRlnY/JSyqxQUzQLKVMAskpY/NZKFA5j2P+0pP2M= +github.com/go-test/deep v1.0.7/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8= github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= @@ -275,6 +276,8 @@ github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d h1:dg1dEPuW github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl/v2 v2.10.1 h1:h4Xx4fsrRE26ohAk/1iGF/JBqRQbyUqu5Lvj60U54ys= github.com/hashicorp/hcl/v2 v2.10.1/go.mod h1:FwWsfWEjyV/CMj8s/gqAuiviY72rJ1/oayI9WftqcKg= +github.com/heimdalr/dag v1.2.1 h1:XJOMaoWqJK1UKdp+4zaO2uwav9GFbHMGCirdViKMRIQ= +github.com/heimdalr/dag v1.2.1/go.mod h1:Of/wUB7Yoj4dwiOcGOOYIq6MHlPF/8/QMBKFJpwg+yc= github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= github.com/holiman/uint256 v1.2.0 h1:gpSYcPLWGv4sG43I2mVLiDZCNDh/EpGjSk8tmtxitHM= From f7c041fdb169c633474cfcf72576454d19c7af89 Mon Sep 17 00:00:00 2001 From: Jerry Date: Mon, 5 Sep 2022 23:18:15 -0700 Subject: [PATCH 08/38] Do not write entire objects directly when applying write set in blockstm --- core/state/statedb.go | 2 -- core/state/statedb_test.go | 16 ++++++++-------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/core/state/statedb.go b/core/state/statedb.go index 74546b1042..a650be1130 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -185,8 +185,6 @@ func (s *StateDB) MVWriteList() []blockstm.WriteDescriptor { for _, v := range s.writeMap { if !v.Path.IsAddress() { writes = append(writes, v) - } else if _, ok := s.newStateObjects[common.BytesToAddress(v.Path[:common.AddressLength])]; ok { - writes = append(writes, v) } } diff --git a/core/state/statedb_test.go b/core/state/statedb_test.go index c374c9256d..73b028bfcc 100644 --- a/core/state/statedb_test.go +++ b/core/state/statedb_test.go @@ -885,7 +885,7 @@ func TestApplyMVWriteSet(t *testing.T) { states[0].SetBalance(addr1, balance1) states[0].SetState(addr2, key2, val2) states[0].GetOrNewStateObject(addr3) - states[0].Finalise(false) + states[0].Finalise(true) states[0].FlushMVWriteSet() sSingleProcess.GetOrNewStateObject(addr1) @@ -896,13 +896,13 @@ func TestApplyMVWriteSet(t *testing.T) { sClean.ApplyMVWriteSet(states[0].MVWriteList()) - assert.Equal(t, sSingleProcess.IntermediateRoot(false), sClean.IntermediateRoot(false)) + assert.Equal(t, sSingleProcess.IntermediateRoot(true), sClean.IntermediateRoot(true)) // Tx1 write states[1].SetState(addr1, key2, val2) states[1].SetBalance(addr1, balance2) states[1].SetNonce(addr1, 1) - states[1].Finalise(false) + states[1].Finalise(true) states[1].FlushMVWriteSet() sSingleProcess.SetState(addr1, key2, val2) @@ -911,13 +911,13 @@ func TestApplyMVWriteSet(t *testing.T) { sClean.ApplyMVWriteSet(states[1].MVWriteList()) - assert.Equal(t, sSingleProcess.IntermediateRoot(false), sClean.IntermediateRoot(false)) + assert.Equal(t, sSingleProcess.IntermediateRoot(true), sClean.IntermediateRoot(true)) // Tx2 write states[2].SetState(addr1, key1, val2) states[2].SetBalance(addr1, balance2) states[2].SetNonce(addr1, 2) - states[2].Finalise(false) + states[2].Finalise(true) states[2].FlushMVWriteSet() sSingleProcess.SetState(addr1, key1, val2) @@ -926,12 +926,12 @@ func TestApplyMVWriteSet(t *testing.T) { sClean.ApplyMVWriteSet(states[2].MVWriteList()) - assert.Equal(t, sSingleProcess.IntermediateRoot(false), sClean.IntermediateRoot(false)) + assert.Equal(t, sSingleProcess.IntermediateRoot(true), sClean.IntermediateRoot(true)) // Tx3 write states[3].Suicide(addr2) states[3].SetCode(addr1, code) - states[3].Finalise(false) + states[3].Finalise(true) states[3].FlushMVWriteSet() sSingleProcess.Suicide(addr2) @@ -939,7 +939,7 @@ func TestApplyMVWriteSet(t *testing.T) { sClean.ApplyMVWriteSet(states[3].MVWriteList()) - assert.Equal(t, sSingleProcess.IntermediateRoot(false), sClean.IntermediateRoot(false)) + assert.Equal(t, sSingleProcess.IntermediateRoot(true), sClean.IntermediateRoot(true)) } // TestCopyOfCopy tests that modified objects are carried over to the copy, and the copy of the copy. From d107c183b844825e64f86c925eef8f5d80295289 Mon Sep 17 00:00:00 2001 From: Pratik Patil Date: Tue, 27 Sep 2022 12:31:13 +0530 Subject: [PATCH 09/38] fixed a small bug in the Report function (#530) --- core/blockstm/dag.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/core/blockstm/dag.go b/core/blockstm/dag.go index 8404395ec0..3fe714a50e 100644 --- a/core/blockstm/dag.go +++ b/core/blockstm/dag.go @@ -74,13 +74,19 @@ func BuildDAG(deps TxnInputOutput) (d DAG) { func (d DAG) Report(out func(string)) { roots := make([]int, 0) rootIds := make([]string, 0) + rootIdMap := make(map[int]string, len(d.GetRoots())) for k, i := range d.GetRoots() { roots = append(roots, i.(int)) - rootIds = append(rootIds, k) + rootIdMap[i.(int)] = k } sort.Ints(roots) + + for _, i := range roots { + rootIds = append(rootIds, rootIdMap[i]) + } + fmt.Println(roots) makeStrs := func(ints []int) (ret []string) { From 471afc8da2dc037783d0fa0cb30ec7b7d48c7fb0 Mon Sep 17 00:00:00 2001 From: Jerry Date: Wed, 21 Sep 2022 18:21:50 -0700 Subject: [PATCH 10/38] Refactor blockstm executor --- core/blockstm/executor.go | 652 +++++++++++++++---------------- core/blockstm/executor_test.go | 138 +++++-- core/blockstm/status.go | 67 +--- core/parallel_state_processor.go | 11 + core/state/statedb.go | 5 +- 5 files changed, 456 insertions(+), 417 deletions(-) diff --git a/core/blockstm/executor.go b/core/blockstm/executor.go index b1c5770866..f0c05a7d94 100644 --- a/core/blockstm/executor.go +++ b/core/blockstm/executor.go @@ -3,7 +3,6 @@ package blockstm import ( "container/heap" "fmt" - "sort" "sync" "time" @@ -127,413 +126,402 @@ type ParallelExecutionResult struct { } const numGoProcs = 2 -const numSpeculativeProcs = 16 +const numSpeculativeProcs = 8 -// Max number of pre-validation to run per loop -const preValidateLimit = 5 - -// Max number of times a transaction (t) can be executed before its dependency is resolved to its previous tx (t-1) -const maxIncarnation = 2 - -// nolint: gocognit -// A stateless executor that executes transactions in parallel -func ExecuteParallel(tasks []ExecTask, profile bool) (ParallelExecutionResult, error) { - if len(tasks) == 0 { - return ParallelExecutionResult{MakeTxnInputOutput(len(tasks)), nil, nil}, nil - } +type ParallelExecutor struct { + tasks []ExecTask // Stores the execution statistics for each task - stats := make([][]uint64, 0, len(tasks)) - statsMutex := sync.Mutex{} + stats [][]uint64 + statsMutex sync.Mutex // Channel for tasks that should be prioritized - chTasks := make(chan ExecVersionView, len(tasks)) + chTasks chan ExecVersionView // Channel for speculative tasks - chSpeculativeTasks := make(chan struct{}, len(tasks)) - - // A priority queue that stores speculative tasks - specTaskQueue := NewSafePriorityQueue(len(tasks)) + chSpeculativeTasks chan struct{} // Channel to signal that the result of a transaction could be written to storage - chSettle := make(chan int, len(tasks)) + specTaskQueue *SafePriorityQueue + + // A priority queue that stores speculative tasks + chSettle chan int // Channel to signal that a transaction has finished executing - chResults := make(chan struct{}, len(tasks)) + chResults chan struct{} // A priority queue that stores the transaction index of results, so we can validate the results in order - resultQueue := NewSafePriorityQueue(len(tasks)) + resultQueue *SafePriorityQueue // A wait group to wait for all settling tasks to finish - var settleWg sync.WaitGroup + settleWg sync.WaitGroup // An integer that tracks the index of last settled transaction - lastSettled := -1 + lastSettled int // For a task that runs only after all of its preceding tasks have finished and passed validation, // its result will be absolutely valid and therefore its validation could be skipped. // This map stores the boolean value indicating whether a task satisfy this condition ( absolutely valid). - skipCheck := make(map[int]bool) - - for i := 0; i < len(tasks); i++ { - skipCheck[i] = false - } + skipCheck map[int]bool // Execution tasks stores the state of each execution task - execTasks := makeStatusManager(len(tasks)) + execTasks taskStatusManager // Validate tasks stores the state of each validation task - validateTasks := makeStatusManager(0) + validateTasks taskStatusManager // Stats for debugging purposes - var cntExec, cntSuccess, cntAbort, cntTotalValidations, cntValidationFail int + cntExec, cntSuccess, cntAbort, cntTotalValidations, cntValidationFail int - diagExecSuccess := make([]int, len(tasks)) - diagExecAbort := make([]int, len(tasks)) + diagExecSuccess, diagExecAbort []int - // Initialize MVHashMap - mvh := MakeMVHashMap() + // Multi-version hash map + mvh *MVHashMap // Stores the inputs and outputs of the last incardanotion of all transactions - lastTxIO := MakeTxnInputOutput(len(tasks)) + lastTxIO *TxnInputOutput // Tracks the incarnation number of each transaction - txIncarnations := make([]int, len(tasks)) + txIncarnations []int // A map that stores the estimated dependency of a transaction if it is aborted without any known dependency - estimateDeps := make(map[int][]int, len(tasks)) - - for i := 0; i < len(tasks); i++ { - estimateDeps[i] = make([]int, 0) - } + estimateDeps map[int][]int // A map that records whether a transaction result has been speculatively validated - preValidated := make(map[int]bool, len(tasks)) + preValidated map[int]bool - begin := time.Now() + // Time records when the parallel execution starts + begin time.Time - workerWg := sync.WaitGroup{} - workerWg.Add(numSpeculativeProcs + numGoProcs) + // Enable profiling + profile bool + + // Worker wait group + workerWg sync.WaitGroup +} + +func NewParallelExecutor(tasks []ExecTask, profile bool) *ParallelExecutor { + numTasks := len(tasks) + + pe := &ParallelExecutor{ + tasks: tasks, + stats: make([][]uint64, numTasks), + chTasks: make(chan ExecVersionView, numTasks), + chSpeculativeTasks: make(chan struct{}, numTasks), + chSettle: make(chan int, numTasks), + chResults: make(chan struct{}, numTasks), + specTaskQueue: NewSafePriorityQueue(numTasks), + resultQueue: NewSafePriorityQueue(numTasks), + lastSettled: -1, + skipCheck: make(map[int]bool), + execTasks: makeStatusManager(numTasks), + validateTasks: makeStatusManager(0), + diagExecSuccess: make([]int, numTasks), + diagExecAbort: make([]int, numTasks), + mvh: MakeMVHashMap(), + lastTxIO: MakeTxnInputOutput(numTasks), + txIncarnations: make([]int, numTasks), + estimateDeps: make(map[int][]int), + preValidated: make(map[int]bool), + begin: time.Now(), + profile: profile, + } + + return pe +} + +func (pe *ParallelExecutor) Prepare() { + prevSenderTx := make(map[common.Address]int) + + for i, t := range pe.tasks { + pe.skipCheck[i] = false + pe.estimateDeps[i] = make([]int, 0) + + if tx, ok := prevSenderTx[t.Sender()]; ok { + pe.execTasks.addDependencies(tx, i) + pe.execTasks.clearPending(i) + } + + prevSenderTx[t.Sender()] = i + } + + pe.workerWg.Add(numSpeculativeProcs + numGoProcs) // Launch workers that execute transactions for i := 0; i < numSpeculativeProcs+numGoProcs; i++ { go func(procNum int) { - defer workerWg.Done() + defer pe.workerWg.Done() doWork := func(task ExecVersionView) { start := time.Duration(0) - if profile { - start = time.Since(begin) + if pe.profile { + start = time.Since(pe.begin) } res := task.Execute() if res.err == nil { - mvh.FlushMVWriteSet(res.txAllOut) + pe.mvh.FlushMVWriteSet(res.txAllOut) } - resultQueue.Push(res.ver.TxnIndex, res) - chResults <- struct{}{} + pe.resultQueue.Push(res.ver.TxnIndex, res) + pe.chResults <- struct{}{} - if profile { - end := time.Since(begin) + if pe.profile { + end := time.Since(pe.begin) stat := []uint64{uint64(res.ver.TxnIndex), uint64(res.ver.Incarnation), uint64(start), uint64(end), uint64(procNum)} - statsMutex.Lock() - stats = append(stats, stat) - statsMutex.Unlock() + pe.statsMutex.Lock() + pe.stats = append(pe.stats, stat) + pe.statsMutex.Unlock() } } if procNum < numSpeculativeProcs { - for range chSpeculativeTasks { - doWork(specTaskQueue.Pop().(ExecVersionView)) + for range pe.chSpeculativeTasks { + doWork(pe.specTaskQueue.Pop().(ExecVersionView)) } } else { - for task := range chTasks { + for task := range pe.chTasks { doWork(task) } } }(i) } - // Launch a worker that settles valid transactions - settleWg.Add(len(tasks)) + pe.settleWg.Add(len(pe.tasks)) go func() { - for t := range chSettle { - tasks[t].Settle() - settleWg.Done() + for t := range pe.chSettle { + pe.tasks[t].Settle() + pe.settleWg.Done() } }() // bootstrap first execution - tx := execTasks.takeNextPending() + tx := pe.execTasks.takeNextPending() if tx != -1 { - cntExec++ + pe.cntExec++ - chTasks <- ExecVersionView{ver: Version{tx, 0}, et: tasks[tx], mvh: mvh, sender: tasks[tx].Sender()} + pe.chTasks <- ExecVersionView{ver: Version{tx, 0}, et: pe.tasks[tx], mvh: pe.mvh, sender: pe.tasks[tx].Sender()} } - - // Before starting execution, going through each task to check their explicit dependencies (whether they are coming from the same account) - prevSenderTx := make(map[common.Address]int) - - for i, t := range tasks { - if tx, ok := prevSenderTx[t.Sender()]; ok { - execTasks.addDependencies(tx, i) - execTasks.clearPending(i) - } - - prevSenderTx[t.Sender()] = i - } - - var res ExecResult - - var err error - - // Start main validation loop - // nolint:nestif - for range chResults { - res = resultQueue.Pop().(ExecResult) - tx := res.ver.TxnIndex - - if res.err == nil { - lastTxIO.recordRead(tx, res.txIn) - - if res.ver.Incarnation == 0 { - lastTxIO.recordWrite(tx, res.txOut) - lastTxIO.recordAllWrite(tx, res.txAllOut) - } else { - if res.txAllOut.hasNewWrite(lastTxIO.AllWriteSet(tx)) { - validateTasks.pushPendingSet(execTasks.getRevalidationRange(tx + 1)) - } - - prevWrite := lastTxIO.AllWriteSet(tx) - - // Remove entries that were previously written but are no longer written - - cmpMap := make(map[Key]bool) - - for _, w := range res.txAllOut { - cmpMap[w.Path] = true - } - - for _, v := range prevWrite { - if _, ok := cmpMap[v.Path]; !ok { - mvh.Delete(v.Path, tx) - } - } - - lastTxIO.recordWrite(tx, res.txOut) - lastTxIO.recordAllWrite(tx, res.txAllOut) - } - - validateTasks.pushPending(tx) - execTasks.markComplete(tx) - diagExecSuccess[tx]++ - cntSuccess++ - - execTasks.removeDependency(tx) - } else if execErr, ok := res.err.(ErrExecAbortError); ok { - - addedDependencies := false - - if execErr.Dependency >= 0 { - l := len(estimateDeps[tx]) - for l > 0 && estimateDeps[tx][l-1] > execErr.Dependency { - execTasks.removeDependency(estimateDeps[tx][l-1]) - estimateDeps[tx] = estimateDeps[tx][:l-1] - l-- - } - if txIncarnations[tx] < maxIncarnation { - addedDependencies = execTasks.addDependencies(execErr.Dependency, tx) - } else { - addedDependencies = execTasks.addDependencies(tx-1, tx) - } - } else { - estimate := 0 - - if len(estimateDeps[tx]) > 0 { - estimate = estimateDeps[tx][len(estimateDeps[tx])-1] - } - addedDependencies = execTasks.addDependencies(estimate, tx) - newEstimate := estimate + (estimate+tx)/2 - if newEstimate >= tx { - newEstimate = tx - 1 - } - estimateDeps[tx] = append(estimateDeps[tx], newEstimate) - } - - execTasks.clearInProgress(tx) - if !addedDependencies { - execTasks.pushPending(tx) - } - txIncarnations[tx]++ - diagExecAbort[tx]++ - cntAbort++ - } else { - err = res.err - break - } - - // do validations ... - maxComplete := execTasks.maxAllComplete() - - var toValidate []int - - for validateTasks.minPending() <= maxComplete && validateTasks.minPending() >= 0 { - toValidate = append(toValidate, validateTasks.takeNextPending()) - } - - for i := 0; i < len(toValidate); i++ { - cntTotalValidations++ - - tx := toValidate[i] - - if skipCheck[tx] || ValidateVersion(tx, lastTxIO, mvh) { - validateTasks.markComplete(tx) - } else { - cntValidationFail++ - diagExecAbort[tx]++ - for _, v := range lastTxIO.AllWriteSet(tx) { - mvh.MarkEstimate(v.Path, tx) - } - // 'create validation tasks for all transactions > tx ...' - validateTasks.pushPendingSet(execTasks.getRevalidationRange(tx + 1)) - validateTasks.clearInProgress(tx) // clear in progress - pending will be added again once new incarnation executes - - addedDependencies := false - if txIncarnations[tx] >= maxIncarnation { - addedDependencies = execTasks.addDependencies(tx-1, tx) - } - - execTasks.clearComplete(tx) - if !addedDependencies { - execTasks.pushPending(tx) - } - - preValidated[tx] = false - txIncarnations[tx]++ - } - } - - preValidateCount := 0 - invalidated := []int{} - - i := sort.SearchInts(validateTasks.pending, maxComplete+1) - - for i < len(validateTasks.pending) && preValidateCount < preValidateLimit { - tx := validateTasks.pending[i] - - if !preValidated[tx] { - cntTotalValidations++ - - if !ValidateVersion(tx, lastTxIO, mvh) { - cntValidationFail++ - diagExecAbort[tx]++ - - invalidated = append(invalidated, tx) - - if execTasks.checkComplete(tx) { - execTasks.clearComplete(tx) - } - - if !execTasks.checkInProgress(tx) { - for _, v := range lastTxIO.AllWriteSet(tx) { - mvh.MarkEstimate(v.Path, tx) - } - - validateTasks.pushPendingSet(execTasks.getRevalidationRange(tx + 1)) - - addedDependencies := false - if txIncarnations[tx] >= maxIncarnation { - addedDependencies = execTasks.addDependencies(tx-1, tx) - } - - if !addedDependencies { - execTasks.pushPending(tx) - } - } - - txIncarnations[tx]++ - - preValidated[tx] = false - } else { - preValidated[tx] = true - } - preValidateCount++ - } - - i++ - } - - for _, tx := range invalidated { - validateTasks.clearPending(tx) - } - - // Settle transactions that have been validated to be correct and that won't be re-executed again - maxValidated := validateTasks.maxAllComplete() - - for lastSettled < maxValidated { - lastSettled++ - if execTasks.checkInProgress(lastSettled) || execTasks.checkPending(lastSettled) || execTasks.blockCount[lastSettled] >= 0 { - lastSettled-- - break - } - chSettle <- lastSettled - } - - if validateTasks.countComplete() == len(tasks) && execTasks.countComplete() == len(tasks) { - log.Debug("blockstm exec summary", "execs", cntExec, "success", cntSuccess, "aborts", cntAbort, "validations", cntTotalValidations, "failures", cntValidationFail, "#tasks/#execs", fmt.Sprintf("%.2f%%", float64(len(tasks))/float64(cntExec)*100)) - break - } - - // Send the next immediate pending transaction to be executed - if execTasks.minPending() != -1 && execTasks.minPending() == maxValidated+1 { - nextTx := execTasks.takeNextPending() - if nextTx != -1 { - cntExec++ - - skipCheck[nextTx] = true - - chTasks <- ExecVersionView{ver: Version{nextTx, txIncarnations[nextTx]}, et: tasks[nextTx], mvh: mvh, sender: tasks[nextTx].Sender()} - } - } - - // Send speculative tasks - for execTasks.peekPendingGE(maxValidated+3) != -1 || len(execTasks.inProgress) == 0 { - // We skip the next transaction to avoid the case where they all have conflicts and could not be - // scheduled for re-execution immediately even when it's their time to run, because they are already in - // speculative queue. - nextTx := execTasks.takePendingGE(maxValidated + 3) - - if nextTx == -1 { - nextTx = execTasks.takeNextPending() - } - - if nextTx != -1 { - cntExec++ - - task := ExecVersionView{ver: Version{nextTx, txIncarnations[nextTx]}, et: tasks[nextTx], mvh: mvh, sender: tasks[nextTx].Sender()} - - specTaskQueue.Push(nextTx, task) - chSpeculativeTasks <- struct{}{} - } - } - } - - close(chTasks) - close(chSpeculativeTasks) - workerWg.Wait() - close(chResults) - settleWg.Wait() - close(chSettle) - - var dag DAG - if profile { - dag = BuildDAG(*lastTxIO) - } - - return ParallelExecutionResult{lastTxIO, &stats, &dag}, err +} + +// nolint: gocognit +func (pe *ParallelExecutor) Step(res ExecResult) (result ParallelExecutionResult, err error) { + tx := res.ver.TxnIndex + + if _, ok := res.err.(ErrExecAbortError); res.err != nil && !ok { + err = res.err + return + } + + // nolint: nestif + if execErr, ok := res.err.(ErrExecAbortError); ok { + addedDependencies := false + + if execErr.Dependency >= 0 { + l := len(pe.estimateDeps[tx]) + for l > 0 && pe.estimateDeps[tx][l-1] > execErr.Dependency { + pe.execTasks.removeDependency(pe.estimateDeps[tx][l-1]) + pe.estimateDeps[tx] = pe.estimateDeps[tx][:l-1] + l-- + } + + addedDependencies = pe.execTasks.addDependencies(execErr.Dependency, tx) + } else { + estimate := 0 + + if len(pe.estimateDeps[tx]) > 0 { + estimate = pe.estimateDeps[tx][len(pe.estimateDeps[tx])-1] + } + addedDependencies = pe.execTasks.addDependencies(estimate, tx) + newEstimate := estimate + (estimate+tx)/2 + if newEstimate >= tx { + newEstimate = tx - 1 + } + pe.estimateDeps[tx] = append(pe.estimateDeps[tx], newEstimate) + } + + pe.execTasks.clearInProgress(tx) + + if !addedDependencies { + pe.execTasks.pushPending(tx) + } + pe.txIncarnations[tx]++ + pe.diagExecAbort[tx]++ + pe.cntAbort++ + } else { + pe.lastTxIO.recordRead(tx, res.txIn) + + if res.ver.Incarnation == 0 { + pe.lastTxIO.recordWrite(tx, res.txOut) + pe.lastTxIO.recordAllWrite(tx, res.txAllOut) + } else { + if res.txAllOut.hasNewWrite(pe.lastTxIO.AllWriteSet(tx)) { + pe.validateTasks.pushPendingSet(pe.execTasks.getRevalidationRange(tx + 1)) + } + + prevWrite := pe.lastTxIO.AllWriteSet(tx) + + // Remove entries that were previously written but are no longer written + + cmpMap := make(map[Key]bool) + + for _, w := range res.txAllOut { + cmpMap[w.Path] = true + } + + for _, v := range prevWrite { + if _, ok := cmpMap[v.Path]; !ok { + pe.mvh.Delete(v.Path, tx) + } + } + + pe.lastTxIO.recordWrite(tx, res.txOut) + pe.lastTxIO.recordAllWrite(tx, res.txAllOut) + } + + pe.validateTasks.pushPending(tx) + pe.execTasks.markComplete(tx) + pe.diagExecSuccess[tx]++ + pe.cntSuccess++ + + pe.execTasks.removeDependency(tx) + } + + // do validations ... + maxComplete := pe.execTasks.maxAllComplete() + + toValidate := make([]int, 0, 2) + + for pe.validateTasks.minPending() <= maxComplete && pe.validateTasks.minPending() >= 0 { + toValidate = append(toValidate, pe.validateTasks.takeNextPending()) + } + + for i := 0; i < len(toValidate); i++ { + pe.cntTotalValidations++ + + tx := toValidate[i] + + if pe.skipCheck[tx] || ValidateVersion(tx, pe.lastTxIO, pe.mvh) { + pe.validateTasks.markComplete(tx) + } else { + pe.cntValidationFail++ + pe.diagExecAbort[tx]++ + for _, v := range pe.lastTxIO.AllWriteSet(tx) { + pe.mvh.MarkEstimate(v.Path, tx) + } + // 'create validation tasks for all transactions > tx ...' + pe.validateTasks.pushPendingSet(pe.execTasks.getRevalidationRange(tx + 1)) + pe.validateTasks.clearInProgress(tx) // clear in progress - pending will be added again once new incarnation executes + + pe.execTasks.clearComplete(tx) + pe.execTasks.pushPending(tx) + + pe.preValidated[tx] = false + pe.txIncarnations[tx]++ + } + } + + // Settle transactions that have been validated to be correct and that won't be re-executed again + maxValidated := pe.validateTasks.maxAllComplete() + + for pe.lastSettled < maxValidated { + pe.lastSettled++ + if pe.execTasks.checkInProgress(pe.lastSettled) || pe.execTasks.checkPending(pe.lastSettled) || pe.execTasks.isBlocked(pe.lastSettled) { + pe.lastSettled-- + break + } + pe.chSettle <- pe.lastSettled + } + + if pe.validateTasks.countComplete() == len(pe.tasks) && pe.execTasks.countComplete() == len(pe.tasks) { + log.Debug("blockstm exec summary", "execs", pe.cntExec, "success", pe.cntSuccess, "aborts", pe.cntAbort, "validations", pe.cntTotalValidations, "failures", pe.cntValidationFail, "#tasks/#execs", fmt.Sprintf("%.2f%%", float64(len(pe.tasks))/float64(pe.cntExec)*100)) + + close(pe.chTasks) + close(pe.chSpeculativeTasks) + pe.workerWg.Wait() + close(pe.chResults) + pe.settleWg.Wait() + close(pe.chSettle) + + var dag DAG + + if pe.profile { + dag = BuildDAG(*pe.lastTxIO) + } + + return ParallelExecutionResult{pe.lastTxIO, &pe.stats, &dag}, err + } + + // Send the next immediate pending transaction to be executed + if pe.execTasks.minPending() != -1 && pe.execTasks.minPending() == maxValidated+1 { + nextTx := pe.execTasks.takeNextPending() + if nextTx != -1 { + pe.cntExec++ + + pe.skipCheck[nextTx] = true + + pe.chTasks <- ExecVersionView{ver: Version{nextTx, pe.txIncarnations[nextTx]}, et: pe.tasks[nextTx], mvh: pe.mvh, sender: pe.tasks[nextTx].Sender()} + } + } + + // Send speculative tasks + for pe.execTasks.minPending() != -1 || len(pe.execTasks.inProgress) == 0 { + nextTx := pe.execTasks.takeNextPending() + + if nextTx == -1 { + nextTx = pe.execTasks.takeNextPending() + } + + if nextTx != -1 { + pe.cntExec++ + + task := ExecVersionView{ver: Version{nextTx, pe.txIncarnations[nextTx]}, et: pe.tasks[nextTx], mvh: pe.mvh, sender: pe.tasks[nextTx].Sender()} + + pe.specTaskQueue.Push(nextTx, task) + pe.chSpeculativeTasks <- struct{}{} + } + } + + return +} + +type PropertyCheck func(*ParallelExecutor) error + +func executeParallelWithCheck(tasks []ExecTask, profile bool, check PropertyCheck) (result ParallelExecutionResult, err error) { + if len(tasks) == 0 { + return ParallelExecutionResult{MakeTxnInputOutput(len(tasks)), nil, nil}, nil + } + + pe := NewParallelExecutor(tasks, profile) + pe.Prepare() + + for range pe.chResults { + res := pe.resultQueue.Pop().(ExecResult) + + result, err = pe.Step(res) + + if err != nil { + return result, err + } + + if check != nil { + err = check(pe) + } + + if result.TxIO != nil || err != nil { + return result, err + } + } + + return +} + +func ExecuteParallel(tasks []ExecTask, profile bool) (result ParallelExecutionResult, err error) { + return executeParallelWithCheck(tasks, profile, func(pe *ParallelExecutor) error { + return nil + }) } diff --git a/core/blockstm/executor_test.go b/core/blockstm/executor_test.go index 47c875007b..e7f6d685f3 100644 --- a/core/blockstm/executor_test.go +++ b/core/blockstm/executor_test.go @@ -36,7 +36,7 @@ type testExecTask struct { nonce int } -type PathGenerator func(addr common.Address, j int, total int) Key +type PathGenerator func(addr common.Address, i int, j int, total int) Key type TaskRunner func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration) @@ -169,11 +169,11 @@ func longTailTimeGenerator(min time.Duration, max time.Duration, i int, j int) f } } -var randomPathGenerator = func(sender common.Address, j int, total int) Key { - return NewStateKey(sender, common.BigToHash((big.NewInt(int64(total))))) +var randomPathGenerator = func(sender common.Address, i int, j int, total int) Key { + return NewStateKey(common.BigToAddress((big.NewInt(int64(i % 10)))), common.BigToHash((big.NewInt(int64(total))))) } -var dexPathGenerator = func(sender common.Address, j int, total int) Key { +var dexPathGenerator = func(sender common.Address, i int, j int, total int) Key { if j == total-1 || j == 2 { return NewSubpathKey(common.BigToAddress(big.NewInt(int64(0))), 1) } else { @@ -226,10 +226,10 @@ func taskFactory(numTask int, sender Sender, readsPerT int, writesPerT int, nonI // Generate time and key path for each op except first two that are always read and write nonce for j := 2; j < len(ops); j++ { if ops[j].opType == readType { - ops[j].key = pathGenerator(s, j, len(ops)) + ops[j].key = pathGenerator(s, i, j, len(ops)) ops[j].duration = readTime(i, j) } else if ops[j].opType == writeType { - ops[j].key = pathGenerator(s, j, len(ops)) + ops[j].key = pathGenerator(s, i, j, len(ops)) ops[j].duration = writeTime(i, j) } else { ops[j].duration = nonIOTime(i, j) @@ -290,13 +290,64 @@ func testExecutorComb(t *testing.T, totalTxs []int, numReads []int, numWrites [] fmt.Printf("Total exec duration: %v, total serial duration: %v, time reduced: %v, time reduced percent: %.2f%%\n", totalExecDuration, totalSerialDuration, totalSerialDuration-totalExecDuration, float64(totalSerialDuration-totalExecDuration)/float64(totalSerialDuration)*100) } -func runParallel(t *testing.T, tasks []ExecTask, validation func(TxnInputOutput) bool) time.Duration { +func composeValidations(checks []PropertyCheck) PropertyCheck { + return func(pe *ParallelExecutor) error { + for _, check := range checks { + err := check(pe) + if err != nil { + return err + } + } + + return nil + } +} + +func checkNoStatusOverlap(pe *ParallelExecutor) error { + seen := make(map[int]string) + + for _, tx := range pe.execTasks.complete { + seen[tx] = "complete" + } + + for _, tx := range pe.execTasks.inProgress { + if v, ok := seen[tx]; ok { + return fmt.Errorf("tx %v is in both %v and inProgress", v, tx) + } + + seen[tx] = "inProgress" + } + + for _, tx := range pe.execTasks.pending { + if v, ok := seen[tx]; ok { + return fmt.Errorf("tx %v is in both %v complete and pending", v, tx) + } + + seen[tx] = "pending" + } + + return nil +} + +func checkNoDroppedTx(pe *ParallelExecutor) error { + for i := 0; i < len(pe.tasks); i++ { + if !pe.execTasks.checkComplete(i) && !pe.execTasks.checkInProgress(i) && !pe.execTasks.checkPending(i) { + if !pe.execTasks.isBlocked(i) { + return fmt.Errorf("tx %v is not in any status and is not blocked by any other tx", i) + } + } + } + + return nil +} + +func runParallel(t *testing.T, tasks []ExecTask, validation PropertyCheck) time.Duration { t.Helper() start := time.Now() - results, _ := ExecuteParallel(tasks, false) + _, err := executeParallelWithCheck(tasks, false, validation) - txio := results.TxIO + assert.NoError(t, err, "error occur during parallel execution") // Need to apply the final write set to storage @@ -317,10 +368,6 @@ func runParallel(t *testing.T, tasks []ExecTask, validation func(TxnInputOutput) duration := time.Since(start) - if validation != nil { - assert.True(t, validation(*txio)) - } - return duration } @@ -333,6 +380,8 @@ func TestLessConflicts(t *testing.T) { numWrites := []int{20, 100, 200} numNonIO := []int{100, 500} + checks := composeValidations([]PropertyCheck{checkNoStatusOverlap, checkNoDroppedTx}) + taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration) { sender := func(i int) common.Address { randomness := rand.Intn(10) + 10 @@ -340,7 +389,28 @@ func TestLessConflicts(t *testing.T) { } tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, readTime, writeTime, nonIOTime) - return runParallel(t, tasks, nil), serialDuration + return runParallel(t, tasks, checks), serialDuration + } + + testExecutorComb(t, totalTxs, numReads, numWrites, numNonIO, taskRunner) +} + +func TestZeroTx(t *testing.T) { + t.Parallel() + rand.Seed(0) + + totalTxs := []int{0} + numReads := []int{20} + numWrites := []int{20} + numNonIO := []int{100} + + checks := composeValidations([]PropertyCheck{checkNoStatusOverlap, checkNoDroppedTx}) + + taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration) { + sender := func(i int) common.Address { return common.BigToAddress(big.NewInt(int64(1))) } + tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, readTime, writeTime, nonIOTime) + + return runParallel(t, tasks, checks), serialDuration } testExecutorComb(t, totalTxs, numReads, numWrites, numNonIO, taskRunner) @@ -355,11 +425,13 @@ func TestAlternatingTx(t *testing.T) { numWrites := []int{20} numNonIO := []int{100} + checks := composeValidations([]PropertyCheck{checkNoStatusOverlap, checkNoDroppedTx}) + taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration) { sender := func(i int) common.Address { return common.BigToAddress(big.NewInt(int64(i % 2))) } tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, readTime, writeTime, nonIOTime) - return runParallel(t, tasks, nil), serialDuration + return runParallel(t, tasks, checks), serialDuration } testExecutorComb(t, totalTxs, numReads, numWrites, numNonIO, taskRunner) @@ -374,6 +446,8 @@ func TestMoreConflicts(t *testing.T) { numWrites := []int{20, 100, 200} numNonIO := []int{100, 500} + checks := composeValidations([]PropertyCheck{checkNoStatusOverlap, checkNoDroppedTx}) + taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration) { sender := func(i int) common.Address { randomness := rand.Intn(10) + 10 @@ -381,7 +455,7 @@ func TestMoreConflicts(t *testing.T) { } tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, readTime, writeTime, nonIOTime) - return runParallel(t, tasks, nil), serialDuration + return runParallel(t, tasks, checks), serialDuration } testExecutorComb(t, totalTxs, numReads, numWrites, numNonIO, taskRunner) @@ -396,12 +470,14 @@ func TestRandomTx(t *testing.T) { numWrites := []int{20, 100, 200} numNonIO := []int{100, 500} + checks := composeValidations([]PropertyCheck{checkNoStatusOverlap, checkNoDroppedTx}) + taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration) { // Randomly assign this tx to one of 10 senders sender := func(i int) common.Address { return common.BigToAddress(big.NewInt(int64(rand.Intn(10)))) } tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, readTime, writeTime, nonIOTime) - return runParallel(t, tasks, nil), serialDuration + return runParallel(t, tasks, checks), serialDuration } testExecutorComb(t, totalTxs, numReads, numWrites, numNonIO, taskRunner) @@ -416,6 +492,8 @@ func TestTxWithLongTailRead(t *testing.T) { numWrites := []int{20, 100, 200} numNonIO := []int{100, 500} + checks := composeValidations([]PropertyCheck{checkNoStatusOverlap, checkNoDroppedTx}) + taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration) { sender := func(i int) common.Address { randomness := rand.Intn(10) + 10 @@ -426,7 +504,7 @@ func TestTxWithLongTailRead(t *testing.T) { tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, longTailReadTimer, writeTime, nonIOTime) - return runParallel(t, tasks, nil), serialDuration + return runParallel(t, tasks, checks), serialDuration } testExecutorComb(t, totalTxs, numReads, numWrites, numNonIO, taskRunner) @@ -441,29 +519,27 @@ func TestDexScenario(t *testing.T) { numWrites := []int{20, 100, 200} numNonIO := []int{100, 500} - validation := func(txio TxnInputOutput) bool { - for i, inputs := range txio.inputs { - foundDep := false - - for _, input := range inputs { - if input.V.TxnIndex == i-1 { - foundDep = true + postValidation := func(pe *ParallelExecutor) error { + if pe.lastSettled == len(pe.tasks) { + for i, inputs := range pe.lastTxIO.inputs { + for _, input := range inputs { + if input.V.TxnIndex != i-1 { + return fmt.Errorf("Tx %d should depend on tx %d, but it actually depends on %d", i, i-1, input.V.TxnIndex) + } } } - - if !foundDep { - return false - } } - return true + return nil } + checks := composeValidations([]PropertyCheck{checkNoStatusOverlap, postValidation, checkNoDroppedTx}) + taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration) { sender := func(i int) common.Address { return common.BigToAddress(big.NewInt(int64(i))) } tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, dexPathGenerator, readTime, writeTime, nonIOTime) - return runParallel(t, tasks, validation), serialDuration + return runParallel(t, tasks, checks), serialDuration } testExecutorComb(t, totalTxs, numReads, numWrites, numNonIO, taskRunner) diff --git a/core/blockstm/status.go b/core/blockstm/status.go index f10957330c..7a5c895b7e 100644 --- a/core/blockstm/status.go +++ b/core/blockstm/status.go @@ -12,10 +12,10 @@ func makeStatusManager(numTasks int) (t taskStatusManager) { } t.dependency = make(map[int]map[int]bool, numTasks) - t.blockCount = make(map[int]int, numTasks) + t.blocker = make(map[int]map[int]bool, numTasks) for i := 0; i < numTasks; i++ { - t.blockCount[i] = -1 + t.blocker[i] = make(map[int]bool) } return @@ -26,7 +26,7 @@ type taskStatusManager struct { inProgress []int complete []int dependency map[int]map[int]bool - blockCount map[int]int + blocker map[int]map[int]bool } func insertInList(l []int, v int) []int { @@ -56,35 +56,6 @@ func (m *taskStatusManager) takeNextPending() int { return x } -func (m *taskStatusManager) peekPendingGE(n int) int { - x := sort.SearchInts(m.pending, n) - if x >= len(m.pending) { - return -1 - } - - return m.pending[x] -} - -// Take a pending task whose transaction index is greater than or equal to the given tx index -func (m *taskStatusManager) takePendingGE(n int) int { - x := sort.SearchInts(m.pending, n) - if x >= len(m.pending) { - return -1 - } - - v := m.pending[x] - - if x < len(m.pending)-1 { - m.pending = append(m.pending[:x], m.pending[x+1:]...) - } else { - m.pending = m.pending[:x] - } - - m.inProgress = insertInList(m.inProgress, v) - - return v -} - func hasNoGap(l []int) bool { return l[0]+len(l) == l[len(l)-1]+1 } @@ -106,11 +77,7 @@ func (m taskStatusManager) maxAllComplete() int { } func (m *taskStatusManager) pushPending(tx int) { - if !m.checkComplete(tx) && !m.checkInProgress(tx) { - m.pending = insertInList(m.pending, tx) - } else { - panic(fmt.Errorf("should not happen - clear complete or inProgress before pushing pending")) - } + m.pending = insertInList(m.pending, tx) } func removeFromList(l []int, v int, expect bool) []int { @@ -155,15 +122,12 @@ func (m *taskStatusManager) addDependencies(blocker int, dependent int) bool { return false } - curBlocker := m.blockCount[dependent] - - if curBlocker > blocker { - return true - } + curblockers := m.blocker[dependent] if m.checkComplete(blocker) { - // Blocking blocker has already completed - m.blockCount[dependent] = -1 + // Blocker has already completed + delete(curblockers, blocker) + return false } @@ -172,16 +136,21 @@ func (m *taskStatusManager) addDependencies(blocker int, dependent int) bool { } m.dependency[blocker][dependent] = true - m.blockCount[dependent] = blocker + curblockers[blocker] = true return true } +func (m *taskStatusManager) isBlocked(tx int) bool { + return len(m.blocker[tx]) > 0 +} + func (m *taskStatusManager) removeDependency(tx int) { if deps, ok := m.dependency[tx]; ok && len(deps) > 0 { for k := range deps { - if m.blockCount[k] == tx { - m.blockCount[k] = -1 + delete(m.blocker[k], tx) + + if len(m.blocker[k]) == 0 { if !m.checkComplete(k) && !m.checkPending(k) && !m.checkInProgress(k) { m.pushPending(k) } @@ -243,9 +212,7 @@ func (m *taskStatusManager) pushPendingSet(set []int) { m.clearComplete(v) } - if !m.checkInProgress(v) { - m.pushPending(v) - } + m.pushPending(v) } } diff --git a/core/parallel_state_processor.go b/core/parallel_state_processor.go index 1267ede20b..871c38668f 100644 --- a/core/parallel_state_processor.go +++ b/core/parallel_state_processor.go @@ -153,6 +153,17 @@ func (task *ExecutionTask) Sender() common.Address { } func (task *ExecutionTask) Settle() { + defer func() { + if r := recover(); r != nil { + // In some rare cases, ApplyMVWriteSet will panic due to an index out of range error when calculating the + // address hash in sha3 module. Recover from panic and continue the execution. + // After recovery, block receipts or merckle root will be incorrect, but this is fine, because the block + // will be rejected and re-synced. + log.Info("Recovered from error", "Error:", r) + return + } + }() + task.finalStateDB.Prepare(task.tx.Hash(), task.index) coinbase, _ := task.blockChain.Engine().Author(task.header) diff --git a/core/state/statedb.go b/core/state/statedb.go index a650be1130..d10b5fd564 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -275,11 +275,8 @@ func MVRead[T any](s *StateDB, k blockstm.Key, defaultV T, readStorage func(s *S } case blockstm.MVReadResultDependency: { - if res.DepIdx() > s.dep { - s.dep = res.DepIdx() - } + s.dep = res.DepIdx() - // Return immediate to executor when we found a dependency panic("Found dependency") } case blockstm.MVReadResultNone: From e63e390ec70b62703b3acf3e15ced1ea9b08d486 Mon Sep 17 00:00:00 2001 From: Jerry Date: Tue, 27 Sep 2022 22:52:45 -0700 Subject: [PATCH 11/38] Recognize bad transactions and break loop in blockstm executor --- core/blockchain.go | 14 ++++++ core/blockstm/executor.go | 50 ++++++++++++------- core/blockstm/executor_test.go | 6 ++- core/parallel_state_processor.go | 15 +++--- core/state_processor_test.go | 86 ++++++++++++++++++-------------- 5 files changed, 104 insertions(+), 67 deletions(-) diff --git a/core/blockchain.go b/core/blockchain.go index fe8172e41e..47164cc5b9 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -223,6 +223,7 @@ type BlockChain struct { // NewBlockChain returns a fully initialised block chain using information // available in the database. It initialises the default Ethereum Validator // and Processor. +// //nolint:gocognit func NewBlockChain(db ethdb.Database, cacheConfig *CacheConfig, chainConfig *params.ChainConfig, engine consensus.Engine, vmConfig vm.Config, shouldPreserve func(header *types.Header) bool, txLookupLimit *uint64, checker ethereum.ChainValidator) (*BlockChain, error) { if cacheConfig == nil { @@ -420,6 +421,19 @@ func NewBlockChain(db ethdb.Database, cacheConfig *CacheConfig, chainConfig *par return bc, nil } +// Similar to NewBlockChain, this function creates a new blockchain object, but with a parallel state processor +func NewParallelBlockChain(db ethdb.Database, cacheConfig *CacheConfig, chainConfig *params.ChainConfig, engine consensus.Engine, vmConfig vm.Config, shouldPreserve func(header *types.Header) bool, txLookupLimit *uint64, checker ethereum.ChainValidator) (*BlockChain, error) { + bc, err := NewBlockChain(db, cacheConfig, chainConfig, engine, vmConfig, shouldPreserve, txLookupLimit, checker) + + if err != nil { + return nil, err + } + + bc.processor = NewParallelStateProcessor(chainConfig, bc, engine) + + return bc, nil +} + // empty returns an indicator whether the blockchain is empty. // Note, it's a special case that we connect a non-empty ancient // database with an empty node, so that we can plugin the ancient diff --git a/core/blockstm/executor.go b/core/blockstm/executor.go index f0c05a7d94..0e2e8af137 100644 --- a/core/blockstm/executor.go +++ b/core/blockstm/executor.go @@ -23,6 +23,7 @@ type ExecTask interface { MVReadList() []ReadDescriptor MVWriteList() []WriteDescriptor MVFullWriteList() []WriteDescriptor + Hash() common.Hash Sender() common.Address Settle() } @@ -48,7 +49,8 @@ func (ev *ExecVersionView) Execute() (er ExecResult) { } type ErrExecAbortError struct { - Dependency int + Dependency int + OriginError error } func (e ErrExecAbortError) Error() string { @@ -308,12 +310,33 @@ func (pe *ParallelExecutor) Prepare() { } } +func (pe *ParallelExecutor) Close(wait bool) { + close(pe.chTasks) + close(pe.chSpeculativeTasks) + + if wait { + pe.workerWg.Wait() + } + + close(pe.chResults) + + if wait { + pe.settleWg.Wait() + } + + close(pe.chSettle) +} + // nolint: gocognit -func (pe *ParallelExecutor) Step(res ExecResult) (result ParallelExecutionResult, err error) { +func (pe *ParallelExecutor) Step(res *ExecResult) (result ParallelExecutionResult, err error) { tx := res.ver.TxnIndex - if _, ok := res.err.(ErrExecAbortError); res.err != nil && !ok { - err = res.err + if abortErr, ok := res.err.(ErrExecAbortError); ok && abortErr.OriginError != nil && pe.skipCheck[tx] { + // If the transaction failed when we know it should not fail, this means the transaction itself is + // bad (e.g. wrong nonce), and we should exit the execution immediately + err = fmt.Errorf("could not apply tx %d [%v]: %w", tx, pe.tasks[tx].Hash(), abortErr.OriginError) + pe.Close(false) + return } @@ -440,12 +463,7 @@ func (pe *ParallelExecutor) Step(res ExecResult) (result ParallelExecutionResult if pe.validateTasks.countComplete() == len(pe.tasks) && pe.execTasks.countComplete() == len(pe.tasks) { log.Debug("blockstm exec summary", "execs", pe.cntExec, "success", pe.cntSuccess, "aborts", pe.cntAbort, "validations", pe.cntTotalValidations, "failures", pe.cntValidationFail, "#tasks/#execs", fmt.Sprintf("%.2f%%", float64(len(pe.tasks))/float64(pe.cntExec)*100)) - close(pe.chTasks) - close(pe.chSpeculativeTasks) - pe.workerWg.Wait() - close(pe.chResults) - pe.settleWg.Wait() - close(pe.chSettle) + pe.Close(true) var dag DAG @@ -469,13 +487,9 @@ func (pe *ParallelExecutor) Step(res ExecResult) (result ParallelExecutionResult } // Send speculative tasks - for pe.execTasks.minPending() != -1 || len(pe.execTasks.inProgress) == 0 { + for pe.execTasks.minPending() != -1 { nextTx := pe.execTasks.takeNextPending() - if nextTx == -1 { - nextTx = pe.execTasks.takeNextPending() - } - if nextTx != -1 { pe.cntExec++ @@ -502,7 +516,7 @@ func executeParallelWithCheck(tasks []ExecTask, profile bool, check PropertyChec for range pe.chResults { res := pe.resultQueue.Pop().(ExecResult) - result, err = pe.Step(res) + result, err = pe.Step(&res) if err != nil { return result, err @@ -521,7 +535,5 @@ func executeParallelWithCheck(tasks []ExecTask, profile bool, check PropertyChec } func ExecuteParallel(tasks []ExecTask, profile bool) (result ParallelExecutionResult, err error) { - return executeParallelWithCheck(tasks, profile, func(pe *ParallelExecutor) error { - return nil - }) + return executeParallelWithCheck(tasks, profile, nil) } diff --git a/core/blockstm/executor_test.go b/core/blockstm/executor_test.go index e7f6d685f3..bed60cdcd4 100644 --- a/core/blockstm/executor_test.go +++ b/core/blockstm/executor_test.go @@ -117,7 +117,7 @@ func (t *testExecTask) Execute(mvh *MVHashMap, incarnation int) error { } if deps != -1 { - return ErrExecAbortError{deps} + return ErrExecAbortError{deps, fmt.Errorf("Dependency error")} } return nil @@ -153,6 +153,10 @@ func (t *testExecTask) Sender() common.Address { return t.sender } +func (t *testExecTask) Hash() common.Hash { + return common.BytesToHash([]byte(fmt.Sprintf("%d", t.txIdx))) +} + func randTimeGenerator(min time.Duration, max time.Duration) func(txIdx int, opIdx int) time.Duration { return func(txIdx int, opIdx int) time.Duration { return time.Duration(rand.Int63n(int64(max-min))) + min diff --git a/core/parallel_state_processor.go b/core/parallel_state_processor.go index 871c38668f..3a530852fa 100644 --- a/core/parallel_state_processor.go +++ b/core/parallel_state_processor.go @@ -19,7 +19,6 @@ package core import ( "fmt" "math/big" - "time" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/consensus" @@ -106,7 +105,7 @@ func (task *ExecutionTask) Execute(mvh *blockstm.MVHashMap, incarnation int) (er task.result, err = ApplyMessageNoFeeBurnOrTip(evm, task.msg, new(GasPool).AddGas(task.gasLimit)) if task.result == nil || err != nil { - return blockstm.ErrExecAbortError{Dependency: task.statedb.DepTxIndex()} + return blockstm.ErrExecAbortError{Dependency: task.statedb.DepTxIndex(), OriginError: err} } reads := task.statedb.MVReadMap() @@ -127,7 +126,7 @@ func (task *ExecutionTask) Execute(mvh *blockstm.MVHashMap, incarnation int) (er } if task.statedb.HadInvalidRead() || err != nil { - err = blockstm.ErrExecAbortError{Dependency: task.statedb.DepTxIndex()} + err = blockstm.ErrExecAbortError{Dependency: task.statedb.DepTxIndex(), OriginError: err} return } @@ -152,6 +151,10 @@ func (task *ExecutionTask) Sender() common.Address { return task.sender } +func (task *ExecutionTask) Hash() common.Hash { + return task.tx.Hash() +} + func (task *ExecutionTask) Settle() { defer func() { if r := recover(); r != nil { @@ -342,14 +345,8 @@ func (p *ParallelStateProcessor) Process(block *types.Block, statedb *state.Stat return nil, nil, 0, err } - statedb.Finalise(p.config.IsEIP158(blockNumber)) - - start := time.Now() - // Finalize the block, applying any consensus engine specific extras (e.g. block rewards) p.engine.Finalize(p.bc, header, statedb, block.Transactions(), block.Uncles()) - fmt.Println("Finalize time of parallel execution:", time.Since(start)) - return receipts, allLogs, *usedGas, nil } diff --git a/core/state_processor_test.go b/core/state_processor_test.go index 5dc076a11c..5eb7938811 100644 --- a/core/state_processor_test.go +++ b/core/state_processor_test.go @@ -234,28 +234,33 @@ func TestStateProcessorErrors(t *testing.T) { }, }, } - genesis = gspec.MustCommit(db) - blockchain, _ = NewBlockChain(db, nil, gspec.Config, ethash.NewFaker(), vm.Config{}, nil, nil, nil) + genesis = gspec.MustCommit(db) + blockchain, _ = NewBlockChain(db, nil, gspec.Config, ethash.NewFaker(), vm.Config{}, nil, nil, nil) + parallelBlockchain, _ = NewBlockChain(db, nil, gspec.Config, ethash.NewFaker(), vm.Config{}, nil, nil, nil) ) defer blockchain.Stop() - for i, tt := range []struct { - txs []*types.Transaction - want string - }{ - { // ErrTxTypeNotSupported - txs: []*types.Transaction{ - mkDynamicTx(0, common.Address{}, params.TxGas-1000, big.NewInt(0), big.NewInt(0)), + defer parallelBlockchain.Stop() + + for _, bc := range []*BlockChain{blockchain, parallelBlockchain} { + for i, tt := range []struct { + txs []*types.Transaction + want string + }{ + { // ErrTxTypeNotSupported + txs: []*types.Transaction{ + mkDynamicTx(0, common.Address{}, params.TxGas-1000, big.NewInt(0), big.NewInt(0)), + }, + want: "could not apply tx 0 [0x88626ac0d53cb65308f2416103c62bb1f18b805573d4f96a3640bbbfff13c14f]: transaction type not supported", }, - want: "could not apply tx 0 [0x88626ac0d53cb65308f2416103c62bb1f18b805573d4f96a3640bbbfff13c14f]: transaction type not supported", - }, - } { - block := GenerateBadBlock(genesis, ethash.NewFaker(), tt.txs, gspec.Config) - _, err := blockchain.InsertChain(types.Blocks{block}) - if err == nil { - t.Fatal("block imported without errors") - } - if have, want := err.Error(), tt.want; have != want { - t.Errorf("test %d:\nhave \"%v\"\nwant \"%v\"\n", i, have, want) + } { + block := GenerateBadBlock(genesis, ethash.NewFaker(), tt.txs, gspec.Config) + _, err := bc.InsertChain(types.Blocks{block}) + if err == nil { + t.Fatal("block imported without errors") + } + if have, want := err.Error(), tt.want; have != want { + t.Errorf("test %d:\nhave \"%v\"\nwant \"%v\"\n", i, have, want) + } } } } @@ -274,28 +279,33 @@ func TestStateProcessorErrors(t *testing.T) { }, }, } - genesis = gspec.MustCommit(db) - blockchain, _ = NewBlockChain(db, nil, gspec.Config, ethash.NewFaker(), vm.Config{}, nil, nil, nil) + genesis = gspec.MustCommit(db) + blockchain, _ = NewBlockChain(db, nil, gspec.Config, ethash.NewFaker(), vm.Config{}, nil, nil, nil) + parallelBlockchain, _ = NewBlockChain(db, nil, gspec.Config, ethash.NewFaker(), vm.Config{}, nil, nil, nil) ) defer blockchain.Stop() - for i, tt := range []struct { - txs []*types.Transaction - want string - }{ - { // ErrSenderNoEOA - txs: []*types.Transaction{ - mkDynamicTx(0, common.Address{}, params.TxGas-1000, big.NewInt(0), big.NewInt(0)), + defer parallelBlockchain.Stop() + + for _, bc := range []*BlockChain{blockchain, parallelBlockchain} { + for i, tt := range []struct { + txs []*types.Transaction + want string + }{ + { // ErrSenderNoEOA + txs: []*types.Transaction{ + mkDynamicTx(0, common.Address{}, params.TxGas-1000, big.NewInt(0), big.NewInt(0)), + }, + want: "could not apply tx 0 [0x88626ac0d53cb65308f2416103c62bb1f18b805573d4f96a3640bbbfff13c14f]: sender not an eoa: address 0x71562b71999873DB5b286dF957af199Ec94617F7, codehash: 0x9280914443471259d4570a8661015ae4a5b80186dbc619658fb494bebc3da3d1", }, - want: "could not apply tx 0 [0x88626ac0d53cb65308f2416103c62bb1f18b805573d4f96a3640bbbfff13c14f]: sender not an eoa: address 0x71562b71999873DB5b286dF957af199Ec94617F7, codehash: 0x9280914443471259d4570a8661015ae4a5b80186dbc619658fb494bebc3da3d1", - }, - } { - block := GenerateBadBlock(genesis, ethash.NewFaker(), tt.txs, gspec.Config) - _, err := blockchain.InsertChain(types.Blocks{block}) - if err == nil { - t.Fatal("block imported without errors") - } - if have, want := err.Error(), tt.want; have != want { - t.Errorf("test %d:\nhave \"%v\"\nwant \"%v\"\n", i, have, want) + } { + block := GenerateBadBlock(genesis, ethash.NewFaker(), tt.txs, gspec.Config) + _, err := bc.InsertChain(types.Blocks{block}) + if err == nil { + t.Fatal("block imported without errors") + } + if have, want := err.Error(), tt.want; have != want { + t.Errorf("test %d:\nhave \"%v\"\nwant \"%v\"\n", i, have, want) + } } } } From ad658b6b997a8fb880cba03cef33f076c801227b Mon Sep 17 00:00:00 2001 From: Jerry Date: Thu, 29 Sep 2022 22:29:06 -0700 Subject: [PATCH 12/38] Minor bug fix --- core/blockstm/status.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/blockstm/status.go b/core/blockstm/status.go index 7a5c895b7e..3025cf6c3e 100644 --- a/core/blockstm/status.go +++ b/core/blockstm/status.go @@ -128,7 +128,7 @@ func (m *taskStatusManager) addDependencies(blocker int, dependent int) bool { // Blocker has already completed delete(curblockers, blocker) - return false + return len(curblockers) > 0 } if _, ok := m.dependency[blocker]; !ok { From 6f0d16fbeb634210ac890233c02d096c0f0abbbd Mon Sep 17 00:00:00 2001 From: Jerry Date: Tue, 4 Oct 2022 17:01:39 -0700 Subject: [PATCH 13/38] Add ability to calculate the longest execution path in a block --- core/blockstm/dag.go | 98 +++++++++++++++++++------------- core/blockstm/executor.go | 27 ++++++--- core/blockstm/executor_test.go | 8 ++- core/parallel_state_processor.go | 24 +++++++- 4 files changed, 110 insertions(+), 47 deletions(-) diff --git a/core/blockstm/dag.go b/core/blockstm/dag.go index 3fe714a50e..d122877fe9 100644 --- a/core/blockstm/dag.go +++ b/core/blockstm/dag.go @@ -2,8 +2,8 @@ package blockstm import ( "fmt" - "sort" "strings" + "time" "github.com/heimdalr/dag" @@ -62,8 +62,6 @@ func BuildDAG(deps TxnInputOutput) (d DAG) { if err != nil { log.Warn("Failed to add edge", "from", txFromId, "to", txToId, "err", err) } - - break // once we add a 'backward' dep we can't execute before that transaction so no need to proceed } } } @@ -71,23 +69,67 @@ func BuildDAG(deps TxnInputOutput) (d DAG) { return } -func (d DAG) Report(out func(string)) { - roots := make([]int, 0) - rootIds := make([]string, 0) - rootIdMap := make(map[int]string, len(d.GetRoots())) +// Find the longest execution path in the DAG +func (d DAG) LongestPath(stats map[int]ExecutionStat) ([]int, uint64) { + prev := make(map[int]int, len(d.GetVertices())) - for k, i := range d.GetRoots() { - roots = append(roots, i.(int)) - rootIdMap[i.(int)] = k + for i := 0; i < len(d.GetVertices()); i++ { + prev[i] = -1 } - sort.Ints(roots) + pathWeights := make(map[int]uint64, len(d.GetVertices())) - for _, i := range roots { - rootIds = append(rootIds, rootIdMap[i]) + maxPath := 0 + maxPathWeight := uint64(0) + + idxToId := make(map[int]string, len(d.GetVertices())) + + for k, i := range d.GetVertices() { + idxToId[i.(int)] = k } - fmt.Println(roots) + for i := 0; i < len(idxToId); i++ { + parents, _ := d.GetParents(idxToId[i]) + + if len(parents) > 0 { + for _, p := range parents { + weight := pathWeights[p.(int)] + stats[i].End - stats[i].Start + if weight > pathWeights[i] { + pathWeights[i] = weight + prev[i] = p.(int) + } + } + } else { + pathWeights[i] = stats[i].End - stats[i].Start + } + + if pathWeights[i] > maxPathWeight { + maxPath = i + maxPathWeight = pathWeights[i] + } + } + + path := make([]int, 0) + for i := maxPath; i != -1; i = prev[i] { + path = append(path, i) + } + + // Reverse the path so the transactions are in the ascending order + for i, j := 0, len(path)-1; i < j; i, j = i+1, j-1 { + path[i], path[j] = path[j], path[i] + } + + return path, maxPathWeight +} + +func (d DAG) Report(stats map[int]ExecutionStat, out func(string)) { + longestPath, weight := d.LongestPath(stats) + + serialWeight := uint64(0) + + for i := 0; i < len(d.GetVertices()); i++ { + serialWeight += stats[i].End - stats[i].Start + } makeStrs := func(ints []int) (ret []string) { for _, v := range ints { @@ -97,29 +139,9 @@ func (d DAG) Report(out func(string)) { return } - maxDesc := 0 - maxDeps := 0 - totalDeps := 0 + out("Longest execution path:") + out(fmt.Sprintf("(%v) %v", len(longestPath), strings.Join(makeStrs(longestPath), "->"))) - for k, v := range roots { - ids := []int{v} - desc, _ := d.GetDescendants(rootIds[k]) - - for _, i := range desc { - ids = append(ids, i.(int)) - } - - sort.Ints(ids) - out(fmt.Sprintf("(%v) %v", len(ids), strings.Join(makeStrs(ids), "->"))) - - if len(desc) > maxDesc { - maxDesc = len(desc) - } - } - - numTx := len(d.DAG.GetVertices()) - out(fmt.Sprintf("max chain length: %v of %v (%v%%)", maxDesc+1, numTx, - fmt.Sprintf("%.1f", float64(maxDesc+1)*100.0/float64(numTx)))) - out(fmt.Sprintf("max dep count: %v of %v (%v%%)", maxDeps, totalDeps, - fmt.Sprintf("%.1f", float64(maxDeps)*100.0/float64(totalDeps)))) + out(fmt.Sprintf("Longest path ideal execution time: %v of %v (serial total), %v%%", time.Duration(weight), + time.Duration(serialWeight), fmt.Sprintf("%.1f", float64(weight)*100.0/float64(serialWeight)))) } diff --git a/core/blockstm/executor.go b/core/blockstm/executor.go index 0e2e8af137..18fd81c1c6 100644 --- a/core/blockstm/executor.go +++ b/core/blockstm/executor.go @@ -123,7 +123,7 @@ func (pq *SafePriorityQueue) Len() int { type ParallelExecutionResult struct { TxIO *TxnInputOutput - Stats *[][]uint64 + Stats *map[int]ExecutionStat Deps *DAG } @@ -133,8 +133,9 @@ const numSpeculativeProcs = 8 type ParallelExecutor struct { tasks []ExecTask - // Stores the execution statistics for each task - stats [][]uint64 + // Stores the execution statistics for the last incarnation of each task + stats map[int]ExecutionStat + statsMutex sync.Mutex // Channel for tasks that should be prioritized @@ -202,12 +203,20 @@ type ParallelExecutor struct { workerWg sync.WaitGroup } +type ExecutionStat struct { + TxIdx int + Incarnation int + Start uint64 + End uint64 + Worker int +} + func NewParallelExecutor(tasks []ExecTask, profile bool) *ParallelExecutor { numTasks := len(tasks) pe := &ParallelExecutor{ tasks: tasks, - stats: make([][]uint64, numTasks), + stats: make(map[int]ExecutionStat, numTasks), chTasks: make(chan ExecVersionView, numTasks), chSpeculativeTasks: make(chan struct{}, numTasks), chSettle: make(chan int, numTasks), @@ -272,10 +281,14 @@ func (pe *ParallelExecutor) Prepare() { if pe.profile { end := time.Since(pe.begin) - stat := []uint64{uint64(res.ver.TxnIndex), uint64(res.ver.Incarnation), uint64(start), uint64(end), uint64(procNum)} - pe.statsMutex.Lock() - pe.stats = append(pe.stats, stat) + pe.stats[res.ver.TxnIndex] = ExecutionStat{ + TxIdx: res.ver.TxnIndex, + Incarnation: res.ver.Incarnation, + Start: uint64(start), + End: uint64(end), + Worker: procNum, + } pe.statsMutex.Unlock() } } diff --git a/core/blockstm/executor_test.go b/core/blockstm/executor_test.go index bed60cdcd4..c62e0ae9a4 100644 --- a/core/blockstm/executor_test.go +++ b/core/blockstm/executor_test.go @@ -348,8 +348,14 @@ func checkNoDroppedTx(pe *ParallelExecutor) error { func runParallel(t *testing.T, tasks []ExecTask, validation PropertyCheck) time.Duration { t.Helper() + profile := false + start := time.Now() - _, err := executeParallelWithCheck(tasks, false, validation) + result, err := executeParallelWithCheck(tasks, profile, validation) + + if result.Deps != nil && profile { + result.Deps.Report(*result.Stats, func(str string) { fmt.Println(str) }) + } assert.NoError(t, err, "error occur during parallel execution") diff --git a/core/parallel_state_processor.go b/core/parallel_state_processor.go index 3a530852fa..c936f399cd 100644 --- a/core/parallel_state_processor.go +++ b/core/parallel_state_processor.go @@ -19,6 +19,7 @@ package core import ( "fmt" "math/big" + "time" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/consensus" @@ -29,6 +30,7 @@ import ( "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/params" ) @@ -247,6 +249,8 @@ func (task *ExecutionTask) Settle() { *task.allLogs = append(*task.allLogs, receipt.Logs...) } +var parallelizabilityTimer = metrics.NewRegisteredTimer("block/parallelizability", 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. @@ -314,7 +318,25 @@ func (p *ParallelStateProcessor) Process(block *types.Block, statedb *state.Stat } backupStateDB := statedb.Copy() - _, err := blockstm.ExecuteParallel(tasks, false) + + profile := false + result, err := blockstm.ExecuteParallel(tasks, profile) + + if err == nil && profile { + _, weight := result.Deps.LongestPath(*result.Stats) + + serialWeight := uint64(0) + + for i := 0; i < len(result.Deps.GetVertices()); i++ { + serialWeight += (*result.Stats)[i].End - (*result.Stats)[i].Start + } + + parallelizabilityTimer.Update(time.Duration(serialWeight * 100 / weight)) + + log.Info("Parallelizability", "Average (%)", parallelizabilityTimer.Mean()) + + log.Info("Parallelizability", "Histogram (%)", parallelizabilityTimer.Percentiles([]float64{0.001, 0.01, 0.05, 0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99, 0.999, 0.9999})) + } for _, task := range tasks { task := task.(*ExecutionTask) From c59bb6ef16a426a9f7a75ae3472c5b89e1421d31 Mon Sep 17 00:00:00 2001 From: Pratik Patil Date: Fri, 7 Oct 2022 09:14:18 +0530 Subject: [PATCH 14/38] Added unit tests for MV HashMap (#492) * Create MVHashMap and use it StateDB * Parallel state processor * Move fee burning and tipping out of state transition to reduce read/write dependencies between transactions * Re-execute parallel tasks when there is a read in coinbase or burn address * Block-stm optimization Added tests for executor and two major improvements: 1. Add a dependency map during execution. This will prevent aborted tasks from being sent for execution immedaitely after failure. 2. Change the key of MVHashMap from string to a byte array. This will reduce time to convert byte slices to strings. * Remove cache from executor test * added mvhashmap unit tests (with as key) * Shard mvhashmap to reduce the time spent in global mutex * Skip applying intermediate states * Dependency improvement * added test for status * Create MVHashMap and use it StateDB * Parallel state processor * Move fee burning and tipping out of state transition to reduce read/write dependencies between transactions * Re-execute parallel tasks when there is a read in coinbase or burn address * Txn prioritizer implemented using mutex map (#487) * basic txn prioritizer implemented using mutex map * Re-execute parallel tasks when there is a read in coinbase or burn address * Re-execute parallel tasks when there is a read in coinbase or burn address * using *sync.RWMutex{} in mutexMap Co-authored-by: Jerry * added getReadMap and getWriteMap (#473) * Block-stm optimization Added tests for executor and some improvements: 1. Add a dependency map during execution. This will prevent aborted tasks from being sent for execution immedaitely after failure. 2. Change the key of MVHashMap from string to a byte array. This will reduce time to convert byte slices to strings. 3. Use sync.Map to reduce the time spent in global mutex. 4. Skip applying intermediate states. 5. Estimate dependency when an execution fails without dependency information. 6. Divide execution task queue into two separate queues. One for relatively certain transactions, and the other for speculative future transactions. 7. Setting dependencies of Txs coming from the same sender before starting parallel execution. 8. Process results in their semantic order (transaction index) instead of the order when they arrive. Replace result channel with a priority queue. * Do not write entire objects directly when applying write set in blockstm * fixed a small bug in the Report function (#530) * linters Co-authored-by: Jerry --- core/blockstm/mvhashmap_test.go | 344 ++++++++++++++++++++++++++++++++ core/blockstm/status_test.go | 84 ++++++++ go.mod | 2 + go.sum | 2 + 4 files changed, 432 insertions(+) create mode 100644 core/blockstm/mvhashmap_test.go create mode 100644 core/blockstm/status_test.go diff --git a/core/blockstm/mvhashmap_test.go b/core/blockstm/mvhashmap_test.go new file mode 100644 index 0000000000..7ed728426c --- /dev/null +++ b/core/blockstm/mvhashmap_test.go @@ -0,0 +1,344 @@ +package blockstm + +import ( + "fmt" + "math/big" + "math/rand" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ethereum/go-ethereum/common" +) + +var randomness = rand.Intn(10) + 10 + +// create test data for a given txIdx and incarnation +func valueFor(txIdx, inc int) []byte { + return []byte(fmt.Sprintf("%ver:%ver:%ver", txIdx*5, txIdx+inc, inc*5)) +} + +func getCommonAddress(i int) common.Address { + return common.BigToAddress(big.NewInt(int64(i % randomness))) +} + +func TestHelperFunctions(t *testing.T) { + t.Parallel() + + ap1 := NewAddressKey(getCommonAddress(1)) + ap2 := NewAddressKey(getCommonAddress(2)) + + mvh := MakeMVHashMap() + + mvh.Write(ap1, Version{0, 1}, valueFor(0, 1)) + mvh.Write(ap1, Version{0, 2}, valueFor(0, 2)) + res := mvh.Read(ap1, 0) + require.Equal(t, -1, res.DepIdx()) + require.Equal(t, -1, res.Incarnation()) + require.Equal(t, 2, res.Status()) + + mvh.Write(ap2, Version{1, 1}, valueFor(1, 1)) + mvh.Write(ap2, Version{1, 2}, valueFor(1, 2)) + res = mvh.Read(ap2, 1) + require.Equal(t, -1, res.DepIdx()) + require.Equal(t, -1, res.Incarnation()) + require.Equal(t, 2, res.Status()) + + mvh.Write(ap1, Version{2, 1}, valueFor(2, 1)) + mvh.Write(ap1, Version{2, 2}, valueFor(2, 2)) + res = mvh.Read(ap1, 2) + require.Equal(t, 0, res.DepIdx()) + require.Equal(t, 2, res.Incarnation()) + require.Equal(t, valueFor(0, 2), res.Value().([]byte)) + require.Equal(t, 0, res.Status()) +} + +func TestFlushMVWrite(t *testing.T) { + t.Parallel() + + ap1 := NewAddressKey(getCommonAddress(1)) + ap2 := NewAddressKey(getCommonAddress(2)) + + mvh := MakeMVHashMap() + + var res MVReadResult + + wd := []WriteDescriptor{} + + wd = append(wd, WriteDescriptor{ + Path: ap1, + V: Version{0, 1}, + Val: valueFor(0, 1), + }) + wd = append(wd, WriteDescriptor{ + Path: ap1, + V: Version{0, 2}, + Val: valueFor(0, 2), + }) + wd = append(wd, WriteDescriptor{ + Path: ap2, + V: Version{1, 1}, + Val: valueFor(1, 1), + }) + wd = append(wd, WriteDescriptor{ + Path: ap2, + V: Version{1, 2}, + Val: valueFor(1, 2), + }) + wd = append(wd, WriteDescriptor{ + Path: ap1, + V: Version{2, 1}, + Val: valueFor(2, 1), + }) + wd = append(wd, WriteDescriptor{ + Path: ap1, + V: Version{2, 2}, + Val: valueFor(2, 2), + }) + + mvh.FlushMVWriteSet(wd) + + res = mvh.Read(ap1, 0) + require.Equal(t, -1, res.DepIdx()) + require.Equal(t, -1, res.Incarnation()) + require.Equal(t, 2, res.Status()) + + res = mvh.Read(ap2, 1) + require.Equal(t, -1, res.DepIdx()) + require.Equal(t, -1, res.Incarnation()) + require.Equal(t, 2, res.Status()) + + res = mvh.Read(ap1, 2) + require.Equal(t, 0, res.DepIdx()) + require.Equal(t, 2, res.Incarnation()) + require.Equal(t, valueFor(0, 2), res.Value().([]byte)) + require.Equal(t, 0, res.Status()) +} + +// TODO - handle panic +func TestLowerIncarnation(t *testing.T) { + t.Parallel() + + ap1 := NewAddressKey(getCommonAddress(1)) + + mvh := MakeMVHashMap() + + mvh.Write(ap1, Version{0, 2}, valueFor(0, 2)) + mvh.Read(ap1, 0) + mvh.Write(ap1, Version{1, 2}, valueFor(1, 2)) + mvh.Write(ap1, Version{0, 5}, valueFor(0, 5)) + mvh.Write(ap1, Version{1, 5}, valueFor(1, 5)) +} + +func TestMarkEstimate(t *testing.T) { + t.Parallel() + + ap1 := NewAddressKey(getCommonAddress(1)) + + mvh := MakeMVHashMap() + + mvh.Write(ap1, Version{7, 2}, valueFor(7, 2)) + mvh.MarkEstimate(ap1, 7) + mvh.Write(ap1, Version{7, 4}, valueFor(7, 4)) +} + +func TestMVHashMapBasics(t *testing.T) { + t.Parallel() + + // memory locations + ap1 := NewAddressKey(getCommonAddress(1)) + ap2 := NewAddressKey(getCommonAddress(2)) + ap3 := NewAddressKey(getCommonAddress(3)) + + mvh := MakeMVHashMap() + + res := mvh.Read(ap1, 5) + require.Equal(t, -1, res.depIdx) + + mvh.Write(ap1, Version{10, 1}, valueFor(10, 1)) + + res = mvh.Read(ap1, 9) + require.Equal(t, -1, res.depIdx, "reads that should go the the DB return dependency -1") + res = mvh.Read(ap1, 10) + require.Equal(t, -1, res.depIdx, "Read returns entries from smaller txns, not txn 10") + + // Reads for a higher txn return the entry written by txn 10. + res = mvh.Read(ap1, 15) + require.Equal(t, 10, res.depIdx, "reads for a higher txn return the entry written by txn 10.") + require.Equal(t, 1, res.incarnation) + require.Equal(t, valueFor(10, 1), res.value) + + // More writes. + mvh.Write(ap1, Version{12, 0}, valueFor(12, 0)) + mvh.Write(ap1, Version{8, 3}, valueFor(8, 3)) + + // Verify reads. + res = mvh.Read(ap1, 15) + require.Equal(t, 12, res.depIdx) + require.Equal(t, 0, res.incarnation) + require.Equal(t, valueFor(12, 0), res.value) + + res = mvh.Read(ap1, 11) + require.Equal(t, 10, res.depIdx) + require.Equal(t, 1, res.incarnation) + require.Equal(t, valueFor(10, 1), res.value) + + res = mvh.Read(ap1, 10) + require.Equal(t, 8, res.depIdx) + require.Equal(t, 3, res.incarnation) + require.Equal(t, valueFor(8, 3), res.value) + + // Mark the entry written by 10 as an estimate. + mvh.MarkEstimate(ap1, 10) + + res = mvh.Read(ap1, 11) + require.Equal(t, 10, res.depIdx) + require.Equal(t, -1, res.incarnation, "dep at tx 10 is now an estimate") + + // Delete the entry written by 10, write to a different ap. + mvh.Delete(ap1, 10) + mvh.Write(ap2, Version{10, 2}, valueFor(10, 2)) + + // Read by txn 11 no longer observes entry from txn 10. + res = mvh.Read(ap1, 11) + require.Equal(t, 8, res.depIdx) + require.Equal(t, 3, res.incarnation) + require.Equal(t, valueFor(8, 3), res.value) + + // Reads, writes for ap2 and ap3. + mvh.Write(ap2, Version{5, 0}, valueFor(5, 0)) + mvh.Write(ap3, Version{20, 4}, valueFor(20, 4)) + + res = mvh.Read(ap2, 10) + require.Equal(t, 5, res.depIdx) + require.Equal(t, 0, res.incarnation) + require.Equal(t, valueFor(5, 0), res.value) + + res = mvh.Read(ap3, 21) + require.Equal(t, 20, res.depIdx) + require.Equal(t, 4, res.incarnation) + require.Equal(t, valueFor(20, 4), res.value) + + // Clear ap1 and ap3. + mvh.Delete(ap1, 12) + mvh.Delete(ap1, 8) + mvh.Delete(ap3, 20) + + // Reads from ap1 and ap3 go to db. + res = mvh.Read(ap1, 30) + require.Equal(t, -1, res.depIdx) + + res = mvh.Read(ap3, 30) + require.Equal(t, -1, res.depIdx) + + // No-op delete at ap2 - doesn't panic because ap2 does exist + mvh.Delete(ap2, 11) + + // Read entry by txn 10 at ap2. + res = mvh.Read(ap2, 15) + require.Equal(t, 10, res.depIdx) + require.Equal(t, 2, res.incarnation) + require.Equal(t, valueFor(10, 2), res.value) +} + +func BenchmarkWriteTimeSameLocationDifferentTxIdx(b *testing.B) { + mvh2 := MakeMVHashMap() + ap2 := NewAddressKey(getCommonAddress(2)) + + randInts := []int{} + for i := 0; i < b.N; i++ { + randInts = append(randInts, rand.Intn(1000000000000000)) + } + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + mvh2.Write(ap2, Version{randInts[i], 1}, valueFor(randInts[i], 1)) + } +} + +func BenchmarkReadTimeSameLocationDifferentTxIdx(b *testing.B) { + mvh2 := MakeMVHashMap() + ap2 := NewAddressKey(getCommonAddress(2)) + txIdxSlice := []int{} + + for i := 0; i < b.N; i++ { + txIdx := rand.Intn(1000000000000000) + txIdxSlice = append(txIdxSlice, txIdx) + mvh2.Write(ap2, Version{txIdx, 1}, valueFor(txIdx, 1)) + } + + b.ResetTimer() + + for _, value := range txIdxSlice { + mvh2.Read(ap2, value) + } +} + +func TestTimeComplexity(t *testing.T) { + t.Parallel() + + // for 1000000 read and write with no dependency at different memory location + mvh1 := MakeMVHashMap() + + for i := 0; i < 1000000; i++ { + ap1 := NewAddressKey(getCommonAddress(i)) + mvh1.Write(ap1, Version{i, 1}, valueFor(i, 1)) + mvh1.Read(ap1, i) + } + + // for 1000000 read and write with dependency at same memory location + mvh2 := MakeMVHashMap() + ap2 := NewAddressKey(getCommonAddress(2)) + + for i := 0; i < 1000000; i++ { + mvh2.Write(ap2, Version{i, 1}, valueFor(i, 1)) + mvh2.Read(ap2, i) + } +} + +func TestWriteTimeSameLocationDifferentTxnIdx(t *testing.T) { + t.Parallel() + + mvh1 := MakeMVHashMap() + ap1 := NewAddressKey(getCommonAddress(1)) + + for i := 0; i < 1000000; i++ { + mvh1.Write(ap1, Version{i, 1}, valueFor(i, 1)) + } +} + +func TestWriteTimeSameLocationSameTxnIdx(t *testing.T) { + t.Parallel() + + mvh1 := MakeMVHashMap() + ap1 := NewAddressKey(getCommonAddress(1)) + + for i := 0; i < 1000000; i++ { + mvh1.Write(ap1, Version{1, i}, valueFor(i, 1)) + } +} + +func TestWriteTimeDifferentLocation(t *testing.T) { + t.Parallel() + + mvh1 := MakeMVHashMap() + + for i := 0; i < 1000000; i++ { + ap1 := NewAddressKey(getCommonAddress(i)) + mvh1.Write(ap1, Version{i, 1}, valueFor(i, 1)) + } +} + +func TestReadTimeSameLocation(t *testing.T) { + t.Parallel() + + mvh1 := MakeMVHashMap() + ap1 := NewAddressKey(getCommonAddress(1)) + + mvh1.Write(ap1, Version{1, 1}, valueFor(1, 1)) + + for i := 0; i < 1000000; i++ { + mvh1.Read(ap1, 2) + } +} diff --git a/core/blockstm/status_test.go b/core/blockstm/status_test.go new file mode 100644 index 0000000000..d76ebaba04 --- /dev/null +++ b/core/blockstm/status_test.go @@ -0,0 +1,84 @@ +package blockstm + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestStatusBasics(t *testing.T) { + t.Parallel() + + s := makeStatusManager(10) + + x := s.takeNextPending() + require.Equal(t, 0, x) + require.True(t, s.checkInProgress(x)) + + x = s.takeNextPending() + require.Equal(t, 1, x) + require.True(t, s.checkInProgress(x)) + + x = s.takeNextPending() + require.Equal(t, 2, x) + require.True(t, s.checkInProgress(x)) + + s.markComplete(0) + require.False(t, s.checkInProgress(0)) + s.markComplete(1) + s.markComplete(2) + require.False(t, s.checkInProgress(1)) + require.False(t, s.checkInProgress(2)) + require.Equal(t, 2, s.maxAllComplete()) + + x = s.takeNextPending() + require.Equal(t, 3, x) + + x = s.takeNextPending() + require.Equal(t, 4, x) + + s.markComplete(x) + require.False(t, s.checkInProgress(4)) + // PSP - is this correct? {s.maxAllComplete() -> 2} + // s -> {[5 6 7 8 9] [3] [0 1 2 4] map[] map[]} + require.Equal(t, 2, s.maxAllComplete(), "zero should still be min complete") + + exp := []int{1, 2} + require.Equal(t, exp, s.getRevalidationRange(1)) +} + +func TestMaxComplete(t *testing.T) { + t.Parallel() + + s := makeStatusManager(10) + + for { + tx := s.takeNextPending() + + if tx == -1 { + break + } + + if tx != 7 { + s.markComplete(tx) + } + } + + require.Equal(t, 6, s.maxAllComplete()) + + s2 := makeStatusManager(10) + + for { + tx := s2.takeNextPending() + + if tx == -1 { + break + } + } + s2.markComplete(2) + s2.markComplete(4) + require.Equal(t, -1, s2.maxAllComplete()) + + s2.complete = insertInList(s2.complete, 4) + require.Equal(t, 2, s2.countComplete()) +} diff --git a/go.mod b/go.mod index cf5a532d77..880ec8581f 100644 --- a/go.mod +++ b/go.mod @@ -85,6 +85,8 @@ require ( pgregory.net/rapid v0.4.8 ) +require github.com/orcaman/concurrent-map/v2 v2.0.0 // indirect + require ( github.com/Azure/azure-sdk-for-go/sdk/azcore v0.21.1 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v0.8.3 // indirect diff --git a/go.sum b/go.sum index 1bd56c0e36..de33a31248 100644 --- a/go.sum +++ b/go.sum @@ -405,6 +405,8 @@ github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFSt github.com/opentracing/opentracing-go v1.0.3-0.20180606204148-bd9c31933947/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/orcaman/concurrent-map/v2 v2.0.0 h1:iSMwuBQvQ1nX5i9gYuGMiSy0fjWHmazdjF+NdSO9JzI= +github.com/orcaman/concurrent-map/v2 v2.0.0/go.mod h1:9Eq3TG2oBe5FirmYWQfYO5iH1q0Jv47PLaNK++uCdOM= github.com/paulbellamy/ratecounter v0.2.0/go.mod h1:Hfx1hDpSGoqxkVVpBi/IlYD7kChlfo5C6hzIHwPqfFE= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= From 2060b4f61cc9465e4d6e45a1700c708d847a6355 Mon Sep 17 00:00:00 2001 From: Jerry Date: Thu, 6 Oct 2022 16:30:57 -0700 Subject: [PATCH 15/38] Add tx dependency to block header --- core/types/block.go | 12 ++++++ core/types/block_test.go | 45 ++++++++++++++++++++++ core/types/gen_header_json.go | 72 +++++++++++++++++++---------------- core/types/gen_header_rlp.go | 14 ++++++- 4 files changed, 109 insertions(+), 34 deletions(-) diff --git a/core/types/block.go b/core/types/block.go index 314990dc99..d4451497af 100644 --- a/core/types/block.go +++ b/core/types/block.go @@ -87,6 +87,8 @@ type Header struct { // BaseFee was added by EIP-1559 and is ignored in legacy headers. BaseFee *big.Int `json:"baseFeePerGas" rlp:"optional"` + TxDependency [][]uint64 `json:"txDependency" rlp:"optional"` + /* TODO (MariusVanDerWijden) Add this field once needed // Random was added during the merge and contains the BeaconState randomness @@ -252,6 +254,15 @@ func CopyHeader(h *Header) *Header { cpy.Extra = make([]byte, len(h.Extra)) copy(cpy.Extra, h.Extra) } + + if len(h.TxDependency) > 0 { + cpy.TxDependency = make([][]uint64, len(h.TxDependency)) + + for i, dep := range h.TxDependency { + cpy.TxDependency[i] = make([]uint64, len(dep)) + copy(cpy.TxDependency[i], dep) + } + } return &cpy } @@ -307,6 +318,7 @@ func (b *Block) TxHash() common.Hash { return b.header.TxHash } func (b *Block) ReceiptHash() common.Hash { return b.header.ReceiptHash } func (b *Block) UncleHash() common.Hash { return b.header.UncleHash } func (b *Block) Extra() []byte { return common.CopyBytes(b.header.Extra) } +func (b *Block) TxDependency() [][]uint64 { return b.header.TxDependency } func (b *Block) BaseFee() *big.Int { if b.header.BaseFee == nil { diff --git a/core/types/block_test.go b/core/types/block_test.go index aa1db2f4fa..dede213bf6 100644 --- a/core/types/block_test.go +++ b/core/types/block_test.go @@ -68,6 +68,51 @@ func TestBlockEncoding(t *testing.T) { } } +func TestTxDependencyBlockEncoding(t *testing.T) { + t.Parallel() + + blockEnc := common.FromHex("f90268f90201a083cafc574e1f51ba9dc0568fc617a08ea2429fb384059c972f13b19fa1c8dd55a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347948888f1f195afa192cfee860698584c030f4c9db1a0ef1552a40b7165c3cd773806b9e0c165b75356e0314bf0706f279c729f51e017a05fe50b260da6308036625b850b5d6ced6d0a9f814c0688bc91ffb7b7a3a54b67a0bc37d79753ad738a6dac4921e57392f145d8887476de3f783dfa7edae9283e52b90100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008302000001832fefd8825208845506eb0780a0bd4472abb6659ebe3ee06ee4d7b72a00a9f4d001caca51342001075469aff49888a13a5a8c8f2bb1c480c6c20201c20180f861f85f800a82c35094095e7baea6a6c7c4c2dfeb977efac326af552d870a801ba09bea4c4daac7c7c52e093e6a4c35dbbcf8856f1af7b059ba20253e70848d094fa08a8fae537ce25ed8cb5af9adac3f141af69bd515bd2ba031522df09b97dd72b1c0") + + var block Block + + if err := rlp.DecodeBytes(blockEnc, &block); err != nil { + t.Fatal("decode error: ", err) + } + + check := func(f string, got, want interface{}) { + if !reflect.DeepEqual(got, want) { + t.Errorf("%s mismatch: got %v, want %v", f, got, want) + } + } + + check("Difficulty", block.Difficulty(), big.NewInt(131072)) + check("GasLimit", block.GasLimit(), uint64(3141592)) + check("GasUsed", block.GasUsed(), uint64(21000)) + check("Coinbase", block.Coinbase(), common.HexToAddress("8888f1f195afa192cfee860698584c030f4c9db1")) + check("MixDigest", block.MixDigest(), common.HexToHash("bd4472abb6659ebe3ee06ee4d7b72a00a9f4d001caca51342001075469aff498")) + check("Root", block.Root(), common.HexToHash("ef1552a40b7165c3cd773806b9e0c165b75356e0314bf0706f279c729f51e017")) + check("Hash", block.Hash(), common.HexToHash("0xc6d8dc8995c0a4374bb9f87bd0dd8c0761e6e026a71edbfed5e961c9e55dbd6a")) + check("Nonce", block.Nonce(), uint64(0xa13a5a8c8f2bb1c4)) + check("Time", block.Time(), uint64(1426516743)) + check("Size", block.Size(), common.StorageSize(len(blockEnc))) + check("TxDependency", block.TxDependency(), [][]uint64{{2, 1}, {1, 0}}) + + tx1 := NewTransaction(0, common.HexToAddress("095e7baea6a6c7c4c2dfeb977efac326af552d87"), big.NewInt(10), 50000, big.NewInt(10), nil) + tx1, _ = tx1.WithSignature(HomesteadSigner{}, common.Hex2Bytes("9bea4c4daac7c7c52e093e6a4c35dbbcf8856f1af7b059ba20253e70848d094f8a8fae537ce25ed8cb5af9adac3f141af69bd515bd2ba031522df09b97dd72b100")) + + check("len(Transactions)", len(block.Transactions()), 1) + check("Transactions[0].Hash", block.Transactions()[0].Hash(), tx1.Hash()) + ourBlockEnc, err := rlp.EncodeToBytes(&block) + + if err != nil { + t.Fatal("encode error: ", err) + } + + if !bytes.Equal(ourBlockEnc, blockEnc) { + t.Errorf("encoded block mismatch:\ngot: %x\nwant: %x", ourBlockEnc, blockEnc) + } +} + func TestEIP1559BlockEncoding(t *testing.T) { blockEnc := common.FromHex("f9030bf901fea083cafc574e1f51ba9dc0568fc617a08ea2429fb384059c972f13b19fa1c8dd55a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347948888f1f195afa192cfee860698584c030f4c9db1a0ef1552a40b7165c3cd773806b9e0c165b75356e0314bf0706f279c729f51e017a05fe50b260da6308036625b850b5d6ced6d0a9f814c0688bc91ffb7b7a3a54b67a0bc37d79753ad738a6dac4921e57392f145d8887476de3f783dfa7edae9283e52b90100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008302000001832fefd8825208845506eb0780a0bd4472abb6659ebe3ee06ee4d7b72a00a9f4d001caca51342001075469aff49888a13a5a8c8f2bb1c4843b9aca00f90106f85f800a82c35094095e7baea6a6c7c4c2dfeb977efac326af552d870a801ba09bea4c4daac7c7c52e093e6a4c35dbbcf8856f1af7b059ba20253e70848d094fa08a8fae537ce25ed8cb5af9adac3f141af69bd515bd2ba031522df09b97dd72b1b8a302f8a0018080843b9aca008301e24194095e7baea6a6c7c4c2dfeb977efac326af552d878080f838f7940000000000000000000000000000000000000001e1a0000000000000000000000000000000000000000000000000000000000000000080a0fe38ca4e44a30002ac54af7cf922a6ac2ba11b7d22f548e8ecb3f51f41cb31b0a06de6a5cbae13c0c856e33acf021b51819636cfc009d39eafb9f606d546e305a8c0") var block Block diff --git a/core/types/gen_header_json.go b/core/types/gen_header_json.go index 75e24b34d6..10f7156749 100644 --- a/core/types/gen_header_json.go +++ b/core/types/gen_header_json.go @@ -16,23 +16,24 @@ var _ = (*headerMarshaling)(nil) // MarshalJSON marshals as JSON. func (h Header) MarshalJSON() ([]byte, error) { type Header struct { - ParentHash common.Hash `json:"parentHash" gencodec:"required"` - UncleHash common.Hash `json:"sha3Uncles" gencodec:"required"` - Coinbase common.Address `json:"miner" gencodec:"required"` - Root common.Hash `json:"stateRoot" gencodec:"required"` - TxHash common.Hash `json:"transactionsRoot" gencodec:"required"` - ReceiptHash common.Hash `json:"receiptsRoot" gencodec:"required"` - Bloom Bloom `json:"logsBloom" gencodec:"required"` - Difficulty *hexutil.Big `json:"difficulty" gencodec:"required"` - Number *hexutil.Big `json:"number" gencodec:"required"` - GasLimit hexutil.Uint64 `json:"gasLimit" gencodec:"required"` - GasUsed hexutil.Uint64 `json:"gasUsed" gencodec:"required"` - Time hexutil.Uint64 `json:"timestamp" gencodec:"required"` - Extra hexutil.Bytes `json:"extraData" gencodec:"required"` - MixDigest common.Hash `json:"mixHash"` - Nonce BlockNonce `json:"nonce"` - BaseFee *hexutil.Big `json:"baseFeePerGas" rlp:"optional"` - Hash common.Hash `json:"hash"` + ParentHash common.Hash `json:"parentHash" gencodec:"required"` + UncleHash common.Hash `json:"sha3Uncles" gencodec:"required"` + Coinbase common.Address `json:"miner" gencodec:"required"` + Root common.Hash `json:"stateRoot" gencodec:"required"` + TxHash common.Hash `json:"transactionsRoot" gencodec:"required"` + ReceiptHash common.Hash `json:"receiptsRoot" gencodec:"required"` + Bloom Bloom `json:"logsBloom" gencodec:"required"` + Difficulty *hexutil.Big `json:"difficulty" gencodec:"required"` + Number *hexutil.Big `json:"number" gencodec:"required"` + GasLimit hexutil.Uint64 `json:"gasLimit" gencodec:"required"` + GasUsed hexutil.Uint64 `json:"gasUsed" gencodec:"required"` + Time hexutil.Uint64 `json:"timestamp" gencodec:"required"` + Extra hexutil.Bytes `json:"extraData" gencodec:"required"` + MixDigest common.Hash `json:"mixHash"` + Nonce BlockNonce `json:"nonce"` + BaseFee *hexutil.Big `json:"baseFeePerGas" rlp:"optional"` + TxDependency [][]uint64 `json:"txDependency" rlp:"optional"` + Hash common.Hash `json:"hash"` } var enc Header enc.ParentHash = h.ParentHash @@ -51,6 +52,7 @@ func (h Header) MarshalJSON() ([]byte, error) { enc.MixDigest = h.MixDigest enc.Nonce = h.Nonce enc.BaseFee = (*hexutil.Big)(h.BaseFee) + enc.TxDependency = h.TxDependency enc.Hash = h.Hash() return json.Marshal(&enc) } @@ -58,22 +60,23 @@ func (h Header) MarshalJSON() ([]byte, error) { // UnmarshalJSON unmarshals from JSON. func (h *Header) UnmarshalJSON(input []byte) error { type Header struct { - ParentHash *common.Hash `json:"parentHash" gencodec:"required"` - UncleHash *common.Hash `json:"sha3Uncles" gencodec:"required"` - Coinbase *common.Address `json:"miner" gencodec:"required"` - Root *common.Hash `json:"stateRoot" gencodec:"required"` - TxHash *common.Hash `json:"transactionsRoot" gencodec:"required"` - ReceiptHash *common.Hash `json:"receiptsRoot" gencodec:"required"` - Bloom *Bloom `json:"logsBloom" gencodec:"required"` - Difficulty *hexutil.Big `json:"difficulty" gencodec:"required"` - Number *hexutil.Big `json:"number" gencodec:"required"` - GasLimit *hexutil.Uint64 `json:"gasLimit" gencodec:"required"` - GasUsed *hexutil.Uint64 `json:"gasUsed" gencodec:"required"` - Time *hexutil.Uint64 `json:"timestamp" gencodec:"required"` - Extra *hexutil.Bytes `json:"extraData" gencodec:"required"` - MixDigest *common.Hash `json:"mixHash"` - Nonce *BlockNonce `json:"nonce"` - BaseFee *hexutil.Big `json:"baseFeePerGas" rlp:"optional"` + ParentHash *common.Hash `json:"parentHash" gencodec:"required"` + UncleHash *common.Hash `json:"sha3Uncles" gencodec:"required"` + Coinbase *common.Address `json:"miner" gencodec:"required"` + Root *common.Hash `json:"stateRoot" gencodec:"required"` + TxHash *common.Hash `json:"transactionsRoot" gencodec:"required"` + ReceiptHash *common.Hash `json:"receiptsRoot" gencodec:"required"` + Bloom *Bloom `json:"logsBloom" gencodec:"required"` + Difficulty *hexutil.Big `json:"difficulty" gencodec:"required"` + Number *hexutil.Big `json:"number" gencodec:"required"` + GasLimit *hexutil.Uint64 `json:"gasLimit" gencodec:"required"` + GasUsed *hexutil.Uint64 `json:"gasUsed" gencodec:"required"` + Time *hexutil.Uint64 `json:"timestamp" gencodec:"required"` + Extra *hexutil.Bytes `json:"extraData" gencodec:"required"` + MixDigest *common.Hash `json:"mixHash"` + Nonce *BlockNonce `json:"nonce"` + BaseFee *hexutil.Big `json:"baseFeePerGas" rlp:"optional"` + TxDependency [][]uint64 `json:"txDependency" rlp:"optional"` } var dec Header if err := json.Unmarshal(input, &dec); err != nil { @@ -140,5 +143,8 @@ func (h *Header) UnmarshalJSON(input []byte) error { if dec.BaseFee != nil { h.BaseFee = (*big.Int)(dec.BaseFee) } + if dec.TxDependency != nil { + h.TxDependency = dec.TxDependency + } return nil } diff --git a/core/types/gen_header_rlp.go b/core/types/gen_header_rlp.go index e1a6873318..10377e2ad2 100644 --- a/core/types/gen_header_rlp.go +++ b/core/types/gen_header_rlp.go @@ -41,7 +41,8 @@ func (obj *Header) EncodeRLP(_w io.Writer) error { w.WriteBytes(obj.MixDigest[:]) w.WriteBytes(obj.Nonce[:]) _tmp1 := obj.BaseFee != nil - if _tmp1 { + _tmp2 := len(obj.TxDependency) > 0 + if _tmp1 || _tmp2 { if obj.BaseFee == nil { w.Write(rlp.EmptyString) } else { @@ -51,6 +52,17 @@ func (obj *Header) EncodeRLP(_w io.Writer) error { w.WriteBigInt(obj.BaseFee) } } + if _tmp2 { + _tmp3 := w.List() + for _, _tmp4 := range obj.TxDependency { + _tmp5 := w.List() + for _, _tmp6 := range _tmp4 { + w.WriteUint64(_tmp6) + } + w.ListEnd(_tmp5) + } + w.ListEnd(_tmp3) + } w.ListEnd(_tmp0) return w.Flush() } From da74f8c7e48150758f2b86cee55b5a3d1111f130 Mon Sep 17 00:00:00 2001 From: Jerry Date: Mon, 14 Nov 2022 15:22:57 -0800 Subject: [PATCH 16/38] Use a new hasher for each account access There seems to be an issue when hasher is used concurrently in parallel execution. This change will ensure no hasher is used by multiple executors at the same time. --- core/state/statedb.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/state/statedb.go b/core/state/statedb.go index d10b5fd564..4b74cb6153 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -867,7 +867,7 @@ func (s *StateDB) getDeletedStateObject(addr common.Address) *stateObject { var data *types.StateAccount if s.snap != nil { // nolint start := time.Now() - acc, err := s.snap.Account(crypto.HashData(s.hasher, addr.Bytes())) + acc, err := s.snap.Account(crypto.HashData(crypto.NewKeccakState(), addr.Bytes())) if metrics.EnabledExpensive { s.SnapshotAccountReads += time.Since(start) } From 3a87233a9cbf3b4eae8afcb8389ef42b4b04d5b5 Mon Sep 17 00:00:00 2001 From: Jerry Date: Mon, 14 Nov 2022 15:29:31 -0800 Subject: [PATCH 17/38] Change functions of Key from pointer to value reference --- core/blockstm/mvhashmap.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/core/blockstm/mvhashmap.go b/core/blockstm/mvhashmap.go index a04fbfd6f0..2a517bcc84 100644 --- a/core/blockstm/mvhashmap.go +++ b/core/blockstm/mvhashmap.go @@ -20,27 +20,27 @@ const KeyLength = common.AddressLength + common.HashLength + 2 type Key [KeyLength]byte -func (k *Key) IsAddress() bool { +func (k Key) IsAddress() bool { return k[KeyLength-1] == addressType } -func (k *Key) IsState() bool { +func (k Key) IsState() bool { return k[KeyLength-1] == stateType } -func (k *Key) IsSubpath() bool { +func (k Key) IsSubpath() bool { return k[KeyLength-1] == subpathType } -func (k *Key) GetAddress() common.Address { +func (k Key) GetAddress() common.Address { return common.BytesToAddress(k[:common.AddressLength]) } -func (k *Key) GetStateKey() common.Hash { +func (k Key) GetStateKey() common.Hash { return common.BytesToHash(k[common.AddressLength : KeyLength-2]) } -func (k *Key) GetSubpath() byte { +func (k Key) GetSubpath() byte { return k[KeyLength-2] } From d53c2e7902c17a7aa8cab1f95c0d77dfe37c1b2f Mon Sep 17 00:00:00 2001 From: Pratik Patil Date: Fri, 23 Dec 2022 09:14:09 +0530 Subject: [PATCH 18/38] Block stm miner dependency (#561) * added support for dependencies (executor_tests) * added a function to get dependency map * getting all dependencies in the GetDep function * updated GetDep function * changed the type of AllDeps * added a function to get dependency map * updated GetDep function * generate and get dependencies from block producer * optimized getDep function * bug fix regarding txn index and dep structure * fixed gas bug * optimized getDep function * tests updated/added * few updates regarding dependencies * added channel to calculate the dependencies in a separate go routine * minor changes in the executor which uses latest changes of dependecies + removed metadata flag/argument * Use channel when metadata is available * small bug fix * getting reads and writes only when the transaction succeeds * fixed bug in adding dependencies * updated logic for delay/not delay * bug fix (shouldDelayFeeCal) in parallel state processor * lint fix * using EnableMVHashMap flag * fixed worker and stateProcessor and removed SetMVHashMapNil fumction from stateDB * addredded few comments and fixed bug in executor tests * commented executor tests with metadata (panic: test timed out after 5m0s) * added a check to check len(mvReadMapList) > 0 in miner * addressed comments, minor refactoring in dag.go * moved blockContext out of Execute and adding it in execution task * removed Author() from Settle() and added in execution task * not calling block.Header() again and again, using instead * removed EnableMVHashMap flag, and updated applyTransaction function * addressed comments * added unit test to check dependencies in the block header Co-authored-by: Jerry --- core/blockstm/dag.go | 54 ++++ core/blockstm/executor.go | 105 ++++++-- core/blockstm/executor_test.go | 415 +++++++++++++++++++++++++++++-- core/blockstm/txio.go | 12 + core/parallel_state_processor.go | 140 ++++++++--- core/state/statedb.go | 12 + core/state_processor.go | 43 +++- core/types/block.go | 5 + miner/worker.go | 114 ++++++++- miner/worker_test.go | 112 +++++++++ 10 files changed, 928 insertions(+), 84 deletions(-) diff --git a/core/blockstm/dag.go b/core/blockstm/dag.go index d122877fe9..47bd0685a3 100644 --- a/core/blockstm/dag.go +++ b/core/blockstm/dag.go @@ -14,6 +14,12 @@ type DAG struct { *dag.DAG } +type TxDep struct { + Index int + ReadList []ReadDescriptor + FullWriteList [][]WriteDescriptor +} + func HasReadDep(txFrom TxnOutput, txTo TxnInput) bool { reads := make(map[Key]bool) @@ -69,6 +75,54 @@ func BuildDAG(deps TxnInputOutput) (d DAG) { return } +func depsHelper(dependencies map[int]map[int]bool, txFrom TxnOutput, txTo TxnInput, i int, j int) map[int]map[int]bool { + if HasReadDep(txFrom, txTo) { + dependencies[i][j] = true + + for k := range dependencies[i] { + _, foundDep := dependencies[j][k] + + if foundDep { + delete(dependencies[i], k) + } + } + } + + return dependencies +} + +func UpdateDeps(deps map[int]map[int]bool, t TxDep) map[int]map[int]bool { + txTo := t.ReadList + + deps[t.Index] = map[int]bool{} + + for j := 0; j <= t.Index-1; j++ { + txFrom := t.FullWriteList[j] + + deps = depsHelper(deps, txFrom, txTo, t.Index, j) + } + + return deps +} + +func GetDep(deps TxnInputOutput) map[int]map[int]bool { + newDependencies := map[int]map[int]bool{} + + for i := 1; i < len(deps.inputs); i++ { + txTo := deps.inputs[i] + + newDependencies[i] = map[int]bool{} + + for j := 0; j <= i-1; j++ { + txFrom := deps.allOutputs[j] + + newDependencies = depsHelper(newDependencies, txFrom, txTo, i, j) + } + } + + return newDependencies +} + // Find the longest execution path in the DAG func (d DAG) LongestPath(stats map[int]ExecutionStat) ([]int, uint64) { prev := make(map[int]int, len(d.GetVertices())) diff --git a/core/blockstm/executor.go b/core/blockstm/executor.go index 18fd81c1c6..a086347610 100644 --- a/core/blockstm/executor.go +++ b/core/blockstm/executor.go @@ -26,6 +26,7 @@ type ExecTask interface { Hash() common.Hash Sender() common.Address Settle() + Dependencies() []int } type ExecVersionView struct { @@ -82,6 +83,34 @@ func (h *IntHeap) Pop() any { return x } +type SafeQueue interface { + Push(v int, d interface{}) + Pop() interface{} + Len() int +} + +type SafeFIFOQueue struct { + c chan interface{} +} + +func NewSafeFIFOQueue(capacity int) *SafeFIFOQueue { + return &SafeFIFOQueue{ + c: make(chan interface{}, capacity), + } +} + +func (q *SafeFIFOQueue) Push(v int, d interface{}) { + q.c <- d +} + +func (q *SafeFIFOQueue) Pop() interface{} { + return <-q.c +} + +func (q *SafeFIFOQueue) Len() int { + return len(q.c) +} + // A thread safe priority queue type SafePriorityQueue struct { m sync.Mutex @@ -122,9 +151,10 @@ func (pq *SafePriorityQueue) Len() int { } type ParallelExecutionResult struct { - TxIO *TxnInputOutput - Stats *map[int]ExecutionStat - Deps *DAG + TxIO *TxnInputOutput + Stats *map[int]ExecutionStat + Deps *DAG + AllDeps map[int]map[int]bool } const numGoProcs = 2 @@ -145,7 +175,7 @@ type ParallelExecutor struct { chSpeculativeTasks chan struct{} // Channel to signal that the result of a transaction could be written to storage - specTaskQueue *SafePriorityQueue + specTaskQueue SafeQueue // A priority queue that stores speculative tasks chSettle chan int @@ -154,7 +184,7 @@ type ParallelExecutor struct { chResults chan struct{} // A priority queue that stores the transaction index of results, so we can validate the results in order - resultQueue *SafePriorityQueue + resultQueue SafeQueue // A wait group to wait for all settling tasks to finish settleWg sync.WaitGroup @@ -211,9 +241,21 @@ type ExecutionStat struct { Worker int } -func NewParallelExecutor(tasks []ExecTask, profile bool) *ParallelExecutor { +func NewParallelExecutor(tasks []ExecTask, profile bool, metadata bool) *ParallelExecutor { numTasks := len(tasks) + var resultQueue SafeQueue + + var specTaskQueue SafeQueue + + if metadata { + resultQueue = NewSafeFIFOQueue(numTasks) + specTaskQueue = NewSafeFIFOQueue(numTasks) + } else { + resultQueue = NewSafePriorityQueue(numTasks) + specTaskQueue = NewSafePriorityQueue(numTasks) + } + pe := &ParallelExecutor{ tasks: tasks, stats: make(map[int]ExecutionStat, numTasks), @@ -221,8 +263,8 @@ func NewParallelExecutor(tasks []ExecTask, profile bool) *ParallelExecutor { chSpeculativeTasks: make(chan struct{}, numTasks), chSettle: make(chan int, numTasks), chResults: make(chan struct{}, numTasks), - specTaskQueue: NewSafePriorityQueue(numTasks), - resultQueue: NewSafePriorityQueue(numTasks), + specTaskQueue: specTaskQueue, + resultQueue: resultQueue, lastSettled: -1, skipCheck: make(map[int]bool), execTasks: makeStatusManager(numTasks), @@ -241,19 +283,36 @@ func NewParallelExecutor(tasks []ExecTask, profile bool) *ParallelExecutor { return pe } +// nolint: gocognit func (pe *ParallelExecutor) Prepare() { - prevSenderTx := make(map[common.Address]int) - for i, t := range pe.tasks { + clearPendingFlag := false + pe.skipCheck[i] = false pe.estimateDeps[i] = make([]int, 0) - if tx, ok := prevSenderTx[t.Sender()]; ok { - pe.execTasks.addDependencies(tx, i) - pe.execTasks.clearPending(i) - } + if len(t.Dependencies()) > 0 { + for _, val := range t.Dependencies() { + clearPendingFlag = true - prevSenderTx[t.Sender()] = i + pe.execTasks.addDependencies(val, i) + } + + if clearPendingFlag { + pe.execTasks.clearPending(i) + + clearPendingFlag = false + } + } else { + prevSenderTx := make(map[common.Address]int) + + if tx, ok := prevSenderTx[t.Sender()]; ok { + pe.execTasks.addDependencies(tx, i) + pe.execTasks.clearPending(i) + } + + prevSenderTx[t.Sender()] = i + } } pe.workerWg.Add(numSpeculativeProcs + numGoProcs) @@ -478,13 +537,13 @@ func (pe *ParallelExecutor) Step(res *ExecResult) (result ParallelExecutionResul pe.Close(true) - var dag DAG + var allDeps map[int]map[int]bool if pe.profile { - dag = BuildDAG(*pe.lastTxIO) + allDeps = GetDep(*pe.lastTxIO) } - return ParallelExecutionResult{pe.lastTxIO, &pe.stats, &dag}, err + return ParallelExecutionResult{pe.lastTxIO, &pe.stats, nil, allDeps}, err } // Send the next immediate pending transaction to be executed @@ -518,12 +577,12 @@ func (pe *ParallelExecutor) Step(res *ExecResult) (result ParallelExecutionResul type PropertyCheck func(*ParallelExecutor) error -func executeParallelWithCheck(tasks []ExecTask, profile bool, check PropertyCheck) (result ParallelExecutionResult, err error) { +func executeParallelWithCheck(tasks []ExecTask, profile bool, check PropertyCheck, metadata bool) (result ParallelExecutionResult, err error) { if len(tasks) == 0 { - return ParallelExecutionResult{MakeTxnInputOutput(len(tasks)), nil, nil}, nil + return ParallelExecutionResult{MakeTxnInputOutput(len(tasks)), nil, nil, nil}, nil } - pe := NewParallelExecutor(tasks, profile) + pe := NewParallelExecutor(tasks, profile, metadata) pe.Prepare() for range pe.chResults { @@ -547,6 +606,6 @@ func executeParallelWithCheck(tasks []ExecTask, profile bool, check PropertyChec return } -func ExecuteParallel(tasks []ExecTask, profile bool) (result ParallelExecutionResult, err error) { - return executeParallelWithCheck(tasks, profile, nil) +func ExecuteParallel(tasks []ExecTask, profile bool, metadata bool) (result ParallelExecutionResult, err error) { + return executeParallelWithCheck(tasks, profile, nil, metadata) } diff --git a/core/blockstm/executor_test.go b/core/blockstm/executor_test.go index c62e0ae9a4..686855b36a 100644 --- a/core/blockstm/executor_test.go +++ b/core/blockstm/executor_test.go @@ -19,6 +19,10 @@ type OpType int const readType = 0 const writeType = 1 const otherType = 2 +const greenTick = "✅" +const redCross = "❌" + +const threeRockets = "🚀🚀🚀" type Op struct { key Key @@ -28,30 +32,34 @@ type Op struct { } type testExecTask struct { - txIdx int - ops []Op - readMap map[Key]ReadDescriptor - writeMap map[Key]WriteDescriptor - sender common.Address - nonce int + txIdx int + ops []Op + readMap map[Key]ReadDescriptor + writeMap map[Key]WriteDescriptor + sender common.Address + nonce int + dependencies []int } type PathGenerator func(addr common.Address, i int, j int, total int) Key type TaskRunner func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration) +type TaskRunnerWithMetadata func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration, time.Duration) + type Timer func(txIdx int, opIdx int) time.Duration type Sender func(int) common.Address func NewTestExecTask(txIdx int, ops []Op, sender common.Address, nonce int) *testExecTask { return &testExecTask{ - txIdx: txIdx, - ops: ops, - readMap: make(map[Key]ReadDescriptor), - writeMap: make(map[Key]WriteDescriptor), - sender: sender, - nonce: nonce, + txIdx: txIdx, + ops: ops, + readMap: make(map[Key]ReadDescriptor), + writeMap: make(map[Key]WriteDescriptor), + sender: sender, + nonce: nonce, + dependencies: []int{}, } } @@ -157,6 +165,10 @@ func (t *testExecTask) Hash() common.Hash { return common.BytesToHash([]byte(fmt.Sprintf("%d", t.txIdx))) } +func (t *testExecTask) Dependencies() []int { + return t.dependencies +} + func randTimeGenerator(min time.Duration, max time.Duration) func(txIdx int, opIdx int) time.Duration { return func(txIdx int, opIdx int) time.Duration { return time.Duration(rand.Int63n(int64(max-min))) + min @@ -275,10 +287,10 @@ func testExecutorComb(t *testing.T, totalTxs []int, numReads []int, numWrites [] } total++ - performance := "✅" + performance := greenTick if execDuration >= expectedSerialDuration { - performance = "❌" + performance = redCross } fmt.Printf("exec duration %v, serial duration %v, time reduced %v %.2f%%, %v \n", execDuration, expectedSerialDuration, expectedSerialDuration-execDuration, float64(expectedSerialDuration-execDuration)/float64(expectedSerialDuration)*100, performance) @@ -294,6 +306,66 @@ func testExecutorComb(t *testing.T, totalTxs []int, numReads []int, numWrites [] fmt.Printf("Total exec duration: %v, total serial duration: %v, time reduced: %v, time reduced percent: %.2f%%\n", totalExecDuration, totalSerialDuration, totalSerialDuration-totalExecDuration, float64(totalSerialDuration-totalExecDuration)/float64(totalSerialDuration)*100) } +// nolint: gocognit +func testExecutorCombWithMetadata(t *testing.T, totalTxs []int, numReads []int, numWrites []int, numNonIOs []int, taskRunner TaskRunnerWithMetadata) { + t.Helper() + log.Root().SetHandler(log.LvlFilterHandler(log.LvlDebug, log.StreamHandler(os.Stderr, log.TerminalFormat(false)))) + + improved := 0 + improvedMetadata := 0 + rocket := 0 + total := 0 + + totalExecDuration := time.Duration(0) + totalExecDurationMetadata := time.Duration(0) + totalSerialDuration := time.Duration(0) + + for _, numTx := range totalTxs { + for _, numRead := range numReads { + for _, numWrite := range numWrites { + for _, numNonIO := range numNonIOs { + log.Info("Executing block", "numTx", numTx, "numRead", numRead, "numWrite", numWrite, "numNonIO", numNonIO) + execDuration, execDurationMetadata, expectedSerialDuration := taskRunner(numTx, numRead, numWrite, numNonIO) + + if execDuration < expectedSerialDuration { + improved++ + } + total++ + + performance := greenTick + + if execDuration >= expectedSerialDuration { + performance = redCross + + if execDurationMetadata <= expectedSerialDuration { + performance = threeRockets + rocket++ + } + } + + if execDuration >= execDurationMetadata { + improvedMetadata++ + } + + fmt.Printf("WITHOUT METADATA: exec duration %v, serial duration %v, time reduced %v %.2f%%, %v \n", execDuration, expectedSerialDuration, expectedSerialDuration-execDuration, float64(expectedSerialDuration-execDuration)/float64(expectedSerialDuration)*100, performance) + fmt.Printf("WITH METADATA: exec duration %v, exec duration with metadata %v, time reduced %v %.2f%%\n", execDuration, execDurationMetadata, execDuration-execDurationMetadata, float64(execDuration-execDurationMetadata)/float64(execDuration)*100) + + totalExecDuration += execDuration + totalExecDurationMetadata += execDurationMetadata + totalSerialDuration += expectedSerialDuration + } + } + } + } + + fmt.Println("\nImproved: ", improved, "Total: ", total, "success rate: ", float64(improved)/float64(total)*100) + fmt.Println("Metadata Better: ", improvedMetadata, "out of: ", total, "success rate: ", float64(improvedMetadata)/float64(total)*100) + fmt.Println("Rockets (Time of: metadata < serial < without metadata): ", rocket) + fmt.Printf("\nWithout metadata <> serial: Total exec duration: %v, total serial duration : %v, time reduced: %v, time reduced percent: %.2f%%\n", totalExecDuration, totalSerialDuration, totalSerialDuration-totalExecDuration, float64(totalSerialDuration-totalExecDuration)/float64(totalSerialDuration)*100) + fmt.Printf("With metadata <> serial: Total exec duration metadata: %v, total serial duration : %v, time reduced: %v, time reduced percent: %.2f%%\n", totalExecDurationMetadata, totalSerialDuration, totalSerialDuration-totalExecDurationMetadata, float64(totalSerialDuration-totalExecDurationMetadata)/float64(totalSerialDuration)*100) + fmt.Printf("Without metadata <> with metadata: Total exec duration: %v, total exec duration metadata: %v, time reduced: %v, time reduced percent: %.2f%%\n", totalExecDuration, totalExecDurationMetadata, totalExecDuration-totalExecDurationMetadata, float64(totalExecDuration-totalExecDurationMetadata)/float64(totalExecDuration)*100) +} + func composeValidations(checks []PropertyCheck) PropertyCheck { return func(pe *ParallelExecutor) error { for _, check := range checks { @@ -345,13 +417,14 @@ func checkNoDroppedTx(pe *ParallelExecutor) error { return nil } -func runParallel(t *testing.T, tasks []ExecTask, validation PropertyCheck) time.Duration { +// nolint: unparam +func runParallel(t *testing.T, tasks []ExecTask, validation PropertyCheck, metadata bool) time.Duration { t.Helper() profile := false start := time.Now() - result, err := executeParallelWithCheck(tasks, profile, validation) + result, err := executeParallelWithCheck(tasks, false, validation, metadata) if result.Deps != nil && profile { result.Deps.Report(*result.Stats, func(str string) { fmt.Println(str) }) @@ -381,6 +454,17 @@ func runParallel(t *testing.T, tasks []ExecTask, validation PropertyCheck) time. return duration } +func runParallelGetMetadata(t *testing.T, tasks []ExecTask, validation PropertyCheck) map[int]map[int]bool { + t.Helper() + + // fmt.Println("len(tasks)", len(tasks)) + res, err := executeParallelWithCheck(tasks, true, validation, false) + + assert.NoError(t, err, "error occur during parallel execution") + + return res.AllDeps +} + func TestLessConflicts(t *testing.T) { t.Parallel() rand.Seed(0) @@ -399,12 +483,58 @@ func TestLessConflicts(t *testing.T) { } tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, readTime, writeTime, nonIOTime) - return runParallel(t, tasks, checks), serialDuration + return runParallel(t, tasks, checks, false), serialDuration } testExecutorComb(t, totalTxs, numReads, numWrites, numNonIO, taskRunner) } +func TestLessConflictsWithMetadata(t *testing.T) { + t.Parallel() + rand.Seed(0) + + totalTxs := []int{300} + numReads := []int{100, 200} + numWrites := []int{100, 200} + numNonIOs := []int{100, 500} + + checks := composeValidations([]PropertyCheck{checkNoStatusOverlap, checkNoDroppedTx}) + + taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration, time.Duration) { + sender := func(i int) common.Address { + randomness := rand.Intn(10) + 10 + return common.BigToAddress(big.NewInt(int64(i % randomness))) + } + tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, readTime, writeTime, nonIOTime) + + parallelDuration := runParallel(t, tasks, checks, false) + + allDeps := runParallelGetMetadata(t, tasks, checks) + + newTasks := make([]ExecTask, 0, len(tasks)) + + for _, t := range tasks { + temp := t.(*testExecTask) + + keys := make([]int, len(allDeps[temp.txIdx])) + + i := 0 + + for k := range allDeps[temp.txIdx] { + keys[i] = k + i++ + } + + temp.dependencies = keys + newTasks = append(newTasks, temp) + } + + return parallelDuration, runParallel(t, newTasks, checks, true), serialDuration + } + + testExecutorCombWithMetadata(t, totalTxs, numReads, numWrites, numNonIOs, taskRunner) +} + func TestZeroTx(t *testing.T) { t.Parallel() rand.Seed(0) @@ -420,7 +550,7 @@ func TestZeroTx(t *testing.T) { sender := func(i int) common.Address { return common.BigToAddress(big.NewInt(int64(1))) } tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, readTime, writeTime, nonIOTime) - return runParallel(t, tasks, checks), serialDuration + return runParallel(t, tasks, checks, false), serialDuration } testExecutorComb(t, totalTxs, numReads, numWrites, numNonIO, taskRunner) @@ -441,12 +571,55 @@ func TestAlternatingTx(t *testing.T) { sender := func(i int) common.Address { return common.BigToAddress(big.NewInt(int64(i % 2))) } tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, readTime, writeTime, nonIOTime) - return runParallel(t, tasks, checks), serialDuration + return runParallel(t, tasks, checks, false), serialDuration } testExecutorComb(t, totalTxs, numReads, numWrites, numNonIO, taskRunner) } +func TestAlternatingTxWithMetadata(t *testing.T) { + t.Parallel() + rand.Seed(0) + + totalTxs := []int{200} + numReads := []int{20} + numWrites := []int{20} + numNonIO := []int{100} + + checks := composeValidations([]PropertyCheck{checkNoStatusOverlap, checkNoDroppedTx}) + + taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration, time.Duration) { + sender := func(i int) common.Address { return common.BigToAddress(big.NewInt(int64(i % 2))) } + tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, readTime, writeTime, nonIOTime) + + parallelDuration := runParallel(t, tasks, checks, false) + + allDeps := runParallelGetMetadata(t, tasks, checks) + + newTasks := make([]ExecTask, 0, len(tasks)) + + for _, t := range tasks { + temp := t.(*testExecTask) + + keys := make([]int, len(allDeps[temp.txIdx])) + + i := 0 + + for k := range allDeps[temp.txIdx] { + keys[i] = k + i++ + } + + temp.dependencies = keys + newTasks = append(newTasks, temp) + } + + return parallelDuration, runParallel(t, newTasks, checks, true), serialDuration + } + + testExecutorCombWithMetadata(t, totalTxs, numReads, numWrites, numNonIO, taskRunner) +} + func TestMoreConflicts(t *testing.T) { t.Parallel() rand.Seed(0) @@ -465,12 +638,58 @@ func TestMoreConflicts(t *testing.T) { } tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, readTime, writeTime, nonIOTime) - return runParallel(t, tasks, checks), serialDuration + return runParallel(t, tasks, checks, false), serialDuration } testExecutorComb(t, totalTxs, numReads, numWrites, numNonIO, taskRunner) } +func TestMoreConflictsWithMetadata(t *testing.T) { + t.Parallel() + rand.Seed(0) + + totalTxs := []int{300} + numReads := []int{100, 200} + numWrites := []int{100, 200} + numNonIO := []int{100, 500} + + checks := composeValidations([]PropertyCheck{checkNoStatusOverlap, checkNoDroppedTx}) + + taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration, time.Duration) { + sender := func(i int) common.Address { + randomness := rand.Intn(10) + 10 + return common.BigToAddress(big.NewInt(int64(i / randomness))) + } + tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, readTime, writeTime, nonIOTime) + + parallelDuration := runParallel(t, tasks, checks, false) + + allDeps := runParallelGetMetadata(t, tasks, checks) + + newTasks := make([]ExecTask, 0, len(tasks)) + + for _, t := range tasks { + temp := t.(*testExecTask) + + keys := make([]int, len(allDeps[temp.txIdx])) + + i := 0 + + for k := range allDeps[temp.txIdx] { + keys[i] = k + i++ + } + + temp.dependencies = keys + newTasks = append(newTasks, temp) + } + + return parallelDuration, runParallel(t, newTasks, checks, true), serialDuration + } + + testExecutorCombWithMetadata(t, totalTxs, numReads, numWrites, numNonIO, taskRunner) +} + func TestRandomTx(t *testing.T) { t.Parallel() rand.Seed(0) @@ -487,12 +706,56 @@ func TestRandomTx(t *testing.T) { sender := func(i int) common.Address { return common.BigToAddress(big.NewInt(int64(rand.Intn(10)))) } tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, readTime, writeTime, nonIOTime) - return runParallel(t, tasks, checks), serialDuration + return runParallel(t, tasks, checks, false), serialDuration } testExecutorComb(t, totalTxs, numReads, numWrites, numNonIO, taskRunner) } +func TestRandomTxWithMetadata(t *testing.T) { + t.Parallel() + rand.Seed(0) + + totalTxs := []int{300} + numReads := []int{100, 200} + numWrites := []int{100, 200} + numNonIO := []int{100, 500} + + checks := composeValidations([]PropertyCheck{checkNoStatusOverlap, checkNoDroppedTx}) + + taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration, time.Duration) { + // Randomly assign this tx to one of 10 senders + sender := func(i int) common.Address { return common.BigToAddress(big.NewInt(int64(rand.Intn(10)))) } + tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, readTime, writeTime, nonIOTime) + + parallelDuration := runParallel(t, tasks, checks, false) + + allDeps := runParallelGetMetadata(t, tasks, checks) + + newTasks := make([]ExecTask, 0, len(tasks)) + + for _, t := range tasks { + temp := t.(*testExecTask) + + keys := make([]int, len(allDeps[temp.txIdx])) + + i := 0 + + for k := range allDeps[temp.txIdx] { + keys[i] = k + i++ + } + + temp.dependencies = keys + newTasks = append(newTasks, temp) + } + + return parallelDuration, runParallel(t, newTasks, checks, true), serialDuration + } + + testExecutorCombWithMetadata(t, totalTxs, numReads, numWrites, numNonIO, taskRunner) +} + func TestTxWithLongTailRead(t *testing.T) { t.Parallel() rand.Seed(0) @@ -514,12 +777,61 @@ func TestTxWithLongTailRead(t *testing.T) { tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, longTailReadTimer, writeTime, nonIOTime) - return runParallel(t, tasks, checks), serialDuration + return runParallel(t, tasks, checks, false), serialDuration } testExecutorComb(t, totalTxs, numReads, numWrites, numNonIO, taskRunner) } +func TestTxWithLongTailReadWithMetadata(t *testing.T) { + t.Parallel() + rand.Seed(0) + + totalTxs := []int{300} + numReads := []int{100, 200} + numWrites := []int{100, 200} + numNonIO := []int{100, 500} + + checks := composeValidations([]PropertyCheck{checkNoStatusOverlap, checkNoDroppedTx}) + + taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration, time.Duration) { + sender := func(i int) common.Address { + randomness := rand.Intn(10) + 10 + return common.BigToAddress(big.NewInt(int64(i / randomness))) + } + + longTailReadTimer := longTailTimeGenerator(4*time.Microsecond, 12*time.Microsecond, 7, 10) + + tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, longTailReadTimer, writeTime, nonIOTime) + + parallelDuration := runParallel(t, tasks, checks, false) + + allDeps := runParallelGetMetadata(t, tasks, checks) + + newTasks := make([]ExecTask, 0, len(tasks)) + + for _, t := range tasks { + temp := t.(*testExecTask) + + keys := make([]int, len(allDeps[temp.txIdx])) + + i := 0 + + for k := range allDeps[temp.txIdx] { + keys[i] = k + i++ + } + + temp.dependencies = keys + newTasks = append(newTasks, temp) + } + + return parallelDuration, runParallel(t, newTasks, checks, true), serialDuration + } + + testExecutorCombWithMetadata(t, totalTxs, numReads, numWrites, numNonIO, taskRunner) +} + func TestDexScenario(t *testing.T) { t.Parallel() rand.Seed(0) @@ -549,8 +861,65 @@ func TestDexScenario(t *testing.T) { sender := func(i int) common.Address { return common.BigToAddress(big.NewInt(int64(i))) } tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, dexPathGenerator, readTime, writeTime, nonIOTime) - return runParallel(t, tasks, checks), serialDuration + return runParallel(t, tasks, checks, false), serialDuration } testExecutorComb(t, totalTxs, numReads, numWrites, numNonIO, taskRunner) } + +func TestDexScenarioWithMetadata(t *testing.T) { + t.Parallel() + rand.Seed(0) + + totalTxs := []int{300} + numReads := []int{100, 200} + numWrites := []int{100, 200} + numNonIO := []int{100, 500} + + postValidation := func(pe *ParallelExecutor) error { + if pe.lastSettled == len(pe.tasks) { + for i, inputs := range pe.lastTxIO.inputs { + for _, input := range inputs { + if input.V.TxnIndex != i-1 { + return fmt.Errorf("Tx %d should depend on tx %d, but it actually depends on %d", i, i-1, input.V.TxnIndex) + } + } + } + } + + return nil + } + + checks := composeValidations([]PropertyCheck{checkNoStatusOverlap, postValidation, checkNoDroppedTx}) + + taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration, time.Duration) { + sender := func(i int) common.Address { return common.BigToAddress(big.NewInt(int64(i))) } + tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, dexPathGenerator, readTime, writeTime, nonIOTime) + + parallelDuration := runParallel(t, tasks, checks, false) + + allDeps := runParallelGetMetadata(t, tasks, checks) + + newTasks := make([]ExecTask, 0, len(tasks)) + + for _, t := range tasks { + temp := t.(*testExecTask) + + keys := make([]int, len(allDeps[temp.txIdx])) + + i := 0 + + for k := range allDeps[temp.txIdx] { + keys[i] = k + i++ + } + + temp.dependencies = keys + newTasks = append(newTasks, temp) + } + + return parallelDuration, runParallel(t, newTasks, checks, true), serialDuration + } + + testExecutorCombWithMetadata(t, totalTxs, numReads, numWrites, numNonIO, taskRunner) +} diff --git a/core/blockstm/txio.go b/core/blockstm/txio.go index a08cf57d22..22541e0a78 100644 --- a/core/blockstm/txio.go +++ b/core/blockstm/txio.go @@ -80,3 +80,15 @@ func (io *TxnInputOutput) recordWrite(txId int, output []WriteDescriptor) { func (io *TxnInputOutput) recordAllWrite(txId int, output []WriteDescriptor) { io.allOutputs[txId] = output } + +func (io *TxnInputOutput) RecordReadAtOnce(inputs [][]ReadDescriptor) { + for ind, val := range inputs { + io.inputs[ind] = val + } +} + +func (io *TxnInputOutput) RecordAllWriteAtOnce(outputs [][]WriteDescriptor) { + for ind, val := range outputs { + io.allOutputs[ind] = val + } +} diff --git a/core/parallel_state_processor.go b/core/parallel_state_processor.go index c936f399cd..23457d5c60 100644 --- a/core/parallel_state_processor.go +++ b/core/parallel_state_processor.go @@ -75,6 +75,15 @@ type ExecutionTask struct { totalUsedGas *uint64 receipts *types.Receipts allLogs *[]*types.Log + + // length of dependencies -> 2 + k (k = a whole number) + // first 2 element in dependencies -> transaction index, and flag representing if delay is allowed or not + // (0 -> delay is not allowed, 1 -> delay is allowed) + // next k elements in dependencies -> transaction indexes on which transaction i is dependent on + dependencies []int + + blockContext vm.BlockContext + coinbase common.Address } func (task *ExecutionTask) Execute(mvh *blockstm.MVHashMap, incarnation int) (err error) { @@ -83,9 +92,7 @@ func (task *ExecutionTask) Execute(mvh *blockstm.MVHashMap, incarnation int) (er task.statedb.SetMVHashmap(mvh) task.statedb.SetIncarnation(incarnation) - blockContext := NewEVMBlockContext(task.header, task.blockChain, nil) - - evm := vm.NewEVM(blockContext, vm.TxContext{}, task.statedb, task.config, task.evmConfig) + evm := vm.NewEVM(task.blockContext, vm.TxContext{}, task.statedb, task.config, task.evmConfig) // Create a new context to be used in the EVM environment. txContext := NewEVMTxContext(task.msg) @@ -112,8 +119,8 @@ func (task *ExecutionTask) Execute(mvh *blockstm.MVHashMap, incarnation int) (er reads := task.statedb.MVReadMap() - if _, ok := reads[blockstm.NewSubpathKey(blockContext.Coinbase, state.BalancePath)]; ok { - log.Info("Coinbase is in MVReadMap", "address", blockContext.Coinbase) + if _, ok := reads[blockstm.NewSubpathKey(task.blockContext.Coinbase, state.BalancePath)]; ok { + log.Info("Coinbase is in MVReadMap", "address", task.blockContext.Coinbase) task.shouldRerunWithoutFeeDelay = true } @@ -157,6 +164,10 @@ func (task *ExecutionTask) Hash() common.Hash { return task.tx.Hash() } +func (task *ExecutionTask) Dependencies() []int { + return task.dependencies +} + func (task *ExecutionTask) Settle() { defer func() { if r := recover(); r != nil { @@ -171,9 +182,7 @@ func (task *ExecutionTask) Settle() { task.finalStateDB.Prepare(task.tx.Hash(), task.index) - coinbase, _ := task.blockChain.Engine().Author(task.header) - - coinbaseBalance := task.finalStateDB.GetBalance(coinbase) + coinbaseBalance := task.finalStateDB.GetBalance(task.coinbase) task.finalStateDB.ApplyMVWriteSet(task.statedb.MVWriteList()) @@ -186,7 +195,7 @@ func (task *ExecutionTask) Settle() { task.finalStateDB.AddBalance(task.result.BurntContractAddress, task.result.FeeBurnt) } - task.finalStateDB.AddBalance(coinbase, task.result.FeeTipped) + task.finalStateDB.AddBalance(task.coinbase, task.result.FeeTipped) output1 := new(big.Int).SetBytes(task.result.SenderInitBalance.Bytes()) output2 := new(big.Int).SetBytes(coinbaseBalance.Bytes()) @@ -196,7 +205,7 @@ func (task *ExecutionTask) Settle() { task.finalStateDB, task.msg.From(), - coinbase, + task.coinbase, task.result.FeeTipped, task.result.SenderInitBalance, @@ -267,6 +276,7 @@ func (p *ParallelStateProcessor) Process(block *types.Block, statedb *state.Stat blockNumber = block.Number() allLogs []*types.Log usedGas = new(uint64) + metadata bool ) // Mutate the block and state according to any hard-fork specs @@ -280,6 +290,14 @@ func (p *ParallelStateProcessor) Process(block *types.Block, statedb *state.Stat coinbase, _ := p.bc.Engine().Author(header) + deps, delayMap := GetDeps(block.Header().TxDependency) + + if block.Header().TxDependency != nil { + metadata = true + } + + blockContext := NewEVMBlockContext(header, p.bc, nil) + // Iterate over and process the individual transactions for i, tx := range block.Transactions() { msg, err := tx.AsMessage(types.MakeSigner(p.config, header.Number), header.BaseFee) @@ -290,37 +308,69 @@ func (p *ParallelStateProcessor) Process(block *types.Block, statedb *state.Stat cleansdb := statedb.Copy() - if msg.From() == coinbase { - shouldDelayFeeCal = false - } + if len(header.TxDependency) > 0 { + shouldDelayFeeCal = delayMap[i] - task := &ExecutionTask{ - msg: msg, - config: p.config, - gasLimit: block.GasLimit(), - blockNumber: blockNumber, - blockHash: blockHash, - tx: tx, - index: i, - cleanStateDB: cleansdb, - finalStateDB: statedb, - blockChain: p.bc, - header: header, - evmConfig: cfg, - shouldDelayFeeCal: &shouldDelayFeeCal, - sender: msg.From(), - totalUsedGas: usedGas, - receipts: &receipts, - allLogs: &allLogs, - } + task := &ExecutionTask{ + msg: msg, + config: p.config, + gasLimit: block.GasLimit(), + blockNumber: blockNumber, + blockHash: blockHash, + tx: tx, + index: i, + cleanStateDB: cleansdb, + finalStateDB: statedb, + blockChain: p.bc, + header: header, + evmConfig: cfg, + shouldDelayFeeCal: &shouldDelayFeeCal, + sender: msg.From(), + totalUsedGas: usedGas, + receipts: &receipts, + allLogs: &allLogs, + dependencies: deps[i], + blockContext: blockContext, + coinbase: coinbase, + } - tasks = append(tasks, task) + tasks = append(tasks, task) + } else { + if msg.From() == coinbase { + shouldDelayFeeCal = false + } + + task := &ExecutionTask{ + msg: msg, + config: p.config, + gasLimit: block.GasLimit(), + blockNumber: blockNumber, + blockHash: blockHash, + tx: tx, + index: i, + cleanStateDB: cleansdb, + finalStateDB: statedb, + blockChain: p.bc, + header: header, + evmConfig: cfg, + shouldDelayFeeCal: &shouldDelayFeeCal, + sender: msg.From(), + totalUsedGas: usedGas, + receipts: &receipts, + allLogs: &allLogs, + dependencies: nil, + blockContext: blockContext, + coinbase: coinbase, + } + + tasks = append(tasks, task) + } } backupStateDB := statedb.Copy() profile := false - result, err := blockstm.ExecuteParallel(tasks, profile) + result, err := blockstm.ExecuteParallel(tasks, false, metadata) if err == nil && profile { _, weight := result.Deps.LongestPath(*result.Stats) @@ -356,7 +406,7 @@ func (p *ParallelStateProcessor) Process(block *types.Block, statedb *state.Stat t.totalUsedGas = usedGas } - _, err = blockstm.ExecuteParallel(tasks, false) + _, err = blockstm.ExecuteParallel(tasks, false, metadata) break } @@ -372,3 +422,23 @@ func (p *ParallelStateProcessor) Process(block *types.Block, statedb *state.Stat return receipts, allLogs, *usedGas, nil } + +func GetDeps(txDependency [][]uint64) (map[int][]int, map[int]bool) { + deps := make(map[int][]int) + delayMap := make(map[int]bool) + + for i := 0; i <= len(txDependency)-1; i++ { + idx := int(txDependency[i][0]) + shouldDelay := txDependency[i][1] == 1 + + delayMap[idx] = shouldDelay + + deps[idx] = []int{} + + for j := 2; j <= len(txDependency[i])-1; j++ { + deps[idx] = append(deps[idx], int(txDependency[i][j])) + } + } + + return deps, delayMap +} diff --git a/core/state/statedb.go b/core/state/statedb.go index 4b74cb6153..b77bd9d763 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -179,6 +179,10 @@ func (sdb *StateDB) SetMVHashmap(mvhm *blockstm.MVHashMap) { sdb.dep = -1 } +func (sdb *StateDB) GetMVHashmap() *blockstm.MVHashMap { + return sdb.mvHashmap +} + func (s *StateDB) MVWriteList() []blockstm.WriteDescriptor { writes := make([]blockstm.WriteDescriptor, 0, len(s.writeMap)) @@ -227,6 +231,14 @@ func (s *StateDB) ensureWriteMap() { } } +func (s *StateDB) ClearReadMap() { + s.readMap = make(map[blockstm.Key]blockstm.ReadDescriptor) +} + +func (s *StateDB) ClearWriteMap() { + s.writeMap = make(map[blockstm.Key]blockstm.WriteDescriptor) +} + func (s *StateDB) HadInvalidRead() bool { return s.dep >= 0 } diff --git a/core/state_processor.go b/core/state_processor.go index d4c77ae410..7cfe613080 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -97,12 +97,51 @@ func applyTransaction(msg types.Message, config *params.ChainConfig, bc ChainCon txContext := NewEVMTxContext(msg) evm.Reset(txContext, statedb) - // Apply the transaction to the current state (included in the env). - result, err := ApplyMessage(evm, msg, gp) + var result *ExecutionResult + + var err error + + backupMVHashMap := statedb.GetMVHashmap() + + // pause recording read and write + statedb.SetMVHashmap(nil) + + coinbaseBalance := statedb.GetBalance(evm.Context.Coinbase) + + // resume recording read and write + statedb.SetMVHashmap(backupMVHashMap) + + result, err = ApplyMessageNoFeeBurnOrTip(evm, msg, gp) if err != nil { return nil, err } + // stop recording read and write + statedb.SetMVHashmap(nil) + + if evm.ChainConfig().IsLondon(blockNumber) { + statedb.AddBalance(result.BurntContractAddress, result.FeeBurnt) + } + + statedb.AddBalance(evm.Context.Coinbase, result.FeeTipped) + output1 := new(big.Int).SetBytes(result.SenderInitBalance.Bytes()) + output2 := new(big.Int).SetBytes(coinbaseBalance.Bytes()) + + // Deprecating transfer log and will be removed in future fork. PLEASE DO NOT USE this transfer log going forward. Parameters won't get updated as expected going forward with EIP1559 + // add transfer log + AddFeeTransferLog( + statedb, + + msg.From(), + evm.Context.Coinbase, + + result.FeeTipped, + result.SenderInitBalance, + coinbaseBalance, + output1.Sub(output1, result.FeeTipped), + output2.Add(output2, result.FeeTipped), + ) + // Update the state with pending changes. var root []byte if config.IsByzantium(blockNumber) { diff --git a/core/types/block.go b/core/types/block.go index d4451497af..0d91e543da 100644 --- a/core/types/block.go +++ b/core/types/block.go @@ -87,6 +87,11 @@ type Header struct { // BaseFee was added by EIP-1559 and is ignored in legacy headers. BaseFee *big.Int `json:"baseFeePerGas" rlp:"optional"` + // length of TxDependency -> n (n = number of transactions in the block) + // length of TxDependency[i] -> 2 + k (k = a whole number) + // first 2 element in TxDependency[i] -> transaction index, and flag representing if delay is allowed or not + // (0 -> delay is not allowed, 1 -> delay is allowed) + // next k elements in TxDependency[i] -> transaction indexes on which transaction i is dependent on TxDependency [][]uint64 `json:"txDependency" rlp:"optional"` /* diff --git a/miner/worker.go b/miner/worker.go index 797e7ea980..2d478c2761 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -35,6 +35,7 @@ import ( "github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/consensus/misc" "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/blockstm" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/event" @@ -81,6 +82,10 @@ const ( // staleThreshold is the maximum depth of the acceptable stale block. staleThreshold = 7 + + // TODO: will be handled (and made mandatory) in a hardfork event + // when true, will get the transaction dependencies for parallel execution, also set in `state_processor.go` + EnableMVHashMap = true ) // environment is the worker's current environment and holds all @@ -918,7 +923,50 @@ func (w *worker) commitTransactions(env *environment, txs *types.TransactionsByP } var coalescedLogs []*types.Log + var depsMVReadList [][]blockstm.ReadDescriptor + + var depsMVFullWriteList [][]blockstm.WriteDescriptor + + var mvReadMapList []map[blockstm.Key]blockstm.ReadDescriptor + + var deps map[int]map[int]bool + + chDeps := make(chan blockstm.TxDep) + + var count int + + var depsWg sync.WaitGroup + + // create and add empty mvHashMap in statedb + if EnableMVHashMap { + depsMVReadList = [][]blockstm.ReadDescriptor{} + + depsMVFullWriteList = [][]blockstm.WriteDescriptor{} + + mvReadMapList = []map[blockstm.Key]blockstm.ReadDescriptor{} + + deps = map[int]map[int]bool{} + + chDeps = make(chan blockstm.TxDep) + + count = 0 + + depsWg.Add(1) + + go func(chDeps chan blockstm.TxDep) { + for t := range chDeps { + deps = blockstm.UpdateDeps(deps, t) + } + + depsWg.Done() + }(chDeps) + } + for { + if EnableMVHashMap { + env.state.AddEmptyMVHashMap() + } + // In the following three cases, we will interrupt the execution of the transaction. // (1) new head block event arrival, the interrupt signal is 1 // (2) worker start or restart, the interrupt signal is 1 @@ -986,7 +1034,23 @@ func (w *worker) commitTransactions(env *environment, txs *types.TransactionsByP // Everything ok, collect the logs and shift in the next transaction from the same account coalescedLogs = append(coalescedLogs, logs...) env.tcount++ - txs.Shift() + + if EnableMVHashMap { + depsMVReadList = append(depsMVReadList, env.state.MVReadList()) + depsMVFullWriteList = append(depsMVFullWriteList, env.state.MVFullWriteList()) + mvReadMapList = append(mvReadMapList, env.state.MVReadMap()) + + temp := blockstm.TxDep{ + Index: env.tcount - 1, + ReadList: depsMVReadList[count], + FullWriteList: depsMVFullWriteList, + } + + chDeps <- temp + count++ + + txs.Shift() + } case errors.Is(err, core.ErrTxTypeNotSupported): // Pop the unsupported transaction without shifting in the next from the account @@ -999,6 +1063,54 @@ func (w *worker) commitTransactions(env *environment, txs *types.TransactionsByP log.Debug("Transaction failed, account skipped", "hash", tx.Hash(), "err", err) txs.Shift() } + + if EnableMVHashMap { + env.state.ClearReadMap() + env.state.ClearWriteMap() + } + } + + // nolint:nestif + if EnableMVHashMap { + close(chDeps) + depsWg.Wait() + + if len(mvReadMapList) > 0 { + tempDeps := make([][]uint64, len(mvReadMapList)) + + // adding for txIdx = 0 + tempDeps[0] = []uint64{uint64(0)} + tempDeps[0] = append(tempDeps[0], 1) + + for j := range deps[0] { + tempDeps[0] = append(tempDeps[0], uint64(j)) + } + + for i := 1; i <= len(mvReadMapList)-1; i++ { + tempDeps[i] = []uint64{uint64(i)} + + reads := mvReadMapList[i-1] + + _, ok1 := reads[blockstm.NewSubpathKey(env.coinbase, state.BalancePath)] + _, ok2 := reads[blockstm.NewSubpathKey(common.HexToAddress(w.chainConfig.Bor.CalculateBurntContract(env.header.Number.Uint64())), state.BalancePath)] + + if ok1 || ok2 { + // 0 -> delay is not allowed + tempDeps[i] = append(tempDeps[i], 0) + } else { + // 1 -> delay is allowed + tempDeps[i] = append(tempDeps[i], 1) + } + + for j := range deps[i] { + tempDeps[i] = append(tempDeps[i], uint64(j)) + } + } + + env.header.TxDependency = tempDeps + } else { + env.header.TxDependency = nil + } } if !w.isRunning() && len(coalescedLogs) > 0 { diff --git a/miner/worker_test.go b/miner/worker_test.go index 011895c854..da40218a7d 100644 --- a/miner/worker_test.go +++ b/miner/worker_test.go @@ -714,3 +714,115 @@ func BenchmarkBorMining(b *testing.B) { } } } + +// uses core.NewParallelBlockChain to use the dependencies present in the block header +func BenchmarkBorMiningBlockSTMMetadata(b *testing.B) { + chainConfig := params.BorUnittestChainConfig + + ctrl := gomock.NewController(b) + defer ctrl.Finish() + + ethAPIMock := api.NewMockCaller(ctrl) + ethAPIMock.EXPECT().Call(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() + + spanner := bor.NewMockSpanner(ctrl) + spanner.EXPECT().GetCurrentValidators(gomock.Any(), gomock.Any(), gomock.Any()).Return([]*valset.Validator{ + { + ID: 0, + Address: TestBankAddress, + VotingPower: 100, + ProposerPriority: 0, + }, + }, nil).AnyTimes() + + heimdallClientMock := mocks.NewMockIHeimdallClient(ctrl) + heimdallClientMock.EXPECT().Close().Times(1) + + contractMock := bor.NewMockGenesisContract(ctrl) + + db, _, _ := NewDBForFakes(b) + + engine := NewFakeBor(b, db, chainConfig, ethAPIMock, spanner, heimdallClientMock, contractMock) + defer engine.Close() + + chainConfig.LondonBlock = big.NewInt(0) + + w, back, _ := NewTestWorker(b, chainConfig, engine, db, 0) + defer w.close() + + // This test chain imports the mined blocks. + db2 := rawdb.NewMemoryDatabase() + back.Genesis.MustCommit(db2) + + chain, _ := core.NewParallelBlockChain(db2, nil, back.chain.Config(), engine, vm.Config{}, nil, nil, nil) + defer chain.Stop() + + // Ignore empty commit here for less noise. + w.skipSealHook = func(task *task) bool { + return len(task.receipts) == 0 + } + + // fulfill tx pool + const ( + totalGas = testGas + params.TxGas + totalBlocks = 10 + ) + + var err error + + txInBlock := int(back.Genesis.GasLimit/totalGas) + 1 + + // a bit risky + for i := 0; i < 2*totalBlocks*txInBlock; i++ { + err = back.txPool.AddLocal(back.newRandomTx(true)) + if err != nil { + b.Fatal("while adding a local transaction", err) + } + + err = back.txPool.AddLocal(back.newRandomTx(false)) + if err != nil { + b.Fatal("while adding a remote transaction", err) + } + } + + // Wait for mined blocks. + sub := w.mux.Subscribe(core.NewMinedBlockEvent{}) + defer sub.Unsubscribe() + + b.ResetTimer() + + prev := uint64(time.Now().Unix()) + + // Start mining! + w.start() + + blockPeriod, ok := back.Genesis.Config.Bor.Period["0"] + if !ok { + blockPeriod = 1 + } + + for i := 0; i < totalBlocks; i++ { + select { + case ev := <-sub.Chan(): + block := ev.Data.(core.NewMinedBlockEvent).Block + + if _, err := chain.InsertChain([]*types.Block{block}); err != nil { + b.Fatalf("failed to insert new mined block %d: %v", block.NumberU64(), err) + } + + // check for dependencies + deps := block.TxDependency() + for i := 1; i < block.Transactions().Len(); i++ { + if deps[i][0] != uint64(i) || deps[i][1] != uint64(0) || deps[i][2] != uint64(i-1) || len(deps[i]) != 3 { + b.Fatalf("wrong dependency") + } + } + + b.Log("block", block.NumberU64(), "time", block.Time()-prev, "txs", block.Transactions().Len(), "gasUsed", block.GasUsed(), "gasLimit", block.GasLimit()) + + prev = block.Time() + case <-time.After(time.Duration(blockPeriod) * time.Second): + b.Fatalf("timeout") + } + } +} From 4968c08246b3a7c4c4a2bf20bfd64f04c002676f Mon Sep 17 00:00:00 2001 From: Pratik Patil Date: Thu, 19 Jan 2023 10:52:01 +0530 Subject: [PATCH 19/38] added hardfork checks (#666) --- miner/worker.go | 12 ++++++++---- miner/worker_test.go | 18 +++++++++++++----- params/config.go | 25 +++++++++++++++++++------ 3 files changed, 40 insertions(+), 15 deletions(-) diff --git a/miner/worker.go b/miner/worker.go index 4784b07115..3cc06644bc 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -89,10 +89,6 @@ const ( // staleThreshold is the maximum depth of the acceptable stale block. staleThreshold = 7 - - // TODO: will be handled (and made mandatory) in a hardfork event - // when true, will get the transaction dependencies for parallel execution, also set in `state_processor.go` - EnableMVHashMap = true ) // metrics gauge to track total and empty blocks sealed by a miner @@ -965,6 +961,14 @@ func (w *worker) commitTransactions(env *environment, txs *types.TransactionsByP var depsWg sync.WaitGroup + var EnableMVHashMap bool + + if w.chainConfig.Bor.IsParallelUniverse(env.header.Number) { + EnableMVHashMap = true + } else { + EnableMVHashMap = false + } + // create and add empty mvHashMap in statedb if EnableMVHashMap { depsMVReadList = [][]blockstm.ReadDescriptor{} diff --git a/miner/worker_test.go b/miner/worker_test.go index da40218a7d..f99e6ae706 100644 --- a/miner/worker_test.go +++ b/miner/worker_test.go @@ -716,6 +716,8 @@ func BenchmarkBorMining(b *testing.B) { } // uses core.NewParallelBlockChain to use the dependencies present in the block header +// params.BorUnittestChainConfig contains the ParallelUniverseBlock ad big.NewInt(5), so the first 4 blocks will not have metadata. +// nolint: gocognit func BenchmarkBorMiningBlockSTMMetadata(b *testing.B) { chainConfig := params.BorUnittestChainConfig @@ -810,11 +812,17 @@ func BenchmarkBorMiningBlockSTMMetadata(b *testing.B) { b.Fatalf("failed to insert new mined block %d: %v", block.NumberU64(), err) } - // check for dependencies - deps := block.TxDependency() - for i := 1; i < block.Transactions().Len(); i++ { - if deps[i][0] != uint64(i) || deps[i][1] != uint64(0) || deps[i][2] != uint64(i-1) || len(deps[i]) != 3 { - b.Fatalf("wrong dependency") + // check for dependencies for block number > 4 + if block.NumberU64() <= 4 { + if block.TxDependency() != nil { + b.Fatalf("dependency not nil") + } + } else { + deps := block.TxDependency() + for i := 1; i < block.Transactions().Len(); i++ { + if deps[i][0] != uint64(i) || deps[i][1] != uint64(0) || deps[i][2] != uint64(i-1) || len(deps[i]) != 3 { + b.Fatalf("wrong dependency") + } } } diff --git a/params/config.go b/params/config.go index d97d6957fa..88c405ea0b 100644 --- a/params/config.go +++ b/params/config.go @@ -311,6 +311,7 @@ var ( BerlinBlock: big.NewInt(0), LondonBlock: big.NewInt(0), Bor: &BorConfig{ + ParallelUniverseBlock: big.NewInt(5), Period: map[string]uint64{ "0": 1, }, @@ -349,8 +350,9 @@ var ( BerlinBlock: big.NewInt(13996000), LondonBlock: big.NewInt(22640000), Bor: &BorConfig{ - JaipurBlock: big.NewInt(22770000), - DelhiBlock: big.NewInt(29638656), + JaipurBlock: big.NewInt(22770000), + DelhiBlock: big.NewInt(29638656), + ParallelUniverseBlock: big.NewInt(0), Period: map[string]uint64{ "0": 2, "25275000": 5, @@ -403,7 +405,8 @@ var ( BerlinBlock: big.NewInt(14750000), LondonBlock: big.NewInt(23850000), Bor: &BorConfig{ - JaipurBlock: big.NewInt(23850000), + JaipurBlock: big.NewInt(23850000), + ParallelUniverseBlock: big.NewInt(0), Period: map[string]uint64{ "0": 2, }, @@ -580,9 +583,10 @@ type BorConfig struct { StateReceiverContract string `json:"stateReceiverContract"` // State receiver contract OverrideStateSyncRecords map[string]int `json:"overrideStateSyncRecords"` // override state records count BlockAlloc map[string]interface{} `json:"blockAlloc"` - BurntContract map[string]string `json:"burntContract"` // governance contract where the token will be sent to and burnt in london fork - JaipurBlock *big.Int `json:"jaipurBlock"` // Jaipur switch block (nil = no fork, 0 = already on jaipur) - DelhiBlock *big.Int `json:"delhiBlock"` // Delhi switch block (nil = no fork, 0 = already on delhi) + BurntContract map[string]string `json:"burntContract"` // governance contract where the token will be sent to and burnt in london fork + JaipurBlock *big.Int `json:"jaipurBlock"` // Jaipur switch block (nil = no fork, 0 = already on jaipur) + DelhiBlock *big.Int `json:"delhiBlock"` // Delhi switch block (nil = no fork, 0 = already on delhi) + ParallelUniverseBlock *big.Int `json:"parallelUniverseBlock"` // TODO: update all occurrence, change name and finalize number (hardfork for block-stm related changes) } // String implements the stringer interface, returning the consensus engine details. @@ -614,6 +618,15 @@ func (c *BorConfig) IsDelhi(number *big.Int) bool { return isForked(c.DelhiBlock, number) } +// TODO: modify this function once the block number is finalized +func (c *BorConfig) IsParallelUniverse(number *big.Int) bool { + if c.ParallelUniverseBlock == big.NewInt(0) { + return false + } + + return isForked(c.ParallelUniverseBlock, number) +} + func (c *BorConfig) calculateBorConfigHelper(field map[string]uint64, number uint64) uint64 { keys := make([]string, 0, len(field)) for k := range field { From d25aa76447bc60b69c7f3ce5dea15068f9f4d7fa Mon Sep 17 00:00:00 2001 From: Jerry Date: Tue, 24 Jan 2023 12:08:08 -0800 Subject: [PATCH 20/38] Minor fix in statedb test --- core/state/statedb_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/state/statedb_test.go b/core/state/statedb_test.go index 73b028bfcc..053d57470a 100644 --- a/core/state/statedb_test.go +++ b/core/state/statedb_test.go @@ -524,8 +524,8 @@ func TestMVHashMapReadWriteDelete(t *testing.T) { states[1].FlushMVWriteSet() // Tx1 read - v = states[2].GetState(addr, key) - b := states[2].GetBalance(addr) + v = states[1].GetState(addr, key) + b := states[1].GetBalance(addr) assert.Equal(t, val, v) assert.Equal(t, balance, b) From 9795a287d00b8ffe9400ac7f9606cb339faca51f Mon Sep 17 00:00:00 2001 From: Pratik Patil Date: Thu, 9 Feb 2023 13:34:06 +0530 Subject: [PATCH 21/38] added 2 flags to enable parallel EVM and set the number of speculative processes (#727) --- builder/files/config.toml | 4 ++++ cmd/utils/flags.go | 1 + core/blockstm/executor.go | 15 ++++++++++----- core/parallel_state_processor.go | 7 +++++++ core/vm/interpreter.go | 4 ++++ docs/config.md | 4 ++++ eth/backend.go | 14 ++++++++++++-- eth/ethconfig/config.go | 3 +++ internal/cli/server/config.go | 16 ++++++++++++++++ internal/cli/server/flags.go | 14 ++++++++++++++ miner/worker_test.go | 2 +- tests/block_test_util.go | 17 +++++++++-------- 12 files changed, 85 insertions(+), 16 deletions(-) diff --git a/builder/files/config.toml b/builder/files/config.toml index 870c164a8d..1dea17c170 100644 --- a/builder/files/config.toml +++ b/builder/files/config.toml @@ -136,3 +136,7 @@ syncmode = "full" # [developer] # dev = false # period = 0 + +# [parallelevm] + # enable = false + # procs = 8 \ No newline at end of file diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 81ce27ef4c..71c4cc0cd9 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -2081,6 +2081,7 @@ func MakeChain(ctx *cli.Context, stack *node.Node) (chain *core.BlockChain, chai // TODO(rjl493456442) disable snapshot generation/wiping if the chain is read only. // Disable transaction indexing/unindexing by default. + // PSP - Check for config.ParallelEVM.Enable here? chain, err = core.NewBlockChain(chainDb, cache, config, engine, vmcfg, nil, nil, nil) if err != nil { Fatalf("Can't create BlockChain: %v", err) diff --git a/core/blockstm/executor.go b/core/blockstm/executor.go index a086347610..37bde617e5 100644 --- a/core/blockstm/executor.go +++ b/core/blockstm/executor.go @@ -36,6 +36,12 @@ type ExecVersionView struct { sender common.Address } +var NumSpeculativeProcs int = 8 + +func SetProcs(specProcs int) { + NumSpeculativeProcs = specProcs +} + func (ev *ExecVersionView) Execute() (er ExecResult) { er.ver = ev.ver if er.err = ev.et.Execute(ev.mvh, ev.ver.Incarnation); er.err != nil { @@ -157,8 +163,7 @@ type ParallelExecutionResult struct { AllDeps map[int]map[int]bool } -const numGoProcs = 2 -const numSpeculativeProcs = 8 +const numGoProcs = 1 type ParallelExecutor struct { tasks []ExecTask @@ -315,10 +320,10 @@ func (pe *ParallelExecutor) Prepare() { } } - pe.workerWg.Add(numSpeculativeProcs + numGoProcs) + pe.workerWg.Add(NumSpeculativeProcs + numGoProcs) // Launch workers that execute transactions - for i := 0; i < numSpeculativeProcs+numGoProcs; i++ { + for i := 0; i < NumSpeculativeProcs+numGoProcs; i++ { go func(procNum int) { defer pe.workerWg.Done() @@ -352,7 +357,7 @@ func (pe *ParallelExecutor) Prepare() { } } - if procNum < numSpeculativeProcs { + if procNum < NumSpeculativeProcs { for range pe.chSpeculativeTasks { doWork(pe.specTaskQueue.Pop().(ExecVersionView)) } diff --git a/core/parallel_state_processor.go b/core/parallel_state_processor.go index 23457d5c60..0c97d074ff 100644 --- a/core/parallel_state_processor.go +++ b/core/parallel_state_processor.go @@ -34,6 +34,11 @@ import ( "github.com/ethereum/go-ethereum/params" ) +type ParallelEVMConfig struct { + Enable bool + SpeculativeProcesses int +} + // StateProcessor is a basic Processor, which takes care of transitioning // state from one point to another. // @@ -269,6 +274,8 @@ var parallelizabilityTimer = metrics.NewRegisteredTimer("block/parallelizability // transactions failed to execute due to insufficient gas it will return an error. // nolint:gocognit func (p *ParallelStateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg vm.Config) (types.Receipts, []*types.Log, uint64, error) { + blockstm.SetProcs(cfg.ParallelSpeculativeProcesses) + var ( receipts types.Receipts header = block.Header() diff --git a/core/vm/interpreter.go b/core/vm/interpreter.go index 21e3c914e1..554a3dc96f 100644 --- a/core/vm/interpreter.go +++ b/core/vm/interpreter.go @@ -34,6 +34,10 @@ type Config struct { JumpTable *JumpTable // EVM instruction table, automatically populated if unset ExtraEips []int // Additional EIPS that are to be enabled + + // parallel EVM configs + ParallelEnable bool + ParallelSpeculativeProcesses int } // ScopeContext contains the things that are per-call, such as stack and memory, diff --git a/docs/config.md b/docs/config.md index 57f4c25fef..ebec217b96 100644 --- a/docs/config.md +++ b/docs/config.md @@ -143,4 +143,8 @@ addr = ":3131" [developer] dev = false period = 0 + +[blockstm] +enable = false +procs = 8 ``` diff --git a/eth/backend.go b/eth/backend.go index 824fec8914..ca8a1759a9 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -108,6 +108,7 @@ type Ethereum struct { shutdownTracker *shutdowncheck.ShutdownTracker // Tracks if and when the node has shutdown ungracefully } +// PSP // New creates a new Ethereum object (including the // initialisation of the common Ethereum object) func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { @@ -206,7 +207,9 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { } var ( vmConfig = vm.Config{ - EnablePreimageRecording: config.EnablePreimageRecording, + EnablePreimageRecording: config.EnablePreimageRecording, + ParallelEnable: config.ParallelEVM.Enable, + ParallelSpeculativeProcesses: config.ParallelEVM.SpeculativeProcesses, } cacheConfig = &core.CacheConfig{ TrieCleanLimit: config.TrieCleanCache, @@ -224,7 +227,14 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { checker := whitelist.NewService(10) - eth.blockchain, err = core.NewBlockChain(chainDb, cacheConfig, chainConfig, eth.engine, vmConfig, eth.shouldPreserve, &config.TxLookupLimit, checker) + // check if Parallel EVM is enabled + // if enabled, use parallel state processor + if config.ParallelEVM.Enable { + eth.blockchain, err = core.NewParallelBlockChain(chainDb, cacheConfig, chainConfig, eth.engine, vmConfig, eth.shouldPreserve, &config.TxLookupLimit, checker) + } else { + eth.blockchain, err = core.NewBlockChain(chainDb, cacheConfig, chainConfig, eth.engine, vmConfig, eth.shouldPreserve, &config.TxLookupLimit, checker) + } + if err != nil { return nil, err } diff --git a/eth/ethconfig/config.go b/eth/ethconfig/config.go index c9272758ab..1353cc488b 100644 --- a/eth/ethconfig/config.go +++ b/eth/ethconfig/config.go @@ -224,6 +224,9 @@ type Config struct { // Bor logs flag BorLogs bool + // Parallel EVM (Block-STM) related config + ParallelEVM core.ParallelEVMConfig `toml:",omitempty"` + // Arrow Glacier block override (TODO: remove after the fork) OverrideArrowGlacier *big.Int `toml:",omitempty"` diff --git a/internal/cli/server/config.go b/internal/cli/server/config.go index 34c17b3f7d..d8b0d8f822 100644 --- a/internal/cli/server/config.go +++ b/internal/cli/server/config.go @@ -104,6 +104,9 @@ type Config struct { // Developer has the developer mode related settings Developer *DeveloperConfig `hcl:"developer,block" toml:"developer,block"` + + // ParallelEVM has the parallel evm related settings + ParallelEVM *ParallelEVMConfig `hcl:"parallelevm,block" toml:"parallelevm,block"` } type P2PConfig struct { @@ -398,6 +401,12 @@ type DeveloperConfig struct { Period uint64 `hcl:"period,optional" toml:"period,optional"` } +type ParallelEVMConfig struct { + Enable bool `hcl:"enable,optional" toml:"enable,optional"` + + SpeculativeProcesses int `hcl:"procs,optional" toml:"procs,optional"` +} + func DefaultConfig() *Config { return &Config{ Chain: "mainnet", @@ -531,6 +540,10 @@ func DefaultConfig() *Config { Enabled: false, Period: 0, }, + ParallelEVM: &ParallelEVMConfig{ + Enable: false, + SpeculativeProcesses: 8, + }, } } @@ -893,6 +906,9 @@ func (c *Config) buildEth(stack *node.Node, accountManager *accounts.Manager) (* n.BorLogs = c.BorLogs n.DatabaseHandles = dbHandles + n.ParallelEVM.Enable = c.ParallelEVM.Enable + n.ParallelEVM.SpeculativeProcesses = c.ParallelEVM.SpeculativeProcesses + return &n, nil } diff --git a/internal/cli/server/flags.go b/internal/cli/server/flags.go index ba9be13376..5467713ab2 100644 --- a/internal/cli/server/flags.go +++ b/internal/cli/server/flags.go @@ -682,5 +682,19 @@ func (c *Command) Flags() *flagset.Flagset { Value: &c.cliConfig.Developer.Period, Default: c.cliConfig.Developer.Period, }) + + // parallelevm + f.BoolFlag(&flagset.BoolFlag{ + Name: "parallelevm.enable", + Usage: "Enable Block STM", + Value: &c.cliConfig.ParallelEVM.Enable, + Default: c.cliConfig.ParallelEVM.Enable, + }) + f.IntFlag(&flagset.IntFlag{ + Name: "parallelevm.procs", + Usage: "Number of speculative processes (cores) in Block STM", + Value: &c.cliConfig.ParallelEVM.SpeculativeProcesses, + Default: c.cliConfig.ParallelEVM.SpeculativeProcesses, + }) return f } diff --git a/miner/worker_test.go b/miner/worker_test.go index f99e6ae706..3306ad4069 100644 --- a/miner/worker_test.go +++ b/miner/worker_test.go @@ -756,7 +756,7 @@ func BenchmarkBorMiningBlockSTMMetadata(b *testing.B) { db2 := rawdb.NewMemoryDatabase() back.Genesis.MustCommit(db2) - chain, _ := core.NewParallelBlockChain(db2, nil, back.chain.Config(), engine, vm.Config{}, nil, nil, nil) + chain, _ := core.NewParallelBlockChain(db2, nil, back.chain.Config(), engine, vm.Config{ParallelEnable: true, ParallelSpeculativeProcesses: 8}, nil, nil, nil) defer chain.Stop() // Ignore empty commit here for less noise. diff --git a/tests/block_test_util.go b/tests/block_test_util.go index 487fd2d4d8..64b9008fe3 100644 --- a/tests/block_test_util.go +++ b/tests/block_test_util.go @@ -176,17 +176,18 @@ func (t *BlockTest) genesis(config *params.ChainConfig) *core.Genesis { } } -/* See https://github.com/ethereum/tests/wiki/Blockchain-Tests-II +/* +See https://github.com/ethereum/tests/wiki/Blockchain-Tests-II - Whether a block is valid or not is a bit subtle, it's defined by presence of - blockHeader, transactions and uncleHeaders fields. If they are missing, the block is - invalid and we must verify that we do not accept it. + Whether a block is valid or not is a bit subtle, it's defined by presence of + blockHeader, transactions and uncleHeaders fields. If they are missing, the block is + invalid and we must verify that we do not accept it. - Since some tests mix valid and invalid blocks we need to check this for every block. + Since some tests mix valid and invalid blocks we need to check this for every block. - If a block is invalid it does not necessarily fail the test, if it's invalidness is - expected we are expected to ignore it and continue processing and then validate the - post state. + If a block is invalid it does not necessarily fail the test, if it's invalidness is + expected we are expected to ignore it and continue processing and then validate the + post state. */ func (t *BlockTest) insertBlocks(blockchain *core.BlockChain) ([]btBlock, error) { validBlocks := make([]btBlock, 0) From 55962e16c6046bac60dac98f2fdc333c53c20bb7 Mon Sep 17 00:00:00 2001 From: Jerry Date: Tue, 14 Feb 2023 15:16:20 -0800 Subject: [PATCH 22/38] [Bug fix] Use parallel processor in unit test --- core/state_processor_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/state_processor_test.go b/core/state_processor_test.go index 5eb7938811..35e53a2e54 100644 --- a/core/state_processor_test.go +++ b/core/state_processor_test.go @@ -236,7 +236,7 @@ func TestStateProcessorErrors(t *testing.T) { } genesis = gspec.MustCommit(db) blockchain, _ = NewBlockChain(db, nil, gspec.Config, ethash.NewFaker(), vm.Config{}, nil, nil, nil) - parallelBlockchain, _ = NewBlockChain(db, nil, gspec.Config, ethash.NewFaker(), vm.Config{}, nil, nil, nil) + parallelBlockchain, _ = NewParallelBlockChain(db, nil, gspec.Config, ethash.NewFaker(), vm.Config{ParallelEnable: true, ParallelSpeculativeProcesses: 8}, nil, nil, nil) ) defer blockchain.Stop() defer parallelBlockchain.Stop() @@ -281,7 +281,7 @@ func TestStateProcessorErrors(t *testing.T) { } genesis = gspec.MustCommit(db) blockchain, _ = NewBlockChain(db, nil, gspec.Config, ethash.NewFaker(), vm.Config{}, nil, nil, nil) - parallelBlockchain, _ = NewBlockChain(db, nil, gspec.Config, ethash.NewFaker(), vm.Config{}, nil, nil, nil) + parallelBlockchain, _ = NewParallelBlockChain(db, nil, gspec.Config, ethash.NewFaker(), vm.Config{ParallelEnable: true, ParallelSpeculativeProcesses: 8}, nil, nil, nil) ) defer blockchain.Stop() defer parallelBlockchain.Stop() From 480ccf2aa8f8baac7ae8478f51d74239cfd33a25 Mon Sep 17 00:00:00 2001 From: Pratik Patil Date: Wed, 5 Apr 2023 11:12:57 +0530 Subject: [PATCH 23/38] Optimized the dependency metadata structure (#804) * removed the first 2 element in TxDependency[i] * addressed comments --- core/parallel_state_processor.go | 28 ++++++++++------------------ core/types/block.go | 8 +++----- miner/worker.go | 20 ++++++++------------ miner/worker_test.go | 5 ++++- 4 files changed, 25 insertions(+), 36 deletions(-) diff --git a/core/parallel_state_processor.go b/core/parallel_state_processor.go index 0c97d074ff..c4f3530374 100644 --- a/core/parallel_state_processor.go +++ b/core/parallel_state_processor.go @@ -297,7 +297,7 @@ func (p *ParallelStateProcessor) Process(block *types.Block, statedb *state.Stat coinbase, _ := p.bc.Engine().Author(header) - deps, delayMap := GetDeps(block.Header().TxDependency) + deps := GetDeps(block.Header().TxDependency) if block.Header().TxDependency != nil { metadata = true @@ -315,9 +315,11 @@ func (p *ParallelStateProcessor) Process(block *types.Block, statedb *state.Stat cleansdb := statedb.Copy() - if len(header.TxDependency) > 0 { - shouldDelayFeeCal = delayMap[i] + if msg.From() == coinbase { + shouldDelayFeeCal = false + } + if len(header.TxDependency) != len(block.Transactions()) { task := &ExecutionTask{ msg: msg, config: p.config, @@ -343,10 +345,6 @@ func (p *ParallelStateProcessor) Process(block *types.Block, statedb *state.Stat tasks = append(tasks, task) } else { - if msg.From() == coinbase { - shouldDelayFeeCal = false - } - task := &ExecutionTask{ msg: msg, config: p.config, @@ -430,22 +428,16 @@ func (p *ParallelStateProcessor) Process(block *types.Block, statedb *state.Stat return receipts, allLogs, *usedGas, nil } -func GetDeps(txDependency [][]uint64) (map[int][]int, map[int]bool) { +func GetDeps(txDependency [][]uint64) map[int][]int { deps := make(map[int][]int) - delayMap := make(map[int]bool) for i := 0; i <= len(txDependency)-1; i++ { - idx := int(txDependency[i][0]) - shouldDelay := txDependency[i][1] == 1 + deps[i] = []int{} - delayMap[idx] = shouldDelay - - deps[idx] = []int{} - - for j := 2; j <= len(txDependency[i])-1; j++ { - deps[idx] = append(deps[idx], int(txDependency[i][j])) + for j := 0; j <= len(txDependency[i])-1; j++ { + deps[i] = append(deps[i], int(txDependency[i][j])) } } - return deps, delayMap + return deps } diff --git a/core/types/block.go b/core/types/block.go index 0d91e543da..0af6a35501 100644 --- a/core/types/block.go +++ b/core/types/block.go @@ -87,11 +87,9 @@ type Header struct { // BaseFee was added by EIP-1559 and is ignored in legacy headers. BaseFee *big.Int `json:"baseFeePerGas" rlp:"optional"` - // length of TxDependency -> n (n = number of transactions in the block) - // length of TxDependency[i] -> 2 + k (k = a whole number) - // first 2 element in TxDependency[i] -> transaction index, and flag representing if delay is allowed or not - // (0 -> delay is not allowed, 1 -> delay is allowed) - // next k elements in TxDependency[i] -> transaction indexes on which transaction i is dependent on + // length of TxDependency -> n (n = number of transactions in the block) + // length of TxDependency[i] -> k (k = a whole number) + // k elements in TxDependency[i] -> transaction indexes on which transaction i is dependent on TxDependency [][]uint64 `json:"txDependency" rlp:"optional"` /* diff --git a/miner/worker.go b/miner/worker.go index 3cc06644bc..b2e420746b 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -1133,28 +1133,20 @@ func (w *worker) commitTransactions(env *environment, txs *types.TransactionsByP if len(mvReadMapList) > 0 { tempDeps := make([][]uint64, len(mvReadMapList)) - // adding for txIdx = 0 - tempDeps[0] = []uint64{uint64(0)} - tempDeps[0] = append(tempDeps[0], 1) - for j := range deps[0] { tempDeps[0] = append(tempDeps[0], uint64(j)) } - for i := 1; i <= len(mvReadMapList)-1; i++ { - tempDeps[i] = []uint64{uint64(i)} + delayFlag := true + for i := 1; i <= len(mvReadMapList)-1; i++ { reads := mvReadMapList[i-1] _, ok1 := reads[blockstm.NewSubpathKey(env.coinbase, state.BalancePath)] _, ok2 := reads[blockstm.NewSubpathKey(common.HexToAddress(w.chainConfig.Bor.CalculateBurntContract(env.header.Number.Uint64())), state.BalancePath)] if ok1 || ok2 { - // 0 -> delay is not allowed - tempDeps[i] = append(tempDeps[i], 0) - } else { - // 1 -> delay is allowed - tempDeps[i] = append(tempDeps[i], 1) + delayFlag = false } for j := range deps[i] { @@ -1162,7 +1154,11 @@ func (w *worker) commitTransactions(env *environment, txs *types.TransactionsByP } } - env.header.TxDependency = tempDeps + if delayFlag { + env.header.TxDependency = tempDeps + } else { + env.header.TxDependency = nil + } } else { env.header.TxDependency = nil } diff --git a/miner/worker_test.go b/miner/worker_test.go index 3306ad4069..7bb5f5bf16 100644 --- a/miner/worker_test.go +++ b/miner/worker_test.go @@ -819,8 +819,11 @@ func BenchmarkBorMiningBlockSTMMetadata(b *testing.B) { } } else { deps := block.TxDependency() + if len(deps[0]) != 0 { + b.Fatalf("wrong dependency") + } for i := 1; i < block.Transactions().Len(); i++ { - if deps[i][0] != uint64(i) || deps[i][1] != uint64(0) || deps[i][2] != uint64(i-1) || len(deps[i]) != 3 { + if deps[i][0] != uint64(i-1) || len(deps[i]) != 1 { b.Fatalf("wrong dependency") } } From 463bea0ce20f7ab916c7c45a15c3e8f5712f819d Mon Sep 17 00:00:00 2001 From: Jerry Date: Tue, 21 Feb 2023 13:00:19 -0800 Subject: [PATCH 24/38] [Bug fix] Fix tx dependency --- core/blockstm/executor.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/blockstm/executor.go b/core/blockstm/executor.go index 37bde617e5..4ead604d08 100644 --- a/core/blockstm/executor.go +++ b/core/blockstm/executor.go @@ -290,6 +290,8 @@ func NewParallelExecutor(tasks []ExecTask, profile bool, metadata bool) *Paralle // nolint: gocognit func (pe *ParallelExecutor) Prepare() { + prevSenderTx := make(map[common.Address]int) + for i, t := range pe.tasks { clearPendingFlag := false @@ -309,8 +311,6 @@ func (pe *ParallelExecutor) Prepare() { clearPendingFlag = false } } else { - prevSenderTx := make(map[common.Address]int) - if tx, ok := prevSenderTx[t.Sender()]; ok { pe.execTasks.addDependencies(tx, i) pe.execTasks.clearPending(i) From 650e1413f3f61ce9378670fbe80b08bd57887925 Mon Sep 17 00:00:00 2001 From: Jerry Date: Tue, 11 Apr 2023 15:30:08 -0700 Subject: [PATCH 25/38] Allow process in state processor to be interrupted by caller --- core/blockchain.go | 2 +- core/blockchain_test.go | 34 ++++++++++++++++++++++++++++---- core/chain_makers.go | 13 ++++++++++++ core/parallel_state_processor.go | 3 ++- core/state_processor.go | 11 ++++++++++- core/types.go | 4 +++- eth/state_accessor.go | 22 +++++++++++---------- 7 files changed, 71 insertions(+), 18 deletions(-) diff --git a/core/blockchain.go b/core/blockchain.go index 37d2aff25b..92d74a8be7 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -1803,7 +1803,7 @@ func (bc *BlockChain) insertChain(chain types.Blocks, verifySeals, setHead bool) // Process block using the parent state as reference point substart := time.Now() - receipts, logs, usedGas, err := bc.processor.Process(block, statedb, bc.vmConfig) + receipts, logs, usedGas, err := bc.processor.Process(block, statedb, bc.vmConfig, nil) if err != nil { bc.reportBlock(block, receipts, err) atomic.StoreUint32(&followupInterrupt, 1) diff --git a/core/blockchain_test.go b/core/blockchain_test.go index fa6b61225e..3c80ef3718 100644 --- a/core/blockchain_test.go +++ b/core/blockchain_test.go @@ -17,6 +17,7 @@ package core import ( + "context" "errors" "fmt" "io/ioutil" @@ -123,9 +124,11 @@ func testFork(t *testing.T, blockchain *BlockChain, i, n int, full bool, compara if full { cur := blockchain.CurrentBlock() tdPre = blockchain.GetTd(cur.Hash(), cur.NumberU64()) - if err := testBlockChainImport(blockChainB, blockchain); err != nil { + + if err := testBlockChainImport(blockChainB, blockchain, nil); err != nil { t.Fatalf("failed to import forked block chain: %v", err) } + last := blockChainB[len(blockChainB)-1] tdPost = blockchain.GetTd(last.Hash(), last.NumberU64()) } else { @@ -143,7 +146,7 @@ func testFork(t *testing.T, blockchain *BlockChain, i, n int, full bool, compara // testBlockChainImport tries to process a chain of blocks, writing them into // the database if successful. -func testBlockChainImport(chain types.Blocks, blockchain *BlockChain) error { +func testBlockChainImport(chain types.Blocks, blockchain *BlockChain, ctx context.Context) error { for _, block := range chain { // Try and process the block err := blockchain.engine.VerifyHeader(blockchain, block.Header(), true) @@ -156,11 +159,14 @@ func testBlockChainImport(chain types.Blocks, blockchain *BlockChain) error { } return err } + statedb, err := state.New(blockchain.GetBlockByHash(block.ParentHash()).Root(), blockchain.stateCache, nil) + if err != nil { return err } - receipts, _, usedGas, err := blockchain.processor.Process(block, statedb, vm.Config{}) + + receipts, _, usedGas, err := blockchain.processor.Process(block, statedb, vm.Config{}, ctx) if err != nil { blockchain.reportBlock(block, receipts, err) return err @@ -180,6 +186,26 @@ func testBlockChainImport(chain types.Blocks, blockchain *BlockChain) error { return nil } +func TestBlockChainImportInterrupt(t *testing.T) { + t.Parallel() + + db, blockchain, err := newCanonical(ethash.NewFaker(), 10, true) + if err != nil { + t.Fatalf("failed to make new canonical chain: %v", err) + } + + defer blockchain.Stop() + + blockChainB := makeFakeNonEmptyBlockChain(blockchain.CurrentBlock(), 5, ethash.NewFaker(), db, forkSeed, 5) + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + if err := testBlockChainImport(blockChainB, blockchain, ctx); err != context.Canceled { + t.Errorf("block chain import is not cancelled correctly, got %v, want %v", err, context.Canceled) + } +} + // testHeaderChainImport tries to process a chain of header, writing them into // the database if successful. func testHeaderChainImport(chain []*types.Header, blockchain *BlockChain) error { @@ -476,7 +502,7 @@ func testBrokenChain(t *testing.T, full bool) { // Create a forked chain, and try to insert with a missing link if full { chain := makeBlockChain(blockchain.CurrentBlock(), 5, ethash.NewFaker(), db, forkSeed)[1:] - if err := testBlockChainImport(chain, blockchain); err == nil { + if err := testBlockChainImport(chain, blockchain, nil); err == nil { t.Errorf("broken block chain not reported") } } else { diff --git a/core/chain_makers.go b/core/chain_makers.go index e9944e4744..0857128288 100644 --- a/core/chain_makers.go +++ b/core/chain_makers.go @@ -335,6 +335,19 @@ func makeBlockChain(parent *types.Block, n int, engine consensus.Engine, db ethd return blocks } +// makeBlockChain creates a deterministic chain of blocks rooted at parent with fake invalid transactions. +func makeFakeNonEmptyBlockChain(parent *types.Block, n int, engine consensus.Engine, db ethdb.Database, seed int, numTx int) []*types.Block { + blocks, _ := GenerateChain(params.TestChainConfig, parent, engine, db, n, func(i int, b *BlockGen) { + addr := common.Address{0: byte(seed), 19: byte(i)} + b.SetCoinbase(addr) + for j := 0; j < numTx; j++ { + b.txs = append(b.txs, types.NewTransaction(0, addr, big.NewInt(1000), params.TxGas, nil, nil)) + } + }) + + return blocks +} + type fakeChainReader struct { config *params.ChainConfig stateSyncData []*types.StateSyncData diff --git a/core/parallel_state_processor.go b/core/parallel_state_processor.go index c4f3530374..4d0de960fa 100644 --- a/core/parallel_state_processor.go +++ b/core/parallel_state_processor.go @@ -17,6 +17,7 @@ package core import ( + "context" "fmt" "math/big" "time" @@ -273,7 +274,7 @@ var parallelizabilityTimer = metrics.NewRegisteredTimer("block/parallelizability // 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. // nolint:gocognit -func (p *ParallelStateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg vm.Config) (types.Receipts, []*types.Log, uint64, error) { +func (p *ParallelStateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg vm.Config, interruptCtx context.Context) (types.Receipts, []*types.Log, uint64, error) { blockstm.SetProcs(cfg.ParallelSpeculativeProcesses) var ( diff --git a/core/state_processor.go b/core/state_processor.go index 7cfe613080..90dcfdccf0 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -17,6 +17,7 @@ package core import ( + "context" "fmt" "math/big" @@ -56,7 +57,7 @@ func NewStateProcessor(config *params.ChainConfig, bc *BlockChain, engine consen // 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) (types.Receipts, []*types.Log, uint64, error) { +func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg vm.Config, interruptCtx context.Context) (types.Receipts, []*types.Log, uint64, error) { var ( receipts types.Receipts usedGas = new(uint64) @@ -74,6 +75,14 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg vmenv := vm.NewEVM(blockContext, vm.TxContext{}, statedb, p.config, cfg) // Iterate over and process the individual transactions for i, tx := range block.Transactions() { + if interruptCtx != nil { + select { + case <-interruptCtx.Done(): + return nil, nil, 0, interruptCtx.Err() + default: + } + } + msg, err := tx.AsMessage(types.MakeSigner(p.config, header.Number), header.BaseFee) if err != nil { return nil, nil, 0, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err) diff --git a/core/types.go b/core/types.go index 4c5b74a498..9cdab38483 100644 --- a/core/types.go +++ b/core/types.go @@ -17,6 +17,8 @@ package core import ( + "context" + "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" @@ -47,5 +49,5 @@ type Processor interface { // 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(block *types.Block, statedb *state.StateDB, cfg vm.Config) (types.Receipts, []*types.Log, uint64, error) + Process(block *types.Block, statedb *state.StateDB, cfg vm.Config, interruptCtx context.Context) (types.Receipts, []*types.Log, uint64, error) } diff --git a/eth/state_accessor.go b/eth/state_accessor.go index f01db93a67..d45b62671b 100644 --- a/eth/state_accessor.go +++ b/eth/state_accessor.go @@ -36,15 +36,15 @@ import ( // base layer statedb can be passed then it's regarded as the statedb of the // parent block. // Parameters: -// - block: The block for which we want the state (== state at the stateRoot of the parent) -// - reexec: The maximum number of blocks to reprocess trying to obtain the desired state -// - base: If the caller is tracing multiple blocks, the caller can provide the parent state -// continuously from the callsite. -// - checklive: if true, then the live 'blockchain' state database is used. If the caller want to -// perform Commit or other 'save-to-disk' changes, this should be set to false to avoid -// storing trash persistently -// - preferDisk: this arg can be used by the caller to signal that even though the 'base' is provided, -// it would be preferrable to start from a fresh state, if we have it on disk. +// - block: The block for which we want the state (== state at the stateRoot of the parent) +// - reexec: The maximum number of blocks to reprocess trying to obtain the desired state +// - base: If the caller is tracing multiple blocks, the caller can provide the parent state +// continuously from the callsite. +// - checklive: if true, then the live 'blockchain' state database is used. If the caller want to +// perform Commit or other 'save-to-disk' changes, this should be set to false to avoid +// storing trash persistently +// - preferDisk: this arg can be used by the caller to signal that even though the 'base' is provided, +// it would be preferrable to start from a fresh state, if we have it on disk. func (eth *Ethereum) StateAtBlock(block *types.Block, reexec uint64, base *state.StateDB, checkLive bool, preferDisk bool) (statedb *state.StateDB, err error) { var ( current *types.Block @@ -131,7 +131,9 @@ func (eth *Ethereum) StateAtBlock(block *types.Block, reexec uint64, base *state if current = eth.blockchain.GetBlockByNumber(next); current == nil { return nil, fmt.Errorf("block #%d not found", next) } - _, _, _, err := eth.blockchain.Processor().Process(current, statedb, vm.Config{}) + + _, _, _, err := eth.blockchain.Processor().Process(current, statedb, vm.Config{}, nil) + if err != nil { return nil, fmt.Errorf("processing block %d failed: %v", current.NumberU64(), err) } From 48c1f9078dd8dad20081047463f0335850db787d Mon Sep 17 00:00:00 2001 From: Jerry Date: Fri, 14 Apr 2023 10:29:59 -0700 Subject: [PATCH 26/38] Allow parallel state processor to be interrupted by caller --- core/blockstm/executor.go | 28 ++++++++++++++++++---------- core/blockstm/executor_test.go | 4 ++-- core/parallel_state_processor.go | 4 ++-- 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/core/blockstm/executor.go b/core/blockstm/executor.go index 4ead604d08..0e7a06771d 100644 --- a/core/blockstm/executor.go +++ b/core/blockstm/executor.go @@ -2,6 +2,7 @@ package blockstm import ( "container/heap" + "context" "fmt" "sync" "time" @@ -369,13 +370,14 @@ func (pe *ParallelExecutor) Prepare() { }(i) } - pe.settleWg.Add(len(pe.tasks)) + pe.settleWg.Add(1) go func() { for t := range pe.chSettle { pe.tasks[t].Settle() - pe.settleWg.Done() } + + pe.settleWg.Done() }() // bootstrap first execution @@ -390,18 +392,15 @@ func (pe *ParallelExecutor) Prepare() { func (pe *ParallelExecutor) Close(wait bool) { close(pe.chTasks) close(pe.chSpeculativeTasks) + close(pe.chSettle) if wait { pe.workerWg.Wait() } - close(pe.chResults) - if wait { pe.settleWg.Wait() } - - close(pe.chSettle) } // nolint: gocognit @@ -412,7 +411,7 @@ func (pe *ParallelExecutor) Step(res *ExecResult) (result ParallelExecutionResul // If the transaction failed when we know it should not fail, this means the transaction itself is // bad (e.g. wrong nonce), and we should exit the execution immediately err = fmt.Errorf("could not apply tx %d [%v]: %w", tx, pe.tasks[tx].Hash(), abortErr.OriginError) - pe.Close(false) + pe.Close(true) return } @@ -582,7 +581,7 @@ func (pe *ParallelExecutor) Step(res *ExecResult) (result ParallelExecutionResul type PropertyCheck func(*ParallelExecutor) error -func executeParallelWithCheck(tasks []ExecTask, profile bool, check PropertyCheck, metadata bool) (result ParallelExecutionResult, err error) { +func executeParallelWithCheck(tasks []ExecTask, profile bool, check PropertyCheck, metadata bool, interruptCtx context.Context) (result ParallelExecutionResult, err error) { if len(tasks) == 0 { return ParallelExecutionResult{MakeTxnInputOutput(len(tasks)), nil, nil, nil}, nil } @@ -591,6 +590,15 @@ func executeParallelWithCheck(tasks []ExecTask, profile bool, check PropertyChec pe.Prepare() for range pe.chResults { + if interruptCtx != nil { + select { + case <-interruptCtx.Done(): + pe.Close(true) + return result, interruptCtx.Err() + default: + } + } + res := pe.resultQueue.Pop().(ExecResult) result, err = pe.Step(&res) @@ -611,6 +619,6 @@ func executeParallelWithCheck(tasks []ExecTask, profile bool, check PropertyChec return } -func ExecuteParallel(tasks []ExecTask, profile bool, metadata bool) (result ParallelExecutionResult, err error) { - return executeParallelWithCheck(tasks, profile, nil, metadata) +func ExecuteParallel(tasks []ExecTask, profile bool, metadata bool, interruptCtx context.Context) (result ParallelExecutionResult, err error) { + return executeParallelWithCheck(tasks, profile, nil, metadata, interruptCtx) } diff --git a/core/blockstm/executor_test.go b/core/blockstm/executor_test.go index 686855b36a..fb1f54ed2a 100644 --- a/core/blockstm/executor_test.go +++ b/core/blockstm/executor_test.go @@ -424,7 +424,7 @@ func runParallel(t *testing.T, tasks []ExecTask, validation PropertyCheck, metad profile := false start := time.Now() - result, err := executeParallelWithCheck(tasks, false, validation, metadata) + result, err := executeParallelWithCheck(tasks, false, validation, metadata, nil) if result.Deps != nil && profile { result.Deps.Report(*result.Stats, func(str string) { fmt.Println(str) }) @@ -458,7 +458,7 @@ func runParallelGetMetadata(t *testing.T, tasks []ExecTask, validation PropertyC t.Helper() // fmt.Println("len(tasks)", len(tasks)) - res, err := executeParallelWithCheck(tasks, true, validation, false) + res, err := executeParallelWithCheck(tasks, true, validation, false, nil) assert.NoError(t, err, "error occur during parallel execution") diff --git a/core/parallel_state_processor.go b/core/parallel_state_processor.go index 4d0de960fa..456f97d691 100644 --- a/core/parallel_state_processor.go +++ b/core/parallel_state_processor.go @@ -376,7 +376,7 @@ func (p *ParallelStateProcessor) Process(block *types.Block, statedb *state.Stat backupStateDB := statedb.Copy() profile := false - result, err := blockstm.ExecuteParallel(tasks, false, metadata) + result, err := blockstm.ExecuteParallel(tasks, false, metadata, interruptCtx) if err == nil && profile { _, weight := result.Deps.LongestPath(*result.Stats) @@ -412,7 +412,7 @@ func (p *ParallelStateProcessor) Process(block *types.Block, statedb *state.Stat t.totalUsedGas = usedGas } - _, err = blockstm.ExecuteParallel(tasks, false, metadata) + _, err = blockstm.ExecuteParallel(tasks, false, metadata, interruptCtx) break } From 5860b8e89cdc8b4ef0a539a8925f8609af006754 Mon Sep 17 00:00:00 2001 From: Jerry Date: Tue, 18 Apr 2023 15:17:48 -0700 Subject: [PATCH 27/38] Create new block context for each parallel task --- core/parallel_state_processor.go | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/core/parallel_state_processor.go b/core/parallel_state_processor.go index 456f97d691..805a89646d 100644 --- a/core/parallel_state_processor.go +++ b/core/parallel_state_processor.go @@ -88,8 +88,7 @@ type ExecutionTask struct { // next k elements in dependencies -> transaction indexes on which transaction i is dependent on dependencies []int - blockContext vm.BlockContext - coinbase common.Address + coinbase common.Address } func (task *ExecutionTask) Execute(mvh *blockstm.MVHashMap, incarnation int) (err error) { @@ -98,7 +97,9 @@ func (task *ExecutionTask) Execute(mvh *blockstm.MVHashMap, incarnation int) (er task.statedb.SetMVHashmap(mvh) task.statedb.SetIncarnation(incarnation) - evm := vm.NewEVM(task.blockContext, vm.TxContext{}, task.statedb, task.config, task.evmConfig) + blockContext := NewEVMBlockContext(task.header, task.blockChain, nil) + + evm := vm.NewEVM(blockContext, vm.TxContext{}, task.statedb, task.config, task.evmConfig) // Create a new context to be used in the EVM environment. txContext := NewEVMTxContext(task.msg) @@ -125,8 +126,8 @@ func (task *ExecutionTask) Execute(mvh *blockstm.MVHashMap, incarnation int) (er reads := task.statedb.MVReadMap() - if _, ok := reads[blockstm.NewSubpathKey(task.blockContext.Coinbase, state.BalancePath)]; ok { - log.Info("Coinbase is in MVReadMap", "address", task.blockContext.Coinbase) + if _, ok := reads[blockstm.NewSubpathKey(blockContext.Coinbase, state.BalancePath)]; ok { + log.Info("Coinbase is in MVReadMap", "address", blockContext.Coinbase) task.shouldRerunWithoutFeeDelay = true } @@ -304,8 +305,6 @@ func (p *ParallelStateProcessor) Process(block *types.Block, statedb *state.Stat metadata = true } - blockContext := NewEVMBlockContext(header, p.bc, nil) - // Iterate over and process the individual transactions for i, tx := range block.Transactions() { msg, err := tx.AsMessage(types.MakeSigner(p.config, header.Number), header.BaseFee) @@ -340,8 +339,6 @@ func (p *ParallelStateProcessor) Process(block *types.Block, statedb *state.Stat receipts: &receipts, allLogs: &allLogs, dependencies: deps[i], - blockContext: blockContext, - coinbase: coinbase, } tasks = append(tasks, task) @@ -365,7 +362,6 @@ func (p *ParallelStateProcessor) Process(block *types.Block, statedb *state.Stat receipts: &receipts, allLogs: &allLogs, dependencies: nil, - blockContext: blockContext, coinbase: coinbase, } @@ -419,7 +415,10 @@ func (p *ParallelStateProcessor) Process(block *types.Block, statedb *state.Stat } if err != nil { - log.Error("blockstm error executing block", "err", err) + if err != context.Canceled { + log.Error("blockstm error executing block", "err", err) + } + return nil, nil, 0, err } From 01fc7e3a5332b5162a852b58825d2f0e5589ad92 Mon Sep 17 00:00:00 2001 From: Jerry Date: Fri, 14 Apr 2023 10:40:17 -0700 Subject: [PATCH 28/38] Run serial and parallel processor at the same time --- core/blockchain.go | 100 ++++++++++++++++++++++++------- core/blockchain_test.go | 26 +++----- core/blockstm/executor.go | 30 ++++++---- core/blockstm/executor_test.go | 61 ++++++++++++++++++- core/parallel_state_processor.go | 10 ++-- 5 files changed, 170 insertions(+), 57 deletions(-) diff --git a/core/blockchain.go b/core/blockchain.go index 92d74a8be7..c485c002a4 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -76,11 +76,13 @@ var ( snapshotStorageReadTimer = metrics.NewRegisteredTimer("chain/snapshot/storage/reads", nil) snapshotCommitTimer = metrics.NewRegisteredTimer("chain/snapshot/commits", nil) - blockImportTimer = metrics.NewRegisteredMeter("chain/imports", nil) - blockInsertTimer = metrics.NewRegisteredTimer("chain/inserts", nil) - blockValidationTimer = metrics.NewRegisteredTimer("chain/validation", nil) - blockExecutionTimer = metrics.NewRegisteredTimer("chain/execution", nil) - blockWriteTimer = metrics.NewRegisteredTimer("chain/write", nil) + blockImportTimer = metrics.NewRegisteredMeter("chain/imports", nil) + blockInsertTimer = metrics.NewRegisteredTimer("chain/inserts", nil) + blockValidationTimer = metrics.NewRegisteredTimer("chain/validation", nil) + blockExecutionTimer = metrics.NewRegisteredTimer("chain/execution", nil) + blockWriteTimer = metrics.NewRegisteredTimer("chain/write", nil) + blockExecutionParallelCounter = metrics.NewRegisteredCounter("chain/execution/parallel", nil) + blockExecutionSerialCounter = metrics.NewRegisteredCounter("chain/execution/serial", nil) blockReorgMeter = metrics.NewRegisteredMeter("chain/reorg/executes", nil) blockReorgAddMeter = metrics.NewRegisteredMeter("chain/reorg/add", nil) @@ -216,12 +218,13 @@ type BlockChain struct { running int32 // 0 if chain is running, 1 when stopped procInterrupt int32 // interrupt signaler for block processing - engine consensus.Engine - validator Validator // Block and state validator interface - prefetcher Prefetcher - processor Processor // Block transaction processor interface - forker *ForkChoice - vmConfig vm.Config + engine consensus.Engine + validator Validator // Block and state validator interface + prefetcher Prefetcher + processor Processor // Block transaction processor interface + parallelProcessor Processor // Parallel block transaction processor interface + forker *ForkChoice + vmConfig vm.Config // Bor related changes borReceiptsCache *lru.Cache // Cache for the most recent bor receipt receipts per block @@ -443,11 +446,73 @@ func NewParallelBlockChain(db ethdb.Database, cacheConfig *CacheConfig, chainCon return nil, err } - bc.processor = NewParallelStateProcessor(chainConfig, bc, engine) + bc.parallelProcessor = NewParallelStateProcessor(chainConfig, bc, engine) return bc, nil } +func (bc *BlockChain) ProcessBlock(block *types.Block, parent *types.Header) (types.Receipts, []*types.Log, uint64, *state.StateDB, error) { + // Process the block using processor and parallelProcessor at the same time, take the one which finishes first, cancel the other, and return the result + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + type Result struct { + receipts types.Receipts + logs []*types.Log + usedGas uint64 + err error + statedb *state.StateDB + counter metrics.Counter + } + + resultChan := make(chan Result, 2) + + processorCount := 0 + + if bc.parallelProcessor != nil { + parallelStatedb, err := state.New(parent.Root, bc.stateCache, bc.snaps) + if err != nil { + return nil, nil, 0, nil, err + } + + processorCount++ + + go func() { + parallelStatedb.StartPrefetcher("chain") + receipts, logs, usedGas, err := bc.parallelProcessor.Process(block, parallelStatedb, bc.vmConfig, ctx) + resultChan <- Result{receipts, logs, usedGas, err, parallelStatedb, blockExecutionParallelCounter} + }() + } + + if bc.processor != nil { + statedb, err := state.New(parent.Root, bc.stateCache, bc.snaps) + if err != nil { + return nil, nil, 0, nil, err + } + + processorCount++ + + go func() { + statedb.StartPrefetcher("chain") + receipts, logs, usedGas, err := bc.processor.Process(block, statedb, bc.vmConfig, ctx) + resultChan <- Result{receipts, logs, usedGas, err, statedb, blockExecutionSerialCounter} + }() + } + + result := <-resultChan + result.counter.Inc(1) + + // Make sure we are not leaking any prefetchers + if processorCount == 2 { + go func() { + second_result := <-resultChan + second_result.statedb.StopPrefetcher() + }() + } + + return result.receipts, result.logs, result.usedGas, result.statedb, result.err +} + // empty returns an indicator whether the blockchain is empty. // Note, it's a special case that we connect a non-empty ancient // database with an empty node, so that we can plugin the ancient @@ -1774,14 +1839,6 @@ func (bc *BlockChain) insertChain(chain types.Blocks, verifySeals, setHead bool) if parent == nil { parent = bc.GetHeader(block.ParentHash(), block.NumberU64()-1) } - statedb, err := state.New(parent.Root, bc.stateCache, bc.snaps) - if err != nil { - return it.index, err - } - - // Enable prefetching to pull in trie node paths while processing transactions - statedb.StartPrefetcher("chain") - activeState = statedb // If we have a followup block, run that against the current state to pre-cache // transactions and probabilistically some of the account/storage trie nodes. @@ -1803,7 +1860,8 @@ func (bc *BlockChain) insertChain(chain types.Blocks, verifySeals, setHead bool) // Process block using the parent state as reference point substart := time.Now() - receipts, logs, usedGas, err := bc.processor.Process(block, statedb, bc.vmConfig, nil) + receipts, logs, usedGas, statedb, err := bc.ProcessBlock(block, parent) + activeState = statedb if err != nil { bc.reportBlock(block, receipts, err) atomic.StoreUint32(&followupInterrupt, 1) diff --git a/core/blockchain_test.go b/core/blockchain_test.go index 3c80ef3718..88093accab 100644 --- a/core/blockchain_test.go +++ b/core/blockchain_test.go @@ -17,7 +17,6 @@ package core import ( - "context" "errors" "fmt" "io/ioutil" @@ -34,7 +33,6 @@ import ( "github.com/ethereum/go-ethereum/consensus/beacon" "github.com/ethereum/go-ethereum/consensus/ethash" "github.com/ethereum/go-ethereum/core/rawdb" - "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto" @@ -125,7 +123,7 @@ func testFork(t *testing.T, blockchain *BlockChain, i, n int, full bool, compara cur := blockchain.CurrentBlock() tdPre = blockchain.GetTd(cur.Hash(), cur.NumberU64()) - if err := testBlockChainImport(blockChainB, blockchain, nil); err != nil { + if err := testBlockChainImport(blockChainB, blockchain); err != nil { t.Fatalf("failed to import forked block chain: %v", err) } @@ -146,7 +144,7 @@ func testFork(t *testing.T, blockchain *BlockChain, i, n int, full bool, compara // testBlockChainImport tries to process a chain of blocks, writing them into // the database if successful. -func testBlockChainImport(chain types.Blocks, blockchain *BlockChain, ctx context.Context) error { +func testBlockChainImport(chain types.Blocks, blockchain *BlockChain) error { for _, block := range chain { // Try and process the block err := blockchain.engine.VerifyHeader(blockchain, block.Header(), true) @@ -160,13 +158,8 @@ func testBlockChainImport(chain types.Blocks, blockchain *BlockChain, ctx contex return err } - statedb, err := state.New(blockchain.GetBlockByHash(block.ParentHash()).Root(), blockchain.stateCache, nil) + receipts, _, usedGas, statedb, err := blockchain.ProcessBlock(block, blockchain.GetBlockByHash(block.ParentHash()).Header()) - if err != nil { - return err - } - - receipts, _, usedGas, err := blockchain.processor.Process(block, statedb, vm.Config{}, ctx) if err != nil { blockchain.reportBlock(block, receipts, err) return err @@ -186,10 +179,12 @@ func testBlockChainImport(chain types.Blocks, blockchain *BlockChain, ctx contex return nil } -func TestBlockChainImportInterrupt(t *testing.T) { +func TestParallelBlockChainImport(t *testing.T) { t.Parallel() db, blockchain, err := newCanonical(ethash.NewFaker(), 10, true) + blockchain.parallelProcessor = NewParallelStateProcessor(blockchain.chainConfig, blockchain, blockchain.engine) + if err != nil { t.Fatalf("failed to make new canonical chain: %v", err) } @@ -198,11 +193,8 @@ func TestBlockChainImportInterrupt(t *testing.T) { blockChainB := makeFakeNonEmptyBlockChain(blockchain.CurrentBlock(), 5, ethash.NewFaker(), db, forkSeed, 5) - ctx, cancel := context.WithCancel(context.Background()) - cancel() - - if err := testBlockChainImport(blockChainB, blockchain, ctx); err != context.Canceled { - t.Errorf("block chain import is not cancelled correctly, got %v, want %v", err, context.Canceled) + if err := testBlockChainImport(blockChainB, blockchain); err == nil { + t.Fatalf("expected error for bad tx") } } @@ -502,7 +494,7 @@ func testBrokenChain(t *testing.T, full bool) { // Create a forked chain, and try to insert with a missing link if full { chain := makeBlockChain(blockchain.CurrentBlock(), 5, ethash.NewFaker(), db, forkSeed)[1:] - if err := testBlockChainImport(chain, blockchain, nil); err == nil { + if err := testBlockChainImport(chain, blockchain); err == nil { t.Errorf("broken block chain not reported") } } else { diff --git a/core/blockstm/executor.go b/core/blockstm/executor.go index 0e7a06771d..9d42153623 100644 --- a/core/blockstm/executor.go +++ b/core/blockstm/executor.go @@ -290,7 +290,7 @@ func NewParallelExecutor(tasks []ExecTask, profile bool, metadata bool) *Paralle } // nolint: gocognit -func (pe *ParallelExecutor) Prepare() { +func (pe *ParallelExecutor) Prepare() error { prevSenderTx := make(map[common.Address]int) for i, t := range pe.tasks { @@ -382,11 +382,16 @@ func (pe *ParallelExecutor) Prepare() { // bootstrap first execution tx := pe.execTasks.takeNextPending() - if tx != -1 { - pe.cntExec++ - pe.chTasks <- ExecVersionView{ver: Version{tx, 0}, et: pe.tasks[tx], mvh: pe.mvh, sender: pe.tasks[tx].Sender()} + if tx == -1 { + return fmt.Errorf("no transaction to execute") } + + pe.cntExec++ + + pe.chTasks <- ExecVersionView{ver: Version{tx, 0}, et: pe.tasks[tx], mvh: pe.mvh, sender: pe.tasks[tx].Sender()} + + return nil } func (pe *ParallelExecutor) Close(wait bool) { @@ -587,16 +592,17 @@ func executeParallelWithCheck(tasks []ExecTask, profile bool, check PropertyChec } pe := NewParallelExecutor(tasks, profile, metadata) - pe.Prepare() + err = pe.Prepare() + + if err != nil { + pe.Close(true) + return + } for range pe.chResults { - if interruptCtx != nil { - select { - case <-interruptCtx.Done(): - pe.Close(true) - return result, interruptCtx.Err() - default: - } + if interruptCtx != nil && interruptCtx.Err() != nil { + pe.Close(true) + return result, interruptCtx.Err() } res := pe.resultQueue.Pop().(ExecResult) diff --git a/core/blockstm/executor_test.go b/core/blockstm/executor_test.go index fb1f54ed2a..511d9c774a 100644 --- a/core/blockstm/executor_test.go +++ b/core/blockstm/executor_test.go @@ -1,6 +1,7 @@ package blockstm import ( + "context" "fmt" "math/big" "math/rand" @@ -457,7 +458,6 @@ func runParallel(t *testing.T, tasks []ExecTask, validation PropertyCheck, metad func runParallelGetMetadata(t *testing.T, tasks []ExecTask, validation PropertyCheck) map[int]map[int]bool { t.Helper() - // fmt.Println("len(tasks)", len(tasks)) res, err := executeParallelWithCheck(tasks, true, validation, false, nil) assert.NoError(t, err, "error occur during parallel execution") @@ -923,3 +923,62 @@ func TestDexScenarioWithMetadata(t *testing.T) { testExecutorCombWithMetadata(t, totalTxs, numReads, numWrites, numNonIO, taskRunner) } + +func TestBreakFromCircularDependency(t *testing.T) { + t.Parallel() + rand.Seed(0) + + tasks := make([]ExecTask, 5) + + for i := range tasks { + tasks[i] = &testExecTask{ + txIdx: i, + dependencies: []int{ + (i + len(tasks) - 1) % len(tasks), + }, + } + } + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + // This should not hang + _, err := ExecuteParallel(tasks, false, true, ctx) + + if err == nil { + t.Error("Expected cancel error") + } +} + +func TestBreakFromPartialCircularDependency(t *testing.T) { + t.Parallel() + rand.Seed(0) + + tasks := make([]ExecTask, 5) + + for i := range tasks { + if i < 3 { + tasks[i] = &testExecTask{ + txIdx: i, + dependencies: []int{ + (i + 2) % 3, + }, + } + } else { + tasks[i] = &testExecTask{ + txIdx: i, + dependencies: []int{}, + } + } + } + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + // This should not hang + _, err := ExecuteParallel(tasks, false, true, ctx) + + if err == nil { + t.Error("Expected cancel error") + } +} diff --git a/core/parallel_state_processor.go b/core/parallel_state_processor.go index 805a89646d..c84427ec4f 100644 --- a/core/parallel_state_processor.go +++ b/core/parallel_state_processor.go @@ -87,8 +87,7 @@ type ExecutionTask struct { // (0 -> delay is not allowed, 1 -> delay is allowed) // next k elements in dependencies -> transaction indexes on which transaction i is dependent on dependencies []int - - coinbase common.Address + coinbase common.Address } func (task *ExecutionTask) Execute(mvh *blockstm.MVHashMap, incarnation int) (err error) { @@ -339,6 +338,7 @@ func (p *ParallelStateProcessor) Process(block *types.Block, statedb *state.Stat receipts: &receipts, allLogs: &allLogs, dependencies: deps[i], + coinbase: coinbase, } tasks = append(tasks, task) @@ -394,6 +394,8 @@ func (p *ParallelStateProcessor) Process(block *types.Block, statedb *state.Stat task := task.(*ExecutionTask) if task.shouldRerunWithoutFeeDelay { shouldDelayFeeCal = false + + statedb.StopPrefetcher() *statedb = *backupStateDB allLogs = []*types.Log{} @@ -415,10 +417,6 @@ func (p *ParallelStateProcessor) Process(block *types.Block, statedb *state.Stat } if err != nil { - if err != context.Canceled { - log.Error("blockstm error executing block", "err", err) - } - return nil, nil, 0, err } From f9171c4946fcfeb62d3aad2282921b7c0feeeba1 Mon Sep 17 00:00:00 2001 From: Jerry Date: Tue, 25 Apr 2023 12:03:04 -0700 Subject: [PATCH 29/38] Add ParallelExecFailedError Parallel execution can fail due to bad dependencies. In the case of bad dependencies, the result of serial execution should be used. --- core/blockchain.go | 13 +++++++++++++ core/blockstm/executor.go | 10 +++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/core/blockchain.go b/core/blockchain.go index c485c002a4..6a44a3acff 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -42,6 +42,7 @@ import ( "github.com/ethereum/go-ethereum/common/prque" "github.com/ethereum/go-ethereum/common/tracing" "github.com/ethereum/go-ethereum/consensus" + "github.com/ethereum/go-ethereum/core/blockstm" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/state/snapshot" @@ -500,6 +501,18 @@ func (bc *BlockChain) ProcessBlock(block *types.Block, parent *types.Header) (ty } result := <-resultChan + + if _, ok := result.err.(blockstm.ParallelExecFailedError); ok { + log.Warn("Parallel state processor failed", "err", result.err) + + // If the parallel processor failed, we will fallback to the serial processor if enabled + if processorCount == 2 { + result.statedb.StopPrefetcher() + result = <-resultChan + processorCount-- + } + } + result.counter.Inc(1) // Make sure we are not leaking any prefetchers diff --git a/core/blockstm/executor.go b/core/blockstm/executor.go index 9d42153623..a685fe0707 100644 --- a/core/blockstm/executor.go +++ b/core/blockstm/executor.go @@ -69,6 +69,14 @@ func (e ErrExecAbortError) Error() string { } } +type ParallelExecFailedError struct { + Msg string +} + +func (e ParallelExecFailedError) Error() string { + return e.Msg +} + type IntHeap []int func (h IntHeap) Len() int { return len(h) } @@ -384,7 +392,7 @@ func (pe *ParallelExecutor) Prepare() error { tx := pe.execTasks.takeNextPending() if tx == -1 { - return fmt.Errorf("no transaction to execute") + return ParallelExecFailedError{"no executable transactions due to bad dependency"} } pe.cntExec++ From c88be5b3b1edfdda8ba2536270d919de38114f50 Mon Sep 17 00:00:00 2001 From: Jerry Date: Fri, 28 Apr 2023 12:34:32 -0700 Subject: [PATCH 30/38] Disable IO tracing --- eth/tracers/api.go | 4 +++- eth/tracers/api_test.go | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/eth/tracers/api.go b/eth/tracers/api.go index 36a2785b6f..198d8623e2 100644 --- a/eth/tracers/api.go +++ b/eth/tracers/api.go @@ -74,6 +74,8 @@ const ( var defaultBorTraceEnabled = newBoolPtr(false) +var allowIOTracing = false // Change this to true to enable IO tracing for debugging + // Backend interface provides the common API services (that are provided by // both full and light clients) with access to necessary functions. type Backend interface { @@ -719,7 +721,7 @@ func (api *API) traceBlock(ctx context.Context, block *types.Block, config *Trac } ioflag := defaultIOFlag - if config != nil && config.IOFlag != nil { + if allowIOTracing && config != nil && config.IOFlag != nil { ioflag = *config.IOFlag } diff --git a/eth/tracers/api_test.go b/eth/tracers/api_test.go index 653b50d349..b7207b5b81 100644 --- a/eth/tracers/api_test.go +++ b/eth/tracers/api_test.go @@ -449,6 +449,8 @@ func TestIOdump(t *testing.T) { } })) + allowIOTracing = true + ioflag := new(bool) *ioflag = true From 82390264bc6119c1d51abef02e81f2786d63ac14 Mon Sep 17 00:00:00 2001 From: Jerry Date: Mon, 1 May 2023 15:32:20 -0700 Subject: [PATCH 31/38] Remove recover from settle --- core/parallel_state_processor.go | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/core/parallel_state_processor.go b/core/parallel_state_processor.go index d5b3beb80e..a5a71753c5 100644 --- a/core/parallel_state_processor.go +++ b/core/parallel_state_processor.go @@ -175,17 +175,6 @@ func (task *ExecutionTask) Dependencies() []int { } func (task *ExecutionTask) Settle() { - defer func() { - if r := recover(); r != nil { - // In some rare cases, ApplyMVWriteSet will panic due to an index out of range error when calculating the - // address hash in sha3 module. Recover from panic and continue the execution. - // After recovery, block receipts or merckle root will be incorrect, but this is fine, because the block - // will be rejected and re-synced. - log.Info("Recovered from error", "Error:", r) - return - } - }() - task.finalStateDB.Prepare(task.tx.Hash(), task.index) coinbaseBalance := task.finalStateDB.GetBalance(task.coinbase) From 01dd019c5e4a22afd13e48e873bd37697115396d Mon Sep 17 00:00:00 2001 From: Jerry Date: Mon, 1 May 2023 21:23:54 -0700 Subject: [PATCH 32/38] Fit linters --- core/state_processor.go | 2 +- eth/tracers/api.go | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/core/state_processor.go b/core/state_processor.go index 09422de357..fac921cd2f 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -88,7 +88,7 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg return nil, nil, 0, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err) } statedb.Prepare(tx.Hash(), i) - receipt, err := applyTransaction(msg, p.config, p.bc, nil, gp, statedb, blockNumber, blockHash, tx, usedGas, vmenv, nil) + receipt, err := applyTransaction(msg, p.config, p.bc, nil, gp, statedb, blockNumber, blockHash, tx, usedGas, vmenv, interruptCtx) if err != nil { return nil, nil, 0, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err) } diff --git a/eth/tracers/api.go b/eth/tracers/api.go index ea4bb77ee4..54f624b0a8 100644 --- a/eth/tracers/api.go +++ b/eth/tracers/api.go @@ -826,6 +826,7 @@ func (api *API) traceBlock(ctx context.Context, block *types.Block, config *Trac if stateSyncPresent && i == len(txs)-1 { if *config.BorTraceEnabled { callmsg := prepareCallMessage(msg) + // nolint : contextcheck if _, err := statefull.ApplyBorMessage(*vmenv, callmsg); err != nil { failed = err break @@ -834,6 +835,7 @@ func (api *API) traceBlock(ctx context.Context, block *types.Block, config *Trac break } } else { + // nolint : contextcheck if _, err := core.ApplyMessage(vmenv, msg, new(core.GasPool).AddGas(msg.Gas()), context.Background()); err != nil { failed = err break @@ -844,7 +846,7 @@ func (api *API) traceBlock(ctx context.Context, block *types.Block, config *Trac } } else { coinbaseBalance := statedb.GetBalance(blockCtx.Coinbase) - + // nolint : contextcheck result, err := core.ApplyMessageNoFeeBurnOrTip(vmenv, msg, new(core.GasPool).AddGas(msg.Gas()), context.Background()) if err != nil { From badb574b255ffc2463bf83686a0ff7acc53fa4dc Mon Sep 17 00:00:00 2001 From: Jerry Date: Wed, 3 May 2023 09:46:24 -0700 Subject: [PATCH 33/38] Skip looking up interruptedTxCache if it is not initialized --- core/vm/interpreter.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/core/vm/interpreter.go b/core/vm/interpreter.go index 398be75971..04184b95f7 100644 --- a/core/vm/interpreter.go +++ b/core/vm/interpreter.go @@ -278,6 +278,10 @@ func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool, i txHash, _ := GetCurrentTxFromContext(interruptCtx) interruptedTxCache, _ := GetCache(interruptCtx) + if interruptedTxCache == nil { + break + } + // if the tx is already in the cache, it means that it has been interrupted before and we will not interrupt it again found, _ := interruptedTxCache.Cache.ContainsOrAdd(txHash, true) if found { @@ -437,6 +441,11 @@ func (in *EVMInterpreter) RunWithDelay(contract *Contract, input []byte, readOnl case <-interruptCtx.Done(): txHash, _ := GetCurrentTxFromContext(interruptCtx) interruptedTxCache, _ := GetCache(interruptCtx) + + if interruptedTxCache == nil { + break + } + // if the tx is already in the cache, it means that it has been interrupted before and we will not interrupt it again found, _ := interruptedTxCache.Cache.ContainsOrAdd(txHash, true) log.Info("FOUND", "found", found, "txHash", txHash) From 193aee0063582c48eea798f18f82b50cc509897b Mon Sep 17 00:00:00 2001 From: Jerry Date: Thu, 4 May 2023 11:36:55 -0700 Subject: [PATCH 34/38] Make block context sharable in parallel state processor --- core/evm.go | 6 ++++++ core/parallel_state_processor.go | 13 ++++++++----- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/core/evm.go b/core/evm.go index bb3afc0006..0adc0ac27e 100644 --- a/core/evm.go +++ b/core/evm.go @@ -18,6 +18,7 @@ package core import ( "math/big" + "sync" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/consensus" @@ -83,7 +84,12 @@ func GetHashFn(ref *types.Header, chain ChainContext) func(n uint64) common.Hash // Then fill up with [refHash.p, refHash.pp, refHash.ppp, ...] var cache []common.Hash + cacheMutex := &sync.Mutex{} + return func(n uint64) common.Hash { + cacheMutex.Lock() + defer cacheMutex.Unlock() + // If there's no hash cache yet, make one if len(cache) == 0 { cache = append(cache, ref.ParentHash) diff --git a/core/parallel_state_processor.go b/core/parallel_state_processor.go index a5a71753c5..9eb56c766f 100644 --- a/core/parallel_state_processor.go +++ b/core/parallel_state_processor.go @@ -88,6 +88,7 @@ type ExecutionTask struct { // next k elements in dependencies -> transaction indexes on which transaction i is dependent on dependencies []int coinbase common.Address + blockContext vm.BlockContext } func (task *ExecutionTask) Execute(mvh *blockstm.MVHashMap, incarnation int) (err error) { @@ -96,9 +97,7 @@ func (task *ExecutionTask) Execute(mvh *blockstm.MVHashMap, incarnation int) (er task.statedb.SetMVHashmap(mvh) task.statedb.SetIncarnation(incarnation) - blockContext := NewEVMBlockContext(task.header, task.blockChain, nil) - - evm := vm.NewEVM(blockContext, vm.TxContext{}, task.statedb, task.config, task.evmConfig) + evm := vm.NewEVM(task.blockContext, vm.TxContext{}, task.statedb, task.config, task.evmConfig) // Create a new context to be used in the EVM environment. txContext := NewEVMTxContext(task.msg) @@ -125,8 +124,8 @@ func (task *ExecutionTask) Execute(mvh *blockstm.MVHashMap, incarnation int) (er reads := task.statedb.MVReadMap() - if _, ok := reads[blockstm.NewSubpathKey(blockContext.Coinbase, state.BalancePath)]; ok { - log.Info("Coinbase is in MVReadMap", "address", blockContext.Coinbase) + if _, ok := reads[blockstm.NewSubpathKey(task.blockContext.Coinbase, state.BalancePath)]; ok { + log.Info("Coinbase is in MVReadMap", "address", task.blockContext.Coinbase) task.shouldRerunWithoutFeeDelay = true } @@ -293,6 +292,8 @@ func (p *ParallelStateProcessor) Process(block *types.Block, statedb *state.Stat metadata = true } + blockContext := NewEVMBlockContext(header, p.bc, nil) + // Iterate over and process the individual transactions for i, tx := range block.Transactions() { msg, err := tx.AsMessage(types.MakeSigner(p.config, header.Number), header.BaseFee) @@ -328,6 +329,7 @@ func (p *ParallelStateProcessor) Process(block *types.Block, statedb *state.Stat allLogs: &allLogs, dependencies: deps[i], coinbase: coinbase, + blockContext: blockContext, } tasks = append(tasks, task) @@ -352,6 +354,7 @@ func (p *ParallelStateProcessor) Process(block *types.Block, statedb *state.Stat allLogs: &allLogs, dependencies: nil, coinbase: coinbase, + blockContext: blockContext, } tasks = append(tasks, task) From adce0e823527da26822a4076e1a977c6bc6e6d00 Mon Sep 17 00:00:00 2001 From: Pratik Patil Date: Wed, 10 May 2023 10:33:43 +0530 Subject: [PATCH 35/38] removed PSP comment --- cmd/utils/flags.go | 1 - core/blockstm/status_test.go | 2 -- eth/backend.go | 1 - 3 files changed, 4 deletions(-) diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index d9b2ad4820..1772913c0e 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -2084,7 +2084,6 @@ func MakeChain(ctx *cli.Context, stack *node.Node) (chain *core.BlockChain, chai // TODO(rjl493456442) disable snapshot generation/wiping if the chain is read only. // Disable transaction indexing/unindexing by default. - // PSP - Check for config.ParallelEVM.Enable here? chain, err = core.NewBlockChain(chainDb, cache, config, engine, vmcfg, nil, nil, nil) if err != nil { Fatalf("Can't create BlockChain: %v", err) diff --git a/core/blockstm/status_test.go b/core/blockstm/status_test.go index d76ebaba04..aff00d9a2f 100644 --- a/core/blockstm/status_test.go +++ b/core/blockstm/status_test.go @@ -39,8 +39,6 @@ func TestStatusBasics(t *testing.T) { s.markComplete(x) require.False(t, s.checkInProgress(4)) - // PSP - is this correct? {s.maxAllComplete() -> 2} - // s -> {[5 6 7 8 9] [3] [0 1 2 4] map[] map[]} require.Equal(t, 2, s.maxAllComplete(), "zero should still be min complete") exp := []int{1, 2} diff --git a/eth/backend.go b/eth/backend.go index 2d4353d68b..10386bfb6b 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -108,7 +108,6 @@ type Ethereum struct { shutdownTracker *shutdowncheck.ShutdownTracker // Tracks if and when the node has shutdown ungracefully } -// PSP // New creates a new Ethereum object (including the // initialisation of the common Ethereum object) func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { From c39c66fb9c1b10fa3021893255e0b5b10f403f67 Mon Sep 17 00:00:00 2001 From: Pratik Patil Date: Thu, 11 May 2023 12:20:58 +0530 Subject: [PATCH 36/38] Added check for circular and out-of-range dependency problem (#841) * added check for circular and out-of-range dependency problem * addressed comment * addressed comments --- core/parallel_state_processor.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/core/parallel_state_processor.go b/core/parallel_state_processor.go index 9eb56c766f..e72fb4e6d8 100644 --- a/core/parallel_state_processor.go +++ b/core/parallel_state_processor.go @@ -290,6 +290,11 @@ func (p *ParallelStateProcessor) Process(block *types.Block, statedb *state.Stat if block.Header().TxDependency != nil { metadata = true + + if !VerifyDeps(deps) { + metadata = false + deps = GetDeps([][]uint64{}) + } } blockContext := NewEVMBlockContext(header, p.bc, nil) @@ -431,3 +436,20 @@ func GetDeps(txDependency [][]uint64) map[int][]int { return deps } + +// returns true if dependencies are correct +func VerifyDeps(deps map[int][]int) bool { + // number of transactions in the block + n := len(deps) + + // Handle out-of-range and circular dependency problem + for tx, val := range deps { + for depTx := range val { + if depTx >= n || depTx < tx { + return false + } + } + } + + return true +} From f2c48fed0be8f2ab8ab7a80bfd239b1792c0540f Mon Sep 17 00:00:00 2001 From: Jerry Date: Thu, 11 May 2023 09:09:01 -0700 Subject: [PATCH 37/38] Remove dependency when a transaction is reverted If a transaction is reverted, its write records should be excluded from dependency calculation. --- core/blockstm/executor.go | 5 ++- core/blockstm/mvhashmap.go | 7 ++++ core/blockstm/txio.go | 12 ++++++ core/parallel_state_processor.go | 32 ++------------- core/state/journal.go | 16 ++++---- core/state/statedb.go | 67 ++++++++++++-------------------- 6 files changed, 58 insertions(+), 81 deletions(-) diff --git a/core/blockstm/executor.go b/core/blockstm/executor.go index a685fe0707..7ce99f9492 100644 --- a/core/blockstm/executor.go +++ b/core/blockstm/executor.go @@ -556,11 +556,14 @@ func (pe *ParallelExecutor) Step(res *ExecResult) (result ParallelExecutionResul var allDeps map[int]map[int]bool + var deps DAG + if pe.profile { allDeps = GetDep(*pe.lastTxIO) + deps = BuildDAG(*pe.lastTxIO) } - return ParallelExecutionResult{pe.lastTxIO, &pe.stats, nil, allDeps}, err + return ParallelExecutionResult{pe.lastTxIO, &pe.stats, &deps, allDeps}, err } // Send the next immediate pending transaction to be executed diff --git a/core/blockstm/mvhashmap.go b/core/blockstm/mvhashmap.go index 2a517bcc84..fdaece261f 100644 --- a/core/blockstm/mvhashmap.go +++ b/core/blockstm/mvhashmap.go @@ -269,6 +269,13 @@ func ValidateVersion(txIdx int, lastInputOutput *TxnInputOutput, versionedData * mvResult := versionedData.Read(rd.Path, txIdx) switch mvResult.Status() { case MVReadResultDone: + // Having a write record for a path in MVHashmap doesn't necessarily mean there is a conflict, because MVHashmap + // is a superset of the actual write set. + // Check if the write record is actually in write set. If not, skip the key. + if mvResult.depIdx >= 0 && !lastInputOutput.HasWritten(mvResult.depIdx, rd.Path) { + continue + } + valid = rd.Kind == ReadKindMap && rd.V == Version{ TxnIndex: mvResult.depIdx, Incarnation: mvResult.incarnation, diff --git a/core/blockstm/txio.go b/core/blockstm/txio.go index 22541e0a78..19955fb152 100644 --- a/core/blockstm/txio.go +++ b/core/blockstm/txio.go @@ -46,6 +46,7 @@ func (txo TxnOutput) hasNewWrite(cmpSet []WriteDescriptor) bool { type TxnInputOutput struct { inputs []TxnInput outputs []TxnOutput // write sets that should be checked during validation + outputsSet []map[Key]struct{} allOutputs []TxnOutput // entire write sets in MVHashMap. allOutputs should always be a parent set of outputs } @@ -61,10 +62,16 @@ func (io *TxnInputOutput) AllWriteSet(txnIdx int) []WriteDescriptor { return io.allOutputs[txnIdx] } +func (io *TxnInputOutput) HasWritten(txnIdx int, k Key) bool { + _, ok := io.outputsSet[txnIdx][k] + return ok +} + func MakeTxnInputOutput(numTx int) *TxnInputOutput { return &TxnInputOutput{ inputs: make([]TxnInput, numTx), outputs: make([]TxnOutput, numTx), + outputsSet: make([]map[Key]struct{}, numTx), allOutputs: make([]TxnOutput, numTx), } } @@ -75,6 +82,11 @@ func (io *TxnInputOutput) recordRead(txId int, input []ReadDescriptor) { func (io *TxnInputOutput) recordWrite(txId int, output []WriteDescriptor) { io.outputs[txId] = output + io.outputsSet[txId] = make(map[Key]struct{}, len(output)) + + for _, v := range output { + io.outputsSet[txId][v.Path] = struct{}{} + } } func (io *TxnInputOutput) recordAllWrite(txId int, output []WriteDescriptor) { diff --git a/core/parallel_state_processor.go b/core/parallel_state_processor.go index e72fb4e6d8..8956f6f4d2 100644 --- a/core/parallel_state_processor.go +++ b/core/parallel_state_processor.go @@ -178,7 +178,7 @@ func (task *ExecutionTask) Settle() { coinbaseBalance := task.finalStateDB.GetBalance(task.coinbase) - task.finalStateDB.ApplyMVWriteSet(task.statedb.MVWriteList()) + task.finalStateDB.ApplyMVWriteSet(task.statedb.MVFullWriteList()) for _, l := range task.statedb.GetLogs(task.tx.Hash(), task.blockHash) { task.finalStateDB.AddLog(l) @@ -290,11 +290,6 @@ func (p *ParallelStateProcessor) Process(block *types.Block, statedb *state.Stat if block.Header().TxDependency != nil { metadata = true - - if !VerifyDeps(deps) { - metadata = false - deps = GetDeps([][]uint64{}) - } } blockContext := NewEVMBlockContext(header, p.bc, nil) @@ -369,9 +364,9 @@ func (p *ParallelStateProcessor) Process(block *types.Block, statedb *state.Stat backupStateDB := statedb.Copy() profile := false - result, err := blockstm.ExecuteParallel(tasks, false, metadata, interruptCtx) + result, err := blockstm.ExecuteParallel(tasks, profile, metadata, interruptCtx) - if err == nil && profile { + if err == nil && profile && result.Deps != nil { _, weight := result.Deps.LongestPath(*result.Stats) serialWeight := uint64(0) @@ -381,10 +376,6 @@ func (p *ParallelStateProcessor) Process(block *types.Block, statedb *state.Stat } parallelizabilityTimer.Update(time.Duration(serialWeight * 100 / weight)) - - log.Info("Parallelizability", "Average (%)", parallelizabilityTimer.Mean()) - - log.Info("Parallelizability", "Histogram (%)", parallelizabilityTimer.Percentiles([]float64{0.001, 0.01, 0.05, 0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99, 0.999, 0.9999})) } for _, task := range tasks { @@ -436,20 +427,3 @@ func GetDeps(txDependency [][]uint64) map[int][]int { return deps } - -// returns true if dependencies are correct -func VerifyDeps(deps map[int][]int) bool { - // number of transactions in the block - n := len(deps) - - // Handle out-of-range and circular dependency problem - for tx, val := range deps { - for depTx := range val { - if depTx >= n || depTx < tx { - return false - } - } - } - - return true -} diff --git a/core/state/journal.go b/core/state/journal.go index 79a4e35422..2a0c81dcb6 100644 --- a/core/state/journal.go +++ b/core/state/journal.go @@ -144,7 +144,7 @@ type ( func (ch createObjectChange) revert(s *StateDB) { delete(s.stateObjects, *ch.account) delete(s.stateObjectsDirty, *ch.account) - MVWrite(s, blockstm.NewAddressKey(*ch.account)) + RevertWrite(s, blockstm.NewAddressKey(*ch.account)) } func (ch createObjectChange) dirtied() *common.Address { @@ -153,7 +153,7 @@ func (ch createObjectChange) dirtied() *common.Address { func (ch resetObjectChange) revert(s *StateDB) { s.setStateObject(ch.prev) - MVWrite(s, blockstm.NewAddressKey(ch.prev.address)) + RevertWrite(s, blockstm.NewAddressKey(ch.prev.address)) if !ch.prevdestruct && s.snap != nil { delete(s.snapDestructs, ch.prev.addrHash) } @@ -168,8 +168,8 @@ func (ch suicideChange) revert(s *StateDB) { if obj != nil { obj.suicided = ch.prev obj.setBalance(ch.prevbalance) - MVWrite(s, blockstm.NewSubpathKey(*ch.account, SuicidePath)) - MVWrite(s, blockstm.NewSubpathKey(*ch.account, BalancePath)) + RevertWrite(s, blockstm.NewSubpathKey(*ch.account, SuicidePath)) + RevertWrite(s, blockstm.NewSubpathKey(*ch.account, BalancePath)) } } @@ -188,7 +188,7 @@ func (ch touchChange) dirtied() *common.Address { func (ch balanceChange) revert(s *StateDB) { s.getStateObject(*ch.account).setBalance(ch.prev) - MVWrite(s, blockstm.NewSubpathKey(*ch.account, BalancePath)) + RevertWrite(s, blockstm.NewSubpathKey(*ch.account, BalancePath)) } func (ch balanceChange) dirtied() *common.Address { @@ -197,7 +197,7 @@ func (ch balanceChange) dirtied() *common.Address { func (ch nonceChange) revert(s *StateDB) { s.getStateObject(*ch.account).setNonce(ch.prev) - MVWrite(s, blockstm.NewSubpathKey(*ch.account, NoncePath)) + RevertWrite(s, blockstm.NewSubpathKey(*ch.account, NoncePath)) } func (ch nonceChange) dirtied() *common.Address { @@ -206,7 +206,7 @@ func (ch nonceChange) dirtied() *common.Address { func (ch codeChange) revert(s *StateDB) { s.getStateObject(*ch.account).setCode(common.BytesToHash(ch.prevhash), ch.prevcode) - MVWrite(s, blockstm.NewSubpathKey(*ch.account, CodePath)) + RevertWrite(s, blockstm.NewSubpathKey(*ch.account, CodePath)) } func (ch codeChange) dirtied() *common.Address { @@ -215,7 +215,7 @@ func (ch codeChange) dirtied() *common.Address { func (ch storageChange) revert(s *StateDB) { s.getStateObject(*ch.account).setState(ch.key, ch.prevalue) - MVWrite(s, blockstm.NewStateKey(*ch.account, ch.key)) + RevertWrite(s, blockstm.NewStateKey(*ch.account, ch.key)) } func (ch storageChange) dirtied() *common.Address { diff --git a/core/state/statedb.go b/core/state/statedb.go index b77bd9d763..881ea8c110 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -81,12 +81,12 @@ type StateDB struct { stateObjectsDirty map[common.Address]struct{} // State objects modified in the current execution // Block-stm related fields - mvHashmap *blockstm.MVHashMap - incarnation int - readMap map[blockstm.Key]blockstm.ReadDescriptor - writeMap map[blockstm.Key]blockstm.WriteDescriptor - newStateObjects map[common.Address]struct{} - dep int + mvHashmap *blockstm.MVHashMap + incarnation int + readMap map[blockstm.Key]blockstm.ReadDescriptor + writeMap map[blockstm.Key]blockstm.WriteDescriptor + revertedKeys map[blockstm.Key]struct{} + dep int // DB error. // State objects are used by the consensus core and VM which are @@ -147,7 +147,7 @@ func New(root common.Hash, db Database, snaps *snapshot.Tree) (*StateDB, error) stateObjects: make(map[common.Address]*stateObject), stateObjectsPending: make(map[common.Address]struct{}), stateObjectsDirty: make(map[common.Address]struct{}), - newStateObjects: make(map[common.Address]struct{}), + revertedKeys: make(map[blockstm.Key]struct{}), logs: make(map[common.Hash][]*types.Log), preimages: make(map[common.Hash][]byte), journal: newJournal(), @@ -187,7 +187,7 @@ func (s *StateDB) MVWriteList() []blockstm.WriteDescriptor { writes := make([]blockstm.WriteDescriptor, 0, len(s.writeMap)) for _, v := range s.writeMap { - if !v.Path.IsAddress() { + if _, ok := s.revertedKeys[v.Path]; !ok { writes = append(writes, v) } } @@ -212,7 +212,7 @@ func (s *StateDB) MVReadMap() map[blockstm.Key]blockstm.ReadDescriptor { func (s *StateDB) MVReadList() []blockstm.ReadDescriptor { reads := make([]blockstm.ReadDescriptor, 0, len(s.readMap)) - for _, v := range s.readMap { + for _, v := range s.MVReadMap() { reads = append(reads, v) } @@ -268,6 +268,14 @@ func MVRead[T any](s *StateDB, k blockstm.Key, defaultV T, readStorage func(s *S } } + if !k.IsAddress() { + // If we are reading subpath from a deleted account, return default value instead of reading from MVHashmap + addr := k.GetAddress() + if s.getStateObject(addr) == nil { + return defaultV + } + } + res := s.mvHashmap.Read(k, s.txIndex) var rd blockstm.ReadDescriptor @@ -320,6 +328,10 @@ func MVWrite(s *StateDB, k blockstm.Key) { } } +func RevertWrite(s *StateDB, k blockstm.Key) { + s.revertedKeys[k] = struct{}{} +} + func MVWritten(s *StateDB, k blockstm.Key) bool { if s.mvHashmap == nil || s.writeMap == nil { return false @@ -349,6 +361,8 @@ func (sw *StateDB) ApplyMVWriteSet(writes []blockstm.WriteDescriptor) { stateKey := path.GetStateKey() state := sr.GetState(addr, stateKey) sw.SetState(addr, stateKey, state) + } else if path.IsAddress() { + continue } else { addr := path.GetAddress() switch path.GetSubpath() { @@ -543,10 +557,6 @@ const SuicidePath = 4 // 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.getStateObject(addr) == nil { - return common.Big0 - } - return MVRead(s, blockstm.NewSubpathKey(addr, BalancePath), common.Big0, func(s *StateDB) *big.Int { stateObject := s.getStateObject(addr) if stateObject != nil { @@ -558,10 +568,6 @@ func (s *StateDB) GetBalance(addr common.Address) *big.Int { } func (s *StateDB) GetNonce(addr common.Address) uint64 { - if s.getStateObject(addr) == nil { - return 0 - } - return MVRead(s, blockstm.NewSubpathKey(addr, NoncePath), 0, func(s *StateDB) uint64 { stateObject := s.getStateObject(addr) if stateObject != nil { @@ -585,10 +591,6 @@ func (s *StateDB) Version() blockstm.Version { } func (s *StateDB) GetCode(addr common.Address) []byte { - if s.getStateObject(addr) == nil { - return nil - } - return MVRead(s, blockstm.NewSubpathKey(addr, CodePath), nil, func(s *StateDB) []byte { stateObject := s.getStateObject(addr) if stateObject != nil { @@ -599,10 +601,6 @@ func (s *StateDB) GetCode(addr common.Address) []byte { } func (s *StateDB) GetCodeSize(addr common.Address) int { - if s.getStateObject(addr) == nil { - return 0 - } - return MVRead(s, blockstm.NewSubpathKey(addr, CodePath), 0, func(s *StateDB) int { stateObject := s.getStateObject(addr) if stateObject != nil { @@ -613,10 +611,6 @@ func (s *StateDB) GetCodeSize(addr common.Address) int { } func (s *StateDB) GetCodeHash(addr common.Address) common.Hash { - if s.getStateObject(addr) == nil { - return common.Hash{} - } - return MVRead(s, blockstm.NewSubpathKey(addr, CodePath), common.Hash{}, func(s *StateDB) common.Hash { stateObject := s.getStateObject(addr) if stateObject == nil { @@ -628,10 +622,6 @@ 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.getStateObject(addr) == nil { - return common.Hash{} - } - return MVRead(s, blockstm.NewStateKey(addr, hash), common.Hash{}, func(s *StateDB) common.Hash { stateObject := s.getStateObject(addr) if stateObject != nil { @@ -666,10 +656,6 @@ func (s *StateDB) GetStorageProof(a common.Address, key common.Hash) ([][]byte, // 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.getStateObject(addr) == nil { - return common.Hash{} - } - return MVRead(s, blockstm.NewStateKey(addr, hash), common.Hash{}, func(s *StateDB) common.Hash { stateObject := s.getStateObject(addr) if stateObject != nil { @@ -697,10 +683,6 @@ func (s *StateDB) StorageTrie(addr common.Address) Trie { } func (s *StateDB) HasSuicided(addr common.Address) bool { - if s.getStateObject(addr) == nil { - return false - } - return MVRead(s, blockstm.NewSubpathKey(addr, SuicidePath), false, func(s *StateDB) bool { stateObject := s.getStateObject(addr) if stateObject != nil { @@ -982,7 +964,6 @@ func (s *StateDB) createObject(addr common.Address) (newobj, prev *stateObject) s.journal.append(resetObjectChange{prev: prev, prevdestruct: prevdestruct}) } s.setStateObject(newobj) - s.newStateObjects[addr] = struct{}{} MVWrite(s, blockstm.NewAddressKey(addr)) if prev != nil && !prev.deleted { @@ -1048,7 +1029,7 @@ func (s *StateDB) Copy() *StateDB { 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)), - newStateObjects: make(map[common.Address]struct{}, len(s.newStateObjects)), + revertedKeys: make(map[blockstm.Key]struct{}), refund: s.refund, logs: make(map[common.Hash][]*types.Log, len(s.logs)), logSize: s.logSize, From 0a7594b17ac872328806afb9c9e7181e3b5e5a34 Mon Sep 17 00:00:00 2001 From: Pratik Patil Date: Fri, 12 May 2023 11:51:01 +0530 Subject: [PATCH 38/38] removed docs/config.md --- docs/config.md | 150 ------------------------------------------------- 1 file changed, 150 deletions(-) delete mode 100644 docs/config.md diff --git a/docs/config.md b/docs/config.md deleted file mode 100644 index ebec217b96..0000000000 --- a/docs/config.md +++ /dev/null @@ -1,150 +0,0 @@ - -# Config - -- The `bor dumpconfig` command prints the default configurations, in the TOML format, on the terminal. - - One can `pipe (>)` this to a file (say `config.toml`) and use it to start bor. - - Command to provide a config file: `bor server -config config.toml` -- Bor uses TOML, HCL, and JSON format config files. -- This is the format of the config file in TOML: - - **NOTE: The values of these following flags are just for reference** - - `config.toml` file: -``` -chain = "mainnet" -identity = "myIdentity" -log-level = "INFO" -datadir = "/var/lib/bor/data" -keystore = "path/to/keystore" -syncmode = "full" -gcmode = "full" -snapshot = true -ethstats = "" - -["eth.requiredblocks"] - -[p2p] -maxpeers = 50 -maxpendpeers = 50 -bind = "0.0.0.0" -port = 30303 -nodiscover = false -nat = "any" - -[p2p.discovery] -v5disc = false -bootnodes = ["enode://d860a01f9722d78051619d1e2351aba3f43f943f6f00718d1b9baa4101932a1f5011f16bb2b1bb35db20d6fe28fa0bf09636d26a87d31de9ec6203eeedb1f666@18.138.108.67:30303", "enode://22a8232c3abc76a16ae9d6c3b164f98775fe226f0917b0ca871128a74a8e9630b458460865bab457221f1d448dd9791d24c4e5d88786180ac185df813a68d4de@3.209.45.79:30303"] -bootnodesv4 = [] -bootnodesv5 = ["enr:-KG4QOtcP9X1FbIMOe17QNMKqDxCpm14jcX5tiOE4_TyMrFqbmhPZHK_ZPG2Gxb1GE2xdtodOfx9-cgvNtxnRyHEmC0ghGV0aDKQ9aX9QgAAAAD__________4JpZIJ2NIJpcIQDE8KdiXNlY3AyNTZrMaEDhpehBDbZjM_L9ek699Y7vhUJ-eAdMyQW_Fil522Y0fODdGNwgiMog3VkcIIjKA", "enr:-KG4QDyytgmE4f7AnvW-ZaUOIi9i79qX4JwjRAiXBZCU65wOfBu-3Nb5I7b_Rmg3KCOcZM_C3y5pg7EBU5XGrcLTduQEhGV0aDKQ9aX9QgAAAAD__________4JpZIJ2NIJpcIQ2_DUbiXNlY3AyNTZrMaEDKnz_-ps3UUOfHWVYaskI5kWYO_vtYMGYCQRAR3gHDouDdGNwgiMog3VkcIIjKA"] -static-nodes = ["enode://8499da03c47d637b20eee24eec3c356c9a2e6148d6fe25ca195c7949ab8ec2c03e3556126b0d7ed644675e78c4318b08691b7b57de10e5f0d40d05b09238fa0a@52.187.207.27:30303"] -trusted-nodes = ["enode://2b252ab6a1d0f971d9722cb839a42cb81db019ba44c08754628ab4a823487071b5695317c8ccd085219c3a03af063495b2f1da8d18218da2d6a82981b45e6ffc@65.108.70.101:30303"] -dns = [] - -[heimdall] -url = "http://localhost:1317" -"bor.without" = false - -[txpool] -locals = ["$ADDRESS1", "$ADDRESS2"] -nolocals = false -journal = "" -rejournal = "1h0m0s" -pricelimit = 30000000000 -pricebump = 10 -accountslots = 16 -globalslots = 32768 -accountqueue = 16 -globalqueue = 32768 -lifetime = "3h0m0s" - -[miner] -mine = false -etherbase = "" -extradata = "" -gaslimit = 20000000 -gasprice = "30000000000" - -[jsonrpc] -ipcdisable = false -ipcpath = "/var/lib/bor/bor.ipc" -gascap = 50000000 -txfeecap = 5e+00 - -[jsonrpc.http] -enabled = false -port = 8545 -prefix = "" -host = "localhost" -api = ["eth", "net", "web3", "txpool", "bor"] -vhosts = ["*"] -corsdomain = ["*"] - -[jsonrpc.ws] -enabled = false -port = 8546 -prefix = "" -host = "localhost" -api = ["web3", "net"] -vhosts = ["*"] -corsdomain = ["*"] - -[jsonrpc.graphql] -enabled = false -port = 0 -prefix = "" -host = "" -api = [] -vhosts = ["*"] -corsdomain = ["*"] - -[gpo] -blocks = 20 -percentile = 60 -maxprice = "5000000000000" -ignoreprice = "2" - -[telemetry] -metrics = false -expensive = false -prometheus-addr = "" -opencollector-endpoint = "" - -[telemetry.influx] -influxdb = false -endpoint = "" -database = "" -username = "" -password = "" -influxdbv2 = false -token = "" -bucket = "" -organization = "" - -[cache] -cache = 1024 -gc = 25 -snapshot = 10 -database = 50 -trie = 15 -journal = "triecache" -rejournal = "1h0m0s" -noprefetch = false -preimages = false -txlookuplimit = 2350000 - -[accounts] -unlock = ["$ADDRESS1", "$ADDRESS2"] -password = "path/to/password.txt" -allow-insecure-unlock = false -lightkdf = false -disable-bor-wallet = false - -[grpc] -addr = ":3131" - -[developer] -dev = false -period = 0 - -[blockstm] -enable = false -procs = 8 -```