Merge pull request #840 from maticnetwork/block-stm

Merge block-stm to develop
This commit is contained in:
Jerry 2023-05-12 09:33:31 -07:00 committed by GitHub
commit eeb08055bd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 5201 additions and 247 deletions

View file

@ -171,6 +171,10 @@ syncmode = "full"
# period = 0
# gaslimit = 11500000
# [parallelevm]
# enable = false
# procs = 8
# [pprof]
# pprof = false
# port = 6060

View file

@ -42,6 +42,7 @@ import (
"github.com/ethereum/go-ethereum/common/prque"
"github.com/ethereum/go-ethereum/common/tracing"
"github.com/ethereum/go-ethereum/consensus"
"github.com/ethereum/go-ethereum/core/blockstm"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/state/snapshot"
@ -81,6 +82,8 @@ var (
blockValidationTimer = metrics.NewRegisteredTimer("chain/validation", nil)
blockExecutionTimer = metrics.NewRegisteredTimer("chain/execution", nil)
blockWriteTimer = metrics.NewRegisteredTimer("chain/write", nil)
blockExecutionParallelCounter = metrics.NewRegisteredCounter("chain/execution/parallel", nil)
blockExecutionSerialCounter = metrics.NewRegisteredCounter("chain/execution/serial", nil)
blockReorgMeter = metrics.NewRegisteredMeter("chain/reorg/executes", nil)
blockReorgAddMeter = metrics.NewRegisteredMeter("chain/reorg/add", nil)
@ -220,6 +223,7 @@ type BlockChain struct {
validator Validator // Block and state validator interface
prefetcher Prefetcher
processor Processor // Block transaction processor interface
parallelProcessor Processor // Parallel block transaction processor interface
forker *ForkChoice
vmConfig vm.Config
@ -435,6 +439,93 @@ func NewBlockChain(db ethdb.Database, cacheConfig *CacheConfig, chainConfig *par
return bc, nil
}
// Similar to NewBlockChain, this function creates a new blockchain object, but with a parallel state processor
func NewParallelBlockChain(db ethdb.Database, cacheConfig *CacheConfig, chainConfig *params.ChainConfig, engine consensus.Engine, vmConfig vm.Config, shouldPreserve func(header *types.Header) bool, txLookupLimit *uint64, checker ethereum.ChainValidator) (*BlockChain, error) {
bc, err := NewBlockChain(db, cacheConfig, chainConfig, engine, vmConfig, shouldPreserve, txLookupLimit, checker)
if err != nil {
return nil, err
}
bc.parallelProcessor = NewParallelStateProcessor(chainConfig, bc, engine)
return bc, nil
}
func (bc *BlockChain) ProcessBlock(block *types.Block, parent *types.Header) (types.Receipts, []*types.Log, uint64, *state.StateDB, error) {
// Process the block using processor and parallelProcessor at the same time, take the one which finishes first, cancel the other, and return the result
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
type Result struct {
receipts types.Receipts
logs []*types.Log
usedGas uint64
err error
statedb *state.StateDB
counter metrics.Counter
}
resultChan := make(chan Result, 2)
processorCount := 0
if bc.parallelProcessor != nil {
parallelStatedb, err := state.New(parent.Root, bc.stateCache, bc.snaps)
if err != nil {
return nil, nil, 0, nil, err
}
processorCount++
go func() {
parallelStatedb.StartPrefetcher("chain")
receipts, logs, usedGas, err := bc.parallelProcessor.Process(block, parallelStatedb, bc.vmConfig, ctx)
resultChan <- Result{receipts, logs, usedGas, err, parallelStatedb, blockExecutionParallelCounter}
}()
}
if bc.processor != nil {
statedb, err := state.New(parent.Root, bc.stateCache, bc.snaps)
if err != nil {
return nil, nil, 0, nil, err
}
processorCount++
go func() {
statedb.StartPrefetcher("chain")
receipts, logs, usedGas, err := bc.processor.Process(block, statedb, bc.vmConfig, ctx)
resultChan <- Result{receipts, logs, usedGas, err, statedb, blockExecutionSerialCounter}
}()
}
result := <-resultChan
if _, ok := result.err.(blockstm.ParallelExecFailedError); ok {
log.Warn("Parallel state processor failed", "err", result.err)
// If the parallel processor failed, we will fallback to the serial processor if enabled
if processorCount == 2 {
result.statedb.StopPrefetcher()
result = <-resultChan
processorCount--
}
}
result.counter.Inc(1)
// Make sure we are not leaking any prefetchers
if processorCount == 2 {
go func() {
second_result := <-resultChan
second_result.statedb.StopPrefetcher()
}()
}
return result.receipts, result.logs, result.usedGas, result.statedb, result.err
}
// empty returns an indicator whether the blockchain is empty.
// Note, it's a special case that we connect a non-empty ancient
// database with an empty node, so that we can plugin the ancient
@ -1761,14 +1852,6 @@ func (bc *BlockChain) insertChain(chain types.Blocks, verifySeals, setHead bool)
if parent == nil {
parent = bc.GetHeader(block.ParentHash(), block.NumberU64()-1)
}
statedb, err := state.New(parent.Root, bc.stateCache, bc.snaps)
if err != nil {
return it.index, err
}
// Enable prefetching to pull in trie node paths while processing transactions
statedb.StartPrefetcher("chain")
activeState = statedb
// If we have a followup block, run that against the current state to pre-cache
// transactions and probabilistically some of the account/storage trie nodes.
@ -1790,7 +1873,8 @@ func (bc *BlockChain) insertChain(chain types.Blocks, verifySeals, setHead bool)
// Process block using the parent state as reference point
substart := time.Now()
receipts, logs, usedGas, err := bc.processor.Process(block, statedb, bc.vmConfig)
receipts, logs, usedGas, statedb, err := bc.ProcessBlock(block, parent)
activeState = statedb
if err != nil {
bc.reportBlock(block, receipts, err)
atomic.StoreUint32(&followupInterrupt, 1)

View file

@ -33,7 +33,6 @@ import (
"github.com/ethereum/go-ethereum/consensus/beacon"
"github.com/ethereum/go-ethereum/consensus/ethash"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/crypto"
@ -123,9 +122,11 @@ func testFork(t *testing.T, blockchain *BlockChain, i, n int, full bool, compara
if full {
cur := blockchain.CurrentBlock()
tdPre = blockchain.GetTd(cur.Hash(), cur.NumberU64())
if err := testBlockChainImport(blockChainB, blockchain); err != nil {
t.Fatalf("failed to import forked block chain: %v", err)
}
last := blockChainB[len(blockChainB)-1]
tdPost = blockchain.GetTd(last.Hash(), last.NumberU64())
} else {
@ -156,11 +157,9 @@ func testBlockChainImport(chain types.Blocks, blockchain *BlockChain) error {
}
return err
}
statedb, err := state.New(blockchain.GetBlockByHash(block.ParentHash()).Root(), blockchain.stateCache, nil)
if err != nil {
return err
}
receipts, _, usedGas, err := blockchain.processor.Process(block, statedb, vm.Config{})
receipts, _, usedGas, statedb, err := blockchain.ProcessBlock(block, blockchain.GetBlockByHash(block.ParentHash()).Header())
if err != nil {
blockchain.reportBlock(block, receipts, err)
return err
@ -180,6 +179,25 @@ func testBlockChainImport(chain types.Blocks, blockchain *BlockChain) error {
return nil
}
func TestParallelBlockChainImport(t *testing.T) {
t.Parallel()
db, blockchain, err := newCanonical(ethash.NewFaker(), 10, true)
blockchain.parallelProcessor = NewParallelStateProcessor(blockchain.chainConfig, blockchain, blockchain.engine)
if err != nil {
t.Fatalf("failed to make new canonical chain: %v", err)
}
defer blockchain.Stop()
blockChainB := makeFakeNonEmptyBlockChain(blockchain.CurrentBlock(), 5, ethash.NewFaker(), db, forkSeed, 5)
if err := testBlockChainImport(blockChainB, blockchain); err == nil {
t.Fatalf("expected error for bad tx")
}
}
// testHeaderChainImport tries to process a chain of header, writing them into
// the database if successful.
func testHeaderChainImport(chain []*types.Header, blockchain *BlockChain) error {

201
core/blockstm/dag.go Normal file
View file

@ -0,0 +1,201 @@
package blockstm
import (
"fmt"
"strings"
"time"
"github.com/heimdalr/dag"
"github.com/ethereum/go-ethereum/log"
)
type DAG struct {
*dag.DAG
}
type TxDep struct {
Index int
ReadList []ReadDescriptor
FullWriteList [][]WriteDescriptor
}
func HasReadDep(txFrom TxnOutput, txTo TxnInput) bool {
reads := make(map[Key]bool)
for _, v := range txTo {
reads[v.Path] = true
}
for _, rd := range txFrom {
if _, ok := reads[rd.Path]; ok {
return true
}
}
return false
}
func BuildDAG(deps TxnInputOutput) (d DAG) {
d = DAG{dag.NewDAG()}
ids := make(map[int]string)
for i := len(deps.inputs) - 1; i > 0; i-- {
txTo := deps.inputs[i]
var txToId string
if _, ok := ids[i]; ok {
txToId = ids[i]
} else {
txToId, _ = d.AddVertex(i)
ids[i] = txToId
}
for j := i - 1; j >= 0; j-- {
txFrom := deps.allOutputs[j]
if HasReadDep(txFrom, txTo) {
var txFromId string
if _, ok := ids[j]; ok {
txFromId = ids[j]
} else {
txFromId, _ = d.AddVertex(j)
ids[j] = txFromId
}
err := d.AddEdge(txFromId, txToId)
if err != nil {
log.Warn("Failed to add edge", "from", txFromId, "to", txToId, "err", err)
}
}
}
}
return
}
func depsHelper(dependencies map[int]map[int]bool, txFrom TxnOutput, txTo TxnInput, i int, j int) map[int]map[int]bool {
if HasReadDep(txFrom, txTo) {
dependencies[i][j] = true
for k := range dependencies[i] {
_, foundDep := dependencies[j][k]
if foundDep {
delete(dependencies[i], k)
}
}
}
return dependencies
}
func UpdateDeps(deps map[int]map[int]bool, t TxDep) map[int]map[int]bool {
txTo := t.ReadList
deps[t.Index] = map[int]bool{}
for j := 0; j <= t.Index-1; j++ {
txFrom := t.FullWriteList[j]
deps = depsHelper(deps, txFrom, txTo, t.Index, j)
}
return deps
}
func GetDep(deps TxnInputOutput) map[int]map[int]bool {
newDependencies := map[int]map[int]bool{}
for i := 1; i < len(deps.inputs); i++ {
txTo := deps.inputs[i]
newDependencies[i] = map[int]bool{}
for j := 0; j <= i-1; j++ {
txFrom := deps.allOutputs[j]
newDependencies = depsHelper(newDependencies, txFrom, txTo, i, j)
}
}
return newDependencies
}
// Find the longest execution path in the DAG
func (d DAG) LongestPath(stats map[int]ExecutionStat) ([]int, uint64) {
prev := make(map[int]int, len(d.GetVertices()))
for i := 0; i < len(d.GetVertices()); i++ {
prev[i] = -1
}
pathWeights := make(map[int]uint64, len(d.GetVertices()))
maxPath := 0
maxPathWeight := uint64(0)
idxToId := make(map[int]string, len(d.GetVertices()))
for k, i := range d.GetVertices() {
idxToId[i.(int)] = k
}
for i := 0; i < len(idxToId); i++ {
parents, _ := d.GetParents(idxToId[i])
if len(parents) > 0 {
for _, p := range parents {
weight := pathWeights[p.(int)] + stats[i].End - stats[i].Start
if weight > pathWeights[i] {
pathWeights[i] = weight
prev[i] = p.(int)
}
}
} else {
pathWeights[i] = stats[i].End - stats[i].Start
}
if pathWeights[i] > maxPathWeight {
maxPath = i
maxPathWeight = pathWeights[i]
}
}
path := make([]int, 0)
for i := maxPath; i != -1; i = prev[i] {
path = append(path, i)
}
// Reverse the path so the transactions are in the ascending order
for i, j := 0, len(path)-1; i < j; i, j = i+1, j-1 {
path[i], path[j] = path[j], path[i]
}
return path, maxPathWeight
}
func (d DAG) Report(stats map[int]ExecutionStat, out func(string)) {
longestPath, weight := d.LongestPath(stats)
serialWeight := uint64(0)
for i := 0; i < len(d.GetVertices()); i++ {
serialWeight += stats[i].End - stats[i].Start
}
makeStrs := func(ints []int) (ret []string) {
for _, v := range ints {
ret = append(ret, fmt.Sprint(v))
}
return
}
out("Longest execution path:")
out(fmt.Sprintf("(%v) %v", len(longestPath), strings.Join(makeStrs(longestPath), "->")))
out(fmt.Sprintf("Longest path ideal execution time: %v of %v (serial total), %v%%", time.Duration(weight),
time.Duration(serialWeight), fmt.Sprintf("%.1f", float64(weight)*100.0/float64(serialWeight))))
}

641
core/blockstm/executor.go Normal file
View file

@ -0,0 +1,641 @@
package blockstm
import (
"container/heap"
"context"
"fmt"
"sync"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
)
type ExecResult struct {
err error
ver Version
txIn TxnInput
txOut TxnOutput
txAllOut TxnOutput
}
type ExecTask interface {
Execute(mvh *MVHashMap, incarnation int) error
MVReadList() []ReadDescriptor
MVWriteList() []WriteDescriptor
MVFullWriteList() []WriteDescriptor
Hash() common.Hash
Sender() common.Address
Settle()
Dependencies() []int
}
type ExecVersionView struct {
ver Version
et ExecTask
mvh *MVHashMap
sender common.Address
}
var NumSpeculativeProcs int = 8
func SetProcs(specProcs int) {
NumSpeculativeProcs = specProcs
}
func (ev *ExecVersionView) Execute() (er ExecResult) {
er.ver = ev.ver
if er.err = ev.et.Execute(ev.mvh, ev.ver.Incarnation); er.err != nil {
return
}
er.txIn = ev.et.MVReadList()
er.txOut = ev.et.MVWriteList()
er.txAllOut = ev.et.MVFullWriteList()
return
}
type ErrExecAbortError struct {
Dependency int
OriginError error
}
func (e ErrExecAbortError) Error() string {
if e.Dependency >= 0 {
return fmt.Sprintf("Execution aborted due to dependency %d", e.Dependency)
} else {
return "Execution aborted"
}
}
type ParallelExecFailedError struct {
Msg string
}
func (e ParallelExecFailedError) Error() string {
return e.Msg
}
type IntHeap []int
func (h IntHeap) Len() int { return len(h) }
func (h IntHeap) Less(i, j int) bool { return h[i] < h[j] }
func (h IntHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
func (h *IntHeap) Push(x any) {
// Push and Pop use pointer receivers because they modify the slice's length,
// not just its contents.
*h = append(*h, x.(int))
}
func (h *IntHeap) Pop() any {
old := *h
n := len(old)
x := old[n-1]
*h = old[0 : n-1]
return x
}
type SafeQueue interface {
Push(v int, d interface{})
Pop() interface{}
Len() int
}
type SafeFIFOQueue struct {
c chan interface{}
}
func NewSafeFIFOQueue(capacity int) *SafeFIFOQueue {
return &SafeFIFOQueue{
c: make(chan interface{}, capacity),
}
}
func (q *SafeFIFOQueue) Push(v int, d interface{}) {
q.c <- d
}
func (q *SafeFIFOQueue) Pop() interface{} {
return <-q.c
}
func (q *SafeFIFOQueue) Len() int {
return len(q.c)
}
// A thread safe priority queue
type SafePriorityQueue struct {
m sync.Mutex
queue *IntHeap
data map[int]interface{}
}
func NewSafePriorityQueue(capacity int) *SafePriorityQueue {
q := make(IntHeap, 0, capacity)
return &SafePriorityQueue{
m: sync.Mutex{},
queue: &q,
data: make(map[int]interface{}, capacity),
}
}
func (pq *SafePriorityQueue) Push(v int, d interface{}) {
pq.m.Lock()
heap.Push(pq.queue, v)
pq.data[v] = d
pq.m.Unlock()
}
func (pq *SafePriorityQueue) Pop() interface{} {
pq.m.Lock()
defer pq.m.Unlock()
v := heap.Pop(pq.queue).(int)
return pq.data[v]
}
func (pq *SafePriorityQueue) Len() int {
return pq.queue.Len()
}
type ParallelExecutionResult struct {
TxIO *TxnInputOutput
Stats *map[int]ExecutionStat
Deps *DAG
AllDeps map[int]map[int]bool
}
const numGoProcs = 1
type ParallelExecutor struct {
tasks []ExecTask
// Stores the execution statistics for the last incarnation of each task
stats map[int]ExecutionStat
statsMutex sync.Mutex
// Channel for tasks that should be prioritized
chTasks chan ExecVersionView
// Channel for speculative tasks
chSpeculativeTasks chan struct{}
// Channel to signal that the result of a transaction could be written to storage
specTaskQueue SafeQueue
// A priority queue that stores speculative tasks
chSettle chan int
// Channel to signal that a transaction has finished executing
chResults chan struct{}
// A priority queue that stores the transaction index of results, so we can validate the results in order
resultQueue SafeQueue
// A wait group to wait for all settling tasks to finish
settleWg sync.WaitGroup
// An integer that tracks the index of last settled transaction
lastSettled int
// For a task that runs only after all of its preceding tasks have finished and passed validation,
// its result will be absolutely valid and therefore its validation could be skipped.
// This map stores the boolean value indicating whether a task satisfy this condition ( absolutely valid).
skipCheck map[int]bool
// Execution tasks stores the state of each execution task
execTasks taskStatusManager
// Validate tasks stores the state of each validation task
validateTasks taskStatusManager
// Stats for debugging purposes
cntExec, cntSuccess, cntAbort, cntTotalValidations, cntValidationFail int
diagExecSuccess, diagExecAbort []int
// Multi-version hash map
mvh *MVHashMap
// Stores the inputs and outputs of the last incardanotion of all transactions
lastTxIO *TxnInputOutput
// Tracks the incarnation number of each transaction
txIncarnations []int
// A map that stores the estimated dependency of a transaction if it is aborted without any known dependency
estimateDeps map[int][]int
// A map that records whether a transaction result has been speculatively validated
preValidated map[int]bool
// Time records when the parallel execution starts
begin time.Time
// Enable profiling
profile bool
// Worker wait group
workerWg sync.WaitGroup
}
type ExecutionStat struct {
TxIdx int
Incarnation int
Start uint64
End uint64
Worker int
}
func NewParallelExecutor(tasks []ExecTask, profile bool, metadata bool) *ParallelExecutor {
numTasks := len(tasks)
var resultQueue SafeQueue
var specTaskQueue SafeQueue
if metadata {
resultQueue = NewSafeFIFOQueue(numTasks)
specTaskQueue = NewSafeFIFOQueue(numTasks)
} else {
resultQueue = NewSafePriorityQueue(numTasks)
specTaskQueue = NewSafePriorityQueue(numTasks)
}
pe := &ParallelExecutor{
tasks: tasks,
stats: make(map[int]ExecutionStat, numTasks),
chTasks: make(chan ExecVersionView, numTasks),
chSpeculativeTasks: make(chan struct{}, numTasks),
chSettle: make(chan int, numTasks),
chResults: make(chan struct{}, numTasks),
specTaskQueue: specTaskQueue,
resultQueue: resultQueue,
lastSettled: -1,
skipCheck: make(map[int]bool),
execTasks: makeStatusManager(numTasks),
validateTasks: makeStatusManager(0),
diagExecSuccess: make([]int, numTasks),
diagExecAbort: make([]int, numTasks),
mvh: MakeMVHashMap(),
lastTxIO: MakeTxnInputOutput(numTasks),
txIncarnations: make([]int, numTasks),
estimateDeps: make(map[int][]int),
preValidated: make(map[int]bool),
begin: time.Now(),
profile: profile,
}
return pe
}
// nolint: gocognit
func (pe *ParallelExecutor) Prepare() error {
prevSenderTx := make(map[common.Address]int)
for i, t := range pe.tasks {
clearPendingFlag := false
pe.skipCheck[i] = false
pe.estimateDeps[i] = make([]int, 0)
if len(t.Dependencies()) > 0 {
for _, val := range t.Dependencies() {
clearPendingFlag = true
pe.execTasks.addDependencies(val, i)
}
if clearPendingFlag {
pe.execTasks.clearPending(i)
clearPendingFlag = false
}
} else {
if tx, ok := prevSenderTx[t.Sender()]; ok {
pe.execTasks.addDependencies(tx, i)
pe.execTasks.clearPending(i)
}
prevSenderTx[t.Sender()] = i
}
}
pe.workerWg.Add(NumSpeculativeProcs + numGoProcs)
// Launch workers that execute transactions
for i := 0; i < NumSpeculativeProcs+numGoProcs; i++ {
go func(procNum int) {
defer pe.workerWg.Done()
doWork := func(task ExecVersionView) {
start := time.Duration(0)
if pe.profile {
start = time.Since(pe.begin)
}
res := task.Execute()
if res.err == nil {
pe.mvh.FlushMVWriteSet(res.txAllOut)
}
pe.resultQueue.Push(res.ver.TxnIndex, res)
pe.chResults <- struct{}{}
if pe.profile {
end := time.Since(pe.begin)
pe.statsMutex.Lock()
pe.stats[res.ver.TxnIndex] = ExecutionStat{
TxIdx: res.ver.TxnIndex,
Incarnation: res.ver.Incarnation,
Start: uint64(start),
End: uint64(end),
Worker: procNum,
}
pe.statsMutex.Unlock()
}
}
if procNum < NumSpeculativeProcs {
for range pe.chSpeculativeTasks {
doWork(pe.specTaskQueue.Pop().(ExecVersionView))
}
} else {
for task := range pe.chTasks {
doWork(task)
}
}
}(i)
}
pe.settleWg.Add(1)
go func() {
for t := range pe.chSettle {
pe.tasks[t].Settle()
}
pe.settleWg.Done()
}()
// bootstrap first execution
tx := pe.execTasks.takeNextPending()
if tx == -1 {
return ParallelExecFailedError{"no executable transactions due to bad dependency"}
}
pe.cntExec++
pe.chTasks <- ExecVersionView{ver: Version{tx, 0}, et: pe.tasks[tx], mvh: pe.mvh, sender: pe.tasks[tx].Sender()}
return nil
}
func (pe *ParallelExecutor) Close(wait bool) {
close(pe.chTasks)
close(pe.chSpeculativeTasks)
close(pe.chSettle)
if wait {
pe.workerWg.Wait()
}
if wait {
pe.settleWg.Wait()
}
}
// nolint: gocognit
func (pe *ParallelExecutor) Step(res *ExecResult) (result ParallelExecutionResult, err error) {
tx := res.ver.TxnIndex
if abortErr, ok := res.err.(ErrExecAbortError); ok && abortErr.OriginError != nil && pe.skipCheck[tx] {
// If the transaction failed when we know it should not fail, this means the transaction itself is
// bad (e.g. wrong nonce), and we should exit the execution immediately
err = fmt.Errorf("could not apply tx %d [%v]: %w", tx, pe.tasks[tx].Hash(), abortErr.OriginError)
pe.Close(true)
return
}
// nolint: nestif
if execErr, ok := res.err.(ErrExecAbortError); ok {
addedDependencies := false
if execErr.Dependency >= 0 {
l := len(pe.estimateDeps[tx])
for l > 0 && pe.estimateDeps[tx][l-1] > execErr.Dependency {
pe.execTasks.removeDependency(pe.estimateDeps[tx][l-1])
pe.estimateDeps[tx] = pe.estimateDeps[tx][:l-1]
l--
}
addedDependencies = pe.execTasks.addDependencies(execErr.Dependency, tx)
} else {
estimate := 0
if len(pe.estimateDeps[tx]) > 0 {
estimate = pe.estimateDeps[tx][len(pe.estimateDeps[tx])-1]
}
addedDependencies = pe.execTasks.addDependencies(estimate, tx)
newEstimate := estimate + (estimate+tx)/2
if newEstimate >= tx {
newEstimate = tx - 1
}
pe.estimateDeps[tx] = append(pe.estimateDeps[tx], newEstimate)
}
pe.execTasks.clearInProgress(tx)
if !addedDependencies {
pe.execTasks.pushPending(tx)
}
pe.txIncarnations[tx]++
pe.diagExecAbort[tx]++
pe.cntAbort++
} else {
pe.lastTxIO.recordRead(tx, res.txIn)
if res.ver.Incarnation == 0 {
pe.lastTxIO.recordWrite(tx, res.txOut)
pe.lastTxIO.recordAllWrite(tx, res.txAllOut)
} else {
if res.txAllOut.hasNewWrite(pe.lastTxIO.AllWriteSet(tx)) {
pe.validateTasks.pushPendingSet(pe.execTasks.getRevalidationRange(tx + 1))
}
prevWrite := pe.lastTxIO.AllWriteSet(tx)
// Remove entries that were previously written but are no longer written
cmpMap := make(map[Key]bool)
for _, w := range res.txAllOut {
cmpMap[w.Path] = true
}
for _, v := range prevWrite {
if _, ok := cmpMap[v.Path]; !ok {
pe.mvh.Delete(v.Path, tx)
}
}
pe.lastTxIO.recordWrite(tx, res.txOut)
pe.lastTxIO.recordAllWrite(tx, res.txAllOut)
}
pe.validateTasks.pushPending(tx)
pe.execTasks.markComplete(tx)
pe.diagExecSuccess[tx]++
pe.cntSuccess++
pe.execTasks.removeDependency(tx)
}
// do validations ...
maxComplete := pe.execTasks.maxAllComplete()
toValidate := make([]int, 0, 2)
for pe.validateTasks.minPending() <= maxComplete && pe.validateTasks.minPending() >= 0 {
toValidate = append(toValidate, pe.validateTasks.takeNextPending())
}
for i := 0; i < len(toValidate); i++ {
pe.cntTotalValidations++
tx := toValidate[i]
if pe.skipCheck[tx] || ValidateVersion(tx, pe.lastTxIO, pe.mvh) {
pe.validateTasks.markComplete(tx)
} else {
pe.cntValidationFail++
pe.diagExecAbort[tx]++
for _, v := range pe.lastTxIO.AllWriteSet(tx) {
pe.mvh.MarkEstimate(v.Path, tx)
}
// 'create validation tasks for all transactions > tx ...'
pe.validateTasks.pushPendingSet(pe.execTasks.getRevalidationRange(tx + 1))
pe.validateTasks.clearInProgress(tx) // clear in progress - pending will be added again once new incarnation executes
pe.execTasks.clearComplete(tx)
pe.execTasks.pushPending(tx)
pe.preValidated[tx] = false
pe.txIncarnations[tx]++
}
}
// Settle transactions that have been validated to be correct and that won't be re-executed again
maxValidated := pe.validateTasks.maxAllComplete()
for pe.lastSettled < maxValidated {
pe.lastSettled++
if pe.execTasks.checkInProgress(pe.lastSettled) || pe.execTasks.checkPending(pe.lastSettled) || pe.execTasks.isBlocked(pe.lastSettled) {
pe.lastSettled--
break
}
pe.chSettle <- pe.lastSettled
}
if pe.validateTasks.countComplete() == len(pe.tasks) && pe.execTasks.countComplete() == len(pe.tasks) {
log.Debug("blockstm exec summary", "execs", pe.cntExec, "success", pe.cntSuccess, "aborts", pe.cntAbort, "validations", pe.cntTotalValidations, "failures", pe.cntValidationFail, "#tasks/#execs", fmt.Sprintf("%.2f%%", float64(len(pe.tasks))/float64(pe.cntExec)*100))
pe.Close(true)
var allDeps map[int]map[int]bool
var deps DAG
if pe.profile {
allDeps = GetDep(*pe.lastTxIO)
deps = BuildDAG(*pe.lastTxIO)
}
return ParallelExecutionResult{pe.lastTxIO, &pe.stats, &deps, allDeps}, err
}
// Send the next immediate pending transaction to be executed
if pe.execTasks.minPending() != -1 && pe.execTasks.minPending() == maxValidated+1 {
nextTx := pe.execTasks.takeNextPending()
if nextTx != -1 {
pe.cntExec++
pe.skipCheck[nextTx] = true
pe.chTasks <- ExecVersionView{ver: Version{nextTx, pe.txIncarnations[nextTx]}, et: pe.tasks[nextTx], mvh: pe.mvh, sender: pe.tasks[nextTx].Sender()}
}
}
// Send speculative tasks
for pe.execTasks.minPending() != -1 {
nextTx := pe.execTasks.takeNextPending()
if nextTx != -1 {
pe.cntExec++
task := ExecVersionView{ver: Version{nextTx, pe.txIncarnations[nextTx]}, et: pe.tasks[nextTx], mvh: pe.mvh, sender: pe.tasks[nextTx].Sender()}
pe.specTaskQueue.Push(nextTx, task)
pe.chSpeculativeTasks <- struct{}{}
}
}
return
}
type PropertyCheck func(*ParallelExecutor) error
func executeParallelWithCheck(tasks []ExecTask, profile bool, check PropertyCheck, metadata bool, interruptCtx context.Context) (result ParallelExecutionResult, err error) {
if len(tasks) == 0 {
return ParallelExecutionResult{MakeTxnInputOutput(len(tasks)), nil, nil, nil}, nil
}
pe := NewParallelExecutor(tasks, profile, metadata)
err = pe.Prepare()
if err != nil {
pe.Close(true)
return
}
for range pe.chResults {
if interruptCtx != nil && interruptCtx.Err() != nil {
pe.Close(true)
return result, interruptCtx.Err()
}
res := pe.resultQueue.Pop().(ExecResult)
result, err = pe.Step(&res)
if err != nil {
return result, err
}
if check != nil {
err = check(pe)
}
if result.TxIO != nil || err != nil {
return result, err
}
}
return
}
func ExecuteParallel(tasks []ExecTask, profile bool, metadata bool, interruptCtx context.Context) (result ParallelExecutionResult, err error) {
return executeParallelWithCheck(tasks, profile, nil, metadata, interruptCtx)
}

View file

@ -0,0 +1,984 @@
package blockstm
import (
"context"
"fmt"
"math/big"
"math/rand"
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
)
type OpType int
const readType = 0
const writeType = 1
const otherType = 2
const greenTick = "✅"
const redCross = "❌"
const threeRockets = "🚀🚀🚀"
type Op struct {
key Key
duration time.Duration
opType OpType
val int
}
type testExecTask struct {
txIdx int
ops []Op
readMap map[Key]ReadDescriptor
writeMap map[Key]WriteDescriptor
sender common.Address
nonce int
dependencies []int
}
type PathGenerator func(addr common.Address, i int, j int, total int) Key
type TaskRunner func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration)
type TaskRunnerWithMetadata func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration, time.Duration)
type Timer func(txIdx int, opIdx int) time.Duration
type Sender func(int) common.Address
func NewTestExecTask(txIdx int, ops []Op, sender common.Address, nonce int) *testExecTask {
return &testExecTask{
txIdx: txIdx,
ops: ops,
readMap: make(map[Key]ReadDescriptor),
writeMap: make(map[Key]WriteDescriptor),
sender: sender,
nonce: nonce,
dependencies: []int{},
}
}
func sleep(i time.Duration) {
start := time.Now()
for time.Since(start) < i {
}
}
func (t *testExecTask) Execute(mvh *MVHashMap, incarnation int) error {
// Sleep for 50 microsecond to simulate setup time
sleep(time.Microsecond * 50)
version := Version{TxnIndex: t.txIdx, Incarnation: incarnation}
t.readMap = make(map[Key]ReadDescriptor)
t.writeMap = make(map[Key]WriteDescriptor)
deps := -1
for i, op := range t.ops {
k := op.key
switch op.opType {
case readType:
if _, ok := t.writeMap[k]; ok {
sleep(op.duration)
continue
}
result := mvh.Read(k, t.txIdx)
val := result.Value()
if i == 0 && val != nil && (val.(int) != t.nonce) {
return ErrExecAbortError{}
}
if result.Status() == MVReadResultDependency {
if result.depIdx > deps {
deps = result.depIdx
}
}
var readKind int
if result.Status() == MVReadResultDone {
readKind = ReadKindMap
} else if result.Status() == MVReadResultNone {
readKind = ReadKindStorage
}
sleep(op.duration)
t.readMap[k] = ReadDescriptor{k, readKind, Version{TxnIndex: result.depIdx, Incarnation: result.incarnation}}
case writeType:
t.writeMap[k] = WriteDescriptor{k, version, op.val}
case otherType:
sleep(op.duration)
default:
panic(fmt.Sprintf("Unknown op type: %d", op.opType))
}
}
if deps != -1 {
return ErrExecAbortError{deps, fmt.Errorf("Dependency error")}
}
return nil
}
func (t *testExecTask) MVWriteList() []WriteDescriptor {
return t.MVFullWriteList()
}
func (t *testExecTask) MVFullWriteList() []WriteDescriptor {
writes := make([]WriteDescriptor, 0, len(t.writeMap))
for _, v := range t.writeMap {
writes = append(writes, v)
}
return writes
}
func (t *testExecTask) MVReadList() []ReadDescriptor {
reads := make([]ReadDescriptor, 0, len(t.readMap))
for _, v := range t.readMap {
reads = append(reads, v)
}
return reads
}
func (t *testExecTask) Settle() {}
func (t *testExecTask) Sender() common.Address {
return t.sender
}
func (t *testExecTask) Hash() common.Hash {
return common.BytesToHash([]byte(fmt.Sprintf("%d", t.txIdx)))
}
func (t *testExecTask) Dependencies() []int {
return t.dependencies
}
func randTimeGenerator(min time.Duration, max time.Duration) func(txIdx int, opIdx int) time.Duration {
return func(txIdx int, opIdx int) time.Duration {
return time.Duration(rand.Int63n(int64(max-min))) + min
}
}
func longTailTimeGenerator(min time.Duration, max time.Duration, i int, j int) func(txIdx int, opIdx int) time.Duration {
return func(txIdx int, opIdx int) time.Duration {
if txIdx%i == 0 && opIdx == j {
return max * 100
} else {
return time.Duration(rand.Int63n(int64(max-min))) + min
}
}
}
var randomPathGenerator = func(sender common.Address, i int, j int, total int) Key {
return NewStateKey(common.BigToAddress((big.NewInt(int64(i % 10)))), common.BigToHash((big.NewInt(int64(total)))))
}
var dexPathGenerator = func(sender common.Address, i int, j int, total int) Key {
if j == total-1 || j == 2 {
return NewSubpathKey(common.BigToAddress(big.NewInt(int64(0))), 1)
} else {
return NewSubpathKey(common.BigToAddress(big.NewInt(int64(j))), 1)
}
}
var readTime = randTimeGenerator(4*time.Microsecond, 12*time.Microsecond)
var writeTime = randTimeGenerator(2*time.Microsecond, 6*time.Microsecond)
var nonIOTime = randTimeGenerator(1*time.Microsecond, 2*time.Microsecond)
func taskFactory(numTask int, sender Sender, readsPerT int, writesPerT int, nonIOPerT int, pathGenerator PathGenerator, readTime Timer, writeTime Timer, nonIOTime Timer) ([]ExecTask, time.Duration) {
exec := make([]ExecTask, 0, numTask)
var serialDuration time.Duration
senderNonces := make(map[common.Address]int)
for i := 0; i < numTask; i++ {
s := sender(i)
// Set first two ops to always read and write nonce
ops := make([]Op, 0, readsPerT+writesPerT+nonIOPerT)
ops = append(ops, Op{opType: readType, key: NewSubpathKey(s, 2), duration: readTime(i, 0), val: senderNonces[s]})
senderNonces[s]++
ops = append(ops, Op{opType: writeType, key: NewSubpathKey(s, 2), duration: writeTime(i, 1), val: senderNonces[s]})
for j := 0; j < readsPerT-1; j++ {
ops = append(ops, Op{opType: readType})
}
for j := 0; j < nonIOPerT; j++ {
ops = append(ops, Op{opType: otherType})
}
for j := 0; j < writesPerT-1; j++ {
ops = append(ops, Op{opType: writeType})
}
// shuffle ops except for the first three (read nonce, write nonce, another read) ops and last write op.
// This enables random path generator to generate deterministic paths for these "special" ops.
for j := 3; j < len(ops)-1; j++ {
k := rand.Intn(len(ops)-j-1) + j
ops[j], ops[k] = ops[k], ops[j]
}
// Generate time and key path for each op except first two that are always read and write nonce
for j := 2; j < len(ops); j++ {
if ops[j].opType == readType {
ops[j].key = pathGenerator(s, i, j, len(ops))
ops[j].duration = readTime(i, j)
} else if ops[j].opType == writeType {
ops[j].key = pathGenerator(s, i, j, len(ops))
ops[j].duration = writeTime(i, j)
} else {
ops[j].duration = nonIOTime(i, j)
}
serialDuration += ops[j].duration
}
if ops[len(ops)-1].opType != writeType {
panic("Last op must be a write")
}
t := NewTestExecTask(i, ops, s, senderNonces[s]-1)
exec = append(exec, t)
}
return exec, serialDuration
}
func testExecutorComb(t *testing.T, totalTxs []int, numReads []int, numWrites []int, numNonIO []int, taskRunner TaskRunner) {
t.Helper()
log.Root().SetHandler(log.LvlFilterHandler(log.LvlDebug, log.StreamHandler(os.Stderr, log.TerminalFormat(false))))
improved := 0
total := 0
totalExecDuration := time.Duration(0)
totalSerialDuration := time.Duration(0)
for _, numTx := range totalTxs {
for _, numRead := range numReads {
for _, numWrite := range numWrites {
for _, numNonIO := range numNonIO {
log.Info("Executing block", "numTx", numTx, "numRead", numRead, "numWrite", numWrite, "numNonIO", numNonIO)
execDuration, expectedSerialDuration := taskRunner(numTx, numRead, numWrite, numNonIO)
if execDuration < expectedSerialDuration {
improved++
}
total++
performance := greenTick
if execDuration >= expectedSerialDuration {
performance = redCross
}
fmt.Printf("exec duration %v, serial duration %v, time reduced %v %.2f%%, %v \n", execDuration, expectedSerialDuration, expectedSerialDuration-execDuration, float64(expectedSerialDuration-execDuration)/float64(expectedSerialDuration)*100, performance)
totalExecDuration += execDuration
totalSerialDuration += expectedSerialDuration
}
}
}
}
fmt.Println("Improved: ", improved, "Total: ", total, "success rate: ", float64(improved)/float64(total)*100)
fmt.Printf("Total exec duration: %v, total serial duration: %v, time reduced: %v, time reduced percent: %.2f%%\n", totalExecDuration, totalSerialDuration, totalSerialDuration-totalExecDuration, float64(totalSerialDuration-totalExecDuration)/float64(totalSerialDuration)*100)
}
// nolint: gocognit
func testExecutorCombWithMetadata(t *testing.T, totalTxs []int, numReads []int, numWrites []int, numNonIOs []int, taskRunner TaskRunnerWithMetadata) {
t.Helper()
log.Root().SetHandler(log.LvlFilterHandler(log.LvlDebug, log.StreamHandler(os.Stderr, log.TerminalFormat(false))))
improved := 0
improvedMetadata := 0
rocket := 0
total := 0
totalExecDuration := time.Duration(0)
totalExecDurationMetadata := time.Duration(0)
totalSerialDuration := time.Duration(0)
for _, numTx := range totalTxs {
for _, numRead := range numReads {
for _, numWrite := range numWrites {
for _, numNonIO := range numNonIOs {
log.Info("Executing block", "numTx", numTx, "numRead", numRead, "numWrite", numWrite, "numNonIO", numNonIO)
execDuration, execDurationMetadata, expectedSerialDuration := taskRunner(numTx, numRead, numWrite, numNonIO)
if execDuration < expectedSerialDuration {
improved++
}
total++
performance := greenTick
if execDuration >= expectedSerialDuration {
performance = redCross
if execDurationMetadata <= expectedSerialDuration {
performance = threeRockets
rocket++
}
}
if execDuration >= execDurationMetadata {
improvedMetadata++
}
fmt.Printf("WITHOUT METADATA: exec duration %v, serial duration %v, time reduced %v %.2f%%, %v \n", execDuration, expectedSerialDuration, expectedSerialDuration-execDuration, float64(expectedSerialDuration-execDuration)/float64(expectedSerialDuration)*100, performance)
fmt.Printf("WITH METADATA: exec duration %v, exec duration with metadata %v, time reduced %v %.2f%%\n", execDuration, execDurationMetadata, execDuration-execDurationMetadata, float64(execDuration-execDurationMetadata)/float64(execDuration)*100)
totalExecDuration += execDuration
totalExecDurationMetadata += execDurationMetadata
totalSerialDuration += expectedSerialDuration
}
}
}
}
fmt.Println("\nImproved: ", improved, "Total: ", total, "success rate: ", float64(improved)/float64(total)*100)
fmt.Println("Metadata Better: ", improvedMetadata, "out of: ", total, "success rate: ", float64(improvedMetadata)/float64(total)*100)
fmt.Println("Rockets (Time of: metadata < serial < without metadata): ", rocket)
fmt.Printf("\nWithout metadata <> serial: Total exec duration: %v, total serial duration : %v, time reduced: %v, time reduced percent: %.2f%%\n", totalExecDuration, totalSerialDuration, totalSerialDuration-totalExecDuration, float64(totalSerialDuration-totalExecDuration)/float64(totalSerialDuration)*100)
fmt.Printf("With metadata <> serial: Total exec duration metadata: %v, total serial duration : %v, time reduced: %v, time reduced percent: %.2f%%\n", totalExecDurationMetadata, totalSerialDuration, totalSerialDuration-totalExecDurationMetadata, float64(totalSerialDuration-totalExecDurationMetadata)/float64(totalSerialDuration)*100)
fmt.Printf("Without metadata <> with metadata: Total exec duration: %v, total exec duration metadata: %v, time reduced: %v, time reduced percent: %.2f%%\n", totalExecDuration, totalExecDurationMetadata, totalExecDuration-totalExecDurationMetadata, float64(totalExecDuration-totalExecDurationMetadata)/float64(totalExecDuration)*100)
}
func composeValidations(checks []PropertyCheck) PropertyCheck {
return func(pe *ParallelExecutor) error {
for _, check := range checks {
err := check(pe)
if err != nil {
return err
}
}
return nil
}
}
func checkNoStatusOverlap(pe *ParallelExecutor) error {
seen := make(map[int]string)
for _, tx := range pe.execTasks.complete {
seen[tx] = "complete"
}
for _, tx := range pe.execTasks.inProgress {
if v, ok := seen[tx]; ok {
return fmt.Errorf("tx %v is in both %v and inProgress", v, tx)
}
seen[tx] = "inProgress"
}
for _, tx := range pe.execTasks.pending {
if v, ok := seen[tx]; ok {
return fmt.Errorf("tx %v is in both %v complete and pending", v, tx)
}
seen[tx] = "pending"
}
return nil
}
func checkNoDroppedTx(pe *ParallelExecutor) error {
for i := 0; i < len(pe.tasks); i++ {
if !pe.execTasks.checkComplete(i) && !pe.execTasks.checkInProgress(i) && !pe.execTasks.checkPending(i) {
if !pe.execTasks.isBlocked(i) {
return fmt.Errorf("tx %v is not in any status and is not blocked by any other tx", i)
}
}
}
return nil
}
// nolint: unparam
func runParallel(t *testing.T, tasks []ExecTask, validation PropertyCheck, metadata bool) time.Duration {
t.Helper()
profile := false
start := time.Now()
result, err := executeParallelWithCheck(tasks, false, validation, metadata, nil)
if result.Deps != nil && profile {
result.Deps.Report(*result.Stats, func(str string) { fmt.Println(str) })
}
assert.NoError(t, err, "error occur during parallel execution")
// Need to apply the final write set to storage
finalWriteSet := make(map[Key]time.Duration)
for _, task := range tasks {
task := task.(*testExecTask)
for _, op := range task.ops {
if op.opType == writeType {
finalWriteSet[op.key] = op.duration
}
}
}
for _, v := range finalWriteSet {
sleep(v)
}
duration := time.Since(start)
return duration
}
func runParallelGetMetadata(t *testing.T, tasks []ExecTask, validation PropertyCheck) map[int]map[int]bool {
t.Helper()
res, err := executeParallelWithCheck(tasks, true, validation, false, nil)
assert.NoError(t, err, "error occur during parallel execution")
return res.AllDeps
}
func TestLessConflicts(t *testing.T) {
t.Parallel()
rand.Seed(0)
totalTxs := []int{10, 50, 100, 200, 300}
numReads := []int{20, 100, 200}
numWrites := []int{20, 100, 200}
numNonIO := []int{100, 500}
checks := composeValidations([]PropertyCheck{checkNoStatusOverlap, checkNoDroppedTx})
taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration) {
sender := func(i int) common.Address {
randomness := rand.Intn(10) + 10
return common.BigToAddress(big.NewInt(int64(i % randomness)))
}
tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, readTime, writeTime, nonIOTime)
return runParallel(t, tasks, checks, false), serialDuration
}
testExecutorComb(t, totalTxs, numReads, numWrites, numNonIO, taskRunner)
}
func TestLessConflictsWithMetadata(t *testing.T) {
t.Parallel()
rand.Seed(0)
totalTxs := []int{300}
numReads := []int{100, 200}
numWrites := []int{100, 200}
numNonIOs := []int{100, 500}
checks := composeValidations([]PropertyCheck{checkNoStatusOverlap, checkNoDroppedTx})
taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration, time.Duration) {
sender := func(i int) common.Address {
randomness := rand.Intn(10) + 10
return common.BigToAddress(big.NewInt(int64(i % randomness)))
}
tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, readTime, writeTime, nonIOTime)
parallelDuration := runParallel(t, tasks, checks, false)
allDeps := runParallelGetMetadata(t, tasks, checks)
newTasks := make([]ExecTask, 0, len(tasks))
for _, t := range tasks {
temp := t.(*testExecTask)
keys := make([]int, len(allDeps[temp.txIdx]))
i := 0
for k := range allDeps[temp.txIdx] {
keys[i] = k
i++
}
temp.dependencies = keys
newTasks = append(newTasks, temp)
}
return parallelDuration, runParallel(t, newTasks, checks, true), serialDuration
}
testExecutorCombWithMetadata(t, totalTxs, numReads, numWrites, numNonIOs, taskRunner)
}
func TestZeroTx(t *testing.T) {
t.Parallel()
rand.Seed(0)
totalTxs := []int{0}
numReads := []int{20}
numWrites := []int{20}
numNonIO := []int{100}
checks := composeValidations([]PropertyCheck{checkNoStatusOverlap, checkNoDroppedTx})
taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration) {
sender := func(i int) common.Address { return common.BigToAddress(big.NewInt(int64(1))) }
tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, readTime, writeTime, nonIOTime)
return runParallel(t, tasks, checks, false), serialDuration
}
testExecutorComb(t, totalTxs, numReads, numWrites, numNonIO, taskRunner)
}
func TestAlternatingTx(t *testing.T) {
t.Parallel()
rand.Seed(0)
totalTxs := []int{200}
numReads := []int{20}
numWrites := []int{20}
numNonIO := []int{100}
checks := composeValidations([]PropertyCheck{checkNoStatusOverlap, checkNoDroppedTx})
taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration) {
sender := func(i int) common.Address { return common.BigToAddress(big.NewInt(int64(i % 2))) }
tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, readTime, writeTime, nonIOTime)
return runParallel(t, tasks, checks, false), serialDuration
}
testExecutorComb(t, totalTxs, numReads, numWrites, numNonIO, taskRunner)
}
func TestAlternatingTxWithMetadata(t *testing.T) {
t.Parallel()
rand.Seed(0)
totalTxs := []int{200}
numReads := []int{20}
numWrites := []int{20}
numNonIO := []int{100}
checks := composeValidations([]PropertyCheck{checkNoStatusOverlap, checkNoDroppedTx})
taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration, time.Duration) {
sender := func(i int) common.Address { return common.BigToAddress(big.NewInt(int64(i % 2))) }
tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, readTime, writeTime, nonIOTime)
parallelDuration := runParallel(t, tasks, checks, false)
allDeps := runParallelGetMetadata(t, tasks, checks)
newTasks := make([]ExecTask, 0, len(tasks))
for _, t := range tasks {
temp := t.(*testExecTask)
keys := make([]int, len(allDeps[temp.txIdx]))
i := 0
for k := range allDeps[temp.txIdx] {
keys[i] = k
i++
}
temp.dependencies = keys
newTasks = append(newTasks, temp)
}
return parallelDuration, runParallel(t, newTasks, checks, true), serialDuration
}
testExecutorCombWithMetadata(t, totalTxs, numReads, numWrites, numNonIO, taskRunner)
}
func TestMoreConflicts(t *testing.T) {
t.Parallel()
rand.Seed(0)
totalTxs := []int{10, 50, 100, 200, 300}
numReads := []int{20, 100, 200}
numWrites := []int{20, 100, 200}
numNonIO := []int{100, 500}
checks := composeValidations([]PropertyCheck{checkNoStatusOverlap, checkNoDroppedTx})
taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration) {
sender := func(i int) common.Address {
randomness := rand.Intn(10) + 10
return common.BigToAddress(big.NewInt(int64(i / randomness)))
}
tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, readTime, writeTime, nonIOTime)
return runParallel(t, tasks, checks, false), serialDuration
}
testExecutorComb(t, totalTxs, numReads, numWrites, numNonIO, taskRunner)
}
func TestMoreConflictsWithMetadata(t *testing.T) {
t.Parallel()
rand.Seed(0)
totalTxs := []int{300}
numReads := []int{100, 200}
numWrites := []int{100, 200}
numNonIO := []int{100, 500}
checks := composeValidations([]PropertyCheck{checkNoStatusOverlap, checkNoDroppedTx})
taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration, time.Duration) {
sender := func(i int) common.Address {
randomness := rand.Intn(10) + 10
return common.BigToAddress(big.NewInt(int64(i / randomness)))
}
tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, readTime, writeTime, nonIOTime)
parallelDuration := runParallel(t, tasks, checks, false)
allDeps := runParallelGetMetadata(t, tasks, checks)
newTasks := make([]ExecTask, 0, len(tasks))
for _, t := range tasks {
temp := t.(*testExecTask)
keys := make([]int, len(allDeps[temp.txIdx]))
i := 0
for k := range allDeps[temp.txIdx] {
keys[i] = k
i++
}
temp.dependencies = keys
newTasks = append(newTasks, temp)
}
return parallelDuration, runParallel(t, newTasks, checks, true), serialDuration
}
testExecutorCombWithMetadata(t, totalTxs, numReads, numWrites, numNonIO, taskRunner)
}
func TestRandomTx(t *testing.T) {
t.Parallel()
rand.Seed(0)
totalTxs := []int{10, 50, 100, 200, 300}
numReads := []int{20, 100, 200}
numWrites := []int{20, 100, 200}
numNonIO := []int{100, 500}
checks := composeValidations([]PropertyCheck{checkNoStatusOverlap, checkNoDroppedTx})
taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration) {
// Randomly assign this tx to one of 10 senders
sender := func(i int) common.Address { return common.BigToAddress(big.NewInt(int64(rand.Intn(10)))) }
tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, readTime, writeTime, nonIOTime)
return runParallel(t, tasks, checks, false), serialDuration
}
testExecutorComb(t, totalTxs, numReads, numWrites, numNonIO, taskRunner)
}
func TestRandomTxWithMetadata(t *testing.T) {
t.Parallel()
rand.Seed(0)
totalTxs := []int{300}
numReads := []int{100, 200}
numWrites := []int{100, 200}
numNonIO := []int{100, 500}
checks := composeValidations([]PropertyCheck{checkNoStatusOverlap, checkNoDroppedTx})
taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration, time.Duration) {
// Randomly assign this tx to one of 10 senders
sender := func(i int) common.Address { return common.BigToAddress(big.NewInt(int64(rand.Intn(10)))) }
tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, readTime, writeTime, nonIOTime)
parallelDuration := runParallel(t, tasks, checks, false)
allDeps := runParallelGetMetadata(t, tasks, checks)
newTasks := make([]ExecTask, 0, len(tasks))
for _, t := range tasks {
temp := t.(*testExecTask)
keys := make([]int, len(allDeps[temp.txIdx]))
i := 0
for k := range allDeps[temp.txIdx] {
keys[i] = k
i++
}
temp.dependencies = keys
newTasks = append(newTasks, temp)
}
return parallelDuration, runParallel(t, newTasks, checks, true), serialDuration
}
testExecutorCombWithMetadata(t, totalTxs, numReads, numWrites, numNonIO, taskRunner)
}
func TestTxWithLongTailRead(t *testing.T) {
t.Parallel()
rand.Seed(0)
totalTxs := []int{10, 50, 100, 200, 300}
numReads := []int{20, 100, 200}
numWrites := []int{20, 100, 200}
numNonIO := []int{100, 500}
checks := composeValidations([]PropertyCheck{checkNoStatusOverlap, checkNoDroppedTx})
taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration) {
sender := func(i int) common.Address {
randomness := rand.Intn(10) + 10
return common.BigToAddress(big.NewInt(int64(i / randomness)))
}
longTailReadTimer := longTailTimeGenerator(4*time.Microsecond, 12*time.Microsecond, 7, 10)
tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, longTailReadTimer, writeTime, nonIOTime)
return runParallel(t, tasks, checks, false), serialDuration
}
testExecutorComb(t, totalTxs, numReads, numWrites, numNonIO, taskRunner)
}
func TestTxWithLongTailReadWithMetadata(t *testing.T) {
t.Parallel()
rand.Seed(0)
totalTxs := []int{300}
numReads := []int{100, 200}
numWrites := []int{100, 200}
numNonIO := []int{100, 500}
checks := composeValidations([]PropertyCheck{checkNoStatusOverlap, checkNoDroppedTx})
taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration, time.Duration) {
sender := func(i int) common.Address {
randomness := rand.Intn(10) + 10
return common.BigToAddress(big.NewInt(int64(i / randomness)))
}
longTailReadTimer := longTailTimeGenerator(4*time.Microsecond, 12*time.Microsecond, 7, 10)
tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, longTailReadTimer, writeTime, nonIOTime)
parallelDuration := runParallel(t, tasks, checks, false)
allDeps := runParallelGetMetadata(t, tasks, checks)
newTasks := make([]ExecTask, 0, len(tasks))
for _, t := range tasks {
temp := t.(*testExecTask)
keys := make([]int, len(allDeps[temp.txIdx]))
i := 0
for k := range allDeps[temp.txIdx] {
keys[i] = k
i++
}
temp.dependencies = keys
newTasks = append(newTasks, temp)
}
return parallelDuration, runParallel(t, newTasks, checks, true), serialDuration
}
testExecutorCombWithMetadata(t, totalTxs, numReads, numWrites, numNonIO, taskRunner)
}
func TestDexScenario(t *testing.T) {
t.Parallel()
rand.Seed(0)
totalTxs := []int{10, 50, 100, 200, 300}
numReads := []int{20, 100, 200}
numWrites := []int{20, 100, 200}
numNonIO := []int{100, 500}
postValidation := func(pe *ParallelExecutor) error {
if pe.lastSettled == len(pe.tasks) {
for i, inputs := range pe.lastTxIO.inputs {
for _, input := range inputs {
if input.V.TxnIndex != i-1 {
return fmt.Errorf("Tx %d should depend on tx %d, but it actually depends on %d", i, i-1, input.V.TxnIndex)
}
}
}
}
return nil
}
checks := composeValidations([]PropertyCheck{checkNoStatusOverlap, postValidation, checkNoDroppedTx})
taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration) {
sender := func(i int) common.Address { return common.BigToAddress(big.NewInt(int64(i))) }
tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, dexPathGenerator, readTime, writeTime, nonIOTime)
return runParallel(t, tasks, checks, false), serialDuration
}
testExecutorComb(t, totalTxs, numReads, numWrites, numNonIO, taskRunner)
}
func TestDexScenarioWithMetadata(t *testing.T) {
t.Parallel()
rand.Seed(0)
totalTxs := []int{300}
numReads := []int{100, 200}
numWrites := []int{100, 200}
numNonIO := []int{100, 500}
postValidation := func(pe *ParallelExecutor) error {
if pe.lastSettled == len(pe.tasks) {
for i, inputs := range pe.lastTxIO.inputs {
for _, input := range inputs {
if input.V.TxnIndex != i-1 {
return fmt.Errorf("Tx %d should depend on tx %d, but it actually depends on %d", i, i-1, input.V.TxnIndex)
}
}
}
}
return nil
}
checks := composeValidations([]PropertyCheck{checkNoStatusOverlap, postValidation, checkNoDroppedTx})
taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration, time.Duration) {
sender := func(i int) common.Address { return common.BigToAddress(big.NewInt(int64(i))) }
tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, dexPathGenerator, readTime, writeTime, nonIOTime)
parallelDuration := runParallel(t, tasks, checks, false)
allDeps := runParallelGetMetadata(t, tasks, checks)
newTasks := make([]ExecTask, 0, len(tasks))
for _, t := range tasks {
temp := t.(*testExecTask)
keys := make([]int, len(allDeps[temp.txIdx]))
i := 0
for k := range allDeps[temp.txIdx] {
keys[i] = k
i++
}
temp.dependencies = keys
newTasks = append(newTasks, temp)
}
return parallelDuration, runParallel(t, newTasks, checks, true), serialDuration
}
testExecutorCombWithMetadata(t, totalTxs, numReads, numWrites, numNonIO, taskRunner)
}
func TestBreakFromCircularDependency(t *testing.T) {
t.Parallel()
rand.Seed(0)
tasks := make([]ExecTask, 5)
for i := range tasks {
tasks[i] = &testExecTask{
txIdx: i,
dependencies: []int{
(i + len(tasks) - 1) % len(tasks),
},
}
}
ctx, cancel := context.WithCancel(context.Background())
cancel()
// This should not hang
_, err := ExecuteParallel(tasks, false, true, ctx)
if err == nil {
t.Error("Expected cancel error")
}
}
func TestBreakFromPartialCircularDependency(t *testing.T) {
t.Parallel()
rand.Seed(0)
tasks := make([]ExecTask, 5)
for i := range tasks {
if i < 3 {
tasks[i] = &testExecTask{
txIdx: i,
dependencies: []int{
(i + 2) % 3,
},
}
} else {
tasks[i] = &testExecTask{
txIdx: i,
dependencies: []int{},
}
}
}
ctx, cancel := context.WithCancel(context.Background())
cancel()
// This should not hang
_, err := ExecuteParallel(tasks, false, true, ctx)
if err == nil {
t.Error("Expected cancel error")
}
}

