diff --git a/builder/files/config.toml b/builder/files/config.toml index f2ed492279..dc97b5e51e 100644 --- a/builder/files/config.toml +++ b/builder/files/config.toml @@ -171,6 +171,10 @@ syncmode = "full" # period = 0 # gaslimit = 11500000 +# [parallelevm] + # enable = false + # procs = 8 + # [pprof] # pprof = false # port = 6060 diff --git a/core/blockchain.go b/core/blockchain.go index 680cb7dce6..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" @@ -76,11 +77,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 +219,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 @@ -435,6 +439,93 @@ 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.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 + + 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 + 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 @@ -1761,14 +1852,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. @@ -1790,7 +1873,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) + 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 fa6b61225e..88093accab 100644 --- a/core/blockchain_test.go +++ b/core/blockchain_test.go @@ -33,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" @@ -123,9 +122,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 { t.Fatalf("failed to import forked block chain: %v", err) } + last := blockChainB[len(blockChainB)-1] tdPost = blockchain.GetTd(last.Hash(), last.NumberU64()) } else { @@ -156,11 +157,9 @@ 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, statedb, err := blockchain.ProcessBlock(block, blockchain.GetBlockByHash(block.ParentHash()).Header()) + if err != nil { blockchain.reportBlock(block, receipts, err) return err @@ -180,6 +179,25 @@ func testBlockChainImport(chain types.Blocks, blockchain *BlockChain) error { return nil } +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) + } + + defer blockchain.Stop() + + blockChainB := makeFakeNonEmptyBlockChain(blockchain.CurrentBlock(), 5, ethash.NewFaker(), db, forkSeed, 5) + + if err := testBlockChainImport(blockChainB, blockchain); err == nil { + t.Fatalf("expected error for bad tx") + } +} + // testHeaderChainImport tries to process a chain of header, writing them into // the database if successful. func testHeaderChainImport(chain []*types.Header, blockchain *BlockChain) error { diff --git a/core/blockstm/dag.go b/core/blockstm/dag.go new file mode 100644 index 0000000000..47bd0685a3 --- /dev/null +++ b/core/blockstm/dag.go @@ -0,0 +1,201 @@ +package blockstm + +import ( + "fmt" + "strings" + "time" + + "github.com/heimdalr/dag" + + "github.com/ethereum/go-ethereum/log" +) + +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) + + 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) + } + } + } + } + + 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())) + + for i := 0; i < len(d.GetVertices()); i++ { + prev[i] = -1 + } + + pathWeights := make(map[int]uint64, len(d.GetVertices())) + + maxPath := 0 + maxPathWeight := uint64(0) + + idxToId := make(map[int]string, len(d.GetVertices())) + + for k, i := range d.GetVertices() { + idxToId[i.(int)] = k + } + + 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 { + ret = append(ret, fmt.Sprint(v)) + } + + return + } + + out("Longest execution path:") + out(fmt.Sprintf("(%v) %v", len(longestPath), strings.Join(makeStrs(longestPath), "->"))) + + 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 new file mode 100644 index 0000000000..7ce99f9492 --- /dev/null +++ b/core/blockstm/executor.go @@ -0,0 +1,641 @@ +package blockstm + +import ( + "container/heap" + "context" + "fmt" + "sync" + "time" + + "github.com/ethereum/go-ethereum/common" + "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 + Hash() common.Hash + Sender() common.Address + Settle() + Dependencies() []int +} + +type ExecVersionView struct { + ver Version + et ExecTask + mvh *MVHashMap + 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 { + return + } + + er.txIn = ev.et.MVReadList() + er.txOut = ev.et.MVWriteList() + er.txAllOut = ev.et.MVFullWriteList() + + return +} + +type ErrExecAbortError struct { + Dependency int + OriginError error +} + +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 ParallelExecFailedError struct { + Msg string +} + +func (e ParallelExecFailedError) Error() string { + return e.Msg +} + +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 +} + +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 + 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 *map[int]ExecutionStat + Deps *DAG + AllDeps map[int]map[int]bool +} + +const numGoProcs = 1 + +type ParallelExecutor struct { + tasks []ExecTask + + // 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 + chTasks chan ExecVersionView + + // Channel for speculative tasks + chSpeculativeTasks chan struct{} + + // Channel to signal that the result of a transaction could be written to storage + specTaskQueue SafeQueue + + // A priority queue that stores speculative tasks + chSettle chan int + + // Channel to signal that a transaction has finished executing + chResults chan struct{} + + // A priority queue that stores the transaction index of results, so we can validate the results in order + resultQueue SafeQueue + + // A wait group to wait for all settling tasks to finish + settleWg sync.WaitGroup + + // An integer that tracks the index of last settled transaction + 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 map[int]bool + + // Execution tasks stores the state of each execution task + execTasks taskStatusManager + + // Validate tasks stores the state of each validation task + validateTasks taskStatusManager + + // Stats for debugging purposes + cntExec, cntSuccess, cntAbort, cntTotalValidations, cntValidationFail int + + diagExecSuccess, diagExecAbort []int + + // Multi-version hash map + mvh *MVHashMap + + // Stores the inputs and outputs of the last incardanotion of all transactions + lastTxIO *TxnInputOutput + + // Tracks the incarnation number of each transaction + txIncarnations []int + + // A map that stores the estimated dependency of a transaction if it is aborted without any known dependency + estimateDeps map[int][]int + + // A map that records whether a transaction result has been speculatively validated + preValidated map[int]bool + + // Time records when the parallel execution starts + begin time.Time + + // Enable profiling + profile bool + + // Worker wait group + workerWg sync.WaitGroup +} + +type ExecutionStat struct { + TxIdx int + Incarnation int + Start uint64 + End uint64 + Worker int +} + +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), + chTasks: make(chan ExecVersionView, numTasks), + chSpeculativeTasks: make(chan struct{}, numTasks), + chSettle: make(chan int, numTasks), + chResults: make(chan struct{}, numTasks), + specTaskQueue: specTaskQueue, + resultQueue: resultQueue, + 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 +} + +// nolint: gocognit +func (pe *ParallelExecutor) Prepare() error { + 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 len(t.Dependencies()) > 0 { + for _, val := range t.Dependencies() { + clearPendingFlag = true + + pe.execTasks.addDependencies(val, i) + } + + if clearPendingFlag { + pe.execTasks.clearPending(i) + + clearPendingFlag = false + } + } else { + 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 pe.workerWg.Done() + + doWork := func(task ExecVersionView) { + start := time.Duration(0) + if pe.profile { + start = time.Since(pe.begin) + } + + res := task.Execute() + + if res.err == nil { + pe.mvh.FlushMVWriteSet(res.txAllOut) + } + + pe.resultQueue.Push(res.ver.TxnIndex, res) + pe.chResults <- struct{}{} + + if pe.profile { + end := time.Since(pe.begin) + + pe.statsMutex.Lock() + 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() + } + } + + if procNum < NumSpeculativeProcs { + for range pe.chSpeculativeTasks { + doWork(pe.specTaskQueue.Pop().(ExecVersionView)) + } + } else { + for task := range pe.chTasks { + doWork(task) + } + } + }(i) + } + + pe.settleWg.Add(1) + + go func() { + for t := range pe.chSettle { + pe.tasks[t].Settle() + } + + pe.settleWg.Done() + }() + + // bootstrap first execution + tx := pe.execTasks.takeNextPending() + + if tx == -1 { + return ParallelExecFailedError{"no executable transactions due to bad dependency"} + } + + 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) { + close(pe.chTasks) + close(pe.chSpeculativeTasks) + close(pe.chSettle) + + if wait { + pe.workerWg.Wait() + } + + if wait { + pe.settleWg.Wait() + } +} + +// nolint: gocognit +func (pe *ParallelExecutor) Step(res *ExecResult) (result ParallelExecutionResult, err error) { + tx := res.ver.TxnIndex + + 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(true) + + 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)) + + pe.Close(true) + + 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, &deps, allDeps}, 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 { + 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, metadata bool, interruptCtx context.Context) (result ParallelExecutionResult, err error) { + if len(tasks) == 0 { + return ParallelExecutionResult{MakeTxnInputOutput(len(tasks)), nil, nil, nil}, nil + } + + pe := NewParallelExecutor(tasks, profile, metadata) + err = pe.Prepare() + + if err != nil { + pe.Close(true) + return + } + + for range pe.chResults { + if interruptCtx != nil && interruptCtx.Err() != nil { + pe.Close(true) + return result, interruptCtx.Err() + } + + 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, 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 new file mode 100644 index 0000000000..511d9c774a --- /dev/null +++ b/core/blockstm/executor_test.go @@ -0,0 +1,984 @@ +package blockstm + +import ( + "context" + "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 +const greenTick = "✅" +const redCross = "❌" + +const threeRockets = "🚀🚀🚀" + +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 + 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, + dependencies: []int{}, + } +} + +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, fmt.Errorf("Dependency error")} + } + + 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 (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 + } +} + +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, 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, i int, 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, i, j, len(ops)) + ops[j].duration = readTime(i, j) + } else if ops[j].opType == writeType { + ops[j].key = pathGenerator(s, i, 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 := greenTick + + if execDuration >= expectedSerialDuration { + 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) + + 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) +} + +// 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 { + 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 +} + +// 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, false, validation, metadata, nil) + + 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") + + // 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) + + return duration +} + +func runParallelGetMetadata(t *testing.T, tasks []ExecTask, validation PropertyCheck) map[int]map[int]bool { + t.Helper() + + res, err := executeParallelWithCheck(tasks, true, validation, false, nil) + + assert.NoError(t, err, "error occur during parallel execution") + + return res.AllDeps +} + +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} + + 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 + return common.BigToAddress(big.NewInt(int64(i % randomness))) + } + tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, readTime, writeTime, nonIOTime) + + 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) + + 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, false), 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} + + 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, 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) + + totalTxs := []int{10, 50, 100, 200, 300} + numReads := []int{20, 100, 200} + 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 + return common.BigToAddress(big.NewInt(int64(i / randomness))) + } + tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, readTime, writeTime, nonIOTime) + + 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) + + totalTxs := []int{10, 50, 100, 200, 300} + numReads := []int{20, 100, 200} + 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, 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) + + totalTxs := []int{10, 50, 100, 200, 300} + numReads := []int{20, 100, 200} + 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 + 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, 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) + + totalTxs := []int{10, 50, 100, 200, 300} + numReads := []int{20, 100, 200} + numWrites := []int{20, 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) { + 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, 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) +} + +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/blockstm/mvhashmap.go b/core/blockstm/mvhashmap.go new file mode 100644 index 0000000000..fdaece261f --- /dev/null +++ b/core/blockstm/mvhashmap.go @@ -0,0 +1,297 @@ +package blockstm + +import ( + "fmt" + "sync" + + "github.com/emirpasic/gods/maps/treemap" + + "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 { + m sync.Map + s sync.Map +} + +func MakeMVHashMap() *MVHashMap { + return &MVHashMap{} +} + +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 Key, fNoKey func(kenc Key) *TxnIndexCells) (cells *TxnIndexCells) { + val, ok := mv.m.Load(k) + + if !ok { + cells = fNoKey(k) + } else { + cells = val.(*TxnIndexCells) + } + + return +} + +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(), + } + cells = n + val, _ := mv.m.LoadOrStore(kenc, n) + cells = val.(*TxnIndexCells) + return + }) + + 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", + k, v.TxnIndex)) + } + + ci.(*WriteCell).flag = FlagDone + ci.(*WriteCell).incarnation = v.Incarnation + ci.(*WriteCell).data = data + } else { + 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) 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(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 Key, txIdx int) { + cells := mv.getKeyCells(k, func(_ Key) *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 Key, txIdx int) (res MVReadResult) { + res.depIdx = -1 + res.incarnation = -1 + + cells := mv.getKeyCells(k, func(_ Key) *TxnIndexCells { + return nil + }) + if cells == nil { + return + } + + cells.rw.RLock() + fk, fv := cells.tm.Floor(txIdx - 1) + cells.rw.RUnlock() + + if 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 (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) { + 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, + } + 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/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.go b/core/blockstm/status.go new file mode 100644 index 0000000000..3025cf6c3e --- /dev/null +++ b/core/blockstm/status.go @@ -0,0 +1,225 @@ +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 + } + + t.dependency = make(map[int]map[int]bool, numTasks) + t.blocker = make(map[int]map[int]bool, numTasks) + + for i := 0; i < numTasks; i++ { + t.blocker[i] = make(map[int]bool) + } + + return +} + +type taskStatusManager struct { + pending []int + inProgress []int + complete []int + dependency map[int]map[int]bool + blocker map[int]map[int]bool +} + +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) addDependencies(blocker int, dependent int) bool { + if blocker < 0 || blocker >= dependent { + return false + } + + curblockers := m.blocker[dependent] + + if m.checkComplete(blocker) { + // Blocker has already completed + delete(curblockers, blocker) + + return len(curblockers) > 0 + } + + if _, ok := m.dependency[blocker]; !ok { + m.dependency[blocker] = make(map[int]bool) + } + + m.dependency[blocker][dependent] = true + 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 { + delete(m.blocker[k], tx) + + if len(m.blocker[k]) == 0 { + 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) 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 +} + +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 +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 { + if m.checkComplete(v) { + m.clearComplete(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/status_test.go b/core/blockstm/status_test.go new file mode 100644 index 0000000000..aff00d9a2f --- /dev/null +++ b/core/blockstm/status_test.go @@ -0,0 +1,82 @@ +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)) + 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/core/blockstm/txio.go b/core/blockstm/txio.go new file mode 100644 index 0000000000..19955fb152 --- /dev/null +++ b/core/blockstm/txio.go @@ -0,0 +1,106 @@ +package blockstm + +const ( + ReadKindMap = 0 + ReadKindStorage = 1 +) + +type ReadDescriptor struct { + Path Key + Kind int + V Version +} + +type WriteDescriptor struct { + Path Key + 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[Key]bool{cmpSet[0].Path: true} + + for i := 1; i < len(cmpSet); i++ { + cmpMap[cmpSet[i].Path] = true + } + + for _, v := range txo { + if !cmpMap[v.Path] { + return true + } + } + + return false +} + +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 +} + +func (io *TxnInputOutput) ReadSet(txnIdx int) []ReadDescriptor { + return io.inputs[txnIdx] +} + +func (io *TxnInputOutput) WriteSet(txnIdx int) []WriteDescriptor { + return io.outputs[txnIdx] +} + +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), + } +} + +func (io *TxnInputOutput) recordRead(txId int, input []ReadDescriptor) { + io.inputs[txId] = input +} + +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) { + 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/chain_makers.go b/core/chain_makers.go index ee44cc4d75..e4bcffd5c1 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/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 new file mode 100644 index 0000000000..8956f6f4d2 --- /dev/null +++ b/core/parallel_state_processor.go @@ -0,0 +1,429 @@ +// 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 ( + "context" + "fmt" + "math/big" + "time" + + "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/metrics" + "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. +// +// 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 + 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 + + // 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 + coinbase common.Address + blockContext vm.BlockContext +} + +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:", r) + + err = blockstm.ErrExecAbortError{Dependency: task.statedb.DepTxIndex()} + + return + } + }() + + // 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), nil) + + if task.result == nil || err != nil { + return blockstm.ErrExecAbortError{Dependency: task.statedb.DepTxIndex(), OriginError: err} + } + + 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) + + task.shouldRerunWithoutFeeDelay = true + } + + 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 { + task.result, err = ApplyMessage(evm, task.msg, new(GasPool).AddGas(task.gasLimit), nil) + } + + if task.statedb.HadInvalidRead() || err != nil { + err = blockstm.ErrExecAbortError{Dependency: task.statedb.DepTxIndex(), OriginError: err} + return + } + + task.statedb.Finalise(task.config.IsEIP158(task.blockNumber)) + + 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() +} + +func (task *ExecutionTask) Sender() common.Address { + return task.sender +} + +func (task *ExecutionTask) Hash() common.Hash { + return task.tx.Hash() +} + +func (task *ExecutionTask) Dependencies() []int { + return task.dependencies +} + +func (task *ExecutionTask) Settle() { + task.finalStateDB.Prepare(task.tx.Hash(), task.index) + + coinbaseBalance := task.finalStateDB.GetBalance(task.coinbase) + + task.finalStateDB.ApplyMVWriteSet(task.statedb.MVFullWriteList()) + + 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(task.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(), + task.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...) +} + +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. +// +// 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, interruptCtx context.Context) (types.Receipts, []*types.Log, uint64, error) { + blockstm.SetProcs(cfg.ParallelSpeculativeProcesses) + + var ( + receipts types.Receipts + header = block.Header() + blockHash = block.Hash() + blockNumber = block.Number() + allLogs []*types.Log + usedGas = new(uint64) + metadata bool + ) + + // 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())) + + shouldDelayFeeCal := true + + coinbase, _ := p.bc.Engine().Author(header) + + deps := 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) + 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() + + if msg.From() == coinbase { + shouldDelayFeeCal = false + } + + if len(header.TxDependency) != len(block.Transactions()) { + 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], + coinbase: coinbase, + blockContext: blockContext, + } + + tasks = append(tasks, task) + } else { + 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, + coinbase: coinbase, + blockContext: blockContext, + } + + tasks = append(tasks, task) + } + } + + backupStateDB := statedb.Copy() + + profile := false + result, err := blockstm.ExecuteParallel(tasks, profile, metadata, interruptCtx) + + if err == nil && profile && result.Deps != nil { + _, 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)) + } + + for _, task := range tasks { + task := task.(*ExecutionTask) + if task.shouldRerunWithoutFeeDelay { + shouldDelayFeeCal = false + + statedb.StopPrefetcher() + *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, metadata, interruptCtx) + + break + } + } + + if err != nil { + return nil, nil, 0, err + } + + // 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 +} + +func GetDeps(txDependency [][]uint64) map[int][]int { + deps := make(map[int][]int) + + for i := 0; i <= len(txDependency)-1; i++ { + deps[i] = []int{} + + for j := 0; j <= len(txDependency[i])-1; j++ { + deps[i] = append(deps[i], int(txDependency[i][j])) + } + } + + return deps +} diff --git a/core/state/journal.go b/core/state/journal.go index 57a692dc7f..2a0c81dcb6 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,6 +144,7 @@ type ( func (ch createObjectChange) revert(s *StateDB) { delete(s.stateObjects, *ch.account) delete(s.stateObjectsDirty, *ch.account) + RevertWrite(s, blockstm.NewAddressKey(*ch.account)) } func (ch createObjectChange) dirtied() *common.Address { @@ -151,6 +153,7 @@ func (ch createObjectChange) dirtied() *common.Address { func (ch resetObjectChange) revert(s *StateDB) { s.setStateObject(ch.prev) + RevertWrite(s, blockstm.NewAddressKey(ch.prev.address)) if !ch.prevdestruct && s.snap != nil { delete(s.snapDestructs, ch.prev.addrHash) } @@ -165,6 +168,8 @@ func (ch suicideChange) revert(s *StateDB) { if obj != nil { obj.suicided = ch.prev obj.setBalance(ch.prevbalance) + RevertWrite(s, blockstm.NewSubpathKey(*ch.account, SuicidePath)) + RevertWrite(s, blockstm.NewSubpathKey(*ch.account, BalancePath)) } } @@ -183,6 +188,7 @@ func (ch touchChange) dirtied() *common.Address { func (ch balanceChange) revert(s *StateDB) { s.getStateObject(*ch.account).setBalance(ch.prev) + RevertWrite(s, blockstm.NewSubpathKey(*ch.account, BalancePath)) } func (ch balanceChange) dirtied() *common.Address { @@ -191,6 +197,7 @@ func (ch balanceChange) dirtied() *common.Address { func (ch nonceChange) revert(s *StateDB) { s.getStateObject(*ch.account).setNonce(ch.prev) + RevertWrite(s, blockstm.NewSubpathKey(*ch.account, NoncePath)) } func (ch nonceChange) dirtied() *common.Address { @@ -199,6 +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) + RevertWrite(s, blockstm.NewSubpathKey(*ch.account, CodePath)) } func (ch codeChange) dirtied() *common.Address { @@ -207,6 +215,7 @@ func (ch codeChange) dirtied() *common.Address { func (ch storageChange) revert(s *StateDB) { s.getStateObject(*ch.account).setState(ch.key, ch.prevalue) + 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 c236a79b5a..881ea8c110 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,14 @@ 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[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 // unable to deal with database-level errors. Any error that occurs @@ -138,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{}), + revertedKeys: make(map[blockstm.Key]struct{}), logs: make(map[common.Hash][]*types.Log), preimages: make(map[common.Hash][]byte), journal: newJournal(), @@ -154,6 +164,281 @@ 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 + sdb.dep = -1 + return sdb, nil + } +} + +func (sdb *StateDB) SetMVHashmap(mvhm *blockstm.MVHashMap) { + sdb.mvHashmap = mvhm + 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)) + + for _, v := range s.writeMap { + if _, ok := s.revertedKeys[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) + } + + return writes +} + +func (s *StateDB) MVReadMap() map[blockstm.Key]blockstm.ReadDescriptor { + return s.readMap +} + +func (s *StateDB) MVReadList() []blockstm.ReadDescriptor { + reads := make([]blockstm.ReadDescriptor, 0, len(s.readMap)) + + for _, v := range s.MVReadMap() { + reads = append(reads, v) + } + + return reads +} + +func (s *StateDB) ensureReadMap() { + if s.readMap == nil { + s.readMap = make(map[blockstm.Key]blockstm.ReadDescriptor) + } +} + +func (s *StateDB) ensureWriteMap() { + if s.writeMap == nil { + s.writeMap = make(map[blockstm.Key]blockstm.WriteDescriptor) + } +} + +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 +} + +func (s *StateDB) DepTxIndex() int { + return s.dep +} + +func (s *StateDB) SetIncarnation(inc int) { + s.incarnation = inc +} + +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) + } + + s.ensureReadMap() + + if s.writeMap != nil { + if _, ok := s.writeMap[k]; ok { + return readStorage(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 + + 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: + { + s.dep = res.DepIdx() + + panic("Found dependency") + } + case blockstm.MVReadResultNone: + { + v = readStorage(s) + rd.Kind = blockstm.ReadKindStorage + } + default: + return defaultV + } + + // 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[k]; !ok { + s.readMap[k] = rd + } + + return +} + +func MVWrite(s *StateDB, k blockstm.Key) { + if s.mvHashmap != nil { + s.ensureWriteMap() + s.writeMap[k] = blockstm.WriteDescriptor{ + Path: k, + V: s.Version(), + Val: s, + } + } +} + +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 + } + + _, ok := s.writeMap[k] + + 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 + sr := writes[i].Val.(*StateDB) + + if path.IsState() { + addr := path.GetAddress() + 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() { + 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: + stateObject := sr.getDeletedStateObject(addr) + if stateObject != nil && stateObject.deleted { + sw.Suicide(addr) + } + default: + panic(fmt.Errorf("unknown key type: %d", path.GetSubpath())) + } + } + } +} + +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. @@ -257,22 +542,40 @@ 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) [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 +// } + +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() - } - return common.Big0 + return MVRead(s, blockstm.NewSubpathKey(addr, 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() - } + return MVRead(s, blockstm.NewSubpathKey(addr, NoncePath), 0, func(s *StateDB) uint64 { + stateObject := s.getStateObject(addr) + if stateObject != nil { + return stateObject.Nonce() + } - return 0 + return 0 + }) } // TxIndex returns the current transaction index set by Prepare. @@ -280,37 +583,52 @@ 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 { + return MVRead(s, blockstm.NewSubpathKey(addr, 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) - } - return 0 + return MVRead(s, blockstm.NewSubpathKey(addr, 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 { - return common.Hash{} - } - return common.BytesToHash(stateObject.CodeHash()) + return MVRead(s, blockstm.NewSubpathKey(addr, 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) - } - return 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) + } + return common.Hash{} + }) } // GetProof returns the Merkle proof for a given account. @@ -338,11 +656,13 @@ 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) - } - return 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) + } + return common.Hash{} + }) } // Database retrieves the low level database supporting the lower level trie ops. @@ -363,11 +683,13 @@ 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 - } - return false + return MVRead(s, blockstm.NewSubpathKey(addr, SuicidePath), false, func(s *StateDB) bool { + stateObject := s.getStateObject(addr) + if stateObject != nil { + return stateObject.suicided + } + return false + }) } /* @@ -377,44 +699,68 @@ 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) + MVWrite(s, blockstm.NewSubpathKey(addr, BalancePath)) } } // 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) + MVWrite(s, blockstm.NewSubpathKey(addr, 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, blockstm.NewSubpathKey(addr, 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, blockstm.NewSubpathKey(addr, 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, blockstm.NewSubpathKey(addr, 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, blockstm.NewStateKey(addr, key)) } } @@ -437,6 +783,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 +793,9 @@ func (s *StateDB) Suicide(addr common.Address) bool { stateObject.markSuicided() stateObject.data.Balance = new(big.Int) + MVWrite(s, blockstm.NewSubpathKey(addr, SuicidePath)) + MVWrite(s, blockstm.NewSubpathKey(addr, BalancePath)) + return true } @@ -501,60 +852,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, blockstm.NewAddressKey(addr), 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(crypto.NewKeccakState(), 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 +923,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 + } + + addrKey := blockstm.NewAddressKey(object.Address()) + + 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, addrKey) + + 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 +964,8 @@ func (s *StateDB) createObject(addr common.Address) (newobj, prev *stateObject) s.journal.append(resetObjectChange{prev: prev, prevdestruct: prevdestruct}) } s.setStateObject(newobj) + + MVWrite(s, blockstm.NewAddressKey(addr)) if prev != nil && !prev.deleted { return newobj, prev } @@ -609,6 +986,7 @@ func (s *StateDB) CreateAccount(addr common.Address) { newObj, prev := s.createObject(addr) if prev != nil { newObj.setBalance(prev.data.Balance) + MVWrite(s, blockstm.NewSubpathKey(addr, BalancePath)) } } @@ -651,6 +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)), + revertedKeys: make(map[blockstm.Key]struct{}), refund: s.refund, logs: make(map[common.Hash][]*types.Log, len(s.logs)), logSize: s.logSize, @@ -738,6 +1117,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..053d57470a 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,457 @@ 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) + states[1].FlushMVWriteSet() + + // Tx1 read + v = states[1].GetState(addr, key) + b := states[1].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) + states[3].FlushMVWriteSet() + + // 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) + states[0].FlushMVWriteSet() + + // 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) + states[1].FlushMVWriteSet() + + // 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) + 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) + b := states[2].GetBalance(addr) + + assert.Equal(t, val, v) + assert.Equal(t, balance, b) + + // Tx1 mark estimate + for _, v := range states[1].MVWriteList() { + mvhm.MarkEstimate(v.Path, 1) + } + + defer func() { + if r := recover(); r == nil { + t.Errorf("The code did not panic") + } else { + t.Log("Recovered in f", r) + } + }() + + // 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) + 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) + 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) + + // 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) + 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)) + 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) + states[1].FlushMVWriteSet() + + 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") + addr3 := common.HexToAddress("0x03") + 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].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(true) + 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()) + + 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(true) + states[1].FlushMVWriteSet() + + sSingleProcess.SetState(addr1, key2, val2) + sSingleProcess.SetBalance(addr1, balance2) + sSingleProcess.SetNonce(addr1, 1) + + sClean.ApplyMVWriteSet(states[1].MVWriteList()) + + 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(true) + states[2].FlushMVWriteSet() + + sSingleProcess.SetState(addr1, key1, val2) + sSingleProcess.SetBalance(addr1, balance2) + sSingleProcess.SetNonce(addr1, 2) + + sClean.ApplyMVWriteSet(states[2].MVWriteList()) + + assert.Equal(t, sSingleProcess.IntermediateRoot(true), sClean.IntermediateRoot(true)) + + // Tx3 write + states[3].Suicide(addr2) + states[3].SetCode(addr1, code) + states[3].Finalise(true) + states[3].FlushMVWriteSet() + + sSingleProcess.Suicide(addr2) + sSingleProcess.SetCode(addr1, code) + + sClean.ApplyMVWriteSet(states[3].MVWriteList()) + + 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. // See https://github.com/ethereum/go-ethereum/pull/15225#issuecomment-380191512 func TestCopyOfCopy(t *testing.T) { diff --git a/core/state_processor.go b/core/state_processor.go index 28c4cbfddb..fac921cd2f 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -57,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) @@ -75,12 +75,20 @@ 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) } 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) } @@ -99,12 +107,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, interruptCtx) + 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, interruptCtx) 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), + ) + if result.Err == vm.ErrInterrupt { return nil, result.Err } diff --git a/core/state_processor_test.go b/core/state_processor_test.go index 5dc076a11c..35e53a2e54 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, _ = NewParallelBlockChain(db, nil, gspec.Config, ethash.NewFaker(), vm.Config{ParallelEnable: true, ParallelSpeculativeProcesses: 8}, 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, _ = NewParallelBlockChain(db, nil, gspec.Config, ethash.NewFaker(), vm.Config{ParallelEnable: true, ParallelSpeculativeProcesses: 8}, 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) + } } } } diff --git a/core/state_transition.go b/core/state_transition.go index 3e6d384719..39e23f3ecb 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -63,6 +63,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. @@ -85,9 +90,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 @@ -184,6 +193,13 @@ func ApplyMessage(evm *vm.EVM, msg Message, gp *GasPool, interruptCtx context.Co return NewStateTransition(evm, msg, gp).TransitionDb(interruptCtx) } +func ApplyMessageNoFeeBurnOrTip(evm *vm.EVM, msg Message, gp *GasPool, interruptCtx context.Context) (*ExecutionResult, error) { + st := NewStateTransition(evm, msg, gp) + st.noFeeBurnAndTip = true + + return st.TransitionDb(interruptCtx) +} + // to returns the recipient of the message. func (st *StateTransition) to() common.Address { if st.msg == nil || st.msg.To() == nil /* contract creation */ { @@ -277,7 +293,12 @@ func (st *StateTransition) preCheck() error { // nil evm execution result. func (st *StateTransition) TransitionDb(interruptCtx context.Context) (*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 @@ -343,34 +364,50 @@ func (st *StateTransition) TransitionDb(interruptCtx context.Context) (*Executio 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 } 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/core/types/block.go b/core/types/block.go index 314990dc99..0af6a35501 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] -> 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"` + /* TODO (MariusVanDerWijden) Add this field once needed // Random was added during the merge and contains the BeaconState randomness @@ -252,6 +257,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 +321,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() } diff --git a/core/vm/interpreter.go b/core/vm/interpreter.go index 91f6254039..04184b95f7 100644 --- a/core/vm/interpreter.go +++ b/core/vm/interpreter.go @@ -56,6 +56,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, @@ -274,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 { @@ -433,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) diff --git a/eth/backend.go b/eth/backend.go index fa5d40780d..0c5648cf46 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -208,7 +208,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, @@ -226,7 +228,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 7581e16761..5d58c70fe4 100644 --- a/eth/ethconfig/config.go +++ b/eth/ethconfig/config.go @@ -238,6 +238,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/eth/state_accessor.go b/eth/state_accessor.go index 956ba5ee1e..b8672e94f8 100644 --- a/eth/state_accessor.go +++ b/eth/state_accessor.go @@ -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) } diff --git a/eth/tracers/api.go b/eth/tracers/api.go index 9fb349878c..54f624b0a8 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" @@ -63,10 +66,16 @@ 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 ) 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 { @@ -196,6 +205,8 @@ type TraceConfig struct { Tracer *string Timeout *string Reexec *uint64 + Path *string + IOFlag *bool BorTraceEnabled *bool BorTx *bool } @@ -694,18 +705,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 allowIOTracing && 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()) @@ -756,10 +786,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} @@ -768,29 +819,89 @@ func (api *API) traceBlock(ctx context.Context, block *types.Block, config *Trac statedb.Prepare(tx.Hash(), i) vmenv := vm.NewEVM(blockCtx, core.NewEVMTxContext(msg), statedb, api.backend.ChainConfig(), vm.Config{}) - //nolint: nestif - if stateSyncPresent && i == len(txs)-1 { - if *config.BorTraceEnabled { - callmsg := prepareCallMessage(msg) - if _, err := statefull.ApplyBorMessage(*vmenv, callmsg); err != nil { - failed = err + + // nolint: nestif + if !ioflag { + //nolint: nestif + 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 + } + } else { break } } else { - break + // nolint : contextcheck + if _, err := core.ApplyMessage(vmenv, msg, new(core.GasPool).AddGas(msg.Gas()), context.Background()); 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) // nolint : contextcheck - if _, err := core.ApplyMessage(vmenv, msg, new(core.GasPool).AddGas(msg.Gas()), context.Background()); err != nil { + result, err := core.ApplyMessageNoFeeBurnOrTip(vmenv, msg, new(core.GasPool).AddGas(msg.Gas()), context.Background()) + + 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...) + } + } + + 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 } - // 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())) + // 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 48307337c7..897e412f2c 100644 --- a/eth/tracers/api_test.go +++ b/eth/tracers/api_test.go @@ -427,6 +427,85 @@ 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) + } + })) + + allowIOTracing = true + + 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 diff --git a/go.mod b/go.mod index ddfa2e0feb..c5fef38ed6 100644 --- a/go.mod +++ b/go.mod @@ -38,6 +38,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/big v0.0.0-20221017200358-a027dc42d04e github.com/holiman/bloomfilter/v2 v2.0.3 github.com/holiman/uint256 v1.2.0 @@ -131,6 +132,7 @@ 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/etcd-io/bbolt v1.3.3 // indirect github.com/fsnotify/fsnotify v1.4.9 // indirect github.com/go-kit/kit v0.10.0 // indirect diff --git a/go.sum b/go.sum index 844cb0e86c..99639bfeb0 100644 --- a/go.sum +++ b/go.sum @@ -149,7 +149,6 @@ github.com/btcsuite/btcd/btcec/v2 v2.1.2/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJ github.com/btcsuite/btcd/btcec/v2 v2.1.3 h1:xM/n3yIhHAhHy04z4i43C8p4ehixJZMsnrVJkgl+MTE= github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= -github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U= github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= github.com/btcsuite/btcutil v0.0.0-20180706230648-ab6388e0c60a/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= @@ -254,6 +253,8 @@ github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFP 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.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= 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= @@ -322,8 +323,9 @@ github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LB github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw= github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= -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/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0= github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY= github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg= @@ -498,6 +500,8 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +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/big v0.0.0-20221017200358-a027dc42d04e h1:pIYdhNkDh+YENVNi3gto8n9hAmRxKxoar0iE6BLucjw= github.com/holiman/big v0.0.0-20221017200358-a027dc42d04e/go.mod h1:j9cQbcqHQujT0oKJ38PylVfqohClLr3CvDC+Qcg+lhU= github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= diff --git a/internal/cli/server/config.go b/internal/cli/server/config.go index 5ca9d4eb15..e75e8264c5 100644 --- a/internal/cli/server/config.go +++ b/internal/cli/server/config.go @@ -127,6 +127,8 @@ 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"` // Develop Fake Author mode to produce blocks without authorisation DevFakeAuthor bool `hcl:"devfakeauthor,optional" toml:"devfakeauthor,optional"` @@ -569,6 +571,12 @@ type DeveloperConfig struct { GasLimit uint64 `hcl:"gaslimit,optional" toml:"gaslimit,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", @@ -748,6 +756,10 @@ func DefaultConfig() *Config { BlockProfileRate: 0, // CPUProfile: "", }, + ParallelEVM: &ParallelEVMConfig{ + Enable: false, + SpeculativeProcesses: 8, + }, } } @@ -1131,6 +1143,8 @@ 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 n.RPCReturnDataLimit = c.RPCReturnDataLimit if c.Ancient != "" { diff --git a/internal/cli/server/flags.go b/internal/cli/server/flags.go index 0c38da3823..470a377012 100644 --- a/internal/cli/server/flags.go +++ b/internal/cli/server/flags.go @@ -904,6 +904,20 @@ 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, + }) f.Uint64Flag(&flagset.Uint64Flag{ Name: "dev.gaslimit", Usage: "Initial block gas limit", diff --git a/miner/worker.go b/miner/worker.go index 1666987381..f546b37029 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -43,6 +43,7 @@ import ( "github.com/ethereum/go-ethereum/consensus/bor" "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/core/vm" @@ -973,6 +974,53 @@ 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 + + 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{} + + 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) + } + initialGasLimit := env.gasPool.Gas() initialTxs := txs.GetTxs() @@ -992,6 +1040,10 @@ func (w *worker) commitTransactions(env *environment, txs *types.TransactionsByP mainloop: for { if interruptCtx != nil { + if EnableMVHashMap { + env.state.AddEmptyMVHashMap() + } + // case of interrupting by timeout select { case <-interruptCtx.Done(): @@ -1081,6 +1133,22 @@ mainloop: // Everything ok, collect the logs and shift in the next transaction from the same account coalescedLogs = append(coalescedLogs, logs...) env.tcount++ + + 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() log.OnDebug(func(lg log.Logging) { @@ -1098,6 +1166,50 @@ mainloop: 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)) + + for j := range deps[0] { + tempDeps[0] = append(tempDeps[0], uint64(j)) + } + + 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 { + delayFlag = false + } + + for j := range deps[i] { + tempDeps[i] = append(tempDeps[i], uint64(j)) + } + } + + if delayFlag { + env.header.TxDependency = tempDeps + } else { + env.header.TxDependency = nil + } + } else { + env.header.TxDependency = nil + } } if !w.isRunning() && len(coalescedLogs) > 0 { diff --git a/miner/worker_test.go b/miner/worker_test.go index 0b4e2757fd..8edd40996a 100644 --- a/miner/worker_test.go +++ b/miner/worker_test.go @@ -854,3 +854,126 @@ 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 + + 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().GetCurrentValidatorsByHash(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, 0, 0, 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{ParallelEnable: true, ParallelSpeculativeProcesses: 8}, 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 for block number > 4 + if block.NumberU64() <= 4 { + if block.TxDependency() != nil { + b.Fatalf("dependency not nil") + } + } 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-1) || len(deps[i]) != 1 { + 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") + } + } +} diff --git a/params/config.go b/params/config.go index 9833c9eac5..1c845321aa 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,8 +405,9 @@ var ( BerlinBlock: big.NewInt(14750000), LondonBlock: big.NewInt(23850000), Bor: &BorConfig{ - JaipurBlock: big.NewInt(23850000), - DelhiBlock: big.NewInt(38189056), + JaipurBlock: big.NewInt(23850000), + ParallelUniverseBlock: big.NewInt(0), + DelhiBlock: big.NewInt(38189056), Period: map[string]uint64{ "0": 2, }, @@ -583,9 +586,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. @@ -617,6 +621,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) IsSprintStart(number uint64) bool { return number%c.CalculateSprint(number) == 0 } 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)