diff --git a/cmd/evm/blockrunner.go b/cmd/evm/blockrunner.go index c5d836e0ea..196e86d591 100644 --- a/cmd/evm/blockrunner.go +++ b/cmd/evm/blockrunner.go @@ -92,7 +92,7 @@ func blockTestCmd(ctx *cli.Context) error { fmt.Println(string(state.Dump(nil))) } } - }); err != nil { + }, "", true); err != nil { return fmt.Errorf("test %v: %w", name, err) } } diff --git a/cmd/geth/main.go b/cmd/geth/main.go index c8ad9de1a2..1e1df35afa 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -169,6 +169,11 @@ var ( utils.RollupComputePendingBlock, utils.RollupHaltOnIncompatibleProtocolVersionFlag, utils.RollupSuperchainUpgradesFlag, + utils.ParallelTxFlag, + utils.ParallelTxNumFlag, + utils.ParallelTxDAGFlag, + utils.ParallelTxDAGFileFlag, + utils.ParallelTxDAGSenderPrivFlag, configFileFlag, utils.LogDebugFlag, utils.LogBacktraceAtFlag, diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 7e44853681..1e050e1e13 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -23,18 +23,20 @@ import ( "encoding/hex" "errors" "fmt" - "github.com/ethereum/go-ethereum/core/txpool/bundlepool" "math" "math/big" "net" "net/http" "os" "path/filepath" + "runtime" godebug "runtime/debug" "strconv" "strings" "time" + "github.com/ethereum/go-ethereum/core/txpool/bundlepool" + pcsclite "github.com/gballet/go-libpcsclite" gopsutil "github.com/shirou/gopsutil/mem" "github.com/urfave/cli/v2" @@ -1093,11 +1095,43 @@ Please note that --` + MetricsHTTPFlag.Name + ` must be set to start the server. Category: flags.MetricsCategory, } + ParallelTxFlag = &cli.BoolFlag{ + Name: "parallel", + Usage: "Enable the experimental parallel transaction execution mode, only valid in full sync mode (default = false)", + Category: flags.VMCategory, + } + + ParallelTxNumFlag = &cli.IntFlag{ + Name: "parallel.num", + Usage: "Number of slot for transaction execution, only valid in parallel mode (runtime calculated, no fixed default value)", + Category: flags.VMCategory, + } + + ParallelTxDAGFlag = &cli.BoolFlag{ + Name: "parallel.txdag", + Usage: "Enable the experimental parallel TxDAG generation, only valid in full sync mode (default = false)", + Category: flags.VMCategory, + } + + ParallelTxDAGFileFlag = &cli.StringFlag{ + Name: "parallel.txdagfile", + Usage: "It indicates the TxDAG file path", + Value: "./parallel-txdag-output.csv", + Category: flags.VMCategory, + } + VMOpcodeOptimizeFlag = &cli.BoolFlag{ Name: "vm.opcode.optimize", Usage: "enable opcode optimization", Category: flags.VMCategory, } + + ParallelTxDAGSenderPrivFlag = &cli.StringFlag{ + Name: "parallel.txdagsenderpriv", + Usage: "private key of the sender who sends the TxDAG transactions", + Value: "", + Category: flags.VMCategory, + } ) var ( @@ -1983,6 +2017,43 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) { cfg.EnablePreimageRecording = ctx.Bool(VMEnableDebugFlag.Name) } + if ctx.IsSet(ParallelTxFlag.Name) { + cfg.ParallelTxMode = ctx.Bool(ParallelTxFlag.Name) + // The best parallel num will be tuned later, we do a simple parallel num set here + numCpu := runtime.NumCPU() + var parallelNum int + if ctx.IsSet(ParallelTxNumFlag.Name) { + // Use value set by "--parallel.num", and "--parallel.num 0" is not allowed and be set to 1 + parallelNum = ctx.Int(ParallelTxNumFlag.Name) + if parallelNum < 1 { + parallelNum = 1 + } + } else if numCpu == 1 { + parallelNum = 1 // single CPU core + } else { + // 1-2 core for merge (with parallel KV check) + // 1-2 core for others (bc optimizer, main) + // 1-2 core for possible other concurrent routine + parallelNum = max(1, numCpu-6) + } + cfg.ParallelTxNum = parallelNum + } + + if ctx.IsSet(ParallelTxDAGFlag.Name) { + cfg.EnableParallelTxDAG = ctx.Bool(ParallelTxDAGFlag.Name) + } + + if ctx.IsSet(ParallelTxDAGFileFlag.Name) { + cfg.ParallelTxDAGFile = ctx.String(ParallelTxDAGFileFlag.Name) + } + + if ctx.IsSet(ParallelTxDAGSenderPrivFlag.Name) { + priHex := ctx.String(ParallelTxDAGSenderPrivFlag.Name) + if cfg.Miner.ParallelTxDAGSenderPriv, err = crypto.HexToECDSA(priHex); err != nil { + Fatalf("Failed to parse txdag private key of %s, err: %v", ParallelTxDAGSenderPrivFlag.Name, err) + } + } + if ctx.IsSet(VMOpcodeOptimizeFlag.Name) { cfg.EnableOpcodeOptimizing = ctx.Bool(VMOpcodeOptimizeFlag.Name) if cfg.EnableOpcodeOptimizing { diff --git a/core/blockchain.go b/core/blockchain.go index 7e4b81b153..c43b7a9f69 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -18,11 +18,16 @@ package core import ( + "bufio" + "bytes" + "encoding/hex" "errors" "fmt" "io" "math/big" + "os" "runtime" + "strconv" "strings" "sync" "sync/atomic" @@ -91,6 +96,11 @@ var ( triedbCommitExternalTimer = metrics.NewRegisteredTimer("chain/triedb/commit/external", nil) innerExecutionTimer = metrics.NewRegisteredTimer("chain/inner/execution", nil) + txDAGGenerateTimer = metrics.NewRegisteredTimer("chain/block/txdag/gen", nil) + + parallelTxNumMeter = metrics.NewRegisteredMeter("chain/parallel/txs", nil) + parallelConflictTxNumMeter = metrics.NewRegisteredMeter("chain/parallel/conflicttxs", nil) + blockGasUsedGauge = metrics.NewRegisteredGauge("chain/block/gas/used", nil) mgaspsGauge = metrics.NewRegisteredGauge("chain/mgas/ps", nil) @@ -294,6 +304,13 @@ type BlockChain struct { processor Processor // Block transaction processor interface forker *ForkChoice vmConfig vm.Config + + parallelExecution bool + enableTxDAG bool + txDAGWriteCh chan TxDAGOutputItem + txDAGReader *TxDAGFileReader + serialProcessor Processor + parallelProcessor Processor } // NewBlockChain returns a fully initialised block chain using information @@ -358,7 +375,6 @@ func NewBlockChain(db ethdb.Database, cacheConfig *CacheConfig, genesis *Genesis bc.stateCache = state.NewDatabaseWithNodeDB(bc.db, bc.triedb) bc.validator = NewBlockValidator(chainConfig, bc, engine) bc.prefetcher = newStatePrefetcher(chainConfig, bc, engine) - bc.processor = NewStateProcessor(chainConfig, bc, engine) err := proofKeeper.Start(bc, db) if err != nil { @@ -512,6 +528,12 @@ func NewBlockChain(db ethdb.Database, cacheConfig *CacheConfig, genesis *Genesis bc.snaps, _ = snapshot.New(snapconfig, bc.db, bc.triedb, head.Root) } + if bc.vmConfig.EnableParallelExec { + bc.CreateParallelProcessor(bc.vmConfig.ParallelTxNum) + bc.CreateSerialProcessor(chainConfig, bc, engine) + } else { + bc.processor = NewStateProcessor(chainConfig, bc, engine) + } // Start future block processor. bc.wg.Add(1) go bc.updateFutureBlocks() @@ -861,6 +883,10 @@ func (bc *BlockChain) setHeadBeyondRoot(head uint64, time uint64, root common.Ha bc.miningStateCache.Purge() bc.futureBlocks.Purge() + if bc.txDAGReader != nil { + bc.txDAGReader.Reset(head) + } + // Clear safe block, finalized block if needed if safe := bc.CurrentSafeBlock(); safe != nil && head < safe.Number.Uint64() { log.Warn("SetHead invalidated safe block") @@ -1045,6 +1071,9 @@ func (bc *BlockChain) stopWithoutSaving() { // Stop stops the blockchain service. If any imports are currently in progress // it will abort them using the procInterrupt. func (bc *BlockChain) Stop() { + if bc.txDAGReader != nil { + bc.txDAGReader.Close() + } bc.stopWithoutSaving() // Ensure that the entirety of the state snapshot is journaled to disk. @@ -1738,7 +1767,6 @@ func (bc *BlockChain) insertChain(chain types.Blocks, setHead bool) (int, error) return it.index, err } lastCanon = block - block, err = it.next() } // Falls through to the block import @@ -1876,9 +1904,23 @@ func (bc *BlockChain) insertChain(chain types.Blocks, setHead bool) (int, error) statedb.StartPrefetcher("chain") activeState = statedb + if bc.vmConfig.EnableParallelExec { + bc.parseTxDAG(block) + txsCount := block.Transactions().Len() + threshold := min(bc.vmConfig.ParallelTxNum/2+2, 4) + if bc.vmConfig.ParallelTxNum < 2 || txsCount < threshold || bc.isEmptyTxDAG() { + bc.UseSerialProcessor() + log.Debug("Disable Parallel Tx execution", "block", block.NumberU64(), "transactions", txsCount, "parallelTxNum", bc.vmConfig.ParallelTxNum) + } else { + bc.UseParallelProcessor() + log.Debug("Enable Parallel Tx execution", "block", block.NumberU64(), "transactions", txsCount, "parallelTxNum", bc.vmConfig.ParallelTxNum) + + } + } // 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. - if !bc.cacheConfig.TrieCleanNoPrefetch { + // parallel mode has a pipeline, similar to this prefetch, to save CPU we disable this prefetch for parallel + if !bc.cacheConfig.TrieCleanNoPrefetch && !bc.parallelExecution { if followup, err := it.peek(); followup != nil && err == nil { throwaway, _ := state.New(parent.Root, bc.stateCache, bc.snaps) @@ -1898,6 +1940,10 @@ func (bc *BlockChain) insertChain(chain types.Blocks, setHead bool) (int, error) // Process block using the parent state as reference point pstart = time.Now() receipts, logs, usedGas, err = bc.processor.Process(block, statedb, bc.vmConfig) + if err == FallbackToSerialProcessorErr { + bc.UseSerialProcessor() + receipts, logs, usedGas, err = bc.processor.Process(block, statedb, bc.vmConfig) + } if err != nil { bc.reportBlock(block, receipts, err) followupInterrupt.Store(true) @@ -1912,9 +1958,31 @@ func (bc *BlockChain) insertChain(chain types.Blocks, setHead bool) (int, error) followupInterrupt.Store(true) return it.index, err } + vtime := time.Since(vstart) proctime := time.Since(start) // processing + validation + if bc.enableTxDAG && !bc.vmConfig.EnableParallelExec { + // compare input TxDAG when it enable in consensus + dag, err := statedb.ResolveTxDAG(len(block.Transactions()), []common.Address{block.Coinbase(), params.OptimismBaseFeeRecipient, params.OptimismL1FeeRecipient}) + if err == nil { + // TODO(galaio): check TxDAG correctness? + log.Debug("Process TxDAG result", "block", block.NumberU64(), "txDAG", dag) + if metrics.EnabledExpensive { + go types.EvaluateTxDAGPerformance(dag, statedb.ResolveStats()) + } + // try to write txDAG into file + if bc.txDAGWriteCh != nil && dag != nil { + bc.txDAGWriteCh <- TxDAGOutputItem{ + blockNumber: block.NumberU64(), + txDAG: dag, + } + } + } else { + log.Error("ResolveTxDAG err", "block", block.NumberU64(), "tx", len(block.Transactions()), "err", err) + } + } + // Update the metrics touched during block processing and validation accountReadTimer.Update(statedb.AccountReads) // Account reads are complete(in processing) storageReadTimer.Update(statedb.StorageReads) // Storage reads are complete(in processing) @@ -1924,8 +1992,9 @@ func (bc *BlockChain) insertChain(chain types.Blocks, setHead bool) (int, error) storageUpdateTimer.Update(statedb.StorageUpdates) // Storage updates are complete(in validation) accountHashTimer.Update(statedb.AccountHashes) // Account hashes are complete(in validation) storageHashTimer.Update(statedb.StorageHashes) // Storage hashes are complete(in validation) - blockExecutionTimer.Update(ptime) // The time spent on block execution - blockValidationTimer.Update(vtime) // The time spent on block validation + txDAGGenerateTimer.Update(statedb.TxDAGGenerate) + blockExecutionTimer.Update(ptime) // The time spent on block execution + blockValidationTimer.Update(vtime) // The time spent on block validation innerExecutionTimer.Update(DebugInnerExecutionDuration) @@ -2025,6 +2094,39 @@ func (bc *BlockChain) insertChain(chain types.Blocks, setHead bool) (int, error) return it.index, err } +func (bc *BlockChain) parseTxDAG(block *types.Block) { + if !bc.enableTxDAG { + return + } + var ( + txDAG types.TxDAG + err error + ) + if bc.txDAGReader != nil { + // load cache txDAG from file first + txDAG = bc.txDAGReader.TxDAG(block.NumberU64()) + } else { + // load TxDAG from block + txDAG, err = types.GetTxDAG(block) + if err != nil { + log.Warn("pevm decode txdag failed", "block", block.NumberU64(), "err", err) + } + } + if err := types.ValidateTxDAG(txDAG, len(block.Transactions())); err != nil { + log.Warn("pevm cannot apply wrong txdag", + "block", block.NumberU64(), "txs", len(block.Transactions()), "err", err) + txDAG = nil + } + bc.vmConfig.TxDAG = txDAG +} + +func (bc *BlockChain) isEmptyTxDAG() bool { + if bc.vmConfig.TxDAG != nil && bc.vmConfig.TxDAG.Type() == types.EmptyTxDAGType { + return true + } + return false +} + // insertSideChain is called when an import batch hits upon a pruned ancestor // error, which happens when a sidechain with a sufficiently old fork-block is // found. @@ -2598,6 +2700,14 @@ func (bc *BlockChain) GetTrieFlushInterval() time.Duration { return time.Duration(bc.flushInterval.Load()) } +func (bc *BlockChain) CreateParallelProcessor(parallelNum int) *BlockChain { + if bc.parallelProcessor == nil { + bc.parallelProcessor = newParallelStateProcessor(bc.Config(), bc, bc.engine, parallelNum) + bc.parallelExecution = true + } + return bc +} + func (bc *BlockChain) NoTries() bool { return bc.stateCache.NoTries() } @@ -2628,3 +2738,222 @@ func createDelFn(bc *BlockChain) func(db ethdb.KeyValueWriter, hash common.Hash, func (bc *BlockChain) HeaderChainForceSetHead(headNumber uint64) { bc.hc.SetHead(headNumber, nil, createDelFn(bc)) } + +func (bc *BlockChain) TxDAGEnabledWhenMine() bool { + return bc.enableTxDAG && bc.txDAGWriteCh == nil && bc.txDAGReader == nil +} + +func (bc *BlockChain) SetupTxDAGGeneration(output string, readFile bool) { + log.Info("node enable TxDAG feature", "output", output) + bc.enableTxDAG = true + if len(output) == 0 { + return + } + // read TxDAG file, and cache in mem + if readFile { + var err error + bc.txDAGReader, err = NewTxDAGFileReader(output) + if err != nil { + log.Error("read TxDAG err", "err", err) + } + // startup with latest block + curHeader := bc.CurrentHeader() + if curHeader != nil && bc.txDAGReader != nil { + bc.txDAGReader.TxDAG(curHeader.Number.Uint64()) + log.Info("load TxDAG from file", "output", output, "block", curHeader.Number, "latest", bc.txDAGReader.Latest()) + } + return + } + + // write handler + go func() { + writeHandle, err := os.OpenFile(output, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) + if err != nil { + log.Error("OpenFile when open the txDAG output file", "file", output, "err", err) + return + } + bc.txDAGWriteCh = make(chan TxDAGOutputItem, 10000) + defer writeHandle.Close() + for { + select { + case <-bc.quit: + return + case item := <-bc.txDAGWriteCh: + if err := writeTxDAGToFile(writeHandle, item); err != nil { + log.Error("encode TxDAG err in OutputHandler", "err", err) + continue + } + } + } + }() +} + +func (bc *BlockChain) UseParallelProcessor() { + if bc.parallelProcessor != nil { + bc.parallelExecution = true + bc.processor = bc.parallelProcessor + } else { + log.Error("bc.ParallelProcessor is nil! fallback to serial processor!") + bc.UseSerialProcessor() + } +} + +func (bc *BlockChain) UseSerialProcessor() { + if bc.serialProcessor != nil { + bc.parallelExecution = false + bc.processor = bc.serialProcessor + } else { + bc.CreateSerialProcessor(bc.chainConfig, bc, bc.engine) + } +} + +type TxDAGOutputItem struct { + blockNumber uint64 + txDAG types.TxDAG +} + +func writeTxDAGToFile(writeHandle *os.File, item TxDAGOutputItem) error { + var buf bytes.Buffer + buf.WriteString(strconv.FormatUint(item.blockNumber, 10)) + buf.WriteByte(',') + enc, err := types.EncodeTxDAG(item.txDAG) + if err != nil { + return err + } + buf.WriteString(hex.EncodeToString(enc)) + buf.WriteByte('\n') + _, err = writeHandle.Write(buf.Bytes()) + return err +} + +var TxDAGCacheSize = uint64(10000) + +type TxDAGFileReader struct { + output string + file *os.File + scanner *bufio.Scanner + cache map[uint64]types.TxDAG + latest uint64 + lock sync.RWMutex +} + +func NewTxDAGFileReader(output string) (*TxDAGFileReader, error) { + reader := &TxDAGFileReader{output: output} + err := reader.openFile(output) + if err != nil { + return nil, err + } + return reader, nil +} + +func (t *TxDAGFileReader) Close() { + t.lock.Lock() + defer t.lock.Unlock() + t.closeFile() +} + +func (t *TxDAGFileReader) openFile(output string) error { + file, err := os.Open(output) + if err != nil { + return err + } + scanner := bufio.NewScanner(file) + scanner.Buffer(make([]byte, 5*1024*1024), 5*1024*1024) + t.file = file + t.scanner = scanner + return nil +} + +func (t *TxDAGFileReader) closeFile() { + if t.scanner != nil { + t.scanner = nil + } + if t.file != nil { + t.file.Close() + t.file = nil + } +} + +func (t *TxDAGFileReader) Latest() uint64 { + t.lock.RLock() + defer t.lock.RUnlock() + return t.latest +} + +func (t *TxDAGFileReader) TxDAG(expect uint64) types.TxDAG { + t.lock.Lock() + defer t.lock.Unlock() + + if t.cache != nil && t.latest >= expect { + return t.cache[expect] + } + if t.scanner == nil { + return nil + } + + logTime := time.Now() + t.cache = make(map[uint64]types.TxDAG, TxDAGCacheSize) + for t.scanner.Scan() { + num, dag, err := readTxDAGItemFromLine(t.scanner.Text()) + if err != nil { + log.Error("query TxDAG error", "latest", t.latest, "err", err) + continue + } + if time.Since(logTime) > 10*time.Second { + logTime = time.Now() + log.Info("try load TxDAG from file", "num", num, "expect", expect, "cached", len(t.cache)) + } + // skip lower blocks + if expect > num { + continue + } + t.cache[num] = dag + t.latest = num + if uint64(len(t.cache)) >= TxDAGCacheSize { + break + } + } + if t.scanner.Err() != nil { + log.Error("scan TxDAG file got err", "expect", expect, "err", t.scanner.Err()) + } + + if time.Since(logTime) > 10*time.Second { + log.Info("try load TxDAG from file", "expect", expect, "cached", len(t.cache)) + } + return t.cache[expect] +} + +func (t *TxDAGFileReader) Reset(number uint64) error { + t.lock.Lock() + defer t.lock.Unlock() + if t.latest-TxDAGCacheSize <= number { + return nil + } + t.closeFile() + if err := t.openFile(t.output); err != nil { + return err + } + t.latest = 0 + t.cache = nil + return nil +} + +func readTxDAGItemFromLine(line string) (uint64, types.TxDAG, error) { + tokens := strings.Split(line, ",") + if len(tokens) != 2 { + return 0, nil, errors.New("txDAG output contain wrong size") + } + num, err := strconv.Atoi(tokens[0]) + if err != nil { + return 0, nil, err + } + enc, err := hex.DecodeString(tokens[1]) + if err != nil { + return 0, nil, err + } + txDAG, err := types.DecodeTxDAG(enc) + if err != nil { + return 0, nil, err + } + return uint64(num), txDAG, nil +} diff --git a/core/blockchain_insert.go b/core/blockchain_insert.go index 82a480d0be..2667323c1e 100644 --- a/core/blockchain_insert.go +++ b/core/blockchain_insert.go @@ -60,7 +60,7 @@ func (st *insertStats) report(chain []*types.Block, index int, snapDiffItems, sn "blocks", st.processed, "txs", txs, "mgas", float64(st.usedGas) / 1000000, "elapsed", common.PrettyDuration(elapsed), "mgasps", float64(st.usedGas) * 1000 / float64(elapsed), } - mgaspsGauge.Update(int64(st.usedGas)*1000/int64(elapsed)) + mgaspsGauge.Update(int64(st.usedGas) * 1000 / int64(elapsed)) if timestamp := time.Unix(int64(end.Time()), 0); time.Since(timestamp) > time.Minute { context = append(context, []interface{}{"age", common.PrettyAge(timestamp)}...) } diff --git a/core/blockchain_test.go b/core/blockchain_test.go index 22db20a23e..afc664171d 100644 --- a/core/blockchain_test.go +++ b/core/blockchain_test.go @@ -22,10 +22,13 @@ import ( "math/big" "math/rand" "os" + "path/filepath" "sync" "testing" "time" + "github.com/stretchr/testify/require" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/consensus" @@ -1631,7 +1634,6 @@ func testEIP155Transition(t *testing.T, scheme string) { block.AddTx(tx) } }) - blockchain, _ := NewBlockChain(rawdb.NewMemoryDatabase(), DefaultCacheConfigWithScheme(scheme), gspec, nil, ethash.NewFaker(), vm.Config{}, nil, nil) defer blockchain.Stop() @@ -4327,3 +4329,96 @@ func TestEIP3651(t *testing.T) { t.Fatalf("sender balance incorrect: expected %d, got %d", expected, actual) } } + +func TestTxDAGFile_ReadWrite(t *testing.T) { + path := filepath.Join(os.TempDir(), "test.csv") + defer func() { + os.Remove(path) + }() + except := map[uint64]types.TxDAG{ + 0: types.NewEmptyTxDAG(), + 1: makeEmptyPlainTxDAG(1), + 2: makeEmptyPlainTxDAG(2, types.NonDependentRelFlag), + } + writeFile, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) + require.NoError(t, err) + for num, dag := range except { + require.NoError(t, writeTxDAGToFile(writeFile, TxDAGOutputItem{blockNumber: num, txDAG: dag})) + } + writeFile.Close() + + except2 := map[uint64]types.TxDAG{ + 3: types.NewEmptyTxDAG(), + 4: makeEmptyPlainTxDAG(4, types.NonDependentRelFlag, types.ExcludedTxFlag), + 5: makeEmptyPlainTxDAG(5, types.NonDependentRelFlag, types.ExcludedTxFlag), + } + writeFile, err = os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) + require.NoError(t, err) + for num, dag := range except2 { + if num == 5 { + writeFile.WriteString("num,txdag\n") + continue + } + require.NoError(t, writeTxDAGToFile(writeFile, TxDAGOutputItem{blockNumber: num, txDAG: dag})) + } + writeFile.Close() + + reader, err := NewTxDAGFileReader(path) + require.NoError(t, err) + for i := 0; i < 5; i++ { + num := uint64(i) + if except[num] != nil { + require.Equal(t, except[num], reader.TxDAG(num)) + continue + } + require.Equal(t, except2[num], reader.TxDAG(num)) + } +} + +func TestTxDAGFile_LargeRead(t *testing.T) { + path := filepath.Join(os.TempDir(), "test.csv") + defer func() { + os.Remove(path) + }() + TxDAGCacheSize = 10 + totalSize := uint64(100) + except := map[uint64]types.TxDAG{} + for i := uint64(0); i < totalSize-1; i++ { + except[i] = makeEmptyPlainTxDAG(1) + } + except[totalSize-1] = makeEmptyPlainTxDAG(510 * 1024) + writeFile, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) + require.NoError(t, err) + for num := uint64(0); num < totalSize; num++ { + require.NoError(t, writeTxDAGToFile(writeFile, TxDAGOutputItem{blockNumber: num, txDAG: except[num]})) + } + writeFile.Close() + + reader, err := NewTxDAGFileReader(path) + require.NoError(t, err) + for i := uint64(0); i < totalSize; i++ { + require.Equal(t, except[i], reader.TxDAG(i), i) + } + + // test reset to genesis + err = reader.Reset(0) + require.NoError(t, err) + for i := uint64(0); i < totalSize; i++ { + require.Equal(t, except[i], reader.TxDAG(i), i) + } + + // test reset skip + err = reader.Reset(totalSize - TxDAGCacheSize) + require.NoError(t, err) + for i := totalSize - TxDAGCacheSize; i < totalSize; i++ { + require.Equal(t, except[i], reader.TxDAG(i), i) + } +} + +func makeEmptyPlainTxDAG(cnt int, flags ...uint8) *types.PlainTxDAG { + dag := types.NewPlainTxDAG(cnt) + for i := range dag.TxDeps { + dag.TxDeps[i] = types.NewTxDep(make([]uint64, 0), flags...) + } + return dag +} diff --git a/core/error.go b/core/error.go index 8c691b17ff..7fa4556fdf 100644 --- a/core/error.go +++ b/core/error.go @@ -113,4 +113,7 @@ var ( // ErrSystemTxNotSupported is returned for any deposit tx with IsSystemTx=true after the Regolith fork ErrSystemTxNotSupported = errors.New("system tx not supported") + + // ErrParallelUnexpectedConflict is returned when execution finally get conflict error that should not occur + ErrParallelUnexpectedConflict = errors.New("parallel execution unexpected conflict") ) diff --git a/core/parallel_state_processor.go b/core/parallel_state_processor.go new file mode 100644 index 0000000000..e61ffe0cb9 --- /dev/null +++ b/core/parallel_state_processor.go @@ -0,0 +1,1236 @@ +package core + +import ( + "context" + "errors" + "fmt" + "github.com/ethereum/go-ethereum/metrics" + "runtime" + "sync" + "sync/atomic" + + "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/state" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/params" +) + +const ( + parallelPrimarySlot = 0 + parallelShadowSlot = 1 + stage2CheckNumber = 30 // ConfirmStage2 will check this number of transaction, to avoid too busy stage2 check + stage2AheadNum = 3 // enter ConfirmStage2 in advance to avoid waiting for Fat Tx +) + +var ( + FallbackToSerialProcessorErr = errors.New("fallback to serial processor") +) + +type ResultHandleEnv struct { + statedb *state.StateDB + gp *GasPool + txCount int +} + +type ParallelStateProcessor struct { + StateProcessor + parallelNum int // leave a CPU to dispatcher + slotState []*SlotState // idle, or pending messages + allTxReqs []*ParallelTxRequest + txResultChan chan *ParallelTxResult // to notify dispatcher that a tx is done + mergedTxIndex atomic.Int32 // the latest finalized tx index + pendingConfirmResults *sync.Map // tx could be executed several times, with several result to check + unconfirmedResults *sync.Map // for stage2 confirm, since pendingConfirmResults can not be accessed in stage2 loop + unconfirmedDBs *sync.Map // intermediate store of slotDB that is not verified + slotDBsToRelease *sync.Map + stopSlotChan chan struct{} + stopConfirmChan chan struct{} + debugConflictRedoNum int + + confirmStage2Chan chan int + stopConfirmStage2Chan chan struct{} + txReqExecuteRecord map[int]int + txReqExecuteCount int + inConfirmStage2 bool + targetStage2Count int + nextStage2TxIndex int + delayGasFee bool + + commonTxs []*types.Transaction + receipts types.Receipts + error error + resultMutex sync.RWMutex + resultProcessChan chan *ResultHandleEnv + resultAppendChan chan struct{} + parallelDBManager *state.ParallelDBManager +} + +func newParallelStateProcessor(config *params.ChainConfig, bc *BlockChain, engine consensus.Engine, parallelNum int) *ParallelStateProcessor { + processor := &ParallelStateProcessor{ + StateProcessor: *NewStateProcessor(config, bc, engine), + parallelNum: parallelNum, + } + processor.init() + return processor +} + +type MergedTxInfo struct { + slotDB *state.StateDB + StateObjectSuicided map[common.Address]struct{} + StateChangeSet map[common.Address]state.StateKeys + BalanceChangeSet map[common.Address]struct{} + CodeChangeSet map[common.Address]struct{} + AddrStateChangeSet map[common.Address]struct{} + txIndex int +} + +type SlotState struct { + pendingTxReqList []*ParallelTxRequest + primaryWakeUpChan chan struct{} + shadowWakeUpChan chan struct{} + primaryStopChan chan struct{} + shadowStopChan chan struct{} + activatedType int32 // 0: primary slot, 1: shadow slot +} + +type ParallelTxResult struct { + executedIndex int32 // record the current execute number of the tx + slotIndex int + txReq *ParallelTxRequest + receipt *types.Receipt + slotDB *state.ParallelStateDB + gpSlot *GasPool + evm *vm.EVM + result *ExecutionResult + originalNonce *uint64 + err error +} + +type ParallelTxRequest struct { + txIndex int + baseStateDB *state.StateDB + staticSlotIndex int + tx *types.Transaction + gasLimit uint64 + msg *Message + block *types.Block + vmConfig vm.Config + usedGas *uint64 + curTxChan chan int + runnable int32 // 0: not runnable 1: runnable - can be scheduled + executedNum atomic.Int32 + conflictIndex atomic.Int32 // the conflicted mainDB index, the txs will not be executed before this number + useDAG bool +} + +// init to initialize and start the execution goroutines +func (p *ParallelStateProcessor) init() { + log.Info("Parallel execution mode is enabled", "Parallel Num", p.parallelNum, + "CPUNum", runtime.NumCPU()) + p.txResultChan = make(chan *ParallelTxResult, 20000) + p.stopSlotChan = make(chan struct{}, 1) + p.stopConfirmChan = make(chan struct{}, 1) + p.stopConfirmStage2Chan = make(chan struct{}, 1) + + p.resultProcessChan = make(chan *ResultHandleEnv, 1) + p.resultAppendChan = make(chan struct{}, 20000) + + p.slotState = make([]*SlotState, p.parallelNum) + + p.parallelDBManager = state.NewParallelDBManager(20000, state.NewEmptySlotDB) + + quickMergeNum := 2 // p.parallelNum / 2 + for i := 0; i < p.parallelNum-quickMergeNum; i++ { + p.slotState[i] = &SlotState{ + primaryWakeUpChan: make(chan struct{}, 1), + shadowWakeUpChan: make(chan struct{}, 1), + primaryStopChan: make(chan struct{}, 1), + shadowStopChan: make(chan struct{}, 1), + } + // start the primary slot's goroutine + go func(slotIndex int) { + p.runSlotLoop(slotIndex, parallelPrimarySlot) // this loop will be permanent live + }(i) + + // start the shadow slot. + // It is back up of the primary slot to make sure transaction can be redone ASAP, + // since the primary slot could be busy at executing another transaction + go func(slotIndex int) { + p.runSlotLoop(slotIndex, parallelShadowSlot) + }(i) + } + + for i := p.parallelNum - quickMergeNum; i < p.parallelNum; i++ { + // init a quick merge slot + p.slotState[i] = &SlotState{ + primaryWakeUpChan: make(chan struct{}, 1), + shadowWakeUpChan: make(chan struct{}, 1), + primaryStopChan: make(chan struct{}, 1), + shadowStopChan: make(chan struct{}, 1), + } + go func(slotIndex int) { + p.runQuickMergeSlotLoop(slotIndex, parallelPrimarySlot) + }(i) + go func(slotIndex int) { + p.runQuickMergeSlotLoop(slotIndex, parallelShadowSlot) + }(i) + } + + p.confirmStage2Chan = make(chan int, 10) + go func() { + p.runConfirmStage2Loop() + }() + + go func() { + p.handlePendingResultLoop() + }() + +} + +// resetState clear slot state for each block. +func (p *ParallelStateProcessor) resetState(txNum int, statedb *state.StateDB) { + if txNum == 0 { + return + } + p.mergedTxIndex.Store(-1) + p.debugConflictRedoNum = 0 + p.inConfirmStage2 = false + + statedb.PrepareForParallel() + p.allTxReqs = make([]*ParallelTxRequest, txNum) + + for _, slot := range p.slotState { + slot.pendingTxReqList = make([]*ParallelTxRequest, 0) + slot.activatedType = parallelPrimarySlot + } + p.unconfirmedResults = new(sync.Map) + p.unconfirmedDBs = new(sync.Map) + p.slotDBsToRelease = new(sync.Map) + p.pendingConfirmResults = new(sync.Map) + p.txReqExecuteRecord = make(map[int]int, txNum) + p.txReqExecuteCount = 0 + p.nextStage2TxIndex = 0 +} + +// Benefits of StaticDispatch: +// +// ** try best to make Txs with same From() in same slot +// ** reduce IPC cost by dispatch in Unit +// ** make sure same From in same slot +// ** try to make it balanced, queue to the most hungry slot for new Address +func (p *ParallelStateProcessor) doStaticDispatch(txReqs []*ParallelTxRequest) { + fromSlotMap := make(map[common.Address]int, 100) + toSlotMap := make(map[common.Address]int, 100) + for _, txReq := range txReqs { + var slotIndex = -1 + if i, ok := fromSlotMap[txReq.msg.From]; ok { + // first: same From goes to same slot + slotIndex = i + } else if txReq.msg.To != nil { + // To Address, with txIndex sorted, could be in different slot. + if i, ok := toSlotMap[*txReq.msg.To]; ok { + slotIndex = i + } + } + + // not found, dispatch to most hungry slot + if slotIndex == -1 { + slotIndex = p.mostHungrySlot() + } + // update + fromSlotMap[txReq.msg.From] = slotIndex + if txReq.msg.To != nil { + toSlotMap[*txReq.msg.To] = slotIndex + } + + slot := p.slotState[slotIndex] + txReq.staticSlotIndex = slotIndex // txReq is better to be executed in this slot + slot.pendingTxReqList = append(slot.pendingTxReqList, txReq) + } +} + +func (p *ParallelStateProcessor) mostHungrySlot() int { + var ( + workload = len(p.slotState[0].pendingTxReqList) + slotIndex = 0 + ) + for i, slot := range p.slotState { // can start from index 1 + if len(slot.pendingTxReqList) < workload { + slotIndex = i + workload = len(slot.pendingTxReqList) + } + // just return the first slot with 0 workload + if workload == 0 { + return slotIndex + } + } + return slotIndex +} + +// hasConflict conducts conflict check +func (p *ParallelStateProcessor) hasConflict(txResult *ParallelTxResult, isStage2 bool) bool { + slotDB := txResult.slotDB + if txResult.err != nil { + log.Info("HasConflict due to err", "err", txResult.err) + return true + } else if slotDB.NeedsRedo() { + log.Info("HasConflict needsRedo") + // if there is any reason that indicates this transaction needs to redo, skip the conflict check + return true + } else { + // check whether the slot db reads during execution are correct. + if !slotDB.IsParallelReadsValid(isStage2) { + return true + } + } + return false +} + +func (p *ParallelStateProcessor) switchSlot(slotIndex int) { + slot := p.slotState[slotIndex] + if atomic.CompareAndSwapInt32(&slot.activatedType, parallelPrimarySlot, parallelShadowSlot) { + // switch from normal to shadow slot + if len(slot.shadowWakeUpChan) == 0 { + slot.shadowWakeUpChan <- struct{}{} + } + } else if atomic.CompareAndSwapInt32(&slot.activatedType, parallelShadowSlot, parallelPrimarySlot) { + // switch from shadow to normal slot + if len(slot.primaryWakeUpChan) == 0 { + slot.primaryWakeUpChan <- struct{}{} + } + } +} + +// executeInSlot do tx execution with thread local slot. +func (p *ParallelStateProcessor) executeInSlot(slotIndex int, txReq *ParallelTxRequest) *ParallelTxResult { + mIndex := p.mergedTxIndex.Load() + conflictIndex := txReq.conflictIndex.Load() + if mIndex < conflictIndex { + // The conflicted TX has not been finished executing, skip. + // the transaction failed at check(nonce or balance), actually it has not been executed yet. + atomic.CompareAndSwapInt32(&txReq.runnable, 0, 1) + return nil + } + execNum := txReq.executedNum.Add(1) + slotDB := state.NewSlotDB(txReq.baseStateDB, txReq.txIndex, int(mIndex), p.parallelDBManager, p.unconfirmedDBs, txReq.useDAG) + blockContext := NewEVMBlockContext(txReq.block.Header(), p.bc, nil, p.config, slotDB) // can share blockContext within a block for efficiency + txContext := NewEVMTxContext(txReq.msg) + vmenv := vm.NewEVM(blockContext, txContext, slotDB, p.config, txReq.vmConfig) + + rules := p.config.Rules(txReq.block.Number(), blockContext.Random != nil, blockContext.Time) + slotDB.Prepare(rules, txReq.msg.From, vmenv.Context.Coinbase, txReq.msg.To, vm.ActivePrecompiles(rules), txReq.msg.AccessList) + + // gasLimit not accurate, but it is ok for block import. + // each slot would use its own gas pool, and will do gas limit check later + gpSlot := new(GasPool).AddGas(txReq.gasLimit) // block.GasLimit() + + on := txReq.tx.Nonce() + if txReq.msg.IsDepositTx && p.config.IsOptimismRegolith(vmenv.Context.Time) { + on = slotDB.GetNonce(txReq.msg.From) + } + + slotDB.SetTxContext(txReq.tx.Hash(), txReq.txIndex) + evm, result, err := applyTransactionStageExecution(txReq.msg, gpSlot, slotDB, vmenv, p.delayGasFee) + txResult := ParallelTxResult{ + executedIndex: execNum, + slotIndex: slotIndex, + txReq: txReq, + receipt: nil, + slotDB: slotDB, + err: err, + gpSlot: gpSlot, + evm: evm, + result: result, + originalNonce: &on, + } + + if err == nil { + p.unconfirmedDBs.Store(txReq.txIndex, slotDB) + } else { + // the transaction failed at check(nonce or balance), actually it has not been executed yet. + // the error here can be either expected or unexpected. + // expected - the execution is correct and the error is normal result + // unexpected - the execution is incorrectly accessed the state because of parallelization. + // In both case, rerun with next version of stateDB, it is a waste and buggy to rerun with same + // version of stateDB that has been marked conflict. + // Therefore, treat it as conflict and rerun, leave the result to conflict check. + // Load conflict as it maybe updated by conflict checker or other execution slots. + // use old mIndex so that we can try the new one that is updated by other thread of merging + // during execution. + conflictIndex = txReq.conflictIndex.Load() + if conflictIndex < mIndex { + if txReq.conflictIndex.CompareAndSwap(conflictIndex, mIndex) { + log.Debug(fmt.Sprintf("Update conflictIndex in execution because of error: %s, new conflictIndex: %d", err.Error(), conflictIndex)) + } + } + atomic.CompareAndSwapInt32(&txReq.runnable, 0, 1) + // the error could be caused by unconfirmed balance reference, + // the balance could insufficient to pay its gas limit, which cause it preCheck.buyGas() failed + // redo could solve it. + log.Debug("In slot execution error", "error", err, + "slotIndex", slotIndex, "txIndex", txReq.txIndex) + } + p.unconfirmedResults.Store(txReq.txIndex, &txResult) + return &txResult +} + +// toConfirmTxIndex confirm a serial TxResults with same txIndex +func (p *ParallelStateProcessor) toConfirmTxIndex(targetTxIndex int, isStage2 bool) *ParallelTxResult { + if isStage2 { + if targetTxIndex <= int(p.mergedTxIndex.Load())+1 { + // `p.mergedTxIndex+1` is the one to be merged, + // in stage2, we do likely conflict check, for these not their turn. + return nil + } + } + + for { + // handle a targetTxIndex in a loop + var targetResult *ParallelTxResult + if isStage2 { + result, ok := p.unconfirmedResults.Load(targetTxIndex) + if !ok { + return nil + } + targetResult = result.(*ParallelTxResult) + + // in stage 2, don't schedule a new redo if the TxReq is: + // a.runnable: it will be redone + // b.running: the new result will be more reliable, we skip check right now + if atomic.LoadInt32(&targetResult.txReq.runnable) == 1 { + return nil + } + if targetResult.executedIndex < targetResult.txReq.executedNum.Load() { + // skip the intermediate result that is not the latest. + return nil + } + } else { + // pop one result as target result. + result, ok := p.pendingConfirmResults.LoadAndDelete(targetTxIndex) + if !ok { + return nil + } + targetResult = result.(*ParallelTxResult) + } + + valid := p.toConfirmTxIndexResult(targetResult, isStage2) + if !valid { + staticSlotIndex := targetResult.txReq.staticSlotIndex + conflictBase := targetResult.slotDB.BaseTxIndex() + conflictIndex := targetResult.txReq.conflictIndex.Load() + if conflictIndex < int32(conflictBase) { + if targetResult.txReq.conflictIndex.CompareAndSwap(conflictIndex, int32(conflictBase)) { + log.Debug("Update conflict index", "conflictIndex", conflictIndex, "conflictBase", conflictBase) + } + } + if isStage2 { + atomic.CompareAndSwapInt32(&targetResult.txReq.runnable, 0, 1) // needs redo + p.debugConflictRedoNum++ + // interrupt the slot's current routine, and switch to the other routine + p.switchSlot(staticSlotIndex) + return nil + } + + if _, ok := p.pendingConfirmResults.Load(targetTxIndex); !ok { // this is the last result to check, and it is not valid + // This means that the tx has been executed more than blockTxCount times, so it exits with the error. + if targetResult.txReq.txIndex == int(p.mergedTxIndex.Load())+1 && + targetResult.slotDB.BaseTxIndex() == int(p.mergedTxIndex.Load()) { + if targetResult.err != nil { + if false { // TODO: delete the printf + fmt.Printf("!!!!!!!!!!! Parallel execution exited with error!!!!!, txIndex:%d, err: %v\n", targetResult.txReq.txIndex, targetResult.err) + } + return targetResult + } else { + // abnormal exit with conflict error, need check the parallel algorithm + targetResult.err = ErrParallelUnexpectedConflict + if false { + fmt.Printf("!!!!!!!!!!! Parallel execution exited unexpected conflict!!!!!, txIndex:%d\n", targetResult.txReq.txIndex) + } + return targetResult + } + //} + } + atomic.CompareAndSwapInt32(&targetResult.txReq.runnable, 0, 1) // needs redo + p.debugConflictRedoNum++ + // interrupt its current routine, and switch to the other routine + p.switchSlot(staticSlotIndex) + // reclaim the result. + p.slotDBsToRelease.Store(targetResult.slotDB, targetResult.slotDB) + return nil + } + continue + } + if isStage2 { + // likely valid, but not sure, can not deliver + return nil + } + return targetResult + } +} + +// to confirm one txResult, return true if the result is valid +// if it is in Stage 2 it is a likely result, not 100% sure +func (p *ParallelStateProcessor) toConfirmTxIndexResult(txResult *ParallelTxResult, isStage2 bool) bool { + txReq := txResult.txReq + if p.hasConflict(txResult, isStage2) { + log.Info(fmt.Sprintf("HasConflict!! block: %d, txIndex: %d\n", txResult.txReq.block.NumberU64(), txResult.txReq.txIndex)) + return false + } + if isStage2 { // not its turn + return true // likely valid, not sure, not finalized right now. + } + + // goroutine unsafe operation will be handled from here for safety + gasConsumed := txReq.gasLimit - txResult.gpSlot.Gas() + if gasConsumed != txResult.result.UsedGas { + log.Error("gasConsumed != result.UsedGas mismatch", + "gasConsumed", gasConsumed, "result.UsedGas", txResult.result.UsedGas) + } + + // ok, time to do finalize, stage2 should not be parallel + txResult.receipt, txResult.err = applyTransactionStageFinalization(txResult.evm, txResult.result, + *txReq.msg, p.config, txResult.slotDB, txReq.block, + txReq.tx, txReq.usedGas, txResult.originalNonce) + return true +} + +func (p *ParallelStateProcessor) runSlotLoop(slotIndex int, slotType int32) { + curSlot := p.slotState[slotIndex] + var wakeupChan chan struct{} + var stopChan chan struct{} + + if slotType == parallelPrimarySlot { + wakeupChan = curSlot.primaryWakeUpChan + stopChan = curSlot.primaryStopChan + } else { + wakeupChan = curSlot.shadowWakeUpChan + stopChan = curSlot.shadowStopChan + } + + lastStartPos := 0 + for { + select { + case <-stopChan: + p.stopSlotChan <- struct{}{} + continue + case <-wakeupChan: + } + + interrupted := false + + for i := lastStartPos; i < len(curSlot.pendingTxReqList); i++ { + // for i, txReq := range curSlot.pendingTxReqList { + txReq := curSlot.pendingTxReqList[i] + if txReq.txIndex <= int(p.mergedTxIndex.Load()) { + continue + } + lastStartPos = i + + if txReq.conflictIndex.Load() > p.mergedTxIndex.Load() { + break + } + + if atomic.LoadInt32(&curSlot.activatedType) != slotType { + interrupted = true + break + } + + // first try next to be merged req. + nextIdx := p.mergedTxIndex.Load() + 1 + if nextIdx < int32(len(p.allTxReqs)) { + nextMergeReq := p.allTxReqs[nextIdx] + if nextMergeReq.runnable == 1 { + if atomic.CompareAndSwapInt32(&nextMergeReq.runnable, 1, 0) { + // execute. + res := p.executeInSlot(slotIndex, nextMergeReq) + if res != nil { + p.txResultChan <- res + } + } + } + } + + if txReq.runnable == 1 { + // try the next req in loop sequence. + if !atomic.CompareAndSwapInt32(&txReq.runnable, 1, 0) { + continue + } + res := p.executeInSlot(slotIndex, txReq) + if res == nil { + continue + } + p.txResultChan <- res + } + } + // switched to the other slot. + if interrupted { + continue + } + + // txReq in this Slot have all been executed, try steal one from other slot. + // as long as the TxReq is runnable, we steal it, mark it as stolen + + for j := int(p.mergedTxIndex.Load()) + 1; j < len(p.allTxReqs); j++ { + stealTxReq := p.allTxReqs[j] + if stealTxReq.txIndex <= int(p.mergedTxIndex.Load()) { + continue + } + + if stealTxReq.conflictIndex.Load() > p.mergedTxIndex.Load() { + break + } + + if atomic.LoadInt32(&curSlot.activatedType) != slotType { + interrupted = true + break + } + + // first try next to be merged req. + nextIdx := p.mergedTxIndex.Load() + 1 + if nextIdx < int32(len(p.allTxReqs)) { + nextMergeReq := p.allTxReqs[nextIdx] + if nextMergeReq.runnable == 1 { + if atomic.CompareAndSwapInt32(&nextMergeReq.runnable, 1, 0) { + // execute. + res := p.executeInSlot(slotIndex, nextMergeReq) + if res != nil { + p.txResultChan <- res + } + } + } + } + + if stealTxReq.runnable == 1 { + if !atomic.CompareAndSwapInt32(&stealTxReq.runnable, 1, 0) { + continue + } + res := p.executeInSlot(slotIndex, stealTxReq) + if res == nil { + continue + } + p.txResultChan <- res + } + } + } +} + +func (p *ParallelStateProcessor) runQuickMergeSlotLoop(slotIndex int, slotType int32) { + curSlot := p.slotState[slotIndex] + var wakeupChan chan struct{} + var stopChan chan struct{} + + if slotType == parallelPrimarySlot { + wakeupChan = curSlot.primaryWakeUpChan + stopChan = curSlot.primaryStopChan + } else { + wakeupChan = curSlot.shadowWakeUpChan + stopChan = curSlot.shadowStopChan + } + for { + select { + case <-stopChan: + p.stopSlotChan <- struct{}{} + continue + case <-wakeupChan: + } + + next := int(p.mergedTxIndex.Load()) + 1 + + executed := 5 + for i := next; i < len(p.allTxReqs); i++ { + txReq := p.allTxReqs[next] + if executed == 0 { + break + } + if txReq.txIndex <= int(p.mergedTxIndex.Load()) { + continue + } + if txReq.conflictIndex.Load() > p.mergedTxIndex.Load() { + break + } + if txReq.runnable == 1 { + if !atomic.CompareAndSwapInt32(&txReq.runnable, 1, 0) { + continue + } + res := p.executeInSlot(slotIndex, txReq) + if res != nil { + executed-- + p.txResultChan <- res + } + } + } + } +} + +func (p *ParallelStateProcessor) runConfirmStage2Loop() { + for { + select { + case <-p.stopConfirmStage2Chan: + for len(p.confirmStage2Chan) > 0 { + <-p.confirmStage2Chan + } + p.stopSlotChan <- struct{}{} + continue + case <-p.confirmStage2Chan: + for len(p.confirmStage2Chan) > 0 { + <-p.confirmStage2Chan // drain the chan to get the latest merged txIndex + } + } + // stage 2,if all tx have been executed at least once, and its result has been received. + // in Stage 2, we will run check when merge is advanced. + // more aggressive tx result confirm, even for these Txs not in turn + startTxIndex := int(p.mergedTxIndex.Load()) + 2 // stage 2's will start from the next target merge index + endTxIndex := startTxIndex + stage2CheckNumber + txSize := len(p.allTxReqs) + if endTxIndex > (txSize - 1) { + endTxIndex = txSize - 1 + } + log.Debug("runConfirmStage2Loop", "startTxIndex", startTxIndex, "endTxIndex", endTxIndex) + for txIndex := startTxIndex; txIndex < endTxIndex; txIndex++ { + p.toConfirmTxIndex(txIndex, true) + } + // make sure all slots are wake up + for i := 0; i < p.parallelNum; i++ { + p.switchSlot(i) + } + } +} + +func (p *ParallelStateProcessor) handleTxResults() *ParallelTxResult { + confirmedResult := p.toConfirmTxIndex(int(p.mergedTxIndex.Load())+1, false) + if confirmedResult == nil { + return nil + } + // schedule stage 2 when new Tx has been merged, schedule once and ASAP + // stage 2,if all tx have been executed at least once, and its result has been received. + // in Stage 2, we will run check when main DB is advanced, i.e., new Tx result has been merged. + if p.inConfirmStage2 && int(p.mergedTxIndex.Load()) >= p.nextStage2TxIndex { + p.nextStage2TxIndex = int(p.mergedTxIndex.Load()) + stage2CheckNumber + p.confirmStage2Chan <- int(p.mergedTxIndex.Load()) + } + return confirmedResult +} + +// wait until the next Tx is executed and its result is merged to the main stateDB +func (p *ParallelStateProcessor) confirmTxResults(statedb *state.StateDB, gp *GasPool) *ParallelTxResult { + result := p.handleTxResults() + if result == nil { + return nil + } + // ok, the tx result is valid and can be merged + if result.err != nil { + return result + } + + if err := gp.SubGas(result.receipt.GasUsed); err != nil { + log.Error("gas limit reached", "block", result.txReq.block.Number(), + "txIndex", result.txReq.txIndex, "GasUsed", result.receipt.GasUsed, "gp.Gas", gp.Gas()) + } + + resultTxIndex := result.txReq.txIndex + + var root []byte + header := result.txReq.block.Header() + + isByzantium := p.config.IsByzantium(header.Number) + isEIP158 := p.config.IsEIP158(header.Number) + result.slotDB.FinaliseForParallel(isByzantium || isEIP158, statedb) + + // merge slotDB into mainDB + statedb.MergeSlotDB(result.slotDB, result.receipt, resultTxIndex, result.result.delayFees) + + delayGasFee := result.result.delayFees + // add delayed gas fee + if delayGasFee != nil { + if delayGasFee.TipFee != nil { + statedb.AddBalance(delayGasFee.Coinbase, delayGasFee.TipFee) + } + if delayGasFee.BaseFee != nil { + statedb.AddBalance(params.OptimismBaseFeeRecipient, delayGasFee.BaseFee) + } + if delayGasFee.L1Fee != nil { + statedb.AddBalance(params.OptimismL1FeeRecipient, delayGasFee.L1Fee) + } + } + + // Do IntermediateRoot after mergeSlotDB. + if !isByzantium { + root = statedb.IntermediateRoot(isEIP158).Bytes() + } + result.receipt.PostState = root + + if resultTxIndex != int(p.mergedTxIndex.Load())+1 { + log.Error("ProcessParallel tx result out of order", "resultTxIndex", resultTxIndex, + "p.mergedTxIndex", p.mergedTxIndex.Load()) + } + p.mergedTxIndex.Store(int32(resultTxIndex)) + + // trigger all slot to run left conflicted txs + for _, slot := range p.slotState { + var wakeupChan chan struct{} + if slot.activatedType == parallelPrimarySlot { + wakeupChan = slot.primaryWakeUpChan + } else { + wakeupChan = slot.shadowWakeUpChan + } + select { + case wakeupChan <- struct{}{}: + default: + } + } + // schedule prefetch once only when unconfirmedResult is valid + if result.err == nil { + if _, ok := p.txReqExecuteRecord[resultTxIndex]; !ok { + p.txReqExecuteRecord[resultTxIndex] = 0 + p.txReqExecuteCount++ + statedb.AddrPrefetch(result.slotDB) + if !p.inConfirmStage2 && p.txReqExecuteCount == p.targetStage2Count { + p.inConfirmStage2 = true + } + } + p.txReqExecuteRecord[resultTxIndex]++ + } + // after merge, the slotDB will not accessible, reclaim the resource + p.slotDBsToRelease.Store(result.slotDB, result.slotDB) + return result +} + +func (p *ParallelStateProcessor) doCleanUp() { + // 1.clean up all slot: primary and shadow, to make sure they are stopped + for _, slot := range p.slotState { + slot.primaryStopChan <- struct{}{} + slot.shadowStopChan <- struct{}{} + <-p.stopSlotChan + <-p.stopSlotChan + } + // 2.discard delayed txResults if any + for { + if len(p.txResultChan) > 0 { + <-p.txResultChan + continue + } + break + } + // 3.make sure the confirmation routine is stopped + p.stopConfirmStage2Chan <- struct{}{} + <-p.stopSlotChan + + p.unconfirmedResults = nil + p.unconfirmedDBs = nil + p.pendingConfirmResults = nil + + go func() { + p.slotDBsToRelease.Range(func(key, value any) bool { + sdb := value.(*state.ParallelStateDB) + sdb.PutSyncPool(p.parallelDBManager) + return true + }) + }() +} + +// Process implements BEP-130 Parallel Transaction Execution +func (p *ParallelStateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg vm.Config) (types.Receipts, []*types.Log, uint64, error) { + var ( + usedGas = new(uint64) + header = block.Header() + gp = new(GasPool).AddGas(block.GasLimit()) + ) + + // 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) + } + if p.config.PreContractForkBlock != nil && p.config.PreContractForkBlock.Cmp(block.Number()) == 0 { + misc.ApplyPreContractHardFork(statedb) + } + + misc.EnsureCreate2Deployer(p.config, block.Time(), statedb) + + allTxs := block.Transactions() + p.resetState(len(allTxs), statedb) + + var ( + // with parallel mode, vmenv will be created inside of slot + blockContext = NewEVMBlockContext(block.Header(), p.bc, nil, p.config, statedb) + vmenv = vm.NewEVM(blockContext, vm.TxContext{}, statedb, p.config, cfg) + signer = types.MakeSigner(p.bc.chainConfig, block.Number(), block.Time()) + ) + + if beaconRoot := block.BeaconRoot(); beaconRoot != nil { + ProcessBeaconBlockRoot(*beaconRoot, vmenv, statedb) + } + statedb.MarkFullProcessed() + txDAG := cfg.TxDAG + + txNum := len(allTxs) + latestExcludedTx := -1 + // Iterate over and process the individual transactions + p.commonTxs = make([]*types.Transaction, 0, txNum) + p.receipts = make([]*types.Receipt, 0, txNum) + + parallelNum := p.parallelNum + + if txNum > parallelNum*2 && txNum >= 4 { + var wg sync.WaitGroup + errChan := make(chan error) + + begin := 0 + // first try to find latestExcludeTx, as for opBNB, they are the first consecutive txs. + for idx := 0; idx < len(allTxs); idx++ { + if txDAG != nil && txDAG.TxDep(idx).CheckFlag(types.ExcludedTxFlag) { + if err := p.transferTxs(allTxs, idx, signer, block, statedb, cfg, usedGas, latestExcludedTx); err != nil { + return nil, nil, 0, err + } + latestExcludedTx = idx + } else { + begin = idx + break + } + } + + // Create a cancelable context + ctx, cancel := context.WithCancel(context.Background()) + + // Create a pool of workers + transactionsPerWorker := (len(allTxs) - begin) / parallelNum + + // Create a pool of workers + for i := 0; i < parallelNum; i++ { + wg.Add(1) + go func(start, end int, signer types.Signer, blk *types.Block, sdb *state.StateDB, cfg vm.Config, usedGas *uint64) { + defer wg.Done() + for j := start; j < end; j++ { + select { + case <-ctx.Done(): + return // Exit the goroutine if the context is canceled + default: + if err := p.transferTxs(allTxs, j, signer, block, statedb, cfg, usedGas, latestExcludedTx); err != nil { + errChan <- err + cancel() // Cancel the context to stop other goroutines + return + } + } + } + }(begin+i*transactionsPerWorker, begin+(i+1)*transactionsPerWorker, signer, block, statedb, cfg, usedGas) + } + + // Distribute any remaining transactions + for i := begin + parallelNum*transactionsPerWorker; i < len(allTxs); i++ { + if err := p.transferTxs(allTxs, i, signer, block, statedb, cfg, usedGas, latestExcludedTx); err != nil { + errChan <- err + cancel() // Cancel the context to stop other goroutines + } + } + + // Wait for all workers to finish and handle errors + go func() { + wg.Wait() + close(errChan) + }() + + for err := range errChan { + return nil, nil, 0, err + } + // + } else { + for i, tx := range allTxs { + msg, err := TransactionToMessage(tx, signer, header.BaseFee) + if err != nil { + return nil, nil, 0, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err) + } + + // find the latestDepTx from TxDAG or latestExcludedTx + latestDepTx := -1 + if dep := types.TxDependency(txDAG, i); len(dep) > 0 { + latestDepTx = int(dep[len(dep)-1]) + } + if latestDepTx < latestExcludedTx { + latestDepTx = latestExcludedTx + } + + // parallel start, wrap an exec message, which will be dispatched to a slot + txReq := &ParallelTxRequest{ + txIndex: i, + baseStateDB: statedb, + staticSlotIndex: -1, + tx: tx, + gasLimit: block.GasLimit(), // gp.Gas(). + msg: msg, + block: block, + vmConfig: cfg, + usedGas: usedGas, + curTxChan: make(chan int, 1), + runnable: 1, // 0: not runnable, 1: runnable + useDAG: txDAG != nil, + } + txReq.executedNum.Store(0) + txReq.conflictIndex.Store(-2) + if latestDepTx >= 0 { + txReq.conflictIndex.Store(int32(latestDepTx)) + } + p.allTxReqs[i] = txReq + if txDAG != nil && txDAG.TxDep(i).CheckFlag(types.ExcludedTxFlag) { + latestExcludedTx = i + } + } + } + allTxCount := len(p.allTxReqs) + // set up stage2 enter criteria + p.targetStage2Count = allTxCount + if p.targetStage2Count > 50 { + // usually, the last Tx could be the bottleneck it could be very slow, + // so it is better for us to enter stage 2 a bit earlier + p.targetStage2Count = p.targetStage2Count - stage2AheadNum + } + + p.delayGasFee = false + p.doStaticDispatch(p.allTxReqs) + if txDAG != nil && txDAG.DelayGasFeeDistribution() { + p.delayGasFee = true + } + + // after static dispatch, we notify the slot to work. + for _, slot := range p.slotState { + slot.primaryWakeUpChan <- struct{}{} + } + + // kick off the result handler. + p.resultProcessChan <- &ResultHandleEnv{statedb: statedb, gp: gp, txCount: allTxCount} + for { + if int(p.mergedTxIndex.Load())+1 == allTxCount { + // put it ahead of chan receive to avoid waiting for empty block + break + } + unconfirmedResult := <-p.txResultChan + if unconfirmedResult.txReq == nil { + // all tx results are merged. + break + } + + unconfirmedTxIndex := unconfirmedResult.txReq.txIndex + if unconfirmedTxIndex <= int(p.mergedTxIndex.Load()) { + log.Debug("drop merged txReq", "unconfirmedTxIndex", unconfirmedTxIndex, "p.mergedTxIndex", p.mergedTxIndex.Load()) + continue + } + prevResult, ok := p.pendingConfirmResults.Load(unconfirmedTxIndex) + if !ok || prevResult.(*ParallelTxResult).slotDB.BaseTxIndex() < unconfirmedResult.slotDB.BaseTxIndex() { + p.pendingConfirmResults.Store(unconfirmedTxIndex, unconfirmedResult) + p.resultAppendChan <- struct{}{} + } + } + + // clean up when the block is processed + p.doCleanUp() + + if p.error != nil { + return nil, nil, 0, p.error + } + + // len(commonTxs) could be 0, such as: https://bscscan.com/block/14580486 + // all txs have been merged at this point, no need to acquire the lock of commonTxs + if p.mergedTxIndex.Load() >= 0 && p.debugConflictRedoNum > 0 { + log.Info("ProcessParallel tx all done", "block", header.Number, "usedGas", *usedGas, + "txNum", txNum, + "len(commonTxs)", len(p.commonTxs), + "conflictNum", p.debugConflictRedoNum, + "redoRate(%)", 100*(p.debugConflictRedoNum)/len(p.commonTxs), + "txDAG", txDAG != nil) + } + if metrics.EnabledExpensive { + parallelTxNumMeter.Mark(int64(len(p.commonTxs))) + parallelConflictTxNumMeter.Mark(int64(p.debugConflictRedoNum)) + } + + // Fail if Shanghai not enabled and len(withdrawals) is non-zero. + withdrawals := block.Withdrawals() + if len(withdrawals) > 0 && !p.config.IsShanghai(block.Number(), block.Time()) { + return nil, nil, 0, errors.New("withdrawals before shanghai") + } + // Finalize the block, applying any consensus engine specific extras (e.g. block rewards) + p.engine.Finalize(p.bc, header, statedb, p.commonTxs, block.Uncles(), withdrawals) + + var allLogs []*types.Log + for _, receipt := range p.receipts { + allLogs = append(allLogs, receipt.Logs...) + } + return p.receipts, allLogs, *usedGas, nil +} + +func (p *ParallelStateProcessor) handlePendingResultLoop() { + var info *ResultHandleEnv + var stateDB *state.StateDB + var gp *GasPool + var txCount int + for { + select { + case info = <-p.resultProcessChan: + stateDB = info.statedb + gp = info.gp + txCount = info.txCount + log.Debug("handlePendingResult get Env", "stateDBTx", stateDB.TxIndex(), "gp", gp.String(), "txCount", txCount) + case <-p.resultAppendChan: + } + + // if all merged, notify the main routine. continue to wait for next block. + if p.error != nil || p.mergedTxIndex.Load()+1 == int32(txCount) { + // log.Info("handlePendingResult merged all") + p.txResultChan <- &ParallelTxResult{txReq: nil, result: nil} + // clear the pending chan. + for len(p.resultAppendChan) > 0 { + <-p.resultAppendChan + } + continue + } + // busy waiting. + for { + nextTxIndex := int(p.mergedTxIndex.Load()) + 1 + if p.error != nil || nextTxIndex == txCount { + p.txResultChan <- &ParallelTxResult{txReq: nil, result: nil} + // clear the pending chan. + for len(p.resultAppendChan) > 0 { + <-p.resultAppendChan + } + break + } + if _, ok := p.pendingConfirmResults.Load(nextTxIndex); !ok { + break + } + log.Debug("Start to check result", "TxIndex", int(nextTxIndex), "stateDBTx", stateDB.TxIndex(), "gp", gp.String()) + + result := p.confirmTxResults(stateDB, gp) + if result == nil { + break + } else { + log.Debug("in Confirm Loop - after confirmTxResults", + "mergedIndex", p.mergedTxIndex.Load(), + "confirmedIndex", result.txReq.txIndex, + "result.err", result.err) + } + p.resultMutex.Lock() + // update tx result + if result.err != nil { + log.Error("ProcessParallel a failed tx", "resultSlotIndex", result.slotIndex, + "resultTxIndex", result.txReq.txIndex, "result.err", result.err) + p.error = fmt.Errorf("could not apply tx %d [%v]: %w", result.txReq.txIndex, result.txReq.tx.Hash().Hex(), result.err) + p.resultMutex.Unlock() + continue + } + p.commonTxs = append(p.commonTxs, result.txReq.tx) + p.receipts = append(p.receipts, result.receipt) + p.resultMutex.Unlock() + } + } +} + +func (p *ParallelStateProcessor) transferTxs(txs types.Transactions, i int, signer types.Signer, block *types.Block, statedb *state.StateDB, cfg vm.Config, usedGas *uint64, latestExcludedTx int) error { + if p.allTxReqs[i] != nil { + return nil + } + tx := txs[i] + txDAG := cfg.TxDAG + msg, err := TransactionToMessage(tx, signer, block.Header().BaseFee) + if err != nil { + return fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err) + } + + // find the latestDepTx from TxDAG or latestExcludedTx + latestDepTx := -1 + if dep := types.TxDependency(txDAG, i); len(dep) > 0 { + latestDepTx = int(dep[len(dep)-1]) + } + if latestDepTx < latestExcludedTx { + latestDepTx = latestExcludedTx + } + + // parallel start, wrap an exec message, which will be dispatched to a slot + txReq := &ParallelTxRequest{ + txIndex: i, + baseStateDB: statedb, + staticSlotIndex: -1, + tx: tx, + gasLimit: block.GasLimit(), // gp.Gas(). + msg: msg, + block: block, + vmConfig: cfg, + usedGas: usedGas, + curTxChan: make(chan int, 1), + runnable: 1, // 0: not runnable, 1: runnable + useDAG: txDAG != nil, + } + txReq.executedNum.Store(0) + txReq.conflictIndex.Store(-2) + if latestDepTx >= 0 { + txReq.conflictIndex.Store(int32(latestDepTx)) + } + p.allTxReqs[i] = txReq + return nil +} + +func applyTransactionStageExecution(msg *Message, gp *GasPool, statedb *state.ParallelStateDB, evm *vm.EVM, delayGasFee bool) (*vm.EVM, *ExecutionResult, error) { + // Create a new context to be used in the EVM environment. + txContext := NewEVMTxContext(msg) + evm.Reset(txContext, statedb) + + // Apply the transaction to the current state (included in the env). + var ( + result *ExecutionResult + err error + ) + if delayGasFee { + result, err = ApplyMessageDelayGasFee(evm, msg, gp) + } else { + result, err = ApplyMessage(evm, msg, gp) + } + + if err != nil { + return nil, nil, err + } + + return evm, result, err +} + +func applyTransactionStageFinalization(evm *vm.EVM, result *ExecutionResult, msg Message, config *params.ChainConfig, statedb *state.ParallelStateDB, block *types.Block, tx *types.Transaction, usedGas *uint64, nonce *uint64) (*types.Receipt, error) { + + *usedGas += result.UsedGas + // Create a new receipt for the transaction, storing the intermediate root and gas used by the tx. + receipt := &types.Receipt{Type: tx.Type(), PostState: nil, CumulativeGasUsed: *usedGas} + if result.Failed() { + receipt.Status = types.ReceiptStatusFailed + } else { + receipt.Status = types.ReceiptStatusSuccessful + } + receipt.TxHash = tx.Hash() + receipt.GasUsed = result.UsedGas + + if msg.IsDepositTx && config.IsOptimismRegolith(evm.Context.Time) { + // The actual nonce for deposit transactions is only recorded from Regolith onwards and + // otherwise must be nil. + receipt.DepositNonce = nonce + // The DepositReceiptVersion for deposit transactions is only recorded from Canyon onwards + // and otherwise must be nil. + if config.IsOptimismCanyon(evm.Context.Time) { + receipt.DepositReceiptVersion = new(uint64) + *receipt.DepositReceiptVersion = types.CanyonDepositReceiptVersion + } + } + if tx.Type() == types.BlobTxType { + receipt.BlobGasUsed = uint64(len(tx.BlobHashes()) * params.BlobTxBlobGasPerBlob) + receipt.BlobGasPrice = evm.Context.BlobBaseFee + } + // If the transaction created a contract, store the creation address in the receipt. + if msg.To == nil { + receipt.ContractAddress = crypto.CreateAddress(evm.TxContext.Origin, *nonce) + } + // Set the receipt logs and create the bloom filter. + receipt.Logs = statedb.GetLogs(tx.Hash(), block.NumberU64(), block.Hash()) + receipt.Bloom = types.CreateBloom(types.Receipts{receipt}) + receipt.BlockHash = block.Hash() + receipt.BlockNumber = block.Number() + receipt.TransactionIndex = uint(statedb.TxIndex()) + return receipt, nil +} diff --git a/core/state/access_list.go b/core/state/access_list.go index 4194691345..942829787a 100644 --- a/core/state/access_list.go +++ b/core/state/access_list.go @@ -134,3 +134,36 @@ func (al *accessList) DeleteSlot(address common.Address, slot common.Hash) { func (al *accessList) DeleteAddress(address common.Address) { delete(al.addresses, address) } + +// Copy creates an independent copy of an accessList. +func (dest *accessList) Append(src *accessList) *accessList { + for addr, sIdx := range src.addresses { + if i, present := dest.addresses[addr]; present { + // dest already has addr. + if sIdx >= 0 { + // has slot in list + if i == -1 { + dest.addresses[addr] = len(dest.slots) + slotmap := src.slots[sIdx] + dest.slots = append(dest.slots, slotmap) + } else { + slotmap := src.slots[sIdx] + for hash := range slotmap { + if _, ok := dest.slots[i][hash]; !ok { + dest.slots[i][hash] = struct{}{} + } + } + } + } + } else { + // dest doesn't have the address + dest.addresses[addr] = -1 + if sIdx >= 0 { + dest.addresses[addr] = len(dest.slots) + slotmap := src.slots[sIdx] + dest.slots = append(dest.slots, slotmap) + } + } + } + return dest +} diff --git a/core/state/dump.go b/core/state/dump.go index 55abb50f1c..1b0c0c0dae 100644 --- a/core/state/dump.go +++ b/core/state/dump.go @@ -160,7 +160,7 @@ func (s *StateDB) DumpToCollector(c DumpCollector, conf *DumpConfig) (nextKey [] address = &addr account.Address = address } - obj := newObject(s, addr, &data) + obj := newObject(s, s.isParallel, addr, &data) if !conf.SkipCode { account.Code = obj.Code() } diff --git a/core/state/interface.go b/core/state/interface.go new file mode 100644 index 0000000000..3e808aa82e --- /dev/null +++ b/core/state/interface.go @@ -0,0 +1,81 @@ +// Copyright 2016 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package state + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/holiman/uint256" +) + +// StateDBer is copied from vm/interface.go +// It is used by StateObject & Journal right now, to abstract StateDB & ParallelStateDB +type StateDBer interface { + getBaseStateDB() *StateDB + getStateObject(common.Address) *stateObject // only accessible for journal + storeStateObj(common.Address, *stateObject) // only accessible for journal + + CreateAccount(common.Address) + + SubBalance(common.Address, *uint256.Int) + AddBalance(common.Address, *uint256.Int) + GetBalance(common.Address) *uint256.Int + + GetNonce(common.Address) uint64 + SetNonce(common.Address, uint64) + + GetCodeHash(common.Address) common.Hash + GetCode(common.Address) []byte + SetCode(common.Address, []byte) + GetCodeSize(common.Address) int + + AddRefund(uint64) + SubRefund(uint64) + GetRefund() uint64 + + GetCommittedState(common.Address, common.Hash) common.Hash + GetState(common.Address, common.Hash) common.Hash + SetState(common.Address, common.Hash, common.Hash) + + SelfDestruct(common.Address) + HasSelfDestructed(common.Address) bool + + // Exist reports whether the given account exists in state. + // Notably this should also return true for suicided accounts. + Exist(common.Address) bool + // Empty returns whether the given account is empty. Empty + // is defined according to EIP161 (balance = nonce = code = 0). + Empty(common.Address) bool + + //PrepareAccessList(sender common.Address, dest *common.Address, precompiles []common.Address, txAccesses types.AccessList) + AddressInAccessList(addr common.Address) bool + SlotInAccessList(addr common.Address, slot common.Hash) (addressOk bool, slotOk bool) + // AddAddressToAccessList adds the given address to the access list. This operation is safe to perform + // even if the feature/fork is not active yet + AddAddressToAccessList(addr common.Address) + // AddSlotToAccessList adds the given (address,slot) to the access list. This operation is safe to perform + // even if the feature/fork is not active yet + AddSlotToAccessList(addr common.Address, slot common.Hash) + + RevertToSnapshot(int) + Snapshot() int + + AddLog(*types.Log) + AddPreimage(common.Hash, []byte) + + GetStateObjectFromUnconfirmedDB(addr common.Address) (*stateObject, bool) +} diff --git a/core/state/journal.go b/core/state/journal.go index 6cdc1fc868..d436dbd5ac 100644 --- a/core/state/journal.go +++ b/core/state/journal.go @@ -25,7 +25,7 @@ import ( // reverted on demand. type journalEntry interface { // revert undoes the changes introduced by this journal entry. - revert(*StateDB) + revert(StateDBer) // dirtied returns the Ethereum address modified by this journal entry. dirtied() *common.Address @@ -56,10 +56,10 @@ func (j *journal) append(entry journalEntry) { // revert undoes a batch of journalled modifications along with any reverted // dirty handling too. -func (j *journal) revert(statedb *StateDB, snapshot int) { +func (j *journal) revert(dber StateDBer, snapshot int) { for i := len(j.entries) - 1; i >= snapshot; i-- { // Undo the changes made by the operation - j.entries[i].revert(statedb) + j.entries[i].revert(dber) // Drop any dirty tracking induced by the change if addr := j.entries[i].dirtied(); addr != nil { @@ -151,8 +151,18 @@ type ( } ) -func (ch createObjectChange) revert(s *StateDB) { - delete(s.stateObjects, *ch.account) +func (ch createObjectChange) revert(dber StateDBer) { + s := dber.getBaseStateDB() + if s.parallel.isSlotDB { + delete(s.parallel.dirtiedStateObjectsInSlot, *ch.account) + delete(s.parallel.addrStateChangesInSlot, *ch.account) + delete(s.parallel.nonceChangesInSlot, *ch.account) + delete(s.parallel.balanceChangesInSlot, *ch.account) + delete(s.parallel.codeChangesInSlot, *ch.account) + delete(s.parallel.kvChangesInSlot, *ch.account) + } else { + s.deleteStateObj(*ch.account) + } delete(s.stateObjectsDirty, *ch.account) } @@ -160,10 +170,27 @@ func (ch createObjectChange) dirtied() *common.Address { return ch.account } -func (ch resetObjectChange) revert(s *StateDB) { - s.setStateObject(ch.prev) +func (ch resetObjectChange) revert(dber StateDBer) { + s := dber.getBaseStateDB() + if s.parallel.isSlotDB { + // ch.prev must be from dirtiedStateObjectsInSlot, put it back + s.parallel.dirtiedStateObjectsInSlot[ch.prev.address] = ch.prev + } else { + // ch.prev was got from main DB, put it back to main DB. + s.setStateObject(ch.prev) + } + if !ch.prevdestruct { - delete(s.stateObjectsDestruct, ch.prev.address) + s.stateObjectDestructLock.Lock() + s.removeStateObjectsDestruct(ch.prev.address) + s.stateObjectDestructLock.Unlock() + if s.isParallel && s.parallel.isSlotDB { + s.snapParallelLock.Lock() + if _, ok := s.snapDestructs[ch.prev.address]; ok { + delete(s.snapDestructs, ch.prev.address) + } + s.snapParallelLock.Unlock() + } } if ch.prevAccount != nil { s.accounts[ch.prev.addrHash] = ch.prevAccount @@ -183,8 +210,8 @@ func (ch resetObjectChange) dirtied() *common.Address { return ch.account } -func (ch selfDestructChange) revert(s *StateDB) { - obj := s.getStateObject(*ch.account) +func (ch selfDestructChange) revert(dber StateDBer) { + obj := dber.getStateObject(*ch.account) if obj != nil { obj.selfDestructed = ch.prev obj.setBalance(ch.prevbalance) @@ -197,46 +224,47 @@ func (ch selfDestructChange) dirtied() *common.Address { var ripemd = common.HexToAddress("0000000000000000000000000000000000000003") -func (ch touchChange) revert(s *StateDB) { +func (ch touchChange) revert(dber StateDBer) { } func (ch touchChange) dirtied() *common.Address { return ch.account } -func (ch balanceChange) revert(s *StateDB) { - s.getStateObject(*ch.account).setBalance(ch.prev) +func (ch balanceChange) revert(dber StateDBer) { + dber.getStateObject(*ch.account).setBalance(ch.prev) } func (ch balanceChange) dirtied() *common.Address { return ch.account } -func (ch nonceChange) revert(s *StateDB) { - s.getStateObject(*ch.account).setNonce(ch.prev) +func (ch nonceChange) revert(dber StateDBer) { + dber.getStateObject(*ch.account).setNonce(ch.prev) } func (ch nonceChange) dirtied() *common.Address { return ch.account } -func (ch codeChange) revert(s *StateDB) { - s.getStateObject(*ch.account).setCode(common.BytesToHash(ch.prevhash), ch.prevcode) +func (ch codeChange) revert(dber StateDBer) { + dber.getStateObject(*ch.account).setCode(common.BytesToHash(ch.prevhash), ch.prevcode) } func (ch codeChange) dirtied() *common.Address { return ch.account } -func (ch storageChange) revert(s *StateDB) { - s.getStateObject(*ch.account).setState(ch.key, ch.prevalue) +func (ch storageChange) revert(dber StateDBer) { + dber.getStateObject(*ch.account).setState(ch.key, ch.prevalue) } func (ch storageChange) dirtied() *common.Address { return ch.account } -func (ch transientStorageChange) revert(s *StateDB) { +func (ch transientStorageChange) revert(dber StateDBer) { + s := dber.getBaseStateDB() s.setTransientState(*ch.account, ch.key, ch.prevalue) } @@ -244,7 +272,8 @@ func (ch transientStorageChange) dirtied() *common.Address { return nil } -func (ch refundChange) revert(s *StateDB) { +func (ch refundChange) revert(dber StateDBer) { + s := dber.getBaseStateDB() s.refund = ch.prev } @@ -252,7 +281,8 @@ func (ch refundChange) dirtied() *common.Address { return nil } -func (ch addLogChange) revert(s *StateDB) { +func (ch addLogChange) revert(dber StateDBer) { + s := dber.getBaseStateDB() logs := s.logs[ch.txhash] if len(logs) == 1 { delete(s.logs, ch.txhash) @@ -266,7 +296,8 @@ func (ch addLogChange) dirtied() *common.Address { return nil } -func (ch addPreimageChange) revert(s *StateDB) { +func (ch addPreimageChange) revert(dber StateDBer) { + s := dber.getBaseStateDB() delete(s.preimages, ch.hash) } @@ -274,7 +305,7 @@ func (ch addPreimageChange) dirtied() *common.Address { return nil } -func (ch accessListAddAccountChange) revert(s *StateDB) { +func (ch accessListAddAccountChange) revert(dber StateDBer) { /* One important invariant here, is that whenever a (addr, slot) is added, if the addr is not already present, the add causes two journal entries: @@ -284,6 +315,7 @@ func (ch accessListAddAccountChange) revert(s *StateDB) { (addr) at this point, since no storage adds can remain when come upon a single (addr) change. */ + s := dber.getBaseStateDB() s.accessList.DeleteAddress(*ch.address) } @@ -291,7 +323,8 @@ func (ch accessListAddAccountChange) dirtied() *common.Address { return nil } -func (ch accessListAddSlotChange) revert(s *StateDB) { +func (ch accessListAddSlotChange) revert(dber StateDBer) { + s := dber.getBaseStateDB() s.accessList.DeleteSlot(*ch.address, *ch.slot) } diff --git a/core/state/parallel_statedb.go b/core/state/parallel_statedb.go new file mode 100644 index 0000000000..4ddc2bdac4 --- /dev/null +++ b/core/state/parallel_statedb.go @@ -0,0 +1,1903 @@ +package state + +import ( + "bytes" + "fmt" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/log" + "github.com/holiman/uint256" + "runtime" + "sort" + "sync" +) + +const defaultNumOfSlots = 5 + +var parallelKvOnce sync.Once + +type ParallelKvCheckUnit struct { + addr common.Address + key common.Hash + val common.Hash +} + +type ParallelKvCheckMessage struct { + slotDB *ParallelStateDB + isStage2 bool + kvUnit ParallelKvCheckUnit +} + +var parallelKvCheckReqCh chan ParallelKvCheckMessage +var parallelKvCheckResCh chan bool + +type ParallelStateDB struct { + StateDB +} + +func (s *ParallelStateDB) GetRefund() uint64 { + return s.refund +} + +func (s *ParallelStateDB) AddressInAccessList(addr common.Address) bool { + return s.accessList.ContainsAddress(addr) +} + +func (s *ParallelStateDB) SlotInAccessList(addr common.Address, slot common.Hash) (addressOk bool, slotOk bool) { + return s.accessList.Contains(addr, slot) +} + +func (s *ParallelStateDB) AddAddressToAccessList(addr common.Address) { + if s.accessList.AddAddress(addr) { + s.journal.append(accessListAddAccountChange{&addr}) + } +} + +func (s *ParallelStateDB) AddSlotToAccessList(addr common.Address, slot common.Hash) { + addrMod, slotMod := s.accessList.AddSlot(addr, slot) + if addrMod { + // In practice, this should not happen, since there is no way to enter the + // scope of 'address' without having the 'address' become already added + // to the access list (via call-variant, create, etc). + // Better safe than sorry, though + s.journal.append(accessListAddAccountChange{&addr}) + } + if slotMod { + s.journal.append(accessListAddSlotChange{ + address: &addr, + slot: &slot, + }) + } +} + +func (s *ParallelStateDB) Snapshot() int { + id := s.nextRevisionId + s.nextRevisionId++ + s.validRevisions = append(s.validRevisions, revision{id, s.journal.length()}) + return id +} + +func hasKvConflict(slotDB *ParallelStateDB, addr common.Address, key common.Hash, val common.Hash, isStage2 bool) bool { + mainDB := slotDB.parallel.baseStateDB + + if isStage2 { // update slotDB's unconfirmed DB list and try + if slotDB.parallel.useDAG { + // DAG never reads from unconfirmedDB, skip check. + return false + } + if valUnconfirm, ok := slotDB.getKVFromUnconfirmedDB(addr, key); ok { + if !bytes.Equal(val.Bytes(), valUnconfirm.Bytes()) { + log.Debug("IsSlotDBReadsValid KV read is invalid in unconfirmed", "addr", addr, + "valSlot", val, "valUnconfirm", valUnconfirm, + "SlotIndex", slotDB.parallel.SlotIndex, + "txIndex", slotDB.txIndex, "baseTxIndex", slotDB.parallel.baseTxIndex) + return true + } + } + } + valMain := slotDB.getStateFromMainNoUpdate(addr, key) // mainDB.GetStateNoUpdate(addr, key) + + if !bytes.Equal(val.Bytes(), valMain.Bytes()) { + log.Debug("hasKvConflict is invalid", "addr", addr, + "key", key, "valSlot", val, + "valMain", valMain, "SlotIndex", slotDB.parallel.SlotIndex, + "txIndex", slotDB.txIndex, "baseTxIndex", slotDB.parallel.baseTxIndex, + "mainDB.TxIndex", mainDB.TxIndex()) + return true // return false, Range will be terminated. + } + return false +} + +// StartKvCheckLoop start several routines to do conflict check +func StartKvCheckLoop() { + parallelKvCheckReqCh = make(chan ParallelKvCheckMessage, 200) + parallelKvCheckResCh = make(chan bool, 10) + for i := 0; i < runtime.NumCPU(); i++ { + go func() { + for { + kvEle1 := <-parallelKvCheckReqCh + parallelKvCheckResCh <- hasKvConflict(kvEle1.slotDB, kvEle1.kvUnit.addr, + kvEle1.kvUnit.key, kvEle1.kvUnit.val, kvEle1.isStage2) + } + }() + } +} + +// NewSlotDB creates a new State DB based on the provided StateDB. +// With parallel, each execution slot would have its own StateDB. +// This method must be called after the baseDB call PrepareParallel() +func NewSlotDB(db *StateDB, txIndex int, baseTxIndex int, manager *ParallelDBManager, unconfirmedDBs *sync.Map, useDAG bool) *ParallelStateDB { + slotDB := db.CopyForSlot(manager) + slotDB.txIndex = txIndex + slotDB.originalRoot = db.originalRoot + slotDB.parallel.baseStateDB = db + slotDB.parallel.baseTxIndex = baseTxIndex + slotDB.parallel.unconfirmedDBs = unconfirmedDBs + slotDB.parallel.useDAG = useDAG + return slotDB +} + +func (s *ParallelStateDB) PutSyncPool(parallelDBManager *ParallelDBManager) { + for key := range s.parallel.locatStateObjects { + delete(s.parallel.locatStateObjects, key) + } + addressToStateObjectsPool.Put(s.parallel.locatStateObjects) + + for key := range s.parallel.codeReadsInSlot { + delete(s.parallel.codeReadsInSlot, key) + } + addressToBytesPool.Put(s.parallel.codeReadsInSlot) + + for key := range s.parallel.codeHashReadsInSlot { + delete(s.parallel.codeHashReadsInSlot, key) + } + addressToHashPool.Put(s.parallel.codeHashReadsInSlot) + + for key := range s.parallel.codeChangesInSlot { + delete(s.parallel.codeChangesInSlot, key) + } + addressToStructPool.Put(s.parallel.codeChangesInSlot) + + for key := range s.parallel.kvChangesInSlot { + delete(s.parallel.kvChangesInSlot, key) + } + addressToStateKeysPool.Put(s.parallel.kvChangesInSlot) + + for key := range s.parallel.kvReadsInSlot { + delete(s.parallel.kvReadsInSlot, key) + } + addressToStoragePool.Put(s.parallel.kvReadsInSlot) + + for key := range s.parallel.balanceChangesInSlot { + delete(s.parallel.balanceChangesInSlot, key) + } + addressToStructPool.Put(s.parallel.balanceChangesInSlot) + + for key := range s.parallel.balanceReadsInSlot { + delete(s.parallel.balanceReadsInSlot, key) + } + balancePool.Put(s.parallel.balanceReadsInSlot) + + for key := range s.parallel.addrStateReadsInSlot { + delete(s.parallel.addrStateReadsInSlot, key) + } + addressToBoolPool.Put(s.parallel.addrStateReadsInSlot) + + for key := range s.parallel.addrStateChangesInSlot { + delete(s.parallel.addrStateChangesInSlot, key) + } + addressToBoolPool.Put(s.parallel.addrStateChangesInSlot) + + for key := range s.parallel.nonceChangesInSlot { + delete(s.parallel.nonceChangesInSlot, key) + } + addressToStructPool.Put(s.parallel.nonceChangesInSlot) + + for key := range s.parallel.nonceReadsInSlot { + delete(s.parallel.nonceReadsInSlot, key) + } + addressToUintPool.Put(s.parallel.nonceReadsInSlot) + + for key := range s.parallel.addrSnapDestructsReadsInSlot { + delete(s.parallel.addrSnapDestructsReadsInSlot, key) + } + addressToBoolPool.Put(s.parallel.addrSnapDestructsReadsInSlot) + + for key := range s.parallel.dirtiedStateObjectsInSlot { + delete(s.parallel.dirtiedStateObjectsInSlot, key) + } + addressToStateObjectsPool.Put(s.parallel.dirtiedStateObjectsInSlot) + + for key := range s.stateObjectsPending { + delete(s.stateObjectsPending, key) + } + addressToStructPool.Put(s.stateObjectsPending) + + for key := range s.stateObjectsDirty { + delete(s.stateObjectsDirty, key) + } + addressToStructPool.Put(s.stateObjectsDirty) + + for key := range s.logs { + delete(s.logs, key) + } + logsPool.Put(s.logs) + + for key := range s.journal.dirties { + delete(s.journal.dirties, key) + } + s.journal.entries = s.journal.entries[:0] + journalPool.Put(s.journal) + + for key := range s.snapDestructs { + delete(s.snapDestructs, key) + } + addressToStructPool.Put(s.snapDestructs) + + for key := range s.parallel.createdObjectRecord { + delete(s.parallel.createdObjectRecord, key) + } + addressToStructPool.Put(s.parallel.createdObjectRecord) + + s.reset() + parallelDBManager.reclaim(s) +} + +// getStateDBBasePtr get the pointer of parallelStateDB. +func (s *ParallelStateDB) getStateDBBasePtr() *StateDB { + return &s.StateDB +} + +func (s *ParallelStateDB) SetSlotIndex(index int) { + s.parallel.SlotIndex = index +} + +// getStateObject get the state object from parallel stateDB for journal revert. +// for parallel execution, try to get dirty StateObject in slot first. +func (s *ParallelStateDB) getStateObject(addr common.Address) *stateObject { + var object *stateObject + if obj, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; ok { + if obj.deleted { + return nil + } + object = obj + } else { + object = s.getStateObjectNoSlot(addr) + } + return object +} + +func (s *ParallelStateDB) storeStateObj(addr common.Address, stateObject *stateObject) { + // The object could be created in SlotDB, if it got the object from DB and + // update it to the `s.parallel.stateObjects` + s.parallel.locatStateObjects[addr] = stateObject +} + +func (s *ParallelStateDB) getStateObjectNoSlot(addr common.Address) *stateObject { + if obj := s.getDeletedStateObject(addr); obj != nil && !obj.deleted { + return obj + } + return nil +} + +// createObject creates a new state object. If there is an existing account with +// the given address, it is overwritten and returned as the second return value. + +// prev is used for CreateAccount to get its balance +// Parallel mode: +// if prev in dirty: revert is ok +// if prev in unconfirmed DB: addr state read record, revert should not put it back +// if prev in main DB: addr state read record, revert should not put it back +// if pre no exist: addr state read record, + +// `prev` is used to handle revert, to recover with the `prev` object +// In Parallel mode, we only need to recover to `prev` in SlotDB, +// +// a.if it is not in SlotDB, `revert` will remove it from the SlotDB +// b.if it is existed in SlotDB, `revert` will recover to the `prev` in SlotDB +// c.as `snapDestructs` it is the same +func (s *ParallelStateDB) createObject(addr common.Address) (newobj *stateObject) { + var prev *stateObject = nil + readFromDB := false + prevdestruct := false + if object, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; ok { + prev = object + } else { + object, ok = s.getStateObjectFromUnconfirmedDB(addr) + if ok { + prev = object + readFromDB = true + } else { + object = s.getDeletedStateObject(addr) // try to get from base db + if object != nil { + prev = object + readFromDB = true + } + } + } + // There can be tx0 create an obj at addr0, tx1 destruct it, and tx2 recreate it use create2. + // so if tx0 is finalized, and tx1 is unconfirmed, we have to check the states of unconfirmed, otherwise there + // will be wrong behavior that we recreate an object that is already there. see. test "TestDeleteThenCreate" + + if prev != nil { + // check slot + _, prevdestruct = s.getStateObjectsDestruct(prev.address) + + if !prevdestruct { + // set Destruct so later accesses in this transaction will not touch the obsoleted state. + s.setStateObjectsDestruct(prev.address, prev.origin) + if readFromDB { + // check nonSlot + s.snapParallelLock.RLock() + _, prevdestruct = s.snapDestructs[prev.address] + s.parallel.addrSnapDestructsReadsInSlot[addr] = prevdestruct + s.snapParallelLock.RUnlock() + } + if !prevdestruct { + s.snapParallelLock.Lock() + s.snapDestructs[prev.address] = struct{}{} + s.snapParallelLock.Unlock() + } + } + } + + newobj = newObject(s, s.isParallel, addr, nil) + newobj.setNonce(0) // sets the object to dirty + if prev == nil { + s.journal.append(createObjectChange{account: &addr}) + } else { + s.journal.append(resetObjectChange{prev: prev, prevdestruct: prevdestruct}) + } + + s.parallel.addrStateChangesInSlot[addr] = true // the object is created + s.parallel.nonceChangesInSlot[addr] = struct{}{} + s.parallel.balanceChangesInSlot[addr] = struct{}{} + s.parallel.codeChangesInSlot[addr] = struct{}{} + // notice: all the KVs are cleared if any + s.parallel.kvChangesInSlot[addr] = make(StateKeys) + newobj.created = true + s.parallel.dirtiedStateObjectsInSlot[addr] = newobj + return newobj +} + +// getDeletedStateObject is similar to getStateObject, but instead of returning +// nil for a deleted state object, it returns the actual object with the deleted +// flag set. This is needed by the state journal to revert to the correct s- +// destructed object instead of wiping all knowledge about the state object. +func (s *ParallelStateDB) getDeletedStateObject(addr common.Address) *stateObject { + + // Prefer live objects if any is available + if obj, _ := s.getStateObjectFromStateObjects(addr); obj != nil { + return obj + } + + data, ok := s.getStateObjectFromSnapshotOrTrie(addr) + if !ok { + return nil + } + + // this is why we have to use a separate getDeletedStateObject for ParallelStateDB + // `s` has to be the ParallelStateDB + obj := newObject(s, s.isParallel, addr, data) + s.storeStateObj(addr, obj) + return obj +} + +// GetOrNewStateObject retrieves a state object or create a new state object if nil. +// dirtyInSlot -> Unconfirmed DB (if not DAG) -> main DB -> snapshot, no? create one +func (s *ParallelStateDB) GetOrNewStateObject(addr common.Address) *stateObject { + var object *stateObject + var ok bool + if object, ok = s.parallel.dirtiedStateObjectsInSlot[addr]; ok { + return object + } + + // try unconfirmedDB + object, _ = s.getStateObjectFromUnconfirmedDB(addr) + if object != nil { + // object found in unconfirmedDB, check existence + if object.deleted || object.selfDestructed { + object = s.createObject(addr) + s.parallel.addrStateReadsInSlot[addr] = false + return object + } + } else { + object = s.getStateObjectNoSlot(addr) // try to get from base db + } + // not found, or found in NoSlot or found in unconfirmed. + exist := true + if object == nil || object.deleted { + object = s.createObject(addr) + exist = false + } + s.parallel.addrStateReadsInSlot[addr] = exist // true: exist, false: not exist + return object +} + +// Exist reports whether the given account address exists in the state. +// Notably this also returns true for suicided accounts. +func (s *ParallelStateDB) Exist(addr common.Address) bool { + // 1.Try to get from dirty + if obj, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; ok { + if obj.deleted { + log.Error("Exist in dirty, but marked as deleted or suicided", + "txIndex", s.txIndex, "baseTxIndex:", s.parallel.baseTxIndex) + return false + } + return true + } + // 2.Try to get from unconfirmed & main DB + // 2.1 Already read before + if exist, ok := s.parallel.addrStateReadsInSlot[addr]; ok { + return exist + } + + // 2.2 Try to get from unconfirmed DB if exist + if exist, ok := s.getAddrStateFromUnconfirmedDB(addr, false); ok { + s.parallel.addrStateReadsInSlot[addr] = exist // update and cache + return exist + } + + // 3.Try to get from main StateDB + exist := s.getStateObjectNoSlot(addr) != nil + s.parallel.addrStateReadsInSlot[addr] = exist // update and cache + return exist +} + +// Empty returns whether the state object is either non-existent +// or empty according to the EIP161 specification (balance = nonce = code = 0) +func (s *ParallelStateDB) Empty(addr common.Address) bool { + // 1.Try to get from dirty + if obj, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; ok { + // dirty object is light copied and fixup on need, + // empty could be wrong, except it is created with this TX + if _, ok := s.parallel.addrStateChangesInSlot[addr]; ok { + return obj.empty() + } + // so we have to check it manually + // empty means: Nonce == 0 && Balance == 0 && CodeHash == emptyCodeHash + if s.GetBalance(addr).Sign() != 0 { // check balance first, since it is most likely not zero + return false + } + if s.GetNonce(addr) != 0 { + return false + } + codeHash := s.GetCodeHash(addr) + return bytes.Equal(codeHash.Bytes(), types.EmptyCodeHash.Bytes()) // code is empty, the object is empty + } + // 2.Try to get from unconfirmed & main DB + // 2.1 Already read before + if exist, ok := s.parallel.addrStateReadsInSlot[addr]; ok { + // exist means not empty + return !exist + } + // 2.2 Try to get from unconfirmed DB if exist + if exist, ok := s.getAddrStateFromUnconfirmedDB(addr, true); ok { + s.parallel.addrStateReadsInSlot[addr] = exist // update read cache + return !exist + } + // 2.3 Try to get from NoSlot. + so := s.getStateObjectNoSlot(addr) + exist := so != nil + empty := (!exist) || so.empty() + + s.parallel.addrStateReadsInSlot[addr] = exist // update read cache + return empty +} + +// GetBalance retrieves the balance from the given address or 0 if object not found +// GetFrom the dirty list => from unconfirmed DB => get from main stateDB +func (s *ParallelStateDB) GetBalance(addr common.Address) *uint256.Int { + var dirtyObj *stateObject + // 0. Test whether it is deleted in dirty. + if o, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; ok { + if o.deleted { + return common.U2560 + } + dirtyObj = o + } + + // 1.Try to get from dirty + if _, ok := s.parallel.balanceChangesInSlot[addr]; ok { + // on balance fixup, addr may not exist in dirtiedStateObjectsInSlot + // we intend to fixup balance based on unconfirmed DB or main DB + return dirtyObj.Balance() + } + // 2.Try to get from unconfirmed DB or main DB + // 2.1 Already read before + if balance, ok := s.parallel.balanceReadsInSlot[addr]; ok { + return balance + } + + balance := common.U2560 + // 2.2 Try to get from unconfirmed DB if exist + if blc := s.getBalanceFromUnconfirmedDB(addr); blc != nil { + balance = blc + } else { + // 3. Try to get from main StateObject + blc = common.U2560 + object := s.getStateObjectNoSlot(addr) + if object != nil { + blc = object.Balance() + } + balance = blc + } + if _, ok := s.parallel.balanceReadsInSlot[addr]; !ok { + s.parallel.balanceReadsInSlot[addr] = balance + } + + // fixup dirties + if dirtyObj != nil && dirtyObj.Balance() != balance { + dirtyObj.setBalance(balance) + } + + return balance +} + +func (s *ParallelStateDB) GetNonce(addr common.Address) uint64 { + var dirtyObj *stateObject + // 0. Test whether it is deleted in dirty. + if o, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; ok { + if o.deleted { + return 0 + } + dirtyObj = o + } + + // 1.Try to get from dirty + if _, ok := s.parallel.nonceChangesInSlot[addr]; ok { + // on nonce fixup, addr may not exist in dirtiedStateObjectsInSlot + // we intend to fixup nonce based on unconfirmed DB or main DB + return dirtyObj.Nonce() + } + // 2.Try to get from unconfirmed DB or main DB + // 2.1 Already read before + if nonce, ok := s.parallel.nonceReadsInSlot[addr]; ok { + return nonce + } + + var nonce uint64 = 0 + // 2.2 Try to get from unconfirmed DB if exist + if nc, ok := s.getNonceFromUnconfirmedDB(addr); ok { + nonce = nc + } else { + // 3.Try to get from main StateDB + nc = 0 + object := s.getStateObjectNoSlot(addr) + if object != nil { + nc = object.Nonce() + } + nonce = nc + } + if _, ok := s.parallel.nonceReadsInSlot[addr]; !ok { + s.parallel.nonceReadsInSlot[addr] = nonce + } + // fixup dirties + if dirtyObj != nil && dirtyObj.Nonce() < nonce { + dirtyObj.setNonce(nonce) + } + return nonce +} + +func (s *ParallelStateDB) GetCode(addr common.Address) []byte { + var dirtyObj *stateObject + // 0. Test whether it is deleted in dirty. + if o, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; ok { + if o.deleted { + return nil + } + dirtyObj = o + } else { + dirtyObj = nil + } + + // 1.Try to get from dirty + if _, ok := s.parallel.codeChangesInSlot[addr]; ok { + // on code fixup, addr may not exist in dirtiedStateObjectsInSlot + // we intend to fixup code based on unconfirmed DB or main DB + return dirtyObj.Code() + } + // 2.Try to get from unconfirmed DB or main DB + // 2.1 Already read before + if code, ok := s.parallel.codeReadsInSlot[addr]; ok { + return code + } + var code []byte + // 2.2 Try to get from unconfirmed DB if exist + if cd, ok := s.getCodeFromUnconfirmedDB(addr); ok { + code = cd + } else { + // 3. Try to get from main StateObject + object := s.getStateObjectNoSlot(addr) + if object != nil { + code = object.Code() + } + } + + if _, ok := s.parallel.codeReadsInSlot[addr]; !ok { + s.parallel.codeReadsInSlot[addr] = code + } + // fixup dirties + log.Debug(fmt.Sprintf("fixup Code, addr: %s, dirtyObj (ptr %p)\n", addr.Hex(), dirtyObj)) + if dirtyObj != nil { + if dirtyObj.code == nil { + dirtyObj.code = code + } + if !bytes.Equal(dirtyObj.code, code) { + log.Debug("fix up code", "addr", addr, "dirty", dirtyObj, "code", code) + dirtyObj.code = code + } + } + return code +} + +func (s *ParallelStateDB) GetCodeSize(addr common.Address) int { + var dirtyObj *stateObject + // 0. Test whether it is deleted in dirty. + if o, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; ok { + if o.deleted { + return 0 + } + dirtyObj = o + } + // 1.Try to get from dirty + if _, ok := s.parallel.codeChangesInSlot[addr]; ok { + // on code fixup, addr may not exist in dirtiedStateObjectsInSlot + // we intend to fixup code based on unconfirmed DB or main DB + return dirtyObj.CodeSize() + } + // 2.Try to get from unconfirmed DB or main DB + // 2.1 Already read before + if code, ok := s.parallel.codeReadsInSlot[addr]; ok { + return len(code) // len(nil) is 0 too + } + + cs := 0 + var code []byte + // 2.2 Try to get from unconfirmed DB if exist + if cd, ok := s.getCodeFromUnconfirmedDB(addr); ok { + cs = len(cd) + code = cd + } else { + // 3. Try to get from main StateObject + var cc []byte + object := s.getStateObjectNoSlot(addr) + if object != nil { + // This is where we update the code from possible db.ContractCode if the original object.code is nil. + cc = object.Code() + cs = object.CodeSize() + } + code = cc + } + if _, ok := s.parallel.codeReadsInSlot[addr]; !ok { + s.parallel.codeReadsInSlot[addr] = code + } + // fixup dirties + if dirtyObj != nil { + if !bytes.Equal(dirtyObj.code, code) { + dirtyObj.code = code + } + } + return cs +} + +// GetCodeHash return: +// - common.Hash{}: the address does not exist +// - emptyCodeHash: the address exist, but code is empty +// - others: the address exist, and code is not empty +func (s *ParallelStateDB) GetCodeHash(addr common.Address) common.Hash { + var dirtyObj *stateObject + // 0. Test whether it is deleted in dirty. + if o, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; ok { + if o.deleted { + return common.Hash{} + } + dirtyObj = o + } + + // 1.Try to get from dirty + if _, ok := s.parallel.codeChangesInSlot[addr]; ok { + // on code fixup, addr may not exist in dirtiedStateObjectsInSlot + // we intend to fixup balance based on unconfirmed DB or main DB + return common.BytesToHash(dirtyObj.CodeHash()) + } + // 2.Try to get from unconfirmed DB or main DB + // 2.1 Already read before + if codeHash, ok := s.parallel.codeHashReadsInSlot[addr]; ok { + return codeHash + } + codeHash := common.Hash{} + // 2.2 Try to get from unconfirmed DB if exist + if cHash, ok := s.getCodeHashFromUnconfirmedDB(addr); ok { + codeHash = cHash + } else { + // 3. Try to get from main StateObject + object := s.getStateObjectNoSlot(addr) + + if object != nil { + codeHash = common.BytesToHash(object.CodeHash()) + } + } + if _, ok := s.parallel.codeHashReadsInSlot[addr]; !ok { + s.parallel.codeHashReadsInSlot[addr] = codeHash + } + + // fill slots in dirty if existed. + // A case for this: + // TX0: createAccount at addr 0x123, set code and codehash + // TX1: AddBalance - now an obj in dirty with empty codehash, and codeChangesInSlot is false (not changed) + // GetCodeHash - get from unconfirmedDB or mainDB, set codeHashReadsInSlot to the new val. + // SELFDESTRUCT - set codeChangesInSlot, but the obj in dirty is with Empty codehash. + // obj marked selfdestructed but not deleted. so CodeHash is not empty. + // GetCodeHash - since the codeChangesInslot is marked, get the object from dirty, and get the + // wrong 'empty' hash. + if dirtyObj != nil { + // found one + if dirtyObj.CodeHash() == nil || bytes.Equal(dirtyObj.CodeHash(), types.EmptyCodeHash.Bytes()) { + dirtyObj.data.CodeHash = codeHash.Bytes() + } + } + return codeHash +} + +// GetState retrieves a value from the given account's storage trie. +// For parallel mode wih, get from the state in order: +// +// -> self dirty, both Slot & MainProcessor +// -> pending of self: Slot on merge +// -> pending of unconfirmed DB +// -> pending of main StateDB +// -> origin +func (s *ParallelStateDB) GetState(addr common.Address, hash common.Hash) common.Hash { + var dirtyObj *stateObject + // 0. Test whether it is deleted in dirty. + if o, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; ok { + if o == nil || o.deleted { + return common.Hash{} + } + dirtyObj = o + } + // 1.Try to get from dirty + if exist, ok := s.parallel.addrStateChangesInSlot[addr]; ok { + if !exist { + // it should be able to get state from selfDestruct address within a Tx: + // e.g. within a transaction: call addr:selfDestruct -> get state: should be ok + log.Info("ParallelStateDB GetState suicided", "addr", addr, "hash", hash) + } else { + // It is possible that an object get created but not dirtied since there is no state set, such as recreate. + // In this case, simply return common.Hash{}. + // This is for corner case: + // B0: TX0 --> createAccount @addr1 -- merged into DB + // B1: Tx1 and Tx2 + // Tx1 account@addr1 selfDestruct -- unconfirmed + // Tx2 recreate account@addr2 -- executing + // Since any state change and suicide could record in s.parallel.addrStateChangeInSlot, it is save to simple + // return common.Hash{} for this case as the previous TX must has the object destructed. + // P.S. if the Tx2 both destruct and recreate the object, it will not fall into this logic, as the change + // will be recorded in dirtiedStateObjectsInSlot. + + // it could be suicided within this SlotDB? + // it should be able to get state from suicided address within a Tx: + // e.g. within a transaction: call addr:suicide -> get state: should be ok + + if dirtyObj == nil { + log.Error("ParallelStateDB GetState access untouched object after create, may check create2") + return common.Hash{} + } + return dirtyObj.GetState(hash) + } + } + + if keys, ok := s.parallel.kvChangesInSlot[addr]; ok { + if _, ok := keys[hash]; ok { + return dirtyObj.GetState(hash) + } + } + // 2.Try to get from unconfirmed DB or main DB + // 2.1 Already read before + if storage, ok := s.parallel.kvReadsInSlot[addr]; ok { + if val, ok := storage.GetValue(hash); ok { + return val + } + } + // 2.2 Object in dirty due to other changes, such as getBalance etc. + // load from dirty directly and the stateObject.GetState() will take care of the KvReadInSlot update. + // So there is no chance for create different objects with same address. (one in dirty and one from non-slot, and inconsistency) + if dirtyObj != nil { + return dirtyObj.GetState(hash) + } + + value := common.Hash{} + // 2.3 Try to get from unconfirmed DB if exist + if val, ok := s.getKVFromUnconfirmedDB(addr, hash); ok { + value = val + } else { + // 3.Get from main StateDB + object := s.getStateObjectNoSlot(addr) + val = common.Hash{} + if object != nil { + val = object.GetState(hash) + } + value = val + } + if s.parallel.kvReadsInSlot[addr] == nil { + s.parallel.kvReadsInSlot[addr] = newStorage(false) + } + if _, ok := s.parallel.kvReadsInSlot[addr].GetValue(hash); !ok { + s.parallel.kvReadsInSlot[addr].StoreValue(hash, value) // update cache + } + + return value +} + +// GetCommittedState retrieves a value from the given account's committed storage trie. +// So it should not access/update dirty, and not check delete of dirty objects. +func (s *ParallelStateDB) GetCommittedState(addr common.Address, hash common.Hash) common.Hash { + + // 1.Try to get from unconfirmed DB or main DB + // KVs in unconfirmed DB can be seen as pending storage + // KVs in main DB are merged from SlotDB and has done finalise() on merge, can be seen as pending storage too. + // 1.1 Already read before + if storage, ok := s.parallel.kvReadsInSlot[addr]; ok { + if val, ok := storage.GetValue(hash); ok { + return val + } + } + value := common.Hash{} + // 1.2 Try to get from unconfirmed DB if exist + if val, ok := s.getKVFromUnconfirmedDB(addr, hash); ok { + value = val + } else { + // 2. Try to get from main DB + val = common.Hash{} + object := s.getStateObjectNoSlot(addr) + if object != nil { + val = object.GetCommittedState(hash) + } + value = val + } + if s.parallel.kvReadsInSlot[addr] == nil { + s.parallel.kvReadsInSlot[addr] = newStorage(false) + } + if _, ok := s.parallel.kvReadsInSlot[addr].GetValue(hash); !ok { + s.parallel.kvReadsInSlot[addr].StoreValue(hash, value) // update cache + } + return value +} + +func (s *ParallelStateDB) HasSelfDestructed(addr common.Address) bool { + // 1.Try to get from dirty + if obj, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; ok { + if obj == nil || obj.deleted { + return false + } + return obj.selfDestructed + } + // 2.Try to get from unconfirmed + if exist, ok := s.getAddrStateFromUnconfirmedDB(addr, false); ok { + return !exist + } + + object := s.getDeletedStateObject(addr) + if object != nil { + return object.selfDestructed + } + return false +} + +// AddBalance adds amount to the account associated with addr. +func (s *ParallelStateDB) AddBalance(addr common.Address, amount *uint256.Int) { + // add balance will perform a read operation first + // if amount == 0, no balance change, but there is still an empty check. + object := s.GetOrNewStateObject(addr) + if object != nil { + if _, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; !ok { + newStateObject := object.lightCopy(s) + // do balance fixup from the confirmed DB, it could be more reliable than main DB + balance := s.GetBalance(addr) // it will record the balance read operation + newStateObject.setBalance(balance) + newStateObject.AddBalance(amount) + s.parallel.dirtiedStateObjectsInSlot[addr] = newStateObject + s.parallel.balanceChangesInSlot[addr] = struct{}{} + return + } + // already dirty, make sure the balance is fixed up since it could be previously dirtied by nonce or KV... + balance := s.GetBalance(addr) + if object.Balance().Cmp(balance) != 0 { + log.Warn("AddBalance in dirty, but balance has not do fixup", "txIndex", s.txIndex, "addr", addr, + "stateObject.Balance()", object.Balance(), "s.GetBalance(addr)", balance) + object.setBalance(balance) + } + + object.AddBalance(amount) + s.parallel.balanceChangesInSlot[addr] = struct{}{} + } +} + +// SubBalance subtracts amount from the account associated with addr. +func (s *ParallelStateDB) SubBalance(addr common.Address, amount *uint256.Int) { + // unlike add, sub 0 balance will not touch empty object + object := s.GetOrNewStateObject(addr) + if object != nil { + if _, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; !ok { + newStateObject := object.lightCopy(s) + // do balance fixup from the confirmed DB, it could be more reliable than main DB + balance := s.GetBalance(addr) + newStateObject.setBalance(balance) + newStateObject.SubBalance(amount) + s.parallel.balanceChangesInSlot[addr] = struct{}{} + s.parallel.dirtiedStateObjectsInSlot[addr] = newStateObject + return + } + // already dirty, make sure the balance is fixed up since it could be previously dirtied by nonce or KV... + balance := s.GetBalance(addr) + if object.Balance().Cmp(balance) != 0 { + log.Warn("SubBalance in dirty, but balance is incorrect", "txIndex", s.txIndex, "addr", addr, + "stateObject.Balance()", object.Balance(), "s.GetBalance(addr)", balance) + object.setBalance(balance) + } + object.SubBalance(amount) + s.parallel.balanceChangesInSlot[addr] = struct{}{} + } +} + +func (s *ParallelStateDB) SetBalance(addr common.Address, amount *uint256.Int) { + object := s.GetOrNewStateObject(addr) + if object != nil { + if _, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; !ok { + newStateObject := object.lightCopy(s) + // update balance for revert, in case child contract is reverted, + // it should revert to the previous balance + balance := s.GetBalance(addr) + newStateObject.setBalance(balance) + newStateObject.SetBalance(amount) + s.parallel.balanceChangesInSlot[addr] = struct{}{} + s.parallel.dirtiedStateObjectsInSlot[addr] = newStateObject + return + } + + balance := s.GetBalance(addr) + object.setBalance(balance) + object.SetBalance(amount) + s.parallel.balanceChangesInSlot[addr] = struct{}{} + } +} + +func (s *ParallelStateDB) SetNonce(addr common.Address, nonce uint64) { + object := s.GetOrNewStateObject(addr) + if object != nil { + if _, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; !ok { + newStateObject := object.lightCopy(s) + noncePre := s.GetNonce(addr) + newStateObject.setNonce(noncePre) // nonce fixup + newStateObject.SetNonce(nonce) + s.parallel.nonceChangesInSlot[addr] = struct{}{} + s.parallel.dirtiedStateObjectsInSlot[addr] = newStateObject + return + } + noncePre := s.GetNonce(addr) + object.setNonce(noncePre) // nonce fixup + object.SetNonce(nonce) + s.parallel.nonceChangesInSlot[addr] = struct{}{} + } +} + +func (s *ParallelStateDB) SetCode(addr common.Address, code []byte) { + object := s.GetOrNewStateObject(addr) + if object != nil { + codeHash := crypto.Keccak256Hash(code) + if _, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; !ok { + newStateObject := object.lightCopy(s) + codePre := s.GetCode(addr) // code fixup + codeHashPre := crypto.Keccak256Hash(codePre) + newStateObject.setCode(codeHashPre, codePre) + newStateObject.SetCode(codeHash, code) + s.parallel.dirtiedStateObjectsInSlot[addr] = newStateObject + s.parallel.codeChangesInSlot[addr] = struct{}{} + return + } + codePre := s.GetCode(addr) // code fixup + codeHashPre := crypto.Keccak256Hash(codePre) + object.setCode(codeHashPre, codePre) + object.SetCode(codeHash, code) + s.parallel.codeChangesInSlot[addr] = struct{}{} + } +} + +func (s *ParallelStateDB) SetState(addr common.Address, key, value common.Hash) { + object := s.GetOrNewStateObject(addr) // attention: if StateObject's lightCopy, its storage is only a part of the full storage, + if object != nil { + if s.parallel.baseTxIndex+1 == s.txIndex { + if s.GetState(addr, key) == value { + log.Debug("Skip set same state", "baseTxIndex", s.parallel.baseTxIndex, + "txIndex", s.txIndex, "addr", addr, + "key", key, "value", value) + return + } + } + + if s.parallel.kvChangesInSlot[addr] == nil { + s.parallel.kvChangesInSlot[addr] = make(StateKeys) + } + + if _, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; !ok { + newStateObject := object.lightCopy(s) + newStateObject.SetState(key, value) + s.parallel.dirtiedStateObjectsInSlot[addr] = newStateObject + s.parallel.addrStateChangesInSlot[addr] = true + return + } + // do State Update + object.SetState(key, value) + s.parallel.addrStateChangesInSlot[addr] = true + } +} + +// SelfDestruct marks the given account as suicided. +// This clears the account balance. +// +// The account's state object is still available until the state is committed, +// getStateObject will return a non-nil account after Suicide. +func (s *ParallelStateDB) SelfDestruct(addr common.Address) { + var object *stateObject + // 1.Try to get from dirty, it could be suicided inside of contract call + object = s.parallel.dirtiedStateObjectsInSlot[addr] + + if object != nil && object.deleted { + return + } + + if object == nil { + // 2.Try to get from unconfirmed, if deleted return false, since the address does not exist + if obj, ok := s.getStateObjectFromUnconfirmedDB(addr); ok { + object = obj + // Treat selfDestructed in unconfirmedDB as deleted since it will be finalised at merge phase. + deleted := object.deleted || object.selfDestructed + s.parallel.addrStateReadsInSlot[addr] = !deleted // true: exist, false: deleted + if deleted { + return + } + } + } + + if object == nil { + // 3.Try to get from main StateDB + object = s.getStateObjectNoSlot(addr) + if object == nil || object.deleted { + s.parallel.addrStateReadsInSlot[addr] = false // true: exist, false: deleted + return + } + s.parallel.addrStateReadsInSlot[addr] = true // true: exist, false: deleted + } + + s.journal.append(selfDestructChange{ + account: &addr, + prev: object.selfDestructed, // todo: must be false? + prevbalance: new(uint256.Int).Set(s.GetBalance(addr)), + }) + + if _, ok := s.parallel.dirtiedStateObjectsInSlot[addr]; !ok { + newStateObject := object.lightCopy(s) + newStateObject.markSelfdestructed() + newStateObject.setBalance(new(uint256.Int)) + s.parallel.dirtiedStateObjectsInSlot[addr] = newStateObject + s.parallel.addrStateChangesInSlot[addr] = false + s.parallel.balanceChangesInSlot[addr] = struct{}{} + s.parallel.codeChangesInSlot[addr] = struct{}{} + return + } + + s.parallel.addrStateChangesInSlot[addr] = false // false: the address does not exist anymore + s.parallel.balanceChangesInSlot[addr] = struct{}{} + s.parallel.codeChangesInSlot[addr] = struct{}{} + object.markSelfdestructed() + object.setBalance(new(uint256.Int)) +} + +func (s *ParallelStateDB) Selfdestruct6780(addr common.Address) { + object := s.getStateObject(addr) + if object == nil { + return + } + if object.created { + s.SelfDestruct(addr) + } +} + +// CreateAccount explicitly creates a state object. If a state object with the address +// already exists the balance is carried over to the new account. +// +// CreateAccount is called during the EVM CREATE operation. The situation might arise that +// a contract does the following: +// +// 1. sends funds to sha(account ++ (nonce + 1)) +// 2. tx_create(sha(account ++ nonce)) (note that this gets the address of 1) +// +// Carrying over the balance ensures that Ether doesn't disappear. +func (s *ParallelStateDB) CreateAccount(addr common.Address) { + // no matter it is got from dirty, unconfirmed or main DB + // if addr not exist, preBalance will be common.U2560, it is same as new(uint256.Int) which + // is the value newObject(), + preBalance := s.GetBalance(addr) + newObj := s.createObject(addr) + newObj.setBalance(new(uint256.Int).Set(preBalance)) // new uint256.Int for newObj + +} + +// RevertToSnapshot reverts all state changes made since the given revision. +func (s *ParallelStateDB) RevertToSnapshot(revid int) { + // Find the snapshot in the stack of valid snapshots. + idx := sort.Search(len(s.validRevisions), func(i int) bool { + return s.validRevisions[i].id >= revid + }) + if idx == len(s.validRevisions) || s.validRevisions[idx].id != revid { + panic(fmt.Errorf("revision id %v cannot be reverted", revid)) + } + snapshot := s.validRevisions[idx].journalIndex + + // Replay the journal to undo changes and remove invalidated snapshots + s.journal.revert(s, snapshot) + s.validRevisions = s.validRevisions[:idx] +} + +// AddRefund adds gas to the refund counter +// journal.append will use ParallelState for revert +func (s *ParallelStateDB) AddRefund(gas uint64) { + s.journal.append(refundChange{prev: s.refund}) + s.refund += gas +} + +// SubRefund removes gas from the refund counter. +// This method will panic if the refund counter goes below zero +func (s *ParallelStateDB) SubRefund(gas uint64) { + s.journal.append(refundChange{prev: s.refund}) + if gas > s.refund { + // we don't need to panic here if we read the wrong state in parallel mode + // we just need to redo this transaction + log.Info(fmt.Sprintf("Refund counter below zero (gas: %d > refund: %d)", gas, s.refund), "tx", s.thash.String()) + s.parallel.needsRedo = true + return + } + s.refund -= gas +} + +// For Parallel Execution Mode, it can be seen as Penetrated Access: +// +// ------------------------------------------------------- +// | BaseTxIndex | Unconfirmed Txs... | Current TxIndex | +// ------------------------------------------------------- +// +// Access from the unconfirmed DB with range&priority: txIndex -1(previous tx) -> baseTxIndex + 1 +func (s *ParallelStateDB) getBalanceFromUnconfirmedDB(addr common.Address) *uint256.Int { + if s.parallel.useDAG { + // DAG never reads from unconfirmedDB, skip check. + return nil + } + for i := s.txIndex - 1; i >= 0 && i > s.BaseTxIndex(); i-- { + db_, ok := s.parallel.unconfirmedDBs.Load(i) + if !ok { + continue + } + db := db_.(*ParallelStateDB) + // 1.Refer the state of address, exist or not in dirtiedStateObjectsInSlot + balanceHit := false + if _, exist := db.parallel.addrStateChangesInSlot[addr]; exist { + balanceHit = true + } + if _, exist := db.parallel.balanceChangesInSlot[addr]; exist { + balanceHit = true + } + if !balanceHit { + continue + } + obj := db.parallel.dirtiedStateObjectsInSlot[addr] + balance := obj.Balance() + if obj.deleted { + balance = common.U2560 + } + return balance + } + return nil +} + +// Similar to getBalanceFromUnconfirmedDB +func (s *ParallelStateDB) getNonceFromUnconfirmedDB(addr common.Address) (uint64, bool) { + if s.parallel.useDAG { + // DAG never reads from unconfirmedDB, skip check. + return 0, false + } + + for i := s.txIndex - 1; i > s.BaseTxIndex(); i-- { + db_, ok := s.parallel.unconfirmedDBs.Load(i) + if !ok { + continue + } + db := db_.(*ParallelStateDB) + + nonceHit := false + if _, ok := db.parallel.addrStateChangesInSlot[addr]; ok { + nonceHit = true + } else if _, ok := db.parallel.nonceChangesInSlot[addr]; ok { + nonceHit = true + } + if !nonceHit { + // nonce refer not hit, try next unconfirmedDb + continue + } + // nonce hit, return the nonce + obj := db.parallel.dirtiedStateObjectsInSlot[addr] + if obj == nil { + log.Debug("Get nonce from UnconfirmedDB, changed but object not exist, ", + "txIndex", s.txIndex, "referred txIndex", i, "addr", addr) + continue + } + // deleted object with nonce == 0 + if obj.deleted || obj.selfDestructed { + return 0, true + } + nonce := obj.Nonce() + return nonce, true + } + return 0, false +} + +// Similar to getBalanceFromUnconfirmedDB +// It is not only for code, but also codeHash and codeSize, we return the *stateObject for convenience. +func (s *ParallelStateDB) getCodeFromUnconfirmedDB(addr common.Address) ([]byte, bool) { + if s.parallel.useDAG { + // DAG never reads from unconfirmedDB, skip check. + return nil, false + } + for i := s.txIndex - 1; i > s.BaseTxIndex(); i-- { + db_, ok := s.parallel.unconfirmedDBs.Load(i) + if !ok { + continue + } + db := db_.(*ParallelStateDB) + + codeHit := false + if _, exist := db.parallel.addrStateChangesInSlot[addr]; exist { + codeHit = true + } + if _, exist := db.parallel.codeChangesInSlot[addr]; exist { + codeHit = true + } + if !codeHit { + // try next unconfirmedDb + continue + } + obj := db.parallel.dirtiedStateObjectsInSlot[addr] + if obj == nil { + log.Debug("Get code from UnconfirmedDB, changed but object not exist, ", + "txIndex", s.txIndex, "referred txIndex", i, "addr", addr) + continue + } + if obj.deleted || obj.selfDestructed { + return nil, true + } + code := obj.Code() + return code, true + } + return nil, false +} + +// Similar to getCodeFromUnconfirmedDB +// but differ when address is deleted or not exist +func (s *ParallelStateDB) getCodeHashFromUnconfirmedDB(addr common.Address) (common.Hash, bool) { + if s.parallel.useDAG { + // DAG never reads from unconfirmedDB, skip check. + return common.Hash{}, false + } + for i := s.txIndex - 1; i > s.BaseTxIndex(); i-- { + db_, ok := s.parallel.unconfirmedDBs.Load(i) + if !ok { + continue + } + db := db_.(*ParallelStateDB) + + hashHit := false + if _, exist := db.parallel.addrStateChangesInSlot[addr]; exist { + hashHit = true + } + if _, exist := db.parallel.codeChangesInSlot[addr]; exist { + hashHit = true + } + if !hashHit { + // try next unconfirmedDb + continue + } + obj := db.parallel.dirtiedStateObjectsInSlot[addr] + if obj == nil { + log.Debug("Get codeHash from UnconfirmedDB, changed but object not exist, ", + "txIndex", s.txIndex, "referred txIndex", i, "addr", addr) + continue + } + if obj.deleted || obj.selfDestructed { + return common.Hash{}, true + } + codeHash := common.BytesToHash(obj.CodeHash()) + return codeHash, true + } + return common.Hash{}, false +} + +// Similar to getCodeFromUnconfirmedDB +// It is for address state check of: Exist(), Empty() and HasSuicided() +// Since the unconfirmed DB should have done Finalise() with `deleteEmptyObjects = true` +// If the dirty address is empty or suicided, it will be marked as deleted, so we only need to return `deleted` or not. +func (s *ParallelStateDB) getAddrStateFromUnconfirmedDB(addr common.Address, testEmpty bool) (bool, bool) { + if s.parallel.useDAG { + // DAG never reads from unconfirmedDB, skip check. + return false, false + } + // check the unconfirmed DB with range: baseTxIndex -> txIndex -1(previous tx) + for i := s.txIndex - 1; i > s.BaseTxIndex(); i-- { + db_, ok := s.parallel.unconfirmedDBs.Load(i) + if !ok { + continue + } + db := db_.(*ParallelStateDB) + if exist, ok := db.parallel.addrStateChangesInSlot[addr]; ok { + if obj, ok := db.parallel.dirtiedStateObjectsInSlot[addr]; !ok { + log.Debug("Get addr State from UnconfirmedDB, changed but object not exist, ", + "txIndex", s.txIndex, "referred txIndex", i, "addr", addr) + continue + } else { + if obj.selfDestructed || obj.deleted { + return false, true + } + if testEmpty && obj.empty() { + return false, true + } + } + return exist, true + } + } + return false, false +} + +func (s *ParallelStateDB) getKVFromUnconfirmedDB(addr common.Address, key common.Hash) (common.Hash, bool) { + if s.parallel.useDAG { + // DAG never reads from unconfirmedDB, skip check. + return common.Hash{}, false + } + // check the unconfirmed DB with range: baseTxIndex -> txIndex -1(previous tx) + for i := s.txIndex - 1; i > s.BaseTxIndex(); i-- { + db_, ok := s.parallel.unconfirmedDBs.Load(i) + if !ok { + continue + } + db := db_.(*ParallelStateDB) + if _, ok := db.parallel.kvChangesInSlot[addr]; ok { + obj := db.parallel.dirtiedStateObjectsInSlot[addr] + if obj.deleted || obj.selfDestructed { + return common.Hash{}, true + } + // The dirty object in unconfirmed DB will never be finalised and changed after execution. + // So no storageRecordsLock requried. + if val, exist := obj.dirtyStorage.GetValue(key); exist { + return val, true + } + } + } + return common.Hash{}, false +} + +func (s *ParallelStateDB) GetStateObjectFromUnconfirmedDB(addr common.Address) (*stateObject, bool) { + return s.getStateObjectFromUnconfirmedDB(addr) +} + +func (s *ParallelStateDB) getStateObjectFromUnconfirmedDB(addr common.Address) (*stateObject, bool) { + if s.parallel.useDAG { + // DAG never reads from unconfirmedDB, skip check. + return nil, false + } + // check the unconfirmed DB with range: baseTxIndex -> txIndex -1(previous tx) + for i := s.txIndex - 1; i > s.BaseTxIndex(); i-- { + db_, ok := s.parallel.unconfirmedDBs.Load(i) + if !ok { + continue + } + db := db_.(*ParallelStateDB) + if obj, ok := db.parallel.dirtiedStateObjectsInSlot[addr]; ok { + return obj, true + } + } + return nil, false +} + +func (s *ParallelStateDB) getStateObjectFromMainDBNoUpdate(addr common.Address) *stateObject { + var mainObj *stateObject + + if m, ok := s.parallel.conflictCheckStateObjectCache.Load(addr); ok { + mainObj = m.(*stateObject) + return mainObj + } else { + mainDB := s.parallel.baseStateDB + mainObj = mainDB.getStateObjectNoUpdate(addr) + s.parallel.conflictCheckStateObjectCache.Store(addr, mainObj) + } + return mainObj +} + +// GetStateNoUpdate retrieves a value from the given account's storage trie, but do not update the db.stateObjects cache. +func (s *ParallelStateDB) getStateFromMainNoUpdate(addr common.Address, key common.Hash) (ret common.Hash) { + + if kvPair, ok := s.parallel.conflictCheckKVReadCache.Load(addr); !ok { + s.parallel.conflictCheckKVReadCache.Store(addr, newStorage(true)) + } else { + st := kvPair.(*StorageSyncMap) + if val, ok := st.GetValue(key); ok { + return val + } + } + val := common.Hash{} + object := s.getStateObjectFromMainDBNoUpdate(addr) + if object != nil { + val = object.GetStateNoUpdate(key) + } + if kvPair, ok := s.parallel.conflictCheckKVReadCache.Load(addr); ok { + st := kvPair.(*StorageSyncMap) + st.StoreValue(key, val) + s.parallel.conflictCheckKVReadCache.Store(addr, st) + } + + return val +} + +// IsParallelReadsValid If stage2 is true, it is a likely conflict check, +// to detect these potential conflict results in advance and schedule redo ASAP. +func (slotDB *ParallelStateDB) IsParallelReadsValid(isStage2 bool) bool { + parallelKvOnce.Do(func() { + StartKvCheckLoop() + }) + + mainDB := slotDB.parallel.baseStateDB + // conservatively use kvRead size as the initial size. + slotDB.parallel.conflictCheckStateObjectCache = new(sync.Map) + slotDB.parallel.conflictCheckKVReadCache = new(sync.Map) + + if isStage2 && slotDB.txIndex < mainDB.TxIndex() { + // already merged, no need to check + return true + } + + // for nonce + for addr, nonceSlot := range slotDB.parallel.nonceReadsInSlot { + if isStage2 { // update slotDB's unconfirmed DB list and try + if slotDB.parallel.useDAG { + // DAG never reads from unconfirmedDB, skip check. + return true + } + if nonceUnconfirm, ok := slotDB.getNonceFromUnconfirmedDB(addr); ok { + if nonceSlot != nonceUnconfirm { + log.Debug("IsSlotDBReadsValid nonce read is invalid in unconfirmed", "addr", addr, + "nonceSlot", nonceSlot, "nonceUnconfirm", nonceUnconfirm, "SlotIndex", slotDB.parallel.SlotIndex, + "txIndex", slotDB.txIndex, "baseTxIndex", slotDB.parallel.baseTxIndex) + return false + } + } + } + var nonceMain uint64 = 0 + mainObj := slotDB.getStateObjectFromMainDBNoUpdate(addr) + if mainObj != nil { + nonceMain = mainObj.Nonce() + } + if nonceSlot != nonceMain { + log.Debug("IsSlotDBReadsValid nonce read is invalid", "addr", addr, + "nonceSlot", nonceSlot, "nonceMain", nonceMain, "SlotIndex", slotDB.parallel.SlotIndex, + "txIndex", slotDB.txIndex, "baseTxIndex", slotDB.parallel.baseTxIndex, + "mainIndex", mainDB.txIndex) + + return false + } + } + // balance + for addr, balanceSlot := range slotDB.parallel.balanceReadsInSlot { + if isStage2 { // update slotDB's unconfirmed DB list and try + if slotDB.parallel.useDAG { + // DAG never reads from unconfirmedDB, skip check. + return true + } + if balanceUnconfirm := slotDB.getBalanceFromUnconfirmedDB(addr); balanceUnconfirm != nil { + if balanceSlot.Cmp(balanceUnconfirm) == 0 { + continue + } + return false + } + } + + balanceMain := common.U2560 + mainObj := slotDB.getStateObjectFromMainDBNoUpdate(addr) + + if mainObj != nil { + balanceMain = mainObj.Balance() + } + + if balanceSlot.Cmp(balanceMain) != 0 { + log.Debug("IsSlotDBReadsValid balance read is invalid", "addr", addr, + "balanceSlot", balanceSlot, "balanceMain", balanceMain, "SlotIndex", slotDB.parallel.SlotIndex, + "txIndex", slotDB.txIndex, "baseTxIndex", slotDB.parallel.baseTxIndex, + "mainIndex", mainDB.txIndex) + return false + } + } + // check KV + var units []ParallelKvCheckUnit // todo: pre-allocate to make it faster + for addr, read := range slotDB.parallel.kvReadsInSlot { + read.Range(func(keySlot, valSlot interface{}) bool { + units = append(units, ParallelKvCheckUnit{addr, keySlot.(common.Hash), valSlot.(common.Hash)}) + return true + }) + } + readLen := len(units) + if readLen < 8 || isStage2 { + for _, unit := range units { + if hasKvConflict(slotDB, unit.addr, unit.key, unit.val, isStage2) { + return false + } + } + } else { + msgHandledNum := 0 + msgSendNum := 0 + for _, unit := range units { + for { // make sure the unit is consumed + consumed := false + select { + case conflict := <-parallelKvCheckResCh: + msgHandledNum++ + if conflict { + // make sure all request are handled or discarded + for { + if msgHandledNum == msgSendNum { + break + } + select { + case <-parallelKvCheckReqCh: + msgHandledNum++ + case <-parallelKvCheckResCh: + msgHandledNum++ + } + } + return false + } + case parallelKvCheckReqCh <- ParallelKvCheckMessage{slotDB, isStage2, unit}: + msgSendNum++ + consumed = true + } + if consumed { + break + } + } + } + for { + if msgHandledNum == readLen { + break + } + conflict := <-parallelKvCheckResCh + msgHandledNum++ + if conflict { + // make sure all request are handled or discarded + for { + if msgHandledNum == msgSendNum { + break + } + select { + case <-parallelKvCheckReqCh: + msgHandledNum++ + case <-parallelKvCheckResCh: + msgHandledNum++ + } + } + return false + } + } + } + + if isStage2 { // stage2 skip check code, or state, since they are likely unchanged. + return true + } + + // check code + for addr, codeSlot := range slotDB.parallel.codeReadsInSlot { + var codeMain []byte = nil + object := slotDB.getStateObjectFromMainDBNoUpdate(addr) + if object != nil { + codeMain = object.Code() + } + if !bytes.Equal(codeSlot, codeMain) { + log.Debug("IsSlotDBReadsValid code read is invalid", "addr", addr, + "len codeSlot", len(codeSlot), "len codeMain", len(codeMain), "SlotIndex", slotDB.parallel.SlotIndex, + "txIndex", slotDB.txIndex, "baseTxIndex", slotDB.parallel.baseTxIndex, + "mainIndex", mainDB.txIndex) + return false + } + } + // check codeHash + for addr, codeHashSlot := range slotDB.parallel.codeHashReadsInSlot { + codeHashMain := common.Hash{} + object := slotDB.getStateObjectFromMainDBNoUpdate(addr) + if object != nil { + codeHashMain = common.BytesToHash(object.CodeHash()) + } + if !bytes.Equal(codeHashSlot.Bytes(), codeHashMain.Bytes()) { + log.Debug("IsSlotDBReadsValid codehash read is invalid", "addr", addr, + "codeHashSlot", codeHashSlot, "codeHashMain", codeHashMain, "SlotIndex", slotDB.parallel.SlotIndex, + "txIndex", slotDB.txIndex, "baseTxIndex", slotDB.parallel.baseTxIndex, "mainIndex", mainDB.txIndex) + return false + } + } + // addr state check + for addr, stateSlot := range slotDB.parallel.addrStateReadsInSlot { + stateMain := false // addr not exist + if slotDB.getStateObjectFromMainDBNoUpdate(addr) != nil { + stateMain = true // addr exist in main DB + } + if stateSlot != stateMain { + log.Debug("IsSlotDBReadsValid addrState read invalid(true: exist, false: not exist)", + "addr", addr, "stateSlot", stateSlot, "stateMain", stateMain, + "SlotIndex", slotDB.parallel.SlotIndex, + "txIndex", slotDB.txIndex, "baseTxIndex", slotDB.parallel.baseTxIndex, "mainIndex", mainDB.txIndex) + return false + } + } + // snapshot destructs check + for addr, destructRead := range slotDB.parallel.addrSnapDestructsReadsInSlot { + mainObj := mainDB.getDeletedStateObjectNoUpdate(addr) + if mainObj == nil { + log.Debug("IsSlotDBReadsValid snapshot destructs read invalid, address should exist", + "addr", addr, "destruct", destructRead, + "SlotIndex", slotDB.parallel.SlotIndex, + "txIndex", slotDB.txIndex, "baseTxIndex", slotDB.parallel.baseTxIndex) + return false + } + slotDB.snapParallelLock.RLock() // fixme: this lock is not needed + _, destructMain := mainDB.snapDestructs[addr] // addr not exist + slotDB.snapParallelLock.RUnlock() + if destructRead != destructMain && addr.Hex() != "0x0000000000000000000000000000000000000001" { + log.Debug("IsSlotDBReadsValid snapshot destructs read invalid", + "addr", addr, "destructRead", destructRead, "destructMain", destructMain, + "SlotIndex", slotDB.parallel.SlotIndex, + "txIndex", slotDB.txIndex, "baseTxIndex", slotDB.parallel.baseTxIndex, + "mainIndex", mainDB.txIndex) + return false + } + } + return true +} + +// NeedsRedo returns true if there is any clear reason that we need to redo this transaction +func (s *ParallelStateDB) NeedsRedo() bool { + return s.parallel.needsRedo +} + +// FinaliseForParallel finalises the state by removing the destructed objects and clears +// the journal as well as the refunds. Finalise, however, will not push any updates +// into the tries just yet. Only IntermediateRoot or Commit will do that. +// It also handles the mainDB dirties for the first TX. +func (s *ParallelStateDB) FinaliseForParallel(deleteEmptyObjects bool, mainDB *StateDB) { + addressesToPrefetch := make([][]byte, 0, len(s.journal.dirties)) + + if s.TxIndex() == 0 && len(mainDB.journal.dirties) > 0 { + mainDB.stateObjectDestructLock.Lock() + for addr, acc := range mainDB.stateObjectsDestructDirty { + mainDB.stateObjectsDestruct[addr] = acc + } + mainDB.stateObjectsDestructDirty = make(map[common.Address]*types.StateAccount) + mainDB.stateObjectDestructLock.Unlock() + for addr := range mainDB.journal.dirties { + var obj *stateObject + var exist bool + obj, exist = mainDB.getStateObjectFromStateObjects(addr) + if !exist { + continue + } + + if obj.selfDestructed || (deleteEmptyObjects && obj.empty()) { + + obj.deleted = true + + // We need to maintain account deletions explicitly (will remain + // set indefinitely). Note only the first occurred self-destruct + // event is tracked. + mainDB.stateObjectDestructLock.Lock() + if _, ok := mainDB.stateObjectsDestruct[obj.address]; !ok { + mainDB.stateObjectsDestruct[obj.address] = obj.origin + } + mainDB.stateObjectDestructLock.Unlock() + // Note, we can't do this only at the end of a block because multiple + // transactions within the same block might self destruct and then + // resurrect an account; but the snapshotter needs both events. + mainDB.AccountMux.Lock() + delete(mainDB.accounts, obj.addrHash) // Clear out any previously updated account data (may be recreated via a resurrect) + delete(mainDB.accountsOrigin, obj.address) // Clear out any previously updated account data (may be recreated via a resurrect) + mainDB.AccountMux.Unlock() + + mainDB.StorageMux.Lock() + delete(mainDB.storages, obj.addrHash) // Clear out any previously updated storage data (may be recreated via a resurrect) + delete(mainDB.storagesOrigin, obj.address) // Clear out any previously updated storage data (may be recreated via a resurrect) + mainDB.StorageMux.Unlock() + } else { + obj.finalise(true) // Prefetch slots in the background + } + + obj.created = false + mainDB.stateObjectsPending[addr] = struct{}{} + mainDB.stateObjectsDirty[addr] = struct{}{} + + // At this point, also ship the address off to the prefetch. The prefetcher + // will start loading tries, and when the change is eventually committed, + // the commit-phase will be a lot faster + addressesToPrefetch = append(addressesToPrefetch, common.CopyBytes(addr[:])) // Copy needed for closure + } + mainDB.clearJournalAndRefund() + } + + for addr := range s.journal.dirties { + var obj *stateObject + var exist bool + if s.parallel.isSlotDB { + obj = s.parallel.dirtiedStateObjectsInSlot[addr] + if obj != nil { + exist = true + } else { + log.Error("StateDB Finalise dirty addr not in dirtiedStateObjectsInSlot", + "addr", addr) + } + } else { + obj, exist = s.getStateObjectFromStateObjects(addr) + } + if !exist { + continue + } + + if obj.selfDestructed || (deleteEmptyObjects && obj.empty()) { + obj.deleted = true + // We need to maintain account deletions explicitly (will remain + // set indefinitely). Note only the first occurred self-destruct + // event is tracked. + // This is the thread local one, no need to acquire the stateObjectsDestructLock. + if _, ok := s.stateObjectsDestruct[obj.address]; !ok { + s.stateObjectsDestruct[obj.address] = obj.origin + } + + // Note, we can't do this only at the end of a block because multiple + // transactions within the same block might self destruct and then + // resurrect an account; but the snapshotter needs both events. + mainDB.AccountMux.Lock() + delete(mainDB.accounts, obj.addrHash) // Clear out any previously updated account data (may be recreated via a resurrect) + delete(mainDB.accountsOrigin, obj.address) // Clear out any previously updated account data (may be recreated via a resurrect) + mainDB.AccountMux.Unlock() + mainDB.StorageMux.Lock() + delete(mainDB.storages, obj.addrHash) // Clear out any previously updated storage data (may be recreated via a resurrect) + delete(mainDB.storagesOrigin, obj.address) // Clear out any previously updated storage data (may be recreated via a resurrect) + mainDB.StorageMux.Unlock() + } else { + // 1.none parallel mode, we do obj.finalise(true) as normal + // 2.with parallel mode, we do obj.finalise(true) on dispatcher, not on slot routine + // obj.finalise(true) will clear its dirtyStorage, will make prefetch broken. + if !s.isParallel || !s.parallel.isSlotDB { + obj.finalise(true) // Prefetch slots in the background + } else { + // don't do finalise() here as to keep dirtyObjects unchanged in dirtyStorages, which avoid contention issue. + obj.fixUpOriginAndResetPendingStorage() + } + } + + if obj.created { + s.parallel.createdObjectRecord[addr] = struct{}{} + } + obj.created = false + + s.stateObjectsPending[addr] = struct{}{} + s.stateObjectsDirty[addr] = struct{}{} + + // At this point, also ship the address off to the prefetcher. The prefetcher + // will start loading tries, and when the change is eventually committed, + // the commit-phase will be a lot faster + addressesToPrefetch = append(addressesToPrefetch, common.CopyBytes(addr[:])) // Copy needed for closure + } + + if mainDB.prefetcher != nil && len(addressesToPrefetch) > 0 { + mainDB.trieParallelLock.Lock() + mainDB.prefetcher.prefetch(common.Hash{}, s.originalRoot, common.Address{}, addressesToPrefetch) + mainDB.trieParallelLock.Unlock() + } + // Invalidate journal because reverting across transactions is not allowed. + s.clearJournalAndRefund() +} + +func (s *ParallelStateDB) reset() { + + s.StateDB.db = nil + s.StateDB.prefetcher = nil + s.StateDB.trie = nil + s.StateDB.noTrie = false + s.StateDB.hasher = crypto.NewKeccakState() + s.StateDB.snaps = nil + s.StateDB.snap = nil + s.StateDB.snapParallelLock = sync.RWMutex{} + s.StateDB.trieParallelLock = sync.Mutex{} + s.StateDB.stateObjectDestructLock = sync.RWMutex{} + s.StateDB.snapDestructs = addressToStructPool.Get().(map[common.Address]struct{}) + s.StateDB.originalRoot = common.Hash{} + s.StateDB.expectedRoot = common.Hash{} + s.StateDB.stateRoot = common.Hash{} + s.StateDB.fullProcessed = false + s.StateDB.AccountMux = sync.Mutex{} + s.StateDB.StorageMux = sync.Mutex{} + s.StateDB.accounts = make(map[common.Hash][]byte) + s.StateDB.storages = make(map[common.Hash]map[common.Hash][]byte) + s.StateDB.accountsOrigin = make(map[common.Address][]byte) + s.StateDB.storagesOrigin = make(map[common.Address]map[common.Hash][]byte) + s.StateDB.stateObjects = make(map[common.Address]*stateObject) // replaced by parallel.stateObjects in parallel mode + s.StateDB.stateObjectsPending = addressToStructPool.Get().(map[common.Address]struct{}) + s.StateDB.stateObjectsDirty = addressToStructPool.Get().(map[common.Address]struct{}) + s.StateDB.stateObjectsDestruct = make(map[common.Address]*types.StateAccount) + s.StateDB.stateObjectsDestructDirty = make(map[common.Address]*types.StateAccount) + s.StateDB.dbErr = nil + s.StateDB.refund = 0 + s.StateDB.thash = common.Hash{} + s.StateDB.txIndex = 0 + s.StateDB.logs = logsPool.Get().(map[common.Hash][]*types.Log) + s.StateDB.logSize = 0 + s.StateDB.rwSet = nil + s.StateDB.mvStates = nil + s.StateDB.stat = nil + s.StateDB.preimages = nil + s.StateDB.accessList = nil + s.StateDB.transientStorage = nil + s.StateDB.journal = journalPool.Get().(*journal) + s.StateDB.validRevisions = nil + s.StateDB.nextRevisionId = 0 + s.StateDB.AccountReads = 0 + s.StateDB.AccountHashes = 0 + s.StateDB.AccountUpdates = 0 + s.StateDB.AccountCommits = 0 + s.StateDB.StorageReads = 0 + s.StateDB.StorageHashes = 0 + s.StateDB.StorageUpdates = 0 + s.StateDB.StorageCommits = 0 + s.StateDB.SnapshotAccountReads = 0 + s.StateDB.SnapshotStorageReads = 0 + s.StateDB.SnapshotCommits = 0 + s.StateDB.TrieDBCommits = 0 + s.StateDB.TrieCommits = 0 + s.StateDB.CodeCommits = 0 + s.StateDB.TxDAGGenerate = 0 + s.StateDB.AccountUpdated = 0 + s.StateDB.StorageUpdated = 0 + s.StateDB.AccountDeleted = 0 + s.StateDB.StorageDeleted = 0 + s.StateDB.isParallel = true + s.StateDB.parallel = ParallelState{} + s.StateDB.onCommit = nil + + s.parallel.isSlotDB = true + s.parallel.SlotIndex = -1 + s.parallel.stateObjects = nil + s.parallel.locatStateObjects = nil + s.parallel.baseStateDB = nil + s.parallel.baseTxIndex = -1 + s.parallel.dirtiedStateObjectsInSlot = addressToStateObjectsPool.Get().(map[common.Address]*stateObject) + s.parallel.unconfirmedDBs = nil + s.parallel.nonceChangesInSlot = addressToStructPool.Get().(map[common.Address]struct{}) + s.parallel.nonceReadsInSlot = addressToUintPool.Get().(map[common.Address]uint64) + s.parallel.balanceChangesInSlot = addressToStructPool.Get().(map[common.Address]struct{}) + s.parallel.balanceReadsInSlot = balancePool.Get().(map[common.Address]*uint256.Int) + s.parallel.locatStateObjects = addressToStateObjectsPool.Get().(map[common.Address]*stateObject) + s.parallel.codeReadsInSlot = addressToBytesPool.Get().(map[common.Address][]byte) + s.parallel.codeHashReadsInSlot = addressToHashPool.Get().(map[common.Address]common.Hash) + s.parallel.codeChangesInSlot = addressToStructPool.Get().(map[common.Address]struct{}) + s.parallel.kvChangesInSlot = addressToStateKeysPool.Get().(map[common.Address]StateKeys) + s.parallel.kvReadsInSlot = addressToStoragePool.Get().(map[common.Address]Storage) + s.parallel.addrStateReadsInSlot = addressToBoolPool.Get().(map[common.Address]bool) + s.parallel.addrStateChangesInSlot = addressToBoolPool.Get().(map[common.Address]bool) + s.parallel.addrSnapDestructsReadsInSlot = addressToBoolPool.Get().(map[common.Address]bool) + s.parallel.createdObjectRecord = addressToStructPool.Get().(map[common.Address]struct{}) + s.parallel.needsRedo = false + s.parallel.useDAG = false + s.parallel.conflictCheckStateObjectCache = nil + s.parallel.conflictCheckKVReadCache = nil +} diff --git a/core/state/snapshot/conversion.go b/core/state/snapshot/conversion.go index 8a0fd1989a..365660caa2 100644 --- a/core/state/snapshot/conversion.go +++ b/core/state/snapshot/conversion.go @@ -300,6 +300,7 @@ func generateTrieRoot(db ethdb.KeyValueWriter, scheme string, it Iterator, accou fullData []byte ) if leafCallback == nil { + fullData, err = types.FullAccountRLP(it.(AccountIterator).Account()) if err != nil { return stop(err) diff --git a/core/state/state_object.go b/core/state/state_object.go index 8696557845..02a0b5bd26 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -19,11 +19,13 @@ package state import ( "bytes" "fmt" + "github.com/ethereum/go-ethereum/log" "io" "sync" "time" "github.com/ethereum/go-ethereum/core/opcodeCompiler/compiler" + "golang.org/x/exp/slices" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" @@ -40,40 +42,136 @@ func (c Code) String() string { return string(c) //strings.Join(Disassemble(c), " ") } -type Storage map[common.Hash]common.Hash +type Storage interface { + String() string + GetValue(hash common.Hash) (common.Hash, bool) + StoreValue(hash common.Hash, value common.Hash) + Length() (length int) + Copy() Storage + Range(func(key, value interface{}) bool) +} + +type StorageMap map[common.Hash]common.Hash -func (s Storage) String() (str string) { +func (s StorageMap) String() (str string) { for key, value := range s { str += fmt.Sprintf("%X : %X\n", key, value) } return } -func (s Storage) Copy() Storage { - cpy := make(Storage, len(s)) +func (s StorageMap) Copy() Storage { + cpy := make(StorageMap, len(s)) for key, value := range s { cpy[key] = value } return cpy } +func (s StorageMap) GetValue(hash common.Hash) (common.Hash, bool) { + value, ok := s[hash] + return value, ok +} + +func (s StorageMap) StoreValue(hash common.Hash, value common.Hash) { + s[hash] = value +} + +func (s StorageMap) Length() int { + return len(s) +} + +func (s StorageMap) Range(f func(hash, value interface{}) bool) { + for k, v := range s { + result := f(k, v) + if !result { + return + } + } +} + +type StorageSyncMap struct { + sync.Map +} + +func (s *StorageSyncMap) String() (str string) { + s.Range(func(key, value interface{}) bool { + str += fmt.Sprintf("%X : %X\n", key, value) + return true + }) + + return +} + +func (s *StorageSyncMap) GetValue(hash common.Hash) (common.Hash, bool) { + value, ok := s.Load(hash) + if !ok { + return common.Hash{}, ok + } + + return value.(common.Hash), ok +} + +func (s *StorageSyncMap) StoreValue(hash common.Hash, value common.Hash) { + s.Store(hash, value) +} + +func (s *StorageSyncMap) Length() (length int) { + s.Range(func(key, value interface{}) bool { + length++ + return true + }) + return length +} + +func (s *StorageSyncMap) Copy() Storage { + cpy := StorageSyncMap{} + s.Range(func(key, value interface{}) bool { + cpy.Store(key, value) + return true + }) + return &cpy +} + +func newStorage(isParallel bool) Storage { + if isParallel { + return &StorageSyncMap{} + } + return make(StorageMap) +} + // stateObject represents an Ethereum account which is being modified. // // The usage pattern is as follows: // - First you need to obtain a state object. // - Account values as well as storages can be accessed and modified through the object. // - Finally, call commit to return the changes of storage trie and update account data. +// +// NOTICE: For Parallel, there is lightCopy and deepCopy used for cloning object between +// slot DB and global DB, and it is not guaranteed to be happened after finalise(), so any +// field added into the stateObject must be handled in lightCopy and deepCopy. type stateObject struct { - db *StateDB + db *StateDB // The baseDB for parallel. + dbItf StateDBer // The slotDB for parallel. address common.Address // address of ethereum account addrHash common.Hash // hash of ethereum address of the account origin *types.StateAccount // Account original data without any change applied, nil means it was not existent data types.StateAccount // Account data with all mutations applied in the scope of block + // dirty account state + dirtyBalance *uint256.Int + dirtyNonce *uint64 + dirtyCodeHash []byte + // Write caches. trie Trie // storage trie, which becomes non-nil on first access code Code // contract bytecode, which gets set when code is loaded + // isParallel indicates this state object is used in parallel mode, in which mode the + // storage would be sync.Map instead of map + isParallel bool + storageRecordsLock sync.RWMutex // for pending/dirty/origin storage read (lightCopy) and write (Intermediate/FixupOrigin) + originStorage Storage // Storage cache of original entries to dedup rewrites pendingStorage Storage // Storage entries that need to be flushed to disk, at the end of an entire block dirtyStorage Storage // Storage entries that have been modified in the current transaction execution, reset for every transaction @@ -96,11 +194,55 @@ type stateObject struct { // empty returns whether the account is considered empty. func (s *stateObject) empty() bool { - return s.data.Nonce == 0 && s.data.Balance.IsZero() && bytes.Equal(s.data.CodeHash, types.EmptyCodeHash.Bytes()) + // return s.data.Nongn() == 0 && bytes.ce == 0 && s.data.Balance.SiEqual(s.data.CodeHash, types.EmptyCodeHash.Bytes()) + // return s.data.Nonce == 0 && s.data.Balance.Sign() == 0 && bytes.Equal(s.data.CodeHash, emptyCodeHash) + + // empty() has 3 use cases: + // 1.StateDB.Empty(), to empty check + // A: It is ok, we have handled it in Empty(), to make sure nonce, balance, codeHash are solid + // 2:AddBalance 0, empty check for touch event + // empty() will add a touch event. + // if we misjudge it, the touch event could be lost, which make address not deleted. // fixme + // 3.Finalise(), to do empty delete + // the address should be dirtied or touched + // if it nonce dirtied, it is ok, since nonce is monotonically increasing, won't be zero + // if balance is dirtied, balance could be zero, we should refer solid nonce & codeHash // fixme + // if codeHash is dirtied, it is ok, since code will not be updated. + // if suicide, it is ok + // if object is new created, it is ok + // if CreateAccount, recreate the address, it is ok. + + // Slot 0 tx 0: AddBalance(100) to addr_1, => addr_1: balance = 100, nonce = 0, code is empty + // Slot 1 tx 1: addr_1 Transfer 99.9979 with GasFee 0.0021, => addr_1: balance = 0, nonce = 1, code is empty + // notice: balance transfer cost 21,000 gas, with gasPrice = 100Gwei, GasFee will be 0.0021 + // Slot 0 tx 2: add balance 0 to addr_1(empty check for touch event), + // the object was lightCopied from tx 0, + + // in parallel mode, we should not check empty by raw nonce, balance, codeHash anymore, + // since it could be invalid. + // e.g., AddBalance() to an address, we will do lightCopy to get a new StateObject, we did balance fixup to + // make sure object's Balance is reliable. But we did not fixup nonce or code, we only do nonce or codehash + // fixup on need, that's when we want to update the nonce or codehash. + // So nonce, balance + // Before the block is processed, addr_1 account: nonce = 0, emptyCodeHash, balance = 100 + // Slot 0 tx 0: no access to addr_1 + // Slot 1 tx 1: sub balance 100, it is empty and deleted + // Slot 0 tx 2: GetNonce, lightCopy based on main DB(balance = 100) , not empty + + if !s.dbItf.GetBalance(s.address).IsZero() { // check balance first, since it is most likely not zero + + return false + } + if s.dbItf.GetNonce(s.address) != 0 { + return false + } + codeHash := s.dbItf.GetCodeHash(s.address) + return bytes.Equal(codeHash.Bytes(), types.EmptyCodeHash.Bytes()) // code is empty, the object is empty } // newObject creates a state object. -func newObject(db *StateDB, address common.Address, acct *types.StateAccount) *stateObject { +func newObject(dbItf StateDBer, isParallel bool, address common.Address, acct *types.StateAccount) *stateObject { + db := dbItf.getBaseStateDB() var ( origin = acct created = acct == nil // true if the account was not existent @@ -108,17 +250,29 @@ func newObject(db *StateDB, address common.Address, acct *types.StateAccount) *s if acct == nil { acct = types.NewEmptyStateAccount() } - return &stateObject{ + s := &stateObject{ db: db, + dbItf: dbItf, address: address, addrHash: crypto.Keccak256Hash(address[:]), origin: origin, - data: *acct, - originStorage: make(Storage), - pendingStorage: make(Storage), - dirtyStorage: make(Storage), + data: *acct.Copy(), + isParallel: isParallel, + originStorage: newStorage(isParallel), + pendingStorage: newStorage(isParallel), + dirtyStorage: newStorage(isParallel), created: created, } + + // dirty data when create a new account + + if created { + s.dirtyBalance = new(uint256.Int).Set(acct.Balance) + s.dirtyNonce = new(uint64) + *s.dirtyNonce = acct.Nonce + s.dirtyCodeHash = acct.CodeHash + } + return s } // EncodeRLP implements rlp.Encoder. @@ -165,32 +319,75 @@ func (s *stateObject) getTrie() (Trie, error) { // GetState retrieves a value from the account storage trie. func (s *stateObject) GetState(key common.Hash) common.Hash { // If we have a dirty value for this state entry, return it - value, dirty := s.dirtyStorage[key] + value, dirty := s.dirtyStorage.GetValue(key) if dirty { return value } // Otherwise return the entry's original value - return s.GetCommittedState(key) + result := s.GetCommittedState(key) + // Record first read for conflict verify + if s.db.isParallel && s.db.parallel.isSlotDB { + addr := s.address + if s.db.parallel.kvReadsInSlot[addr] == nil { + s.db.parallel.kvReadsInSlot[addr] = newStorage(false) + } + if _, ok := s.db.parallel.kvReadsInSlot[addr].GetValue(key); !ok { + s.db.parallel.kvReadsInSlot[addr].StoreValue(key, result) + } + } + return result } // GetCommittedState retrieves a value from the committed account storage trie. func (s *stateObject) GetCommittedState(key common.Hash) common.Hash { // If we have a pending write or clean cached, return that - if value, pending := s.pendingStorage[key]; pending { + // if value, pending := s.pendingStorage[key]; pending { + if value, pending := s.pendingStorage.GetValue(key); pending { return value } - if value, cached := s.originStorage[key]; cached { + if value, cached := s.originStorage.GetValue(key); cached { return value } + + if s.db.isParallel && s.db.parallel.isSlotDB { + // Need to confirm the object is not destructed in unconfirmed db and resurrected in this tx. + // otherwise there is an issue for cases like: + // B0: TX0 --> createAccount @addr1 -- merged into DB + // B1: Tx1 and Tx2 + // Tx1 account@addr1, setState(key0), setState(key1) selfDestruct -- unconfirmed + // Tx2 recreate account@addr2, setState(key0) -- executing + // TX2 GetState(addr2, key1) --- + // key1 is never set after recurrsect, and should not return state in trie as it destructed in unconfirmed + if s.db.parallel.useDAG != true { + obj, exist := s.dbItf.GetStateObjectFromUnconfirmedDB(s.address) + if exist { + if obj.deleted || obj.selfDestructed { + return common.Hash{} + } + } + } + // also test whether the object is in mainDB and deleted. + pdb := s.db.parallel.baseStateDB + obj, exist := pdb.getStateObjectFromStateObjects(s.address) + if exist { + if obj.deleted || obj.selfDestructed { + return common.Hash{} + } + } + } // If the object was destructed in *this* block (and potentially resurrected), // the storage has been cleared out, and we should *not* consult the previous // database about any storage values. The only possible alternatives are: // 1) resurrect happened, and new slot values were set -- those should // have been handles via pendingStorage above. // 2) we don't have new values, and can deliver empty response back - if _, destructed := s.db.stateObjectsDestruct[s.address]; destructed { + s.db.stateObjectDestructLock.RLock() + if _, destructed := s.db.getStateObjectsDestruct(s.address); destructed { // fixme: use sync.Map, instead of RWMutex? + s.db.stateObjectDestructLock.RUnlock() return common.Hash{} } + s.db.stateObjectDestructLock.RUnlock() + // If no live objects are available, attempt to use snapshots var ( enc []byte @@ -214,6 +411,8 @@ func (s *stateObject) GetCommittedState(key common.Hash) common.Hash { // If the snapshot is unavailable or reading from it fails, load from the database. if s.db.snap == nil || err != nil { start := time.Now() + s.db.trieParallelLock.Lock() + defer s.db.trieParallelLock.Unlock() tr, err := s.getTrie() if err != nil { s.db.setError(err) @@ -229,14 +428,21 @@ func (s *stateObject) GetCommittedState(key common.Hash) common.Hash { } value.SetBytes(val) } - s.originStorage[key] = value + s.originStorage.StoreValue(key, value) return value } // SetState updates a value in account storage. func (s *stateObject) SetState(key, value common.Hash) { // If the new value is the same as old, don't set - prev := s.GetState(key) + // In parallel mode, it has to get from StateDB, in case: + // a.the Slot did not set the key before and try to set it to `val_1` + // b.Unconfirmed DB has set the key to `val_2` + // c.if we use StateObject.GetState, and the key load from the main DB is `val_1` + // this `SetState could be skipped` + // d.Finally, the key's value will be `val_2`, while it should be `val_1` + // such as: https://bscscan.com/txs?block=2491181 + prev := s.dbItf.GetState(s.address, key) if prev == value { return } @@ -246,28 +452,76 @@ func (s *stateObject) SetState(key, value common.Hash) { key: key, prevalue: prev, }) + + if s.db.isParallel && s.db.parallel.isSlotDB { + s.db.parallel.kvChangesInSlot[s.address][key] = struct{}{} + } s.setState(key, value) } func (s *stateObject) setState(key, value common.Hash) { - s.dirtyStorage[key] = value + s.dirtyStorage.StoreValue(key, value) } // finalise moves all dirty storage slots into the pending area to be hashed or // committed later. It is invoked at the end of every transaction. func (s *stateObject) finalise(prefetch bool) { - slotsToPrefetch := make([][]byte, 0, len(s.dirtyStorage)) - for key, value := range s.dirtyStorage { - s.pendingStorage[key] = value - if value != s.originStorage[key] { - slotsToPrefetch = append(slotsToPrefetch, common.CopyBytes(key[:])) // Copy needed for closure + s.storageRecordsLock.Lock() + defer s.storageRecordsLock.Unlock() + slotsToPrefetch := make([][]byte, 0, s.dirtyStorage.Length()) + s.dirtyStorage.Range(func(key, value interface{}) bool { + s.pendingStorage.StoreValue(key.(common.Hash), value.(common.Hash)) + originalValue, _ := s.originStorage.GetValue(key.(common.Hash)) + if value.(common.Hash) != originalValue { + originalKey := key.(common.Hash) + slotsToPrefetch = append(slotsToPrefetch, common.CopyBytes(originalKey[:])) // Copy needed for closure } + return true + }) + if s.dirtyNonce != nil { + s.data.Nonce = *s.dirtyNonce + s.dirtyNonce = nil + } + if s.dirtyBalance != nil { + if s.address.Hex() == "0x13f4EA83D0bd40E75C8222255bc855a974568Dd4" { + log.Debug("finalise - set dirtyBalance", "addr", s.address.Hex(), "balance", s.data.Balance, "dirtyBalance", s.dirtyBalance) + } + s.data.Balance = s.dirtyBalance + s.dirtyBalance = nil + } + if s.dirtyCodeHash != nil { + s.data.CodeHash = s.dirtyCodeHash + s.dirtyCodeHash = nil } if s.db.prefetcher != nil && prefetch && len(slotsToPrefetch) > 0 && s.data.Root != types.EmptyRootHash { + s.db.trieParallelLock.Lock() s.db.prefetcher.prefetch(s.addrHash, s.data.Root, s.address, slotsToPrefetch) + s.db.trieParallelLock.Unlock() } - if len(s.dirtyStorage) > 0 { - s.dirtyStorage = make(Storage) + if s.dirtyStorage.Length() > 0 { + s.dirtyStorage = newStorage(s.isParallel) + } +} + +func (s *stateObject) finaliseRWSet() { + s.dirtyStorage.Range(func(key, value interface{}) bool { + // three are some unclean dirtyStorage from previous reverted txs, it will skip finalise + // so add a new rule, if val has no change, then skip it + if value == s.GetCommittedState(key.(common.Hash)) { + return true + } + s.db.RecordWrite(types.StorageStateKey(s.address, key.(common.Hash)), value.(common.Hash)) + return true + }) + + if s.dirtyNonce != nil && *s.dirtyNonce != s.data.Nonce { + s.db.RecordWrite(types.AccountStateKey(s.address, types.AccountNonce), *s.dirtyNonce) + } + if s.dirtyBalance != nil && s.dirtyBalance.Cmp(s.data.Balance) != 0 { + s.db.RecordWrite(types.AccountStateKey(s.address, types.AccountBalance), new(uint256.Int).Set(s.dirtyBalance)) + } + if s.dirtyCodeHash != nil && !slices.Equal(s.dirtyCodeHash, s.data.CodeHash) { + s.db.RecordWrite(types.AccountStateKey(s.address, types.AccountCodeHash), s.dirtyCodeHash) } } @@ -278,16 +532,25 @@ func (s *stateObject) finalise(prefetch bool) { // this function will return the mutated storage trie, or nil if there is no // storage change at all. func (s *stateObject) updateTrie() (Trie, error) { + maindb := s.db + if s.db.isParallel && s.db.parallel.isSlotDB { + // we need to fixup the origin storage with the mainDB. otherwise the changes maybe problematic since the origin + // is wrong. + maindb = s.db.parallel.baseStateDB + // For dirty/pending/origin Storage access and update. + s.storageRecordsLock.Lock() + defer s.storageRecordsLock.Unlock() + } // Make sure all dirty slots are finalized into the pending storage area s.finalise(false) // Short circuit if nothing changed, don't bother with hashing anything - if len(s.pendingStorage) == 0 { + if s.pendingStorage.Length() == 0 { return s.trie, nil } // Track the amount of time wasted on updating the storage trie if metrics.EnabledExpensive { - defer func(start time.Time) { s.db.StorageUpdates += time.Since(start) }(time.Now()) + defer func(start time.Time) { maindb.StorageUpdates += time.Since(start) }(time.Now()) } // The snapshot storage map for the object var ( @@ -297,17 +560,21 @@ func (s *stateObject) updateTrie() (Trie, error) { ) tr, err := s.getTrie() if err != nil { - s.db.setError(err) + maindb.setError(err) return nil, err } + // Insert all the pending storage updates into the trie - usedStorage := make([][]byte, 0, len(s.pendingStorage)) + usedStorage := make([][]byte, 0, s.pendingStorage.Length()) dirtyStorage := make(map[common.Hash][]byte) - for key, value := range s.pendingStorage { + s.pendingStorage.Range(func(keyItf, valueItf interface{}) bool { + key := keyItf.(common.Hash) + value := valueItf.(common.Hash) // Skip noop changes, persist actual changes - if value == s.originStorage[key] { - continue + originalValue, _ := s.originStorage.GetValue(key) + if value == originalValue { + return true } var v []byte if value != (common.Hash{}) { @@ -315,7 +582,8 @@ func (s *stateObject) updateTrie() (Trie, error) { v = common.TrimLeftZeroes(value[:]) } dirtyStorage[key] = v - } + return true + }) var wg sync.WaitGroup wg.Add(1) go func() { @@ -323,14 +591,14 @@ func (s *stateObject) updateTrie() (Trie, error) { for key, value := range dirtyStorage { if len(value) == 0 { if err := tr.DeleteStorage(s.address, key[:]); err != nil { - s.db.setError(err) + maindb.setError(err) } - s.db.StorageDeleted += 1 + maindb.StorageDeleted += 1 } else { if err := tr.UpdateStorage(s.address, key[:], value); err != nil { - s.db.setError(err) + maindb.setError(err) } - s.db.StorageUpdated += 1 + maindb.StorageUpdated += 1 } // Cache the items for preloading usedStorage = append(usedStorage, common.CopyBytes(key[:])) @@ -340,20 +608,21 @@ func (s *stateObject) updateTrie() (Trie, error) { wg.Add(1) go func() { defer wg.Done() - s.db.StorageMux.Lock() + maindb.StorageMux.Lock() + // The snapshot storage map for the object - storage = s.db.storages[s.addrHash] + storage = maindb.storages[s.addrHash] if storage == nil { storage = make(map[common.Hash][]byte, len(dirtyStorage)) - s.db.storages[s.addrHash] = storage + maindb.storages[s.addrHash] = storage } // Cache the original value of mutated storage slots - origin = s.db.storagesOrigin[s.address] + origin = maindb.storagesOrigin[s.address] if origin == nil { origin = make(map[common.Hash][]byte) - s.db.storagesOrigin[s.address] = origin + maindb.storagesOrigin[s.address] = origin } - s.db.StorageMux.Unlock() + maindb.StorageMux.Unlock() for key, value := range dirtyStorage { khash := crypto.HashData(hasher, key[:]) @@ -365,8 +634,8 @@ func (s *stateObject) updateTrie() (Trie, error) { storage[khash] = encoded // encoded will be nil if it's deleted // Track the original value of slot only if it's mutated first time - prev := s.originStorage[key] - s.originStorage[key] = common.BytesToHash(value) // fill back left zeroes by BytesToHash + prev, _ := s.originStorage.GetValue(key) + s.originStorage.StoreValue(key, common.BytesToHash(value)) // fill back left zeroes by BytesToHash if _, ok := origin[khash]; !ok { if prev == (common.Hash{}) { origin[khash] = nil // nil if it was not present previously @@ -380,17 +649,19 @@ func (s *stateObject) updateTrie() (Trie, error) { }() wg.Wait() - if s.db.prefetcher != nil { - s.db.prefetcher.used(s.addrHash, s.data.Root, usedStorage) + if maindb.prefetcher != nil { + maindb.prefetcher.used(s.addrHash, s.data.Root, usedStorage) } - s.pendingStorage = make(Storage) // reset pending map + + s.pendingStorage = newStorage(s.isParallel) // reset pending map return tr, nil } // updateRoot flushes all cached storage mutations to trie, recalculating the // new storage trie root. func (s *stateObject) updateRoot() { - // If node runs in no trie mode, set root to empty. + + // If node runs in no trie mode, set root to empty defer func() { if s.db.db.NoTries() { s.data.Root = types.EmptyRootHash @@ -399,6 +670,9 @@ func (s *stateObject) updateRoot() { // Flush cached storage mutations into trie, short circuit if any error // is occurred or there is not change in the trie. + s.db.trieParallelLock.Lock() + defer s.db.trieParallelLock.Unlock() + tr, err := s.updateTrie() if err != nil || tr == nil { return @@ -408,6 +682,7 @@ func (s *stateObject) updateRoot() { defer func(start time.Time) { s.db.StorageHashes += time.Since(start) }(time.Now()) } s.data.Root = tr.Hash() + log.Debug("updateRoot", "addr", s.address, "data", s.data) } // commit obtains a set of dirty storage trie nodes and updates the account data. @@ -431,7 +706,6 @@ func (s *stateObject) commit() (*trienode.NodeSet, error) { return nil, err } s.data.Root = root - // Update original account data after commit s.origin = s.data.Copy() return nodes, nil @@ -463,34 +737,104 @@ func (s *stateObject) SubBalance(amount *uint256.Int) { func (s *stateObject) SetBalance(amount *uint256.Int) { s.db.journal.append(balanceChange{ account: &s.address, - prev: new(uint256.Int).Set(s.data.Balance), + prev: new(uint256.Int).Set(s.Balance()), }) s.setBalance(amount) } func (s *stateObject) setBalance(amount *uint256.Int) { - s.data.Balance = amount + if s.address.Hex() == "0x13f4EA83D0bd40E75C8222255bc855a974568Dd4" { + log.Debug("setBalance", "addr", "amount", amount, "s.dirtyBlance", s.dirtyBalance, "s.data.balance", s.data.Balance) + } + s.dirtyBalance = amount } +// ReturnGas Return the gas back to the origin. Used by the Virtual machine or Closures +func (s *stateObject) ReturnGas(gas *uint256.Int) {} + +func (s *stateObject) lightCopy(db *ParallelStateDB) *stateObject { + object := newObject(db, s.isParallel, s.address, &s.data) + if s.trie != nil { + s.db.trieParallelLock.Lock() + object.trie = db.db.CopyTrie(s.trie) + s.db.trieParallelLock.Unlock() + } + object.code = s.code + object.selfDestructed = s.selfDestructed // should be false + object.dirtyCode = s.dirtyCode // it is not used in slot, but keep it is ok + object.deleted = s.deleted // should be false + + object.dirtyBalance = s.dirtyBalance + object.dirtyNonce = s.dirtyNonce + object.dirtyCodeHash = s.dirtyCodeHash + + // object generated by lightCopy() is supposed to be used in the slot. + // and the origin storage will be filled at GetState() etc. + // the dirty and pending will be recorded in the execution for new changes. + // so no need to do the copy. + // moreover, copy storage here is tricky, as the stateDB is changed concurrently with + // the slot execution, and the snap is updated only at Commit stage. + // so the origin may different between the time NOW and the time of merge, so the conflict check is vital to avoid + // the problem. fortunately, the KVRead will record this and compare it with mainDB. + + //object.dirtyStorage = s.dirtyStorage.Copy() + s.storageRecordsLock.RLock() + object.originStorage = s.originStorage.Copy() + object.pendingStorage = s.pendingStorage.Copy() + s.storageRecordsLock.RUnlock() + return object +} + +// deepCopy happens only at global serial execution stage. +// E.g. prepareForParallel and merge (copy slotObj to mainDB) +// otherwise the origin/dirty/pending storages may cause incorrect issue. func (s *stateObject) deepCopy(db *StateDB) *stateObject { - obj := &stateObject{ - db: db, - address: s.address, - addrHash: s.addrHash, - origin: s.origin, - data: s.data, + object := &stateObject{ + db: db.getBaseStateDB(), + dbItf: db, + address: s.address, + addrHash: s.addrHash, + origin: s.origin, + data: *s.data.Copy(), + isParallel: s.isParallel, } if s.trie != nil { - obj.trie = db.db.CopyTrie(s.trie) + s.db.trieParallelLock.Lock() + object.trie = db.db.CopyTrie(s.trie) + s.db.trieParallelLock.Unlock() } - obj.code = s.code - obj.dirtyStorage = s.dirtyStorage.Copy() - obj.originStorage = s.originStorage.Copy() - obj.pendingStorage = s.pendingStorage.Copy() - obj.selfDestructed = s.selfDestructed - obj.dirtyCode = s.dirtyCode - obj.deleted = s.deleted - return obj + + object.code = s.code + object.dirtyStorage = s.dirtyStorage.Copy() + object.originStorage = s.originStorage.Copy() + object.pendingStorage = s.pendingStorage.Copy() + object.selfDestructed = s.selfDestructed + object.dirtyCode = s.dirtyCode + object.deleted = s.deleted + object.dirtyBalance = s.dirtyBalance + object.dirtyNonce = s.dirtyNonce + object.dirtyCodeHash = s.dirtyCodeHash + return object +} + +func (s *stateObject) MergeSlotObject(db Database, dirtyObjs *stateObject, keys StateKeys) { + for key := range keys { + // In parallel mode, always GetState by StateDB, not by StateObject directly, + // since it the KV could exist in unconfirmed DB. + // But here, it should be ok, since the KV should be changed and valid in the SlotDB, + s.setState(key, dirtyObjs.GetState(key)) + } + + // The dirtyObject may have new state accessed from Snap and Trie, so merge the origins. + dirtyObjs.originStorage.Range(func(keyItf, valueItf interface{}) bool { + key := keyItf.(common.Hash) + value := valueItf.(common.Hash) + // Skip noop changes, persist actual changes + if _, ok := s.originStorage.GetValue(key); !ok { + s.originStorage.StoreValue(key, value) + } + return true + }) } // @@ -536,7 +880,7 @@ func (s *stateObject) CodeSize() int { } func (s *stateObject) SetCode(codeHash common.Hash, code []byte) { - prevcode := s.Code() + prevcode := s.dbItf.GetCode(s.address) s.db.journal.append(codeChange{ account: &s.address, prevhash: s.CodeHash(), @@ -547,35 +891,162 @@ func (s *stateObject) SetCode(codeHash common.Hash, code []byte) { func (s *stateObject) setCode(codeHash common.Hash, code []byte) { s.code = code - s.data.CodeHash = codeHash[:] + s.dirtyCodeHash = codeHash[:] s.dirtyCode = true compiler.GenOrLoadOptimizedCode(codeHash, s.code) } func (s *stateObject) SetNonce(nonce uint64) { + prevNonce := s.dbItf.GetNonce(s.address) s.db.journal.append(nonceChange{ account: &s.address, - prev: s.data.Nonce, + prev: prevNonce, }) s.setNonce(nonce) } func (s *stateObject) setNonce(nonce uint64) { - s.data.Nonce = nonce + s.dirtyNonce = &nonce } func (s *stateObject) CodeHash() []byte { + if len(s.dirtyCodeHash) > 0 { + return s.dirtyCodeHash + } return s.data.CodeHash } func (s *stateObject) Balance() *uint256.Int { + if s.dirtyBalance != nil { + return s.dirtyBalance + } return s.data.Balance } func (s *stateObject) Nonce() uint64 { + if s.dirtyNonce != nil { + return *s.dirtyNonce + } return s.data.Nonce } func (s *stateObject) Root() common.Hash { return s.data.Root } + +// GetStateNoUpdate retrieves a value from the account storage trie, but never update the stateDB cache +func (s *stateObject) GetStateNoUpdate(key common.Hash) common.Hash { + // If we have a dirty value for this state entry, return it + value, dirty := s.dirtyStorage.GetValue(key) + if dirty { + return value + } + // Otherwise return the entry's original value + result := s.GetCommittedStateNoUpdate(key) + return result +} + +// GetCommittedStateNoUpdate retrieves a value from the committed account storage trie, but never update the +// stateDB cache (object.originStorage) +func (s *stateObject) GetCommittedStateNoUpdate(key common.Hash) common.Hash { + // If we have a pending write or clean cached, return that + // if value, pending := s.pendingStorage[key]; pending { + if value, pending := s.pendingStorage.GetValue(key); pending { + return value + } + if value, cached := s.originStorage.GetValue(key); cached { + return value + } + + // If the object was destructed in *this* block (and potentially resurrected), + // the storage has been cleared out, and we should *not* consult the previous + // database about any storage values. The only possible alternatives are: + // 1) resurrect happened, and new slot values were set -- those should + // have been handles via pendingStorage above. + // 2) we don't have new values, and can deliver empty response back + //if _, destructed := s.db.stateObjectsDestruct[s.address]; destructed { + s.db.stateObjectDestructLock.RLock() + if _, destructed := s.db.getStateObjectsDestruct(s.address); destructed { // fixme: use sync.Map, instead of RWMutex? + s.db.stateObjectDestructLock.RUnlock() + return common.Hash{} + } + s.db.stateObjectDestructLock.RUnlock() + + // If no live objects are available, attempt to use snapshots + var ( + enc []byte + err error + value common.Hash + ) + if s.db.snap != nil { + start := time.Now() + enc, err = s.db.snap.Storage(s.addrHash, crypto.Keccak256Hash(key.Bytes())) + if metrics.EnabledExpensive { + s.db.SnapshotStorageReads += time.Since(start) + } + if len(enc) > 0 { + _, content, _, err := rlp.Split(enc) + if err != nil { + s.db.setError(err) + } + value.SetBytes(content) + } + } + // If the snapshot is unavailable or reading from it fails, load from the database. + if s.db.snap == nil || err != nil { + start := time.Now() + s.db.trieParallelLock.Lock() + defer s.db.trieParallelLock.Unlock() + tr, err := s.getTrie() + if err != nil { + s.db.setError(err) + return common.Hash{} + } + val, err := tr.GetStorage(s.address, key.Bytes()) + if metrics.EnabledExpensive { + s.db.StorageReads += time.Since(start) + } + if err != nil { + s.db.setError(err) + return common.Hash{} + } + value.SetBytes(val) + } + return value +} + +// fixUpOriginAndResetPendingStorage is used for slot object only, the target is to fix up the origin storage of the +// object with the latest mainDB. And reset the pendingStorage as the execution recorded the changes in dirty and the +// dirties will be merged to pending at finalise. so the current pendingStorage contains obsoleted info mainly from +// lightCopy() +func (s *stateObject) fixUpOriginAndResetPendingStorage() { + if s.db.isParallel && s.db.parallel.isSlotDB { + mainDB := s.db.parallel.baseStateDB + origObj := mainDB.getStateObject(s.address) + s.storageRecordsLock.Lock() + if origObj != nil && origObj.originStorage.Length() != 0 { + // There can be racing issue with CopyForSlot/LightCopy + origObj.storageRecordsLock.RLock() + originStorage := origObj.originStorage.Copy() + origObj.storageRecordsLock.RUnlock() + // During the tx execution, the originStorage can be updated with GetCommittedState() + // But is never get updated for the already existed one as there is no finalise called in execution. + // so here get the latest object in MainDB, and update the object storage with + s.originStorage.Range(func(keyItf, valueItf interface{}) bool { + key := keyItf.(common.Hash) + value := valueItf.(common.Hash) + // Skip noop changes, persist actual changes + if _, ok := originStorage.GetValue(key); !ok { + originStorage.StoreValue(key, value) + } + return true + }) + s.originStorage = originStorage + } + // isParallel is unnecessary since the pendingStorage for slotObject will be used serially from now on. + if s.pendingStorage.Length() > 0 { + s.pendingStorage = newStorage(false) + } + s.storageRecordsLock.Unlock() + } +} diff --git a/core/state/state_test.go b/core/state/state_test.go index 9be610f962..063d4b5567 100644 --- a/core/state/state_test.go +++ b/core/state/state_test.go @@ -269,30 +269,46 @@ func compareStateObjects(so0, so1 *stateObject, t *testing.T) { t.Fatalf("Code mismatch: have %v, want %v", so0.code, so1.code) } - if len(so1.dirtyStorage) != len(so0.dirtyStorage) { - t.Errorf("Dirty storage size mismatch: have %d, want %d", len(so1.dirtyStorage), len(so0.dirtyStorage)) + if so1.dirtyStorage.Length() != so0.dirtyStorage.Length() { + t.Errorf("Dirty storage size mismatch: have %d, want %d", so1.dirtyStorage.Length(), so0.dirtyStorage.Length()) } - for k, v := range so1.dirtyStorage { - if so0.dirtyStorage[k] != v { - t.Errorf("Dirty storage key %x mismatch: have %v, want %v", k, so0.dirtyStorage[k], v) + so1.dirtyStorage.Range(func(key, value interface{}) bool { + k, v := key.(common.Hash), value.(common.Hash) + + if tmpV, _ := so0.dirtyStorage.GetValue(k); tmpV != v { + t.Errorf("Dirty storage key %x mismatch: have %v, want %v", k, tmpV.String(), v) } - } - for k, v := range so0.dirtyStorage { - if so1.dirtyStorage[k] != v { + return true + }) + + so0.dirtyStorage.Range(func(key, value interface{}) bool { + k, v := key.(common.Hash), value.(common.Hash) + + if tmpV, _ := so1.dirtyStorage.GetValue(k); tmpV != v { t.Errorf("Dirty storage key %x mismatch: have %v, want none.", k, v) } + return true + }) + + if so1.originStorage.Length() != so0.originStorage.Length() { + t.Errorf("Origin storage size mismatch: have %d, want %d", so1.originStorage.Length(), so0.originStorage.Length()) } - if len(so1.originStorage) != len(so0.originStorage) { - t.Errorf("Origin storage size mismatch: have %d, want %d", len(so1.originStorage), len(so0.originStorage)) - } - for k, v := range so1.originStorage { - if so0.originStorage[k] != v { - t.Errorf("Origin storage key %x mismatch: have %v, want %v", k, so0.originStorage[k], v) + + so1.originStorage.Range(func(key, value interface{}) bool { + k, v := key.(common.Hash), value.(common.Hash) + + if tmpV, _ := so0.originStorage.GetValue(k); tmpV != v { + t.Errorf("Origin storage key %x mismatch: have %v, want %v", k, tmpV, v) } - } - for k, v := range so0.originStorage { - if so1.originStorage[k] != v { + return true + }) + + so0.originStorage.Range(func(key, value interface{}) bool { + k, v := key.(common.Hash), value.(common.Hash) + + if tmpV, _ := so1.originStorage.GetValue(k); tmpV != v { t.Errorf("Origin storage key %x mismatch: have %v, want none.", k, v) } - } + return true + }) } diff --git a/core/state/statedb.go b/core/state/statedb.go index f5464eb23c..52ba04b8ae 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -18,6 +18,8 @@ package state import ( + "container/list" + "errors" "fmt" "runtime" "sort" @@ -51,6 +53,119 @@ type revision struct { journalIndex int } +var emptyAddr = common.Address{} + +type StateKeys map[common.Hash]struct{} + +type StateObjectSyncMap struct { + sync.Map +} + +type DelayedGasFee struct { + BaseFee *uint256.Int + TipFee *uint256.Int + L1Fee *uint256.Int + Coinbase common.Address +} + +func (s *StateObjectSyncMap) LoadStateObject(addr common.Address) (*stateObject, bool) { + so, ok := s.Load(addr) + if !ok { + return nil, ok + } + return so.(*stateObject), ok +} + +func (s *StateObjectSyncMap) StoreStateObject(addr common.Address, stateObject *stateObject) { + s.Store(addr, stateObject) +} + +// loadStateObj is the entry for loading state object from stateObjects in StateDB or stateObjects in parallel +func (s *StateDB) loadStateObj(addr common.Address) (*stateObject, bool) { + if s.isParallel { + if s.parallel.isSlotDB { + if ret, ok := s.parallel.locatStateObjects[addr]; ok { + return ret, ok + } else { + ret, ok := s.parallel.baseStateDB.loadStateObj(addr) + return ret, ok + } + } + ret, ok := s.parallel.stateObjects.LoadStateObject(addr) + return ret, ok + } + + obj, ok := s.stateObjects[addr] + return obj, ok +} + +// storeStateObj is the entry for storing state object to stateObjects in StateDB or stateObjects in parallel +func (s *StateDB) storeStateObj(addr common.Address, stateObject *stateObject) { + if s.isParallel { + if s.parallel.isSlotDB { + s.parallel.locatStateObjects[addr] = stateObject + } else { + s.parallel.stateObjects.StoreStateObject(addr, stateObject) + } + } else { + s.stateObjects[addr] = stateObject + } +} + +// deleteStateObj is the entry for deleting state object to stateObjects in StateDB or stateObjects in parallel +func (s *StateDB) deleteStateObj(addr common.Address) { + if s.isParallel { + if s.parallel.isSlotDB { + delete(s.parallel.locatStateObjects, addr) + } + s.parallel.stateObjects.Delete(addr) + } else { + delete(s.stateObjects, addr) + } +} + +// ParallelState is for parallel mode only +type ParallelState struct { + isSlotDB bool // denotes StateDB is used in slot, we will try to remove it + SlotIndex int // for debug + // stateObjects holds the state objects in the base slot db + stateObjects *StateObjectSyncMap + locatStateObjects map[common.Address]*stateObject + + baseStateDB *StateDB // for parallel mode, there will be a base StateDB in dispatcher routine. + baseTxIndex int // slotDB is created base on this tx index. + dirtiedStateObjectsInSlot map[common.Address]*stateObject + unconfirmedDBs *sync.Map // do unconfirmed reference in same slot. + + // record the read detail for conflict check and + // the changed addr or key for object merge, the changed detail can be achieved from the dirty object + nonceChangesInSlot map[common.Address]struct{} + nonceReadsInSlot map[common.Address]uint64 + balanceChangesInSlot map[common.Address]struct{} // the address's balance has been changed + balanceReadsInSlot map[common.Address]*uint256.Int // the address's balance has been read and used. + // codeSize can be derived based on code, but codeHash can not be directly derived based on code + // - codeSize is 0 for address not exist or empty code + // - codeHash is `common.Hash{}` for address not exist, emptyCodeHash(`Keccak256Hash(nil)`) for empty code, + // so we use codeReadsInSlot & codeHashReadsInSlot to keep code and codeHash, codeSize is derived from code + codeReadsInSlot map[common.Address][]byte // empty if address not exist or no code in this address + codeHashReadsInSlot map[common.Address]common.Hash + codeChangesInSlot map[common.Address]struct{} + kvReadsInSlot map[common.Address]Storage + kvChangesInSlot map[common.Address]StateKeys // value will be kept in dirtiedStateObjectsInSlot + // Actions such as SetCode, Suicide will change address's state. + // Later call like Exist(), Empty(), HasSuicided() depend on the address's state. + addrStateReadsInSlot map[common.Address]bool // true: exist, false: not exist or deleted + addrStateChangesInSlot map[common.Address]bool // true: created, false: deleted + + addrSnapDestructsReadsInSlot map[common.Address]bool + createdObjectRecord map[common.Address]struct{} + // we may need to redo for some specific reasons, like we read the wrong state and need to panic in sequential mode in SubRefund + needsRedo bool + useDAG bool + conflictCheckStateObjectCache *sync.Map + conflictCheckKVReadCache *sync.Map +} + // StateDB structs within the ethereum protocol are used to store anything // within the merkle trie. StateDBs take care of caching and storing // nested states. It's the general query interface to retrieve: @@ -71,6 +186,11 @@ type StateDB struct { snaps *snapshot.Tree // Nil if snapshot is not available snap snapshot.Snapshot // Nil if snapshot is not available + snapParallelLock sync.RWMutex // for parallel mode, for main StateDB, slot will read snapshot, while processor will write. + trieParallelLock sync.Mutex // for parallel mode of trie, mostly for get states/objects from trie, lock required to handle trie tracer. + stateObjectDestructLock sync.RWMutex // for parallel mode, used in mainDB for mergeSlot and conflict check. + snapDestructs map[common.Address]struct{} + // originalRoot is the pre-state root, before any changes were made. // It will be updated when the Commit is called. originalRoot common.Hash @@ -90,10 +210,11 @@ type StateDB struct { // This map holds 'live' objects, which will get modified while processing // a state transition. - stateObjects map[common.Address]*stateObject - stateObjectsPending map[common.Address]struct{} // State objects finalized but not yet written to the trie - stateObjectsDirty map[common.Address]struct{} // State objects modified in the current execution - stateObjectsDestruct map[common.Address]*types.StateAccount // State objects destructed in the block along with its previous value + stateObjects map[common.Address]*stateObject + stateObjectsPending map[common.Address]struct{} // State objects finalized but not yet written to the trie + stateObjectsDirty map[common.Address]struct{} // State objects modified in the current execution + stateObjectsDestruct map[common.Address]*types.StateAccount // State objects destructed in the block along with its previous value + stateObjectsDestructDirty map[common.Address]*types.StateAccount // DB error. // State objects are used by the consensus core and VM which are @@ -113,6 +234,11 @@ type StateDB struct { logs map[common.Hash][]*types.Log logSize uint + // parallel EVM related + rwSet *types.RWSet + mvStates *types.MVStates + stat *types.ExeStat + // Preimages occurred seen by VM in the scope of block. preimages map[common.Hash][]byte @@ -143,41 +269,56 @@ type StateDB struct { TrieDBCommits time.Duration TrieCommits time.Duration CodeCommits time.Duration + TxDAGGenerate time.Duration AccountUpdated int StorageUpdated int AccountDeleted int StorageDeleted int + isParallel bool + parallel ParallelState // to keep all the parallel execution elements // Testing hooks onCommit func(states *triestate.Set) // Hook invoked when commit is performed } +func (s *StateDB) GetStateObjectFromUnconfirmedDB(addr common.Address) (*stateObject, bool) { + return nil, false +} + // New creates a new state from a given trie. func New(root common.Hash, db Database, snaps *snapshot.Tree) (*StateDB, error) { tr, err := db.OpenTrie(root) + if err != nil { return nil, err } sdb := &StateDB{ - db: db, - trie: tr, - originalRoot: root, - snaps: snaps, - accounts: make(map[common.Hash][]byte), - storages: make(map[common.Hash]map[common.Hash][]byte), - accountsOrigin: make(map[common.Address][]byte), - storagesOrigin: make(map[common.Address]map[common.Hash][]byte), - stateObjects: make(map[common.Address]*stateObject), - stateObjectsPending: make(map[common.Address]struct{}), - stateObjectsDirty: make(map[common.Address]struct{}), - stateObjectsDestruct: make(map[common.Address]*types.StateAccount), - logs: make(map[common.Hash][]*types.Log), - preimages: make(map[common.Hash][]byte), - journal: newJournal(), - accessList: newAccessList(), - transientStorage: newTransientStorage(), - hasher: crypto.NewKeccakState(), + db: db, + trie: tr, + originalRoot: root, + snaps: snaps, + snapDestructs: make(map[common.Address]struct{}), + accounts: make(map[common.Hash][]byte), + storages: make(map[common.Hash]map[common.Hash][]byte), + accountsOrigin: make(map[common.Address][]byte), + storagesOrigin: make(map[common.Address]map[common.Hash][]byte), + stateObjects: make(map[common.Address]*stateObject), + stateObjectsPending: make(map[common.Address]struct{}), + stateObjectsDirty: make(map[common.Address]struct{}), + stateObjectsDestruct: make(map[common.Address]*types.StateAccount), + stateObjectsDestructDirty: make(map[common.Address]*types.StateAccount, defaultNumOfSlots), + logs: make(map[common.Hash][]*types.Log), + preimages: make(map[common.Hash][]byte), + journal: newJournal(), + accessList: newAccessList(), + transientStorage: newTransientStorage(), + hasher: crypto.NewKeccakState(), + + parallel: ParallelState{ + SlotIndex: -1, + }, + txIndex: -1, } if sdb.snaps != nil { sdb.snap = sdb.snaps.Snapshot(root) @@ -193,6 +334,7 @@ func NewStateDBByTrie(tr Trie, db Database, snaps *snapshot.Tree) (*StateDB, err trie: tr, originalRoot: tr.Hash(), snaps: snaps, + snapDestructs: make(map[common.Address]struct{}), accounts: make(map[common.Hash][]byte), storages: make(map[common.Hash]map[common.Hash][]byte), accountsOrigin: make(map[common.Address][]byte), @@ -215,6 +357,18 @@ func NewStateDBByTrie(tr Trie, db Database, snaps *snapshot.Tree) (*StateDB, err return sdb, nil } +func (s *StateDB) IsParallel() bool { + return s.isParallel +} + +func (s *StateDB) getBaseStateDB() *StateDB { + return s +} + +func (s *StateDB) getStateObjectFromStateObjects(addr common.Address) (*stateObject, bool) { + return s.loadStateObj(addr) +} + // StartPrefetcher initializes a new trie prefetcher to pull in nodes from the // state trie concurrently while the state is mutated so that when we reach the // commit phase, most of the needed data is already hot. @@ -264,7 +418,6 @@ func (s *StateDB) Error() error { func (s *StateDB) AddLog(log *types.Log) { s.journal.append(addLogChange{txhash: s.thash}) - log.TxHash = s.thash log.TxIndex = uint(s.txIndex) log.Index = s.logSize @@ -336,7 +489,10 @@ func (s *StateDB) Empty(addr common.Address) bool { } // GetBalance retrieves the balance from the given address or 0 if object not found -func (s *StateDB) GetBalance(addr common.Address) *uint256.Int { +func (s *StateDB) GetBalance(addr common.Address) (ret *uint256.Int) { + defer func() { + s.RecordRead(types.AccountStateKey(addr, types.AccountBalance), ret) + }() stateObject := s.getStateObject(addr) if stateObject != nil { return stateObject.Balance() @@ -345,12 +501,14 @@ func (s *StateDB) GetBalance(addr common.Address) *uint256.Int { } // GetNonce retrieves the nonce from the given address or 0 if object not found -func (s *StateDB) GetNonce(addr common.Address) uint64 { +func (s *StateDB) GetNonce(addr common.Address) (ret uint64) { + defer func() { + s.RecordRead(types.AccountStateKey(addr, types.AccountNonce), ret) + }() stateObject := s.getStateObject(addr) if stateObject != nil { return stateObject.Nonce() } - return 0 } @@ -369,7 +527,15 @@ func (s *StateDB) TxIndex() int { return s.txIndex } +// BaseTxIndex returns the tx index that slot db based. +func (s *StateDB) BaseTxIndex() int { + return s.parallel.baseTxIndex +} + func (s *StateDB) GetCode(addr common.Address) []byte { + defer func() { + s.RecordRead(types.AccountStateKey(addr, types.AccountCodeHash), s.GetCodeHash(addr)) + }() stateObject := s.getStateObject(addr) if stateObject != nil { return stateObject.Code() @@ -378,6 +544,9 @@ func (s *StateDB) GetCode(addr common.Address) []byte { } func (s *StateDB) GetCodeSize(addr common.Address) int { + defer func() { + s.RecordRead(types.AccountStateKey(addr, types.AccountCodeHash), s.GetCodeHash(addr)) + }() stateObject := s.getStateObject(addr) if stateObject != nil { return stateObject.CodeSize() @@ -385,7 +554,14 @@ func (s *StateDB) GetCodeSize(addr common.Address) int { return 0 } -func (s *StateDB) GetCodeHash(addr common.Address) common.Hash { +// GetCodeHash return: +// - common.Hash{}: the address does not exist +// - emptyCodeHash: the address exist, but code is empty +// - others: the address exist, and code is not empty +func (s *StateDB) GetCodeHash(addr common.Address) (ret common.Hash) { + defer func() { + s.RecordRead(types.AccountStateKey(addr, types.AccountCodeHash), ret.Bytes()) + }() stateObject := s.getStateObject(addr) if stateObject != nil { return common.BytesToHash(stateObject.CodeHash()) @@ -394,7 +570,10 @@ func (s *StateDB) GetCodeHash(addr common.Address) common.Hash { } // GetState retrieves a value from the given account's storage trie. -func (s *StateDB) GetState(addr common.Address, hash common.Hash) common.Hash { +func (s *StateDB) GetState(addr common.Address, hash common.Hash) (ret common.Hash) { + defer func() { + s.RecordRead(types.StorageStateKey(addr, hash), ret) + }() stateObject := s.getStateObject(addr) if stateObject != nil { return stateObject.GetState(hash) @@ -403,7 +582,10 @@ func (s *StateDB) GetState(addr common.Address, hash common.Hash) common.Hash { } // GetCommittedState retrieves a value from the given account's committed storage trie. -func (s *StateDB) GetCommittedState(addr common.Address, hash common.Hash) common.Hash { +func (s *StateDB) GetCommittedState(addr common.Address, hash common.Hash) (ret common.Hash) { + defer func() { + s.RecordRead(types.StorageStateKey(addr, hash), ret) + }() stateObject := s.getStateObject(addr) if stateObject != nil { return stateObject.GetCommittedState(hash) @@ -432,16 +614,22 @@ func (s *StateDB) HasSelfDestructed(addr common.Address) bool { func (s *StateDB) AddBalance(addr common.Address, amount *uint256.Int) { stateObject := s.getOrNewStateObject(addr) if stateObject != nil { + s.RecordRead(types.AccountStateKey(addr, types.AccountBalance), stateObject.Balance()) stateObject.AddBalance(amount) + return } + s.RecordRead(types.AccountStateKey(addr, types.AccountBalance), common.U2560) } // SubBalance subtracts amount from the account associated with addr. func (s *StateDB) SubBalance(addr common.Address, amount *uint256.Int) { stateObject := s.getOrNewStateObject(addr) if stateObject != nil { + s.RecordRead(types.AccountStateKey(addr, types.AccountBalance), stateObject.Balance()) stateObject.SubBalance(amount) + return } + s.RecordRead(types.AccountStateKey(addr, types.AccountBalance), common.U2560) } func (s *StateDB) SetBalance(addr common.Address, amount *uint256.Int) { @@ -466,6 +654,7 @@ func (s *StateDB) SetCode(addr common.Address, code []byte) { } func (s *StateDB) SetState(addr common.Address, key, value common.Hash) { + stateObject := s.getOrNewStateObject(addr) if stateObject != nil { stateObject.SetState(key, value) @@ -484,8 +673,8 @@ func (s *StateDB) SetStorage(addr common.Address, storage map[common.Hash]common // // TODO(rjl493456442) this function should only be supported by 'unwritable' // state and all mutations made should all be discarded afterwards. - if _, ok := s.stateObjectsDestruct[addr]; !ok { - s.stateObjectsDestruct[addr] = nil + if _, ok := s.getStateObjectsDestruct(addr); !ok { + s.setStateObjectsDestruct(addr, nil) } stateObject := s.getOrNewStateObject(addr) for k, v := range storage { @@ -509,7 +698,7 @@ func (s *StateDB) SelfDestruct(addr common.Address) { prevbalance: new(uint256.Int).Set(stateObject.Balance()), }) stateObject.markSelfdestructed() - stateObject.data.Balance = new(uint256.Int) + stateObject.setBalance(new(uint256.Int)) } func (s *StateDB) Selfdestruct6780(addr common.Address) { @@ -517,7 +706,6 @@ func (s *StateDB) Selfdestruct6780(addr common.Address) { if stateObject == nil { return } - if stateObject.created { s.SelfDestruct(addr) } @@ -556,6 +744,9 @@ func (s *StateDB) GetTransientState(addr common.Address, key common.Hash) common // updateStateObject writes the given object to the trie. func (s *StateDB) updateStateObject(obj *stateObject) { + log.Debug("updateStateObject", "addr", obj.address, "data", obj.data, + "object.dirtyNonce", obj.dirtyNonce, + "object.dirtyBalance", obj.dirtyBalance) if !s.noTrie { // Track the amount of time wasted on updating the account from the trie if metrics.EnabledExpensive { @@ -563,14 +754,18 @@ func (s *StateDB) updateStateObject(obj *stateObject) { } // Encode the account and update the account trie addr := obj.Address() + s.trieParallelLock.Lock() if err := s.trie.UpdateAccount(addr, &obj.data); err != nil { s.setError(fmt.Errorf("updateStateObject (%x) error: %v", addr[:], err)) } if obj.dirtyCode { s.trie.UpdateContractCode(obj.Address(), common.BytesToHash(obj.CodeHash()), obj.code) } + s.trieParallelLock.Unlock() } + s.AccountMux.Lock() + defer s.AccountMux.Unlock() // Cache the data until commit. Note, this update mechanism is not symmetric // to the deletion, because whereas it is enough to track account updates // at commit time, deletions need tracking at transaction boundary level to @@ -591,6 +786,7 @@ func (s *StateDB) updateStateObject(obj *stateObject) { // deleteStateObject removes the given object from the state trie. func (s *StateDB) deleteStateObject(obj *stateObject) { + log.Debug("deleteStateObject", "addr", obj.address, "data", obj.data) if s.noTrie { return } @@ -616,17 +812,73 @@ func (s *StateDB) getStateObject(addr common.Address) *stateObject { return nil } -// getDeletedStateObject is similar to getStateObject, but instead of returning -// nil for a deleted state object, it returns the actual object with the deleted -// flag set. This is needed by the state journal to revert to the correct s- -// destructed object instead of wiping all knowledge about the state object. -func (s *StateDB) getDeletedStateObject(addr common.Address) *stateObject { +// GetStateNoUpdate retrieves a value from the given account's storage trie, but do not update the db.stateObjects cache. +func (s *StateDB) GetStateNoUpdate(addr common.Address, hash common.Hash) (ret common.Hash) { + object := s.getStateObjectNoUpdate(addr) + if object != nil { + return object.GetStateNoUpdate(hash) + } + return common.Hash{} +} + +// getStateObjectNoUpdate is similar with getStateObject except that it does not +// update stateObjects records. +func (s *StateDB) getStateObjectNoUpdate(addr common.Address) *stateObject { + obj := s.getDeletedStateObjectNoUpdate(addr) + if obj != nil && !obj.deleted { + return obj + } + return nil +} + +func (s *StateDB) getDeletedStateObjectNoUpdate(addr common.Address) *stateObject { // Prefer live objects if any is available - if obj := s.stateObjects[addr]; obj != nil { + if obj, _ := s.getStateObjectFromStateObjects(addr); obj != nil { return obj } + + data, ok := s.getStateObjectFromSnapshotOrTrie(addr) + if !ok { + return nil + } + obj := newObject(s, s.isParallel, addr, data) + return obj +} + +func (s *StateDB) GetStateObjectFromSnapshotOrTrie(addr common.Address) (data *types.StateAccount, ok bool) { + return s.getStateObjectFromSnapshotOrTrie(addr) +} + +func (s *StateDB) SnapHasAccount(addr common.Address) (exist bool) { + if s.snap == nil { + return false + } + acc, _ := s.snap.Account(crypto.HashData(s.hasher, addr.Bytes())) + return acc != nil +} + +func (s *StateDB) TriHasAccount(addr common.Address) (exist bool) { + if s.trie == nil { + return false + } + + acc, _ := s.trie.GetAccount(addr) + return acc != nil +} + +func (s *StateDB) GetTrie() Trie { + if s.trie == nil { + return nil + } + return s.trie +} + +func (s *StateDB) SetTrie(trie Trie) { + s.trie = trie +} + +func (s *StateDB) getStateObjectFromSnapshotOrTrie(addr common.Address) (data *types.StateAccount, ok bool) { // 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())) @@ -635,8 +887,9 @@ func (s *StateDB) getDeletedStateObject(addr common.Address) *stateObject { } if err == nil { if acc == nil { - return nil + return nil, false } + data = &types.StateAccount{ Nonce: acc.Nonce, Balance: acc.Balance, @@ -651,30 +904,84 @@ func (s *StateDB) getDeletedStateObject(addr common.Address) *stateObject { } } } + // If snapshot unavailable or reading from it failed, load from the database if data == nil { + s.trieParallelLock.Lock() + defer s.trieParallelLock.Unlock() + var trie Trie + if s.isParallel { + // hold lock for parallel + if s.parallel.isSlotDB { + if s.parallel.baseStateDB == nil { + return nil, false + } else { + tr, err := s.parallel.baseStateDB.db.OpenTrie(s.originalRoot) + if err != nil { + log.Error("Can not openTrie for parallel SlotDB\n") + return nil, false + } + trie = tr + } + } else { + trie = s.trie + } + } else { + trie = s.trie + } + start := time.Now() var err error - data, err = s.trie.GetAccount(addr) + data, err = trie.GetAccount(addr) if metrics.EnabledExpensive { s.AccountReads += time.Since(start) } if err != nil { s.setError(fmt.Errorf("getDeleteStateObject (%x) error: %w", addr.Bytes(), err)) - return nil + return nil, false } if data == nil { - return nil + return nil, false } } + + return data, true +} + +// getDeletedStateObject is similar to getStateObject, but instead of returning +// nil for a deleted state object, it returns the actual object with the deleted +// flag set. This is needed by the state journal to revert to the correct s- +// destructed object instead of wiping all knowledge about the state object. +func (s *StateDB) getDeletedStateObject(addr common.Address) *stateObject { + s.RecordRead(types.AccountStateKey(addr, types.AccountSelf), struct{}{}) + + // Prefer live objects if any is available + if obj, _ := s.getStateObjectFromStateObjects(addr); obj != nil { + return obj + } + + data, ok := s.getStateObjectFromSnapshotOrTrie(addr) + if !ok { + return nil + } // Insert into the live set - obj := newObject(s, addr, data) + obj := newObject(s, s.isParallel, addr, data) s.setStateObject(obj) return obj } func (s *StateDB) setStateObject(object *stateObject) { - s.stateObjects[object.Address()] = object + if s.isParallel { + if s.parallel.isSlotDB { + s.parallel.locatStateObjects[object.address] = object + } else { + // When a state object is stored into s.parallel.stateObjects, + // it belongs to base StateDB, it is confirmed and valid. + s.parallel.stateObjects.Store(object.address, object) + } + } else { + s.stateObjects[object.Address()] = object + } } // getOrNewStateObject retrieves a state object or create a new state object if nil. @@ -688,9 +995,22 @@ func (s *StateDB) getOrNewStateObject(addr common.Address) *stateObject { // createObject creates a new state object. If there is an existing account with // the given address, it is overwritten and returned as the second return value. -func (s *StateDB) createObject(addr common.Address) (newobj, prev *stateObject) { +// prev is used for CreateAccount to get its balance +// Parallel mode: +// if prev in dirty: revert is ok +// if prev in unconfirmed DB: addr state read record, revert should not put it back +// if prev in main DB: addr state read record, revert should not put it back +// if pre no exist: addr state read record, + +// `prev` is used to handle revert, to recover with the `prev` object +// In Parallel mode, we only need to recover to `prev` in SlotDB, +// +// a.if it is not in SlotDB, `revert` will remove it from the SlotDB +// b.if it is existed in SlotDB, `revert` will recover to the `prev` in SlotDB +// c.as `snapDestructs` it is the same +func (s *StateDB) createObject(addr common.Address) (newobj *stateObject, prev *stateObject) { prev = s.getDeletedStateObject(addr) // Note, prev might have been deleted, we need that! - newobj = newObject(s, addr, nil) + newobj = newObject(s, s.isParallel, addr, nil) if prev == nil { s.journal.append(createObjectChange{account: &addr}) } else { @@ -698,10 +1018,12 @@ func (s *StateDB) createObject(addr common.Address) (newobj, prev *stateObject) // account and storage data should be cleared as well. Note, it must // be done here, otherwise the destruction event of "original account" // will be lost. - _, prevdestruct := s.stateObjectsDestruct[prev.address] + s.stateObjectDestructLock.Lock() + _, prevdestruct := s.getStateObjectsDestruct(prev.address) if !prevdestruct { - s.stateObjectsDestruct[prev.address] = prev.origin + s.setStateObjectsDestruct(prev.address, prev.origin) } + s.stateObjectDestructLock.Unlock() // There may be some cached account/storage data already since IntermediateRoot // will be called for each transaction before byzantium fork which will always // cache the latest account/storage data. @@ -716,11 +1038,17 @@ func (s *StateDB) createObject(addr common.Address) (newobj, prev *stateObject) prevAccountOrigin: prevAccount, prevStorageOrigin: s.storagesOrigin[prev.address], }) + s.AccountMux.Lock() delete(s.accounts, prev.addrHash) - delete(s.storages, prev.addrHash) delete(s.accountsOrigin, prev.address) + s.AccountMux.Unlock() + s.StorageMux.Lock() + delete(s.storages, prev.addrHash) delete(s.storagesOrigin, prev.address) + s.StorageMux.Unlock() } + + newobj.created = true s.setStateObject(newobj) if prev != nil && !prev.deleted { return newobj, prev @@ -739,34 +1067,60 @@ func (s *StateDB) createObject(addr common.Address) (newobj, prev *stateObject) // // Carrying over the balance ensures that Ether doesn't disappear. func (s *StateDB) CreateAccount(addr common.Address) { + // no matter it is got from dirty, unconfirmed or main DB + // if addr not exist, preBalance will be common.U2560, it is same as new(big.Int) which + // is the value newObject(), newObj, prev := s.createObject(addr) + if addr.Hex() == "0x13f4EA83D0bd40E75C8222255bc855a974568Dd4" { + log.Debug("CreateAccount - setBalance", "addr", addr.Hex(), "prev", prev) + } if prev != nil { - newObj.setBalance(prev.data.Balance) + newObj.setBalance(prev.Balance()) } } +// CopyWithMvStates will copy state with MVStates +func (s *StateDB) CopyWithMvStates(doPrefetch bool) *StateDB { + state := s.copyInternal(doPrefetch) + if s.mvStates != nil { + state.mvStates = s.mvStates + } + return state +} + // Copy creates a deep, independent copy of the state. // Snapshots of the copied state cannot be applied to the copy. func (s *StateDB) Copy() *StateDB { + return s.copyInternal(false) +} + +// CopyDoPrefetch It is mainly for state prefetcher to do trie prefetch right now. +func (s *StateDB) CopyDoPrefetch() *StateDB { + return s.copyInternal(true) +} + +func (s *StateDB) copyInternal(doPrefetch bool) *StateDB { // Copy all the basic fields, initialize the memory ones state := &StateDB{ - db: s.db, - trie: s.db.CopyTrie(s.trie), - originalRoot: s.originalRoot, - accounts: make(map[common.Hash][]byte), - storages: make(map[common.Hash]map[common.Hash][]byte), - accountsOrigin: make(map[common.Address][]byte), - storagesOrigin: make(map[common.Address]map[common.Hash][]byte), - 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)), - stateObjectsDestruct: make(map[common.Address]*types.StateAccount, len(s.stateObjectsDestruct)), - refund: s.refund, - logs: make(map[common.Hash][]*types.Log, len(s.logs)), - logSize: s.logSize, - preimages: make(map[common.Hash][]byte, len(s.preimages)), - journal: newJournal(), - hasher: crypto.NewKeccakState(), + db: s.db, + trie: s.db.CopyTrie(s.trie), + originalRoot: s.originalRoot, + snapDestructs: make(map[common.Address]struct{}), + accounts: make(map[common.Hash][]byte), + storages: make(map[common.Hash]map[common.Hash][]byte), + accountsOrigin: make(map[common.Address][]byte), + storagesOrigin: make(map[common.Address]map[common.Hash][]byte), + 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)), + stateObjectsDestruct: make(map[common.Address]*types.StateAccount, len(s.stateObjectsDestruct)), + stateObjectsDestructDirty: make(map[common.Address]*types.StateAccount, len(s.stateObjectsDestructDirty)), + refund: s.refund, + logs: make(map[common.Hash][]*types.Log, len(s.logs)), + logSize: s.logSize, + preimages: make(map[common.Hash][]byte, len(s.preimages)), + journal: newJournal(), + hasher: crypto.NewKeccakState(), // In order for the block producer to be able to use and make additions // to the snapshot tree, we need to copy that as well. Otherwise, any @@ -774,6 +1128,8 @@ func (s *StateDB) Copy() *StateDB { // miner to operate trie-backed only. snaps: s.snaps, snap: s.snap, + + parallel: ParallelState{}, } // Copy the dirty states, logs, and preimages for addr := range s.journal.dirties { @@ -781,11 +1137,11 @@ func (s *StateDB) Copy() *StateDB { // and in the Finalise-method, there is a case where an object is in the journal but not // in the stateObjects: OOG after touch on ripeMD prior to Byzantium. Thus, we need to check for // nil - if object, exist := s.stateObjects[addr]; exist { + if object, exist := s.getStateObjectFromStateObjects(addr); exist { // Even though the original object is dirty, we are not copying the journal, // so we need to make sure that any side-effect the journal would have caused // during a commit (or similar op) is already applied to the copy. - state.stateObjects[addr] = object.deepCopy(state) + state.storeStateObj(addr, object.deepCopy(state)) state.stateObjectsDirty[addr] = struct{}{} // Mark the copy dirty to force internal (code/state) commits state.stateObjectsPending[addr] = struct{}{} // Mark the copy pending to force external (account) commits @@ -796,14 +1152,16 @@ func (s *StateDB) Copy() *StateDB { // is empty. Thus, here we iterate over stateObjects, to enable copies // of copies. for addr := range s.stateObjectsPending { - if _, exist := state.stateObjects[addr]; !exist { - state.stateObjects[addr] = s.stateObjects[addr].deepCopy(state) + if _, exist := state.getStateObjectFromStateObjects(addr); !exist { + object, _ := s.getStateObjectFromStateObjects(addr) + state.storeStateObj(addr, object.deepCopy(state)) } state.stateObjectsPending[addr] = struct{}{} } for addr := range s.stateObjectsDirty { - if _, exist := state.stateObjects[addr]; !exist { - state.stateObjects[addr] = s.stateObjects[addr].deepCopy(state) + if _, exist := state.getStateObjectFromStateObjects(addr); !exist { + object, _ := s.getStateObjectFromStateObjects(addr) + state.storeStateObj(addr, object.deepCopy(state)) } state.stateObjectsDirty[addr] = struct{}{} } @@ -811,12 +1169,20 @@ func (s *StateDB) Copy() *StateDB { for addr, value := range s.stateObjectsDestruct { state.stateObjectsDestruct[addr] = value } + for addr, value := range s.stateObjectsDestructDirty { + state.stateObjectsDestructDirty[addr] = value + } // Deep copy the state changes made in the scope of block // along with their original values. + s.AccountMux.Lock() state.accounts = copySet(s.accounts) - state.storages = copy2DSet(s.storages) state.accountsOrigin = copySet(state.accountsOrigin) + s.AccountMux.Unlock() + + s.StorageMux.Lock() + state.storages = copy2DSet(s.storages) state.storagesOrigin = copy2DSet(state.storagesOrigin) + s.StorageMux.Unlock() // Deep copy the logs occurred in the scope of block for hash, logs := range s.logs { @@ -846,6 +1212,147 @@ func (s *StateDB) Copy() *StateDB { if s.prefetcher != nil { state.prefetcher = s.prefetcher.copy() } + + return state +} + +var journalPool = sync.Pool{ + New: func() interface{} { + return &journal{ + dirties: make(map[common.Address]int, defaultNumOfSlots), + entries: make([]journalEntry, 0, defaultNumOfSlots), + } + }, +} + +var addressToStructPool = sync.Pool{ + New: func() interface{} { return make(map[common.Address]struct{}, defaultNumOfSlots) }, +} + +var addressToStateKeysPool = sync.Pool{ + New: func() interface{} { return make(map[common.Address]StateKeys, defaultNumOfSlots) }, +} + +var addressToStoragePool = sync.Pool{ + New: func() interface{} { return make(map[common.Address]Storage, defaultNumOfSlots) }, +} + +var addressToStateObjectsPool = sync.Pool{ + New: func() interface{} { return make(map[common.Address]*stateObject, defaultNumOfSlots) }, +} + +var balancePool = sync.Pool{ + New: func() interface{} { return make(map[common.Address]*uint256.Int, defaultNumOfSlots) }, +} + +var addressToHashPool = sync.Pool{ + New: func() interface{} { return make(map[common.Address]common.Hash, defaultNumOfSlots) }, +} + +var addressToBytesPool = sync.Pool{ + New: func() interface{} { return make(map[common.Address][]byte, defaultNumOfSlots) }, +} + +var addressToBoolPool = sync.Pool{ + New: func() interface{} { return make(map[common.Address]bool, defaultNumOfSlots) }, +} + +var addressToUintPool = sync.Pool{ + New: func() interface{} { return make(map[common.Address]uint64, defaultNumOfSlots) }, +} + +var logsPool = sync.Pool{ + New: func() interface{} { return make(map[common.Hash][]*types.Log, defaultNumOfSlots) }, +} + +func NewEmptySlotDB() *ParallelStateDB { + parallel := ParallelState{ + // The stateObjects in Parallel is thread-local. + // The base stateDB's stateObjects is thread-unsafe as it is not guarded by lock. + // The base stateDB's parallel.stateObjects is SyncMap and thread-safe. and no extra lock needed (TODO-dav). + // The base stateDB's parallel.stateObjects are updated by mergeSlotDB with Lock. + // The base stateDB's stateObject is read-only and never be updated once parallel execution happens. + // AND, presumably, the stateDB's stateObject is usually empty for real on-chain cases. + // Before execution, the slotDB should copy objects from base stateDB's parallel.stateObjects and stateObjects + // NOTICE: + // We are not reusing the base slot db's stateObjects although copy can be avoid. Because multiple thread + // access has lock check and there might be tricky bug such as thread1 handle tx0 at the same time with thread2 + // handle tx1, so what thread1's slotDB see in the s.parallel.stateObjects might be the middle result of Thread2. + // + // We are not do simple copy (lightweight pointer copy) as the stateObject can be accessed by different thread. + + stateObjects: nil, /* The parallel execution will not use this field, except the base DB */ + locatStateObjects: addressToStateObjectsPool.Get().(map[common.Address]*stateObject), + codeReadsInSlot: addressToBytesPool.Get().(map[common.Address][]byte), + codeHashReadsInSlot: addressToHashPool.Get().(map[common.Address]common.Hash), + codeChangesInSlot: addressToStructPool.Get().(map[common.Address]struct{}), + kvChangesInSlot: addressToStateKeysPool.Get().(map[common.Address]StateKeys), + kvReadsInSlot: addressToStoragePool.Get().(map[common.Address]Storage), + balanceChangesInSlot: addressToStructPool.Get().(map[common.Address]struct{}), + balanceReadsInSlot: balancePool.Get().(map[common.Address]*uint256.Int), + addrStateReadsInSlot: addressToBoolPool.Get().(map[common.Address]bool), + addrStateChangesInSlot: addressToBoolPool.Get().(map[common.Address]bool), + nonceChangesInSlot: addressToStructPool.Get().(map[common.Address]struct{}), + nonceReadsInSlot: addressToUintPool.Get().(map[common.Address]uint64), + addrSnapDestructsReadsInSlot: addressToBoolPool.Get().(map[common.Address]bool), + isSlotDB: true, + dirtiedStateObjectsInSlot: addressToStateObjectsPool.Get().(map[common.Address]*stateObject), + createdObjectRecord: addressToStructPool.Get().(map[common.Address]struct{}), + } + state := &ParallelStateDB{ + StateDB: StateDB{ + db: nil, + trie: nil, // Parallel StateDB may access the trie, but it takes no effect to the baseDB. + accounts: make(map[common.Hash][]byte), + storages: make(map[common.Hash]map[common.Hash][]byte), + accountsOrigin: make(map[common.Address][]byte), + storagesOrigin: make(map[common.Address]map[common.Hash][]byte), + stateObjects: make(map[common.Address]*stateObject), // replaced by parallel.stateObjects in parallel mode + stateObjectsPending: addressToStructPool.Get().(map[common.Address]struct{}), + stateObjectsDirty: addressToStructPool.Get().(map[common.Address]struct{}), + stateObjectsDestruct: make(map[common.Address]*types.StateAccount), + refund: 0, // should be 0 + logs: logsPool.Get().(map[common.Hash][]*types.Log), + logSize: 0, + preimages: nil, + journal: journalPool.Get().(*journal), + hasher: crypto.NewKeccakState(), + isParallel: true, + parallel: parallel, + }, + } + state.snapDestructs = addressToStructPool.Get().(map[common.Address]struct{}) + return state +} + +// CopyForSlot copy all the basic fields, initialize the memory ones +func (s *StateDB) CopyForSlot(parallelDBManager *ParallelDBManager) *ParallelStateDB { + state := parallelDBManager.allocate() + state.db = s.db + s.preimages = make(map[common.Hash][]byte, len(s.preimages)) + + s.snapParallelLock.RLock() + for k, v := range s.snapDestructs { + state.snapDestructs[k] = v + } + s.snapParallelLock.RUnlock() + + if s.snaps != nil { + state.snaps = s.snaps + state.snap = s.snap + } + + // Deep copy the state changes made in the scope of block + // along with their original values. + s.AccountMux.Lock() + state.accounts = copySet(s.accounts) + state.accountsOrigin = copySet(state.accountsOrigin) + s.AccountMux.Unlock() + s.StorageMux.Lock() + state.storages = copy2DSet(s.storages) + state.storagesOrigin = copy2DSet(state.storagesOrigin) + s.StorageMux.Unlock() + return state } @@ -883,8 +1390,27 @@ func (s *StateDB) GetRefund() uint64 { // into the tries just yet. Only IntermediateRoot or Commit will do that. func (s *StateDB) Finalise(deleteEmptyObjects bool) { addressesToPrefetch := make([][]byte, 0, len(s.journal.dirties)) + + // finalise stateObjectsDestruct + // The finalise of stateDB is called at verify & commit phase, which is global, no need to acquire the lock. + for addr, acc := range s.stateObjectsDestructDirty { + s.stateObjectsDestruct[addr] = acc + } + s.stateObjectsDestructDirty = make(map[common.Address]*types.StateAccount) for addr := range s.journal.dirties { - obj, exist := s.stateObjects[addr] + var obj *stateObject + var exist bool + if s.parallel.isSlotDB { + obj = s.parallel.dirtiedStateObjectsInSlot[addr] + if obj != nil { + exist = true + } else { + log.Error("StateDB Finalise dirty addr not in dirtiedStateObjectsInSlot", + "addr", addr) + } + } else { + obj, exist = s.getStateObjectFromStateObjects(addr) + } if !exist { // ripeMD is 'touched' at block 1714175, in tx 0x1237f737031e40bcde4a8b7e717b2d15e3ecadfe49bb1bbc71ee9deb09c6fcf2 // That tx goes out of gas, and although the notion of 'touched' does not exist there, the @@ -911,8 +1437,14 @@ func (s *StateDB) Finalise(deleteEmptyObjects bool) { delete(s.accountsOrigin, obj.address) // Clear out any previously updated account data (may be recreated via a resurrect) delete(s.storagesOrigin, obj.address) // Clear out any previously updated storage data (may be recreated via a resurrect) } else { - obj.finalise(true) // Prefetch slots in the background + // 1.none parallel mode, we do obj.finalise(true) as normal + // 2.with parallel mode, we do obj.finalise(true) on dispatcher, not on slot routine + // obj.finalise(true) will clear its dirtyStorage, will make prefetch broken. + if !s.isParallel || !s.parallel.isSlotDB { + obj.finalise(true) // Prefetch slots in the background + } } + obj.created = false s.stateObjectsPending[addr] = struct{}{} s.stateObjectsDirty[addr] = struct{}{} @@ -932,8 +1464,10 @@ func (s *StateDB) Finalise(deleteEmptyObjects bool) { // IntermediateRoot computes the current root hash of the state trie. // It is called in between transactions to get the root hash that // goes into transaction receipts. +// TODO: For parallel SlotDB, IntermediateRootForSlot is used, need to clean up this method. func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash { // Finalise all the dirty storage states and write them into the tries + log.Debug("IntermediateRoot", "deleteEmptyObjects", deleteEmptyObjects) s.Finalise(deleteEmptyObjects) s.AccountsIntermediateRoot() return s.StateIntermediateRoot() @@ -963,19 +1497,37 @@ func (s *StateDB) AccountsIntermediateRoot() { // first, giving the account prefetches just a few more milliseconds of time // to pull useful data from disk. for addr := range s.stateObjectsPending { - if obj := s.stateObjects[addr]; !obj.deleted { - wg.Add(1) - tasks <- func() { - defer wg.Done() - obj.updateRoot() - - // Cache the data until commit. Note, this update mechanism is not symmetric - // to the deletion, because whereas it is enough to track account updates - // at commit time, deletions need tracking at transaction boundary level to - // ensure we capture state clearing. - s.AccountMux.Lock() - s.accounts[obj.addrHash] = types.SlimAccountRLP(obj.data) - s.AccountMux.Unlock() + if s.parallel.isSlotDB { + if obj := s.parallel.dirtiedStateObjectsInSlot[addr]; !obj.deleted { + wg.Add(1) + tasks <- func() { + defer wg.Done() + obj.updateRoot() + + // Cache the data until commit. Note, this update mechanism is not symmetric + // to the deletion, because whereas it is enough to track account updates + // at commit time, deletions need tracking at transaction boundary level to + // ensure we capture state clearing. + s.AccountMux.Lock() + s.accounts[obj.addrHash] = types.SlimAccountRLP(obj.data) + s.AccountMux.Unlock() + } + } + } else { + if obj, _ := s.getStateObjectFromStateObjects(addr); !obj.deleted { + wg.Add(1) + tasks <- func() { + defer wg.Done() + obj.updateRoot() + + // Cache the data until commit. Note, this update mechanism is not symmetric + // to the deletion, because whereas it is enough to track account updates + // at commit time, deletions need tracking at transaction boundary level to + // ensure we capture state clearing. + s.AccountMux.Lock() + s.accounts[obj.addrHash] = types.SlimAccountRLP(obj.data) + s.AccountMux.Unlock() + } } } } @@ -991,18 +1543,22 @@ func (s *StateDB) StateIntermediateRoot() common.Hash { // the remainder without, but pre-byzantium even the initial prefetcher is // useless, so no sleep lost. prefetcher := s.prefetcher + r := s.originalRoot if s.prefetcher != nil { defer func() { s.prefetcher.close() s.prefetcher = nil }() + if s.isParallel { + r = s.trie.Hash() + } } - // Now we're about to start to write changes to the trie. The trie is so far // _untouched_. We can check with the prefetcher, if it can give us a trie // which has the same root, but also has some content loaded into it. + // The parallel execution do the change incrementally, so can not check the prefetcher here if prefetcher != nil { - if trie := prefetcher.trie(common.Hash{}, s.originalRoot); trie != nil { + if trie := prefetcher.trie(common.Hash{}, r); trie != nil { s.trie = trie } } @@ -1015,8 +1571,17 @@ func (s *StateDB) StateIntermediateRoot() common.Hash { } usedAddrs := make([][]byte, 0, len(s.stateObjectsPending)) + for addr := range s.stateObjectsPending { - if obj := s.stateObjects[addr]; obj.deleted { + if s.parallel.isSlotDB { + if obj := s.parallel.dirtiedStateObjectsInSlot[addr]; obj.deleted { + s.deleteStateObject(obj) + s.AccountDeleted += 1 + } else { + s.updateStateObject(obj) + s.AccountUpdated += 1 + } + } else if obj, _ := s.getStateObjectFromStateObjects(addr); obj.deleted { s.deleteStateObject(obj) s.AccountDeleted += 1 } else { @@ -1028,6 +1593,9 @@ func (s *StateDB) StateIntermediateRoot() common.Hash { if prefetcher != nil { prefetcher.used(common.Hash{}, s.originalRoot, usedAddrs) } + // parallel slotDB trie will be updated to mainDB since intermediateRoot happens after conflict check. + // so it should be save to clear pending here. + // otherwise there can be a case that the deleted object get ignored and processes as live object in verify phase. if len(s.stateObjectsPending) > 0 { s.stateObjectsPending = make(map[common.Address]struct{}) @@ -1038,8 +1606,10 @@ func (s *StateDB) StateIntermediateRoot() common.Hash { } if s.noTrie { + log.Debug("StateIntermediateRoot", "root noTrie", s.expectedRoot) return s.expectedRoot } else { + log.Debug("StateIntermediateRoot", "root", s.trie.Hash()) return s.trie.Hash() } } @@ -1221,6 +1791,7 @@ func (s *StateDB) handleDestruction(nodes *trienode.MergedNodeSet) (map[common.A if s.db.TrieDB().Scheme() == rawdb.HashScheme { return incomplete, nil } + for addr, prev := range s.stateObjectsDestruct { // The original account was non-existing, and it's marked as destructed // in the scope of block. It can be case (a) or (b). @@ -1241,6 +1812,7 @@ func (s *StateDB) handleDestruction(nodes *trienode.MergedNodeSet) (map[common.A if prev.Root == types.EmptyRootHash { continue } + // Remove storage slots belong to the account. aborted, slots, set, err := s.deleteStorage(addr, addrHash, prev.Root) if err != nil { @@ -1363,7 +1935,7 @@ func (s *StateDB) Commit(block uint64, deleteEmptyObjects bool) (common.Hash, er } for addr := range s.stateObjectsDirty { - if obj := s.stateObjects[addr]; !obj.deleted { + if obj, _ := s.getStateObjectFromStateObjects(addr); !obj.deleted { tasks <- func() { // Write any storage changes in the state object to its storage trie if !s.noTrie { @@ -1447,7 +2019,7 @@ func (s *StateDB) Commit(block uint64, deleteEmptyObjects bool) (common.Hash, er } codeWriter := s.db.DiskDB().NewBatch() for addr := range s.stateObjectsDirty { - if obj := s.stateObjects[addr]; !obj.deleted { + if obj, _ := s.getStateObjectFromStateObjects(addr); !obj.deleted { // Write any contract code associated with the state object if obj.code != nil && obj.dirtyCode { rawdb.WriteCode(codeWriter, common.BytesToHash(obj.CodeHash()), obj.code) @@ -1608,7 +2180,7 @@ func (s *StateDB) SlotInAccessList(addr common.Address, slot common.Hash) (addre func (s *StateDB) convertAccountSet(set map[common.Address]*types.StateAccount) map[common.Hash]struct{} { ret := make(map[common.Hash]struct{}, len(set)) for addr := range set { - obj, exist := s.stateObjects[addr] + obj, exist := s.getStateObjectFromStateObjects(addr) if !exist { ret[crypto.Keccak256Hash(addr[:])] = struct{}{} } else { @@ -1632,6 +2204,208 @@ func (s *StateDB) GetSnap() snapshot.Snapshot { return s.snap } +func (s *StateDB) BeforeTxTransition() { + if s.isParallel && s.parallel.isSlotDB { + return + } + log.Debug("BeforeTxTransition", "mvStates", s.mvStates == nil, "rwSet", s.rwSet == nil) + if s.mvStates == nil { + return + } + s.rwSet = types.NewRWSet(types.StateVersion{ + TxIndex: s.txIndex, + }) +} + +func (s *StateDB) BeginTxStat(index int) { + if s.isParallel && s.parallel.isSlotDB { + return + } + if s.mvStates == nil { + return + } + if metrics.EnabledExpensive { + s.stat = types.NewExeStat(index).Begin() + } +} + +func (s *StateDB) StopTxStat(usedGas uint64) { + if s.isParallel && s.parallel.isSlotDB { + return + } + if s.mvStates == nil { + return + } + // record stat first + if metrics.EnabledExpensive && s.stat != nil { + s.stat.Done().WithGas(usedGas) + rwSet := s.mvStates.RWSet(s.txIndex) + if rwSet != nil { + s.stat.WithRead(len(rwSet.ReadSet())) + } + } +} + +func (s *StateDB) RecordRead(key types.RWKey, val interface{}) { + if s.isParallel && s.parallel.isSlotDB { + return + } + if s.rwSet == nil { + return + } + s.rwSet.RecordRead(key, types.StateVersion{ + TxIndex: -1, + }, val) +} + +func (s *StateDB) RecordWrite(key types.RWKey, val interface{}) { + if s.isParallel && s.parallel.isSlotDB { + return + } + if s.rwSet == nil { + return + } + s.rwSet.RecordWrite(key, val) +} + +func (s *StateDB) ResetMVStates(txCount int) { + if s.isParallel && s.parallel.isSlotDB { + return + } + if s.mvStates != nil { + s.mvStates.Stop() + } + s.mvStates = types.NewMVStates(txCount).EnableAsyncDepGen() + s.rwSet = nil +} + +func (s *StateDB) FinaliseRWSet() error { + if s.isParallel && s.parallel.isSlotDB { + return nil + } + if s.rwSet == nil { + return nil + } + rwSet := s.rwSet + stat := s.stat + if metrics.EnabledExpensive { + defer func(start time.Time) { + s.TxDAGGenerate += time.Since(start) + }(time.Now()) + } + ver := types.StateVersion{ + TxIndex: s.txIndex, + } + if ver != rwSet.Version() { + return errors.New("you finalize a wrong ver of RWSet") + } + + // finalise stateObjectsDestruct + for addr := range s.stateObjectsDestructDirty { + s.RecordWrite(types.AccountStateKey(addr, types.AccountSuicide), struct{}{}) + } + for addr := range s.journal.dirties { + obj, exist := s.getStateObjectFromStateObjects(addr) + if !exist { + continue + } + if obj.selfDestructed || obj.empty() { + // We need to maintain account deletions explicitly (will remain + // set indefinitely). Note only the first occurred self-destruct + // event is tracked. + if _, ok := s.stateObjectsDestruct[obj.address]; !ok { + log.Debug("FinaliseRWSet find Destruct", "tx", s.txIndex, "addr", addr, "selfDestructed", obj.selfDestructed) + s.RecordWrite(types.AccountStateKey(addr, types.AccountSuicide), struct{}{}) + } + } else { + // finalise account & storages + obj.finaliseRWSet() + } + } + + // reset stateDB + s.rwSet = nil + if err := s.mvStates.FulfillRWSet(rwSet, stat); err != nil { + return err + } + // just Finalise rwSet in serial execution + return s.mvStates.Finalise(s.txIndex) +} + +func (s *StateDB) getStateObjectsDestruct(addr common.Address) (*types.StateAccount, bool) { + if !(s.isParallel && s.parallel.isSlotDB) { + if acc, ok := s.stateObjectsDestructDirty[addr]; ok { + return acc, ok + } + } + acc, ok := s.stateObjectsDestruct[addr] + return acc, ok +} + +func (s *StateDB) setStateObjectsDestruct(addr common.Address, acc *types.StateAccount) { + if !(s.isParallel && s.parallel.isSlotDB) { + s.stateObjectsDestructDirty[addr] = acc + return + } + s.stateObjectsDestruct[addr] = acc + return +} + +func (s *StateDB) removeStateObjectsDestruct(addr common.Address) { + if !(s.isParallel && s.parallel.isSlotDB) { + delete(s.stateObjectsDestructDirty, addr) + return + } + delete(s.stateObjectsDestruct, addr) +} + +func (s *StateDB) ResolveTxDAG(txCnt int, gasFeeReceivers []common.Address) (types.TxDAG, error) { + if s.isParallel && s.parallel.isSlotDB { + return nil, nil + } + if s.mvStates == nil { + return types.NewEmptyTxDAG(), nil + } + if metrics.EnabledExpensive { + defer func(start time.Time) { + s.TxDAGGenerate += time.Since(start) + }(time.Now()) + } + + return s.mvStates.ResolveTxDAG(txCnt, gasFeeReceivers) +} + +func (s *StateDB) ResolveStats() map[int]*types.ExeStat { + if s.isParallel && s.parallel.isSlotDB { + return nil + } + if s.mvStates == nil { + return nil + } + + return s.mvStates.Stats() +} + +func (s *StateDB) MVStates() *types.MVStates { + if s.isParallel && s.parallel.isSlotDB { + return nil + } + return s.mvStates +} + +func (s *StateDB) RecordSystemTxRWSet(index int) { + if s.isParallel && s.parallel.isSlotDB { + return + } + if s.mvStates == nil { + return + } + s.mvStates.FulfillRWSet(types.NewRWSet(types.StateVersion{ + TxIndex: index, + }).WithExcludedTxFlag(), types.NewExeStat(index).WithExcludedTxFlag()) + s.mvStates.Finalise(index) +} + // copySet returns a deep-copied set. func copySet[k comparable](set map[k][]byte) map[k][]byte { copied := make(map[k][]byte, len(set)) @@ -1652,3 +2426,306 @@ func copy2DSet[k comparable](set map[k]map[common.Hash][]byte) map[k]map[common. } return copied } + +// PrepareForParallel prepares for state db to be used in parallel execution mode. +func (s *StateDB) PrepareForParallel() { + s.isParallel = true + s.parallel.stateObjects = &StateObjectSyncMap{} + // copy objects in stateObjects into parallel if not exist. + // This is lock free as the PrepareForParallel() is invoked at serial phase. + for addr, objPtr := range s.stateObjects { + if _, exist := s.parallel.stateObjects.LoadStateObject(addr); !exist { + newObj := objPtr.deepCopy(s) + s.parallel.stateObjects.StoreStateObject(addr, newObj) + } + } +} + +func (s *StateDB) AddrPrefetch(slotDb *ParallelStateDB) { + addressesToPrefetch := make([][]byte, 0, len(slotDb.parallel.dirtiedStateObjectsInSlot)) + for addr, obj := range slotDb.parallel.dirtiedStateObjectsInSlot { + addressesToPrefetch = append(addressesToPrefetch, common.CopyBytes(addr[:])) // Copy needed for closure + if obj.deleted { + continue + } + obj.storageRecordsLock.RLock() + // copied from obj.finalise(true) + slotsToPrefetch := make([][]byte, 0, obj.dirtyStorage.Length()) + obj.dirtyStorage.Range(func(key, value interface{}) bool { + originalValue, _ := obj.originStorage.GetValue(key.(common.Hash)) + if value.(common.Hash) != originalValue { + originalKey := key.(common.Hash) + slotsToPrefetch = append(slotsToPrefetch, common.CopyBytes(originalKey[:])) // Copy needed for closure + } + return true + }) + obj.storageRecordsLock.RUnlock() + if s.prefetcher != nil && len(slotsToPrefetch) > 0 { + s.trieParallelLock.Lock() + s.prefetcher.prefetch(obj.addrHash, obj.data.Root, obj.address, slotsToPrefetch) + s.trieParallelLock.Unlock() + } + } + + if s.prefetcher != nil && len(addressesToPrefetch) > 0 { + // log.Info("AddrPrefetch", "slotDb.TxIndex", slotDb.TxIndex(), + // "len(addressesToPrefetch)", len(slotDb.parallel.addressesToPrefetch)) + s.trieParallelLock.Lock() + s.prefetcher.prefetch(common.Hash{}, s.originalRoot, emptyAddr, addressesToPrefetch) + s.trieParallelLock.Unlock() + } +} + +// MergeSlotDB is for Parallel execution mode, when the transaction has been +// finalized(dirty -> pending) on execution slot, the execution results should be +// merged back to the main StateDB. +func (s *StateDB) MergeSlotDB(slotDb *ParallelStateDB, slotReceipt *types.Receipt, txIndex int, fees *DelayedGasFee) *StateDB { + + for s.nextRevisionId < slotDb.nextRevisionId { + if len(slotDb.validRevisions) > 0 { + r := slotDb.validRevisions[s.nextRevisionId] + s.validRevisions = append(s.validRevisions, r) + } + s.nextRevisionId++ + if len(slotDb.validRevisions) < s.nextRevisionId { + continue + } + } + + // receipt.Logs use unified log index within a block + // align slotDB's log index to the block stateDB's logSize + for _, l := range slotReceipt.Logs { + l.Index += s.logSize + s.logs[s.thash] = append(s.logs[s.thash], l) + } + + s.logSize += slotDb.logSize + + // only merge dirty objects + addressesToPrefetch := make([][]byte, 0, len(slotDb.stateObjectsDirty)) + + for addr := range slotDb.stateObjectsDirty { + if _, exist := s.stateObjectsDirty[addr]; !exist { + s.stateObjectsDirty[addr] = struct{}{} + } + + // stateObjects: KV, balance, nonce... + dirtyObj, ok := slotDb.parallel.dirtiedStateObjectsInSlot[addr] + if !ok { + log.Error("parallel merge, but dirty object not exist!", "SlotIndex", slotDb.parallel.SlotIndex, "txIndex:", slotDb.txIndex, "addr", addr) + continue + } + mainObj, exist := s.loadStateObj(addr) + if !exist || mainObj.deleted { + // addr not exist on main DB, the object is created in the merging tx. + mainObj = dirtyObj.deepCopy(s) + if !dirtyObj.deleted { + mainObj.finalise(true) + } + s.storeStateObj(addr, mainObj) + + // fixme: should not delete, would cause unconfirmed DB incorrect? + // delete(slotDb.parallel.dirtiedStateObjectsInSlot, addr) // transfer ownership, fixme: shared read? + if dirtyObj.deleted { + // remove the addr from snapAccounts&snapStorage only when object is deleted. + // "deleted" is not equal to "snapDestructs", since createObject() will add an addr for + // snapDestructs to destroy previous object, while it will keep the addr in snapAccounts & snapAccounts + s.AccountMux.Lock() + delete(s.accounts, dirtyObj.addrHash) // Clear out any previously updated account data (may be recreated via a resurrect) + delete(s.accountsOrigin, dirtyObj.address) // Clear out any previously updated account data (may be recreated via a resurrect) + s.AccountMux.Unlock() + s.StorageMux.Lock() + delete(s.storages, dirtyObj.addrHash) // Clear out any previously updated storage data (may be recreated via a resurrect) + delete(s.storagesOrigin, dirtyObj.address) // Clear out any previously updated storage data (may be recreated via a resurrect) + s.StorageMux.Unlock() + } + } else { + // addr already in main DB, do merge: balance, KV, code, State(create, suicide) + // can not do copy or ownership transfer directly, since dirtyObj could have outdated + // data(maybe updated within the conflict window) + var newMainObj = mainObj // we don't need to copy the object since the storages are thread safe + if createdOrChanged, ok := slotDb.parallel.addrStateChangesInSlot[addr]; ok { + // there are 4 kinds of state change: + // 1.Suicide + // 2.Empty Delete + // 3.createObject + // a: AddBalance,SetState to a non-exist or deleted(suicide, empty delete) address. + // b: CreateAccount: like DAO the fork, regenerate an account carry its balance without KV + // 4. setState. + // For these state change + if createdOrChanged { + // Need to differentiate the case of createObject and setState, since the mainDB at this moment contains + // the latest update of the object, which cause the object.data.root newer then the dirtyObject. so + // the deepCopy() here can not be used for setState as it introduces issue that the pendingStorage + // may not empty until block validation. so the pendingStorage filled by the execution of previous txs + // in same block may get overwritten by deepCopy here, which causes issue in root calculation. + if _, created := s.parallel.createdObjectRecord[addr]; created { + newMainObj = dirtyObj.deepCopy(s) + } else { + // Merge the dirtyObject with mainObject + if _, balanced := slotDb.parallel.balanceChangesInSlot[addr]; balanced { + if addr.Hex() == "0x13f4EA83D0bd40E75C8222255bc855a974568Dd4" { + log.Debug("MergeSlotDB", "update MainOBJ balance, origin dirtyBalance", newMainObj.dirtyBalance, + "origin balance", newMainObj.data.Balance, "dirty dirtyBalance", dirtyObj.dirtyBalance, + "dirty.dataBalance", dirtyObj.data.Balance) + } + newMainObj.dirtyBalance = dirtyObj.dirtyBalance + newMainObj.data.Balance = dirtyObj.data.Balance + } + if _, coded := slotDb.parallel.codeChangesInSlot[addr]; coded { + newMainObj.code = dirtyObj.code + newMainObj.dirtyCodeHash = dirtyObj.dirtyCodeHash + newMainObj.data.CodeHash = dirtyObj.data.CodeHash + newMainObj.dirtyCode = true + } + if keys, stated := slotDb.parallel.kvChangesInSlot[addr]; stated { + newMainObj.MergeSlotObject(s.db, dirtyObj, keys) + } + if _, nonced := slotDb.parallel.nonceChangesInSlot[addr]; nonced { + // dirtyObj.Nonce() should not be less than newMainObj + newMainObj.data.Nonce = dirtyObj.data.Nonce + newMainObj.dirtyNonce = dirtyObj.dirtyNonce + } + newMainObj.deleted = dirtyObj.deleted + } + } else { + // The object is deleted in the TX. + newMainObj = dirtyObj.deepCopy(s) + } + + // All cases with addrStateChange set to true/false can be deleted. so handle it here. + if dirtyObj.deleted { + // remove the addr from snapAccounts&snapStorage only when object is deleted. + // "deleted" is not equal to "snapDestructs", since createObject() will add an addr for + // snapDestructs to destroy previous object, while it will keep the addr in snapAccounts & snapAccounts + s.AccountMux.Lock() + delete(s.accounts, dirtyObj.addrHash) // Clear out any previously updated account data (may be recreated via a resurrect) + delete(s.accountsOrigin, dirtyObj.address) // Clear out any previously updated account data (may be recreated via a resurrect) + s.AccountMux.Unlock() + s.StorageMux.Lock() + delete(s.storages, dirtyObj.addrHash) // Clear out any previously updated storage data (may be recreated via a resurrect) + delete(s.storagesOrigin, dirtyObj.address) // Clear out any previously updated storage data (may be recreated via a resurrect) + s.StorageMux.Unlock() + } + } else { + // deepCopy a temporary *stateObject for safety, since slot could read the address, + // dispatch should avoid overwrite the StateObject directly otherwise, it could + // crash for: concurrent map iteration and map write + // As there is dirtyBalance, Nonce and codehash, we keep it to mainObj and leave the merging work + // to "mainObj.finalise()", just in case that newMainObj.delete == true and somewhere potentially + // access the Nonce, balance or codehash later. + if _, balanced := slotDb.parallel.balanceChangesInSlot[addr]; balanced { + if addr.Hex() == "0x13f4EA83D0bd40E75C8222255bc855a974568Dd4" { + log.Debug("MergeSlotDB", "update MainOBJ balance, origin dirtyBalance", newMainObj.dirtyBalance, + "origin balance", newMainObj.data.Balance, "dirty dirtyBalance", dirtyObj.dirtyBalance, + "dirty.dataBalance", dirtyObj.data.Balance) + } + newMainObj.dirtyBalance = dirtyObj.dirtyBalance + newMainObj.data.Balance = dirtyObj.data.Balance + } + if _, coded := slotDb.parallel.codeChangesInSlot[addr]; coded { + newMainObj.code = dirtyObj.code + newMainObj.dirtyCodeHash = dirtyObj.dirtyCodeHash + newMainObj.data.CodeHash = dirtyObj.data.CodeHash + newMainObj.dirtyCode = true + } + if keys, stated := slotDb.parallel.kvChangesInSlot[addr]; stated { + newMainObj.MergeSlotObject(s.db, dirtyObj, keys) + } + if _, nonced := slotDb.parallel.nonceChangesInSlot[addr]; nonced { + // dirtyObj.Nonce() should not be less than newMainObj + newMainObj.data.Nonce = dirtyObj.data.Nonce + newMainObj.dirtyNonce = dirtyObj.dirtyNonce + } + newMainObj.deleted = dirtyObj.deleted + } + if !newMainObj.deleted { + newMainObj.finalise(true) // true: prefetch on dispatcher + } + // update the object + s.storeStateObj(addr, newMainObj) + } + addressesToPrefetch = append(addressesToPrefetch, common.CopyBytes(addr[:])) // Copy needed for closure + } + + if s.prefetcher != nil && len(addressesToPrefetch) > 0 { + s.trieParallelLock.Lock() + s.prefetcher.prefetch(common.Hash{}, s.originalRoot, emptyAddr, addressesToPrefetch) // prefetch for trie node of account + s.trieParallelLock.Unlock() + } + + for addr := range slotDb.stateObjectsPending { + if _, exist := s.stateObjectsPending[addr]; !exist { + s.stateObjectsPending[addr] = struct{}{} + } + } + + s.stateObjectDestructLock.Lock() + for addr := range slotDb.stateObjectsDestruct { + if acc, exist := s.stateObjectsDestruct[addr]; !exist { + s.stateObjectsDestruct[addr] = acc + } + } + s.stateObjectDestructLock.Unlock() + // slotDb.logs: logs will be kept in receipts, no need to do merge + for hash, preimage := range slotDb.preimages { + s.preimages[hash] = preimage + } + + if s.accessList != nil && slotDb.accessList != nil { + s.accessList.Append(slotDb.accessList) + } + + for k := range slotDb.snapDestructs { + s.snapParallelLock.Lock() + s.snapDestructs[k] = struct{}{} + s.snapParallelLock.Unlock() + } + + s.SetTxContext(slotDb.thash, slotDb.txIndex) + return s +} + +// NewParallelDBManager creates a new ParallelDBManager with the specified number of instance +func NewParallelDBManager(initialCount int, newFunc func() *ParallelStateDB) *ParallelDBManager { + manager := &ParallelDBManager{ + pool: list.New(), + mutex: sync.Mutex{}, + newFunc: newFunc, + } + + for i := 0; i < initialCount; i++ { + manager.pool.PushBack(newFunc()) + } + + return manager +} + +// ParallelDBManager manages a pool of ParallelDB instances +type ParallelDBManager struct { + pool *list.List + mutex sync.Mutex + newFunc func() *ParallelStateDB // Function to create a new ParallelDB instance +} + +// allocate acquires a ParallelStateDB instance from the pool +// if the pool is empty, directly create a new one. +func (m *ParallelDBManager) allocate() *ParallelStateDB { + m.mutex.Lock() + defer m.mutex.Unlock() + + if m.pool.Len() == 0 { + return m.newFunc() + } + + elem := m.pool.Front() + m.pool.Remove(elem) + ret := elem.Value.(*ParallelStateDB) + return ret +} + +func (m *ParallelDBManager) reclaim(s *ParallelStateDB) { + m.mutex.Lock() + defer m.mutex.Unlock() + m.pool.PushBack(s) +} diff --git a/core/state/statedb_test.go b/core/state/statedb_test.go index e71c984f12..0a759021d9 100644 --- a/core/state/statedb_test.go +++ b/core/state/statedb_test.go @@ -19,6 +19,7 @@ package state import ( "bytes" "encoding/binary" + "encoding/hex" "errors" "fmt" "math" @@ -43,6 +44,10 @@ import ( "github.com/holiman/uint256" ) +var ( + testAddress = common.HexToAddress("0xffffFFFfFFffffffffffffffFfFFFfffFFFfFFfE") +) + // Tests that updating a state trie does not leak any database writes prior to // actually committing the state. func TestUpdateLeaks(t *testing.T) { @@ -180,6 +185,7 @@ func TestCopy(t *testing.T) { // modify all in memory for i := byte(0); i < 255; i++ { + origObj := orig.getOrNewStateObject(common.BytesToAddress([]byte{i})) copyObj := copy.getOrNewStateObject(common.BytesToAddress([]byte{i})) ccopyObj := ccopy.getOrNewStateObject(common.BytesToAddress([]byte{i})) @@ -465,7 +471,7 @@ func forEachStorage(s *StateDB, addr common.Address, cb func(key, value common.H for it.Next() { key := common.BytesToHash(s.trie.GetKey(it.Key)) - if value, dirty := so.dirtyStorage[key]; dirty { + if value, dirty := so.dirtyStorage.GetValue(key); dirty { if !cb(key, value) { return nil } @@ -1191,3 +1197,378 @@ func TestDeleteStorage(t *testing.T) { t.Fatalf("difference found:\nfast: %v\nslow: %v\n", fastRes, slowRes) } } + +func TestSuicide(t *testing.T) { + // Create an initial state with a few accounts + db := rawdb.NewMemoryDatabase() + state, _ := New(types.EmptyRootHash, NewDatabase(db), nil) + unconfirmedDBs := new(sync.Map) + + state.PrepareForParallel() + manager := NewParallelDBManager(1, NewEmptySlotDB) + slotDb := NewSlotDB(state, 0, 0, manager, unconfirmedDBs, false) + + addr := common.BytesToAddress([]byte("so")) + slotDb.SetBalance(addr, uint256.NewInt(1)) + + slotDb.SelfDestruct(addr) + + if _, ok := slotDb.parallel.addrStateChangesInSlot[addr]; !ok { + t.Fatalf("address should exist in addrStateChangesInSlot") + } + + if _, ok := slotDb.parallel.dirtiedStateObjectsInSlot[addr]; !ok { + t.Fatalf("address should exist in dirtiedStateObjectsInSlot") + } + + hasSuicide := slotDb.HasSelfDestructed(addr) + if !hasSuicide { + t.Fatalf("address should be suicided") + } + + if _, ok := slotDb.parallel.addrStateReadsInSlot[addr]; !ok { + t.Fatalf("address should exist in addrStateReadsInSlot") + } +} + +func TestSetAndGetState(t *testing.T) { + memDb := rawdb.NewMemoryDatabase() + db := NewDatabase(memDb) + state, _ := New(types.EmptyRootHash, db, nil) + + addr := common.BytesToAddress([]byte("so")) + state.SetBalance(addr, uint256.NewInt(1)) + unconfirmedDBs := new(sync.Map) + state.PrepareForParallel() + manager := NewParallelDBManager(1, NewEmptySlotDB) + slotDb := NewSlotDB(state, 0, 0, manager, unconfirmedDBs, false) + slotDb.SetState(addr, common.BytesToHash([]byte("test key")), common.BytesToHash([]byte("test store"))) + + if _, ok := slotDb.parallel.dirtiedStateObjectsInSlot[addr]; !ok { + t.Fatalf("address should exist in dirtiedStateObjectsInSlot") + } + + if _, ok := slotDb.parallel.addrStateChangesInSlot[addr]; !ok { + t.Fatalf("address should exist in stateChangesInSlot") + } + + oldValueRead := state.GetState(addr, common.BytesToHash([]byte("test key"))) + emptyHash := common.Hash{} + if oldValueRead != emptyHash { + t.Fatalf("value read in old state should be empty") + } + + valueRead := slotDb.GetState(addr, common.BytesToHash([]byte("test key"))) + if valueRead != common.BytesToHash([]byte("test store")) { + t.Fatalf("value read should be equal to the stored value") + } + + if _, ok := slotDb.parallel.addrStateReadsInSlot[addr]; !ok { + t.Fatalf("address should exist in stateReadsInSlot") + } +} + +func TestSetAndGetCode(t *testing.T) { + memDb := rawdb.NewMemoryDatabase() + db := NewDatabase(memDb) + state, _ := New(common.Hash{}, db, nil) + + addr := common.BytesToAddress([]byte("so")) + state.SetBalance(addr, uint256.NewInt(1)) + state.PrepareForParallel() + + unconfirmedDBs := new(sync.Map) + manager := NewParallelDBManager(1, NewEmptySlotDB) + slotDb := NewSlotDB(state, 0, 0, manager, unconfirmedDBs, false) + if _, ok := slotDb.parallel.dirtiedStateObjectsInSlot[addr]; ok { + t.Fatalf("address should not exist in dirtiedStateObjectsInSlot") + } + + slotDb.SetCode(addr, []byte("test code")) + + if _, ok := slotDb.parallel.dirtiedStateObjectsInSlot[addr]; !ok { + t.Fatalf("address should exist in dirtiedStateObjectsInSlot") + } + + if _, ok := slotDb.parallel.codeChangesInSlot[addr]; !ok { + t.Fatalf("address should exist in codeChangesInSlot") + } + + codeRead := slotDb.GetCode(addr) + if string(codeRead) != "test code" { + t.Fatalf("code read should be equal to the code stored") + } + + if _, ok := slotDb.parallel.codeReadsInSlot[addr]; !ok { + t.Fatalf("address should exist in codeReadsInSlot") + } +} + +func TestGetCodeSize(t *testing.T) { + memDb := rawdb.NewMemoryDatabase() + db := NewDatabase(memDb) + state, _ := New(common.Hash{}, db, nil) + + addr := common.BytesToAddress([]byte("so")) + state.SetBalance(addr, uint256.NewInt(1)) + state.PrepareForParallel() + + unconfirmedDBs := new(sync.Map) + manager := NewParallelDBManager(1, NewEmptySlotDB) + slotDb := NewSlotDB(state, 0, 0, manager, unconfirmedDBs, false) + slotDb.SetCode(addr, []byte("test code")) + + codeSize := slotDb.GetCodeSize(addr) + if codeSize != 9 { + t.Fatalf("code size should be 9") + } + + if _, ok := slotDb.parallel.codeReadsInSlot[addr]; !ok { + t.Fatalf("address should exist in codeReadsInSlot") + } +} + +func TestGetCodeHash(t *testing.T) { + memDb := rawdb.NewMemoryDatabase() + db := NewDatabase(memDb) + state, _ := New(common.Hash{}, db, nil) + + addr := common.BytesToAddress([]byte("so")) + state.SetBalance(addr, uint256.NewInt(1)) + state.PrepareForParallel() + unconfirmedDBs := new(sync.Map) + manager := NewParallelDBManager(1, NewEmptySlotDB) + slotDb := NewSlotDB(state, 0, 0, manager, unconfirmedDBs, false) + + slotDb.SetCode(addr, []byte("test code")) + + codeSize := slotDb.GetCodeHash(addr) + + if hex.EncodeToString(codeSize[:]) != "6e73fa02f7828b28608b078b007a4023fb40453c3e102b83828a3609a94d8cbb" { + t.Fatalf("code hash should be 6e73fa02f7828b28608b078b007a4023fb40453c3e102b83828a3609a94d8cbb") + } + if _, ok := slotDb.parallel.codeReadsInSlot[addr]; !ok { + t.Fatalf("address should exist in codeReadsInSlot") + } +} + +func TestSetNonce(t *testing.T) { + memDb := rawdb.NewMemoryDatabase() + db := NewDatabase(memDb) + state, _ := New(common.Hash{}, db, nil) + + addr := common.BytesToAddress([]byte("so")) + state.SetBalance(addr, uint256.NewInt(1)) + state.SetNonce(addr, 1) + state.PrepareForParallel() + + unconfirmedDBs := new(sync.Map) + manager := NewParallelDBManager(1, NewEmptySlotDB) + slotDb := NewSlotDB(state, 0, 0, manager, unconfirmedDBs, false) + slotDb.SetNonce(addr, 2) + + oldNonce := state.GetNonce(addr) + if oldNonce != 1 { + t.Fatalf("old nonce should be 1") + } + + newNonce := slotDb.GetNonce(addr) + if newNonce != 2 { + t.Fatalf("new nonce should be 2") + } + if _, ok := slotDb.parallel.dirtiedStateObjectsInSlot[addr]; !ok { + t.Fatalf("address should exist in dirtiedStateObjectsInSlot") + } +} + +func TestSetAndGetBalance(t *testing.T) { + memDb := rawdb.NewMemoryDatabase() + db := NewDatabase(memDb) + state, _ := New(common.Hash{}, db, nil) + + addr := testAddress + state.SetBalance(addr, uint256.NewInt(1)) + state.PrepareForParallel() + unconfirmedDBs := new(sync.Map) + manager := NewParallelDBManager(1, NewEmptySlotDB) + slotDb := NewSlotDB(state, 0, 0, manager, unconfirmedDBs, false) + + slotDb.SetBalance(addr, uint256.NewInt(2)) + + oldBalance := state.GetBalance(addr) + if oldBalance.Uint64() != 1 { + t.Fatalf("old balance should be 1") + } + + if _, ok := slotDb.parallel.dirtiedStateObjectsInSlot[addr]; !ok { + t.Fatalf("address should exist in dirtiedStateObjectsInSlot") + } + + if _, ok := slotDb.parallel.balanceChangesInSlot[addr]; !ok { + t.Fatalf("address should exist in balanceChangesInSlot") + } + + newBalance := slotDb.GetBalance(addr) + if newBalance.Uint64() != 2 { + t.Fatalf("new nonce should be 2") + } + + if _, ok := slotDb.parallel.balanceReadsInSlot[addr]; !ok { + t.Fatalf("address should exist in balanceReadsInSlot") + } +} + +func TestSubBalance(t *testing.T) { + memDb := rawdb.NewMemoryDatabase() + db := NewDatabase(memDb) + state, _ := New(common.Hash{}, db, nil) + addr := testAddress + state.SetBalance(addr, uint256.NewInt(2)) + + state.PrepareForParallel() + unconfirmedDBs := new(sync.Map) + manager := NewParallelDBManager(1, NewEmptySlotDB) + slotDb := NewSlotDB(state, 0, 0, manager, unconfirmedDBs, false) + slotDb.SubBalance(addr, uint256.NewInt(1)) + + oldBalance := state.GetBalance(addr) + if oldBalance.Uint64() != 2 { + t.Fatalf("old balance should be 1") + } + + if _, ok := slotDb.parallel.dirtiedStateObjectsInSlot[addr]; !ok { + t.Fatalf("address should exist in dirtiedStateObjectsInSlot") + } + + if _, ok := slotDb.parallel.balanceChangesInSlot[addr]; !ok { + t.Fatalf("address should exist in balanceChangesInSlot") + } + + if _, ok := slotDb.parallel.balanceReadsInSlot[addr]; !ok { + t.Fatalf("address should exist in balanceReadsInSlot") + } + + newBalance := slotDb.GetBalance(addr) + if newBalance.Uint64() != 1 { + t.Fatalf("new nonce should be 2") + } +} + +func TestAddBalance(t *testing.T) { + memDb := rawdb.NewMemoryDatabase() + db := NewDatabase(memDb) + state, _ := New(common.Hash{}, db, nil) + addr := testAddress + state.SetBalance(addr, uint256.NewInt(2)) + state.PrepareForParallel() + unconfirmedDBs := new(sync.Map) + manager := NewParallelDBManager(1, NewEmptySlotDB) + slotDb := NewSlotDB(state, 0, 0, manager, unconfirmedDBs, false) + slotDb.AddBalance(addr, uint256.NewInt(1)) + + oldBalance := state.GetBalance(addr) + if oldBalance.Uint64() != 2 { + t.Fatalf("old balance should be 1") + } + + if _, ok := slotDb.parallel.dirtiedStateObjectsInSlot[addr]; !ok { + t.Fatalf("address should exist in dirtiedStateObjectsInSlot") + } + + if _, ok := slotDb.parallel.balanceChangesInSlot[addr]; !ok { + t.Fatalf("address should exist in balanceChangesInSlot") + } + + if _, ok := slotDb.parallel.balanceReadsInSlot[addr]; !ok { + t.Fatalf("address should exist in balanceReadsInSlot") + } + + newBalance := slotDb.GetBalance(addr) + if newBalance.Uint64() != 3 { + t.Fatalf("new nonce should be 2") + } +} + +func TestEmpty(t *testing.T) { + memDb := rawdb.NewMemoryDatabase() + db := NewDatabase(memDb) + state, _ := New(common.Hash{}, db, nil) + addr := testAddress + state.SetBalance(addr, uint256.NewInt(2)) + state.PrepareForParallel() + + unconfirmedDBs := new(sync.Map) + manager := NewParallelDBManager(1, NewEmptySlotDB) + slotDb := NewSlotDB(state, 0, 0, manager, unconfirmedDBs, false) + + empty := slotDb.Empty(addr) + if empty { + t.Fatalf("address should exist") + } + + if _, ok := slotDb.parallel.addrStateReadsInSlot[addr]; !ok { + t.Fatalf("address should exist in addrStateReadsInSlot") + } +} + +func TestExist(t *testing.T) { + memDb := rawdb.NewMemoryDatabase() + db := NewDatabase(memDb) + state, _ := New(common.Hash{}, db, nil) + addr := testAddress + state.SetBalance(addr, uint256.NewInt(2)) + state.PrepareForParallel() + unconfirmedDBs := new(sync.Map) + manager := NewParallelDBManager(1, NewEmptySlotDB) + slotDb := NewSlotDB(state, 0, 0, manager, unconfirmedDBs, false) + + exist := slotDb.Exist(addr) + if !exist { + t.Fatalf("address should exist") + } + + if _, ok := slotDb.parallel.addrStateReadsInSlot[addr]; !ok { + t.Fatalf("address should exist in addrStateReadsInSlot") + } +} + +func TestMergeSlotDB(t *testing.T) { + memDb := rawdb.NewMemoryDatabase() + db := NewDatabase(memDb) + state, _ := New(common.Hash{}, db, nil) + state.PrepareForParallel() + unconfirmedDBs := new(sync.Map) + manager := NewParallelDBManager(2, NewEmptySlotDB) + oldSlotDb := NewSlotDB(state, 0, 0, manager, unconfirmedDBs, false) + + newSlotDb := NewSlotDB(state, 0, 0, manager, unconfirmedDBs, false) + + addr := testAddress + newSlotDb.SetBalance(addr, uint256.NewInt(2)) + newSlotDb.SetState(addr, common.BytesToHash([]byte("test key")), common.BytesToHash([]byte("test store"))) + newSlotDb.SetCode(addr, []byte("test code")) + newSlotDb.SelfDestruct(addr) + newSlotDb.Finalise(true) + + changeList := oldSlotDb.MergeSlotDB(newSlotDb, &types.Receipt{}, 0, nil) + + if ok := changeList.getDeletedStateObject(addr); ok == nil || !ok.selfDestructed { + t.Fatalf("address should exist in StateObjectSuicided") + } + + if ok := changeList.getStateObject(addr); ok != nil { + t.Fatalf("address should exist in StateChangeSet") + } + + if ok := changeList.GetBalance(addr); ok != common.U2560 { + t.Fatalf("address should exist in StateChangeSet") + } + + if ok := changeList.GetCode(addr); ok != nil { + t.Fatalf("address should exist in CodeChangeSet") + } + + if ok := changeList.getStateObject(addr); ok != nil { + t.Fatalf("address should exist in AddrStateChangeSet") + } +} diff --git a/core/state/transient_storage.go b/core/state/transient_storage.go index 66e563efa7..ea2b5bfefe 100644 --- a/core/state/transient_storage.go +++ b/core/state/transient_storage.go @@ -21,7 +21,7 @@ import ( ) // transientStorage is a representation of EIP-1153 "Transient Storage". -type transientStorage map[common.Address]Storage +type transientStorage map[common.Address]StorageMap // newTransientStorage creates a new instance of a transientStorage. func newTransientStorage() transientStorage { @@ -31,7 +31,7 @@ func newTransientStorage() transientStorage { // Set sets the transient-storage `value` for `key` at the given `addr`. func (t transientStorage) Set(addr common.Address, key, value common.Hash) { if _, ok := t[addr]; !ok { - t[addr] = make(Storage) + t[addr] = make(StorageMap) } t[addr][key] = value } @@ -49,7 +49,8 @@ func (t transientStorage) Get(addr common.Address, key common.Hash) common.Hash func (t transientStorage) Copy() transientStorage { storage := make(transientStorage) for key, value := range t { - storage[key] = value.Copy() + m := value.Copy() + storage[key] = m.(StorageMap) } return storage } diff --git a/core/state_processor.go b/core/state_processor.go index c9df98536c..df9d788707 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -54,6 +54,14 @@ func NewStateProcessor(config *params.ChainConfig, bc *BlockChain, engine consen } } +// CreateSerialProcessor create a new StateProcessor +func (bc *BlockChain) CreateSerialProcessor(config *params.ChainConfig, bc2 *BlockChain, engine consensus.Engine) { + if bc.serialProcessor == nil { + bc.serialProcessor = NewStateProcessor(config, bc2, engine) + bc.parallelExecution = false + } +} + // 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. @@ -90,8 +98,12 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg ProcessBeaconBlockRoot(*beaconRoot, vmenv, statedb) } statedb.MarkFullProcessed() + if p.bc.enableTxDAG && !p.bc.vmConfig.EnableParallelExec { + statedb.ResetMVStates(len(block.Transactions())) + } // Iterate over and process the individual transactions for i, tx := range block.Transactions() { + statedb.BeginTxStat(i) start := time.Now() msg, err := TransactionToMessage(tx, signer, header.BaseFee) if err != nil { @@ -103,11 +115,16 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg return nil, nil, 0, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err) } + // if systemTx or depositTx, tag it + if tx.IsSystemTx() || tx.IsDepositTx() { + statedb.RecordSystemTxRWSet(i) + } receipts = append(receipts, receipt) allLogs = append(allLogs, receipt.Logs...) if metrics.EnabledExpensive { processTxTimer.UpdateSince(start) } + statedb.StopTxStat(receipt.GasUsed) } // Fail if Shanghai not enabled and len(withdrawals) is non-zero. withdrawals := block.Withdrawals() @@ -116,7 +133,6 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg } // Finalize the block, applying any consensus engine specific extras (e.g. block rewards) p.engine.Finalize(p.bc, header, statedb, block.Transactions(), block.Uncles(), withdrawals) - return receipts, allLogs, *usedGas, nil } diff --git a/core/state_transition.go b/core/state_transition.go index a23a26468e..5b87b84fdd 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -18,6 +18,8 @@ package core import ( "fmt" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/log" "math" "math/big" "time" @@ -41,6 +43,7 @@ type ExecutionResult struct { RefundedGas uint64 // Total gas refunded after execution 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) + delayFees *state.DelayedGasFee } // Unwrap returns the internal evm error which allows us for further @@ -197,6 +200,12 @@ func ApplyMessage(evm *vm.EVM, msg *Message, gp *GasPool) (*ExecutionResult, err return NewStateTransition(evm, msg, gp).TransitionDb() } +func ApplyMessageDelayGasFee(evm *vm.EVM, msg *Message, gp *GasPool) (*ExecutionResult, error) { + transition := NewStateTransition(evm, msg, gp) + transition.delayGasFee = true + return transition.TransitionDb() +} + // StateTransition represents a state transition. // // == The State Transitioning Model @@ -226,6 +235,7 @@ type StateTransition struct { initialGas uint64 state vm.StateDB evm *vm.EVM + delayGasFee bool } // NewStateTransition initialises and returns a new state transition object. @@ -408,6 +418,10 @@ func (st *StateTransition) preCheck() error { // However if any consensus issue encountered, return the error directly with // nil evm execution result. func (st *StateTransition) TransitionDb() (*ExecutionResult, error) { + // start record rw set in here + if !st.msg.IsSystemTx && !st.msg.IsDepositTx { + st.state.BeforeTxTransition() + } if mint := st.msg.Mint; mint != nil { mintU256, overflow := uint256.FromBig(mint) if overflow { @@ -432,6 +446,10 @@ func (st *StateTransition) TransitionDb() (*ExecutionResult, error) { if st.msg.IsSystemTx && !st.evm.ChainConfig().IsRegolith(st.evm.Context.Time) { gasUsed = 0 } + // just record error tx here + if ferr := st.state.FinaliseRWSet(); ferr != nil { + log.Error("finalise error deposit tx rwSet fail", "block", st.evm.Context.BlockNumber, "tx", st.evm.StateDB.TxIndex(), "err", ferr) + } result = &ExecutionResult{ UsedGas: gasUsed, Err: fmt.Errorf("failed deposit: %w", err), @@ -439,6 +457,12 @@ func (st *StateTransition) TransitionDb() (*ExecutionResult, error) { } err = nil } + if err != nil { + // just record error tx here + if ferr := st.state.FinaliseRWSet(); ferr != nil { + log.Error("finalise error tx rwSet fail", "block", st.evm.Context.BlockNumber, "tx", st.evm.StateDB.TxIndex(), "err", ferr) + } + } return result, err } @@ -519,6 +543,11 @@ func (st *StateTransition) innerTransitionDb() (*ExecutionResult, error) { } DebugInnerExecutionDuration += time.Since(start) + // stop record rw set in here, skip gas fee distribution + if ferr := st.state.FinaliseRWSet(); ferr != nil { + log.Error("finalise tx rwSet fail", "block", st.evm.Context.BlockNumber, "tx", st.evm.StateDB.TxIndex(), "err", ferr) + } + // if deposit: skip refunds, skip tipping coinbase // Regolith changes this behaviour to report the actual gasUsed instead of always reporting all gas used. if st.msg.IsDepositTx && !rules.IsOptimismRegolith { @@ -554,12 +583,19 @@ func (st *StateTransition) innerTransitionDb() (*ExecutionResult, error) { ReturnData: ret, }, nil } + + var ( + tipFee *uint256.Int + baseFee *uint256.Int + l1Fee *uint256.Int + ) effectiveTip := msg.GasPrice if rules.IsLondon { effectiveTip = cmath.BigMin(msg.GasTipCap, new(big.Int).Sub(msg.GasFeeCap, st.evm.Context.BaseFee)) } effectiveTipU256, _ := uint256.FromBig(effectiveTip) + // delay gas fee calculation, provide from TxDAG if st.evm.Config.NoBaseFee && msg.GasFeeCap.Sign() == 0 && msg.GasTipCap.Sign() == 0 { // Skip fee payment when NoBaseFee is set and the fee fields // are 0. This avoids a negative effectiveTip being applied to @@ -567,7 +603,11 @@ func (st *StateTransition) innerTransitionDb() (*ExecutionResult, error) { } else { fee := new(uint256.Int).SetUint64(st.gasUsed()) fee.Mul(fee, effectiveTipU256) - st.state.AddBalance(st.evm.Context.Coinbase, fee) + if st.delayGasFee { + tipFee = fee + } else { + st.state.AddBalance(st.evm.Context.Coinbase, fee) + } } // Check that we are post bedrock to enable op-geth to be able to create pseudo pre-bedrock blocks (these are pre-bedrock, but don't follow l2 geth rules) @@ -578,23 +618,40 @@ func (st *StateTransition) innerTransitionDb() (*ExecutionResult, error) { if overflow { return nil, fmt.Errorf("optimism gas cost overflows U256: %d", gasCost) } - st.state.AddBalance(params.OptimismBaseFeeRecipient, amtU256) + if st.delayGasFee { + baseFee = amtU256 + } else { + st.state.AddBalance(params.OptimismBaseFeeRecipient, amtU256) + } if st.msg.GasPrice.Cmp(big.NewInt(0)) == 0 && st.evm.ChainConfig().IsWright(st.evm.Context.Time) { - st.state.AddBalance(params.OptimismL1FeeRecipient, uint256.NewInt(0)) + if st.delayGasFee { + l1Fee = uint256.NewInt(0) + } else { + st.state.AddBalance(params.OptimismL1FeeRecipient, uint256.NewInt(0)) + } } else if l1Cost := st.evm.Context.L1CostFunc(st.msg.RollupCostData, st.evm.Context.Time); l1Cost != nil { amtU256, overflow = uint256.FromBig(l1Cost) if overflow { return nil, fmt.Errorf("optimism l1 cost overflows U256: %d", l1Cost) } - st.state.AddBalance(params.OptimismL1FeeRecipient, amtU256) + if st.delayGasFee { + l1Fee = amtU256 + } else { + st.state.AddBalance(params.OptimismL1FeeRecipient, amtU256) + } } } - return &ExecutionResult{ UsedGas: st.gasUsed(), RefundedGas: gasRefund, Err: vmerr, ReturnData: ret, + delayFees: &state.DelayedGasFee{ + TipFee: tipFee, + BaseFee: baseFee, + L1Fee: l1Fee, + Coinbase: st.evm.Context.Coinbase, + }, }, nil } diff --git a/core/types/dag.go b/core/types/dag.go new file mode 100644 index 0000000000..e46f7bb897 --- /dev/null +++ b/core/types/dag.go @@ -0,0 +1,647 @@ +package types + +import ( + "bytes" + "errors" + "fmt" + "strings" + "time" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/metrics" + "github.com/ethereum/go-ethereum/rlp" + "golang.org/x/exp/slices" +) + +const TxDAGAbiJson = ` +[ + { + "type": "function", + "name": "setTxDAG", + "inputs": [ + { + "name": "data", + "type": "bytes", + "internalType": "bytes" + } + ], + "outputs": [], + "stateMutability": "nonpayable" + } +] +` + +var TxDAGABI abi.ABI + +func init() { + var err error + // must be able to register the TxDAGABI + TxDAGABI, err = abi.JSON(strings.NewReader(TxDAGAbiJson)) + if err != nil { + panic(err) + } +} + +// TxDAGType Used to extend TxDAG and customize a new DAG structure +const ( + EmptyTxDAGType byte = iota + PlainTxDAGType +) + +var ( + // NonDependentRelFlag indicates that the txs described is non-dependent + // and is used to reduce storage when there are a large number of dependencies. + NonDependentRelFlag uint8 = 0x01 + // ExcludedTxFlag indicates that the tx is excluded from TxDAG, user should execute them in sequence. + // These excluded transactions should be consecutive in the head or tail. + ExcludedTxFlag uint8 = 0x02 + TxDepFlagMask = NonDependentRelFlag | ExcludedTxFlag +) + +type TxDAG interface { + // Type return TxDAG type + Type() byte + + // Inner return inner instance + Inner() interface{} + + // DelayGasFeeDistribution check if delay the distribution of GasFee + DelayGasFeeDistribution() bool + + // TxDep query TxDeps from TxDAG + TxDep(int) *TxDep + + // TxCount return tx count + TxCount() int + + // SetTxDep at the last one + SetTxDep(int, TxDep) error +} + +func DecodeTxDAGCalldata(data []byte) (TxDAG, error) { + // trim the method id before unpack + if len(data) < 4 { + return nil, fmt.Errorf("invalid txDAG calldata, len(data)=%d", len(data)) + } + calldata, err := TxDAGABI.Methods["setTxDAG"].Inputs.Unpack(data[4:]) + if err != nil { + return nil, fmt.Errorf("failed to call abi unpack, err: %v", err) + } + if len(calldata) <= 0 { + return nil, fmt.Errorf("invalid txDAG calldata, len(calldata)=%d", len(calldata)) + } + data, ok := calldata[0].([]byte) + if !ok { + return nil, fmt.Errorf("invalid txDAG calldata parameter") + } + return DecodeTxDAG(data) +} + +func EncodeTxDAGCalldata(dag TxDAG) ([]byte, error) { + data, err := EncodeTxDAG(dag) + if err != nil { + return nil, fmt.Errorf("failed to encode txDAG, err: %v", err) + } + data, err = TxDAGABI.Pack("setTxDAG", data) + if err != nil { + return nil, fmt.Errorf("failed to call abi pack, err: %v", err) + } + return data, nil +} + +func EncodeTxDAG(dag TxDAG) ([]byte, error) { + if dag == nil { + return nil, errors.New("input nil TxDAG") + } + var buf bytes.Buffer + buf.WriteByte(dag.Type()) + if err := rlp.Encode(&buf, dag.Inner()); err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +func DecodeTxDAG(enc []byte) (TxDAG, error) { + if len(enc) <= 1 { + return nil, errors.New("too short TxDAG bytes") + } + + switch enc[0] { + case EmptyTxDAGType: + return NewEmptyTxDAG(), nil + case PlainTxDAGType: + dag := new(PlainTxDAG) + if err := rlp.DecodeBytes(enc[1:], dag); err != nil { + return nil, err + } + return dag, nil + default: + return nil, errors.New("unsupported TxDAG bytes") + } +} + +func ValidateTxDAG(d TxDAG, txCnt int) error { + if d == nil { + return nil + } + + switch d.Type() { + case EmptyTxDAGType: + return nil + case PlainTxDAGType: + return ValidatePlainTxDAG(d, txCnt) + default: + return fmt.Errorf("unsupported TxDAG type: %v", d.Type()) + } +} + +func ValidatePlainTxDAG(d TxDAG, txCnt int) error { + if d.TxCount() != txCnt { + return fmt.Errorf("PlainTxDAG contains wrong txs count, expect: %v, actual: %v", txCnt, d.TxCount()) + } + for i := 0; i < txCnt; i++ { + dep := d.TxDep(i) + if dep == nil { + return fmt.Errorf("PlainTxDAG contains nil txdep, tx: %v", i) + } + for j, tx := range dep.TxIndexes { + if tx >= uint64(i) || tx >= uint64(txCnt) { + return fmt.Errorf("PlainTxDAG contains the exceed range dependency, tx: %v", i) + } + if j > 0 && dep.TxIndexes[j] <= dep.TxIndexes[j-1] { + return fmt.Errorf("PlainTxDAG contains unordered dependency, tx: %v", i) + } + } + if dep.Flags != nil && *dep.Flags & ^TxDepFlagMask > 0 { + return fmt.Errorf("PlainTxDAG contains unknown flags, flags: %v", *dep.Flags) + } + } + return nil +} + +// GetTxDAG return TxDAG bytes from block if there is any, or return nil if not exist +// the txDAG is stored in the calldata of the last transaction of the block +func GetTxDAG(block *Block) (TxDAG, error) { + txs := block.Transactions() + if txs.Len() <= 0 { + return nil, fmt.Errorf("no txdag found") + } + // get data from the last tx + return DecodeTxDAGCalldata(txs[txs.Len()-1].Data()) +} + +func TxDependency(d TxDAG, i int) []uint64 { + if d == nil || i < 0 || i >= d.TxCount() { + return []uint64{} + } + dep := d.TxDep(i) + if dep.CheckFlag(ExcludedTxFlag) { + return []uint64{} + } + if dep.CheckFlag(NonDependentRelFlag) { + txs := make([]uint64, 0, d.TxCount()-dep.Count()) + for j := 0; j < i; j++ { + if !dep.Exist(j) && j != i { + txs = append(txs, uint64(j)) + } + } + return txs + } + return dep.TxIndexes +} + +// EmptyTxDAG indicate that execute txs in sequence +// It means no transactions or need timely distribute transaction fees +// it only keep partial serial execution when tx cannot delay the distribution or just execute txs in sequence +type EmptyTxDAG struct { +} + +func NewEmptyTxDAG() TxDAG { + return &EmptyTxDAG{} +} + +func (d *EmptyTxDAG) Type() byte { + return EmptyTxDAGType +} + +func (d *EmptyTxDAG) Inner() interface{} { + return d +} + +func (d *EmptyTxDAG) DelayGasFeeDistribution() bool { + return false +} + +func (d *EmptyTxDAG) TxDep(int) *TxDep { + dep := TxDep{ + TxIndexes: nil, + Flags: new(uint8), + } + dep.SetFlag(NonDependentRelFlag) + return &dep +} + +func (d *EmptyTxDAG) TxCount() int { + return 0 +} + +func (d *EmptyTxDAG) SetTxDep(int, TxDep) error { + return nil +} + +func (d *EmptyTxDAG) String() string { + return "EmptyTxDAG" +} + +// PlainTxDAG indicate how to use the dependency of txs, and delay the distribution of GasFee +type PlainTxDAG struct { + // Tx Dependency List, the list index is equal to TxIndex + TxDeps []TxDep +} + +func (d *PlainTxDAG) Type() byte { + return PlainTxDAGType +} + +func (d *PlainTxDAG) Inner() interface{} { + return d +} + +func (d *PlainTxDAG) DelayGasFeeDistribution() bool { + return true +} + +func (d *PlainTxDAG) TxDep(i int) *TxDep { + return &d.TxDeps[i] +} + +func (d *PlainTxDAG) TxCount() int { + return len(d.TxDeps) +} + +func (d *PlainTxDAG) SetTxDep(i int, dep TxDep) error { + if i < 0 || i > len(d.TxDeps) { + return fmt.Errorf("SetTxDep with wrong index: %d", i) + } + if i < len(d.TxDeps) { + d.TxDeps[i] = dep + return nil + } + d.TxDeps = append(d.TxDeps, dep) + return nil +} + +func NewPlainTxDAG(txLen int) *PlainTxDAG { + return &PlainTxDAG{ + TxDeps: make([]TxDep, txLen), + } +} + +func (d *PlainTxDAG) String() string { + builder := strings.Builder{} + for _, txDep := range d.TxDeps { + if txDep.Flags != nil { + builder.WriteString(fmt.Sprintf("%v|%v\n", txDep.TxIndexes, *txDep.Flags)) + continue + } + builder.WriteString(fmt.Sprintf("%v\n", txDep.TxIndexes)) + } + return builder.String() +} + +func (d *PlainTxDAG) Size() int { + enc, err := EncodeTxDAG(d) + if err != nil { + return 0 + } + return len(enc) +} + +// MergeTxDAGExecutionPaths will merge duplicate tx path for scheduling parallel. +// Any tx cannot exist in >= 2 paths. +func MergeTxDAGExecutionPaths(d TxDAG, from, to uint64) ([][]uint64, error) { + if from > to || to >= uint64(d.TxCount()) { + return nil, fmt.Errorf("input wrong from: %v, to: %v, txCnt:%v", from, to, d.TxCount()) + } + mergeMap := make(map[uint64][]uint64, d.TxCount()) + txMap := make(map[uint64]uint64, d.TxCount()) + for i := int(to); i >= int(from); i-- { + index, merge := uint64(i), uint64(i) + deps := TxDependency(d, i) + // drop the out range txs + deps = depExcludeTxRange(deps, from, to) + if oldIdx, exist := findTxPathIndex(deps, index, txMap); exist { + merge = oldIdx + } + for _, tx := range deps { + txMap[tx] = merge + } + txMap[index] = merge + } + + // result by index order + for f, t := range txMap { + if mergeMap[t] == nil { + mergeMap[t] = make([]uint64, 0) + } + if f < from || f > to { + continue + } + mergeMap[t] = append(mergeMap[t], f) + } + mergePaths := make([][]uint64, 0, len(mergeMap)) + for i := from; i <= to; i++ { + path, ok := mergeMap[i] + if !ok { + continue + } + slices.Sort(path) + mergePaths = append(mergePaths, path) + } + + return mergePaths, nil +} + +// depExcludeTxRange drop all from~to items, and deps is ordered. +func depExcludeTxRange(deps []uint64, from uint64, to uint64) []uint64 { + if len(deps) == 0 { + return deps + } + start, end := 0, len(deps)-1 + for start < len(deps) && deps[start] < from { + start++ + } + for end >= 0 && deps[end] > to { + end-- + } + if start > end { + return nil + } + return deps[start : end+1] +} + +func findTxPathIndex(path []uint64, cur uint64, txMap map[uint64]uint64) (uint64, bool) { + if old, ok := txMap[cur]; ok { + return old, true + } + + for _, index := range path { + if old, ok := txMap[index]; ok { + return old, true + } + } + + return 0, false +} + +// travelTxDAGExecutionPaths will print all tx execution path +func travelTxDAGExecutionPaths(d TxDAG) [][]uint64 { + exePaths := make([][]uint64, 0) + // travel tx deps with BFS + for i := uint64(0); i < uint64(d.TxCount()); i++ { + exePaths = append(exePaths, travelTxDAGTargetPath(d, i)) + } + return exePaths +} + +// TxDep store the current tx dependency relation with other txs +type TxDep struct { + TxIndexes []uint64 + // Flags may has multi flag meaning, ref NonDependentRelFlag, ExcludedTxFlag. + Flags *uint8 `rlp:"optional"` +} + +func NewTxDep(indexes []uint64, flags ...uint8) TxDep { + dep := TxDep{ + TxIndexes: indexes, + } + if len(flags) == 0 { + return dep + } + dep.Flags = new(uint8) + for _, flag := range flags { + dep.SetFlag(flag) + } + return dep +} + +func (d *TxDep) AppendDep(i int) { + d.TxIndexes = append(d.TxIndexes, uint64(i)) +} + +func (d *TxDep) Exist(i int) bool { + for _, index := range d.TxIndexes { + if index == uint64(i) { + return true + } + } + + return false +} + +func (d *TxDep) Count() int { + return len(d.TxIndexes) +} + +func (d *TxDep) Last() int { + if d.Count() == 0 { + return -1 + } + return int(d.TxIndexes[len(d.TxIndexes)-1]) +} + +func (d *TxDep) CheckFlag(flag uint8) bool { + var flags uint8 + if d.Flags != nil { + flags = *d.Flags + } + return flags&flag == flag +} + +func (d *TxDep) SetFlag(flag uint8) { + if d.Flags == nil { + d.Flags = new(uint8) + } + *d.Flags |= flag +} + +func (d *TxDep) ClearFlag(flag uint8) { + if d.Flags == nil { + return + } + *d.Flags &= ^flag +} + +var ( + longestTimeTimer = metrics.NewRegisteredTimer("dag/longesttime", nil) + longestGasTimer = metrics.NewRegisteredTimer("dag/longestgas", nil) + serialTimeTimer = metrics.NewRegisteredTimer("dag/serialtime", nil) + totalTxMeter = metrics.NewRegisteredMeter("dag/txcnt", nil) + totalNoDepMeter = metrics.NewRegisteredMeter("dag/nodepcnt", nil) + total2DepMeter = metrics.NewRegisteredMeter("dag/2depcnt", nil) + total4DepMeter = metrics.NewRegisteredMeter("dag/4depcnt", nil) + total8DepMeter = metrics.NewRegisteredMeter("dag/8depcnt", nil) + total16DepMeter = metrics.NewRegisteredMeter("dag/16depcnt", nil) + total32DepMeter = metrics.NewRegisteredMeter("dag/32depcnt", nil) +) + +func EvaluateTxDAGPerformance(dag TxDAG, stats map[int]*ExeStat) { + if len(stats) != dag.TxCount() || dag.TxCount() == 0 { + return + } + paths := travelTxDAGExecutionPaths(dag) + // Attention: this is based on best schedule, it will reduce a lot by executing previous txs in parallel + // It assumes that there is no parallel thread limit + txCount := dag.TxCount() + var ( + maxGasIndex int + maxGas uint64 + maxTimeIndex int + maxTime time.Duration + txTimes = make([]time.Duration, txCount) + txGases = make([]uint64, txCount) + txReads = make([]int, txCount) + noDepCnt int + ) + + totalTxMeter.Mark(int64(txCount)) + for i, path := range paths { + if stats[i].excludedTx { + continue + } + if len(path) <= 1 { + noDepCnt++ + totalNoDepMeter.Mark(1) + } + if len(path) <= 3 { + total2DepMeter.Mark(1) + } + if len(path) <= 5 { + total4DepMeter.Mark(1) + } + if len(path) <= 9 { + total8DepMeter.Mark(1) + } + if len(path) <= 17 { + total16DepMeter.Mark(1) + } + if len(path) <= 33 { + total32DepMeter.Mark(1) + } + + // find the biggest cost time from dependency txs + for j := 0; j < len(path)-1; j++ { + prev := path[j] + if txTimes[prev] > txTimes[i] { + txTimes[i] = txTimes[prev] + } + if txGases[prev] > txGases[i] { + txGases[i] = txGases[prev] + } + if txReads[prev] > txReads[i] { + txReads[i] = txReads[prev] + } + } + txTimes[i] += stats[i].costTime + txGases[i] += stats[i].usedGas + txReads[i] += stats[i].readCount + + // try to find max gas + if txGases[i] > maxGas { + maxGas = txGases[i] + maxGasIndex = i + } + if txTimes[i] > maxTime { + maxTime = txTimes[i] + maxTimeIndex = i + } + } + + longestTimeTimer.Update(txTimes[maxTimeIndex]) + longestGasTimer.Update(txTimes[maxGasIndex]) + // serial path + var ( + sTime time.Duration + sGas uint64 + sRead int + sPath []int + ) + for i, stat := range stats { + if stat.excludedTx { + continue + } + sPath = append(sPath, i) + sTime += stat.costTime + sGas += stat.usedGas + sRead += stat.readCount + } + serialTimeTimer.Update(sTime) +} + +// travelTxDAGTargetPath will print target execution path +func travelTxDAGTargetPath(d TxDAG, from uint64) []uint64 { + var ( + queue []uint64 + path []uint64 + ) + + queue = append(queue, from) + path = append(path, from) + for len(queue) > 0 { + var next []uint64 + for _, i := range queue { + for _, dep := range TxDependency(d, int(i)) { + if !slices.Contains(path, dep) { + path = append(path, dep) + next = append(next, dep) + } + } + } + queue = next + } + slices.Sort(path) + return path +} + +// ExeStat records tx execution info +type ExeStat struct { + txIndex int + usedGas uint64 + readCount int + startTime time.Time + costTime time.Duration + + // some flags + excludedTx bool +} + +func NewExeStat(txIndex int) *ExeStat { + return &ExeStat{ + txIndex: txIndex, + } +} + +func (s *ExeStat) Begin() *ExeStat { + s.startTime = time.Now() + return s +} + +func (s *ExeStat) Done() *ExeStat { + s.costTime = time.Since(s.startTime) + return s +} + +func (s *ExeStat) WithExcludedTxFlag() *ExeStat { + s.excludedTx = true + return s +} + +func (s *ExeStat) WithGas(gas uint64) *ExeStat { + s.usedGas = gas + return s +} + +func (s *ExeStat) WithRead(rc int) *ExeStat { + s.readCount = rc + return s +} diff --git a/core/types/dag_test.go b/core/types/dag_test.go new file mode 100644 index 0000000000..cc50f2e5db --- /dev/null +++ b/core/types/dag_test.go @@ -0,0 +1,421 @@ +package types + +import ( + "encoding/hex" + "testing" + "time" + + "github.com/golang/snappy" + + "github.com/cometbft/cometbft/libs/rand" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + mockAddr = common.HexToAddress("0x482bA86399ab6Dcbe54071f8d22258688B4509b1") + mockHash = common.HexToHash("0xdc13f8d7bdb8ec4de02cd4a50a1aa2ab73ec8814e0cdb550341623be3dd8ab7a") +) + +func TestEncodeTxDAGCalldata(t *testing.T) { + tg := mockSimpleDAG() + data, err := EncodeTxDAGCalldata(tg) + assert.Equal(t, nil, err) + tg, err = DecodeTxDAGCalldata(data) + assert.Equal(t, nil, err) + assert.Equal(t, true, tg.TxCount() > 0) + + _, err = DecodeTxDAGCalldata(nil) + assert.NotEqual(t, nil, err) +} + +func TestTxDAG_SetTxDep(t *testing.T) { + dag := mockSimpleDAG() + require.NoError(t, dag.SetTxDep(9, NewTxDep(nil, NonDependentRelFlag))) + require.NoError(t, dag.SetTxDep(10, NewTxDep(nil, NonDependentRelFlag))) + require.Error(t, dag.SetTxDep(12, NewTxDep(nil, NonDependentRelFlag))) + dag = NewEmptyTxDAG() + require.NoError(t, dag.SetTxDep(0, NewTxDep(nil, NonDependentRelFlag))) + require.NoError(t, dag.SetTxDep(11, NewTxDep(nil, NonDependentRelFlag))) +} + +func TestTxDAG(t *testing.T) { + dag := mockSimpleDAG() + t.Log(dag) + dag = mockSystemTxDAG() + t.Log(dag) +} + +func TestEvaluateTxDAG(t *testing.T) { + dag := mockSystemTxDAG() + stats := make(map[int]*ExeStat, dag.TxCount()) + for i := 0; i < dag.TxCount(); i++ { + stats[i] = NewExeStat(i).WithGas(uint64(i)).WithRead(i) + stats[i].costTime = time.Duration(i) + txDep := dag.TxDep(i) + if txDep.CheckFlag(NonDependentRelFlag) { + stats[i].WithExcludedTxFlag() + } + } + EvaluateTxDAGPerformance(dag, stats) +} + +func TestMergeTxDAGExecutionPaths_Simple(t *testing.T) { + tests := []struct { + d TxDAG + from uint64 + to uint64 + expect [][]uint64 + }{ + { + d: mockSimpleDAG(), + from: 0, + to: 9, + expect: [][]uint64{ + {0, 3, 4}, + {1, 2, 5, 6, 7}, + {8, 9}, + }, + }, + { + d: mockSimpleDAG(), + from: 1, + to: 1, + expect: [][]uint64{ + {1}, + }, + }, + { + d: mockSimpleDAGWithLargeDeps(), + from: 0, + to: 9, + expect: [][]uint64{ + {5, 6}, + {0, 1, 2, 3, 4, 7, 8, 9}, + }, + }, + { + d: mockSystemTxDAGWithLargeDeps(), + from: 0, + to: 11, + expect: [][]uint64{ + {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, + {10}, + {11}, + }, + }, + { + d: mockSimpleDAGWithLargeDeps(), + from: 5, + to: 8, + expect: [][]uint64{ + {5, 6}, + {7}, + {8}, + }, + }, + { + d: mockSimpleDAGWithLargeDeps(), + from: 5, + to: 9, + expect: [][]uint64{ + {5, 6}, + {7}, + {8, 9}, + }, + }, + } + for i, item := range tests { + paths, err := MergeTxDAGExecutionPaths(item.d, item.from, item.to) + require.NoError(t, err) + require.Equal(t, item.expect, paths, i) + } +} + +func TestMergeTxDAGExecutionPaths_Random(t *testing.T) { + dag := mockRandomDAG(10000) + paths, _ := MergeTxDAGExecutionPaths(dag, 0, uint64(dag.TxCount()-1)) + txMap := make(map[uint64]uint64, dag.TxCount()) + for _, path := range paths { + for _, index := range path { + old, ok := txMap[index] + require.False(t, ok, index, path, old) + txMap[index] = path[0] + } + } + require.Equal(t, dag.TxCount(), len(txMap)) +} + +func TestTxDAG_Compression(t *testing.T) { + dag := mockRandomDAG(10000) + enc, err := EncodeTxDAG(dag) + require.NoError(t, err) + encoded := snappy.Encode(nil, enc) + t.Log("enc", len(enc), "compressed", len(encoded), "ratio", 1-(float64(len(encoded))/float64(len(enc)))) +} + +func BenchmarkMergeTxDAGExecutionPaths(b *testing.B) { + dag := mockRandomDAG(100000) + for i := 0; i < b.N; i++ { + MergeTxDAGExecutionPaths(dag, 0, uint64(dag.TxCount()-1)) + } +} + +func BenchmarkTxDAG_Encode(b *testing.B) { + dag := mockRandomDAG(10000) + for i := 0; i < b.N; i++ { + EncodeTxDAG(dag) + } +} + +func BenchmarkTxDAG_Decode(b *testing.B) { + dag := mockRandomDAG(10000) + enc, _ := EncodeTxDAG(dag) + for i := 0; i < b.N; i++ { + DecodeTxDAG(enc) + } +} + +func mockSimpleDAG() TxDAG { + dag := NewPlainTxDAG(10) + dag.TxDeps[0].TxIndexes = []uint64{} + dag.TxDeps[1].TxIndexes = []uint64{} + dag.TxDeps[2].TxIndexes = []uint64{} + dag.TxDeps[3].TxIndexes = []uint64{0} + dag.TxDeps[4].TxIndexes = []uint64{0} + dag.TxDeps[5].TxIndexes = []uint64{1, 2} + dag.TxDeps[6].TxIndexes = []uint64{5} + dag.TxDeps[7].TxIndexes = []uint64{6} + dag.TxDeps[8].TxIndexes = []uint64{} + dag.TxDeps[9].TxIndexes = []uint64{8} + return dag +} + +func mockSimpleDAGWithLargeDeps() TxDAG { + dag := NewPlainTxDAG(10) + dag.TxDeps[0].TxIndexes = []uint64{} + dag.TxDeps[1].TxIndexes = []uint64{} + dag.TxDeps[2].TxIndexes = []uint64{} + dag.TxDeps[3].TxIndexes = []uint64{0} + dag.TxDeps[4].TxIndexes = []uint64{0} + dag.TxDeps[5].TxIndexes = []uint64{} + dag.TxDeps[6].TxIndexes = []uint64{5} + dag.TxDeps[7].TxIndexes = []uint64{2, 4} + dag.TxDeps[8].TxIndexes = []uint64{} + //dag.TxDeps[9].TxIndexes = []uint64{0, 1, 3, 4, 8} + dag.TxDeps[9] = NewTxDep([]uint64{2, 5, 6, 7}, NonDependentRelFlag) + return dag +} + +func mockRandomDAG(txLen int) TxDAG { + dag := NewPlainTxDAG(txLen) + for i := 0; i < txLen; i++ { + deps := make([]uint64, 0) + if i == 0 || rand.Bool() { + dag.TxDeps[i].TxIndexes = deps + continue + } + depCnt := rand.Int()%i + 1 + for j := 0; j < depCnt; j++ { + var dep uint64 + if j > 0 && deps[j-1]+1 == uint64(i) { + break + } + if j > 0 { + dep = uint64(rand.Int())%(uint64(i)-deps[j-1]-1) + deps[j-1] + 1 + } else { + dep = uint64(rand.Int() % i) + } + deps = append(deps, dep) + } + dag.TxDeps[i].TxIndexes = deps + } + return dag +} + +func mockSystemTxDAG() TxDAG { + dag := NewPlainTxDAG(12) + dag.TxDeps[0].TxIndexes = []uint64{} + dag.TxDeps[1].TxIndexes = []uint64{} + dag.TxDeps[2].TxIndexes = []uint64{} + dag.TxDeps[3].TxIndexes = []uint64{0} + dag.TxDeps[4].TxIndexes = []uint64{0} + dag.TxDeps[5].TxIndexes = []uint64{1, 2} + dag.TxDeps[6].TxIndexes = []uint64{5} + dag.TxDeps[7].TxIndexes = []uint64{6} + dag.TxDeps[8].TxIndexes = []uint64{} + dag.TxDeps[9].TxIndexes = []uint64{8} + dag.TxDeps[10] = NewTxDep([]uint64{}, ExcludedTxFlag) + dag.TxDeps[11] = NewTxDep([]uint64{}, ExcludedTxFlag) + return dag +} + +func mockSystemTxDAG2() TxDAG { + dag := NewPlainTxDAG(12) + dag.TxDeps[0] = NewTxDep([]uint64{}) + dag.TxDeps[1] = NewTxDep([]uint64{}) + dag.TxDeps[2] = NewTxDep([]uint64{}) + dag.TxDeps[3] = NewTxDep([]uint64{0}) + dag.TxDeps[4] = NewTxDep([]uint64{0}) + dag.TxDeps[5] = NewTxDep([]uint64{1, 2}) + dag.TxDeps[6] = NewTxDep([]uint64{5}) + dag.TxDeps[7] = NewTxDep([]uint64{6}) + dag.TxDeps[8] = NewTxDep([]uint64{}) + dag.TxDeps[9] = NewTxDep([]uint64{8}) + dag.TxDeps[10] = NewTxDep([]uint64{}, NonDependentRelFlag) + dag.TxDeps[11] = NewTxDep([]uint64{}, NonDependentRelFlag) + return dag +} + +func mockSystemTxDAGWithLargeDeps() TxDAG { + dag := NewPlainTxDAG(12) + dag.TxDeps[0].TxIndexes = []uint64{} + dag.TxDeps[1].TxIndexes = []uint64{} + dag.TxDeps[2].TxIndexes = []uint64{} + dag.TxDeps[3].TxIndexes = []uint64{0} + dag.TxDeps[4].TxIndexes = []uint64{0} + dag.TxDeps[5].TxIndexes = []uint64{1, 2} + dag.TxDeps[6].TxIndexes = []uint64{5} + dag.TxDeps[7].TxIndexes = []uint64{3} + dag.TxDeps[8].TxIndexes = []uint64{} + //dag.TxDeps[9].TxIndexes = []uint64{0, 1, 2, 6, 7, 8} + dag.TxDeps[9] = NewTxDep([]uint64{3, 4, 5, 10, 11}, NonDependentRelFlag) + dag.TxDeps[10] = NewTxDep([]uint64{}, ExcludedTxFlag) + dag.TxDeps[11] = NewTxDep([]uint64{}, ExcludedTxFlag) + return dag +} + +func TestTxDAG_Encode_Decode(t *testing.T) { + tests := []struct { + expect TxDAG + }{ + { + expect: TxDAG(&EmptyTxDAG{}), + }, + { + expect: mockSimpleDAG(), + }, + { + expect: mockRandomDAG(100), + }, + { + expect: mockSystemTxDAG(), + }, + { + expect: mockSystemTxDAG2(), + }, + { + expect: mockSystemTxDAGWithLargeDeps(), + }, + } + for i, item := range tests { + enc, err := EncodeTxDAG(item.expect) + t.Log(hex.EncodeToString(enc)) + require.NoError(t, err, i) + actual, err := DecodeTxDAG(enc) + require.NoError(t, err, i) + require.Equal(t, item.expect, actual, i) + if i%2 == 0 { + enc[0] = 2 + _, err = DecodeTxDAG(enc) + require.Error(t, err) + } + } +} + +func TestDecodeTxDAG(t *testing.T) { + tests := []struct { + enc string + err bool + }{ + {"00c0", false}, + {"01dddcc1c0c1c0c1c0c2c180c2c180c3c20102c3c20205c2c106c1c0c2c108", false}, + {"01e3e2c1c0c1c0c1c0c2c180c2c180c3c20102c3c20205c2c106c1c0c2c108c2c001c2c001", false}, + {"0132e212", true}, + {"01dfdec280c0c280c0c380c101c380c102c380c103c380c104c380c105c380c106", true}, + {"01cdccc280c0c280c0c280c0c280c0", true}, + } + for i, item := range tests { + enc, err := hex.DecodeString(item.enc) + require.NoError(t, err, i) + txDAG, err := DecodeTxDAG(enc) + if item.err { + require.Error(t, err, i) + continue + } + require.NoError(t, err, i) + t.Log(txDAG) + } +} + +func TestTxDep_Flags(t *testing.T) { + dep := NewTxDep(nil) + dep.ClearFlag(NonDependentRelFlag) + dep.SetFlag(NonDependentRelFlag) + dep.SetFlag(ExcludedTxFlag) + compared := NewTxDep(nil, NonDependentRelFlag, ExcludedTxFlag) + require.Equal(t, dep, compared) + require.Equal(t, NonDependentRelFlag|ExcludedTxFlag, *dep.Flags) + dep.ClearFlag(ExcludedTxFlag) + require.Equal(t, NonDependentRelFlag, *dep.Flags) + require.True(t, dep.CheckFlag(NonDependentRelFlag)) + require.False(t, dep.CheckFlag(ExcludedTxFlag)) +} + +func TestDepExcludeTxRange(t *testing.T) { + tests := []struct { + src []uint64 + from uint64 + to uint64 + expect []uint64 + }{ + { + src: nil, + from: 0, + to: 4, + expect: nil, + }, + { + src: []uint64{}, + from: 0, + to: 4, + expect: []uint64{}, + }, + { + src: []uint64{0, 1, 2, 3, 4}, + from: 4, + to: 4, + expect: []uint64{4}, + }, + { + src: []uint64{0, 1, 2, 3, 4}, + from: 1, + to: 3, + expect: []uint64{1, 2, 3}, + }, + { + src: []uint64{0, 1, 2, 3, 4}, + from: 5, + to: 6, + expect: nil, + }, + { + src: []uint64{2, 3, 4}, + from: 0, + to: 1, + expect: nil, + }, + { + src: []uint64{0, 1, 2, 3, 4}, + from: 0, + to: 4, + expect: []uint64{0, 1, 2, 3, 4}, + }, + } + + for i, item := range tests { + require.Equal(t, item.expect, depExcludeTxRange(item.src, item.from, item.to), i) + } +} diff --git a/core/types/mvstates.go b/core/types/mvstates.go new file mode 100644 index 0000000000..4637b71d2f --- /dev/null +++ b/core/types/mvstates.go @@ -0,0 +1,661 @@ +package types + +import ( + "encoding/hex" + "errors" + "fmt" + "strings" + "sync" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/log" + "github.com/holiman/uint256" + "golang.org/x/exp/slices" +) + +const ( + AccountStatePrefix = 'a' + StorageStatePrefix = 's' +) + +type RWKey [1 + common.AddressLength + common.HashLength]byte + +type AccountState byte + +const ( + AccountSelf AccountState = iota + AccountNonce + AccountBalance + AccountCodeHash + AccountSuicide +) + +const ( + asyncDepGenChanSize = 10000 +) + +func AccountStateKey(account common.Address, state AccountState) RWKey { + var key RWKey + key[0] = AccountStatePrefix + copy(key[1:], account.Bytes()) + key[1+common.AddressLength] = byte(state) + return key +} + +func StorageStateKey(account common.Address, state common.Hash) RWKey { + var key RWKey + key[0] = StorageStatePrefix + copy(key[1:], account.Bytes()) + copy(key[1+common.AddressLength:], state.Bytes()) + return key +} + +func (key *RWKey) IsAccountState() (bool, AccountState) { + return AccountStatePrefix == key[0], AccountState(key[1+common.AddressLength]) +} + +func (key *RWKey) IsAccountSelf() bool { + ok, s := key.IsAccountState() + if !ok { + return false + } + return s == AccountSelf +} + +func (key *RWKey) IsAccountSuicide() bool { + ok, s := key.IsAccountState() + if !ok { + return false + } + return s == AccountSuicide +} + +func (key *RWKey) ToAccountSelf() RWKey { + return AccountStateKey(key.Addr(), AccountSelf) +} + +func (key *RWKey) IsStorageState() bool { + return StorageStatePrefix == key[0] +} + +func (key *RWKey) String() string { + return hex.EncodeToString(key[:]) +} + +func (key *RWKey) Addr() common.Address { + return common.BytesToAddress(key[1 : 1+common.AddressLength]) +} + +// StateVersion record specific TxIndex & TxIncarnation +// if TxIndex equals to -1, it means the state read from DB. +type StateVersion struct { + TxIndex int + // Tx incarnation used for multi ver state + TxIncarnation int +} + +// RWSet record all read & write set in txs +// Attention: this is not a concurrent safety structure +type RWSet struct { + ver StateVersion + readSet map[RWKey]*RWItem + writeSet map[RWKey]*RWItem + + // some flags + rwRecordDone bool + excludedTx bool +} + +func NewRWSet(ver StateVersion) *RWSet { + return &RWSet{ + ver: ver, + readSet: make(map[RWKey]*RWItem, 64), + writeSet: make(map[RWKey]*RWItem, 32), + } +} + +func (s *RWSet) RecordRead(key RWKey, ver StateVersion, val interface{}) { + // only record the first read version + if _, exist := s.readSet[key]; exist { + return + } + s.readSet[key] = &RWItem{ + Ver: ver, + Val: val, + } +} + +func (s *RWSet) RecordWrite(key RWKey, val interface{}) { + wr, exist := s.writeSet[key] + if !exist { + s.writeSet[key] = &RWItem{ + Ver: s.ver, + Val: val, + } + return + } + wr.Val = val +} + +func (s *RWSet) Version() StateVersion { + return s.ver +} + +func (s *RWSet) ReadSet() map[RWKey]*RWItem { + return s.readSet +} + +func (s *RWSet) WriteSet() map[RWKey]*RWItem { + return s.writeSet +} + +func (s *RWSet) WithExcludedTxFlag() *RWSet { + s.excludedTx = true + return s +} + +func (s *RWSet) String() string { + builder := strings.Builder{} + builder.WriteString(fmt.Sprintf("tx: %v, inc: %v\nreadSet: [", s.ver.TxIndex, s.ver.TxIncarnation)) + i := 0 + for key, _ := range s.readSet { + if i > 0 { + builder.WriteString(fmt.Sprintf(", %v", key.String())) + continue + } + builder.WriteString(fmt.Sprintf("%v", key.String())) + i++ + } + builder.WriteString("]\nwriteSet: [") + i = 0 + for key, _ := range s.writeSet { + if i > 0 { + builder.WriteString(fmt.Sprintf(", %v", key.String())) + continue + } + builder.WriteString(fmt.Sprintf("%v", key.String())) + i++ + } + builder.WriteString("]\n") + return builder.String() +} + +// isEqualRWVal compare state +func isEqualRWVal(key RWKey, src interface{}, compared interface{}) bool { + if ok, state := key.IsAccountState(); ok { + switch state { + case AccountBalance: + if src != nil && compared != nil { + return equalUint256(src.(*uint256.Int), compared.(*uint256.Int)) + } + return src == compared + case AccountNonce: + return src.(uint64) == compared.(uint64) + case AccountCodeHash: + if src != nil && compared != nil { + return slices.Equal(src.([]byte), compared.([]byte)) + } + return src == compared + } + return false + } + + if src != nil && compared != nil { + return src.(common.Hash) == compared.(common.Hash) + } + return src == compared +} + +func equalUint256(s, c *uint256.Int) bool { + if s != nil && c != nil { + return s.Eq(c) + } + + return s == c +} + +type RWItem struct { + Ver StateVersion + Val interface{} +} + +func NewRWItem(ver StateVersion, val interface{}) *RWItem { + return &RWItem{ + Ver: ver, + Val: val, + } +} + +func (w *RWItem) TxIndex() int { + return w.Ver.TxIndex +} + +func (w *RWItem) TxIncarnation() int { + return w.Ver.TxIncarnation +} + +type PendingWrites struct { + list []*RWItem +} + +func NewPendingWrites() *PendingWrites { + return &PendingWrites{ + list: make([]*RWItem, 0, 8), + } +} + +func (w *PendingWrites) Append(pw *RWItem) { + if i, found := w.SearchTxIndex(pw.TxIndex()); found { + w.list[i] = pw + return + } + + w.list = append(w.list, pw) + for i := len(w.list) - 1; i > 0; i-- { + if w.list[i].TxIndex() > w.list[i-1].TxIndex() { + break + } + w.list[i-1], w.list[i] = w.list[i], w.list[i-1] + } +} + +func (w *PendingWrites) SearchTxIndex(txIndex int) (int, bool) { + n := len(w.list) + i, j := 0, n + for i < j { + h := int(uint(i+j) >> 1) + // i ≤ h < j + if w.list[h].TxIndex() < txIndex { + i = h + 1 + } else { + j = h + } + } + return i, i < n && w.list[i].TxIndex() == txIndex +} + +func (w *PendingWrites) FindLastWrite(txIndex int) *RWItem { + var i, _ = w.SearchTxIndex(txIndex) + for j := i - 1; j >= 0; j-- { + if w.list[j].TxIndex() < txIndex { + return w.list[j] + } + } + + return nil +} + +func (w *PendingWrites) FindPrevWrites(txIndex int) []*RWItem { + var i, _ = w.SearchTxIndex(txIndex) + for j := i - 1; j >= 0; j-- { + if w.list[j].TxIndex() < txIndex { + return w.list[:j+1] + } + } + + return nil +} + +type MVStates struct { + rwSets map[int]*RWSet + pendingWriteSet map[RWKey]*PendingWrites + nextFinaliseIndex int + + // dependency map cache for generating TxDAG + // depMapCache[i].exist(j) means j->i, and i > j + depMapCache map[int]TxDepMap + depsCache map[int][]uint64 + + // async dep analysis + depsGenChan chan int + stopChan chan struct{} + asyncRunning bool + + // execution stat infos + stats map[int]*ExeStat + lock sync.RWMutex +} + +func NewMVStates(txCount int) *MVStates { + return &MVStates{ + rwSets: make(map[int]*RWSet, txCount), + pendingWriteSet: make(map[RWKey]*PendingWrites, txCount*8), + depMapCache: make(map[int]TxDepMap, txCount), + depsCache: make(map[int][]uint64, txCount), + stats: make(map[int]*ExeStat, txCount), + } +} + +func (s *MVStates) EnableAsyncDepGen() *MVStates { + s.lock.Lock() + defer s.lock.Unlock() + s.depsGenChan = make(chan int, asyncDepGenChanSize) + s.stopChan = make(chan struct{}) + s.asyncRunning = true + go s.asyncDepGenLoop() + return s +} + +func (s *MVStates) Stop() error { + s.lock.Lock() + defer s.lock.Unlock() + s.stopAsyncDepGen() + return nil +} + +func (s *MVStates) stopAsyncDepGen() { + if !s.asyncRunning { + return + } + s.asyncRunning = false + if s.stopChan != nil { + close(s.stopChan) + } +} + +func (s *MVStates) asyncDepGenLoop() { + timeout := time.After(3 * time.Second) + for { + select { + case tx := <-s.depsGenChan: + s.lock.Lock() + s.resolveDepsCacheByWrites(tx, s.rwSets[tx]) + s.lock.Unlock() + case <-s.stopChan: + return + case <-timeout: + log.Warn("asyncDepGenLoop exit by timeout") + return + } + } +} + +func (s *MVStates) RWSets() map[int]*RWSet { + s.lock.RLock() + defer s.lock.RUnlock() + return s.rwSets +} + +func (s *MVStates) Stats() map[int]*ExeStat { + s.lock.RLock() + defer s.lock.RUnlock() + return s.stats +} + +func (s *MVStates) RWSet(index int) *RWSet { + s.lock.RLock() + defer s.lock.RUnlock() + if index >= len(s.rwSets) { + return nil + } + return s.rwSets[index] +} + +// ReadState read state from MVStates +func (s *MVStates) ReadState(txIndex int, key RWKey) *RWItem { + s.lock.RLock() + defer s.lock.RUnlock() + + wset, ok := s.pendingWriteSet[key] + if !ok { + return nil + } + return wset.FindLastWrite(txIndex) +} + +// FulfillRWSet it can execute as async, and rwSet & stat must guarantee read-only +// try to generate TxDAG, when fulfill RWSet +func (s *MVStates) FulfillRWSet(rwSet *RWSet, stat *ExeStat) error { + log.Debug("FulfillRWSet", "total", len(s.rwSets), "cur", rwSet.ver.TxIndex, "reads", len(rwSet.readSet), "writes", len(rwSet.writeSet)) + s.lock.Lock() + defer s.lock.Unlock() + index := rwSet.ver.TxIndex + if index < s.nextFinaliseIndex { + return errors.New("fulfill a finalized RWSet") + } + if stat != nil { + if stat.txIndex != index { + return errors.New("wrong execution stat") + } + s.stats[index] = stat + } + + //if metrics.EnabledExpensive { + // for k := range rwSet.writeSet { + // // this action is only for testing, it runs when enable expensive metrics. + // checkRWSetInconsistent(index, k, rwSet.readSet, rwSet.writeSet) + // } + //} + s.rwSets[index] = rwSet + return nil +} + +// Finalise it will put target write set into pending writes. +func (s *MVStates) Finalise(index int) error { + log.Debug("Finalise", "total", len(s.rwSets), "index", index) + s.lock.Lock() + + rwSet := s.rwSets[index] + if rwSet == nil { + s.lock.Unlock() + return fmt.Errorf("finalise a non-exist RWSet, index: %d", index) + } + + if index != s.nextFinaliseIndex { + s.lock.Unlock() + return fmt.Errorf("finalise in wrong order, next: %d, input: %d", s.nextFinaliseIndex, index) + } + + // append to pending write set + for k, v := range rwSet.writeSet { + if _, exist := s.pendingWriteSet[k]; !exist { + s.pendingWriteSet[k] = NewPendingWrites() + } + s.pendingWriteSet[k].Append(v) + } + s.nextFinaliseIndex++ + s.lock.Unlock() + // async resolve dependency, but non-block action + if s.asyncRunning && s.depsGenChan != nil { + s.depsGenChan <- index + } + return nil +} + +func (s *MVStates) resolveDepsCacheByWrites(index int, rwSet *RWSet) { + // analysis dep, if the previous transaction is not executed/validated, re-analysis is required + s.depMapCache[index] = NewTxDeps(8) + if rwSet.excludedTx { + return + } + seen := make(map[int]struct{}, 8) + // check tx dependency, only check key, skip version + if len(s.pendingWriteSet) > len(rwSet.readSet) { + for key := range rwSet.readSet { + // check self destruct + if key.IsAccountSelf() { + key = AccountStateKey(key.Addr(), AccountSuicide) + } + writes := s.pendingWriteSet[key] + if writes == nil { + continue + } + items := writes.FindPrevWrites(index) + for _, item := range items { + seen[item.TxIndex()] = struct{}{} + } + } + } else { + for k, w := range s.pendingWriteSet { + // check suicide, add read address flag, it only for check suicide quickly, and cannot for other scenarios. + if k.IsAccountSuicide() { + k = k.ToAccountSelf() + } + if _, ok := rwSet.readSet[k]; !ok { + continue + } + items := w.FindPrevWrites(index) + for _, item := range items { + seen[item.TxIndex()] = struct{}{} + } + } + } + for prev := 0; prev < index; prev++ { + if _, ok := seen[prev]; !ok { + continue + } + s.depMapCache[index].add(prev) + // clear redundancy deps compared with prev + for dep := range s.depMapCache[index] { + if s.depMapCache[prev].exist(dep) { + s.depMapCache[index].remove(dep) + } + } + } + s.depsCache[index] = s.depMapCache[index].toArray() +} + +func (s *MVStates) resolveDepsCache(index int, rwSet *RWSet) { + // analysis dep, if the previous transaction is not executed/validated, re-analysis is required + s.depMapCache[index] = NewTxDeps(0) + if rwSet.excludedTx { + return + } + for prev := 0; prev < index; prev++ { + // if there are some parallel execution or system txs, it will fulfill in advance + // it's ok, and try re-generate later + prevSet, ok := s.rwSets[prev] + if !ok { + continue + } + // if prev tx is tagged ExcludedTxFlag, just skip the check + if prevSet.excludedTx { + continue + } + // check if there has written op before i + if checkDependency(prevSet.writeSet, rwSet.readSet) { + s.depMapCache[index].add(prev) + // clear redundancy deps compared with prev + for dep := range s.depMapCache[index] { + if s.depMapCache[prev].exist(dep) { + s.depMapCache[index].remove(dep) + } + } + } + } + s.depsCache[index] = s.depMapCache[index].toArray() +} + +func checkRWSetInconsistent(index int, k RWKey, readSet map[RWKey]*RWItem, writeSet map[RWKey]*RWItem) bool { + var ( + readOk bool + writeOk bool + r *RWItem + ) + + if k.IsAccountSuicide() { + _, readOk = readSet[k.ToAccountSelf()] + } else { + _, readOk = readSet[k] + } + + r, writeOk = writeSet[k] + if readOk != writeOk { + // check if it's correct? read nil, write non-nil + log.Warn("checkRWSetInconsistent find inconsistent", "tx", index, "k", k.String(), "read", readOk, "write", writeOk, "val", r.Val) + return true + } + + return false +} + +// ResolveTxDAG generate TxDAG from RWSets +func (s *MVStates) ResolveTxDAG(txCnt int, gasFeeReceivers []common.Address) (TxDAG, error) { + s.lock.Lock() + defer s.lock.Unlock() + if len(s.rwSets) != txCnt { + return nil, fmt.Errorf("wrong rwSet count, expect: %v, actual: %v", txCnt, len(s.rwSets)) + } + if txCnt != s.nextFinaliseIndex { + return nil, fmt.Errorf("resolve in wrong order, next: %d, input: %d", s.nextFinaliseIndex, txCnt) + } + s.stopAsyncDepGen() + txDAG := NewPlainTxDAG(len(s.rwSets)) + for i := 0; i < txCnt; i++ { + // check if there are RW with gas fee receiver for gas delay calculation + for _, addr := range gasFeeReceivers { + if _, ok := s.rwSets[i].readSet[AccountStateKey(addr, AccountSelf)]; ok { + return NewEmptyTxDAG(), nil + } + } + txDAG.TxDeps[i].TxIndexes = []uint64{} + if s.rwSets[i].excludedTx { + txDAG.TxDeps[i].SetFlag(ExcludedTxFlag) + continue + } + if s.depMapCache[i] == nil { + s.resolveDepsCacheByWrites(i, s.rwSets[i]) + } + deps := s.depsCache[i] + if len(deps) <= (txCnt-1)/2 { + txDAG.TxDeps[i].TxIndexes = deps + continue + } + // if tx deps larger than half of txs, then convert with NonDependentRelFlag + txDAG.TxDeps[i].SetFlag(NonDependentRelFlag) + for j := uint64(0); j < uint64(txCnt); j++ { + if !slices.Contains(deps, j) && j != uint64(i) { + txDAG.TxDeps[i].TxIndexes = append(txDAG.TxDeps[i].TxIndexes, j) + } + } + } + + return txDAG, nil +} + +func checkDependency(writeSet map[RWKey]*RWItem, readSet map[RWKey]*RWItem) bool { + // check tx dependency, only check key, skip version + for k, _ := range writeSet { + // check suicide, add read address flag, it only for check suicide quickly, and cannot for other scenarios. + if k.IsAccountSuicide() { + if _, ok := readSet[k.ToAccountSelf()]; ok { + return true + } + continue + } + if _, ok := readSet[k]; ok { + return true + } + } + + return false +} + +type TxDepMap map[int]struct{} + +func NewTxDeps(cap int) TxDepMap { + return make(map[int]struct{}, cap) +} + +func (m TxDepMap) add(index int) { + m[index] = struct{}{} +} + +func (m TxDepMap) exist(index int) bool { + _, ok := m[index] + return ok +} + +func (m TxDepMap) toArray() []uint64 { + ret := make([]uint64, 0, len(m)) + for index := range m { + ret = append(ret, uint64(index)) + } + slices.Sort(ret) + return ret +} + +func (m TxDepMap) remove(index int) { + delete(m, index) +} diff --git a/core/types/mvstates_test.go b/core/types/mvstates_test.go new file mode 100644 index 0000000000..7a0e16db8c --- /dev/null +++ b/core/types/mvstates_test.go @@ -0,0 +1,459 @@ +package types + +import ( + "bytes" + "compress/gzip" + "fmt" + "testing" + "time" + + "github.com/cometbft/cometbft/libs/rand" + "github.com/golang/snappy" + + "github.com/holiman/uint256" + "github.com/stretchr/testify/require" +) + +const mockRWSetSize = 5000 + +func TestMVStates_BasicUsage(t *testing.T) { + ms := NewMVStates(0) + require.NoError(t, ms.FulfillRWSet(mockRWSetWithVal(0, []interface{}{"0x00", 0}, []interface{}{"0x00", 0}), nil)) + require.Nil(t, ms.ReadState(0, str2key("0x00"))) + require.NoError(t, ms.Finalise(0)) + require.Error(t, ms.Finalise(0)) + require.Error(t, ms.FulfillRWSet(mockRWSetWithVal(0, nil, nil), nil)) + require.Nil(t, ms.ReadState(0, str2key("0x00"))) + require.Equal(t, NewRWItem(StateVersion{TxIndex: 0}, 0), ms.ReadState(1, str2key("0x00"))) + + require.NoError(t, ms.FulfillRWSet(mockRWSetWithVal(1, []interface{}{"0x01", 1}, []interface{}{"0x01", 1}), nil)) + require.Nil(t, ms.ReadState(1, str2key("0x01"))) + require.NoError(t, ms.Finalise(1)) + require.Nil(t, ms.ReadState(0, str2key("0x01"))) + require.Equal(t, NewRWItem(StateVersion{TxIndex: 1}, 1), ms.ReadState(2, str2key("0x01"))) + + require.NoError(t, ms.FulfillRWSet(mockRWSetWithVal(2, []interface{}{"0x02", 2, "0x01", 1}, []interface{}{"0x01", 2, "0x02", 2}), nil)) + require.NoError(t, ms.Finalise(2)) + require.Equal(t, NewRWItem(StateVersion{TxIndex: 1}, 1), ms.ReadState(2, str2key("0x01"))) + require.Equal(t, NewRWItem(StateVersion{TxIndex: 2}, 2), ms.ReadState(3, str2key("0x01"))) + + require.NoError(t, ms.FulfillRWSet(mockRWSetWithVal(3, []interface{}{"0x03", 3, "0x00", 0, "0x01", 2}, []interface{}{"0x00", 3, "0x01", 3, "0x03", 3}), nil)) + require.Nil(t, ms.ReadState(3, str2key("0x03"))) + require.NoError(t, ms.Finalise(3)) + require.Nil(t, ms.ReadState(0, str2key("0x01"))) + require.Equal(t, NewRWItem(StateVersion{TxIndex: 1}, 1), ms.ReadState(2, str2key("0x01"))) + require.Equal(t, NewRWItem(StateVersion{TxIndex: 2}, 2), ms.ReadState(3, str2key("0x01"))) + require.Equal(t, NewRWItem(StateVersion{TxIndex: 3}, 3), ms.ReadState(4, str2key("0x01"))) + require.Nil(t, ms.ReadState(0, str2key("0x00"))) + require.Equal(t, NewRWItem(StateVersion{TxIndex: 3}, 3), ms.ReadState(5, str2key("0x00"))) +} + +func TestMVStates_SimpleResolveTxDAG(t *testing.T) { + ms := NewMVStates(10) + finaliseRWSets(t, ms, []*RWSet{ + mockRWSet(0, []string{"0x00"}, []string{"0x00"}), + mockRWSet(1, []string{"0x01"}, []string{"0x01"}), + mockRWSet(2, []string{"0x02"}, []string{"0x02"}), + mockRWSet(3, []string{"0x00", "0x03"}, []string{"0x03"}), + mockRWSet(4, []string{"0x00", "0x04"}, []string{"0x04"}), + mockRWSet(5, []string{"0x01", "0x02", "0x05"}, []string{"0x05"}), + mockRWSet(6, []string{"0x02", "0x05", "0x06"}, []string{"0x06"}), + mockRWSet(7, []string{"0x06", "0x07"}, []string{"0x07"}), + mockRWSet(8, []string{"0x08"}, []string{"0x08"}), + mockRWSet(9, []string{"0x08", "0x09"}, []string{"0x09"}), + }) + + dag, err := ms.ResolveTxDAG(10, nil) + require.NoError(t, err) + require.Equal(t, mockSimpleDAG(), dag) + t.Log(dag) +} + +func TestMVStates_AsyncDepGen_SimpleResolveTxDAG(t *testing.T) { + ms := NewMVStates(10).EnableAsyncDepGen() + finaliseRWSets(t, ms, []*RWSet{ + mockRWSet(0, []string{"0x00"}, []string{"0x00"}), + mockRWSet(1, []string{"0x01"}, []string{"0x01"}), + mockRWSet(2, []string{"0x02"}, []string{"0x02"}), + mockRWSet(3, []string{"0x00", "0x03"}, []string{"0x03"}), + mockRWSet(4, []string{"0x00", "0x04"}, []string{"0x04"}), + mockRWSet(5, []string{"0x01", "0x02", "0x05"}, []string{"0x05"}), + mockRWSet(6, []string{"0x02", "0x05", "0x06"}, []string{"0x06"}), + mockRWSet(7, []string{"0x06", "0x07"}, []string{"0x07"}), + mockRWSet(8, []string{"0x08"}, []string{"0x08"}), + mockRWSet(9, []string{"0x08", "0x09"}, []string{"0x09"}), + }) + time.Sleep(10 * time.Millisecond) + + dag, err := ms.ResolveTxDAG(10, nil) + require.NoError(t, err) + time.Sleep(100 * time.Millisecond) + require.NoError(t, ms.Stop()) + require.Equal(t, mockSimpleDAG(), dag) + t.Log(dag) +} + +func TestMVStates_ResolveTxDAG_Async(t *testing.T) { + txCnt := 10000 + rwSets := mockRandomRWSet(txCnt) + ms1 := NewMVStates(txCnt).EnableAsyncDepGen() + for i := 0; i < txCnt; i++ { + require.NoError(t, ms1.FulfillRWSet(rwSets[i], nil)) + require.NoError(t, ms1.Finalise(i)) + } + time.Sleep(100 * time.Millisecond) + _, err := ms1.ResolveTxDAG(txCnt, nil) + require.NoError(t, err) +} + +func TestMVStates_ResolveTxDAG_Compare(t *testing.T) { + txCnt := 3000 + rwSets := mockRandomRWSet(txCnt) + ms1 := NewMVStates(txCnt) + ms2 := NewMVStates(txCnt) + for i, rwSet := range rwSets { + ms1.rwSets[i] = rwSet + ms2.rwSets[i] = rwSet + require.NoError(t, ms2.Finalise(i)) + } + + d1 := resolveTxDAGInMVStates(ms1) + d2 := resolveTxDAGByWritesInMVStates(ms2) + require.Equal(t, d1.(*PlainTxDAG).String(), d2.(*PlainTxDAG).String()) +} + +func TestMVStates_TxDAG_Compression(t *testing.T) { + txCnt := 10000 + rwSets := mockRandomRWSet(txCnt) + ms1 := NewMVStates(txCnt) + for i, rwSet := range rwSets { + ms1.rwSets[i] = rwSet + ms1.Finalise(i) + } + dag := resolveTxDAGByWritesInMVStates(ms1) + enc, err := EncodeTxDAG(dag) + require.NoError(t, err) + + // snappy compression + start := time.Now() + encoded := snappy.Encode(nil, enc) + t.Log("snappy", "enc", len(enc), "compressed", len(encoded), + "ratio", 1-(float64(len(encoded))/float64(len(enc))), + "time", float64(time.Since(start).Microseconds())/1000) + + // gzip compression + start = time.Now() + var buf bytes.Buffer + zw := gzip.NewWriter(&buf) + _, err = zw.Write(enc) + require.NoError(t, err) + err = zw.Close() + require.NoError(t, err) + encoded = buf.Bytes() + t.Log("gzip", "enc", len(enc), "compressed", len(encoded), + "ratio", 1-(float64(len(encoded))/float64(len(enc))), + "time", float64(time.Since(start).Microseconds())/1000) +} + +func BenchmarkResolveTxDAGInMVStates(b *testing.B) { + rwSets := mockRandomRWSet(mockRWSetSize) + ms1 := NewMVStates(mockRWSetSize) + for i, rwSet := range rwSets { + ms1.rwSets[i] = rwSet + } + for i := 0; i < b.N; i++ { + resolveTxDAGInMVStates(ms1) + } +} + +func BenchmarkResolveTxDAGByWritesInMVStates(b *testing.B) { + rwSets := mockRandomRWSet(mockRWSetSize) + ms1 := NewMVStates(mockRWSetSize) + for i, rwSet := range rwSets { + ms1.rwSets[i] = rwSet + ms1.Finalise(i) + } + for i := 0; i < b.N; i++ { + resolveTxDAGByWritesInMVStates(ms1) + } +} + +func BenchmarkMVStates_Finalise(b *testing.B) { + rwSets := mockRandomRWSet(mockRWSetSize) + ms1 := NewMVStates(mockRWSetSize) + for i := 0; i < b.N; i++ { + for k, rwSet := range rwSets { + ms1.rwSets[k] = rwSet + ms1.Finalise(k) + } + } +} + +func resolveTxDAGInMVStates(s *MVStates) TxDAG { + txDAG := NewPlainTxDAG(len(s.rwSets)) + for i := 0; i < len(s.rwSets); i++ { + s.resolveDepsCache(i, s.rwSets[i]) + txDAG.TxDeps[i].TxIndexes = s.depsCache[i] + } + return txDAG +} + +func resolveTxDAGByWritesInMVStates(s *MVStates) TxDAG { + txDAG := NewPlainTxDAG(len(s.rwSets)) + for i := 0; i < len(s.rwSets); i++ { + s.resolveDepsCacheByWrites(i, s.rwSets[i]) + txDAG.TxDeps[i].TxIndexes = s.depsCache[i] + } + return txDAG +} + +func TestMVStates_SystemTxResolveTxDAG(t *testing.T) { + ms := NewMVStates(12) + finaliseRWSets(t, ms, []*RWSet{ + mockRWSet(0, []string{"0x00"}, []string{"0x00"}), + mockRWSet(1, []string{"0x01"}, []string{"0x01"}), + mockRWSet(2, []string{"0x02"}, []string{"0x02"}), + mockRWSet(3, []string{"0x00", "0x03"}, []string{"0x03"}), + mockRWSet(4, []string{"0x00", "0x04"}, []string{"0x04"}), + mockRWSet(5, []string{"0x01", "0x02", "0x05"}, []string{"0x05"}), + mockRWSet(6, []string{"0x02", "0x05", "0x06"}, []string{"0x06"}), + mockRWSet(7, []string{"0x06", "0x07"}, []string{"0x07"}), + mockRWSet(8, []string{"0x08"}, []string{"0x08"}), + mockRWSet(9, []string{"0x08", "0x09"}, []string{"0x09"}), + mockRWSet(10, []string{"0x10"}, []string{"0x10"}).WithExcludedTxFlag(), + mockRWSet(11, []string{"0x11"}, []string{"0x11"}).WithExcludedTxFlag(), + }) + + dag, err := ms.ResolveTxDAG(12, nil) + require.NoError(t, err) + require.Equal(t, mockSystemTxDAG(), dag) + t.Log(dag) +} + +func TestMVStates_SystemTxWithLargeDepsResolveTxDAG(t *testing.T) { + ms := NewMVStates(12) + finaliseRWSets(t, ms, []*RWSet{ + mockRWSet(0, []string{"0x00"}, []string{"0x00"}), + mockRWSet(1, []string{"0x01"}, []string{"0x01"}), + mockRWSet(2, []string{"0x02"}, []string{"0x02"}), + mockRWSet(3, []string{"0x00", "0x03"}, []string{"0x03"}), + mockRWSet(4, []string{"0x00", "0x04"}, []string{"0x04"}), + mockRWSet(5, []string{"0x01", "0x02", "0x05"}, []string{"0x05"}), + mockRWSet(6, []string{"0x02", "0x05", "0x06"}, []string{"0x06"}), + mockRWSet(7, []string{"0x00", "0x03", "0x07"}, []string{"0x07"}), + mockRWSet(8, []string{"0x08"}, []string{"0x08"}), + mockRWSet(9, []string{"0x00", "0x01", "0x02", "0x06", "0x07", "0x08", "0x09"}, []string{"0x09"}), + mockRWSet(10, []string{"0x10"}, []string{"0x10"}).WithExcludedTxFlag(), + mockRWSet(11, []string{"0x11"}, []string{"0x11"}).WithExcludedTxFlag(), + }) + dag, err := ms.ResolveTxDAG(12, nil) + require.NoError(t, err) + require.Equal(t, mockSystemTxDAGWithLargeDeps(), dag) + t.Log(dag) +} + +func TestIsEqualRWVal(t *testing.T) { + tests := []struct { + key RWKey + src interface{} + compared interface{} + isEqual bool + }{ + { + key: AccountStateKey(mockAddr, AccountNonce), + src: uint64(0), + compared: uint64(0), + isEqual: true, + }, + { + key: AccountStateKey(mockAddr, AccountNonce), + src: uint64(0), + compared: uint64(1), + isEqual: false, + }, + { + key: AccountStateKey(mockAddr, AccountBalance), + src: new(uint256.Int).SetUint64(1), + compared: new(uint256.Int).SetUint64(1), + isEqual: true, + }, + { + key: AccountStateKey(mockAddr, AccountBalance), + src: nil, + compared: new(uint256.Int).SetUint64(1), + isEqual: false, + }, + { + key: AccountStateKey(mockAddr, AccountBalance), + src: (*uint256.Int)(nil), + compared: new(uint256.Int).SetUint64(1), + isEqual: false, + }, + { + key: AccountStateKey(mockAddr, AccountBalance), + src: (*uint256.Int)(nil), + compared: (*uint256.Int)(nil), + isEqual: true, + }, + { + key: AccountStateKey(mockAddr, AccountCodeHash), + src: []byte{1}, + compared: []byte{1}, + isEqual: true, + }, + { + key: AccountStateKey(mockAddr, AccountCodeHash), + src: nil, + compared: []byte{1}, + isEqual: false, + }, + { + key: AccountStateKey(mockAddr, AccountCodeHash), + src: ([]byte)(nil), + compared: []byte{1}, + isEqual: false, + }, + { + key: AccountStateKey(mockAddr, AccountCodeHash), + src: ([]byte)(nil), + compared: ([]byte)(nil), + isEqual: true, + }, + { + key: AccountStateKey(mockAddr, AccountSuicide), + src: struct{}{}, + compared: struct{}{}, + isEqual: false, + }, + { + key: AccountStateKey(mockAddr, AccountSuicide), + src: nil, + compared: struct{}{}, + isEqual: false, + }, + { + key: StorageStateKey(mockAddr, mockHash), + src: mockHash, + compared: mockHash, + isEqual: true, + }, + { + key: StorageStateKey(mockAddr, mockHash), + src: nil, + compared: mockHash, + isEqual: false, + }, + } + + for i, item := range tests { + require.Equal(t, item.isEqual, isEqualRWVal(item.key, item.src, item.compared), i) + } +} + +func mockRWSet(index int, read []string, write []string) *RWSet { + ver := StateVersion{ + TxIndex: index, + } + set := NewRWSet(ver) + for _, k := range read { + set.readSet[str2key(k)] = &RWItem{ + Ver: ver, + Val: struct{}{}, + } + } + for _, k := range write { + set.writeSet[str2key(k)] = &RWItem{ + Ver: ver, + Val: struct{}{}, + } + } + + return set +} + +func mockRWSetWithVal(index int, read []interface{}, write []interface{}) *RWSet { + ver := StateVersion{ + TxIndex: index, + } + set := NewRWSet(ver) + + if len(read)%2 != 0 { + panic("wrong read size") + } + if len(write)%2 != 0 { + panic("wrong write size") + } + + for i := 0; i < len(read); { + set.readSet[str2key(read[i].(string))] = &RWItem{ + Ver: StateVersion{ + TxIndex: index - 1, + }, + Val: read[i+1], + } + i += 2 + } + for i := 0; i < len(write); { + set.writeSet[str2key(write[i].(string))] = &RWItem{ + Ver: ver, + Val: write[i+1], + } + i += 2 + } + + return set +} + +func mockRandomRWSet(count int) []*RWSet { + var ret []*RWSet + for i := 0; i < count; i++ { + read := []string{fmt.Sprintf("0x%d", i)} + write := []string{fmt.Sprintf("0x%d", i)} + if i != 0 && rand.Bool() { + depCnt := rand.Int()%i + 1 + last := 0 + for j := 0; j < depCnt; j++ { + num, ok := randInRange(last, i) + if !ok { + break + } + read = append(read, fmt.Sprintf("0x%d", num)) + last = num + } + } + // random read + for j := 0; j < 20; j++ { + read = append(read, fmt.Sprintf("rr-%d-%d", j, rand.Int())) + } + for j := 0; j < 5; j++ { + read = append(read, fmt.Sprintf("rw-%d-%d", j, rand.Int())) + } + // random write + s := mockRWSet(i, read, write) + ret = append(ret, s) + } + return ret +} + +func finaliseRWSets(t *testing.T, mv *MVStates, rwSets []*RWSet) { + for i, rwSet := range rwSets { + require.NoError(t, mv.FulfillRWSet(rwSet, nil)) + require.NoError(t, mv.Finalise(i)) + } +} + +func randInRange(i, j int) (int, bool) { + if i >= j { + return 0, false + } + return rand.Int()%(j-i) + i, true +} + +func str2key(k string) RWKey { + key := RWKey{} + if len(k) > len(key) { + k = k[:len(key)] + } + copy(key[:], k) + return key +} diff --git a/core/types/receipt.go b/core/types/receipt.go index 67c1addb3d..8cb2bbdad8 100644 --- a/core/types/receipt.go +++ b/core/types/receipt.go @@ -18,6 +18,7 @@ package types import ( "bytes" + "encoding/json" "errors" "fmt" "io" @@ -605,3 +606,11 @@ func u32ptrTou64ptr(a *uint32) *uint64 { b := uint64(*a) return &b } + +// Debug PrettyPrint +func (r Receipt) PrettyPrint() (string, error) { + b, err := r.MarshalJSON() + var prettyJSON bytes.Buffer + json.Indent(&prettyJSON, b, "", "\t") + return prettyJSON.String(), err +} diff --git a/core/vm/evm.go b/core/vm/evm.go index 43ab27308b..82d0c19b8b 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -17,10 +17,11 @@ package vm import ( - "github.com/ethereum/go-ethereum/core/opcodeCompiler/compiler" "math/big" "sync/atomic" + "github.com/ethereum/go-ethereum/core/opcodeCompiler/compiler" + "github.com/holiman/uint256" "github.com/ethereum/go-ethereum/common" diff --git a/core/vm/interface.go b/core/vm/interface.go index 25bfa06720..537e300b97 100644 --- a/core/vm/interface.go +++ b/core/vm/interface.go @@ -79,6 +79,11 @@ type StateDB interface { AddLog(*types.Log) AddPreimage(common.Hash, []byte) + TxIndex() int + + // parallel DAG related + BeforeTxTransition() + FinaliseRWSet() error } // CallContext provides a basic interface for the EVM calling conventions. The EVM diff --git a/core/vm/interpreter.go b/core/vm/interpreter.go index 80acdcc013..2278b81710 100644 --- a/core/vm/interpreter.go +++ b/core/vm/interpreter.go @@ -19,6 +19,7 @@ package vm import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/math" + "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params" @@ -33,8 +34,11 @@ type Config struct { NoBaseFee bool // Forces the EIP-1559 baseFee to 0 (needed for 0 price calls) EnablePreimageRecording bool // Enables recording of SHA3/keccak preimages ExtraEips []int // Additional EIPS that are to be enabled + EnableParallelExec bool // Whether to execute transaction in parallel mode when do full sync + ParallelTxNum int // Number of slot for transaction execution OptimismPrecompileOverrides PrecompileOverrides // Precompile overrides for Optimism EnableOpcodeOptimizations bool // Enable opcode optimization + TxDAG types.TxDAG } // ScopeContext contains the things that are per-call, such as stack and memory, diff --git a/eth/backend.go b/eth/backend.go index b690938e87..03e354bb87 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -223,6 +223,8 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { var ( vmConfig = vm.Config{ EnablePreimageRecording: config.EnablePreimageRecording, + EnableParallelExec: config.ParallelTxMode, + ParallelTxNum: config.ParallelTxNum, EnableOpcodeOptimizations: config.EnableOpcodeOptimizing, } cacheConfig = &core.CacheConfig{ @@ -270,6 +272,9 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { if err != nil { return nil, err } + if config.EnableParallelTxDAG { + eth.blockchain.SetupTxDAGGeneration(config.ParallelTxDAGFile, config.ParallelTxMode) + } if chainConfig := eth.blockchain.Config(); chainConfig.Optimism != nil { // config.Genesis.Config.ChainID cannot be used because it's based on CLI flags only, thus default to mainnet L1 config.NetworkId = chainConfig.ChainID.Uint64() // optimism defaults eth network ID to chain ID eth.networkID = config.NetworkId diff --git a/eth/ethconfig/config.go b/eth/ethconfig/config.go index 383641ffc3..3f9624dc7c 100644 --- a/eth/ethconfig/config.go +++ b/eth/ethconfig/config.go @@ -218,7 +218,11 @@ type Config struct { RollupDisableTxPoolAdmission bool RollupHaltOnIncompatibleProtocolVersion string + ParallelTxMode bool // Whether to execute transaction in parallel mode when do full sync + ParallelTxNum int // Number of slot for transaction execution EnableOpcodeOptimizing bool + EnableParallelTxDAG bool + ParallelTxDAGFile string } // CreateConsensusEngine creates a consensus engine for the given chain config. diff --git a/go.mod b/go.mod index 73768e2685..2cc116668f 100644 --- a/go.mod +++ b/go.mod @@ -58,6 +58,7 @@ require ( github.com/olekukonko/tablewriter v0.0.5 github.com/panjf2000/ants/v2 v2.4.5 github.com/peterh/liner v1.2.0 + github.com/prometheus/client_golang v1.14.0 github.com/protolambda/bls12-381-util v0.0.0-20220416220906-d8552aa452c7 github.com/prysmaticlabs/prysm/v4 v4.2.0 github.com/rs/cors v1.8.3 @@ -148,7 +149,6 @@ require ( github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/client_golang v1.14.0 // indirect github.com/prometheus/client_model v0.4.0 // indirect github.com/prometheus/common v0.42.0 // indirect github.com/prometheus/procfs v0.9.0 // indirect diff --git a/miner/miner.go b/miner/miner.go index b65b226238..08c60c186e 100644 --- a/miner/miner.go +++ b/miner/miner.go @@ -19,14 +19,16 @@ package miner import ( "context" + "crypto/ecdsa" "errors" "fmt" - "github.com/ethereum/go-ethereum/consensus/misc/eip1559" - "github.com/ethereum/go-ethereum/consensus/misc/eip4844" "math/big" "sync" "time" + "github.com/ethereum/go-ethereum/consensus/misc/eip1559" + "github.com/ethereum/go-ethereum/consensus/misc/eip4844" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/consensus" @@ -62,7 +64,8 @@ var ( snapshotAccountReadTimer = metrics.NewRegisteredTimer("miner/snapshot/account/reads", nil) snapshotStorageReadTimer = metrics.NewRegisteredTimer("miner/snapshot/storage/reads", nil) - waitPayloadTimer = metrics.NewRegisteredTimer("miner/wait/payload", nil) + waitPayloadTimer = metrics.NewRegisteredTimer("miner/wait/payload", nil) + txDAGGenerateTimer = metrics.NewRegisteredTimer("miner/txdag/gen", nil) isBuildBlockInterruptCounter = metrics.NewRegisteredCounter("miner/build/interrupt", nil) ) @@ -107,6 +110,8 @@ type Config struct { EffectiveGasCeil uint64 // if non-zero, a gas ceiling to apply independent of the header's gaslimit value Mev MevConfig // Mev configuration + + ParallelTxDAGSenderPriv *ecdsa.PrivateKey // The private key for the parallel tx DAG sender } // DefaultConfig contains default settings for miner. diff --git a/miner/worker.go b/miner/worker.go index c5686f4d5d..0b5e807b16 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -18,14 +18,16 @@ package miner import ( "context" + "crypto/ecdsa" "errors" "fmt" - mapset "github.com/deckarep/golang-set/v2" "math/big" "sync" "sync/atomic" "time" + mapset "github.com/deckarep/golang-set/v2" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/consensus/misc" @@ -36,6 +38,7 @@ import ( "github.com/ethereum/go-ethereum/core/txpool" "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/eth/tracers" "github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/log" @@ -99,6 +102,10 @@ var ( txErrReplayMeter = metrics.NewRegisteredMeter("miner/tx/replay", nil) ) +var ( + DefaultTxDAGAddress = common.HexToAddress("0xda90000000000000000000000000000000000000") +) + // environment is the worker's current environment and holds all // information of the sealing block generation. type environment struct { @@ -1025,6 +1032,60 @@ func (w *worker) commitTransactions(env *environment, plainTxs, blobTxs *transac return nil } +// generateDAGTx generates a DAG transaction for the block +func (w *worker) generateDAGTx(env *environment) error { + // get txDAG data from the stateDB + txDAG, err := env.state.ResolveTxDAG(env.tcount, []common.Address{env.coinbase, params.OptimismBaseFeeRecipient, params.OptimismL1FeeRecipient}) + if txDAG == nil || err != nil { + return err + } + // txIndex is the index of this txDAG transaction + txDAG.SetTxDep(env.tcount, types.TxDep{Flags: &types.NonDependentRelFlag}) + + if env.signer == nil { + return fmt.Errorf("current signer is nil") + } + + //privateKey, err := crypto.HexToECDSA(privateKeyHex) + sender := w.config.ParallelTxDAGSenderPriv + receiver := DefaultTxDAGAddress + if sender == nil { + return fmt.Errorf("missing sender private key") + } + + publicKey := sender.Public() + publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey) + if !ok { + return fmt.Errorf("error casting public key to ECDSA") + } + fromAddress := crypto.PubkeyToAddress(*publicKeyECDSA) + + // get nonce from the + nonce := env.state.GetNonce(fromAddress) + + data, err := types.EncodeTxDAGCalldata(txDAG) + if err != nil { + return fmt.Errorf("failed to encode txDAG, err: %v", err) + } + + // Create the transaction + tx := types.NewTransaction(nonce, receiver, big.NewInt(0), 21100, big.NewInt(0), data) + + // Sign the transaction with the private key + signedTx, err := types.SignTx(tx, env.signer, sender) + if err != nil { + return fmt.Errorf("failed to sign transaction, err: %v", err) + } + + _, err = w.commitTransaction(env, signedTx) + if err != nil { + log.Warn("failed to commit DAG tx", "err", err) + return err + } + env.tcount++ + return nil +} + // generateParams wraps various of settings for generating sealing task. type generateParams struct { timestamp uint64 // The timestamp for sealing task @@ -1179,6 +1240,7 @@ func (w *worker) fillTransactions(interrupt *atomic.Int32, env *environment) err w.mu.RUnlock() start := time.Now() + // Retrieve the pending transactions pre-filtered by the 1559/4844 dynamic fees filter := txpool.PendingFilter{ MinTip: tip, @@ -1263,6 +1325,9 @@ func (w *worker) generateWork(genParams *generateParams) *newPayloadResult { misc.EnsureCreate2Deployer(w.chainConfig, work.header.Time, work.state) start := time.Now() + if w.chain.TxDAGEnabledWhenMine() { + work.state.ResetMVStates(0) + } for _, tx := range genParams.txs { from, _ := types.Sender(work.signer, tx) work.state.SetTxContext(tx.Hash(), work.tcount) @@ -1270,6 +1335,9 @@ func (w *worker) generateWork(genParams *generateParams) *newPayloadResult { if err != nil { return &newPayloadResult{err: fmt.Errorf("failed to force-include tx: %s type: %d sender: %s nonce: %d, err: %w", tx.Hash(), tx.Type(), from, tx.Nonce(), err)} } + if tx.IsSystemTx() || tx.IsDepositTx() { + work.state.RecordSystemTxRWSet(work.tcount) + } work.tcount++ } commitDepositTxsTimer.UpdateSince(start) @@ -1323,6 +1391,12 @@ func (w *worker) generateWork(genParams *generateParams) *newPayloadResult { if intr := genParams.interrupt; intr != nil && genParams.isUpdate && intr.Load() != commitInterruptNone { return &newPayloadResult{err: errInterruptedUpdate} } + //append the tx DAG transaction to the block + if w.chain.TxDAGEnabledWhenMine() { + if err := w.generateDAGTx(work); err != nil { + log.Warn("failed to generate DAG tx", "err", err) + } + } start = time.Now() block, err := w.engine.FinalizeAndAssemble(w.chain, work.header, work.state, work.txs, nil, work.receipts, genParams.withdrawals) @@ -1344,6 +1418,7 @@ func (w *worker) generateWork(genParams *generateParams) *newPayloadResult { storageUpdateTimer.Update(work.state.StorageUpdates) // Storage updates are complete(in FinalizeAndAssemble) accountHashTimer.Update(work.state.AccountHashes) // Account hashes are complete(in FinalizeAndAssemble) storageHashTimer.Update(work.state.StorageHashes) // Storage hashes are complete(in FinalizeAndAssemble) + txDAGGenerateTimer.Update(work.state.TxDAGGenerate) innerExecutionTimer.Update(core.DebugInnerExecutionDuration) diff --git a/tests/block_test.go b/tests/block_test.go index fb355085fd..ae0290862a 100644 --- a/tests/block_test.go +++ b/tests/block_test.go @@ -17,14 +17,61 @@ package tests import ( + "fmt" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/rawdb" "math/rand" + "os" + "path/filepath" "runtime" + "sync/atomic" "testing" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/rawdb" ) +func TestBlockchainWithTxDAG(t *testing.T) { + //log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(os.Stderr, log.LevelInfo, true))) + bt := new(testMatcher) + // General state tests are 'exported' as blockchain tests, but we can run them natively. + // For speedier CI-runs, the line below can be uncommented, so those are skipped. + // For now, in hardfork-times (Berlin), we run the tests both as StateTests and + // as blockchain tests, since the latter also covers things like receipt root + bt.skipLoad(`^GeneralStateTests/`) + + // Skip random failures due to selfish mining test + bt.skipLoad(`.*bcForgedTest/bcForkUncle\.json`) + + // Slow tests + bt.slow(`.*bcExploitTest/DelegateCallSpam.json`) + bt.slow(`.*bcExploitTest/ShanghaiLove.json`) + bt.slow(`.*bcExploitTest/SuicideIssue.json`) + bt.slow(`.*/bcForkStressTest/`) + bt.slow(`.*/bcGasPricerTest/RPC_API_Test.json`) + bt.slow(`.*/bcWalletTest/`) + + // Very slow test + bt.skipLoad(`.*/stTimeConsuming/.*`) + // test takes a lot for time and goes easily OOM because of sha3 calculation on a huge range, + // using 4.6 TGas + bt.skipLoad(`.*randomStatetest94.json.*`) + + bt.walk(t, blockTestDir, func(t *testing.T, name string, test *BlockTest) { + if runtime.GOARCH == "386" && runtime.GOOS == "windows" && rand.Int63()%2 == 0 { + t.Skip("test (randomly) skipped on 32-bit windows") + } + execBlockTestWithTxDAG(t, bt, test) + }) + //bt := new(testMatcher) + //path := filepath.Join(blockTestDir, "ValidBlocks", "bcEIP1559", "intrinsic.json") + //_, name := filepath.Split(path) + //t.Run(name, func(t *testing.T) { + // bt.runTestFile(t, path, name, func(t *testing.T, name string, test *BlockTest) { + // if runtime.GOARCH == "386" && runtime.GOOS == "windows" && rand.Int63()%2 == 0 { + // t.Skip("test (randomly) skipped on 32-bit windows") + // } + // execBlockTestWithTxDAG(t, bt, test) + // }) + //}) +} func TestBlockchain(t *testing.T) { bt := new(testMatcher) // General state tests are 'exported' as blockchain tests, but we can run them natively. @@ -73,21 +120,41 @@ func TestExecutionSpecBlocktests(t *testing.T) { }) } +var txDAGFileCounter atomic.Uint64 + +func execBlockTestWithTxDAG(t *testing.T, bt *testMatcher, test *BlockTest) { + txDAGFile := filepath.Join(os.TempDir(), fmt.Sprintf("test_txdag_%v.csv", txDAGFileCounter.Add(1))) + if err := bt.checkFailure(t, test.Run(true, rawdb.PathScheme, nil, nil, txDAGFile, false)); err != nil { + t.Errorf("test in path mode with snapshotter failed: %v", err) + return + } + + // run again with dagFile + if err := bt.checkFailure(t, test.Run(true, rawdb.PathScheme, nil, nil, txDAGFile, true)); err != nil { + t.Errorf("test in path mode with snapshotter failed: %v", err) + return + } + + // clean + os.Remove(txDAGFile) +} + func execBlockTest(t *testing.T, bt *testMatcher, test *BlockTest) { - if err := bt.checkFailure(t, test.Run(false, rawdb.HashScheme, nil, nil)); err != nil { + if err := bt.checkFailure(t, test.Run(false, rawdb.HashScheme, nil, nil, "", true)); err != nil { t.Errorf("test in hash mode without snapshotter failed: %v", err) return } - if err := bt.checkFailure(t, test.Run(true, rawdb.HashScheme, nil, nil)); err != nil { + if err := bt.checkFailure(t, test.Run(true, rawdb.HashScheme, nil, nil, "", true)); err != nil { t.Errorf("test in hash mode with snapshotter failed: %v", err) return } - if err := bt.checkFailure(t, test.Run(false, rawdb.PathScheme, nil, nil)); err != nil { + if err := bt.checkFailure(t, test.Run(false, rawdb.PathScheme, nil, nil, "", true)); err != nil { t.Errorf("test in path mode without snapshotter failed: %v", err) return } - if err := bt.checkFailure(t, test.Run(true, rawdb.PathScheme, nil, nil)); err != nil { + if err := bt.checkFailure(t, test.Run(true, rawdb.PathScheme, nil, nil, "", true)); err != nil { t.Errorf("test in path mode with snapshotter failed: %v", err) return } + } diff --git a/tests/block_test_util.go b/tests/block_test_util.go index 5f77a1c326..643f719c35 100644 --- a/tests/block_test_util.go +++ b/tests/block_test_util.go @@ -109,7 +109,7 @@ type btHeaderMarshaling struct { ExcessBlobGas *math.HexOrDecimal64 } -func (t *BlockTest) Run(snapshotter bool, scheme string, tracer vm.EVMLogger, postCheck func(error, *core.BlockChain)) (result error) { +func (t *BlockTest) Run(snapshotter bool, scheme string, tracer vm.EVMLogger, postCheck func(error, *core.BlockChain), dagFile string, enableParallel bool) (result error) { config, ok := Forks[t.json.Network] if !ok { return UnsupportedForkError{t.json.Network} @@ -151,13 +151,17 @@ func (t *BlockTest) Run(snapshotter bool, scheme string, tracer vm.EVMLogger, po cache.SnapshotWait = true } chain, err := core.NewBlockChain(db, cache, gspec, nil, engine, vm.Config{ - Tracer: tracer, + EnableParallelExec: enableParallel, + ParallelTxNum: 4, + Tracer: tracer, }, nil, nil) if err != nil { return err } defer chain.Stop() - + if len(dagFile) > 0 { + chain.SetupTxDAGGeneration(dagFile, enableParallel) + } validBlocks, err := t.insertBlocks(chain) if err != nil { return err