297
core/blockstm/mvhashmap.go Normal file
View file

@ -0,0 +1,297 @@
package blockstm
import (
"fmt"
"sync"
"github.com/emirpasic/gods/maps/treemap"
"github.com/ethereum/go-ethereum/common"
)
const FlagDone = 0
const FlagEstimate = 1
const addressType = 1
const stateType = 2
const subpathType = 3
const KeyLength = common.AddressLength + common.HashLength + 2
type Key [KeyLength]byte
func (k Key) IsAddress() bool {
return k[KeyLength-1] == addressType
}
func (k Key) IsState() bool {
return k[KeyLength-1] == stateType
}
func (k Key) IsSubpath() bool {
return k[KeyLength-1] == subpathType
}
func (k Key) GetAddress() common.Address {
return common.BytesToAddress(k[:common.AddressLength])
}
func (k Key) GetStateKey() common.Hash {
return common.BytesToHash(k[common.AddressLength : KeyLength-2])
}
func (k Key) GetSubpath() byte {
return k[KeyLength-2]
}
func newKey(addr common.Address, hash common.Hash, subpath byte, keyType byte) Key {
var k Key
copy(k[:common.AddressLength], addr.Bytes())
copy(k[common.AddressLength:KeyLength-2], hash.Bytes())
k[KeyLength-2] = subpath
k[KeyLength-1] = keyType
return k
}
func NewAddressKey(addr common.Address) Key {
return newKey(addr, common.Hash{}, 0, addressType)
}
func NewStateKey(addr common.Address, hash common.Hash) Key {
k := newKey(addr, hash, 0, stateType)
if !k.IsState() {
panic(fmt.Errorf("key is not a state key"))
}
return k
}
func NewSubpathKey(addr common.Address, subpath byte) Key {
return newKey(addr, common.Hash{}, subpath, subpathType)
}
type MVHashMap struct {
m sync.Map
s sync.Map
}
func MakeMVHashMap() *MVHashMap {
return &MVHashMap{}
}
type WriteCell struct {
flag uint
incarnation int
data interface{}
}
type TxnIndexCells struct {
rw sync.RWMutex
tm *treemap.Map
}
type Version struct {
TxnIndex int
Incarnation int
}
func (mv *MVHashMap) getKeyCells(k Key, fNoKey func(kenc Key) *TxnIndexCells) (cells *TxnIndexCells) {
val, ok := mv.m.Load(k)
if !ok {
cells = fNoKey(k)
} else {
cells = val.(*TxnIndexCells)
}
return
}
func (mv *MVHashMap) Write(k Key, v Version, data interface{}) {
cells := mv.getKeyCells(k, func(kenc Key) (cells *TxnIndexCells) {
n := &TxnIndexCells{
rw: sync.RWMutex{},
tm: treemap.NewWithIntComparator(),
}
cells = n
val, _ := mv.m.LoadOrStore(kenc, n)
cells = val.(*TxnIndexCells)
return
})
cells.rw.RLock()
ci, ok := cells.tm.Get(v.TxnIndex)
cells.rw.RUnlock()
if ok {
if ci.(*WriteCell).incarnation > v.Incarnation {
panic(fmt.Errorf("existing transaction value does not have lower incarnation: %v, %v",
k, v.TxnIndex))
}
ci.(*WriteCell).flag = FlagDone
ci.(*WriteCell).incarnation = v.Incarnation
ci.(*WriteCell).data = data
} else {
cells.rw.Lock()
if ci, ok = cells.tm.Get(v.TxnIndex); !ok {
cells.tm.Put(v.TxnIndex, &WriteCell{
flag: FlagDone,
incarnation: v.Incarnation,
data: data,
})
} else {
ci.(*WriteCell).flag = FlagDone
ci.(*WriteCell).incarnation = v.Incarnation
ci.(*WriteCell).data = data
}
cells.rw.Unlock()
}
}
func (mv *MVHashMap) ReadStorage(k Key, fallBack func() any) any {
data, ok := mv.s.Load(string(k[:]))
if !ok {
data = fallBack()
data, _ = mv.s.LoadOrStore(string(k[:]), data)
}
return data
}
func (mv *MVHashMap) MarkEstimate(k Key, txIdx int) {
cells := mv.getKeyCells(k, func(_ Key) *TxnIndexCells {
panic(fmt.Errorf("path must already exist"))
})
cells.rw.RLock()
if ci, ok := cells.tm.Get(txIdx); !ok {
panic(fmt.Sprintf("should not happen - cell should be present for path. TxIdx: %v, path, %x, cells keys: %v", txIdx, k, cells.tm.Keys()))
} else {
ci.(*WriteCell).flag = FlagEstimate
}
cells.rw.RUnlock()
}
func (mv *MVHashMap) Delete(k Key, txIdx int) {
cells := mv.getKeyCells(k, func(_ Key) *TxnIndexCells {
panic(fmt.Errorf("path must already exist"))
})
cells.rw.Lock()
defer cells.rw.Unlock()
cells.tm.Remove(txIdx)
}
const (
MVReadResultDone = 0
MVReadResultDependency = 1
MVReadResultNone = 2
)
type MVReadResult struct {
depIdx int
incarnation int
value interface{}
}
func (res *MVReadResult) DepIdx() int {
return res.depIdx
}
func (res *MVReadResult) Incarnation() int {
return res.incarnation
}
func (res *MVReadResult) Value() interface{} {
return res.value
}
func (mvr MVReadResult) Status() int {
if mvr.depIdx != -1 {
if mvr.incarnation == -1 {
return MVReadResultDependency
} else {
return MVReadResultDone
}
}
return MVReadResultNone
}
func (mv *MVHashMap) Read(k Key, txIdx int) (res MVReadResult) {
res.depIdx = -1
res.incarnation = -1
cells := mv.getKeyCells(k, func(_ Key) *TxnIndexCells {
return nil
})
if cells == nil {
return
}
cells.rw.RLock()
fk, fv := cells.tm.Floor(txIdx - 1)
cells.rw.RUnlock()
if fk != nil && fv != nil {
c := fv.(*WriteCell)
switch c.flag {
case FlagEstimate:
res.depIdx = fk.(int)
res.value = c.data
case FlagDone:
{
res.depIdx = fk.(int)
res.incarnation = c.incarnation
res.value = c.data
}
default:
panic(fmt.Errorf("should not happen - unknown flag value"))
}
}
return
}
func (mv *MVHashMap) FlushMVWriteSet(writes []WriteDescriptor) {
for _, v := range writes {
mv.Write(v.Path, v.V, v.Val)
}
}
func ValidateVersion(txIdx int, lastInputOutput *TxnInputOutput, versionedData *MVHashMap) (valid bool) {
valid = true
for _, rd := range lastInputOutput.ReadSet(txIdx) {
mvResult := versionedData.Read(rd.Path, txIdx)
switch mvResult.Status() {
case MVReadResultDone:
// Having a write record for a path in MVHashmap doesn't necessarily mean there is a conflict, because MVHashmap
// is a superset of the actual write set.
// Check if the write record is actually in write set. If not, skip the key.
if mvResult.depIdx >= 0 && !lastInputOutput.HasWritten(mvResult.depIdx, rd.Path) {
continue
}
valid = rd.Kind == ReadKindMap && rd.V == Version{
TxnIndex: mvResult.depIdx,
Incarnation: mvResult.incarnation,
}
case MVReadResultDependency:
valid = false
case MVReadResultNone:
valid = rd.Kind == ReadKindStorage // feels like an assertion?
default:
panic(fmt.Errorf("should not happen - undefined mv read status: %ver", mvResult.Status()))
}
if !valid {
break
}
}
return
}

View file

@ -0,0 +1,344 @@
package blockstm
import (
"fmt"
"math/big"
"math/rand"
"testing"
"github.com/stretchr/testify/require"
"github.com/ethereum/go-ethereum/common"
)
var randomness = rand.Intn(10) + 10
// create test data for a given txIdx and incarnation
func valueFor(txIdx, inc int) []byte {
return []byte(fmt.Sprintf("%ver:%ver:%ver", txIdx*5, txIdx+inc, inc*5))
}
func getCommonAddress(i int) common.Address {
return common.BigToAddress(big.NewInt(int64(i % randomness)))
}
func TestHelperFunctions(t *testing.T) {
t.Parallel()
ap1 := NewAddressKey(getCommonAddress(1))
ap2 := NewAddressKey(getCommonAddress(2))
mvh := MakeMVHashMap()
mvh.Write(ap1, Version{0, 1}, valueFor(0, 1))
mvh.Write(ap1, Version{0, 2}, valueFor(0, 2))
res := mvh.Read(ap1, 0)
require.Equal(t, -1, res.DepIdx())
require.Equal(t, -1, res.Incarnation())
require.Equal(t, 2, res.Status())
mvh.Write(ap2, Version{1, 1}, valueFor(1, 1))
mvh.Write(ap2, Version{1, 2}, valueFor(1, 2))
res = mvh.Read(ap2, 1)
require.Equal(t, -1, res.DepIdx())
require.Equal(t, -1, res.Incarnation())
require.Equal(t, 2, res.Status())
mvh.Write(ap1, Version{2, 1}, valueFor(2, 1))
mvh.Write(ap1, Version{2, 2}, valueFor(2, 2))
res = mvh.Read(ap1, 2)
require.Equal(t, 0, res.DepIdx())
require.Equal(t, 2, res.Incarnation())
require.Equal(t, valueFor(0, 2), res.Value().([]byte))
require.Equal(t, 0, res.Status())
}
func TestFlushMVWrite(t *testing.T) {
t.Parallel()
ap1 := NewAddressKey(getCommonAddress(1))
ap2 := NewAddressKey(getCommonAddress(2))
mvh := MakeMVHashMap()
var res MVReadResult
wd := []WriteDescriptor{}
wd = append(wd, WriteDescriptor{
Path: ap1,
V: Version{0, 1},
Val: valueFor(0, 1),
})
wd = append(wd, WriteDescriptor{
Path: ap1,
V: Version{0, 2},
Val: valueFor(0, 2),
})
wd = append(wd, WriteDescriptor{
Path: ap2,
V: Version{1, 1},
Val: valueFor(1, 1),
})
wd = append(wd, WriteDescriptor{
Path: ap2,
V: Version{1, 2},
Val: valueFor(1, 2),
})
wd = append(wd, WriteDescriptor{
Path: ap1,
V: Version{2, 1},
Val: valueFor(2, 1),
})
wd = append(wd, WriteDescriptor{
Path: ap1,
V: Version{2, 2},
Val: valueFor(2, 2),
})
mvh.FlushMVWriteSet(wd)
res = mvh.Read(ap1, 0)
require.Equal(t, -1, res.DepIdx())
require.Equal(t, -1, res.Incarnation())
require.Equal(t, 2, res.Status())
res = mvh.Read(ap2, 1)
require.Equal(t, -1, res.DepIdx())
require.Equal(t, -1, res.Incarnation())
require.Equal(t, 2, res.Status())
res = mvh.Read(ap1, 2)
require.Equal(t, 0, res.DepIdx())
require.Equal(t, 2, res.Incarnation())
require.Equal(t, valueFor(0, 2), res.Value().([]byte))
require.Equal(t, 0, res.Status())
}
// TODO - handle panic
func TestLowerIncarnation(t *testing.T) {
t.Parallel()
ap1 := NewAddressKey(getCommonAddress(1))
mvh := MakeMVHashMap()
mvh.Write(ap1, Version{0, 2}, valueFor(0, 2))
mvh.Read(ap1, 0)
mvh.Write(ap1, Version{1, 2}, valueFor(1, 2))
mvh.Write(ap1, Version{0, 5}, valueFor(0, 5))
mvh.Write(ap1, Version{1, 5}, valueFor(1, 5))
}
func TestMarkEstimate(t *testing.T) {
t.Parallel()
ap1 := NewAddressKey(getCommonAddress(1))
mvh := MakeMVHashMap()
mvh.Write(ap1, Version{7, 2}, valueFor(7, 2))
mvh.MarkEstimate(ap1, 7)
mvh.Write(ap1, Version{7, 4}, valueFor(7, 4))
}
func TestMVHashMapBasics(t *testing.T) {
t.Parallel()
// memory locations
ap1 := NewAddressKey(getCommonAddress(1))
ap2 := NewAddressKey(getCommonAddress(2))
ap3 := NewAddressKey(getCommonAddress(3))
mvh := MakeMVHashMap()
res := mvh.Read(ap1, 5)
require.Equal(t, -1, res.depIdx)
mvh.Write(ap1, Version{10, 1}, valueFor(10, 1))
res = mvh.Read(ap1, 9)
require.Equal(t, -1, res.depIdx, "reads that should go the the DB return dependency -1")
res = mvh.Read(ap1, 10)
require.Equal(t, -1, res.depIdx, "Read returns entries from smaller txns, not txn 10")
// Reads for a higher txn return the entry written by txn 10.
res = mvh.Read(ap1, 15)
require.Equal(t, 10, res.depIdx, "reads for a higher txn return the entry written by txn 10.")
require.Equal(t, 1, res.incarnation)
require.Equal(t, valueFor(10, 1), res.value)
// More writes.
mvh.Write(ap1, Version{12, 0}, valueFor(12, 0))
mvh.Write(ap1, Version{8, 3}, valueFor(8, 3))
// Verify reads.
res = mvh.Read(ap1, 15)
require.Equal(t, 12, res.depIdx)
require.Equal(t, 0, res.incarnation)
require.Equal(t, valueFor(12, 0), res.value)
res = mvh.Read(ap1, 11)
require.Equal(t, 10, res.depIdx)
require.Equal(t, 1, res.incarnation)
require.Equal(t, valueFor(10, 1), res.value)
res = mvh.Read(ap1, 10)
require.Equal(t, 8, res.depIdx)
require.Equal(t, 3, res.incarnation)
require.Equal(t, valueFor(8, 3), res.value)
// Mark the entry written by 10 as an estimate.
mvh.MarkEstimate(ap1, 10)
res = mvh.Read(ap1, 11)
require.Equal(t, 10, res.depIdx)
require.Equal(t, -1, res.incarnation, "dep at tx 10 is now an estimate")
// Delete the entry written by 10, write to a different ap.
mvh.Delete(ap1, 10)
mvh.Write(ap2, Version{10, 2}, valueFor(10, 2))
// Read by txn 11 no longer observes entry from txn 10.
res = mvh.Read(ap1, 11)
require.Equal(t, 8, res.depIdx)
require.Equal(t, 3, res.incarnation)
require.Equal(t, valueFor(8, 3), res.value)
// Reads, writes for ap2 and ap3.
mvh.Write(ap2, Version{5, 0}, valueFor(5, 0))
mvh.Write(ap3, Version{20, 4}, valueFor(20, 4))
res = mvh.Read(ap2, 10)
require.Equal(t, 5, res.depIdx)
require.Equal(t, 0, res.incarnation)
require.Equal(t, valueFor(5, 0), res.value)
res = mvh.Read(ap3, 21)
require.Equal(t, 20, res.depIdx)
require.Equal(t, 4, res.incarnation)
require.Equal(t, valueFor(20, 4), res.value)
// Clear ap1 and ap3.
mvh.Delete(ap1, 12)
mvh.Delete(ap1, 8)
mvh.Delete(ap3, 20)
// Reads from ap1 and ap3 go to db.
res = mvh.Read(ap1, 30)
require.Equal(t, -1, res.depIdx)
res = mvh.Read(ap3, 30)
require.Equal(t, -1, res.depIdx)
// No-op delete at ap2 - doesn't panic because ap2 does exist
mvh.Delete(ap2, 11)
// Read entry by txn 10 at ap2.
res = mvh.Read(ap2, 15)
require.Equal(t, 10, res.depIdx)
require.Equal(t, 2, res.incarnation)
require.Equal(t, valueFor(10, 2), res.value)
}
func BenchmarkWriteTimeSameLocationDifferentTxIdx(b *testing.B) {
mvh2 := MakeMVHashMap()
ap2 := NewAddressKey(getCommonAddress(2))
randInts := []int{}
for i := 0; i < b.N; i++ {
randInts = append(randInts, rand.Intn(1000000000000000))
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
mvh2.Write(ap2, Version{randInts[i], 1}, valueFor(randInts[i], 1))
}
}
func BenchmarkReadTimeSameLocationDifferentTxIdx(b *testing.B) {
mvh2 := MakeMVHashMap()
ap2 := NewAddressKey(getCommonAddress(2))
txIdxSlice := []int{}
for i := 0; i < b.N; i++ {
txIdx := rand.Intn(1000000000000000)
txIdxSlice = append(txIdxSlice, txIdx)
mvh2.Write(ap2, Version{txIdx, 1}, valueFor(txIdx, 1))
}
b.ResetTimer()
for _, value := range txIdxSlice {
mvh2.Read(ap2, value)
}
}
func TestTimeComplexity(t *testing.T) {
t.Parallel()
// for 1000000 read and write with no dependency at different memory location
mvh1 := MakeMVHashMap()
for i := 0; i < 1000000; i++ {
ap1 := NewAddressKey(getCommonAddress(i))
mvh1.Write(ap1, Version{i, 1}, valueFor(i, 1))
mvh1.Read(ap1, i)
}
// for 1000000 read and write with dependency at same memory location
mvh2 := MakeMVHashMap()
ap2 := NewAddressKey(getCommonAddress(2))
for i := 0; i < 1000000; i++ {
mvh2.Write(ap2, Version{i, 1}, valueFor(i, 1))
mvh2.Read(ap2, i)
}
}
func TestWriteTimeSameLocationDifferentTxnIdx(t *testing.T) {
t.Parallel()
mvh1 := MakeMVHashMap()
ap1 := NewAddressKey(getCommonAddress(1))
for i := 0; i < 1000000; i++ {
mvh1.Write(ap1, Version{i, 1}, valueFor(i, 1))
}
}
func TestWriteTimeSameLocationSameTxnIdx(t *testing.T) {
t.Parallel()
mvh1 := MakeMVHashMap()
ap1 := NewAddressKey(getCommonAddress(1))
for i := 0; i < 1000000; i++ {
mvh1.Write(ap1, Version{1, i}, valueFor(i, 1))
}
}
func TestWriteTimeDifferentLocation(t *testing.T) {
t.Parallel()
mvh1 := MakeMVHashMap()
for i := 0; i < 1000000; i++ {
ap1 := NewAddressKey(getCommonAddress(i))
mvh1.Write(ap1, Version{i, 1}, valueFor(i, 1))
}
}
func TestReadTimeSameLocation(t *testing.T) {
t.Parallel()
mvh1 := MakeMVHashMap()
ap1 := NewAddressKey(getCommonAddress(1))
mvh1.Write(ap1, Version{1, 1}, valueFor(1, 1))
for i := 0; i < 1000000; i++ {
mvh1.Read(ap1, 2)
}
}

225
core/blockstm/status.go Normal file
View file

@ -0,0 +1,225 @@
package blockstm
import (
"fmt"
"sort"
)
func makeStatusManager(numTasks int) (t taskStatusManager) {
t.pending = make([]int, numTasks)
for i := 0; i < numTasks; i++ {
t.pending[i] = i
}
t.dependency = make(map[int]map[int]bool, numTasks)
t.blocker = make(map[int]map[int]bool, numTasks)
for i := 0; i < numTasks; i++ {
t.blocker[i] = make(map[int]bool)
}
return
}
type taskStatusManager struct {
pending []int
inProgress []int
complete []int
dependency map[int]map[int]bool
blocker map[int]map[int]bool
}
func insertInList(l []int, v int) []int {
if len(l) == 0 || v > l[len(l)-1] {
return append(l, v)
} else {
x := sort.SearchInts(l, v)
if x < len(l) && l[x] == v {
// already in list
return l
}
a := append(l[:x+1], l[x:]...)
a[x] = v
return a
}
}
func (m *taskStatusManager) takeNextPending() int {
if len(m.pending) == 0 {
return -1
}
x := m.pending[0]
m.pending = m.pending[1:]
m.inProgress = insertInList(m.inProgress, x)
return x
}
func hasNoGap(l []int) bool {
return l[0]+len(l) == l[len(l)-1]+1
}
func (m taskStatusManager) maxAllComplete() int {
if len(m.complete) == 0 || m.complete[0] != 0 {
return -1
} else if m.complete[len(m.complete)-1] == len(m.complete)-1 {
return m.complete[len(m.complete)-1]
} else {
for i := len(m.complete) - 2; i >= 0; i-- {
if hasNoGap(m.complete[:i+1]) {
return m.complete[i]
}
}
}
return -1
}
func (m *taskStatusManager) pushPending(tx int) {
m.pending = insertInList(m.pending, tx)
}
func removeFromList(l []int, v int, expect bool) []int {
x := sort.SearchInts(l, v)
if x == -1 || l[x] != v {
if expect {
panic(fmt.Errorf("should not happen - element expected in list"))
}
return l
}
switch x {
case 0:
return l[1:]
case len(l) - 1:
return l[:len(l)-1]
default:
return append(l[:x], l[x+1:]...)
}
}
func (m *taskStatusManager) markComplete(tx int) {
m.inProgress = removeFromList(m.inProgress, tx, true)
m.complete = insertInList(m.complete, tx)
}
func (m *taskStatusManager) minPending() int {
if len(m.pending) == 0 {
return -1
} else {
return m.pending[0]
}
}
func (m *taskStatusManager) countComplete() int {
return len(m.complete)
}
func (m *taskStatusManager) addDependencies(blocker int, dependent int) bool {
if blocker < 0 || blocker >= dependent {
return false
}
curblockers := m.blocker[dependent]
if m.checkComplete(blocker) {
// Blocker has already completed
delete(curblockers, blocker)
return len(curblockers) > 0
}
if _, ok := m.dependency[blocker]; !ok {
m.dependency[blocker] = make(map[int]bool)
}
m.dependency[blocker][dependent] = true
curblockers[blocker] = true
return true
}
func (m *taskStatusManager) isBlocked(tx int) bool {
return len(m.blocker[tx]) > 0
}
func (m *taskStatusManager) removeDependency(tx int) {
if deps, ok := m.dependency[tx]; ok && len(deps) > 0 {
for k := range deps {
delete(m.blocker[k], tx)
if len(m.blocker[k]) == 0 {
if !m.checkComplete(k) && !m.checkPending(k) && !m.checkInProgress(k) {
m.pushPending(k)
}
}
}
delete(m.dependency, tx)
}
}
func (m *taskStatusManager) clearInProgress(tx int) {
m.inProgress = removeFromList(m.inProgress, tx, true)
}
func (m *taskStatusManager) checkInProgress(tx int) bool {
x := sort.SearchInts(m.inProgress, tx)
if x < len(m.inProgress) && m.inProgress[x] == tx {
return true
}
return false
}
func (m *taskStatusManager) checkPending(tx int) bool {
x := sort.SearchInts(m.pending, tx)
if x < len(m.pending) && m.pending[x] == tx {
return true
}
return false
}
func (m *taskStatusManager) checkComplete(tx int) bool {
x := sort.SearchInts(m.complete, tx)
if x < len(m.complete) && m.complete[x] == tx {
return true
}
return false
}
// getRevalidationRange: this range will be all tasks from tx (inclusive) that are not currently in progress up to the
//
// 'all complete' limit
func (m *taskStatusManager) getRevalidationRange(txFrom int) (ret []int) {
max := m.maxAllComplete() // haven't learned to trust compilers :)
for x := txFrom; x <= max; x++ {
if !m.checkInProgress(x) {
ret = append(ret, x)
}
}
return
}
func (m *taskStatusManager) pushPendingSet(set []int) {
for _, v := range set {
if m.checkComplete(v) {
m.clearComplete(v)
}
m.pushPending(v)
}
}
func (m *taskStatusManager) clearComplete(tx int) {
m.complete = removeFromList(m.complete, tx, false)
}
func (m *taskStatusManager) clearPending(tx int) {
m.pending = removeFromList(m.pending, tx, false)
}

View file

@ -0,0 +1,82 @@
package blockstm
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestStatusBasics(t *testing.T) {
t.Parallel()
s := makeStatusManager(10)
x := s.takeNextPending()
require.Equal(t, 0, x)
require.True(t, s.checkInProgress(x))
x = s.takeNextPending()
require.Equal(t, 1, x)
require.True(t, s.checkInProgress(x))
x = s.takeNextPending()
require.Equal(t, 2, x)
require.True(t, s.checkInProgress(x))
s.markComplete(0)
require.False(t, s.checkInProgress(0))
s.markComplete(1)
s.markComplete(2)
require.False(t, s.checkInProgress(1))
require.False(t, s.checkInProgress(2))
require.Equal(t, 2, s.maxAllComplete())
x = s.takeNextPending()
require.Equal(t, 3, x)
x = s.takeNextPending()
require.Equal(t, 4, x)
s.markComplete(x)
require.False(t, s.checkInProgress(4))
require.Equal(t, 2, s.maxAllComplete(), "zero should still be min complete")
exp := []int{1, 2}
require.Equal(t, exp, s.getRevalidationRange(1))
}
func TestMaxComplete(t *testing.T) {
t.Parallel()
s := makeStatusManager(10)
for {
tx := s.takeNextPending()
if tx == -1 {
break
}
if tx != 7 {
s.markComplete(tx)
}
}
require.Equal(t, 6, s.maxAllComplete())
s2 := makeStatusManager(10)
for {
tx := s2.takeNextPending()
if tx == -1 {
break
}
}
s2.markComplete(2)
s2.markComplete(4)
require.Equal(t, -1, s2.maxAllComplete())
s2.complete = insertInList(s2.complete, 4)
require.Equal(t, 2, s2.countComplete())
}

106
core/blockstm/txio.go Normal file
View file

@ -0,0 +1,106 @@
package blockstm
const (
ReadKindMap = 0
ReadKindStorage = 1
)
type ReadDescriptor struct {
Path Key
Kind int
V Version
}
type WriteDescriptor struct {
Path Key
V Version
Val interface{}
}
type TxnInput []ReadDescriptor
type TxnOutput []WriteDescriptor
// hasNewWrite: returns true if the current set has a new write compared to the input
func (txo TxnOutput) hasNewWrite(cmpSet []WriteDescriptor) bool {
if len(txo) == 0 {
return false
} else if len(cmpSet) == 0 || len(txo) > len(cmpSet) {
return true
}
cmpMap := map[Key]bool{cmpSet[0].Path: true}
for i := 1; i < len(cmpSet); i++ {
cmpMap[cmpSet[i].Path] = true
}
for _, v := range txo {
if !cmpMap[v.Path] {
return true
}
}
return false
}
type TxnInputOutput struct {
inputs []TxnInput
outputs []TxnOutput // write sets that should be checked during validation
outputsSet []map[Key]struct{}
allOutputs []TxnOutput // entire write sets in MVHashMap. allOutputs should always be a parent set of outputs
}
func (io *TxnInputOutput) ReadSet(txnIdx int) []ReadDescriptor {
return io.inputs[txnIdx]
}
func (io *TxnInputOutput) WriteSet(txnIdx int) []WriteDescriptor {
return io.outputs[txnIdx]
}
func (io *TxnInputOutput) AllWriteSet(txnIdx int) []WriteDescriptor {
return io.allOutputs[txnIdx]
}
func (io *TxnInputOutput) HasWritten(txnIdx int, k Key) bool {
_, ok := io.outputsSet[txnIdx][k]
return ok
}
func MakeTxnInputOutput(numTx int) *TxnInputOutput {
return &TxnInputOutput{
inputs: make([]TxnInput, numTx),
outputs: make([]TxnOutput, numTx),
outputsSet: make([]map[Key]struct{}, numTx),
allOutputs: make([]TxnOutput, numTx),
}
}
func (io *TxnInputOutput) recordRead(txId int, input []ReadDescriptor) {
io.inputs[txId] = input
}
func (io *TxnInputOutput) recordWrite(txId int, output []WriteDescriptor) {
io.outputs[txId] = output
io.outputsSet[txId] = make(map[Key]struct{}, len(output))
for _, v := range output {
io.outputsSet[txId][v.Path] = struct{}{}
}
}
func (io *TxnInputOutput) recordAllWrite(txId int, output []WriteDescriptor) {
io.allOutputs[txId] = output
}
func (io *TxnInputOutput) RecordReadAtOnce(inputs [][]ReadDescriptor) {
for ind, val := range inputs {
io.inputs[ind] = val
}
}
func (io *TxnInputOutput) RecordAllWriteAtOnce(outputs [][]WriteDescriptor) {
for ind, val := range outputs {
io.allOutputs[ind] = val
}
}

View file

@ -335,6 +335,19 @@ func makeBlockChain(parent *types.Block, n int, engine consensus.Engine, db ethd
return blocks
}
// makeBlockChain creates a deterministic chain of blocks rooted at parent with fake invalid transactions.
func makeFakeNonEmptyBlockChain(parent *types.Block, n int, engine consensus.Engine, db ethdb.Database, seed int, numTx int) []*types.Block {
blocks, _ := GenerateChain(params.TestChainConfig, parent, engine, db, n, func(i int, b *BlockGen) {
addr := common.Address{0: byte(seed), 19: byte(i)}
b.SetCoinbase(addr)
for j := 0; j < numTx; j++ {
b.txs = append(b.txs, types.NewTransaction(0, addr, big.NewInt(1000), params.TxGas, nil, nil))
}
})
return blocks
}
type fakeChainReader struct {
config *params.ChainConfig
stateSyncData []*types.StateSyncData

View file

@ -18,6 +18,7 @@ package core
import (
"math/big"
"sync"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/consensus"
@ -83,7 +84,12 @@ func GetHashFn(ref *types.Header, chain ChainContext) func(n uint64) common.Hash
// Then fill up with [refHash.p, refHash.pp, refHash.ppp, ...]
var cache []common.Hash
cacheMutex := &sync.Mutex{}
return func(n uint64) common.Hash {
cacheMutex.Lock()
defer cacheMutex.Unlock()
// If there's no hash cache yet, make one
if len(cache) == 0 {
cache = append(cache, ref.ParentHash)

View file

@ -0,0 +1,429 @@
// Copyright 2015 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package core
import (
"context"
"fmt"
"math/big"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/consensus"
"github.com/ethereum/go-ethereum/consensus/misc"
"github.com/ethereum/go-ethereum/core/blockstm"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/metrics"
"github.com/ethereum/go-ethereum/params"
)
type ParallelEVMConfig struct {
Enable bool
SpeculativeProcesses int
}
// StateProcessor is a basic Processor, which takes care of transitioning
// state from one point to another.
//
// StateProcessor implements Processor.
type ParallelStateProcessor struct {
config *params.ChainConfig // Chain configuration options
bc *BlockChain // Canonical block chain
engine consensus.Engine // Consensus engine used for block rewards
}
// NewStateProcessor initialises a new StateProcessor.
func NewParallelStateProcessor(config *params.ChainConfig, bc *BlockChain, engine consensus.Engine) *ParallelStateProcessor {
return &ParallelStateProcessor{
config: config,
bc: bc,
engine: engine,
}
}
type ExecutionTask struct {
msg types.Message
config *params.ChainConfig
gasLimit uint64
blockNumber *big.Int
blockHash common.Hash
tx *types.Transaction
index int
statedb *state.StateDB // State database that stores the modified values after tx execution.
cleanStateDB *state.StateDB // A clean copy of the initial statedb. It should not be modified.
finalStateDB *state.StateDB // The final statedb.
header *types.Header
blockChain *BlockChain
evmConfig vm.Config
result *ExecutionResult
shouldDelayFeeCal *bool
shouldRerunWithoutFeeDelay bool
sender common.Address
totalUsedGas *uint64
receipts *types.Receipts
allLogs *[]*types.Log
// length of dependencies -> 2 + k (k = a whole number)
// first 2 element in dependencies -> transaction index, and flag representing if delay is allowed or not
// (0 -> delay is not allowed, 1 -> delay is allowed)
// next k elements in dependencies -> transaction indexes on which transaction i is dependent on
dependencies []int
coinbase common.Address
blockContext vm.BlockContext
}
func (task *ExecutionTask) Execute(mvh *blockstm.MVHashMap, incarnation int) (err error) {
task.statedb = task.cleanStateDB.Copy()
task.statedb.Prepare(task.tx.Hash(), task.index)
task.statedb.SetMVHashmap(mvh)
task.statedb.SetIncarnation(incarnation)
evm := vm.NewEVM(task.blockContext, vm.TxContext{}, task.statedb, task.config, task.evmConfig)
// Create a new context to be used in the EVM environment.
txContext := NewEVMTxContext(task.msg)
evm.Reset(txContext, task.statedb)
defer func() {
if r := recover(); r != nil {
// In some pre-matured executions, EVM will panic. Recover from panic and retry the execution.
log.Debug("Recovered from EVM failure.", "Error:", r)
err = blockstm.ErrExecAbortError{Dependency: task.statedb.DepTxIndex()}
return
}
}()
// Apply the transaction to the current state (included in the env).
if *task.shouldDelayFeeCal {
task.result, err = ApplyMessageNoFeeBurnOrTip(evm, task.msg, new(GasPool).AddGas(task.gasLimit), nil)
if task.result == nil || err != nil {
return blockstm.ErrExecAbortError{Dependency: task.statedb.DepTxIndex(), OriginError: err}
}
reads := task.statedb.MVReadMap()
if _, ok := reads[blockstm.NewSubpathKey(task.blockContext.Coinbase, state.BalancePath)]; ok {
log.Info("Coinbase is in MVReadMap", "address", task.blockContext.Coinbase)
task.shouldRerunWithoutFeeDelay = true
}
if _, ok := reads[blockstm.NewSubpathKey(task.result.BurntContractAddress, state.BalancePath)]; ok {
log.Info("BurntContractAddress is in MVReadMap", "address", task.result.BurntContractAddress)
task.shouldRerunWithoutFeeDelay = true
}
} else {
task.result, err = ApplyMessage(evm, task.msg, new(GasPool).AddGas(task.gasLimit), nil)
}
if task.statedb.HadInvalidRead() || err != nil {
err = blockstm.ErrExecAbortError{Dependency: task.statedb.DepTxIndex(), OriginError: err}
return
}
task.statedb.Finalise(task.config.IsEIP158(task.blockNumber))
return
}
func (task *ExecutionTask) MVReadList() []blockstm.ReadDescriptor {
return task.statedb.MVReadList()
}
func (task *ExecutionTask) MVWriteList() []blockstm.WriteDescriptor {
return task.statedb.MVWriteList()
}
func (task *ExecutionTask) MVFullWriteList() []blockstm.WriteDescriptor {
return task.statedb.MVFullWriteList()
}
func (task *ExecutionTask) Sender() common.Address {
return task.sender
}
func (task *ExecutionTask) Hash() common.Hash {
return task.tx.Hash()
}
func (task *ExecutionTask) Dependencies() []int {
return task.dependencies
}
func (task *ExecutionTask) Settle() {
task.finalStateDB.Prepare(task.tx.Hash(), task.index)
coinbaseBalance := task.finalStateDB.GetBalance(task.coinbase)
task.finalStateDB.ApplyMVWriteSet(task.statedb.MVFullWriteList())
for _, l := range task.statedb.GetLogs(task.tx.Hash(), task.blockHash) {
task.finalStateDB.AddLog(l)
}
if *task.shouldDelayFeeCal {
if task.config.IsLondon(task.blockNumber) {
task.finalStateDB.AddBalance(task.result.BurntContractAddress, task.result.FeeBurnt)
}
task.finalStateDB.AddBalance(task.coinbase, task.result.FeeTipped)
output1 := new(big.Int).SetBytes(task.result.SenderInitBalance.Bytes())
output2 := new(big.Int).SetBytes(coinbaseBalance.Bytes())
// Deprecating transfer log and will be removed in future fork. PLEASE DO NOT USE this transfer log going forward. Parameters won't get updated as expected going forward with EIP1559
// add transfer log
AddFeeTransferLog(
task.finalStateDB,
task.msg.From(),
task.coinbase,
task.result.FeeTipped,
task.result.SenderInitBalance,
coinbaseBalance,
output1.Sub(output1, task.result.FeeTipped),
output2.Add(output2, task.result.FeeTipped),
)
}
for k, v := range task.statedb.Preimages() {
task.finalStateDB.AddPreimage(k, v)
}
// Update the state with pending changes.
var root []byte
if task.config.IsByzantium(task.blockNumber) {
task.finalStateDB.Finalise(true)
} else {
root = task.finalStateDB.IntermediateRoot(task.config.IsEIP158(task.blockNumber)).Bytes()
}
*task.totalUsedGas += task.result.UsedGas
// Create a new receipt for the transaction, storing the intermediate root and gas used
// by the tx.
receipt := &types.Receipt{Type: task.tx.Type(), PostState: root, CumulativeGasUsed: *task.totalUsedGas}
if task.result.Failed() {
receipt.Status = types.ReceiptStatusFailed
} else {
receipt.Status = types.ReceiptStatusSuccessful
}
receipt.TxHash = task.tx.Hash()
receipt.GasUsed = task.result.UsedGas
// If the transaction created a contract, store the creation address in the receipt.
if task.msg.To() == nil {
receipt.ContractAddress = crypto.CreateAddress(task.msg.From(), task.tx.Nonce())
}
// Set the receipt logs and create the bloom filter.
receipt.Logs = task.finalStateDB.GetLogs(task.tx.Hash(), task.blockHash)
receipt.Bloom = types.CreateBloom(types.Receipts{receipt})
receipt.BlockHash = task.blockHash
receipt.BlockNumber = task.blockNumber
receipt.TransactionIndex = uint(task.finalStateDB.TxIndex())
*task.receipts = append(*task.receipts, receipt)
*task.allLogs = append(*task.allLogs, receipt.Logs...)
}
var parallelizabilityTimer = metrics.NewRegisteredTimer("block/parallelizability", nil)
// Process processes the state changes according to the Ethereum rules by running
// the transaction messages using the statedb and applying any rewards to both
// the processor (coinbase) and any included uncles.
//
// Process returns the receipts and logs accumulated during the process and
// returns the amount of gas that was used in the process. If any of the
// transactions failed to execute due to insufficient gas it will return an error.
// nolint:gocognit
func (p *ParallelStateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg vm.Config, interruptCtx context.Context) (types.Receipts, []*types.Log, uint64, error) {
blockstm.SetProcs(cfg.ParallelSpeculativeProcesses)
var (
receipts types.Receipts
header = block.Header()
blockHash = block.Hash()
blockNumber = block.Number()
allLogs []*types.Log
usedGas = new(uint64)
metadata bool
)
// Mutate the block and state according to any hard-fork specs
if p.config.DAOForkSupport && p.config.DAOForkBlock != nil && p.config.DAOForkBlock.Cmp(block.Number()) == 0 {
misc.ApplyDAOHardFork(statedb)
}
tasks := make([]blockstm.ExecTask, 0, len(block.Transactions()))
shouldDelayFeeCal := true
coinbase, _ := p.bc.Engine().Author(header)
deps := GetDeps(block.Header().TxDependency)
if block.Header().TxDependency != nil {
metadata = true
}
blockContext := NewEVMBlockContext(header, p.bc, nil)
// Iterate over and process the individual transactions
for i, tx := range block.Transactions() {
msg, err := tx.AsMessage(types.MakeSigner(p.config, header.Number), header.BaseFee)
if err != nil {
log.Error("error creating message", "err", err)
return nil, nil, 0, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err)
}
cleansdb := statedb.Copy()
if msg.From() == coinbase {
shouldDelayFeeCal = false
}
if len(header.TxDependency) != len(block.Transactions()) {
task := &ExecutionTask{
msg: msg,
config: p.config,
gasLimit: block.GasLimit(),
blockNumber: blockNumber,
blockHash: blockHash,
tx: tx,
index: i,
cleanStateDB: cleansdb,
finalStateDB: statedb,
blockChain: p.bc,
header: header,
evmConfig: cfg,
shouldDelayFeeCal: &shouldDelayFeeCal,
sender: msg.From(),
totalUsedGas: usedGas,
receipts: &receipts,
allLogs: &allLogs,
dependencies: deps[i],
coinbase: coinbase,
blockContext: blockContext,
}
tasks = append(tasks, task)
} else {
task := &ExecutionTask{
msg: msg,
config: p.config,
gasLimit: block.GasLimit(),
blockNumber: blockNumber,
blockHash: blockHash,
tx: tx,
index: i,
cleanStateDB: cleansdb,
finalStateDB: statedb,
blockChain: p.bc,
header: header,
evmConfig: cfg,
shouldDelayFeeCal: &shouldDelayFeeCal,
sender: msg.From(),
totalUsedGas: usedGas,
receipts: &receipts,
allLogs: &allLogs,
dependencies: nil,
coinbase: coinbase,
blockContext: blockContext,
}
tasks = append(tasks, task)
}
}
backupStateDB := statedb.Copy()
profile := false
result, err := blockstm.ExecuteParallel(tasks, profile, metadata, interruptCtx)
if err == nil && profile && result.Deps != nil {
_, weight := result.Deps.LongestPath(*result.Stats)
serialWeight := uint64(0)
for i := 0; i < len(result.Deps.GetVertices()); i++ {
serialWeight += (*result.Stats)[i].End - (*result.Stats)[i].Start
}
parallelizabilityTimer.Update(time.Duration(serialWeight * 100 / weight))
}
for _, task := range tasks {
task := task.(*ExecutionTask)
if task.shouldRerunWithoutFeeDelay {
shouldDelayFeeCal = false
statedb.StopPrefetcher()
*statedb = *backupStateDB
allLogs = []*types.Log{}
receipts = types.Receipts{}
usedGas = new(uint64)
for _, t := range tasks {
t := t.(*ExecutionTask)
t.finalStateDB = backupStateDB
t.allLogs = &allLogs
t.receipts = &receipts
t.totalUsedGas = usedGas
}
_, err = blockstm.ExecuteParallel(tasks, false, metadata, interruptCtx)
break
}
}
if err != nil {
return nil, nil, 0, err
}
// Finalize the block, applying any consensus engine specific extras (e.g. block rewards)
p.engine.Finalize(p.bc, header, statedb, block.Transactions(), block.Uncles())
return receipts, allLogs, *usedGas, nil
}
func GetDeps(txDependency [][]uint64) map[int][]int {
deps := make(map[int][]int)
for i := 0; i <= len(txDependency)-1; i++ {
deps[i] = []int{}
for j := 0; j <= len(txDependency[i])-1; j++ {
deps[i] = append(deps[i], int(txDependency[i][j]))
}
}
return deps
}

View file

@ -20,6 +20,7 @@ import (
"math/big"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/blockstm"
)
// journalEntry is a modification entry in the state change journal that can be
@ -143,6 +144,7 @@ type (
func (ch createObjectChange) revert(s *StateDB) {
delete(s.stateObjects, *ch.account)
delete(s.stateObjectsDirty, *ch.account)
RevertWrite(s, blockstm.NewAddressKey(*ch.account))
}
func (ch createObjectChange) dirtied() *common.Address {
@ -151,6 +153,7 @@ func (ch createObjectChange) dirtied() *common.Address {
func (ch resetObjectChange) revert(s *StateDB) {
s.setStateObject(ch.prev)
RevertWrite(s, blockstm.NewAddressKey(ch.prev.address))
if !ch.prevdestruct && s.snap != nil {
delete(s.snapDestructs, ch.prev.addrHash)
}
@ -165,6 +168,8 @@ func (ch suicideChange) revert(s *StateDB) {
if obj != nil {
obj.suicided = ch.prev
obj.setBalance(ch.prevbalance)
RevertWrite(s, blockstm.NewSubpathKey(*ch.account, SuicidePath))
RevertWrite(s, blockstm.NewSubpathKey(*ch.account, BalancePath))
}
}
@ -183,6 +188,7 @@ func (ch touchChange) dirtied() *common.Address {
func (ch balanceChange) revert(s *StateDB) {
s.getStateObject(*ch.account).setBalance(ch.prev)
RevertWrite(s, blockstm.NewSubpathKey(*ch.account, BalancePath))
}
func (ch balanceChange) dirtied() *common.Address {
@ -191,6 +197,7 @@ func (ch balanceChange) dirtied() *common.Address {
func (ch nonceChange) revert(s *StateDB) {
s.getStateObject(*ch.account).setNonce(ch.prev)
RevertWrite(s, blockstm.NewSubpathKey(*ch.account, NoncePath))
}
func (ch nonceChange) dirtied() *common.Address {
@ -199,6 +206,7 @@ func (ch nonceChange) dirtied() *common.Address {
func (ch codeChange) revert(s *StateDB) {
s.getStateObject(*ch.account).setCode(common.BytesToHash(ch.prevhash), ch.prevcode)
RevertWrite(s, blockstm.NewSubpathKey(*ch.account, CodePath))
}
func (ch codeChange) dirtied() *common.Address {
@ -207,6 +215,7 @@ func (ch codeChange) dirtied() *common.Address {
func (ch storageChange) revert(s *StateDB) {
s.getStateObject(*ch.account).setState(ch.key, ch.prevalue)
RevertWrite(s, blockstm.NewStateKey(*ch.account, ch.key))
}
func (ch storageChange) dirtied() *common.Address {

View file

@ -25,6 +25,7 @@ import (
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/blockstm"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/state/snapshot"
"github.com/ethereum/go-ethereum/core/types"
@ -79,6 +80,14 @@ type StateDB struct {
stateObjectsPending map[common.Address]struct{} // State objects finalized but not yet written to the trie
stateObjectsDirty map[common.Address]struct{} // State objects modified in the current execution
// Block-stm related fields
mvHashmap *blockstm.MVHashMap
incarnation int
readMap map[blockstm.Key]blockstm.ReadDescriptor
writeMap map[blockstm.Key]blockstm.WriteDescriptor
revertedKeys map[blockstm.Key]struct{}
dep int
// DB error.
// State objects are used by the consensus core and VM which are
// unable to deal with database-level errors. Any error that occurs
@ -138,6 +147,7 @@ func New(root common.Hash, db Database, snaps *snapshot.Tree) (*StateDB, error)
stateObjects: make(map[common.Address]*stateObject),
stateObjectsPending: make(map[common.Address]struct{}),
stateObjectsDirty: make(map[common.Address]struct{}),
revertedKeys: make(map[blockstm.Key]struct{}),
logs: make(map[common.Hash][]*types.Log),
preimages: make(map[common.Hash][]byte),
journal: newJournal(),
@ -154,6 +164,281 @@ func New(root common.Hash, db Database, snaps *snapshot.Tree) (*StateDB, error)
return sdb, nil
}
func NewWithMVHashmap(root common.Hash, db Database, snaps *snapshot.Tree, mvhm *blockstm.MVHashMap) (*StateDB, error) {
if sdb, err := New(root, db, snaps); err != nil {
return nil, err
} else {
sdb.mvHashmap = mvhm
sdb.dep = -1
return sdb, nil
}
}
func (sdb *StateDB) SetMVHashmap(mvhm *blockstm.MVHashMap) {
sdb.mvHashmap = mvhm
sdb.dep = -1
}
func (sdb *StateDB) GetMVHashmap() *blockstm.MVHashMap {
return sdb.mvHashmap
}
func (s *StateDB) MVWriteList() []blockstm.WriteDescriptor {
writes := make([]blockstm.WriteDescriptor, 0, len(s.writeMap))
for _, v := range s.writeMap {
if _, ok := s.revertedKeys[v.Path]; !ok {
writes = append(writes, v)
}
}
return writes
}
func (s *StateDB) MVFullWriteList() []blockstm.WriteDescriptor {
writes := make([]blockstm.WriteDescriptor, 0, len(s.writeMap))
for _, v := range s.writeMap {
writes = append(writes, v)
}
return writes
}
func (s *StateDB) MVReadMap() map[blockstm.Key]blockstm.ReadDescriptor {
return s.readMap
}
func (s *StateDB) MVReadList() []blockstm.ReadDescriptor {
reads := make([]blockstm.ReadDescriptor, 0, len(s.readMap))
for _, v := range s.MVReadMap() {
reads = append(reads, v)
}
return reads
}
func (s *StateDB) ensureReadMap() {
if s.readMap == nil {
s.readMap = make(map[blockstm.Key]blockstm.ReadDescriptor)
}
}
func (s *StateDB) ensureWriteMap() {
if s.writeMap == nil {
s.writeMap = make(map[blockstm.Key]blockstm.WriteDescriptor)
}
}
func (s *StateDB) ClearReadMap() {
s.readMap = make(map[blockstm.Key]blockstm.ReadDescriptor)
}
func (s *StateDB) ClearWriteMap() {
s.writeMap = make(map[blockstm.Key]blockstm.WriteDescriptor)
}
func (s *StateDB) HadInvalidRead() bool {
return s.dep >= 0
}
func (s *StateDB) DepTxIndex() int {
return s.dep
}
func (s *StateDB) SetIncarnation(inc int) {
s.incarnation = inc
}
type StorageVal[T any] struct {
Value *T
}
func MVRead[T any](s *StateDB, k blockstm.Key, defaultV T, readStorage func(s *StateDB) T) (v T) {
if s.mvHashmap == nil {
return readStorage(s)
}
s.ensureReadMap()
if s.writeMap != nil {
if _, ok := s.writeMap[k]; ok {
return readStorage(s)
}
}
if !k.IsAddress() {
// If we are reading subpath from a deleted account, return default value instead of reading from MVHashmap
addr := k.GetAddress()
if s.getStateObject(addr) == nil {
return defaultV
}
}
res := s.mvHashmap.Read(k, s.txIndex)
var rd blockstm.ReadDescriptor
rd.V = blockstm.Version{
TxnIndex: res.DepIdx(),
Incarnation: res.Incarnation(),
}
rd.Path = k
switch res.Status() {
case blockstm.MVReadResultDone:
{
v = readStorage(res.Value().(*StateDB))
rd.Kind = blockstm.ReadKindMap
}
case blockstm.MVReadResultDependency:
{
s.dep = res.DepIdx()
panic("Found dependency")
}
case blockstm.MVReadResultNone:
{
v = readStorage(s)
rd.Kind = blockstm.ReadKindStorage
}
default:
return defaultV
}
// TODO: I assume we don't want to overwrite an existing read because this could - for example - change a storage
// read to map if the same value is read multiple times.
if _, ok := s.readMap[k]; !ok {
s.readMap[k] = rd
}
return
}
func MVWrite(s *StateDB, k blockstm.Key) {
if s.mvHashmap != nil {
s.ensureWriteMap()
s.writeMap[k] = blockstm.WriteDescriptor{
Path: k,
V: s.Version(),
Val: s,
}
}
}
func RevertWrite(s *StateDB, k blockstm.Key) {
s.revertedKeys[k] = struct{}{}
}
func MVWritten(s *StateDB, k blockstm.Key) bool {
if s.mvHashmap == nil || s.writeMap == nil {
return false
}
_, ok := s.writeMap[k]
return ok
}
// Apply entries in the write set to MVHashMap. Note that this function does not clear the write set.
func (s *StateDB) FlushMVWriteSet() {
if s.mvHashmap != nil && s.writeMap != nil {
s.mvHashmap.FlushMVWriteSet(s.MVFullWriteList())
}
}
// Apply entries in a given write set to StateDB. Note that this function does not change MVHashMap nor write set
// of the current StateDB.
func (sw *StateDB) ApplyMVWriteSet(writes []blockstm.WriteDescriptor) {
for i := range writes {
path := writes[i].Path
sr := writes[i].Val.(*StateDB)
if path.IsState() {
addr := path.GetAddress()
stateKey := path.GetStateKey()
state := sr.GetState(addr, stateKey)
sw.SetState(addr, stateKey, state)
} else if path.IsAddress() {
continue
} else {
addr := path.GetAddress()
switch path.GetSubpath() {
case BalancePath:
sw.SetBalance(addr, sr.GetBalance(addr))
case NoncePath:
sw.SetNonce(addr, sr.GetNonce(addr))
case CodePath:
sw.SetCode(addr, sr.GetCode(addr))
case SuicidePath:
stateObject := sr.getDeletedStateObject(addr)
if stateObject != nil && stateObject.deleted {
sw.Suicide(addr)
}
default:
panic(fmt.Errorf("unknown key type: %d", path.GetSubpath()))
}
}
}
}
type DumpStruct struct {
TxIdx int
TxInc int
VerIdx int
VerInc int
Path []byte
Op string
}
// get readMap Dump of format: "TxIdx, Inc, Path, Read"
func (s *StateDB) GetReadMapDump() []DumpStruct {
readList := s.MVReadList()
res := make([]DumpStruct, 0, len(readList))
for _, val := range readList {
temp := &DumpStruct{
TxIdx: s.txIndex,
TxInc: s.incarnation,
VerIdx: val.V.TxnIndex,
VerInc: val.V.Incarnation,
Path: val.Path[:],
Op: "Read\n",
}
res = append(res, *temp)
}
return res
}
// get writeMap Dump of format: "TxIdx, Inc, Path, Write"
func (s *StateDB) GetWriteMapDump() []DumpStruct {
writeList := s.MVReadList()
res := make([]DumpStruct, 0, len(writeList))
for _, val := range writeList {
temp := &DumpStruct{
TxIdx: s.txIndex,
TxInc: s.incarnation,
VerIdx: val.V.TxnIndex,
VerInc: val.V.Incarnation,
Path: val.Path[:],
Op: "Write\n",
}
res = append(res, *temp)
}
return res
}
// add empty MVHashMap to StateDB
func (s *StateDB) AddEmptyMVHashMap() {
mvh := blockstm.MakeMVHashMap()
s.mvHashmap = mvh
}
// StartPrefetcher initializes a new trie prefetcher to pull in nodes from the
// state trie concurrently while the state is mutated so that when we reach the
// commit phase, most of the needed data is already hot.
@ -257,22 +542,40 @@ func (s *StateDB) Empty(addr common.Address) bool {
return so == nil || so.empty()
}
// Create a unique path for special fields (e.g. balance, code) in a state object.
// func subPath(prefix []byte, s uint8) [blockstm.KeyLength]byte {
// path := append(prefix, common.Hash{}.Bytes()...) // append a full empty hash to avoid collision with storage state
// path = append(path, s) // append the special field identifier
// return path
// }
const BalancePath = 1
const NoncePath = 2
const CodePath = 3
const SuicidePath = 4
// GetBalance retrieves the balance from the given address or 0 if object not found
func (s *StateDB) GetBalance(addr common.Address) *big.Int {
return MVRead(s, blockstm.NewSubpathKey(addr, BalancePath), common.Big0, func(s *StateDB) *big.Int {
stateObject := s.getStateObject(addr)
if stateObject != nil {
return stateObject.Balance()
}
return common.Big0
})
}
func (s *StateDB) GetNonce(addr common.Address) uint64 {
return MVRead(s, blockstm.NewSubpathKey(addr, NoncePath), 0, func(s *StateDB) uint64 {
stateObject := s.getStateObject(addr)
if stateObject != nil {
return stateObject.Nonce()
}
return 0
})
}
// TxIndex returns the current transaction index set by Prepare.
@ -280,37 +583,52 @@ func (s *StateDB) TxIndex() int {
return s.txIndex
}
func (s *StateDB) Version() blockstm.Version {
return blockstm.Version{
TxnIndex: s.txIndex,
Incarnation: s.incarnation,
}
}
func (s *StateDB) GetCode(addr common.Address) []byte {
return MVRead(s, blockstm.NewSubpathKey(addr, CodePath), nil, func(s *StateDB) []byte {
stateObject := s.getStateObject(addr)
if stateObject != nil {
return stateObject.Code(s.db)
}
return nil
})
}
func (s *StateDB) GetCodeSize(addr common.Address) int {
return MVRead(s, blockstm.NewSubpathKey(addr, CodePath), 0, func(s *StateDB) int {
stateObject := s.getStateObject(addr)
if stateObject != nil {
return stateObject.CodeSize(s.db)
}
return 0
})
}
func (s *StateDB) GetCodeHash(addr common.Address) common.Hash {
return MVRead(s, blockstm.NewSubpathKey(addr, CodePath), common.Hash{}, func(s *StateDB) common.Hash {
stateObject := s.getStateObject(addr)
if stateObject == nil {
return common.Hash{}
}
return common.BytesToHash(stateObject.CodeHash())
})
}
// GetState retrieves a value from the given account's storage trie.
func (s *StateDB) GetState(addr common.Address, hash common.Hash) common.Hash {
return MVRead(s, blockstm.NewStateKey(addr, hash), common.Hash{}, func(s *StateDB) common.Hash {
stateObject := s.getStateObject(addr)
if stateObject != nil {
return stateObject.GetState(s.db, hash)
}
return common.Hash{}
})
}
// GetProof returns the Merkle proof for a given account.
@ -338,11 +656,13 @@ func (s *StateDB) GetStorageProof(a common.Address, key common.Hash) ([][]byte,
// GetCommittedState retrieves a value from the given account's committed storage trie.
func (s *StateDB) GetCommittedState(addr common.Address, hash common.Hash) common.Hash {
return MVRead(s, blockstm.NewStateKey(addr, hash), common.Hash{}, func(s *StateDB) common.Hash {
stateObject := s.getStateObject(addr)
if stateObject != nil {
return stateObject.GetCommittedState(s.db, hash)
}
return common.Hash{}
})
}
// Database retrieves the low level database supporting the lower level trie ops.
@ -363,11 +683,13 @@ func (s *StateDB) StorageTrie(addr common.Address) Trie {
}
func (s *StateDB) HasSuicided(addr common.Address) bool {
return MVRead(s, blockstm.NewSubpathKey(addr, SuicidePath), false, func(s *StateDB) bool {
stateObject := s.getStateObject(addr)
if stateObject != nil {
return stateObject.suicided
}
return false
})
}
/*
@ -377,44 +699,68 @@ func (s *StateDB) HasSuicided(addr common.Address) bool {
// AddBalance adds amount to the account associated with addr.
func (s *StateDB) AddBalance(addr common.Address, amount *big.Int) {
stateObject := s.GetOrNewStateObject(addr)
if s.mvHashmap != nil {
// ensure a read balance operation is recorded in mvHashmap
s.GetBalance(addr)
}
if stateObject != nil {
stateObject = s.mvRecordWritten(stateObject)
stateObject.AddBalance(amount)
MVWrite(s, blockstm.NewSubpathKey(addr, BalancePath))
}
}
// SubBalance subtracts amount from the account associated with addr.
func (s *StateDB) SubBalance(addr common.Address, amount *big.Int) {
stateObject := s.GetOrNewStateObject(addr)
if s.mvHashmap != nil {
// ensure a read balance operation is recorded in mvHashmap
s.GetBalance(addr)
}
if stateObject != nil {
stateObject = s.mvRecordWritten(stateObject)
stateObject.SubBalance(amount)
MVWrite(s, blockstm.NewSubpathKey(addr, BalancePath))
}
}
func (s *StateDB) SetBalance(addr common.Address, amount *big.Int) {
stateObject := s.GetOrNewStateObject(addr)
if stateObject != nil {
stateObject = s.mvRecordWritten(stateObject)
stateObject.SetBalance(amount)
MVWrite(s, blockstm.NewSubpathKey(addr, BalancePath))
}
}
func (s *StateDB) SetNonce(addr common.Address, nonce uint64) {
stateObject := s.GetOrNewStateObject(addr)
if stateObject != nil {
stateObject = s.mvRecordWritten(stateObject)
stateObject.SetNonce(nonce)
MVWrite(s, blockstm.NewSubpathKey(addr, NoncePath))
}
}
func (s *StateDB) SetCode(addr common.Address, code []byte) {
stateObject := s.GetOrNewStateObject(addr)
if stateObject != nil {
stateObject = s.mvRecordWritten(stateObject)
stateObject.SetCode(crypto.Keccak256Hash(code), code)
MVWrite(s, blockstm.NewSubpathKey(addr, CodePath))
}
}
func (s *StateDB) SetState(addr common.Address, key, value common.Hash) {
stateObject := s.GetOrNewStateObject(addr)
if stateObject != nil {
stateObject = s.mvRecordWritten(stateObject)
stateObject.SetState(s.db, key, value)
MVWrite(s, blockstm.NewStateKey(addr, key))
}
}
@ -437,6 +783,8 @@ func (s *StateDB) Suicide(addr common.Address) bool {
if stateObject == nil {
return false
}
stateObject = s.mvRecordWritten(stateObject)
s.journal.append(suicideChange{
account: &addr,
prev: stateObject.suicided,
@ -445,6 +793,9 @@ func (s *StateDB) Suicide(addr common.Address) bool {
stateObject.markSuicided()
stateObject.data.Balance = new(big.Int)
MVWrite(s, blockstm.NewSubpathKey(addr, SuicidePath))
MVWrite(s, blockstm.NewSubpathKey(addr, BalancePath))
return true
}
@ -501,15 +852,16 @@ func (s *StateDB) getStateObject(addr common.Address) *stateObject {
// flag set. This is needed by the state journal to revert to the correct s-
// destructed object instead of wiping all knowledge about the state object.
func (s *StateDB) getDeletedStateObject(addr common.Address) *stateObject {
return MVRead(s, blockstm.NewAddressKey(addr), nil, func(s *StateDB) *stateObject {
// Prefer live objects if any is available
if obj := s.stateObjects[addr]; obj != nil {
return obj
}
// If no live objects are available, attempt to use snapshots
var data *types.StateAccount
if s.snap != nil {
if s.snap != nil { // nolint
start := time.Now()
acc, err := s.snap.Account(crypto.HashData(s.hasher, addr.Bytes()))
acc, err := s.snap.Account(crypto.HashData(crypto.NewKeccakState(), addr.Bytes()))
if metrics.EnabledExpensive {
s.SnapshotAccountReads += time.Since(start)
}
@ -555,6 +907,7 @@ func (s *StateDB) getDeletedStateObject(addr common.Address) *stateObject {
obj := newObject(s, addr, *data)
s.setStateObject(obj)
return obj
})
}
func (s *StateDB) setStateObject(object *stateObject) {
@ -570,6 +923,28 @@ func (s *StateDB) GetOrNewStateObject(addr common.Address) *stateObject {
return stateObject
}
// mvRecordWritten checks whether a state object is already present in the current MV writeMap.
// If yes, it returns the object directly.
// If not, it clones the object and inserts it into the writeMap before returning it.
func (s *StateDB) mvRecordWritten(object *stateObject) *stateObject {
if s.mvHashmap == nil {
return object
}
addrKey := blockstm.NewAddressKey(object.Address())
if MVWritten(s, addrKey) {
return object
}
// Deepcopy is needed to ensure that objects are not written by multiple transactions at the same time, because
// the input state object can come from a different transaction.
s.setStateObject(object.deepCopy(s))
MVWrite(s, addrKey)
return s.stateObjects[object.Address()]
}
// createObject creates a new state object. If there is an existing account with
// the given address, it is overwritten and returned as the second return value.
func (s *StateDB) createObject(addr common.Address) (newobj, prev *stateObject) {
@ -589,6 +964,8 @@ func (s *StateDB) createObject(addr common.Address) (newobj, prev *stateObject)
s.journal.append(resetObjectChange{prev: prev, prevdestruct: prevdestruct})
}
s.setStateObject(newobj)
MVWrite(s, blockstm.NewAddressKey(addr))
if prev != nil && !prev.deleted {
return newobj, prev
}
@ -609,6 +986,7 @@ func (s *StateDB) CreateAccount(addr common.Address) {
newObj, prev := s.createObject(addr)
if prev != nil {
newObj.setBalance(prev.data.Balance)
MVWrite(s, blockstm.NewSubpathKey(addr, BalancePath))
}
}
@ -651,6 +1029,7 @@ func (s *StateDB) Copy() *StateDB {
stateObjects: make(map[common.Address]*stateObject, len(s.journal.dirties)),
stateObjectsPending: make(map[common.Address]struct{}, len(s.stateObjectsPending)),
stateObjectsDirty: make(map[common.Address]struct{}, len(s.journal.dirties)),
revertedKeys: make(map[blockstm.Key]struct{}),
refund: s.refund,
logs: make(map[common.Hash][]*types.Log, len(s.logs)),
logSize: s.logSize,
@ -738,6 +1117,10 @@ func (s *StateDB) Copy() *StateDB {
state.snapStorage[k] = temp
}
}
if s.mvHashmap != nil {
state.mvHashmap = s.mvHashmap
}
return state
}

View file

@ -29,7 +29,10 @@ import (
"testing"
"testing/quick"
"github.com/stretchr/testify/assert"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/blockstm"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/types"
)
@ -488,6 +491,457 @@ func TestTouchDelete(t *testing.T) {
}
}
func TestMVHashMapReadWriteDelete(t *testing.T) {
t.Parallel()
db := NewDatabase(rawdb.NewMemoryDatabase())
mvhm := blockstm.MakeMVHashMap()
s, _ := NewWithMVHashmap(common.Hash{}, db, nil, mvhm)
states := []*StateDB{s}
// Create copies of the original state for each transition
for i := 1; i <= 4; i++ {
sCopy := s.Copy()
sCopy.txIndex = i
states = append(states, sCopy)
}
addr := common.HexToAddress("0x01")
key := common.HexToHash("0x01")
val := common.HexToHash("0x01")
balance := new(big.Int).SetUint64(uint64(100))
// Tx0 read
v := states[0].GetState(addr, key)
assert.Equal(t, common.Hash{}, v)
// Tx1 write
states[1].GetOrNewStateObject(addr)
states[1].SetState(addr, key, val)
states[1].SetBalance(addr, balance)
states[1].FlushMVWriteSet()
// Tx1 read
v = states[1].GetState(addr, key)
b := states[1].GetBalance(addr)
assert.Equal(t, val, v)
assert.Equal(t, balance, b)
// Tx2 read
v = states[2].GetState(addr, key)
b = states[2].GetBalance(addr)
assert.Equal(t, val, v)
assert.Equal(t, balance, b)
// Tx3 delete
states[3].Suicide(addr)
// Within Tx 3, the state should not change before finalize
v = states[3].GetState(addr, key)
assert.Equal(t, val, v)
// After finalizing Tx 3, the state will change
states[3].Finalise(false)
v = states[3].GetState(addr, key)
assert.Equal(t, common.Hash{}, v)
states[3].FlushMVWriteSet()
// Tx4 read
v = states[4].GetState(addr, key)
b = states[4].GetBalance(addr)
assert.Equal(t, common.Hash{}, v)
assert.Equal(t, common.Big0, b)
}
func TestMVHashMapRevert(t *testing.T) {
t.Parallel()
db := NewDatabase(rawdb.NewMemoryDatabase())
mvhm := blockstm.MakeMVHashMap()
s, _ := NewWithMVHashmap(common.Hash{}, db, nil, mvhm)
states := []*StateDB{s}
// Create copies of the original state for each transition
for i := 1; i <= 4; i++ {
sCopy := s.Copy()
sCopy.txIndex = i
states = append(states, sCopy)
}
addr := common.HexToAddress("0x01")
key := common.HexToHash("0x01")
val := common.HexToHash("0x01")
balance := new(big.Int).SetUint64(uint64(100))
// Tx0 write
states[0].GetOrNewStateObject(addr)
states[0].SetState(addr, key, val)
states[0].SetBalance(addr, balance)
states[0].FlushMVWriteSet()
// Tx1 perform some ops and then revert
snapshot := states[1].Snapshot()
states[1].AddBalance(addr, new(big.Int).SetUint64(uint64(100)))
states[1].SetState(addr, key, common.HexToHash("0x02"))
v := states[1].GetState(addr, key)
b := states[1].GetBalance(addr)
assert.Equal(t, new(big.Int).SetUint64(uint64(200)), b)
assert.Equal(t, common.HexToHash("0x02"), v)
states[1].Suicide(addr)
states[1].RevertToSnapshot(snapshot)
v = states[1].GetState(addr, key)
b = states[1].GetBalance(addr)
assert.Equal(t, val, v)
assert.Equal(t, balance, b)
states[1].Finalise(false)
states[1].FlushMVWriteSet()
// Tx2 check the state and balance
v = states[2].GetState(addr, key)
b = states[2].GetBalance(addr)
assert.Equal(t, val, v)
assert.Equal(t, balance, b)
}
func TestMVHashMapMarkEstimate(t *testing.T) {
t.Parallel()
db := NewDatabase(rawdb.NewMemoryDatabase())
mvhm := blockstm.MakeMVHashMap()
s, _ := NewWithMVHashmap(common.Hash{}, db, nil, mvhm)
states := []*StateDB{s}
// Create copies of the original state for each transition
for i := 1; i <= 4; i++ {
sCopy := s.Copy()
sCopy.txIndex = i
states = append(states, sCopy)
}
addr := common.HexToAddress("0x01")
key := common.HexToHash("0x01")
val := common.HexToHash("0x01")
balance := new(big.Int).SetUint64(uint64(100))
// Tx0 read
v := states[0].GetState(addr, key)
assert.Equal(t, common.Hash{}, v)
// Tx0 write
states[0].SetState(addr, key, val)
v = states[0].GetState(addr, key)
assert.Equal(t, val, v)
states[0].FlushMVWriteSet()
// Tx1 write
states[1].GetOrNewStateObject(addr)
states[1].SetState(addr, key, val)
states[1].SetBalance(addr, balance)
states[1].FlushMVWriteSet()
// Tx2 read
v = states[2].GetState(addr, key)
b := states[2].GetBalance(addr)
assert.Equal(t, val, v)
assert.Equal(t, balance, b)
// Tx1 mark estimate
for _, v := range states[1].MVWriteList() {
mvhm.MarkEstimate(v.Path, 1)
}
defer func() {
if r := recover(); r == nil {
t.Errorf("The code did not panic")
} else {
t.Log("Recovered in f", r)
}
}()
// Tx2 read again should get default (empty) vals because its dependency Tx1 is marked as estimate
states[2].GetState(addr, key)
states[2].GetBalance(addr)
// Tx1 read again should get Tx0 vals
v = states[1].GetState(addr, key)
assert.Equal(t, val, v)
}
func TestMVHashMapOverwrite(t *testing.T) {
t.Parallel()
db := NewDatabase(rawdb.NewMemoryDatabase())
mvhm := blockstm.MakeMVHashMap()
s, _ := NewWithMVHashmap(common.Hash{}, db, nil, mvhm)
states := []*StateDB{s}
// Create copies of the original state for each transition
for i := 1; i <= 4; i++ {
sCopy := s.Copy()
sCopy.txIndex = i
states = append(states, sCopy)
}
addr := common.HexToAddress("0x01")
key := common.HexToHash("0x01")
val1 := common.HexToHash("0x01")
balance1 := new(big.Int).SetUint64(uint64(100))
val2 := common.HexToHash("0x02")
balance2 := new(big.Int).SetUint64(uint64(200))
// Tx0 write
states[0].GetOrNewStateObject(addr)
states[0].SetState(addr, key, val1)
states[0].SetBalance(addr, balance1)
states[0].FlushMVWriteSet()
// Tx1 write
states[1].SetState(addr, key, val2)
states[1].SetBalance(addr, balance2)
v := states[1].GetState(addr, key)
b := states[1].GetBalance(addr)
states[1].FlushMVWriteSet()
assert.Equal(t, val2, v)
assert.Equal(t, balance2, b)
// Tx2 read should get Tx1's value
v = states[2].GetState(addr, key)
b = states[2].GetBalance(addr)
assert.Equal(t, val2, v)
assert.Equal(t, balance2, b)
// Tx1 delete
for _, v := range states[1].writeMap {
mvhm.Delete(v.Path, 1)
states[1].writeMap = nil
}
// Tx2 read should get Tx0's value
v = states[2].GetState(addr, key)
b = states[2].GetBalance(addr)
assert.Equal(t, val1, v)
assert.Equal(t, balance1, b)
// Tx1 read should get Tx0's value
v = states[1].GetState(addr, key)
b = states[1].GetBalance(addr)
assert.Equal(t, val1, v)
assert.Equal(t, balance1, b)
// Tx0 delete
for _, v := range states[0].writeMap {
mvhm.Delete(v.Path, 0)
states[0].writeMap = nil
}
// Tx2 read again should get default vals
v = states[2].GetState(addr, key)
b = states[2].GetBalance(addr)
assert.Equal(t, common.Hash{}, v)
assert.Equal(t, common.Big0, b)
}
func TestMVHashMapWriteNoConflict(t *testing.T) {
t.Parallel()
db := NewDatabase(rawdb.NewMemoryDatabase())
mvhm := blockstm.MakeMVHashMap()
s, _ := NewWithMVHashmap(common.Hash{}, db, nil, mvhm)
states := []*StateDB{s}
// Create copies of the original state for each transition
for i := 1; i <= 4; i++ {
sCopy := s.Copy()
sCopy.txIndex = i
states = append(states, sCopy)
}
addr := common.HexToAddress("0x01")
key1 := common.HexToHash("0x01")
key2 := common.HexToHash("0x02")
val1 := common.HexToHash("0x01")
balance1 := new(big.Int).SetUint64(uint64(100))
val2 := common.HexToHash("0x02")
// Tx0 write
states[0].GetOrNewStateObject(addr)
states[0].FlushMVWriteSet()
// Tx2 write
states[2].SetState(addr, key2, val2)
states[2].FlushMVWriteSet()
// Tx1 write
tx1Snapshot := states[1].Snapshot()
states[1].SetState(addr, key1, val1)
states[1].SetBalance(addr, balance1)
states[1].FlushMVWriteSet()
// Tx1 read
assert.Equal(t, val1, states[1].GetState(addr, key1))
assert.Equal(t, balance1, states[1].GetBalance(addr))
// Tx1 should see empty value in key2
assert.Equal(t, common.Hash{}, states[1].GetState(addr, key2))
// Tx2 read
assert.Equal(t, val2, states[2].GetState(addr, key2))
// Tx2 should see values written by Tx1
assert.Equal(t, val1, states[2].GetState(addr, key1))
assert.Equal(t, balance1, states[2].GetBalance(addr))
// Tx3 read
assert.Equal(t, val1, states[3].GetState(addr, key1))
assert.Equal(t, val2, states[3].GetState(addr, key2))
assert.Equal(t, balance1, states[3].GetBalance(addr))
// Tx2 delete
for _, v := range states[2].writeMap {
mvhm.Delete(v.Path, 2)
states[2].writeMap = nil
}
assert.Equal(t, val1, states[3].GetState(addr, key1))
assert.Equal(t, balance1, states[3].GetBalance(addr))
assert.Equal(t, common.Hash{}, states[3].GetState(addr, key2))
// Tx1 revert
states[1].RevertToSnapshot(tx1Snapshot)
states[1].FlushMVWriteSet()
assert.Equal(t, common.Hash{}, states[3].GetState(addr, key1))
assert.Equal(t, common.Hash{}, states[3].GetState(addr, key2))
assert.Equal(t, common.Big0, states[3].GetBalance(addr))
// Tx1 delete
for _, v := range states[1].writeMap {
mvhm.Delete(v.Path, 1)
states[1].writeMap = nil
}
assert.Equal(t, common.Hash{}, states[3].GetState(addr, key1))
assert.Equal(t, common.Hash{}, states[3].GetState(addr, key2))
assert.Equal(t, common.Big0, states[3].GetBalance(addr))
}
func TestApplyMVWriteSet(t *testing.T) {
t.Parallel()
db := NewDatabase(rawdb.NewMemoryDatabase())
mvhm := blockstm.MakeMVHashMap()
s, _ := NewWithMVHashmap(common.Hash{}, db, nil, mvhm)
sClean := s.Copy()
sClean.mvHashmap = nil
sSingleProcess := sClean.Copy()
states := []*StateDB{s}
// Create copies of the original state for each transition
for i := 1; i <= 4; i++ {
sCopy := s.Copy()
sCopy.txIndex = i
states = append(states, sCopy)
}
addr1 := common.HexToAddress("0x01")
addr2 := common.HexToAddress("0x02")
addr3 := common.HexToAddress("0x03")
key1 := common.HexToHash("0x01")
key2 := common.HexToHash("0x02")
val1 := common.HexToHash("0x01")
balance1 := new(big.Int).SetUint64(uint64(100))
val2 := common.HexToHash("0x02")
balance2 := new(big.Int).SetUint64(uint64(200))
code := []byte{1, 2, 3}
// Tx0 write
states[0].GetOrNewStateObject(addr1)
states[0].SetState(addr1, key1, val1)
states[0].SetBalance(addr1, balance1)
states[0].SetState(addr2, key2, val2)
states[0].GetOrNewStateObject(addr3)
states[0].Finalise(true)
states[0].FlushMVWriteSet()
sSingleProcess.GetOrNewStateObject(addr1)
sSingleProcess.SetState(addr1, key1, val1)
sSingleProcess.SetBalance(addr1, balance1)
sSingleProcess.SetState(addr2, key2, val2)
sSingleProcess.GetOrNewStateObject(addr3)
sClean.ApplyMVWriteSet(states[0].MVWriteList())
assert.Equal(t, sSingleProcess.IntermediateRoot(true), sClean.IntermediateRoot(true))
// Tx1 write
states[1].SetState(addr1, key2, val2)
states[1].SetBalance(addr1, balance2)
states[1].SetNonce(addr1, 1)
states[1].Finalise(true)
states[1].FlushMVWriteSet()
sSingleProcess.SetState(addr1, key2, val2)
sSingleProcess.SetBalance(addr1, balance2)
sSingleProcess.SetNonce(addr1, 1)
sClean.ApplyMVWriteSet(states[1].MVWriteList())
assert.Equal(t, sSingleProcess.IntermediateRoot(true), sClean.IntermediateRoot(true))
// Tx2 write
states[2].SetState(addr1, key1, val2)
states[2].SetBalance(addr1, balance2)
states[2].SetNonce(addr1, 2)
states[2].Finalise(true)
states[2].FlushMVWriteSet()
sSingleProcess.SetState(addr1, key1, val2)
sSingleProcess.SetBalance(addr1, balance2)
sSingleProcess.SetNonce(addr1, 2)
sClean.ApplyMVWriteSet(states[2].MVWriteList())
assert.Equal(t, sSingleProcess.IntermediateRoot(true), sClean.IntermediateRoot(true))
// Tx3 write
states[3].Suicide(addr2)
states[3].SetCode(addr1, code)
states[3].Finalise(true)
states[3].FlushMVWriteSet()
sSingleProcess.Suicide(addr2)
sSingleProcess.SetCode(addr1, code)
sClean.ApplyMVWriteSet(states[3].MVWriteList())
assert.Equal(t, sSingleProcess.IntermediateRoot(true), sClean.IntermediateRoot(true))
}
// TestCopyOfCopy tests that modified objects are carried over to the copy, and the copy of the copy.
// See https://github.com/ethereum/go-ethereum/pull/15225#issuecomment-380191512
func TestCopyOfCopy(t *testing.T) {

View file

@ -57,7 +57,7 @@ func NewStateProcessor(config *params.ChainConfig, bc *BlockChain, engine consen
// Process returns the receipts and logs accumulated during the process and
// returns the amount of gas that was used in the process. If any of the
// transactions failed to execute due to insufficient gas it will return an error.
func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg vm.Config) (types.Receipts, []*types.Log, uint64, error) {
func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg vm.Config, interruptCtx context.Context) (types.Receipts, []*types.Log, uint64, error) {
var (
receipts types.Receipts
usedGas = new(uint64)
@ -75,12 +75,20 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg
vmenv := vm.NewEVM(blockContext, vm.TxContext{}, statedb, p.config, cfg)
// Iterate over and process the individual transactions
for i, tx := range block.Transactions() {
if interruptCtx != nil {
select {
case <-interruptCtx.Done():
return nil, nil, 0, interruptCtx.Err()
default:
}
}
msg, err := tx.AsMessage(types.MakeSigner(p.config, header.Number), header.BaseFee)
if err != nil {
return nil, nil, 0, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err)
}
statedb.Prepare(tx.Hash(), i)
receipt, err := applyTransaction(msg, p.config, p.bc, nil, gp, statedb, blockNumber, blockHash, tx, usedGas, vmenv, nil)
receipt, err := applyTransaction(msg, p.config, p.bc, nil, gp, statedb, blockNumber, blockHash, tx, usedGas, vmenv, interruptCtx)
if err != nil {
return nil, nil, 0, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err)
}
@ -99,12 +107,51 @@ func applyTransaction(msg types.Message, config *params.ChainConfig, bc ChainCon
txContext := NewEVMTxContext(msg)
evm.Reset(txContext, statedb)
// Apply the transaction to the current state (included in the env).
result, err := ApplyMessage(evm, msg, gp, interruptCtx)
var result *ExecutionResult
var err error
backupMVHashMap := statedb.GetMVHashmap()
// pause recording read and write
statedb.SetMVHashmap(nil)
coinbaseBalance := statedb.GetBalance(evm.Context.Coinbase)
// resume recording read and write
statedb.SetMVHashmap(backupMVHashMap)
result, err = ApplyMessageNoFeeBurnOrTip(evm, msg, gp, interruptCtx)
if err != nil {
return nil, err
}
// stop recording read and write
statedb.SetMVHashmap(nil)
if evm.ChainConfig().IsLondon(blockNumber) {
statedb.AddBalance(result.BurntContractAddress, result.FeeBurnt)
}
statedb.AddBalance(evm.Context.Coinbase, result.FeeTipped)
output1 := new(big.Int).SetBytes(result.SenderInitBalance.Bytes())
output2 := new(big.Int).SetBytes(coinbaseBalance.Bytes())
// Deprecating transfer log and will be removed in future fork. PLEASE DO NOT USE this transfer log going forward. Parameters won't get updated as expected going forward with EIP1559
// add transfer log
AddFeeTransferLog(
statedb,
msg.From(),
evm.Context.Coinbase,
result.FeeTipped,
result.SenderInitBalance,
coinbaseBalance,
output1.Sub(output1, result.FeeTipped),
output2.Add(output2, result.FeeTipped),
)
if result.Err == vm.ErrInterrupt {
return nil, result.Err
}

View file

@ -236,8 +236,12 @@ func TestStateProcessorErrors(t *testing.T) {
}
genesis = gspec.MustCommit(db)
blockchain, _ = NewBlockChain(db, nil, gspec.Config, ethash.NewFaker(), vm.Config{}, nil, nil, nil)
parallelBlockchain, _ = NewParallelBlockChain(db, nil, gspec.Config, ethash.NewFaker(), vm.Config{ParallelEnable: true, ParallelSpeculativeProcesses: 8}, nil, nil, nil)
)
defer blockchain.Stop()
defer parallelBlockchain.Stop()
for _, bc := range []*BlockChain{blockchain, parallelBlockchain} {
for i, tt := range []struct {
txs []*types.Transaction
want string
@ -250,7 +254,7 @@ func TestStateProcessorErrors(t *testing.T) {
},
} {
block := GenerateBadBlock(genesis, ethash.NewFaker(), tt.txs, gspec.Config)
_, err := blockchain.InsertChain(types.Blocks{block})
_, err := bc.InsertChain(types.Blocks{block})
if err == nil {
t.Fatal("block imported without errors")
}
@ -259,6 +263,7 @@ func TestStateProcessorErrors(t *testing.T) {
}
}
}
}
// ErrSenderNoEOA, for this we need the sender to have contract code
{
@ -276,8 +281,12 @@ func TestStateProcessorErrors(t *testing.T) {
}
genesis = gspec.MustCommit(db)
blockchain, _ = NewBlockChain(db, nil, gspec.Config, ethash.NewFaker(), vm.Config{}, nil, nil, nil)
parallelBlockchain, _ = NewParallelBlockChain(db, nil, gspec.Config, ethash.NewFaker(), vm.Config{ParallelEnable: true, ParallelSpeculativeProcesses: 8}, nil, nil, nil)
)
defer blockchain.Stop()
defer parallelBlockchain.Stop()
for _, bc := range []*BlockChain{blockchain, parallelBlockchain} {
for i, tt := range []struct {
txs []*types.Transaction
want string
@ -290,7 +299,7 @@ func TestStateProcessorErrors(t *testing.T) {
},
} {
block := GenerateBadBlock(genesis, ethash.NewFaker(), tt.txs, gspec.Config)
_, err := blockchain.InsertChain(types.Blocks{block})
_, err := bc.InsertChain(types.Blocks{block})
if err == nil {
t.Fatal("block imported without errors")
}
@ -300,6 +309,7 @@ func TestStateProcessorErrors(t *testing.T) {
}
}
}
}
// GenerateBadBlock constructs a "block" which contains the transactions. The transactions are not expected to be
// valid, and no proper post-state can be made. But from the perspective of the blockchain, the block is sufficiently

View file

@ -63,6 +63,11 @@ type StateTransition struct {
data []byte
state vm.StateDB
evm *vm.EVM
// If true, fee burning and tipping won't happen during transition. Instead, their values will be included in the
// ExecutionResult, which caller can use the values to update the balance of burner and coinbase account.
// This is useful during parallel state transition, where the common account read/write should be minimized.
noFeeBurnAndTip bool
}
// Message represents a message sent to a contract.
@ -88,6 +93,10 @@ type ExecutionResult struct {
UsedGas uint64 // Total used gas but include the refunded gas
Err error // Any error encountered during the execution(listed in core/vm/errors.go)
ReturnData []byte // Returned data from evm(function result or data supplied with revert opcode)
SenderInitBalance *big.Int
FeeBurnt *big.Int
BurntContractAddress common.Address
FeeTipped *big.Int
}
// Unwrap returns the internal evm error which allows us for further
@ -184,6 +193,13 @@ func ApplyMessage(evm *vm.EVM, msg Message, gp *GasPool, interruptCtx context.Co
return NewStateTransition(evm, msg, gp).TransitionDb(interruptCtx)
}
func ApplyMessageNoFeeBurnOrTip(evm *vm.EVM, msg Message, gp *GasPool, interruptCtx context.Context) (*ExecutionResult, error) {
st := NewStateTransition(evm, msg, gp)
st.noFeeBurnAndTip = true
return st.TransitionDb(interruptCtx)
}
// to returns the recipient of the message.
func (st *StateTransition) to() common.Address {
if st.msg == nil || st.msg.To() == nil /* contract creation */ {
@ -277,7 +293,12 @@ func (st *StateTransition) preCheck() error {
// nil evm execution result.
func (st *StateTransition) TransitionDb(interruptCtx context.Context) (*ExecutionResult, error) {
input1 := st.state.GetBalance(st.msg.From())
input2 := st.state.GetBalance(st.evm.Context.Coinbase)
var input2 *big.Int
if !st.noFeeBurnAndTip {
input2 = st.state.GetBalance(st.evm.Context.Coinbase)
}
// First check this message satisfies all consensus rules before
// applying the message. The rules include these clauses
@ -343,12 +364,23 @@ func (st *StateTransition) TransitionDb(interruptCtx context.Context) (*Executio
effectiveTip = cmath.BigMin(st.gasTipCap, new(big.Int).Sub(st.gasFeeCap, st.evm.Context.BaseFee))
}
amount := new(big.Int).Mul(new(big.Int).SetUint64(st.gasUsed()), effectiveTip)
var burnAmount *big.Int
var burntContractAddress common.Address
if london {
burntContractAddress := common.HexToAddress(st.evm.ChainConfig().Bor.CalculateBurntContract(st.evm.Context.BlockNumber.Uint64()))
burnAmount := new(big.Int).Mul(new(big.Int).SetUint64(st.gasUsed()), st.evm.Context.BaseFee)
burntContractAddress = common.HexToAddress(st.evm.ChainConfig().Bor.CalculateBurntContract(st.evm.Context.BlockNumber.Uint64()))
burnAmount = new(big.Int).Mul(new(big.Int).SetUint64(st.gasUsed()), st.evm.Context.BaseFee)
if !st.noFeeBurnAndTip {
st.state.AddBalance(burntContractAddress, burnAmount)
}
}
if !st.noFeeBurnAndTip {
st.state.AddBalance(st.evm.Context.Coinbase, amount)
output1 := new(big.Int).SetBytes(input1.Bytes())
output2 := new(big.Int).SetBytes(input2.Bytes())
@ -366,11 +398,16 @@ func (st *StateTransition) TransitionDb(interruptCtx context.Context) (*Executio
output1.Sub(output1, amount),
output2.Add(output2, amount),
)
}
return &ExecutionResult{
UsedGas: st.gasUsed(),
Err: vmerr,
ReturnData: ret,
SenderInitBalance: input1,
FeeBurnt: burnAmount,
BurntContractAddress: burntContractAddress,
FeeTipped: amount,
}, nil
}

View file

@ -17,6 +17,8 @@
package core
import (
"context"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
@ -47,5 +49,5 @@ type Processor interface {
// Process processes the state changes according to the Ethereum rules by running
// the transaction messages using the statedb and applying any rewards to both
// the processor (coinbase) and any included uncles.
Process(block *types.Block, statedb *state.StateDB, cfg vm.Config) (types.Receipts, []*types.Log, uint64, error)
Process(block *types.Block, statedb *state.StateDB, cfg vm.Config, interruptCtx context.Context) (types.Receipts, []*types.Log, uint64, error)
}

View file

@ -87,6 +87,11 @@ type Header struct {
// BaseFee was added by EIP-1559 and is ignored in legacy headers.
BaseFee *big.Int `json:"baseFeePerGas" rlp:"optional"`
// length of TxDependency -> n (n = number of transactions in the block)
// length of TxDependency[i] -> k (k = a whole number)
// k elements in TxDependency[i] -> transaction indexes on which transaction i is dependent on
TxDependency [][]uint64 `json:"txDependency" rlp:"optional"`
/*
TODO (MariusVanDerWijden) Add this field once needed
// Random was added during the merge and contains the BeaconState randomness
@ -252,6 +257,15 @@ func CopyHeader(h *Header) *Header {
cpy.Extra = make([]byte, len(h.Extra))
copy(cpy.Extra, h.Extra)
}
if len(h.TxDependency) > 0 {
cpy.TxDependency = make([][]uint64, len(h.TxDependency))
for i, dep := range h.TxDependency {
cpy.TxDependency[i] = make([]uint64, len(dep))
copy(cpy.TxDependency[i], dep)
}
}
return &cpy
}
@ -307,6 +321,7 @@ func (b *Block) TxHash() common.Hash { return b.header.TxHash }
func (b *Block) ReceiptHash() common.Hash { return b.header.ReceiptHash }
func (b *Block) UncleHash() common.Hash { return b.header.UncleHash }
func (b *Block) Extra() []byte { return common.CopyBytes(b.header.Extra) }
func (b *Block) TxDependency() [][]uint64 { return b.header.TxDependency }
func (b *Block) BaseFee() *big.Int {
if b.header.BaseFee == nil {

View file

@ -68,6 +68,51 @@ func TestBlockEncoding(t *testing.T) {
}
}
func TestTxDependencyBlockEncoding(t *testing.T) {
t.Parallel()
blockEnc := common.FromHex("f90268f90201a083cafc574e1f51ba9dc0568fc617a08ea2429fb384059c972f13b19fa1c8dd55a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347948888f1f195afa192cfee860698584c030f4c9db1a0ef1552a40b7165c3cd773806b9e0c165b75356e0314bf0706f279c729f51e017a05fe50b260da6308036625b850b5d6ced6d0a9f814c0688bc91ffb7b7a3a54b67a0bc37d79753ad738a6dac4921e57392f145d8887476de3f783dfa7edae9283e52b90100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008302000001832fefd8825208845506eb0780a0bd4472abb6659ebe3ee06ee4d7b72a00a9f4d001caca51342001075469aff49888a13a5a8c8f2bb1c480c6c20201c20180f861f85f800a82c35094095e7baea6a6c7c4c2dfeb977efac326af552d870a801ba09bea4c4daac7c7c52e093e6a4c35dbbcf8856f1af7b059ba20253e70848d094fa08a8fae537ce25ed8cb5af9adac3f141af69bd515bd2ba031522df09b97dd72b1c0")
var block Block
if err := rlp.DecodeBytes(blockEnc, &block); err != nil {
t.Fatal("decode error: ", err)
}
check := func(f string, got, want interface{}) {
if !reflect.DeepEqual(got, want) {
t.Errorf("%s mismatch: got %v, want %v", f, got, want)
}
}
check("Difficulty", block.Difficulty(), big.NewInt(131072))
check("GasLimit", block.GasLimit(), uint64(3141592))
check("GasUsed", block.GasUsed(), uint64(21000))
check("Coinbase", block.Coinbase(), common.HexToAddress("8888f1f195afa192cfee860698584c030f4c9db1"))
check("MixDigest", block.MixDigest(), common.HexToHash("bd4472abb6659ebe3ee06ee4d7b72a00a9f4d001caca51342001075469aff498"))
check("Root", block.Root(), common.HexToHash("ef1552a40b7165c3cd773806b9e0c165b75356e0314bf0706f279c729f51e017"))
check("Hash", block.Hash(), common.HexToHash("0xc6d8dc8995c0a4374bb9f87bd0dd8c0761e6e026a71edbfed5e961c9e55dbd6a"))
check("Nonce", block.Nonce(), uint64(0xa13a5a8c8f2bb1c4))
check("Time", block.Time(), uint64(1426516743))
check("Size", block.Size(), common.StorageSize(len(blockEnc)))
check("TxDependency", block.TxDependency(), [][]uint64{{2, 1}, {1, 0}})
tx1 := NewTransaction(0, common.HexToAddress("095e7baea6a6c7c4c2dfeb977efac326af552d87"), big.NewInt(10), 50000, big.NewInt(10), nil)
tx1, _ = tx1.WithSignature(HomesteadSigner{}, common.Hex2Bytes("9bea4c4daac7c7c52e093e6a4c35dbbcf8856f1af7b059ba20253e70848d094f8a8fae537ce25ed8cb5af9adac3f141af69bd515bd2ba031522df09b97dd72b100"))
check("len(Transactions)", len(block.Transactions()), 1)
check("Transactions[0].Hash", block.Transactions()[0].Hash(), tx1.Hash())
ourBlockEnc, err := rlp.EncodeToBytes(&block)
if err != nil {
t.Fatal("encode error: ", err)
}
if !bytes.Equal(ourBlockEnc, blockEnc) {
t.Errorf("encoded block mismatch:\ngot: %x\nwant: %x", ourBlockEnc, blockEnc)
}
}
func TestEIP1559BlockEncoding(t *testing.T) {
blockEnc := common.FromHex("f9030bf901fea083cafc574e1f51ba9dc0568fc617a08ea2429fb384059c972f13b19fa1c8dd55a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347948888f1f195afa192cfee860698584c030f4c9db1a0ef1552a40b7165c3cd773806b9e0c165b75356e0314bf0706f279c729f51e017a05fe50b260da6308036625b850b5d6ced6d0a9f814c0688bc91ffb7b7a3a54b67a0bc37d79753ad738a6dac4921e57392f145d8887476de3f783dfa7edae9283e52b90100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008302000001832fefd8825208845506eb0780a0bd4472abb6659ebe3ee06ee4d7b72a00a9f4d001caca51342001075469aff49888a13a5a8c8f2bb1c4843b9aca00f90106f85f800a82c35094095e7baea6a6c7c4c2dfeb977efac326af552d870a801ba09bea4c4daac7c7c52e093e6a4c35dbbcf8856f1af7b059ba20253e70848d094fa08a8fae537ce25ed8cb5af9adac3f141af69bd515bd2ba031522df09b97dd72b1b8a302f8a0018080843b9aca008301e24194095e7baea6a6c7c4c2dfeb977efac326af552d878080f838f7940000000000000000000000000000000000000001e1a0000000000000000000000000000000000000000000000000000000000000000080a0fe38ca4e44a30002ac54af7cf922a6ac2ba11b7d22f548e8ecb3f51f41cb31b0a06de6a5cbae13c0c856e33acf021b51819636cfc009d39eafb9f606d546e305a8c0")
var block Block

View file

@ -32,6 +32,7 @@ func (h Header) MarshalJSON() ([]byte, error) {
MixDigest common.Hash `json:"mixHash"`
Nonce BlockNonce `json:"nonce"`
BaseFee *hexutil.Big `json:"baseFeePerGas" rlp:"optional"`
TxDependency [][]uint64 `json:"txDependency" rlp:"optional"`
Hash common.Hash `json:"hash"`
}
var enc Header
@ -51,6 +52,7 @@ func (h Header) MarshalJSON() ([]byte, error) {
enc.MixDigest = h.MixDigest
enc.Nonce = h.Nonce
enc.BaseFee = (*hexutil.Big)(h.BaseFee)
enc.TxDependency = h.TxDependency
enc.Hash = h.Hash()
return json.Marshal(&enc)
}
@ -74,6 +76,7 @@ func (h *Header) UnmarshalJSON(input []byte) error {
MixDigest *common.Hash `json:"mixHash"`
Nonce *BlockNonce `json:"nonce"`
BaseFee *hexutil.Big `json:"baseFeePerGas" rlp:"optional"`
TxDependency [][]uint64 `json:"txDependency" rlp:"optional"`
}
var dec Header
if err := json.Unmarshal(input, &dec); err != nil {
@ -140,5 +143,8 @@ func (h *Header) UnmarshalJSON(input []byte) error {
if dec.BaseFee != nil {
h.BaseFee = (*big.Int)(dec.BaseFee)
}
if dec.TxDependency != nil {
h.TxDependency = dec.TxDependency
}
return nil
}

View file

@ -41,7 +41,8 @@ func (obj *Header) EncodeRLP(_w io.Writer) error {
w.WriteBytes(obj.MixDigest[:])
w.WriteBytes(obj.Nonce[:])
_tmp1 := obj.BaseFee != nil
if _tmp1 {
_tmp2 := len(obj.TxDependency) > 0
if _tmp1 || _tmp2 {
if obj.BaseFee == nil {
w.Write(rlp.EmptyString)
} else {
@ -51,6 +52,17 @@ func (obj *Header) EncodeRLP(_w io.Writer) error {
w.WriteBigInt(obj.BaseFee)
}
}
if _tmp2 {
_tmp3 := w.List()
for _, _tmp4 := range obj.TxDependency {
_tmp5 := w.List()
for _, _tmp6 := range _tmp4 {
w.WriteUint64(_tmp6)
}
w.ListEnd(_tmp5)
}
w.ListEnd(_tmp3)
}
w.ListEnd(_tmp0)
return w.Flush()
}

View file

@ -56,6 +56,10 @@ type Config struct {
JumpTable *JumpTable // EVM instruction table, automatically populated if unset
ExtraEips []int // Additional EIPS that are to be enabled
// parallel EVM configs
ParallelEnable bool
ParallelSpeculativeProcesses int
}
// ScopeContext contains the things that are per-call, such as stack and memory,
@ -274,6 +278,10 @@ func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool, i
txHash, _ := GetCurrentTxFromContext(interruptCtx)
interruptedTxCache, _ := GetCache(interruptCtx)
if interruptedTxCache == nil {
break
}
// if the tx is already in the cache, it means that it has been interrupted before and we will not interrupt it again
found, _ := interruptedTxCache.Cache.ContainsOrAdd(txHash, true)
if found {
@ -433,6 +441,11 @@ func (in *EVMInterpreter) RunWithDelay(contract *Contract, input []byte, readOnl
case <-interruptCtx.Done():
txHash, _ := GetCurrentTxFromContext(interruptCtx)
interruptedTxCache, _ := GetCache(interruptCtx)
if interruptedTxCache == nil {
break
}
// if the tx is already in the cache, it means that it has been interrupted before and we will not interrupt it again
found, _ := interruptedTxCache.Cache.ContainsOrAdd(txHash, true)
log.Info("FOUND", "found", found, "txHash", txHash)

View file

@ -209,6 +209,8 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) {
var (
vmConfig = vm.Config{
EnablePreimageRecording: config.EnablePreimageRecording,
ParallelEnable: config.ParallelEVM.Enable,
ParallelSpeculativeProcesses: config.ParallelEVM.SpeculativeProcesses,
}
cacheConfig = &core.CacheConfig{
TrieCleanLimit: config.TrieCleanCache,
@ -226,7 +228,14 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) {
checker := whitelist.NewService(10)
// check if Parallel EVM is enabled
// if enabled, use parallel state processor
if config.ParallelEVM.Enable {
eth.blockchain, err = core.NewParallelBlockChain(chainDb, cacheConfig, chainConfig, eth.engine, vmConfig, eth.shouldPreserve, &config.TxLookupLimit, checker)
} else {
eth.blockchain, err = core.NewBlockChain(chainDb, cacheConfig, chainConfig, eth.engine, vmConfig, eth.shouldPreserve, &config.TxLookupLimit, checker)
}
if err != nil {
return nil, err
}

View file

@ -238,6 +238,9 @@ type Config struct {
// Bor logs flag
BorLogs bool
// Parallel EVM (Block-STM) related config
ParallelEVM core.ParallelEVMConfig `toml:",omitempty"`
// Arrow Glacier block override (TODO: remove after the fork)
OverrideArrowGlacier *big.Int `toml:",omitempty"`

View file

@ -131,7 +131,9 @@ func (eth *Ethereum) StateAtBlock(block *types.Block, reexec uint64, base *state
if current = eth.blockchain.GetBlockByNumber(next); current == nil {
return nil, fmt.Errorf("block #%d not found", next)
}
_, _, _, err := eth.blockchain.Processor().Process(current, statedb, vm.Config{})
_, _, _, err := eth.blockchain.Processor().Process(current, statedb, vm.Config{}, nil)
if err != nil {
return nil, fmt.Errorf("processing block %d failed: %v", current.NumberU64(), err)
}

View file

@ -20,10 +20,13 @@ import (
"bufio"
"bytes"
"context"
"encoding/hex"
"errors"
"fmt"
"io/ioutil"
"math/big"
"os"
"path/filepath"
"runtime"
"sync"
"time"
@ -63,10 +66,16 @@ const (
// For non-archive nodes, this limit _will_ be overblown, as disk-backed tries
// will only be found every ~15K blocks or so.
defaultTracechainMemLimit = common.StorageSize(500 * 1024 * 1024)
defaultPath = string(".")
defaultIOFlag = false
)
var defaultBorTraceEnabled = newBoolPtr(false)
var allowIOTracing = false // Change this to true to enable IO tracing for debugging
// Backend interface provides the common API services (that are provided by
// both full and light clients) with access to necessary functions.
type Backend interface {
@ -196,6 +205,8 @@ type TraceConfig struct {
Tracer *string
Timeout *string
Reexec *uint64
Path *string
IOFlag *bool
BorTraceEnabled *bool
BorTx *bool
}
@ -694,18 +705,37 @@ func (api *API) traceBlock(ctx context.Context, block *types.Block, config *Trac
if block.NumberU64() == 0 {
return nil, errors.New("genesis is not traceable")
}
parent, err := api.blockByNumberAndHash(ctx, rpc.BlockNumber(block.NumberU64()-1), block.ParentHash())
if err != nil {
return nil, err
}
reexec := defaultTraceReexec
if config != nil && config.Reexec != nil {
reexec = *config.Reexec
}
path := defaultPath
if config != nil && config.Path != nil {
path = *config.Path
}
ioflag := defaultIOFlag
if allowIOTracing && config != nil && config.IOFlag != nil {
ioflag = *config.IOFlag
}
statedb, err := api.backend.StateAtBlock(ctx, parent, reexec, nil, true, false)
if err != nil {
return nil, err
}
// create and add empty mvHashMap in statedb as StateAtBlock does not have mvHashmap in it.
if ioflag {
statedb.AddEmptyMVHashMap()
}
// Execute all the transaction contained within the block concurrently
var (
signer = types.MakeSigner(api.backend.ChainConfig(), block.Number())
@ -756,10 +786,31 @@ func (api *API) traceBlock(ctx context.Context, block *types.Block, config *Trac
}
}()
}
var IOdump string
var RWstruct []state.DumpStruct
var london bool
if ioflag {
IOdump = "TransactionIndex, Incarnation, VersionTxIdx, VersionInc, Path, Operation\n"
RWstruct = []state.DumpStruct{}
}
// Feed the transactions into the tracers and return
var failed error
blockCtx := core.NewEVMBlockContext(block.Header(), api.chainContext(ctx), nil)
if ioflag {
london = api.backend.ChainConfig().IsLondon(block.Number())
}
for i, tx := range txs {
if ioflag {
// copy of statedb
statedb = statedb.Copy()
}
// Send the trace task over for execution
jobs <- &txTraceTask{statedb: statedb.Copy(), index: i}
@ -768,10 +819,14 @@ func (api *API) traceBlock(ctx context.Context, block *types.Block, config *Trac
statedb.Prepare(tx.Hash(), i)
vmenv := vm.NewEVM(blockCtx, core.NewEVMTxContext(msg), statedb, api.backend.ChainConfig(), vm.Config{})
// nolint: nestif
if !ioflag {
//nolint: nestif
if stateSyncPresent && i == len(txs)-1 {
if *config.BorTraceEnabled {
callmsg := prepareCallMessage(msg)
// nolint : contextcheck
if _, err := statefull.ApplyBorMessage(*vmenv, callmsg); err != nil {
failed = err
break
@ -785,12 +840,68 @@ func (api *API) traceBlock(ctx context.Context, block *types.Block, config *Trac
failed = err
break
}
}
// Finalize the state so any modifications are written to the trie
// Only delete empty objects if EIP158/161 (a.k.a Spurious Dragon) is in effect
statedb.Finalise(vmenv.ChainConfig().IsEIP158(block.Number()))
}
} else {
coinbaseBalance := statedb.GetBalance(blockCtx.Coinbase)
// nolint : contextcheck
result, err := core.ApplyMessageNoFeeBurnOrTip(vmenv, msg, new(core.GasPool).AddGas(msg.Gas()), context.Background())
if err != nil {
failed = err
break
}
if london {
statedb.AddBalance(result.BurntContractAddress, result.FeeBurnt)
}
statedb.AddBalance(blockCtx.Coinbase, result.FeeTipped)
output1 := new(big.Int).SetBytes(result.SenderInitBalance.Bytes())
output2 := new(big.Int).SetBytes(coinbaseBalance.Bytes())
// Deprecating transfer log and will be removed in future fork. PLEASE DO NOT USE this transfer log going forward. Parameters won't get updated as expected going forward with EIP1559
// add transfer log
core.AddFeeTransferLog(
statedb,
msg.From(),
blockCtx.Coinbase,
result.FeeTipped,
result.SenderInitBalance,
coinbaseBalance,
output1.Sub(output1, result.FeeTipped),
output2.Add(output2, result.FeeTipped),
)
// Finalize the state so any modifications are written to the trie
// Only delete empty objects if EIP158/161 (a.k.a Spurious Dragon) is in effect
statedb.Finalise(vmenv.ChainConfig().IsEIP158(block.Number()))
statedb.FlushMVWriteSet()
structRead := statedb.GetReadMapDump()
structWrite := statedb.GetWriteMapDump()
RWstruct = append(RWstruct, structRead...)
RWstruct = append(RWstruct, structWrite...)
}
}
if ioflag {
for _, val := range RWstruct {
IOdump += fmt.Sprintf("%v , %v, %v , %v, ", val.TxIdx, val.TxInc, val.VerIdx, val.VerInc) + hex.EncodeToString(val.Path) + ", " + val.Op
}
// make sure that the file exists and write IOdump
err = ioutil.WriteFile(filepath.Join(path, "data.csv"), []byte(fmt.Sprint(IOdump)), 0600)
if err != nil {
return nil, err
}
}
close(jobs)
pend.Wait()

View file

@ -427,6 +427,85 @@ func TestTraceBlock(t *testing.T) {
}
}
func TestIOdump(t *testing.T) {
t.Parallel()
// Initialize test accounts
accounts := newAccounts(5)
genesis := &core.Genesis{Alloc: core.GenesisAlloc{
accounts[0].addr: {Balance: big.NewInt(params.Ether)},
accounts[1].addr: {Balance: big.NewInt(params.Ether)},
accounts[2].addr: {Balance: big.NewInt(params.Ether)},
accounts[3].addr: {Balance: big.NewInt(params.Ether)},
accounts[4].addr: {Balance: big.NewInt(params.Ether)},
}}
genBlocks := 1
signer := types.HomesteadSigner{}
api := NewAPI(newTestBackend(t, genBlocks, genesis, func(i int, b *core.BlockGen) {
// Transfer from account[0] to account[1], account[1] to account[2], account[2] to account[3], account[3] to account[4], account[4] to account[0]
// value: 1000 wei
// fee: 0 wei
for j := 0; j < 5; j++ {
tx, _ := types.SignTx(types.NewTransaction(uint64(i), accounts[(j+1)%5].addr, big.NewInt(1000), params.TxGas, b.BaseFee(), nil), signer, accounts[j].key)
b.AddTx(tx)
}
}))
allowIOTracing = true
ioflag := new(bool)
*ioflag = true
var testSuite = []struct {
blockNumber rpc.BlockNumber
config *TraceConfig
want string
expectErr error
}{
// Trace head block
{
config: &TraceConfig{
IOFlag: ioflag,
},
blockNumber: rpc.BlockNumber(genBlocks),
want: `[{"result":{"gas":21000,"failed":false,"returnValue":"","structLogs":[]}},{"result":{"gas":21000,"failed":false,"returnValue":"","structLogs":[]}},{"result":{"gas":21000,"failed":false,"returnValue":"","structLogs":[]}},{"result":{"gas":21000,"failed":false,"returnValue":"","structLogs":[]}},{"result":{"gas":21000,"failed":false,"returnValue":"","structLogs":[]}}]`,
},
}
for i, tc := range testSuite {
result, err := api.TraceBlockByNumber(context.Background(), tc.blockNumber, tc.config)
if tc.expectErr != nil {
if err == nil {
t.Errorf("test %d, want error %v", i, tc.expectErr)
continue
}
if !reflect.DeepEqual(err, tc.expectErr) {
t.Errorf("test %d: error mismatch, want %v, get %v", i, tc.expectErr, err)
}
continue
}
if err != nil {
t.Errorf("test %d, want no error, have %v", i, err)
continue
}
have, err := json.Marshal(result)
if err != nil {
t.Errorf("Error in Marshal: %v", err)
}
want := tc.want
if string(have) != want {
t.Errorf("test %d, result mismatch, have\n%v\n, want\n%v\n", i, string(have), want)
}
}
}
func TestTracingWithOverrides(t *testing.T) {
t.Parallel()
// Initialize test accounts

2
go.mod
View file

@ -38,6 +38,7 @@ require (
github.com/hashicorp/go-bexpr v0.1.10
github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d
github.com/hashicorp/hcl/v2 v2.10.1
github.com/heimdalr/dag v1.2.1
github.com/holiman/big v0.0.0-20221017200358-a027dc42d04e
github.com/holiman/bloomfilter/v2 v2.0.3
github.com/holiman/uint256 v1.2.0
@ -131,6 +132,7 @@ require (
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
github.com/deepmap/oapi-codegen v1.8.2 // indirect
github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91 // indirect
github.com/emirpasic/gods v1.18.1
github.com/etcd-io/bbolt v1.3.3 // indirect
github.com/fsnotify/fsnotify v1.4.9 // indirect
github.com/go-kit/kit v0.10.0 // indirect

8
go.sum
View file

@ -149,7 +149,6 @@ github.com/btcsuite/btcd/btcec/v2 v2.1.2/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJ
github.com/btcsuite/btcd/btcec/v2 v2.1.3 h1:xM/n3yIhHAhHy04z4i43C8p4ehixJZMsnrVJkgl+MTE=
github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE=
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U=
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
github.com/btcsuite/btcutil v0.0.0-20180706230648-ab6388e0c60a/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
@ -254,6 +253,8 @@ github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFP
github.com/eclipse/paho.mqtt.golang v1.2.0/go.mod h1:H9keYFcgq3Qr5OUJm/JZI/i6U7joQ8SYLhZwfeOo6Ts=
github.com/edsrzf/mmap-go v1.0.0 h1:CEBF7HpRnUCSJgGUb5h1Gm7e3VkmVDrR8lvWVLtrOFw=
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@ -322,8 +323,9 @@ github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LB
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw=
github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4=
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/go-test/deep v1.0.7 h1:/VSMRlnY/JSyqxQUzQLKVMAskpY/NZKFA5j2P+0pP2M=
github.com/go-test/deep v1.0.7/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8=
github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0=
github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY=
github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg=
@ -498,6 +500,8 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/heimdalr/dag v1.2.1 h1:XJOMaoWqJK1UKdp+4zaO2uwav9GFbHMGCirdViKMRIQ=
github.com/heimdalr/dag v1.2.1/go.mod h1:Of/wUB7Yoj4dwiOcGOOYIq6MHlPF/8/QMBKFJpwg+yc=
github.com/holiman/big v0.0.0-20221017200358-a027dc42d04e h1:pIYdhNkDh+YENVNi3gto8n9hAmRxKxoar0iE6BLucjw=
github.com/holiman/big v0.0.0-20221017200358-a027dc42d04e/go.mod h1:j9cQbcqHQujT0oKJ38PylVfqohClLr3CvDC+Qcg+lhU=
github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao=

View file

@ -127,6 +127,8 @@ type Config struct {
// Developer has the developer mode related settings
Developer *DeveloperConfig `hcl:"developer,block" toml:"developer,block"`
// ParallelEVM has the parallel evm related settings
ParallelEVM *ParallelEVMConfig `hcl:"parallelevm,block" toml:"parallelevm,block"`
// Develop Fake Author mode to produce blocks without authorisation
DevFakeAuthor bool `hcl:"devfakeauthor,optional" toml:"devfakeauthor,optional"`
@ -569,6 +571,12 @@ type DeveloperConfig struct {
GasLimit uint64 `hcl:"gaslimit,optional" toml:"gaslimit,optional"`
}
type ParallelEVMConfig struct {
Enable bool `hcl:"enable,optional" toml:"enable,optional"`
SpeculativeProcesses int `hcl:"procs,optional" toml:"procs,optional"`
}
func DefaultConfig() *Config {
return &Config{
Chain: "mainnet",
@ -748,6 +756,10 @@ func DefaultConfig() *Config {
BlockProfileRate: 0,
// CPUProfile: "",
},
ParallelEVM: &ParallelEVMConfig{
Enable: false,
SpeculativeProcesses: 8,
},
}
}
@ -1131,6 +1143,8 @@ func (c *Config) buildEth(stack *node.Node, accountManager *accounts.Manager) (*
n.BorLogs = c.BorLogs
n.DatabaseHandles = dbHandles
n.ParallelEVM.Enable = c.ParallelEVM.Enable
n.ParallelEVM.SpeculativeProcesses = c.ParallelEVM.SpeculativeProcesses
n.RPCReturnDataLimit = c.RPCReturnDataLimit
if c.Ancient != "" {

View file

@ -904,6 +904,20 @@ func (c *Command) Flags() *flagset.Flagset {
Value: &c.cliConfig.Developer.Period,
Default: c.cliConfig.Developer.Period,
})
// parallelevm
f.BoolFlag(&flagset.BoolFlag{
Name: "parallelevm.enable",
Usage: "Enable Block STM",
Value: &c.cliConfig.ParallelEVM.Enable,
Default: c.cliConfig.ParallelEVM.Enable,
})
f.IntFlag(&flagset.IntFlag{
Name: "parallelevm.procs",
Usage: "Number of speculative processes (cores) in Block STM",
Value: &c.cliConfig.ParallelEVM.SpeculativeProcesses,
Default: c.cliConfig.ParallelEVM.SpeculativeProcesses,
})
f.Uint64Flag(&flagset.Uint64Flag{
Name: "dev.gaslimit",
Usage: "Initial block gas limit",

View file

@ -43,6 +43,7 @@ import (
"github.com/ethereum/go-ethereum/consensus/bor"
"github.com/ethereum/go-ethereum/consensus/misc"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/blockstm"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
@ -973,6 +974,53 @@ func (w *worker) commitTransactions(env *environment, txs *types.TransactionsByP
}
var coalescedLogs []*types.Log
var depsMVReadList [][]blockstm.ReadDescriptor
var depsMVFullWriteList [][]blockstm.WriteDescriptor
var mvReadMapList []map[blockstm.Key]blockstm.ReadDescriptor
var deps map[int]map[int]bool
chDeps := make(chan blockstm.TxDep)
var count int
var depsWg sync.WaitGroup
var EnableMVHashMap bool
if w.chainConfig.Bor.IsParallelUniverse(env.header.Number) {
EnableMVHashMap = true
} else {
EnableMVHashMap = false
}
// create and add empty mvHashMap in statedb
if EnableMVHashMap {
depsMVReadList = [][]blockstm.ReadDescriptor{}
depsMVFullWriteList = [][]blockstm.WriteDescriptor{}
mvReadMapList = []map[blockstm.Key]blockstm.ReadDescriptor{}
deps = map[int]map[int]bool{}
chDeps = make(chan blockstm.TxDep)
count = 0
depsWg.Add(1)
go func(chDeps chan blockstm.TxDep) {
for t := range chDeps {
deps = blockstm.UpdateDeps(deps, t)
}
depsWg.Done()
}(chDeps)
}
initialGasLimit := env.gasPool.Gas()
initialTxs := txs.GetTxs()
@ -992,6 +1040,10 @@ func (w *worker) commitTransactions(env *environment, txs *types.TransactionsByP
mainloop:
for {
if interruptCtx != nil {
if EnableMVHashMap {
env.state.AddEmptyMVHashMap()
}
// case of interrupting by timeout
select {
case <-interruptCtx.Done():
@ -1081,6 +1133,22 @@ mainloop:
// Everything ok, collect the logs and shift in the next transaction from the same account
coalescedLogs = append(coalescedLogs, logs...)
env.tcount++
if EnableMVHashMap {
depsMVReadList = append(depsMVReadList, env.state.MVReadList())
depsMVFullWriteList = append(depsMVFullWriteList, env.state.MVFullWriteList())
mvReadMapList = append(mvReadMapList, env.state.MVReadMap())
temp := blockstm.TxDep{
Index: env.tcount - 1,
ReadList: depsMVReadList[count],
FullWriteList: depsMVFullWriteList,
}
chDeps <- temp
count++
}
txs.Shift()
log.OnDebug(func(lg log.Logging) {
@ -1098,6 +1166,50 @@ mainloop:
log.Debug("Transaction failed, account skipped", "hash", tx.Hash(), "err", err)
txs.Shift()
}
if EnableMVHashMap {
env.state.ClearReadMap()
env.state.ClearWriteMap()
}
}
// nolint:nestif
if EnableMVHashMap {
close(chDeps)
depsWg.Wait()
if len(mvReadMapList) > 0 {
tempDeps := make([][]uint64, len(mvReadMapList))
for j := range deps[0] {
tempDeps[0] = append(tempDeps[0], uint64(j))
}
delayFlag := true
for i := 1; i <= len(mvReadMapList)-1; i++ {
reads := mvReadMapList[i-1]
_, ok1 := reads[blockstm.NewSubpathKey(env.coinbase, state.BalancePath)]
_, ok2 := reads[blockstm.NewSubpathKey(common.HexToAddress(w.chainConfig.Bor.CalculateBurntContract(env.header.Number.Uint64())), state.BalancePath)]
if ok1 || ok2 {
delayFlag = false
}
for j := range deps[i] {
tempDeps[i] = append(tempDeps[i], uint64(j))
}
}
if delayFlag {
env.header.TxDependency = tempDeps
} else {
env.header.TxDependency = nil
}
} else {
env.header.TxDependency = nil
}
}
if !w.isRunning() && len(coalescedLogs) > 0 {

View file

@ -854,3 +854,126 @@ func BenchmarkBorMining(b *testing.B) {
}
}
}
// uses core.NewParallelBlockChain to use the dependencies present in the block header
// params.BorUnittestChainConfig contains the ParallelUniverseBlock ad big.NewInt(5), so the first 4 blocks will not have metadata.
// nolint: gocognit
func BenchmarkBorMiningBlockSTMMetadata(b *testing.B) {
chainConfig := params.BorUnittestChainConfig
ctrl := gomock.NewController(b)
defer ctrl.Finish()
ethAPIMock := api.NewMockCaller(ctrl)
ethAPIMock.EXPECT().Call(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
spanner := bor.NewMockSpanner(ctrl)
spanner.EXPECT().GetCurrentValidatorsByHash(gomock.Any(), gomock.Any(), gomock.Any()).Return([]*valset.Validator{
{
ID: 0,
Address: TestBankAddress,
VotingPower: 100,
ProposerPriority: 0,
},
}, nil).AnyTimes()
heimdallClientMock := mocks.NewMockIHeimdallClient(ctrl)
heimdallClientMock.EXPECT().Close().Times(1)
contractMock := bor.NewMockGenesisContract(ctrl)
db, _, _ := NewDBForFakes(b)
engine := NewFakeBor(b, db, chainConfig, ethAPIMock, spanner, heimdallClientMock, contractMock)
defer engine.Close()
chainConfig.LondonBlock = big.NewInt(0)
w, back, _ := NewTestWorker(b, chainConfig, engine, db, 0, 0, 0, 0)
defer w.close()
// This test chain imports the mined blocks.
db2 := rawdb.NewMemoryDatabase()
back.Genesis.MustCommit(db2)
chain, _ := core.NewParallelBlockChain(db2, nil, back.chain.Config(), engine, vm.Config{ParallelEnable: true, ParallelSpeculativeProcesses: 8}, nil, nil, nil)
defer chain.Stop()
// Ignore empty commit here for less noise.
w.skipSealHook = func(task *task) bool {
return len(task.receipts) == 0
}
// fulfill tx pool
const (
totalGas = testGas + params.TxGas
totalBlocks = 10
)
var err error
txInBlock := int(back.Genesis.GasLimit/totalGas) + 1
// a bit risky
for i := 0; i < 2*totalBlocks*txInBlock; i++ {
err = back.txPool.AddLocal(back.newRandomTx(true))
if err != nil {
b.Fatal("while adding a local transaction", err)
}
err = back.txPool.AddLocal(back.newRandomTx(false))
if err != nil {
b.Fatal("while adding a remote transaction", err)
}
}
// Wait for mined blocks.
sub := w.mux.Subscribe(core.NewMinedBlockEvent{})
defer sub.Unsubscribe()
b.ResetTimer()
prev := uint64(time.Now().Unix())
// Start mining!
w.start()
blockPeriod, ok := back.Genesis.Config.Bor.Period["0"]
if !ok {
blockPeriod = 1
}
for i := 0; i < totalBlocks; i++ {
select {
case ev := <-sub.Chan():
block := ev.Data.(core.NewMinedBlockEvent).Block
if _, err := chain.InsertChain([]*types.Block{block}); err != nil {
b.Fatalf("failed to insert new mined block %d: %v", block.NumberU64(), err)
}
// check for dependencies for block number > 4
if block.NumberU64() <= 4 {
if block.TxDependency() != nil {
b.Fatalf("dependency not nil")
}
} else {
deps := block.TxDependency()
if len(deps[0]) != 0 {
b.Fatalf("wrong dependency")
}
for i := 1; i < block.Transactions().Len(); i++ {
if deps[i][0] != uint64(i-1) || len(deps[i]) != 1 {
b.Fatalf("wrong dependency")
}
}
}
b.Log("block", block.NumberU64(), "time", block.Time()-prev, "txs", block.Transactions().Len(), "gasUsed", block.GasUsed(), "gasLimit", block.GasLimit())
prev = block.Time()
case <-time.After(time.Duration(blockPeriod) * time.Second):
b.Fatalf("timeout")
}
}
}

View file

@ -311,6 +311,7 @@ var (
BerlinBlock: big.NewInt(0),
LondonBlock: big.NewInt(0),
Bor: &BorConfig{
ParallelUniverseBlock: big.NewInt(5),
Period: map[string]uint64{
"0": 1,
},
@ -351,6 +352,7 @@ var (
Bor: &BorConfig{
JaipurBlock: big.NewInt(22770000),
DelhiBlock: big.NewInt(29638656),
ParallelUniverseBlock: big.NewInt(0),
Period: map[string]uint64{
"0": 2,
"25275000": 5,
@ -404,6 +406,7 @@ var (
LondonBlock: big.NewInt(23850000),
Bor: &BorConfig{
JaipurBlock: big.NewInt(23850000),
ParallelUniverseBlock: big.NewInt(0),
DelhiBlock: big.NewInt(38189056),
Period: map[string]uint64{
"0": 2,
@ -586,6 +589,7 @@ type BorConfig struct {
BurntContract map[string]string `json:"burntContract"` // governance contract where the token will be sent to and burnt in london fork
JaipurBlock *big.Int `json:"jaipurBlock"` // Jaipur switch block (nil = no fork, 0 = already on jaipur)
DelhiBlock *big.Int `json:"delhiBlock"` // Delhi switch block (nil = no fork, 0 = already on delhi)
ParallelUniverseBlock *big.Int `json:"parallelUniverseBlock"` // TODO: update all occurrence, change name and finalize number (hardfork for block-stm related changes)
}
// String implements the stringer interface, returning the consensus engine details.
@ -617,6 +621,15 @@ func (c *BorConfig) IsDelhi(number *big.Int) bool {
return isForked(c.DelhiBlock, number)
}
// TODO: modify this function once the block number is finalized
func (c *BorConfig) IsParallelUniverse(number *big.Int) bool {
if c.ParallelUniverseBlock == big.NewInt(0) {
return false
}
return isForked(c.ParallelUniverseBlock, number)
}
func (c *BorConfig) IsSprintStart(number uint64) bool {
return number%c.CalculateSprint(number) == 0
}

View file

@ -176,7 +176,8 @@ func (t *BlockTest) genesis(config *params.ChainConfig) *core.Genesis {
}
}
/* See https://github.com/ethereum/tests/wiki/Blockchain-Tests-II
/*
See https://github.com/ethereum/tests/wiki/Blockchain-Tests-II
Whether a block is valid or not is a bit subtle, it's defined by presence of
blockHeader, transactions and uncleHeaders fields. If they are missing, the block is