mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-06-19 13:21:37 +00:00
Merge 4cbf0e314b into b1baab4427
This commit is contained in:
commit
40224091fb
53 changed files with 8158 additions and 1667 deletions
|
|
@ -72,11 +72,10 @@ var (
|
||||||
accountReadTimer = metrics.NewRegisteredResettingTimer("chain/account/reads", nil)
|
accountReadTimer = metrics.NewRegisteredResettingTimer("chain/account/reads", nil)
|
||||||
accountHashTimer = metrics.NewRegisteredResettingTimer("chain/account/hashes", nil)
|
accountHashTimer = metrics.NewRegisteredResettingTimer("chain/account/hashes", nil)
|
||||||
accountUpdateTimer = metrics.NewRegisteredResettingTimer("chain/account/updates", nil)
|
accountUpdateTimer = metrics.NewRegisteredResettingTimer("chain/account/updates", nil)
|
||||||
accountCommitTimer = metrics.NewRegisteredResettingTimer("chain/account/commits", nil)
|
hasherCommitTimer = metrics.NewRegisteredResettingTimer("chain/trie/commits", nil)
|
||||||
|
|
||||||
storageReadTimer = metrics.NewRegisteredResettingTimer("chain/storage/reads", nil)
|
storageReadTimer = metrics.NewRegisteredResettingTimer("chain/storage/reads", nil)
|
||||||
storageUpdateTimer = metrics.NewRegisteredResettingTimer("chain/storage/updates", nil)
|
storageUpdateTimer = metrics.NewRegisteredResettingTimer("chain/storage/updates", nil)
|
||||||
storageCommitTimer = metrics.NewRegisteredResettingTimer("chain/storage/commits", nil)
|
|
||||||
codeReadTimer = metrics.NewRegisteredResettingTimer("chain/code/reads", nil)
|
codeReadTimer = metrics.NewRegisteredResettingTimer("chain/code/reads", nil)
|
||||||
codeReadBytesTimer = metrics.NewRegisteredResettingTimer("chain/code/readbytes", nil)
|
codeReadBytesTimer = metrics.NewRegisteredResettingTimer("chain/code/readbytes", nil)
|
||||||
|
|
||||||
|
|
@ -2112,30 +2111,53 @@ type ExecuteConfig struct {
|
||||||
// it writes the block and associated state to database.
|
// it writes the block and associated state to database.
|
||||||
func (bc *BlockChain) ProcessBlock(ctx context.Context, parentRoot common.Hash, block *types.Block, config ExecuteConfig) (result *blockProcessingResult, blockEndErr error) {
|
func (bc *BlockChain) ProcessBlock(ctx context.Context, parentRoot common.Hash, block *types.Block, config ExecuteConfig) (result *blockProcessingResult, blockEndErr error) {
|
||||||
var (
|
var (
|
||||||
err error
|
err error
|
||||||
startTime = time.Now()
|
startTime = time.Now()
|
||||||
statedb *state.StateDB
|
interrupt atomic.Bool
|
||||||
interrupt atomic.Bool
|
sdb = state.NewDatabase(bc.triedb, bc.codedb).WithSnapshot(bc.snaps)
|
||||||
sdb = state.NewDatabase(bc.triedb, bc.codedb).WithSnapshot(bc.snaps)
|
makeWitness bool
|
||||||
|
|
||||||
|
throwaway *state.StateDB // StateDB for speculative transaction pre-executor
|
||||||
|
statedb *state.StateDB // StateDB for sequential transaction executor
|
||||||
)
|
)
|
||||||
|
if bc.chainConfig.IsByzantium(block.Number()) && (config.StatelessSelfValidation || config.MakeWitness) {
|
||||||
|
makeWitness = true
|
||||||
|
}
|
||||||
defer interrupt.Store(true) // terminate the prefetch at the end
|
defer interrupt.Store(true) // terminate the prefetch at the end
|
||||||
|
|
||||||
|
// Enable trie node prewarming after the Byzantium fork. Before that, state
|
||||||
|
// computation occurs at transaction boundaries, making prewarming ineffective.
|
||||||
|
// The read-only state should also be prewarmed to construct a comprehensive
|
||||||
|
// execution witness.
|
||||||
|
if bc.chainConfig.IsByzantium(block.Number()) {
|
||||||
|
sdb = sdb.EnablePrefetch(makeWitness)
|
||||||
|
|
||||||
|
// Explicitly terminate all the background prefetcher. This is essential
|
||||||
|
// to prevent goroutine leaks.
|
||||||
|
defer func() {
|
||||||
|
if statedb != nil {
|
||||||
|
statedb.StopPrefetcher()
|
||||||
|
}
|
||||||
|
if throwaway != nil {
|
||||||
|
throwaway.StopPrefetcher()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
if bc.cfg.NoPrefetch {
|
if bc.cfg.NoPrefetch {
|
||||||
statedb, err = state.New(parentRoot, sdb)
|
statedb, err = state.New(parentRoot, sdb)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// If prefetching is enabled, run that against the current state to pre-cache
|
// If transaction prefetching is enabled, run that against the current state
|
||||||
// transactions and probabilistically some of the account/storage trie nodes.
|
// to pre-cache transactions. Note: the main processor and prefetcher share
|
||||||
//
|
// the same reader with a local cache for mitigating the overhead of state
|
||||||
// Note: the main processor and prefetcher share the same reader with a local
|
// access.
|
||||||
// cache for mitigating the overhead of state access.
|
|
||||||
prefetch, process, err := sdb.ReadersWithCacheStats(parentRoot)
|
prefetch, process, err := sdb.ReadersWithCacheStats(parentRoot)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
throwaway, err := state.NewWithReader(parentRoot, sdb, prefetch)
|
throwaway, err = state.NewWithReader(parentRoot, sdb, prefetch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -2171,20 +2193,16 @@ func (bc *BlockChain) ProcessBlock(ctx context.Context, parentRoot common.Hash,
|
||||||
// while processing transactions. Before Byzantium the prefetcher is mostly
|
// while processing transactions. Before Byzantium the prefetcher is mostly
|
||||||
// useless due to the intermediate root hashing after each transaction.
|
// useless due to the intermediate root hashing after each transaction.
|
||||||
var witness *stateless.Witness
|
var witness *stateless.Witness
|
||||||
if bc.chainConfig.IsByzantium(block.Number()) {
|
if makeWitness {
|
||||||
// Generate witnesses either if we're self-testing, or if it's the
|
// Generate witnesses either if we're self-testing, or if it's the
|
||||||
// only block being inserted. A bit crude, but witnesses are huge,
|
// only block being inserted. A bit crude, but witnesses are huge,
|
||||||
// so we refuse to make an entire chain of them.
|
// so we refuse to make an entire chain of them.
|
||||||
if config.StatelessSelfValidation || config.MakeWitness {
|
witness, err = stateless.NewWitness(block.Header(), bc, config.EnableWitnessStats)
|
||||||
witness, err = stateless.NewWitness(block.Header(), bc, config.EnableWitnessStats)
|
if err != nil {
|
||||||
if err != nil {
|
return nil, err
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
statedb.StartPrefetcher("chain", witness)
|
statedb.TraceWitness(witness)
|
||||||
defer statedb.StopPrefetcher()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Instrument the blockchain tracing
|
// Instrument the blockchain tracing
|
||||||
if config.EnableTracer {
|
if config.EnableTracer {
|
||||||
if bc.logger != nil && bc.logger.OnBlockStart != nil {
|
if bc.logger != nil && bc.logger.OnBlockStart != nil {
|
||||||
|
|
@ -2222,64 +2240,10 @@ func (bc *BlockChain) ProcessBlock(ctx context.Context, parentRoot common.Hash,
|
||||||
}
|
}
|
||||||
vtime := time.Since(vstart)
|
vtime := time.Since(vstart)
|
||||||
|
|
||||||
// If witnesses was generated and stateless self-validation requested, do
|
|
||||||
// that now. Self validation should *never* run in production, it's more of
|
|
||||||
// a tight integration to enable running *all* consensus tests through the
|
|
||||||
// witness builder/runner, which would otherwise be impossible due to the
|
|
||||||
// various invalid chain states/behaviors being contained in those tests.
|
|
||||||
xvstart := time.Now()
|
|
||||||
if witness := statedb.Witness(); witness != nil && config.StatelessSelfValidation {
|
|
||||||
log.Warn("Running stateless self-validation", "block", block.Number(), "hash", block.Hash())
|
|
||||||
|
|
||||||
// Remove critical computed fields from the block to force true recalculation
|
|
||||||
context := block.Header()
|
|
||||||
context.Root = common.Hash{}
|
|
||||||
context.ReceiptHash = common.Hash{}
|
|
||||||
|
|
||||||
task := types.NewBlockWithHeader(context).WithBody(*block.Body())
|
|
||||||
|
|
||||||
// Run the stateless self-cross-validation
|
|
||||||
crossStateRoot, crossReceiptRoot, err := ExecuteStateless(ctx, bc.chainConfig, bc.cfg.VmConfig, task, witness)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("stateless self-validation failed: %v", err)
|
|
||||||
}
|
|
||||||
if crossStateRoot != block.Root() {
|
|
||||||
return nil, fmt.Errorf("stateless self-validation root mismatch (cross: %x local: %x)", crossStateRoot, block.Root())
|
|
||||||
}
|
|
||||||
if crossReceiptRoot != block.ReceiptHash() {
|
|
||||||
return nil, fmt.Errorf("stateless self-validation receipt root mismatch (cross: %x local: %x)", crossReceiptRoot, block.ReceiptHash())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
xvtime = time.Since(xvstart)
|
|
||||||
proctime = time.Since(startTime) // processing + validation + cross validation
|
proctime = time.Since(startTime) // processing + validation + cross validation
|
||||||
stats = &ExecuteStats{}
|
stats = NewExecuteStats(statedb, ptime, vtime)
|
||||||
)
|
)
|
||||||
// Update the metrics touched during block processing and validation
|
|
||||||
stats.AccountReads = statedb.AccountReads // Account reads are complete(in processing)
|
|
||||||
stats.StorageReads = statedb.StorageReads // Storage reads are complete(in processing)
|
|
||||||
stats.AccountUpdates = statedb.AccountUpdates // Account updates are complete(in validation)
|
|
||||||
stats.StorageUpdates = statedb.StorageUpdates // Storage updates are complete(in validation)
|
|
||||||
stats.AccountHashes = statedb.AccountHashes // Account hashes are complete(in validation)
|
|
||||||
stats.CodeReads = statedb.CodeReads
|
|
||||||
|
|
||||||
stats.AccountLoaded = statedb.AccountLoaded
|
|
||||||
stats.AccountUpdated = statedb.AccountUpdated
|
|
||||||
stats.AccountDeleted = statedb.AccountDeleted
|
|
||||||
stats.StorageLoaded = statedb.StorageLoaded
|
|
||||||
stats.StorageUpdated = int(statedb.StorageUpdated.Load())
|
|
||||||
stats.StorageDeleted = int(statedb.StorageDeleted.Load())
|
|
||||||
|
|
||||||
stats.CodeLoaded = statedb.CodeLoaded
|
|
||||||
stats.CodeLoadBytes = statedb.CodeLoadBytes
|
|
||||||
stats.CodeUpdated = statedb.CodeUpdated
|
|
||||||
stats.CodeUpdateBytes = statedb.CodeUpdateBytes
|
|
||||||
|
|
||||||
stats.Execution = ptime - (statedb.AccountReads + statedb.StorageReads + statedb.CodeReads) // The time spent on EVM processing
|
|
||||||
stats.Validation = vtime - (statedb.AccountHashes + statedb.AccountUpdates + statedb.StorageUpdates) // The time spent on block validation
|
|
||||||
stats.CrossValidation = xvtime // The time spent on stateless cross validation
|
|
||||||
|
|
||||||
// Write the block to the chain and get the status.
|
// Write the block to the chain and get the status.
|
||||||
var status WriteStatus
|
var status WriteStatus
|
||||||
if config.WriteState {
|
if config.WriteState {
|
||||||
|
|
@ -2294,10 +2258,9 @@ func (bc *BlockChain) ProcessBlock(ctx context.Context, parentRoot common.Hash,
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
// Update the metrics touched during block commit
|
// Update the metrics touched during block commit
|
||||||
stats.AccountCommits = statedb.AccountCommits // Account commits are complete, we can mark them
|
stats.HasherCommit = statedb.HasherCommits // Storage commits are complete, we can mark them
|
||||||
stats.StorageCommits = statedb.StorageCommits // Storage commits are complete, we can mark them
|
|
||||||
stats.DatabaseCommit = statedb.DatabaseCommits // Database commits are complete, we can mark them
|
stats.DatabaseCommit = statedb.DatabaseCommits // Database commits are complete, we can mark them
|
||||||
stats.BlockWrite = time.Since(wstart) - max(statedb.AccountCommits, statedb.StorageCommits) /* concurrent */ - statedb.DatabaseCommits
|
stats.BlockWrite = time.Since(wstart) - statedb.HasherCommits - statedb.DatabaseCommits
|
||||||
}
|
}
|
||||||
// Report the collected witness statistics
|
// Report the collected witness statistics
|
||||||
if witness != nil {
|
if witness != nil {
|
||||||
|
|
@ -2307,6 +2270,11 @@ func (bc *BlockChain) ProcessBlock(ctx context.Context, parentRoot common.Hash,
|
||||||
stats.TotalTime = elapsed
|
stats.TotalTime = elapsed
|
||||||
stats.MgasPerSecond = float64(res.GasUsed) * 1000 / float64(elapsed)
|
stats.MgasPerSecond = float64(res.GasUsed) * 1000 / float64(elapsed)
|
||||||
|
|
||||||
|
if config.StatelessSelfValidation {
|
||||||
|
if err := bc.crossValidation(ctx, statedb, block); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
return &blockProcessingResult{
|
return &blockProcessingResult{
|
||||||
usedGas: res.GasUsed,
|
usedGas: res.GasUsed,
|
||||||
procTime: proctime,
|
procTime: proctime,
|
||||||
|
|
@ -2316,6 +2284,39 @@ func (bc *BlockChain) ProcessBlock(ctx context.Context, parentRoot common.Hash,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (bc *BlockChain) crossValidation(ctx context.Context, statedb *state.StateDB, block *types.Block) error {
|
||||||
|
// If witnesses was generated and stateless self-validation requested, do
|
||||||
|
// that now. Self validation should *never* run in production, it's more of
|
||||||
|
// a tight integration to enable running *all* consensus tests through the
|
||||||
|
// witness builder/runner, which would otherwise be impossible due to the
|
||||||
|
// various invalid chain states/behaviors being contained in those tests.
|
||||||
|
if witness := statedb.Witness(); witness != nil {
|
||||||
|
xvstart := time.Now()
|
||||||
|
log.Warn("Running stateless self-validation", "block", block.Number(), "hash", block.Hash())
|
||||||
|
|
||||||
|
// Remove critical computed fields from the block to force true recalculation
|
||||||
|
context := block.Header()
|
||||||
|
context.Root = common.Hash{}
|
||||||
|
context.ReceiptHash = common.Hash{}
|
||||||
|
|
||||||
|
task := types.NewBlockWithHeader(context).WithBody(*block.Body())
|
||||||
|
|
||||||
|
// Run the stateless self-cross-validation
|
||||||
|
crossStateRoot, crossReceiptRoot, err := ExecuteStateless(ctx, bc.chainConfig, bc.cfg.VmConfig, task, witness)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("stateless self-validation failed: %v", err)
|
||||||
|
}
|
||||||
|
if crossStateRoot != block.Root() {
|
||||||
|
return fmt.Errorf("stateless self-validation root mismatch (cross: %x local: %x)", crossStateRoot, block.Root())
|
||||||
|
}
|
||||||
|
if crossReceiptRoot != block.ReceiptHash() {
|
||||||
|
return fmt.Errorf("stateless self-validation receipt root mismatch (cross: %x local: %x)", crossReceiptRoot, block.ReceiptHash())
|
||||||
|
}
|
||||||
|
blockCrossValidationTimer.UpdateSince(xvstart)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// insertSideChain is called when an import batch hits upon a pruned ancestor
|
// insertSideChain is called when an import batch hits upon a pruned ancestor
|
||||||
// error, which happens when a sidechain with a sufficiently old fork-block is
|
// error, which happens when a sidechain with a sufficiently old fork-block is
|
||||||
// found.
|
// found.
|
||||||
|
|
|
||||||
|
|
@ -424,6 +424,25 @@ func (bc *BlockChain) StateAt(root common.Hash) (*state.StateDB, error) {
|
||||||
return state.New(root, state.NewDatabase(bc.triedb, bc.codedb).WithSnapshot(bc.snaps))
|
return state.New(root, state.NewDatabase(bc.triedb, bc.codedb).WithSnapshot(bc.snaps))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StateConfig specifies the configuration for initializating the stateDB.
|
||||||
|
type StateConfig struct {
|
||||||
|
Prefetch bool
|
||||||
|
PrefetchRead bool
|
||||||
|
WithSnapshot bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// StateWithConfig returns a new mutable state based on a particular point in time.
|
||||||
|
func (bc *BlockChain) StateWithConfig(root common.Hash, config StateConfig) (*state.StateDB, error) {
|
||||||
|
sdb := state.NewDatabase(bc.triedb, bc.codedb)
|
||||||
|
if config.WithSnapshot {
|
||||||
|
sdb = sdb.WithSnapshot(bc.snaps)
|
||||||
|
}
|
||||||
|
if config.Prefetch {
|
||||||
|
sdb = sdb.EnablePrefetch(config.PrefetchRead)
|
||||||
|
}
|
||||||
|
return state.New(root, sdb)
|
||||||
|
}
|
||||||
|
|
||||||
// HistoricState returns a historic state specified by the given root.
|
// HistoricState returns a historic state specified by the given root.
|
||||||
// Live states are not available and won't be served, please use `State`
|
// Live states are not available and won't be served, please use `State`
|
||||||
// or `StateAt` instead.
|
// or `StateAt` instead.
|
||||||
|
|
|
||||||
|
|
@ -29,14 +29,27 @@ import (
|
||||||
// ExecuteStats includes all the statistics of a block execution in details.
|
// ExecuteStats includes all the statistics of a block execution in details.
|
||||||
type ExecuteStats struct {
|
type ExecuteStats struct {
|
||||||
// State read times
|
// State read times
|
||||||
AccountReads time.Duration // Time spent on the account reads
|
AccountReads time.Duration // Time spent on the account reads
|
||||||
StorageReads time.Duration // Time spent on the storage reads
|
StorageReads time.Duration // Time spent on the storage reads
|
||||||
|
CodeReads time.Duration // Time spent on the contract code read
|
||||||
|
|
||||||
|
// State hash times
|
||||||
AccountHashes time.Duration // Time spent on the account trie hash
|
AccountHashes time.Duration // Time spent on the account trie hash
|
||||||
AccountUpdates time.Duration // Time spent on the account trie update
|
AccountUpdates time.Duration // Time spent on the account trie update
|
||||||
AccountCommits time.Duration // Time spent on the account trie commit
|
|
||||||
StorageUpdates time.Duration // Time spent on the storage trie update
|
StorageUpdates time.Duration // Time spent on the storage trie update
|
||||||
StorageCommits time.Duration // Time spent on the storage trie commit
|
|
||||||
CodeReads time.Duration // Time spent on the contract code read
|
// EVM execution and validation time
|
||||||
|
Execution time.Duration // Time spent on the EVM execution
|
||||||
|
Validation time.Duration // Time spent on the block validation
|
||||||
|
|
||||||
|
// Commit times
|
||||||
|
HasherCommit time.Duration // Time spent on trie commit
|
||||||
|
DatabaseCommit time.Duration // Time spent on database commit
|
||||||
|
BlockWrite time.Duration // Time spent on block write
|
||||||
|
|
||||||
|
// Others
|
||||||
|
TotalTime time.Duration // The total time spent on block execution
|
||||||
|
MgasPerSecond float64 // The million gas processed per second
|
||||||
|
|
||||||
AccountLoaded int // Number of accounts loaded
|
AccountLoaded int // Number of accounts loaded
|
||||||
AccountUpdated int // Number of accounts updated
|
AccountUpdated int // Number of accounts updated
|
||||||
|
|
@ -49,19 +62,39 @@ type ExecuteStats struct {
|
||||||
CodeUpdated int // Number of contract code written (CREATE/CREATE2 + EIP-7702)
|
CodeUpdated int // Number of contract code written (CREATE/CREATE2 + EIP-7702)
|
||||||
CodeUpdateBytes int // Total bytes of code written
|
CodeUpdateBytes int // Total bytes of code written
|
||||||
|
|
||||||
Execution time.Duration // Time spent on the EVM execution
|
|
||||||
Validation time.Duration // Time spent on the block validation
|
|
||||||
CrossValidation time.Duration // Optional, time spent on the block cross validation
|
|
||||||
DatabaseCommit time.Duration // Time spent on database commit
|
|
||||||
BlockWrite time.Duration // Time spent on block write
|
|
||||||
TotalTime time.Duration // The total time spent on block execution
|
|
||||||
MgasPerSecond float64 // The million gas processed per second
|
|
||||||
|
|
||||||
// Cache hit rates
|
// Cache hit rates
|
||||||
StateReadCacheStats state.ReaderStats
|
StateReadCacheStats state.ReaderStats
|
||||||
StatePrefetchCacheStats state.ReaderStats
|
StatePrefetchCacheStats state.ReaderStats
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewExecuteStats(stateDB *state.StateDB, process time.Duration, validation time.Duration) *ExecuteStats {
|
||||||
|
return &ExecuteStats{
|
||||||
|
// State read times
|
||||||
|
AccountReads: stateDB.AccountReads,
|
||||||
|
StorageReads: stateDB.StorageReads,
|
||||||
|
CodeReads: stateDB.CodeReads,
|
||||||
|
|
||||||
|
// State hash times
|
||||||
|
AccountHashes: stateDB.AccountHashes,
|
||||||
|
AccountUpdates: stateDB.AccountUpdates,
|
||||||
|
StorageUpdates: stateDB.StorageUpdates,
|
||||||
|
|
||||||
|
Execution: process - stateDB.StateReadTime(),
|
||||||
|
Validation: validation - stateDB.StateHashTime(),
|
||||||
|
|
||||||
|
AccountLoaded: stateDB.AccountLoaded,
|
||||||
|
AccountUpdated: stateDB.AccountUpdated,
|
||||||
|
AccountDeleted: stateDB.AccountDeleted,
|
||||||
|
StorageLoaded: stateDB.StorageLoaded,
|
||||||
|
StorageUpdated: int(stateDB.StorageUpdated.Load()),
|
||||||
|
StorageDeleted: int(stateDB.StorageDeleted.Load()),
|
||||||
|
CodeLoaded: stateDB.CodeLoaded,
|
||||||
|
CodeLoadBytes: stateDB.CodeLoadBytes,
|
||||||
|
CodeUpdated: stateDB.CodeUpdated,
|
||||||
|
CodeUpdateBytes: stateDB.CodeUpdateBytes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// reportMetrics uploads execution statistics to the metrics system.
|
// reportMetrics uploads execution statistics to the metrics system.
|
||||||
func (s *ExecuteStats) reportMetrics() {
|
func (s *ExecuteStats) reportMetrics() {
|
||||||
if s.AccountLoaded != 0 {
|
if s.AccountLoaded != 0 {
|
||||||
|
|
@ -80,12 +113,10 @@ func (s *ExecuteStats) reportMetrics() {
|
||||||
accountUpdateTimer.Update(s.AccountUpdates) // Account updates are complete(in validation)
|
accountUpdateTimer.Update(s.AccountUpdates) // Account updates are complete(in validation)
|
||||||
storageUpdateTimer.Update(s.StorageUpdates) // Storage updates are complete(in validation)
|
storageUpdateTimer.Update(s.StorageUpdates) // Storage updates are complete(in validation)
|
||||||
accountHashTimer.Update(s.AccountHashes) // Account hashes are complete(in validation)
|
accountHashTimer.Update(s.AccountHashes) // Account hashes are complete(in validation)
|
||||||
accountCommitTimer.Update(s.AccountCommits) // Account commits are complete, we can mark them
|
hasherCommitTimer.Update(s.HasherCommit) // Trie commits are complete, we can mark them
|
||||||
storageCommitTimer.Update(s.StorageCommits) // Storage commits are complete, we can mark them
|
|
||||||
|
|
||||||
blockExecutionTimer.Update(s.Execution) // The time spent on EVM processing
|
blockExecutionTimer.Update(s.Execution) // The time spent on EVM processing
|
||||||
blockValidationTimer.Update(s.Validation) // The time spent on block validation
|
blockValidationTimer.Update(s.Validation) // The time spent on block validation
|
||||||
blockCrossValidationTimer.Update(s.CrossValidation) // The time spent on stateless cross validation
|
|
||||||
triedbCommitTimer.Update(s.DatabaseCommit) // Trie database commits are complete, we can mark them
|
triedbCommitTimer.Update(s.DatabaseCommit) // Trie database commits are complete, we can mark them
|
||||||
blockWriteTimer.Update(s.BlockWrite) // The time spent on block write
|
blockWriteTimer.Update(s.BlockWrite) // The time spent on block write
|
||||||
blockInsertTimer.Update(s.TotalTime) // The total time spent on block execution
|
blockInsertTimer.Update(s.TotalTime) // The total time spent on block execution
|
||||||
|
|
@ -206,7 +237,7 @@ func (s *ExecuteStats) logSlow(block *types.Block, slowBlockThreshold time.Durat
|
||||||
ExecutionMs: durationToMs(s.Execution),
|
ExecutionMs: durationToMs(s.Execution),
|
||||||
StateReadMs: durationToMs(s.AccountReads + s.StorageReads + s.CodeReads),
|
StateReadMs: durationToMs(s.AccountReads + s.StorageReads + s.CodeReads),
|
||||||
StateHashMs: durationToMs(s.AccountHashes + s.AccountUpdates + s.StorageUpdates),
|
StateHashMs: durationToMs(s.AccountHashes + s.AccountUpdates + s.StorageUpdates),
|
||||||
CommitMs: durationToMs(max(s.AccountCommits, s.StorageCommits) + s.DatabaseCommit + s.BlockWrite),
|
CommitMs: durationToMs(s.HasherCommit + s.DatabaseCommit + s.BlockWrite),
|
||||||
TotalMs: durationToMs(s.TotalTime),
|
TotalMs: durationToMs(s.TotalTime),
|
||||||
},
|
},
|
||||||
Throughput: slowBlockThru{
|
Throughput: slowBlockThru{
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,34 @@ func DeleteStorageSnapshot(db ethdb.KeyValueWriter, accountHash, storageHash com
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReadBinTrieStem retrieves the flat-state stem blob for the given 31-byte
|
||||||
|
// stem. Returns nil if no entry exists under this stem.
|
||||||
|
//
|
||||||
|
// The stem blob is a packed representation of the (offset, value) pairs at
|
||||||
|
// that stem in the binary trie; callers must decode it to extract any
|
||||||
|
// specific offset. See trie/bintrie and EIP-7864 for the on-trie layout.
|
||||||
|
func ReadBinTrieStem(db ethdb.KeyValueReader, stem []byte) []byte {
|
||||||
|
data, _ := db.Get(binTrieStemKey(stem))
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteBinTrieStem stores the flat-state stem blob for the given 31-byte
|
||||||
|
// stem. The blob is written verbatim; encoding/decoding is the caller's
|
||||||
|
// responsibility.
|
||||||
|
func WriteBinTrieStem(db ethdb.KeyValueWriter, stem []byte, blob []byte) {
|
||||||
|
if err := db.Put(binTrieStemKey(stem), blob); err != nil {
|
||||||
|
log.Crit("Failed to store bintrie stem", "err", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteBinTrieStem removes the flat-state stem blob entry for the given
|
||||||
|
// 31-byte stem.
|
||||||
|
func DeleteBinTrieStem(db ethdb.KeyValueWriter, stem []byte) {
|
||||||
|
if err := db.Delete(binTrieStemKey(stem)); err != nil {
|
||||||
|
log.Crit("Failed to delete bintrie stem", "err", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// IterateStorageSnapshots returns an iterator for walking the entire storage
|
// IterateStorageSnapshots returns an iterator for walking the entire storage
|
||||||
// space of a specific account.
|
// space of a specific account.
|
||||||
func IterateStorageSnapshots(db ethdb.Iteratee, accountHash common.Hash) ethdb.Iterator {
|
func IterateStorageSnapshots(db ethdb.Iteratee, accountHash common.Hash) ethdb.Iterator {
|
||||||
|
|
|
||||||
|
|
@ -126,6 +126,20 @@ var (
|
||||||
TrieNodeStoragePrefix = []byte("O") // TrieNodeStoragePrefix + accountHash + hexPath -> trie node
|
TrieNodeStoragePrefix = []byte("O") // TrieNodeStoragePrefix + accountHash + hexPath -> trie node
|
||||||
stateIDPrefix = []byte("L") // stateIDPrefix + state root -> state id
|
stateIDPrefix = []byte("L") // stateIDPrefix + state root -> state id
|
||||||
|
|
||||||
|
// Binary-trie flat-state scheme. A stem is 31 bytes per EIP-7864 (the
|
||||||
|
// common prefix of the 32-byte tree key); the stored value is a packed
|
||||||
|
// blob containing the subset of 256 offset values that are populated
|
||||||
|
// for this stem (layout: 32-byte bitmap of present offsets, followed
|
||||||
|
// by N 32-byte values in offset order).
|
||||||
|
//
|
||||||
|
// Note: bintrie pathdb wraps the disk database in a table keyed by
|
||||||
|
// VerklePrefix ("v"), so this prefix is effectively nested inside "v"
|
||||||
|
// when used by pathdb. It is defined as a distinct top-level byte
|
||||||
|
// ("X") to prevent accidental collisions with other top-level
|
||||||
|
// namespaces (e.g. blockBodyPrefix "b") when the codec is ever used
|
||||||
|
// against an unwrapped database.
|
||||||
|
BinTrieStemPrefix = []byte("X") // BinTrieStemPrefix + stem(31B) -> stem blob
|
||||||
|
|
||||||
// State history indexing within path-based storage scheme
|
// State history indexing within path-based storage scheme
|
||||||
StateHistoryIndexPrefix = []byte("m") // The global prefix of state history index data
|
StateHistoryIndexPrefix = []byte("m") // The global prefix of state history index data
|
||||||
StateHistoryAccountMetadataPrefix = []byte("ma") // StateHistoryAccountMetadataPrefix + account address hash => account metadata
|
StateHistoryAccountMetadataPrefix = []byte("ma") // StateHistoryAccountMetadataPrefix + account address hash => account metadata
|
||||||
|
|
@ -297,6 +311,22 @@ func storageTrieNodeKey(accountHash common.Hash, path []byte) []byte {
|
||||||
return buf
|
return buf
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// binTrieStemKey = BinTrieStemPrefix + stem (31 bytes).
|
||||||
|
//
|
||||||
|
// A bintrie stem is the common 31-byte prefix of the 32-byte tree key (see
|
||||||
|
// EIP-7864). The stem blob stored under this key holds the packed set of
|
||||||
|
// (offset, value) pairs at that stem, from which BasicData (offset 0),
|
||||||
|
// CodeHash (offset 1), header storage (offsets 64-127), code chunks
|
||||||
|
// (offsets 128-255) and main-storage slots can be extracted.
|
||||||
|
func binTrieStemKey(stem []byte) []byte {
|
||||||
|
// Callers always pass a 31-byte stem. We allocate the exact size to
|
||||||
|
// avoid accidental aliasing with backing storage.
|
||||||
|
buf := make([]byte, len(BinTrieStemPrefix)+len(stem))
|
||||||
|
n := copy(buf, BinTrieStemPrefix)
|
||||||
|
copy(buf[n:], stem)
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
// IsLegacyTrieNode reports whether a provided database entry is a legacy trie
|
// IsLegacyTrieNode reports whether a provided database entry is a legacy trie
|
||||||
// node. The characteristics of legacy trie node are:
|
// node. The characteristics of legacy trie node are:
|
||||||
// - the key length is 32 bytes
|
// - the key length is 32 bytes
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,6 @@
|
||||||
package state
|
package state
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
"github.com/ethereum/go-ethereum/core/overlay"
|
"github.com/ethereum/go-ethereum/core/overlay"
|
||||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||||
|
|
@ -29,7 +27,6 @@ import (
|
||||||
"github.com/ethereum/go-ethereum/log"
|
"github.com/ethereum/go-ethereum/log"
|
||||||
"github.com/ethereum/go-ethereum/trie"
|
"github.com/ethereum/go-ethereum/trie"
|
||||||
"github.com/ethereum/go-ethereum/trie/bintrie"
|
"github.com/ethereum/go-ethereum/trie/bintrie"
|
||||||
"github.com/ethereum/go-ethereum/trie/transitiontrie"
|
|
||||||
"github.com/ethereum/go-ethereum/trie/trienode"
|
"github.com/ethereum/go-ethereum/trie/trienode"
|
||||||
"github.com/ethereum/go-ethereum/triedb"
|
"github.com/ethereum/go-ethereum/triedb"
|
||||||
)
|
)
|
||||||
|
|
@ -43,6 +40,9 @@ type Database interface {
|
||||||
// through which the account iterator and storage iterator can be created.
|
// through which the account iterator and storage iterator can be created.
|
||||||
Iteratee(root common.Hash) (Iteratee, error)
|
Iteratee(root common.Hash) (Iteratee, error)
|
||||||
|
|
||||||
|
// Hasher returns a state hasher associated with the specified state root.
|
||||||
|
Hasher(root common.Hash) (Hasher, error)
|
||||||
|
|
||||||
// OpenTrie opens the main account trie.
|
// OpenTrie opens the main account trie.
|
||||||
OpenTrie(root common.Hash) (Trie, error)
|
OpenTrie(root common.Hash) (Trie, error)
|
||||||
|
|
||||||
|
|
@ -150,6 +150,9 @@ type CachingDB struct {
|
||||||
triedb *triedb.Database
|
triedb *triedb.Database
|
||||||
codedb *CodeDB
|
codedb *CodeDB
|
||||||
snap *snapshot.Tree
|
snap *snapshot.Tree
|
||||||
|
|
||||||
|
prefetch bool
|
||||||
|
prefetchRead bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDatabase creates a state database with the provided data sources.
|
// NewDatabase creates a state database with the provided data sources.
|
||||||
|
|
@ -177,6 +180,13 @@ func (db *CachingDB) WithSnapshot(snapshot *snapshot.Tree) *CachingDB {
|
||||||
return db
|
return db
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EnablePrefetch enables the hasher prefetching feature.
|
||||||
|
func (db *CachingDB) EnablePrefetch(prefetchRead bool) *CachingDB {
|
||||||
|
db.prefetch = true
|
||||||
|
db.prefetchRead = prefetchRead
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
// StateReader returns a state reader associated with the specified state root.
|
// StateReader returns a state reader associated with the specified state root.
|
||||||
func (db *CachingDB) StateReader(stateRoot common.Hash) (StateReader, error) {
|
func (db *CachingDB) StateReader(stateRoot common.Hash) (StateReader, error) {
|
||||||
var readers []StateReader
|
var readers []StateReader
|
||||||
|
|
@ -194,10 +204,25 @@ func (db *CachingDB) StateReader(stateRoot common.Hash) (StateReader, error) {
|
||||||
// This reader offers improved performance but is optional and only
|
// This reader offers improved performance but is optional and only
|
||||||
// partially useful if the snapshot data in path database is not
|
// partially useful if the snapshot data in path database is not
|
||||||
// fully generated.
|
// fully generated.
|
||||||
|
//
|
||||||
|
// For binary-trie databases the reader needs codec-specific key
|
||||||
|
// derivation (EIP-7864 stem || offset) and a separate decode path
|
||||||
|
// (BasicData/CodeHash leaves rather than slim RLP), so we install
|
||||||
|
// a bintrieFlatReader instead of the historical merkle flatReader.
|
||||||
|
// If the underlying path-database reader can't expose raw-byte
|
||||||
|
// access — e.g. a hypothetical wrapper that only implements the
|
||||||
|
// minimal database.StateReader — we silently fall through to the
|
||||||
|
// trie reader, which always works.
|
||||||
if db.TrieDB().Scheme() == rawdb.PathScheme {
|
if db.TrieDB().Scheme() == rawdb.PathScheme {
|
||||||
reader, err := db.triedb.StateReader(stateRoot)
|
reader, err := db.triedb.StateReader(stateRoot)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
readers = append(readers, newFlatReader(reader))
|
if db.TrieDB().IsVerkle() {
|
||||||
|
if br := newBintrieFlatReader(reader); br != nil {
|
||||||
|
readers = append(readers, br)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
readers = append(readers, newFlatReader(reader))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Configure the trie reader, which is expected to be available as the
|
// Configure the trie reader, which is expected to be available as the
|
||||||
|
|
@ -221,6 +246,15 @@ func (db *CachingDB) Reader(stateRoot common.Hash) (Reader, error) {
|
||||||
return newReader(db.codedb.Reader(), sr), nil
|
return newReader(db.codedb.Reader(), sr), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hasher implements Database, returning a hasher associated with the specified
|
||||||
|
// state root.
|
||||||
|
func (db *CachingDB) Hasher(stateRoot common.Hash) (Hasher, error) {
|
||||||
|
if db.TrieDB().IsVerkle() {
|
||||||
|
return newBinaryHasher(stateRoot, db.triedb, db.prefetch, db.prefetchRead)
|
||||||
|
}
|
||||||
|
return newMerkleHasher(stateRoot, db.triedb, db.prefetch, db.prefetchRead)
|
||||||
|
}
|
||||||
|
|
||||||
// ReadersWithCacheStats creates a pair of state readers that share the same
|
// ReadersWithCacheStats creates a pair of state readers that share the same
|
||||||
// underlying state reader and internal state cache, while maintaining separate
|
// underlying state reader and internal state cache, while maintaining separate
|
||||||
// statistics respectively.
|
// statistics respectively.
|
||||||
|
|
@ -297,7 +331,11 @@ func (db *CachingDB) Commit(update *stateUpdate) error {
|
||||||
}
|
}
|
||||||
// If snapshotting is enabled, update the snapshot tree with this new version
|
// If snapshotting is enabled, update the snapshot tree with this new version
|
||||||
if db.snap != nil && db.snap.Snapshot(update.originRoot) != nil {
|
if db.snap != nil && db.snap.Snapshot(update.originRoot) != nil {
|
||||||
if err := db.snap.Update(update.root, update.originRoot, update.accounts, update.storages); err != nil {
|
accounts, _, storages, _, err := update.encodeMerkle()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := db.snap.Update(update.root, update.originRoot, accounts, storages); err != nil {
|
||||||
log.Warn("Failed to update snapshot tree", "from", update.originRoot, "to", update.root, "err", err)
|
log.Warn("Failed to update snapshot tree", "from", update.originRoot, "to", update.root, "err", err)
|
||||||
}
|
}
|
||||||
// Keep 128 diff layers in the memory, persistent layer is 129th.
|
// Keep 128 diff layers in the memory, persistent layer is 129th.
|
||||||
|
|
@ -308,7 +346,11 @@ func (db *CachingDB) Commit(update *stateUpdate) error {
|
||||||
log.Warn("Failed to cap snapshot tree", "root", update.root, "layers", TriesInMemory, "err", err)
|
log.Warn("Failed to cap snapshot tree", "root", update.root, "layers", TriesInMemory, "err", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return db.triedb.Update(update.root, update.originRoot, update.blockNumber, update.nodes, update.stateSet())
|
stateSet, err := update.stateSet(!db.TrieDB().IsVerkle())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return db.triedb.Update(update.root, update.originRoot, update.blockNumber, update.nodes, stateSet)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Iteratee returns a state iteratee associated with the specified state root,
|
// Iteratee returns a state iteratee associated with the specified state root,
|
||||||
|
|
@ -316,15 +358,3 @@ func (db *CachingDB) Commit(update *stateUpdate) error {
|
||||||
func (db *CachingDB) Iteratee(root common.Hash) (Iteratee, error) {
|
func (db *CachingDB) Iteratee(root common.Hash) (Iteratee, error) {
|
||||||
return newStateIteratee(!db.triedb.IsVerkle(), root, db.triedb, db.snap)
|
return newStateIteratee(!db.triedb.IsVerkle(), root, db.triedb, db.snap)
|
||||||
}
|
}
|
||||||
|
|
||||||
// mustCopyTrie returns a deep-copied trie.
|
|
||||||
func mustCopyTrie(t Trie) Trie {
|
|
||||||
switch t := t.(type) {
|
|
||||||
case *trie.StateTrie:
|
|
||||||
return t.Copy()
|
|
||||||
case *transitiontrie.TransitionTrie:
|
|
||||||
return t.Copy()
|
|
||||||
default:
|
|
||||||
panic(fmt.Errorf("unknown trie type %T", t))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
202
core/state/database_hasher.go
Normal file
202
core/state/database_hasher.go
Normal file
|
|
@ -0,0 +1,202 @@
|
||||||
|
// Copyright 2026 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 state
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/core/stateless"
|
||||||
|
"github.com/ethereum/go-ethereum/ethdb"
|
||||||
|
"github.com/ethereum/go-ethereum/trie/trienode"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CodeMut represents a mutation to contract code.
|
||||||
|
type CodeMut struct {
|
||||||
|
Code []byte // Null for deletion
|
||||||
|
}
|
||||||
|
|
||||||
|
// AccountMut represents a mutation to an account.
|
||||||
|
// Semantics:
|
||||||
|
// - Account == nil: delete the account
|
||||||
|
// - Code == nil: leave code unchanged
|
||||||
|
// - Code != nil: apply the given code mutation
|
||||||
|
// - CodeSize: the account's CURRENT total code size, not just the bytes
|
||||||
|
// carried in Code. It is used by implementations that pack the code
|
||||||
|
// size into their on-trie account encoding (e.g. the binary trie
|
||||||
|
// BasicData leaf). Callers must always populate this field to the
|
||||||
|
// account's real code size, obtained via stateObject.CodeSize() or an
|
||||||
|
// equivalent source — even on balance/nonce-only updates where the
|
||||||
|
// code bytes themselves are not loaded. Leaving it at zero on a
|
||||||
|
// non-code-touching update silently corrupts on-trie state for any
|
||||||
|
// hasher that stores code size.
|
||||||
|
type AccountMut struct {
|
||||||
|
Account *Account // Null for deletion
|
||||||
|
Code *CodeMut // Null for unchanged
|
||||||
|
CodeSize int // Current code length (must be set by the caller)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hashes encapsulates a trie root together with its original (pre-update) root.
|
||||||
|
type Hashes struct {
|
||||||
|
Hash common.Hash // Post-mutation root
|
||||||
|
Prev common.Hash // Pre-mutation root
|
||||||
|
}
|
||||||
|
|
||||||
|
// StemWrite describes a single write to a bintrie stem offset. It is used
|
||||||
|
// by LeafProducer-capable hashers to report flat-state mutations derived
|
||||||
|
// from their trie updates so a downstream flat-state layer can be kept
|
||||||
|
// consistent with the hasher's on-trie view.
|
||||||
|
//
|
||||||
|
// Stem is the 31-byte common prefix of the EIP-7864 tree key. Offset is
|
||||||
|
// the index into the stem's 256-value group (0..255). Value is the
|
||||||
|
// 32-byte leaf value that was written; the caller uses the per-call
|
||||||
|
// policy documented on the binary hasher:
|
||||||
|
// - Account create/update: two writes (BasicData, CodeHash) with
|
||||||
|
// non-nil 32-byte values.
|
||||||
|
// - Storage update to a non-zero value: one write with the 32-byte
|
||||||
|
// normalized value.
|
||||||
|
// - Storage update to zero (the bintrie's "delete" convention): one
|
||||||
|
// write with 32 zero bytes (tombstone / present with zero).
|
||||||
|
// - Account delete: two writes with nil values, signalling the flat
|
||||||
|
// state to clear the corresponding offsets.
|
||||||
|
type StemWrite struct {
|
||||||
|
Stem [31]byte
|
||||||
|
Offset byte
|
||||||
|
Value []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// LeafProducer is an optional extension to Hasher for implementations
|
||||||
|
// that track flat-state mutations alongside trie updates. Callers use it
|
||||||
|
// to harvest the set of stem writes needed to keep an out-of-band flat
|
||||||
|
// state layer consistent with the hasher's trie mutations.
|
||||||
|
//
|
||||||
|
// The binary hasher implements this interface; the merkle hasher does
|
||||||
|
// not, because merkle flat state is MPT-shaped and does not use stems.
|
||||||
|
// Callers check via a type assertion:
|
||||||
|
//
|
||||||
|
// if lp, ok := h.(LeafProducer); ok {
|
||||||
|
// writes := lp.DrainStemWrites()
|
||||||
|
// // ... propagate writes into the state update ...
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// DrainStemWrites is intended to be called ONCE per block, AFTER all
|
||||||
|
// UpdateAccount/UpdateStorage calls for that block have completed. The
|
||||||
|
// implementation must reset its internal buffer on drain so subsequent
|
||||||
|
// calls return only writes accumulated since the last drain.
|
||||||
|
type LeafProducer interface {
|
||||||
|
// DrainStemWrites returns all stem writes accumulated since the last
|
||||||
|
// drain, in the order they were produced, and resets the internal
|
||||||
|
// buffer. The returned slice is owned by the caller; the hasher
|
||||||
|
// allocates a fresh slice on the next update.
|
||||||
|
DrainStemWrites() []StemWrite
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hasher defines the minimal interface for computing state root hashes.
|
||||||
|
//
|
||||||
|
// It abstracts over different trie implementations, such as the traditional
|
||||||
|
// two-layer Merkle Patricia Trie (separate account and storage tries) and a
|
||||||
|
// unified single-layer binary trie (a single trie covering accounts, storages
|
||||||
|
// and contract code).
|
||||||
|
//
|
||||||
|
// This abstraction also enables alternative implementations, such as a no-op
|
||||||
|
// hasher for flat-state-only nodes (i.e. nodes that do not store trie data and
|
||||||
|
// do not perform state validation).
|
||||||
|
//
|
||||||
|
// The Hash method may be invoked multiple times and must return a hash that
|
||||||
|
// reflects all preceding state mutations. This behavior is required for
|
||||||
|
// compatibility with pre-Byzantium semantics.
|
||||||
|
type Hasher interface {
|
||||||
|
// UpdateAccount writes a list of accounts into the state.
|
||||||
|
UpdateAccount(addresses []common.Address, accounts []AccountMut) error
|
||||||
|
|
||||||
|
// UpdateStorage writes a list of storage slot values.
|
||||||
|
UpdateStorage(address common.Address, keys []common.Hash, values []common.Hash) error
|
||||||
|
|
||||||
|
// Hash computes and returns the state root hash without committing.
|
||||||
|
Hash() common.Hash
|
||||||
|
|
||||||
|
// Commit finalizes all pending changes and returns the resulting state root
|
||||||
|
// hash, along with the set of dirty trie nodes generated by the updates.
|
||||||
|
//
|
||||||
|
// Additionally, if the hasher uses a two-layer structure, the roots of the
|
||||||
|
// secondary tries together with their original hashes will also be returned
|
||||||
|
// for all mutated accounts, regardless of whether their storage was modified.
|
||||||
|
Commit() (common.Hash, *trienode.MergedNodeSet, map[common.Address]Hashes, error)
|
||||||
|
|
||||||
|
// Copy returns a deep-copied hasher instance.
|
||||||
|
Copy() Hasher
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefetcher is an optional extension implemented by hashers that can
|
||||||
|
// asynchronously warm up trie/state data ahead of hashing.
|
||||||
|
type Prefetcher interface {
|
||||||
|
// PrefetchAccount schedules the account for prefetching.
|
||||||
|
PrefetchAccount(addresses []common.Address, read bool)
|
||||||
|
|
||||||
|
// PrefetchStorage schedules the storage slot for prefetching.
|
||||||
|
PrefetchStorage(addr common.Address, keys []common.Hash, read bool)
|
||||||
|
|
||||||
|
// TermPrefetch terminates all the background prefetching activities.
|
||||||
|
TermPrefetch()
|
||||||
|
}
|
||||||
|
|
||||||
|
// WitnessCollector is an optional extension implemented by hashers that can
|
||||||
|
// construct a state witness for the most recent committed state transition.
|
||||||
|
type WitnessCollector interface {
|
||||||
|
// CollectWitness returns the state witness corresponding to the most recent
|
||||||
|
// committed state transition.
|
||||||
|
CollectWitness(*stateless.Witness)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prover is an optional extension implemented by hashers that can construct
|
||||||
|
// proofs against the current state.
|
||||||
|
type Prover interface {
|
||||||
|
// ProveAccount constructs a proof for the given account.
|
||||||
|
//
|
||||||
|
// The returned proof contains all encoded nodes on the path to the account.
|
||||||
|
// The account itself is included in the last node and can be retrieved by
|
||||||
|
// verifying the proof.
|
||||||
|
//
|
||||||
|
// If the account does not exist, the returned proof contains all nodes of
|
||||||
|
// the longest existing prefix of the account key (at least the root), ending
|
||||||
|
// with the node that proves the absence of the account.
|
||||||
|
ProveAccount(addr common.Address, proofDb ethdb.KeyValueWriter) error
|
||||||
|
|
||||||
|
// ProveStorage constructs a proof for the given storage slot of the
|
||||||
|
// specified account.
|
||||||
|
//
|
||||||
|
// The returned proof contains all encoded nodes on the path to the storage
|
||||||
|
// slot. The slot value itself is included in the last node and can be
|
||||||
|
// retrieved by verifying the proof.
|
||||||
|
//
|
||||||
|
// If the account or storage slot does not exist, the returned proof contains
|
||||||
|
// the nodes required to prove its absence.
|
||||||
|
ProveStorage(addr common.Address, key common.Hash, proofDb ethdb.KeyValueWriter) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// noopHasher is a Hasher implementation that performs no work and always
|
||||||
|
// returns an empty state root.
|
||||||
|
type noopHasher struct{}
|
||||||
|
|
||||||
|
func (n *noopHasher) UpdateAccount([]common.Address, []AccountMut) error { return nil }
|
||||||
|
func (n *noopHasher) UpdateStorage(common.Address, []common.Hash, []common.Hash) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (n *noopHasher) Hash() common.Hash { return common.Hash{} }
|
||||||
|
func (n *noopHasher) Commit() (common.Hash, *trienode.MergedNodeSet, map[common.Address]Hashes, error) {
|
||||||
|
return common.Hash{}, trienode.NewMergedNodeSet(), make(map[common.Address]Hashes), nil
|
||||||
|
}
|
||||||
|
func (n *noopHasher) Copy() Hasher { return &noopHasher{} }
|
||||||
|
func (n *noopHasher) Close() {}
|
||||||
397
core/state/database_hasher_binary.go
Normal file
397
core/state/database_hasher_binary.go
Normal file
|
|
@ -0,0 +1,397 @@
|
||||||
|
// Copyright 2026 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 state
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/core/stateless"
|
||||||
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
|
"github.com/ethereum/go-ethereum/ethdb"
|
||||||
|
"github.com/ethereum/go-ethereum/trie/bintrie"
|
||||||
|
"github.com/ethereum/go-ethereum/trie/trienode"
|
||||||
|
"github.com/ethereum/go-ethereum/triedb"
|
||||||
|
)
|
||||||
|
|
||||||
|
// wrapBinTrie pairs a BinaryTrie with an optional background prefetcher that
|
||||||
|
// preloads trie nodes ahead of mutation.
|
||||||
|
type wrapBinTrie struct {
|
||||||
|
*bintrie.BinaryTrie
|
||||||
|
prefetcher *prefetcher
|
||||||
|
}
|
||||||
|
|
||||||
|
// newWrapBinTrie creates a binary trie with the optional prefetcher enabled.
|
||||||
|
func newWrapBinTrie(root common.Hash, db *triedb.Database, prefetch bool, prefetchRead bool) (*wrapBinTrie, error) {
|
||||||
|
t, err := bintrie.NewBinaryTrie(root, db)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var p *prefetcher
|
||||||
|
if prefetch {
|
||||||
|
p = newPrefetcher(t, prefetchRead)
|
||||||
|
}
|
||||||
|
return &wrapBinTrie{BinaryTrie: t, prefetcher: p}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// term synchronously terminates the prefetcher (no-op if nil or already done).
|
||||||
|
// After termination the prefetcher reference is nilled so subsequent calls are
|
||||||
|
// a cheap pointer check.
|
||||||
|
func (tr *wrapBinTrie) term() {
|
||||||
|
if tr.prefetcher == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tr.prefetcher.terminate()
|
||||||
|
tr.prefetcher = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// The methods below shadow the embedded bintrie.BinaryTrie so that any direct trie
|
||||||
|
// access auto-terminates the prefetcher first. This makes data-race freedom
|
||||||
|
// structural: callers never need to remember to call term() manually.
|
||||||
|
|
||||||
|
func (tr *wrapBinTrie) UpdateAccount(address common.Address, acc *types.StateAccount, codeLen int) error {
|
||||||
|
tr.term()
|
||||||
|
return tr.BinaryTrie.UpdateAccount(address, acc, codeLen)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tr *wrapBinTrie) DeleteAccount(address common.Address) error {
|
||||||
|
tr.term()
|
||||||
|
return tr.BinaryTrie.DeleteAccount(address)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tr *wrapBinTrie) UpdateStorage(address common.Address, key, value []byte) error {
|
||||||
|
tr.term()
|
||||||
|
return tr.BinaryTrie.UpdateStorage(address, key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tr *wrapBinTrie) DeleteStorage(address common.Address, key []byte) error {
|
||||||
|
tr.term()
|
||||||
|
return tr.BinaryTrie.DeleteStorage(address, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tr *wrapBinTrie) Hash() common.Hash {
|
||||||
|
tr.term()
|
||||||
|
return tr.BinaryTrie.Hash()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tr *wrapBinTrie) Commit(collectLeaf bool) (common.Hash, *trienode.NodeSet) {
|
||||||
|
tr.term()
|
||||||
|
return tr.BinaryTrie.Commit(collectLeaf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tr *wrapBinTrie) Prove(key []byte, proofDb ethdb.KeyValueWriter) error {
|
||||||
|
tr.term()
|
||||||
|
return tr.BinaryTrie.Prove(key, proofDb)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tr *wrapBinTrie) Witness() map[string][]byte {
|
||||||
|
tr.term()
|
||||||
|
return tr.BinaryTrie.Witness()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tr *wrapBinTrie) prefetchAccounts(addresses []common.Address, read bool) {
|
||||||
|
if tr.prefetcher == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tr.prefetcher.scheduleAccounts(addresses, read)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tr *wrapBinTrie) prefetchStorage(addr common.Address, keys []common.Hash, read bool) {
|
||||||
|
if tr.prefetcher == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tr.prefetcher.scheduleSlots(addr, keys, read)
|
||||||
|
}
|
||||||
|
|
||||||
|
// copy returns a deep-copied state trie. Notably the prefetcher is deliberately
|
||||||
|
// not copied, as it only belongs to the original one.
|
||||||
|
func (tr *wrapBinTrie) copy() *wrapBinTrie {
|
||||||
|
tr.term()
|
||||||
|
return &wrapBinTrie{BinaryTrie: tr.BinaryTrie.Copy()}
|
||||||
|
}
|
||||||
|
|
||||||
|
// binaryHasher is a Hasher implementation backed by a unified single-layer
|
||||||
|
// binary trie. Accounts, storage slots, and contract code all reside in one
|
||||||
|
// trie, keyed according to the EIP-7864 address space layout.
|
||||||
|
//
|
||||||
|
// binaryHasher also implements LeafProducer: alongside every trie mutation
|
||||||
|
// it records the corresponding (stem, offset, value) write into an
|
||||||
|
// internal buffer. StateDB.commit() drains this buffer once per block
|
||||||
|
// via LeafProducer.DrainStemWrites and hands the writes to the pathdb
|
||||||
|
// flat-state layer via stateUpdate.encodeBinary, keeping the bintrie
|
||||||
|
// trie and its flat-state mirror consistent without recomputing the
|
||||||
|
// bintrie key derivation twice.
|
||||||
|
type binaryHasher struct {
|
||||||
|
db *triedb.Database
|
||||||
|
root common.Hash
|
||||||
|
|
||||||
|
prefetch bool
|
||||||
|
trie *wrapBinTrie
|
||||||
|
|
||||||
|
// leaves buffers flat-state writes produced as a side-effect of
|
||||||
|
// UpdateAccount/UpdateStorage/deleteAccount. It is cleared by
|
||||||
|
// DrainStemWrites. Direct reads and writes to this slice are only
|
||||||
|
// safe from the single goroutine that owns the hasher; the Hasher
|
||||||
|
// interface already requires single-threaded use per block.
|
||||||
|
leaves []StemWrite
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compile-time assertion that binaryHasher implements LeafProducer.
|
||||||
|
var _ LeafProducer = (*binaryHasher)(nil)
|
||||||
|
|
||||||
|
func newBinaryHasher(root common.Hash, db *triedb.Database, prefetch bool, prefetchRead bool) (*binaryHasher, error) {
|
||||||
|
tr, err := newWrapBinTrie(root, db, prefetch, prefetchRead)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &binaryHasher{
|
||||||
|
db: db,
|
||||||
|
root: root,
|
||||||
|
prefetch: prefetch,
|
||||||
|
trie: tr,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DrainStemWrites implements LeafProducer. It returns the buffered stem
|
||||||
|
// writes accumulated since the last drain and resets the buffer. The
|
||||||
|
// returned slice is owned by the caller; the hasher allocates a fresh
|
||||||
|
// backing array on the next update.
|
||||||
|
func (h *binaryHasher) DrainStemWrites() []StemWrite {
|
||||||
|
out := h.leaves
|
||||||
|
h.leaves = nil
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// recordLeaf appends a single stem write to the internal buffer. The
|
||||||
|
// stem is taken from the first 31 bytes of the supplied 32-byte tree
|
||||||
|
// key, and the offset is the last byte. Value may be nil (for clearing
|
||||||
|
// a slot in the flat state, matching account deletion) or a 32-byte
|
||||||
|
// slice (for writes).
|
||||||
|
func (h *binaryHasher) recordLeaf(fullKey []byte, value []byte) {
|
||||||
|
var w StemWrite
|
||||||
|
copy(w.Stem[:], fullKey[:bintrie.StemSize])
|
||||||
|
w.Offset = fullKey[bintrie.StemSize]
|
||||||
|
if value != nil {
|
||||||
|
w.Value = make([]byte, len(value))
|
||||||
|
copy(w.Value, value)
|
||||||
|
}
|
||||||
|
h.leaves = append(h.leaves, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// deleteAccount removes the account specified by the address from the state.
|
||||||
|
//
|
||||||
|
// In addition to the trie mutation, this records two "clear" stem writes
|
||||||
|
// (one for BasicData at offset 0 and one for CodeHash at offset 1) so
|
||||||
|
// the flat-state mirror can drop the matching entries.
|
||||||
|
//
|
||||||
|
// Note: BinaryTrie.DeleteAccount is currently a no-op upstream
|
||||||
|
// (tracked as a standalone bugfix PR against ethereum/go-ethereum).
|
||||||
|
// Until that fix lands the on-trie deletion does nothing, but the
|
||||||
|
// flat-state mirror will still drop its copy — a minor temporary
|
||||||
|
// inconsistency scoped to the account-delete path. Once the trie fix
|
||||||
|
// lands the two sides converge.
|
||||||
|
//
|
||||||
|
// Storage slots and code chunks at the same or other stems are NOT
|
||||||
|
// touched by this function; callers that need a full account wipe must
|
||||||
|
// walk storage explicitly. Pre-EIP-6780 self-destruct wipe is a
|
||||||
|
// documented scope limitation.
|
||||||
|
func (h *binaryHasher) deleteAccount(addr common.Address) error {
|
||||||
|
// Record the flat-state mutations BEFORE the trie call so the
|
||||||
|
// buffer still reflects the intended write even if the trie layer
|
||||||
|
// errors and we need to roll things back.
|
||||||
|
basicDataKey := bintrie.GetBinaryTreeKeyBasicData(addr)
|
||||||
|
codeHashKey := bintrie.GetBinaryTreeKeyCodeHash(addr)
|
||||||
|
h.recordLeaf(basicDataKey, nil) // nil → clear the flat-state offset
|
||||||
|
h.recordLeaf(codeHashKey, nil)
|
||||||
|
|
||||||
|
return h.trie.DeleteAccount(addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// update writes the account specified by the address into the state.
|
||||||
|
//
|
||||||
|
// The account's code size is taken from AccountMut.CodeSize, which the
|
||||||
|
// caller (StateDB.IntermediateRoot) populates via stateObject.CodeSize().
|
||||||
|
// Per EIP-7864 the code_size field is packed into the BasicData leaf
|
||||||
|
// (bytes 5-7) and is consensus-critical; BinaryTrie.UpdateAccount rewrites
|
||||||
|
// the entire BasicData blob on every call, so passing the wrong codeLen
|
||||||
|
// would silently overwrite the stored code_size. In particular, for
|
||||||
|
// balance/nonce-only updates the new code bytes (account.Code) are nil
|
||||||
|
// and len(obj.code) is 0, yet the account may still have a non-zero code
|
||||||
|
// size that must be preserved — the caller gets this right by consulting
|
||||||
|
// the stateObject, which falls back to a reader code-size lookup when
|
||||||
|
// the bytes are not loaded.
|
||||||
|
func (h *binaryHasher) updateAccount(addr common.Address, account AccountMut) error {
|
||||||
|
data := &types.StateAccount{
|
||||||
|
Nonce: account.Account.Nonce,
|
||||||
|
Balance: account.Account.Balance,
|
||||||
|
CodeHash: account.Account.CodeHash,
|
||||||
|
}
|
||||||
|
if err := h.trie.UpdateAccount(addr, data, account.CodeSize); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Record the two flat-state writes that correspond to the on-trie
|
||||||
|
// BasicData (offset 0) and CodeHash (offset 1) at the account's
|
||||||
|
// stem. PackBasicData produces the same 32-byte blob that the trie
|
||||||
|
// layer packs internally, so the flat-state mirror encodes
|
||||||
|
// bit-identically.
|
||||||
|
basicData := bintrie.PackBasicData(data.Nonce, data.Balance, account.CodeSize)
|
||||||
|
h.recordLeaf(bintrie.GetBinaryTreeKeyBasicData(addr), basicData[:])
|
||||||
|
|
||||||
|
// CodeHash is a 32-byte value written straight into offset 1.
|
||||||
|
// EOAs store types.EmptyCodeHash here (a known non-zero hash) so
|
||||||
|
// the flat-state offset is always set after any non-delete update.
|
||||||
|
h.recordLeaf(bintrie.GetBinaryTreeKeyCodeHash(addr), data.CodeHash)
|
||||||
|
|
||||||
|
// Write chunked code into the trie when dirty.
|
||||||
|
if account.Code != nil && len(account.Code.Code) > 0 {
|
||||||
|
codeHash := common.BytesToHash(account.Account.CodeHash)
|
||||||
|
if err := h.trie.UpdateContractCode(addr, codeHash, account.Code.Code); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateAccount implements Hasher, writing a list of account mutations
|
||||||
|
// into the state. The assumption is held all the storage changes have
|
||||||
|
// already been written beforehand.
|
||||||
|
func (h *binaryHasher) UpdateAccount(addresses []common.Address, accounts []AccountMut) error {
|
||||||
|
var err error
|
||||||
|
for i, addr := range addresses {
|
||||||
|
if accounts[i].Account == nil {
|
||||||
|
err = h.deleteAccount(addr)
|
||||||
|
} else {
|
||||||
|
err = h.updateAccount(addr, accounts[i])
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateStorage implements Hasher, writing a list of storage slot mutations
|
||||||
|
// into the state. This function must be invoked first before writing the
|
||||||
|
// associated account metadata into the state.
|
||||||
|
//
|
||||||
|
// Each mutation is also recorded as a flat-state stem write. A zero value
|
||||||
|
// is the bintrie's "delete" convention: the trie writes 32 zero bytes at
|
||||||
|
// the slot, and the flat-state mirror does the same (a present-with-zero
|
||||||
|
// tombstone) rather than removing the offset from its bitmap. This keeps
|
||||||
|
// the trie and flat-state views bit-identical for the slot.
|
||||||
|
func (h *binaryHasher) UpdateStorage(address common.Address, keys []common.Hash, values []common.Hash) error {
|
||||||
|
var err error
|
||||||
|
for i, key := range keys {
|
||||||
|
// BinaryTrie.UpdateStorage right-justifies a shorter input into
|
||||||
|
// 32 bytes; for a non-zero common.Hash the input is already 32
|
||||||
|
// bytes so the normalization is a no-op. For the zero-value
|
||||||
|
// case we emit 32 zero bytes explicitly to match the trie's
|
||||||
|
// tombstone convention.
|
||||||
|
var blob [bintrie.HashSize]byte
|
||||||
|
if values[i] == (common.Hash{}) {
|
||||||
|
err = h.trie.DeleteStorage(address, key[:])
|
||||||
|
} else {
|
||||||
|
copy(blob[:], values[i][:])
|
||||||
|
err = h.trie.UpdateStorage(address, key[:], blob[:])
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Record the flat-state mirror write regardless of zero/non-zero:
|
||||||
|
// the blob is 32 zero bytes in the delete case and the value in
|
||||||
|
// the non-delete case.
|
||||||
|
storageKey := bintrie.GetBinaryTreeKeyStorageSlot(address, key[:])
|
||||||
|
h.recordLeaf(storageKey, blob[:])
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash implements Hasher, computing the state root hash without committing.
|
||||||
|
func (h *binaryHasher) Hash() common.Hash {
|
||||||
|
return h.trie.Hash()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit implements Hasher, finalizing all pending changes and returning
|
||||||
|
// the resulting state root hash, along with the set of dirty trie nodes
|
||||||
|
// generated by the updates.
|
||||||
|
func (h *binaryHasher) Commit() (common.Hash, *trienode.MergedNodeSet, map[common.Address]Hashes, error) {
|
||||||
|
nodes := trienode.NewMergedNodeSet()
|
||||||
|
root, set := h.trie.Commit(false)
|
||||||
|
if set != nil {
|
||||||
|
if err := nodes.Merge(set); err != nil {
|
||||||
|
return common.Hash{}, nil, nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The binary trie is a single unified structure with no per-account
|
||||||
|
// storage sub-tries, so there are no secondary hashes to report.
|
||||||
|
return root, nodes, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy implements Hasher, returning a deep-copied hasher instance.
|
||||||
|
func (h *binaryHasher) Copy() Hasher {
|
||||||
|
return &binaryHasher{
|
||||||
|
db: h.db,
|
||||||
|
root: h.root,
|
||||||
|
prefetch: false,
|
||||||
|
trie: h.trie.copy(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProveAccount implements Prover. NOTE: BinaryTrie.Prove is not yet
|
||||||
|
// implemented (panics at runtime). The key derivation also needs to use
|
||||||
|
// bintrie tree keys instead of keccak256. Do not call until the bintrie
|
||||||
|
// proof path is implemented.
|
||||||
|
func (h *binaryHasher) ProveAccount(addr common.Address, proofDb ethdb.KeyValueWriter) error {
|
||||||
|
return h.trie.Prove(crypto.Keccak256(addr.Bytes()), proofDb)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProveStorage implements Prover. NOTE: same limitation as ProveAccount —
|
||||||
|
// BinaryTrie.Prove panics and the key derivation is wrong.
|
||||||
|
func (h *binaryHasher) ProveStorage(addr common.Address, key common.Hash, proofDb ethdb.KeyValueWriter) error {
|
||||||
|
return h.trie.Prove(crypto.Keccak256(key.Bytes()), proofDb)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CollectWitness implements WitnessCollector. It aggregates all trie nodes
|
||||||
|
// accessed during the state transition from the unified binary trie into
|
||||||
|
// a single state witness.
|
||||||
|
func (h *binaryHasher) CollectWitness(witness *stateless.Witness) {
|
||||||
|
witness.AddState(h.trie.Witness(), common.Hash{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrefetchAccount implements Prefetcher, preloading the nodes of specific accounts.
|
||||||
|
func (h *binaryHasher) PrefetchAccount(addresses []common.Address, read bool) {
|
||||||
|
if !h.prefetch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.trie.prefetchAccounts(addresses, read)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrefetchStorage implements Prefetcher, scheduling storage slot nodes for
|
||||||
|
// background loading in the unified binary trie.
|
||||||
|
func (h *binaryHasher) PrefetchStorage(addr common.Address, keys []common.Hash, read bool) {
|
||||||
|
if !h.prefetch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.trie.prefetchStorage(addr, keys, read)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TermPrefetch terminates all prefetcher goroutines. Safe to call multiple times.
|
||||||
|
func (h *binaryHasher) TermPrefetch() {
|
||||||
|
if h == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.trie.term()
|
||||||
|
}
|
||||||
548
core/state/database_hasher_binary_test.go
Normal file
548
core/state/database_hasher_binary_test.go
Normal file
|
|
@ -0,0 +1,548 @@
|
||||||
|
// Copyright 2026 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 state
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||||
|
"github.com/ethereum/go-ethereum/core/stateless"
|
||||||
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
|
"github.com/ethereum/go-ethereum/trie/bintrie"
|
||||||
|
"github.com/ethereum/go-ethereum/triedb"
|
||||||
|
)
|
||||||
|
|
||||||
|
// newTestBinaryHasher creates a binaryHasher backed by an in-memory path database.
|
||||||
|
func newTestBinaryHasher(t *testing.T, db *triedb.Database, root common.Hash, cfg hasherTestConfig) *binaryHasher {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
h, err := newBinaryHasher(root, db, cfg.prefetch, cfg.prefetchRead)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { h.TermPrefetch() })
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
// commitAndReopenBinary commits the hasher's state and reopens a fresh hasher
|
||||||
|
// from the committed root. This simulates a block boundary.
|
||||||
|
func commitAndReopenBinary(t *testing.T, h *binaryHasher, cfg hasherTestConfig) *binaryHasher {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
root, nodes, _, err := h.Commit()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if nodes != nil {
|
||||||
|
if err := h.db.Update(root, h.root, 0, nodes, triedb.NewStateSet()); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := h.db.Commit(root, false); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h2, err := newBinaryHasher(root, h.db, cfg.prefetch, cfg.prefetchRead)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { h2.TermPrefetch() })
|
||||||
|
return h2
|
||||||
|
}
|
||||||
|
|
||||||
|
// makeBinaryBaseState creates a non-empty state as the starting point for tests.
|
||||||
|
// The base contains:
|
||||||
|
// - addr1: nonce=1, balance=100, storage={slot1: val1, slot2: val2}
|
||||||
|
// - addr2: nonce=2, balance=200, no storage
|
||||||
|
//
|
||||||
|
// The state is committed and flushed so the hasher returned opens from disk.
|
||||||
|
func makeBinaryBaseState(t *testing.T, cfg hasherTestConfig) *binaryHasher {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
noPrefetch := hasherTestConfig{"base", false, false}
|
||||||
|
db := triedb.NewDatabase(rawdb.NewMemoryDatabase(), triedb.VerkleDefaults)
|
||||||
|
h := newTestBinaryHasher(t, db, types.EmptyBinaryHash, noPrefetch)
|
||||||
|
|
||||||
|
if err := h.UpdateStorage(hasherAddr1, []common.Hash{hasherSlot1, hasherSlot2}, []common.Hash{hasherVal1, hasherVal2}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := h.UpdateAccount(
|
||||||
|
[]common.Address{hasherAddr1, hasherAddr2},
|
||||||
|
[]AccountMut{hasherAccount(1, 100), hasherAccount(2, 200)},
|
||||||
|
); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return commitAndReopenBinary(t, h, cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBinaryHasherBasic verifies that mutating storage and accounts on top of
|
||||||
|
// a non-empty base state produces a deterministic, non-empty root and that the
|
||||||
|
// root survives a commit+reopen cycle.
|
||||||
|
func TestBinaryHasherBasic(t *testing.T) {
|
||||||
|
for _, cfg := range hasherTestConfigs {
|
||||||
|
t.Run(cfg.name, func(t *testing.T) {
|
||||||
|
h := makeBinaryBaseState(t, cfg)
|
||||||
|
|
||||||
|
if cfg.prefetch {
|
||||||
|
h.PrefetchStorage(hasherAddr1, []common.Hash{hasherSlot3}, false)
|
||||||
|
h.PrefetchAccount([]common.Address{hasherAddr1, hasherAddr3}, false)
|
||||||
|
}
|
||||||
|
if err := h.UpdateStorage(hasherAddr1, []common.Hash{hasherSlot3}, []common.Hash{hasherVal3}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := h.UpdateAccount(
|
||||||
|
[]common.Address{hasherAddr1, hasherAddr3},
|
||||||
|
[]AccountMut{hasherAccount(1, 100), hasherAccount(3, 300)},
|
||||||
|
); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
root := h.Hash()
|
||||||
|
if root == types.EmptyRootHash {
|
||||||
|
t.Fatal("expected non-empty root after mutations")
|
||||||
|
}
|
||||||
|
h2 := commitAndReopenBinary(t, h, cfg)
|
||||||
|
if h2.Hash() != root {
|
||||||
|
t.Fatalf("root mismatch after reopen: got %x, want %x", h2.Hash(), root)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBinaryHasherPrefetchReadOnly verifies that read-only prefetching (for
|
||||||
|
// accounts and storage that are never subsequently mutated) does not corrupt
|
||||||
|
// state. Both prefetchRead=true (requests are processed) and prefetchRead=false
|
||||||
|
// (requests are dropped) are tested.
|
||||||
|
func TestBinaryHasherPrefetchReadOnly(t *testing.T) {
|
||||||
|
for _, prefetchRead := range []bool{false, true} {
|
||||||
|
name := "readDropped"
|
||||||
|
if prefetchRead {
|
||||||
|
name = "readProcessed"
|
||||||
|
}
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
cfg := hasherTestConfig{name, true, prefetchRead}
|
||||||
|
h := makeBinaryBaseState(t, cfg)
|
||||||
|
rootBefore := h.Hash()
|
||||||
|
|
||||||
|
// Prefetch addr1's account and storage (read-only).
|
||||||
|
h.PrefetchAccount([]common.Address{hasherAddr1, hasherAddr2}, true)
|
||||||
|
h.PrefetchStorage(hasherAddr1, []common.Hash{hasherSlot1, hasherSlot2}, true)
|
||||||
|
|
||||||
|
// Only mutate addr2 — addr1's prefetched data is never written.
|
||||||
|
if err := h.UpdateAccount(
|
||||||
|
[]common.Address{hasherAddr2},
|
||||||
|
[]AccountMut{hasherAccount(2, 300)},
|
||||||
|
); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
root := h.Hash()
|
||||||
|
if root == rootBefore {
|
||||||
|
t.Fatal("expected root to change after balance update")
|
||||||
|
}
|
||||||
|
h2 := commitAndReopenBinary(t, h, hasherTestConfig{"verify", false, false})
|
||||||
|
if h2.Hash() != root {
|
||||||
|
t.Fatalf("root mismatch: got %x, want %x", h2.Hash(), root)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBinaryHasherPrefetchDeterminism verifies that the resulting root is
|
||||||
|
// identical across all prefetch configurations for the same set of mutations.
|
||||||
|
func TestBinaryHasherPrefetchDeterminism(t *testing.T) {
|
||||||
|
var roots []common.Hash
|
||||||
|
for _, cfg := range hasherTestConfigs {
|
||||||
|
h := makeBinaryBaseState(t, cfg)
|
||||||
|
|
||||||
|
if cfg.prefetch {
|
||||||
|
h.PrefetchAccount([]common.Address{hasherAddr1, hasherAddr3}, false)
|
||||||
|
h.PrefetchStorage(hasherAddr1, []common.Hash{hasherSlot3}, false)
|
||||||
|
h.PrefetchStorage(hasherAddr3, []common.Hash{hasherSlot1}, false)
|
||||||
|
}
|
||||||
|
if err := h.UpdateStorage(hasherAddr1, []common.Hash{hasherSlot3}, []common.Hash{hasherVal3}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := h.UpdateStorage(hasherAddr3, []common.Hash{hasherSlot1}, []common.Hash{hasherVal1}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := h.UpdateAccount(
|
||||||
|
[]common.Address{hasherAddr1, hasherAddr3},
|
||||||
|
[]AccountMut{hasherAccount(1, 100), hasherAccount(3, 300)},
|
||||||
|
); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
roots = append(roots, h.Hash())
|
||||||
|
}
|
||||||
|
for i := 1; i < len(roots); i++ {
|
||||||
|
if roots[i] != roots[0] {
|
||||||
|
t.Fatalf("root diverged: config[0]=%x config[%d]=%x", roots[0], i, roots[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBinaryHasherCopy verifies that Copy produces an independent snapshot:
|
||||||
|
// mutations on the copy must not affect the original's hash.
|
||||||
|
func TestBinaryHasherCopy(t *testing.T) {
|
||||||
|
cfg := hasherTestConfig{"prefetchAll", true, true}
|
||||||
|
h := makeBinaryBaseState(t, cfg)
|
||||||
|
|
||||||
|
h.PrefetchAccount([]common.Address{hasherAddr1}, false)
|
||||||
|
h.PrefetchStorage(hasherAddr1, []common.Hash{hasherSlot3}, false)
|
||||||
|
if err := h.UpdateStorage(hasherAddr1, []common.Hash{hasherSlot3}, []common.Hash{hasherVal3}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := h.UpdateAccount([]common.Address{hasherAddr1}, []AccountMut{hasherAccount(1, 100)}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
origRoot := h.Hash()
|
||||||
|
|
||||||
|
cpy := h.Copy()
|
||||||
|
defer cpy.(*binaryHasher).TermPrefetch()
|
||||||
|
|
||||||
|
// Mutate the copy: delete slot3, add slot2 with new value.
|
||||||
|
if err := cpy.UpdateStorage(hasherAddr1, []common.Hash{hasherSlot3, hasherSlot2}, []common.Hash{{}, hasherVal3}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := cpy.UpdateAccount([]common.Address{hasherAddr1}, []AccountMut{hasherAccount(1, 100)}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if cpy.Hash() == origRoot {
|
||||||
|
t.Fatal("copy should diverge after mutation")
|
||||||
|
}
|
||||||
|
if h.Hash() != origRoot {
|
||||||
|
t.Fatal("original root changed after mutating copy")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBinaryHasherWitness verifies that the witness returned by CollectWitness
|
||||||
|
// contains trie nodes for accessed accounts and storage. When read-only
|
||||||
|
// prefetching is enabled, the prefetched (but never written) data must also
|
||||||
|
// appear in the witness.
|
||||||
|
func TestBinaryHasherWitness(t *testing.T) {
|
||||||
|
// Collect witness WITHOUT read-prefetching: only mutated paths are tracked.
|
||||||
|
collectWitness := func(prefetchRead bool) int {
|
||||||
|
cfg := hasherTestConfig{"witness", true, prefetchRead}
|
||||||
|
h := makeBinaryBaseState(t, cfg)
|
||||||
|
|
||||||
|
// Read-only prefetch of addr1 account and slot1 (never mutated below).
|
||||||
|
h.PrefetchAccount([]common.Address{hasherAddr1}, true)
|
||||||
|
h.PrefetchStorage(hasherAddr1, []common.Hash{hasherSlot1}, true)
|
||||||
|
|
||||||
|
// Mutate only addr2 (no storage).
|
||||||
|
if err := h.UpdateAccount(
|
||||||
|
[]common.Address{hasherAddr2},
|
||||||
|
[]AccountMut{hasherAccount(2, 300)},
|
||||||
|
); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
h.Hash()
|
||||||
|
|
||||||
|
witness := &stateless.Witness{
|
||||||
|
Codes: make(map[string]struct{}),
|
||||||
|
State: make(map[string]struct{}),
|
||||||
|
}
|
||||||
|
h.CollectWitness(witness)
|
||||||
|
return len(witness.State)
|
||||||
|
}
|
||||||
|
nodesWithoutRead := collectWitness(false)
|
||||||
|
nodesWithRead := collectWitness(true)
|
||||||
|
|
||||||
|
if nodesWithoutRead == 0 {
|
||||||
|
t.Fatal("witness should contain trie nodes even without read prefetching")
|
||||||
|
}
|
||||||
|
if nodesWithRead <= nodesWithoutRead {
|
||||||
|
t.Fatalf("read-only prefetching should add extra nodes to witness: got %d (with read) vs %d (without)", nodesWithRead, nodesWithoutRead)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBinaryHasherLeafProduction verifies that binaryHasher implements
|
||||||
|
// LeafProducer and reports stem writes corresponding to each trie
|
||||||
|
// mutation. Covers the three mutation kinds the hasher performs:
|
||||||
|
// account update, storage update, and account delete.
|
||||||
|
func TestBinaryHasherLeafProduction(t *testing.T) {
|
||||||
|
db := triedb.NewDatabase(rawdb.NewMemoryDatabase(), triedb.VerkleDefaults)
|
||||||
|
h := newTestBinaryHasher(t, db, types.EmptyBinaryHash, hasherTestConfig{"leaf", false, false})
|
||||||
|
|
||||||
|
// Type assertion: binaryHasher must satisfy LeafProducer.
|
||||||
|
lp, ok := Hasher(h).(LeafProducer)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("binaryHasher should implement LeafProducer")
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Account update: expect two writes (BasicData + CodeHash) ---
|
||||||
|
if err := h.UpdateAccount(
|
||||||
|
[]common.Address{hasherAddr1},
|
||||||
|
[]AccountMut{hasherAccount(1, 100)},
|
||||||
|
); err != nil {
|
||||||
|
t.Fatalf("UpdateAccount: %v", err)
|
||||||
|
}
|
||||||
|
writes := lp.DrainStemWrites()
|
||||||
|
if len(writes) != 2 {
|
||||||
|
t.Fatalf("UpdateAccount: got %d stem writes, want 2 (BasicData + CodeHash)", len(writes))
|
||||||
|
}
|
||||||
|
// Offsets 0 and 1 respectively, and the BasicData stem matches the
|
||||||
|
// CodeHash stem (same address → same 31-byte stem).
|
||||||
|
if writes[0].Offset != bintrie.BasicDataLeafKey {
|
||||||
|
t.Errorf("write[0].Offset = %d, want %d (BasicDataLeafKey)", writes[0].Offset, bintrie.BasicDataLeafKey)
|
||||||
|
}
|
||||||
|
if writes[1].Offset != bintrie.CodeHashLeafKey {
|
||||||
|
t.Errorf("write[1].Offset = %d, want %d (CodeHashLeafKey)", writes[1].Offset, bintrie.CodeHashLeafKey)
|
||||||
|
}
|
||||||
|
if writes[0].Stem != writes[1].Stem {
|
||||||
|
t.Errorf("stems differ: %x vs %x", writes[0].Stem, writes[1].Stem)
|
||||||
|
}
|
||||||
|
if len(writes[0].Value) != 32 {
|
||||||
|
t.Errorf("write[0].Value length = %d, want 32", len(writes[0].Value))
|
||||||
|
}
|
||||||
|
if len(writes[1].Value) != 32 {
|
||||||
|
t.Errorf("write[1].Value length = %d, want 32", len(writes[1].Value))
|
||||||
|
}
|
||||||
|
// The code hash leaf should be the empty-code hash (non-zero).
|
||||||
|
if !bytes.Equal(writes[1].Value, types.EmptyCodeHash.Bytes()) {
|
||||||
|
t.Errorf("write[1].Value = %x, want empty code hash %x", writes[1].Value, types.EmptyCodeHash.Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Drain again: should be empty (drain is destructive) ---
|
||||||
|
if again := lp.DrainStemWrites(); len(again) != 0 {
|
||||||
|
t.Fatalf("second drain should be empty, got %d writes", len(again))
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Storage update: non-zero value produces one write ---
|
||||||
|
if err := h.UpdateStorage(hasherAddr1, []common.Hash{hasherSlot1}, []common.Hash{hasherVal1}); err != nil {
|
||||||
|
t.Fatalf("UpdateStorage: %v", err)
|
||||||
|
}
|
||||||
|
writes = lp.DrainStemWrites()
|
||||||
|
if len(writes) != 1 {
|
||||||
|
t.Fatalf("UpdateStorage: got %d writes, want 1", len(writes))
|
||||||
|
}
|
||||||
|
// The recorded value should match hasherVal1 (a common.Hash), which
|
||||||
|
// is already 32 bytes wide.
|
||||||
|
if !bytes.Equal(writes[0].Value, hasherVal1[:]) {
|
||||||
|
t.Errorf("UpdateStorage value: got %x, want %x", writes[0].Value, hasherVal1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Storage "delete" (zero value): one write with 32 zero bytes ---
|
||||||
|
if err := h.UpdateStorage(hasherAddr1, []common.Hash{hasherSlot1}, []common.Hash{{}}); err != nil {
|
||||||
|
t.Fatalf("UpdateStorage (zero): %v", err)
|
||||||
|
}
|
||||||
|
writes = lp.DrainStemWrites()
|
||||||
|
if len(writes) != 1 {
|
||||||
|
t.Fatalf("UpdateStorage (zero): got %d writes, want 1", len(writes))
|
||||||
|
}
|
||||||
|
var zeros [32]byte
|
||||||
|
if !bytes.Equal(writes[0].Value, zeros[:]) {
|
||||||
|
t.Errorf("zero-value storage write should record 32 zero bytes, got %x", writes[0].Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Account delete: two writes with nil values ---
|
||||||
|
if err := h.UpdateAccount(
|
||||||
|
[]common.Address{hasherAddr1},
|
||||||
|
[]AccountMut{{Account: nil}},
|
||||||
|
); err != nil {
|
||||||
|
t.Fatalf("UpdateAccount delete: %v", err)
|
||||||
|
}
|
||||||
|
writes = lp.DrainStemWrites()
|
||||||
|
if len(writes) != 2 {
|
||||||
|
t.Fatalf("delete: got %d writes, want 2 (BasicData + CodeHash clear)", len(writes))
|
||||||
|
}
|
||||||
|
for i, w := range writes {
|
||||||
|
if w.Value != nil {
|
||||||
|
t.Errorf("delete write[%d] should have nil Value (clear), got %x", i, w.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if writes[0].Offset != bintrie.BasicDataLeafKey || writes[1].Offset != bintrie.CodeHashLeafKey {
|
||||||
|
t.Errorf("delete offsets: got %d,%d, want %d,%d", writes[0].Offset, writes[1].Offset, bintrie.BasicDataLeafKey, bintrie.CodeHashLeafKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestMerkleHasherNoLeafProducer verifies that merkleHasher does NOT
|
||||||
|
// implement LeafProducer — the interface is strictly opt-in and the MPT
|
||||||
|
// path has no concept of stem writes.
|
||||||
|
func TestMerkleHasherNoLeafProducer(t *testing.T) {
|
||||||
|
db := triedb.NewDatabase(rawdb.NewMemoryDatabase(), nil)
|
||||||
|
h, err := newMerkleHasher(types.EmptyRootHash, db, false, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, ok := Hasher(h).(LeafProducer); ok {
|
||||||
|
t.Fatal("merkleHasher should NOT implement LeafProducer")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBinaryHasherWritesBothBasicAndCodeHash is a load-bearing invariant
|
||||||
|
// test for the A1 remediation. The bintrieFlatReader.Account method
|
||||||
|
// performs TWO independent AccountRLP reads (BasicData at offset 0 and
|
||||||
|
// CodeHash at offset 1). Cross-read consistency is only safe if the
|
||||||
|
// hasher ALWAYS co-writes both leaves whenever it touches an account —
|
||||||
|
// if a future optimization (e.g., a code-only update) emitted only the
|
||||||
|
// CodeHash leaf, the two reads could resolve to different layers and
|
||||||
|
// return a torn view.
|
||||||
|
//
|
||||||
|
// This test locks the invariant down: after an UpdateAccount call, the
|
||||||
|
// drained stem writes must contain EXACTLY ONE BasicData write and
|
||||||
|
// EXACTLY ONE CodeHash write for the touched address, both at the same
|
||||||
|
// stem. Any change to binaryHasher.updateAccount that drops either
|
||||||
|
// write will fail this test and the developer will be forced to
|
||||||
|
// re-evaluate the bintrieFlatReader.Account torn-read argument before
|
||||||
|
// shipping.
|
||||||
|
func TestBinaryHasherWritesBothBasicAndCodeHash(t *testing.T) {
|
||||||
|
db := triedb.NewDatabase(rawdb.NewMemoryDatabase(), triedb.VerkleDefaults)
|
||||||
|
h := newTestBinaryHasher(t, db, types.EmptyBinaryHash, hasherTestConfig{"inv", false, false})
|
||||||
|
|
||||||
|
lp, ok := Hasher(h).(LeafProducer)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("binaryHasher should implement LeafProducer")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update a single account. The hasher MUST emit exactly two stem
|
||||||
|
// writes: BasicData (offset 0) and CodeHash (offset 1), at the
|
||||||
|
// same stem.
|
||||||
|
if err := h.UpdateAccount(
|
||||||
|
[]common.Address{hasherAddr1},
|
||||||
|
[]AccountMut{hasherAccount(1, 100)},
|
||||||
|
); err != nil {
|
||||||
|
t.Fatalf("UpdateAccount: %v", err)
|
||||||
|
}
|
||||||
|
writes := lp.DrainStemWrites()
|
||||||
|
if len(writes) != 2 {
|
||||||
|
t.Fatalf("expected exactly 2 stem writes per UpdateAccount (BasicData + CodeHash), got %d", len(writes))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify one is BasicData and one is CodeHash.
|
||||||
|
seenBasic := false
|
||||||
|
seenCode := false
|
||||||
|
for _, w := range writes {
|
||||||
|
switch w.Offset {
|
||||||
|
case bintrie.BasicDataLeafKey:
|
||||||
|
seenBasic = true
|
||||||
|
case bintrie.CodeHashLeafKey:
|
||||||
|
seenCode = true
|
||||||
|
default:
|
||||||
|
t.Errorf("unexpected stem write offset %d (want %d or %d)", w.Offset, bintrie.BasicDataLeafKey, bintrie.CodeHashLeafKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !seenBasic {
|
||||||
|
t.Error("UpdateAccount did NOT emit a BasicData leaf write — bintrieFlatReader.Account torn-read invariant broken")
|
||||||
|
}
|
||||||
|
if !seenCode {
|
||||||
|
t.Error("UpdateAccount did NOT emit a CodeHash leaf write — bintrieFlatReader.Account torn-read invariant broken")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify both writes target the same stem.
|
||||||
|
if writes[0].Stem != writes[1].Stem {
|
||||||
|
t.Errorf("BasicData and CodeHash writes at different stems: %x vs %x", writes[0].Stem, writes[1].Stem)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exercise the delete path too: binaryHasher.deleteAccount should
|
||||||
|
// also emit both nil writes.
|
||||||
|
if err := h.UpdateAccount(
|
||||||
|
[]common.Address{hasherAddr1},
|
||||||
|
[]AccountMut{{Account: nil}},
|
||||||
|
); err != nil {
|
||||||
|
t.Fatalf("UpdateAccount (delete): %v", err)
|
||||||
|
}
|
||||||
|
deleteWrites := lp.DrainStemWrites()
|
||||||
|
if len(deleteWrites) != 2 {
|
||||||
|
t.Fatalf("expected 2 stem writes per account delete, got %d", len(deleteWrites))
|
||||||
|
}
|
||||||
|
for i, w := range deleteWrites {
|
||||||
|
if w.Value != nil {
|
||||||
|
t.Errorf("delete write[%d] should have nil Value, got %x", i, w.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestStateUpdateEncodeBinaryFromLeaves verifies that stateUpdate.encodeBinary
|
||||||
|
// turns a slice of StemWrite values into the per-offset accountData map that
|
||||||
|
// pathdb's bintrie codec consumes. Three things matter:
|
||||||
|
//
|
||||||
|
// 1. Every leaf becomes one accountData entry, keyed by stem||offset.
|
||||||
|
// 2. nil-value leaves (account/storage deletes) become nil entries.
|
||||||
|
// 3. Non-nil leaves are deeply copied — encodeBinary must not retain
|
||||||
|
// pointers into the hasher's internal slab.
|
||||||
|
//
|
||||||
|
// storages/storageOrigin/accountOrigin remain empty: the bintrie path uses
|
||||||
|
// only accountData (per the layered-read design) and does not yet support
|
||||||
|
// state-history rollback.
|
||||||
|
func TestStateUpdateEncodeBinaryFromLeaves(t *testing.T) {
|
||||||
|
// Build a small leaves slice covering each kind of write the binary
|
||||||
|
// hasher emits: account update (BasicData + CodeHash), storage write,
|
||||||
|
// and a delete (nil value).
|
||||||
|
var (
|
||||||
|
stemA [bintrie.StemSize]byte
|
||||||
|
stemB [bintrie.StemSize]byte
|
||||||
|
)
|
||||||
|
for i := range stemA {
|
||||||
|
stemA[i] = byte(0x10 + i)
|
||||||
|
stemB[i] = byte(0xA0 + i)
|
||||||
|
}
|
||||||
|
basicDataValue := bytes.Repeat([]byte{0xAA}, 32)
|
||||||
|
codeHashValue := bytes.Repeat([]byte{0xBB}, 32)
|
||||||
|
storageValue := bytes.Repeat([]byte{0xCC}, 32)
|
||||||
|
|
||||||
|
leaves := []StemWrite{
|
||||||
|
// Account update at stemA: BasicData + CodeHash.
|
||||||
|
{Stem: stemA, Offset: bintrie.BasicDataLeafKey, Value: basicDataValue},
|
||||||
|
{Stem: stemA, Offset: bintrie.CodeHashLeafKey, Value: codeHashValue},
|
||||||
|
// Storage write at stemB.
|
||||||
|
{Stem: stemB, Offset: 7, Value: storageValue},
|
||||||
|
// Account delete at a third stem (nil values clear offsets 0+1).
|
||||||
|
{Stem: [bintrie.StemSize]byte{0xFF, 0xFF}, Offset: bintrie.BasicDataLeafKey, Value: nil},
|
||||||
|
{Stem: [bintrie.StemSize]byte{0xFF, 0xFF}, Offset: bintrie.CodeHashLeafKey, Value: nil},
|
||||||
|
}
|
||||||
|
|
||||||
|
su := &stateUpdate{leaves: leaves}
|
||||||
|
accounts, accountOrigin, storages, storageOrigin, err := su.encodeBinary()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("encodeBinary: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(accounts) != len(leaves) {
|
||||||
|
t.Fatalf("accounts len = %d, want %d", len(accounts), len(leaves))
|
||||||
|
}
|
||||||
|
if len(storages) != 0 {
|
||||||
|
t.Errorf("storages should be empty for bintrie, got %d entries", len(storages))
|
||||||
|
}
|
||||||
|
if len(accountOrigin) != 0 || len(storageOrigin) != 0 {
|
||||||
|
t.Errorf("origin maps should be empty for bintrie")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check each leaf round-trips through the map under its full key.
|
||||||
|
for i, w := range leaves {
|
||||||
|
var fullKey common.Hash
|
||||||
|
copy(fullKey[:bintrie.StemSize], w.Stem[:])
|
||||||
|
fullKey[bintrie.StemSize] = w.Offset
|
||||||
|
got, ok := accounts[fullKey]
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("leaf %d: missing key %x", i, fullKey)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if w.Value == nil {
|
||||||
|
if got != nil {
|
||||||
|
t.Errorf("leaf %d: nil leaf became %x", i, got)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !bytes.Equal(got, w.Value) {
|
||||||
|
t.Errorf("leaf %d: got %x, want %x", i, got, w.Value)
|
||||||
|
}
|
||||||
|
// Aliasing check: the encoder must own its bytes.
|
||||||
|
if len(got) > 0 && &got[0] == &w.Value[0] {
|
||||||
|
t.Errorf("leaf %d: encodeBinary aliased the input slice", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
470
core/state/database_hasher_merkle.go
Normal file
470
core/state/database_hasher_merkle.go
Normal file
|
|
@ -0,0 +1,470 @@
|
||||||
|
// Copyright 2026 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 state
|
||||||
|
|
||||||
|
import (
|
||||||
|
"maps"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/core/stateless"
|
||||||
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
|
"github.com/ethereum/go-ethereum/ethdb"
|
||||||
|
"github.com/ethereum/go-ethereum/trie"
|
||||||
|
"github.com/ethereum/go-ethereum/trie/trienode"
|
||||||
|
"github.com/ethereum/go-ethereum/triedb"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
)
|
||||||
|
|
||||||
|
// wrapTrie pairs a StateTrie with an optional background prefetcher that
|
||||||
|
// preloads trie nodes ahead of mutation.
|
||||||
|
type wrapTrie struct {
|
||||||
|
*trie.StateTrie
|
||||||
|
prefetcher *prefetcher
|
||||||
|
}
|
||||||
|
|
||||||
|
// newWrapTrie creates a merkle trie with the optional prefetcher enabled.
|
||||||
|
func newWrapTrie(id *trie.ID, db *triedb.Database, prefetch bool, prefetchRead bool) (*wrapTrie, error) {
|
||||||
|
t, err := trie.NewStateTrie(id, db)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var p *prefetcher
|
||||||
|
if prefetch {
|
||||||
|
p = newPrefetcher(t, prefetchRead)
|
||||||
|
}
|
||||||
|
return &wrapTrie{StateTrie: t, prefetcher: p}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// term synchronously terminates the prefetcher (no-op if nil or already done).
|
||||||
|
// After termination the prefetcher reference is nilled so subsequent calls are
|
||||||
|
// a cheap pointer check.
|
||||||
|
func (tr *wrapTrie) term() {
|
||||||
|
if tr.prefetcher == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tr.prefetcher.terminate()
|
||||||
|
tr.prefetcher = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// The methods below shadow the embedded trie.StateTrie so that any direct trie
|
||||||
|
// access auto-terminates the prefetcher first. This makes data-race freedom
|
||||||
|
// structural: callers never need to remember to call term() manually.
|
||||||
|
|
||||||
|
func (tr *wrapTrie) UpdateAccount(address common.Address, acc *types.StateAccount) error {
|
||||||
|
tr.term()
|
||||||
|
return tr.StateTrie.UpdateAccount(address, acc, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tr *wrapTrie) DeleteAccount(address common.Address) error {
|
||||||
|
tr.term()
|
||||||
|
return tr.StateTrie.DeleteAccount(address)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tr *wrapTrie) UpdateStorage(address common.Address, key, value []byte) error {
|
||||||
|
tr.term()
|
||||||
|
return tr.StateTrie.UpdateStorage(address, key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tr *wrapTrie) DeleteStorage(address common.Address, key []byte) error {
|
||||||
|
tr.term()
|
||||||
|
return tr.StateTrie.DeleteStorage(address, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tr *wrapTrie) Hash() common.Hash {
|
||||||
|
tr.term()
|
||||||
|
return tr.StateTrie.Hash()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tr *wrapTrie) Commit(collectLeaf bool) (common.Hash, *trienode.NodeSet) {
|
||||||
|
tr.term()
|
||||||
|
return tr.StateTrie.Commit(collectLeaf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tr *wrapTrie) Prove(key []byte, proofDb ethdb.KeyValueWriter) error {
|
||||||
|
tr.term()
|
||||||
|
return tr.StateTrie.Prove(key, proofDb)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tr *wrapTrie) Witness() map[string][]byte {
|
||||||
|
tr.term()
|
||||||
|
return tr.StateTrie.Witness()
|
||||||
|
}
|
||||||
|
|
||||||
|
// prefetchAccounts prewarms the trie with the specified account list.
|
||||||
|
func (tr *wrapTrie) prefetchAccounts(addresses []common.Address, read bool) {
|
||||||
|
if tr.prefetcher == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tr.prefetcher.scheduleAccounts(addresses, read)
|
||||||
|
}
|
||||||
|
|
||||||
|
// prefetchStorage prewarms the trie with the specified storage list.
|
||||||
|
func (tr *wrapTrie) prefetchStorage(addr common.Address, keys []common.Hash, read bool) {
|
||||||
|
if tr.prefetcher == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tr.prefetcher.scheduleSlots(addr, keys, read)
|
||||||
|
}
|
||||||
|
|
||||||
|
// copy returns a deep-copied state trie. Notably the prefetcher is deliberately
|
||||||
|
// not copied, as it only belongs to the original one.
|
||||||
|
func (tr *wrapTrie) copy() *wrapTrie {
|
||||||
|
tr.term()
|
||||||
|
return &wrapTrie{StateTrie: tr.StateTrie.Copy()}
|
||||||
|
}
|
||||||
|
|
||||||
|
// storageRootReader wraps the account trie for loading the storage root. It is
|
||||||
|
// essential to use an independent trie to prevent potential data races with
|
||||||
|
// the optional prefetcher.
|
||||||
|
//
|
||||||
|
// TODO(rjl493456442) use the flat state for better read efficiency.
|
||||||
|
type storageRootReader struct {
|
||||||
|
tr *trie.StateTrie
|
||||||
|
}
|
||||||
|
|
||||||
|
func newStorageRootReader(root common.Hash, db *triedb.Database) (*storageRootReader, error) {
|
||||||
|
t, err := trie.NewStateTrie(trie.StateTrieID(root), db)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &storageRootReader{tr: t}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *storageRootReader) read(address common.Address) (common.Hash, error) {
|
||||||
|
acct, err := r.tr.GetAccount(address)
|
||||||
|
if err != nil {
|
||||||
|
return common.Hash{}, err
|
||||||
|
}
|
||||||
|
if acct == nil {
|
||||||
|
return types.EmptyRootHash, nil
|
||||||
|
}
|
||||||
|
return acct.Root, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *storageRootReader) copy() *storageRootReader {
|
||||||
|
return &storageRootReader{tr: r.tr.Copy()}
|
||||||
|
}
|
||||||
|
|
||||||
|
// merkleHasher is a Hasher implementation backed by the traditional two-layer
|
||||||
|
// Merkle Patricia Trie (separate account trie and per-account storage tries).
|
||||||
|
type merkleHasher struct {
|
||||||
|
db *triedb.Database
|
||||||
|
root common.Hash
|
||||||
|
reader *storageRootReader
|
||||||
|
prefetch bool
|
||||||
|
prefetchRead bool
|
||||||
|
|
||||||
|
acctTrie *wrapTrie
|
||||||
|
storageTries map[common.Address]*wrapTrie
|
||||||
|
|
||||||
|
// deletedTries preserves storage tries of accounts that were deleted
|
||||||
|
// during the block keyed by address. Only the first deletion per
|
||||||
|
// address is recorded (the pre-block incarnation).
|
||||||
|
deletedTries map[common.Address]*wrapTrie
|
||||||
|
|
||||||
|
// storageRoots tracks the storage root transition for each resolved
|
||||||
|
// account. Prev is captured on first touch; Hash is updated by
|
||||||
|
// UpdateStorage or set to EmptyRootHash on deletion.
|
||||||
|
storageRoots map[common.Address]Hashes
|
||||||
|
|
||||||
|
// Lock guards storage trie fields
|
||||||
|
storageLock sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMerkleHasher(root common.Hash, db *triedb.Database, prefetch bool, prefetchRead bool) (*merkleHasher, error) {
|
||||||
|
tr, err := newWrapTrie(trie.StateTrieID(root), db, prefetch, prefetchRead)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
r, err := newStorageRootReader(root, db)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &merkleHasher{
|
||||||
|
db: db,
|
||||||
|
root: root,
|
||||||
|
prefetch: prefetch,
|
||||||
|
prefetchRead: prefetchRead,
|
||||||
|
reader: r,
|
||||||
|
acctTrie: tr,
|
||||||
|
storageTries: make(map[common.Address]*wrapTrie),
|
||||||
|
deletedTries: make(map[common.Address]*wrapTrie),
|
||||||
|
storageRoots: make(map[common.Address]Hashes),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// storageRoot returns the current tracked storage root for addr. On first
|
||||||
|
// access for a given address the root is read from the account trie and
|
||||||
|
// recorded as the Prev value for the commit-time transition report.
|
||||||
|
func (h *merkleHasher) storageRoot(addr common.Address) (common.Hash, error) {
|
||||||
|
if hashes, ok := h.storageRoots[addr]; ok {
|
||||||
|
return hashes.Hash, nil
|
||||||
|
}
|
||||||
|
root, err := h.reader.read(addr)
|
||||||
|
if err != nil {
|
||||||
|
return common.Hash{}, err
|
||||||
|
}
|
||||||
|
h.storageRoots[addr] = Hashes{
|
||||||
|
Prev: root,
|
||||||
|
Hash: root,
|
||||||
|
}
|
||||||
|
return root, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// openStorageTrie returns the cached storage trie for addr, or opens one from
|
||||||
|
// the database if not already cached.
|
||||||
|
func (h *merkleHasher) openStorageTrie(address common.Address, prefetch bool) (*wrapTrie, error) {
|
||||||
|
h.storageLock.Lock()
|
||||||
|
defer h.storageLock.Unlock()
|
||||||
|
|
||||||
|
if tr, ok := h.storageTries[address]; ok {
|
||||||
|
return tr, nil
|
||||||
|
}
|
||||||
|
root, err := h.storageRoot(address)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
id := trie.StorageTrieID(h.root, crypto.Keccak256Hash(address.Bytes()), root)
|
||||||
|
|
||||||
|
tr, err := newWrapTrie(id, h.db, h.prefetch && prefetch, h.prefetchRead)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
h.storageTries[address] = tr
|
||||||
|
return tr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// deleteAccount removes the account specified by the address from the state.
|
||||||
|
func (h *merkleHasher) deleteAccount(addr common.Address) error {
|
||||||
|
// Capture the original storage root before modifying the trie.
|
||||||
|
_, err := h.storageRoot(addr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
h.storageRoots[addr] = Hashes{
|
||||||
|
Prev: h.storageRoots[addr].Prev,
|
||||||
|
Hash: types.EmptyRootHash,
|
||||||
|
}
|
||||||
|
// Preserve the first deleted storage trie per address for
|
||||||
|
// witness collection.
|
||||||
|
if tr, ok := h.storageTries[addr]; ok && h.deletedTries[addr] == nil {
|
||||||
|
h.deletedTries[addr] = tr
|
||||||
|
}
|
||||||
|
delete(h.storageTries, addr)
|
||||||
|
|
||||||
|
return h.acctTrie.DeleteAccount(addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// update writes the account specified by the address into the state.
|
||||||
|
func (h *merkleHasher) updateAccount(addr common.Address, account AccountMut) error {
|
||||||
|
root, err := h.storageRoot(addr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
data := &types.StateAccount{
|
||||||
|
Nonce: account.Account.Nonce,
|
||||||
|
Balance: account.Account.Balance,
|
||||||
|
Root: root,
|
||||||
|
CodeHash: account.Account.CodeHash,
|
||||||
|
}
|
||||||
|
return h.acctTrie.UpdateAccount(addr, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateAccount implements Hasher, writing a list of account mutations
|
||||||
|
// into the state. The assumption is held all the storage changes have
|
||||||
|
// already been written beforehand.
|
||||||
|
func (h *merkleHasher) UpdateAccount(addresses []common.Address, accounts []AccountMut) error {
|
||||||
|
var err error
|
||||||
|
for i, addr := range addresses {
|
||||||
|
if accounts[i].Account == nil {
|
||||||
|
err = h.deleteAccount(addr)
|
||||||
|
} else {
|
||||||
|
err = h.updateAccount(addr, accounts[i])
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateStorage implements Hasher, writing a list of storage slot mutations
|
||||||
|
// into the state. This function must be invoked first before writing the
|
||||||
|
// associated account metadata into the state.
|
||||||
|
func (h *merkleHasher) UpdateStorage(address common.Address, keys []common.Hash, values []common.Hash) error {
|
||||||
|
tr, err := h.openStorageTrie(address, false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for i, key := range keys {
|
||||||
|
if values[i] == (common.Hash{}) {
|
||||||
|
err = tr.DeleteStorage(address, key[:])
|
||||||
|
} else {
|
||||||
|
err = tr.UpdateStorage(address, key[:], common.TrimLeftZeroes(values[i][:]))
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Hash outside the lock to allow full parallelism across accounts.
|
||||||
|
hash := tr.Hash()
|
||||||
|
|
||||||
|
// Write back the storage root back for reflecting the most recent
|
||||||
|
// changes.
|
||||||
|
h.storageLock.Lock()
|
||||||
|
h.storageRoots[address] = Hashes{
|
||||||
|
Prev: h.storageRoots[address].Prev,
|
||||||
|
Hash: hash,
|
||||||
|
}
|
||||||
|
h.storageLock.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash implements Hasher, computing the state root hash without committing.
|
||||||
|
func (h *merkleHasher) Hash() common.Hash {
|
||||||
|
return h.acctTrie.Hash()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit implements Hasher, finalizing all pending changes and returning
|
||||||
|
// the resulting state root hash, along with the set of dirty trie nodes
|
||||||
|
// generated by the updates.
|
||||||
|
func (h *merkleHasher) Commit() (common.Hash, *trienode.MergedNodeSet, map[common.Address]Hashes, error) {
|
||||||
|
var (
|
||||||
|
eg errgroup.Group
|
||||||
|
root common.Hash
|
||||||
|
|
||||||
|
lock sync.Mutex
|
||||||
|
nodes = trienode.NewMergedNodeSet()
|
||||||
|
merge = func(set *trienode.NodeSet) error {
|
||||||
|
lock.Lock()
|
||||||
|
defer lock.Unlock()
|
||||||
|
|
||||||
|
return nodes.Merge(set)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
eg.Go(func() error {
|
||||||
|
r, set := h.acctTrie.Commit(true)
|
||||||
|
root = r
|
||||||
|
if set == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return merge(set)
|
||||||
|
})
|
||||||
|
for _, tr := range h.storageTries {
|
||||||
|
eg.Go(func() error {
|
||||||
|
_, set := tr.Commit(false)
|
||||||
|
if set == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return merge(set)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if err := eg.Wait(); err != nil {
|
||||||
|
return common.Hash{}, nil, nil, err
|
||||||
|
}
|
||||||
|
return root, nodes, h.storageRoots, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy implements Hasher, returning a deep-copied hasher instance.
|
||||||
|
func (h *merkleHasher) Copy() Hasher {
|
||||||
|
cpy := &merkleHasher{
|
||||||
|
db: h.db,
|
||||||
|
root: h.root,
|
||||||
|
reader: h.reader.copy(),
|
||||||
|
prefetch: false,
|
||||||
|
prefetchRead: false,
|
||||||
|
acctTrie: h.acctTrie.copy(),
|
||||||
|
storageTries: make(map[common.Address]*wrapTrie, len(h.storageTries)),
|
||||||
|
deletedTries: make(map[common.Address]*wrapTrie, len(h.deletedTries)),
|
||||||
|
storageRoots: maps.Clone(h.storageRoots),
|
||||||
|
}
|
||||||
|
for addr, tr := range h.storageTries {
|
||||||
|
cpy.storageTries[addr] = tr.copy()
|
||||||
|
}
|
||||||
|
for addr, tr := range h.deletedTries {
|
||||||
|
cpy.deletedTries[addr] = tr.copy()
|
||||||
|
}
|
||||||
|
return cpy
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProveAccount implements Prover, constructing a proof for the given account.
|
||||||
|
func (h *merkleHasher) ProveAccount(addr common.Address, proofDb ethdb.KeyValueWriter) error {
|
||||||
|
return h.acctTrie.Prove(crypto.Keccak256(addr.Bytes()), proofDb)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProveStorage implements Prover, constructing a proof for the given storage
|
||||||
|
// slot of the specified account.
|
||||||
|
func (h *merkleHasher) ProveStorage(addr common.Address, key common.Hash, proofDb ethdb.KeyValueWriter) error {
|
||||||
|
tr, err := h.openStorageTrie(addr, false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return tr.Prove(crypto.Keccak256(key.Bytes()), proofDb)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CollectWitness implements WitnessCollector. It aggregates all trie nodes
|
||||||
|
// accessed (both read and write) across the account trie, all active storage
|
||||||
|
// tries and deleted storage tries into a single state witness.
|
||||||
|
func (h *merkleHasher) CollectWitness(witness *stateless.Witness) {
|
||||||
|
witness.AddState(h.acctTrie.Witness(), common.Hash{})
|
||||||
|
for addr, tr := range h.storageTries {
|
||||||
|
witness.AddState(tr.Witness(), crypto.Keccak256Hash(addr.Bytes()))
|
||||||
|
}
|
||||||
|
for addr, tr := range h.deletedTries {
|
||||||
|
witness.AddState(tr.Witness(), crypto.Keccak256Hash(addr.Bytes()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrefetchAccount implements Prefetcher, preloading the nodes of specific accounts.
|
||||||
|
func (h *merkleHasher) PrefetchAccount(addresses []common.Address, read bool) {
|
||||||
|
if !h.prefetch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.acctTrie.prefetchAccounts(addresses, read)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrefetchStorage implements Prefetcher. The storage trie is opened eagerly
|
||||||
|
// so the prefetcher can begin loading nodes in the background.
|
||||||
|
func (h *merkleHasher) PrefetchStorage(addr common.Address, keys []common.Hash, read bool) {
|
||||||
|
if !h.prefetch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !h.prefetchRead && read {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tr, err := h.openStorageTrie(addr, true)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tr.prefetchStorage(addr, keys, read)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TermPrefetch terminates all prefetcher goroutines. Safe to call multiple times.
|
||||||
|
func (h *merkleHasher) TermPrefetch() {
|
||||||
|
if h == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.acctTrie.term()
|
||||||
|
for _, tr := range h.storageTries {
|
||||||
|
tr.term()
|
||||||
|
}
|
||||||
|
for _, tr := range h.deletedTries {
|
||||||
|
tr.term()
|
||||||
|
}
|
||||||
|
}
|
||||||
629
core/state/database_hasher_merkle_test.go
Normal file
629
core/state/database_hasher_merkle_test.go
Normal file
|
|
@ -0,0 +1,629 @@
|
||||||
|
// Copyright 2026 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 state
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||||
|
"github.com/ethereum/go-ethereum/core/stateless"
|
||||||
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
|
"github.com/ethereum/go-ethereum/triedb"
|
||||||
|
"github.com/holiman/uint256"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
hasherAddr1 = common.HexToAddress("0x1111111111111111111111111111111111111111")
|
||||||
|
hasherAddr2 = common.HexToAddress("0x2222222222222222222222222222222222222222")
|
||||||
|
hasherAddr3 = common.HexToAddress("0x3333333333333333333333333333333333333333")
|
||||||
|
|
||||||
|
hasherSlot1 = common.HexToHash("0x01")
|
||||||
|
hasherSlot2 = common.HexToHash("0x02")
|
||||||
|
hasherSlot3 = common.HexToHash("0x03")
|
||||||
|
|
||||||
|
hasherVal1 = common.HexToHash("0xaa")
|
||||||
|
hasherVal2 = common.HexToHash("0xbb")
|
||||||
|
hasherVal3 = common.HexToHash("0xcc")
|
||||||
|
)
|
||||||
|
|
||||||
|
// hasherTestConfig captures the prefetch flags varied across subtests.
|
||||||
|
type hasherTestConfig struct {
|
||||||
|
name string
|
||||||
|
prefetch bool
|
||||||
|
prefetchRead bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// hasherTestConfigs enumerates the interesting (prefetch, prefetchRead) combinations:
|
||||||
|
// - no prefetch at all
|
||||||
|
// - prefetch writes only (read prefetch requests are dropped)
|
||||||
|
// - prefetch reads and writes
|
||||||
|
var hasherTestConfigs = []hasherTestConfig{
|
||||||
|
{"noPrefetch", false, false},
|
||||||
|
{"prefetchWriteOnly", true, false},
|
||||||
|
{"prefetchAll", true, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasherAccount(nonce uint64, balance uint64) AccountMut {
|
||||||
|
return AccountMut{
|
||||||
|
Account: &Account{
|
||||||
|
Nonce: nonce,
|
||||||
|
Balance: uint256.NewInt(balance),
|
||||||
|
CodeHash: types.EmptyCodeHash.Bytes(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasherDeleteAccount() AccountMut {
|
||||||
|
return AccountMut{Account: nil}
|
||||||
|
}
|
||||||
|
|
||||||
|
// newTestHasher creates a merkleHasher backed by an in-memory database.
|
||||||
|
func newTestHasher(t *testing.T, db *triedb.Database, root common.Hash, cfg hasherTestConfig) *merkleHasher {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
h, err := newMerkleHasher(root, db, cfg.prefetch, cfg.prefetchRead)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { h.TermPrefetch() })
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
// commitAndReopen commits the hasher's state and reopens a fresh hasher from
|
||||||
|
// the committed root. This simulates a block boundary.
|
||||||
|
func commitAndReopen(t *testing.T, h *merkleHasher, cfg hasherTestConfig) *merkleHasher {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
root, nodes, _, err := h.Commit()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if nodes != nil {
|
||||||
|
if err := h.db.Update(root, h.root, 0, nodes, nil); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := h.db.Commit(root, false); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h2, err := newMerkleHasher(root, h.db, cfg.prefetch, cfg.prefetchRead)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { h2.TermPrefetch() })
|
||||||
|
return h2
|
||||||
|
}
|
||||||
|
|
||||||
|
// makeBaseState creates a non-empty state as the starting point for tests.
|
||||||
|
// The base contains:
|
||||||
|
// - addr1: nonce=1, balance=100, storage={slot1: val1, slot2: val2}
|
||||||
|
// - addr2: nonce=2, balance=200, no storage
|
||||||
|
//
|
||||||
|
// The state is committed and flushed so the hasher returned opens from disk,
|
||||||
|
// exercising rootReader and existing-trie code paths.
|
||||||
|
func makeBaseState(t *testing.T, cfg hasherTestConfig) *merkleHasher {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
noPrefetch := hasherTestConfig{"base", false, false}
|
||||||
|
db := triedb.NewDatabase(rawdb.NewMemoryDatabase(), nil)
|
||||||
|
h := newTestHasher(t, db, types.EmptyRootHash, noPrefetch)
|
||||||
|
|
||||||
|
if err := h.UpdateStorage(hasherAddr1, []common.Hash{hasherSlot1, hasherSlot2}, []common.Hash{hasherVal1, hasherVal2}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := h.UpdateAccount(
|
||||||
|
[]common.Address{hasherAddr1, hasherAddr2},
|
||||||
|
[]AccountMut{hasherAccount(1, 100), hasherAccount(2, 200)},
|
||||||
|
); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return commitAndReopen(t, h, cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestMerkleHasherBasic verifies that mutating storage and accounts on top of
|
||||||
|
// a non-empty base state produces a deterministic, non-empty root and that the
|
||||||
|
// root survives a commit+reopen cycle.
|
||||||
|
func TestMerkleHasherBasic(t *testing.T) {
|
||||||
|
for _, cfg := range hasherTestConfigs {
|
||||||
|
t.Run(cfg.name, func(t *testing.T) {
|
||||||
|
h := makeBaseState(t, cfg)
|
||||||
|
|
||||||
|
if cfg.prefetch {
|
||||||
|
h.PrefetchStorage(hasherAddr1, []common.Hash{hasherSlot3}, false)
|
||||||
|
h.PrefetchAccount([]common.Address{hasherAddr1, hasherAddr3}, false)
|
||||||
|
}
|
||||||
|
// Add slot3 to addr1 and create addr3.
|
||||||
|
if err := h.UpdateStorage(hasherAddr1, []common.Hash{hasherSlot3}, []common.Hash{hasherVal3}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := h.UpdateAccount(
|
||||||
|
[]common.Address{hasherAddr1, hasherAddr3},
|
||||||
|
[]AccountMut{hasherAccount(1, 100), hasherAccount(3, 300)},
|
||||||
|
); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
root := h.Hash()
|
||||||
|
if root == types.EmptyRootHash {
|
||||||
|
t.Fatal("expected non-empty root after mutations")
|
||||||
|
}
|
||||||
|
h2 := commitAndReopen(t, h, cfg)
|
||||||
|
if h2.Hash() != root {
|
||||||
|
t.Fatalf("root mismatch after reopen: got %x, want %x", h2.Hash(), root)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestMerkleHasherPrefetchReadOnly verifies that read-only prefetching (for
|
||||||
|
// accounts and storage that are never subsequently mutated) does not corrupt
|
||||||
|
// state and does not leak goroutines. Both prefetchRead=true (requests are
|
||||||
|
// processed) and prefetchRead=false (requests are dropped) are tested.
|
||||||
|
func TestMerkleHasherPrefetchReadOnly(t *testing.T) {
|
||||||
|
for _, prefetchRead := range []bool{false, true} {
|
||||||
|
name := "readDropped"
|
||||||
|
if prefetchRead {
|
||||||
|
name = "readProcessed"
|
||||||
|
}
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
cfg := hasherTestConfig{name, true, prefetchRead}
|
||||||
|
h := makeBaseState(t, cfg)
|
||||||
|
rootBefore := h.Hash()
|
||||||
|
|
||||||
|
// Prefetch addr1's account and storage (read-only). Whether
|
||||||
|
// these are actually processed depends on prefetchRead.
|
||||||
|
h.PrefetchAccount([]common.Address{hasherAddr1, hasherAddr2}, true)
|
||||||
|
h.PrefetchStorage(hasherAddr1, []common.Hash{hasherSlot1, hasherSlot2}, true)
|
||||||
|
|
||||||
|
// Only mutate addr2 (no storage) — addr1's prefetched tries
|
||||||
|
// are never accessed through a shadow method.
|
||||||
|
if err := h.UpdateAccount(
|
||||||
|
[]common.Address{hasherAddr2},
|
||||||
|
[]AccountMut{hasherAccount(2, 300)},
|
||||||
|
); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
root := h.Hash()
|
||||||
|
if root == rootBefore {
|
||||||
|
t.Fatal("expected root to change after balance update")
|
||||||
|
}
|
||||||
|
h2 := commitAndReopen(t, h, hasherTestConfig{"verify", false, false})
|
||||||
|
if h2.Hash() != root {
|
||||||
|
t.Fatalf("root mismatch: got %x, want %x", h2.Hash(), root)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestMerkleHasherDeleteAccount verifies that deleting an account with storage
|
||||||
|
// produces an empty storage root in the commit result, with Prev reflecting
|
||||||
|
// the original non-empty root.
|
||||||
|
func TestMerkleHasherDeleteAccount(t *testing.T) {
|
||||||
|
for _, cfg := range hasherTestConfigs {
|
||||||
|
t.Run(cfg.name, func(t *testing.T) {
|
||||||
|
h := makeBaseState(t, cfg)
|
||||||
|
|
||||||
|
if cfg.prefetch {
|
||||||
|
h.PrefetchAccount([]common.Address{hasherAddr1}, false)
|
||||||
|
h.PrefetchStorage(hasherAddr1, []common.Hash{hasherSlot1, hasherSlot2}, false)
|
||||||
|
}
|
||||||
|
// Delete addr1 (which has storage slots 1,2).
|
||||||
|
if err := h.UpdateAccount(
|
||||||
|
[]common.Address{hasherAddr1},
|
||||||
|
[]AccountMut{hasherDeleteAccount()},
|
||||||
|
); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
_, _, storageRoots, err := h.Commit()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
sr, ok := storageRoots[hasherAddr1]
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("deleted account missing from storageRoots")
|
||||||
|
}
|
||||||
|
if sr.Hash != types.EmptyRootHash {
|
||||||
|
t.Fatalf("deleted account storage root: got %x, want EmptyRootHash", sr.Hash)
|
||||||
|
}
|
||||||
|
if sr.Prev == types.EmptyRootHash {
|
||||||
|
t.Fatal("deleted account Prev should be non-empty (had storage)")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestMerkleHasherDeleteRecreate verifies that deleting an account and
|
||||||
|
// recreating it with different storage in the same block produces a correct
|
||||||
|
// root that survives a commit+reopen cycle. The storageRoots report must show
|
||||||
|
// the original Prev and a new Hash.
|
||||||
|
func TestMerkleHasherDeleteRecreate(t *testing.T) {
|
||||||
|
for _, cfg := range hasherTestConfigs {
|
||||||
|
t.Run(cfg.name, func(t *testing.T) {
|
||||||
|
h := makeBaseState(t, cfg)
|
||||||
|
|
||||||
|
if cfg.prefetch {
|
||||||
|
h.PrefetchAccount([]common.Address{hasherAddr1}, false)
|
||||||
|
h.PrefetchStorage(hasherAddr1, []common.Hash{hasherSlot1, hasherSlot2}, false)
|
||||||
|
}
|
||||||
|
// Delete addr1.
|
||||||
|
if err := h.UpdateAccount([]common.Address{hasherAddr1}, []AccountMut{hasherDeleteAccount()}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// Recreate with slot3 only.
|
||||||
|
if err := h.UpdateStorage(hasherAddr1, []common.Hash{hasherSlot3}, []common.Hash{hasherVal3}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := h.UpdateAccount([]common.Address{hasherAddr1}, []AccountMut{hasherAccount(10, 500)}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
root := h.Hash()
|
||||||
|
if root == types.EmptyRootHash {
|
||||||
|
t.Fatal("expected non-empty root after recreate")
|
||||||
|
}
|
||||||
|
h2 := commitAndReopen(t, h, hasherTestConfig{"verify", false, false})
|
||||||
|
|
||||||
|
sr := h.storageRoots[hasherAddr1]
|
||||||
|
if sr.Hash == types.EmptyRootHash {
|
||||||
|
t.Fatal("recreated account should have non-empty storage root")
|
||||||
|
}
|
||||||
|
if sr.Prev == types.EmptyRootHash {
|
||||||
|
t.Fatal("Prev should reflect the pre-deletion storage root")
|
||||||
|
}
|
||||||
|
if sr.Hash == sr.Prev {
|
||||||
|
t.Fatal("Hash and Prev should differ after delete+recreate with different slots")
|
||||||
|
}
|
||||||
|
if h2.Hash() != root {
|
||||||
|
t.Fatalf("root mismatch after reopen: got %x, want %x", h2.Hash(), root)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestMerkleHasherPrefetchDeterminism verifies that the resulting root is
|
||||||
|
// identical across all prefetch configurations for the same set of mutations.
|
||||||
|
func TestMerkleHasherPrefetchDeterminism(t *testing.T) {
|
||||||
|
var roots []common.Hash
|
||||||
|
for _, cfg := range hasherTestConfigs {
|
||||||
|
h := makeBaseState(t, cfg)
|
||||||
|
|
||||||
|
if cfg.prefetch {
|
||||||
|
h.PrefetchAccount([]common.Address{hasherAddr1, hasherAddr3}, false)
|
||||||
|
h.PrefetchStorage(hasherAddr1, []common.Hash{hasherSlot3}, false)
|
||||||
|
h.PrefetchStorage(hasherAddr3, []common.Hash{hasherSlot1}, false)
|
||||||
|
}
|
||||||
|
// Add slot3 to addr1, create addr3 with slot1.
|
||||||
|
if err := h.UpdateStorage(hasherAddr1, []common.Hash{hasherSlot3}, []common.Hash{hasherVal3}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := h.UpdateStorage(hasherAddr3, []common.Hash{hasherSlot1}, []common.Hash{hasherVal1}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := h.UpdateAccount(
|
||||||
|
[]common.Address{hasherAddr1, hasherAddr3},
|
||||||
|
[]AccountMut{hasherAccount(1, 100), hasherAccount(3, 300)},
|
||||||
|
); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
roots = append(roots, h.Hash())
|
||||||
|
}
|
||||||
|
for i := 1; i < len(roots); i++ {
|
||||||
|
if roots[i] != roots[0] {
|
||||||
|
t.Fatalf("root diverged: config[0]=%x config[%d]=%x", roots[0], i, roots[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestMerkleHasherCommitStorageRoots exhaustively checks the Prev/Hash pairs
|
||||||
|
// returned by Commit for every interesting mutation pattern:
|
||||||
|
//
|
||||||
|
// (1) delete account with non-empty storage
|
||||||
|
// (2) delete account with empty storage
|
||||||
|
// (3) delete + recreate with new non-empty storage
|
||||||
|
// (4) delete + recreate without storage (empty→empty after recreate)
|
||||||
|
// (5) delete + recreate: originally empty storage, recreated with storage
|
||||||
|
// (6) mutate account only, no storage (empty storage throughout)
|
||||||
|
// (7) mutate account only, non-empty storage unchanged
|
||||||
|
// (8) mutate account with modified storage
|
||||||
|
func TestMerkleHasherCommitStorageRoots(t *testing.T) {
|
||||||
|
var (
|
||||||
|
// Addresses for each case — distinct so they don't interfere.
|
||||||
|
addrDeleteNonEmpty = common.HexToAddress("0xaa01") // (1)
|
||||||
|
addrDeleteEmpty = common.HexToAddress("0xaa02") // (2)
|
||||||
|
addrRecreateStorage = common.HexToAddress("0xaa03") // (3)
|
||||||
|
addrRecreateNoStore = common.HexToAddress("0xaa04") // (4)
|
||||||
|
addrRecreateFromNone = common.HexToAddress("0xaa05") // (5)
|
||||||
|
addrMutateNoStorage = common.HexToAddress("0xaa06") // (6)
|
||||||
|
addrMutateKeepStore = common.HexToAddress("0xaa07") // (7)
|
||||||
|
addrMutateModStore = common.HexToAddress("0xaa08") // (8)
|
||||||
|
)
|
||||||
|
for _, cfg := range hasherTestConfigs {
|
||||||
|
t.Run(cfg.name, func(t *testing.T) {
|
||||||
|
// ---------- base state (committed to disk) ----------
|
||||||
|
noPrefetch := hasherTestConfig{"base", false, false}
|
||||||
|
db := triedb.NewDatabase(rawdb.NewMemoryDatabase(), nil)
|
||||||
|
base := newTestHasher(t, db, types.EmptyRootHash, noPrefetch)
|
||||||
|
|
||||||
|
// Accounts with storage.
|
||||||
|
for _, addr := range []common.Address{addrDeleteNonEmpty, addrRecreateStorage, addrRecreateNoStore, addrMutateKeepStore, addrMutateModStore} {
|
||||||
|
if err := base.UpdateStorage(addr, []common.Hash{hasherSlot1}, []common.Hash{hasherVal1}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// All accounts (some with storage above, some without).
|
||||||
|
allAddrs := []common.Address{
|
||||||
|
addrDeleteNonEmpty, addrDeleteEmpty,
|
||||||
|
addrRecreateStorage, addrRecreateNoStore, addrRecreateFromNone,
|
||||||
|
addrMutateNoStorage, addrMutateKeepStore, addrMutateModStore,
|
||||||
|
}
|
||||||
|
allAccounts := make([]AccountMut, len(allAddrs))
|
||||||
|
for i := range allAccounts {
|
||||||
|
allAccounts[i] = hasherAccount(1, 100)
|
||||||
|
}
|
||||||
|
if err := base.UpdateAccount(allAddrs, allAccounts); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
h := commitAndReopen(t, base, cfg)
|
||||||
|
|
||||||
|
// ---------- block mutations ----------
|
||||||
|
|
||||||
|
// (1) Delete account with non-empty storage.
|
||||||
|
// (2) Delete account with empty storage.
|
||||||
|
if err := h.UpdateAccount(
|
||||||
|
[]common.Address{addrDeleteNonEmpty, addrDeleteEmpty},
|
||||||
|
[]AccountMut{hasherDeleteAccount(), hasherDeleteAccount()},
|
||||||
|
); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// (3) Delete + recreate with new storage.
|
||||||
|
if err := h.UpdateAccount([]common.Address{addrRecreateStorage}, []AccountMut{hasherDeleteAccount()}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := h.UpdateStorage(addrRecreateStorage, []common.Hash{hasherSlot2}, []common.Hash{hasherVal2}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := h.UpdateAccount([]common.Address{addrRecreateStorage}, []AccountMut{hasherAccount(2, 200)}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// (4) Delete + recreate without storage (had storage before).
|
||||||
|
if err := h.UpdateAccount([]common.Address{addrRecreateNoStore}, []AccountMut{hasherDeleteAccount()}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := h.UpdateAccount([]common.Address{addrRecreateNoStore}, []AccountMut{hasherAccount(2, 200)}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// (5) Delete + recreate: originally no storage, recreated with storage.
|
||||||
|
if err := h.UpdateAccount([]common.Address{addrRecreateFromNone}, []AccountMut{hasherDeleteAccount()}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := h.UpdateStorage(addrRecreateFromNone, []common.Hash{hasherSlot1}, []common.Hash{hasherVal3}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := h.UpdateAccount([]common.Address{addrRecreateFromNone}, []AccountMut{hasherAccount(2, 200)}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// (6) Mutate account only, no storage.
|
||||||
|
if err := h.UpdateAccount([]common.Address{addrMutateNoStorage}, []AccountMut{hasherAccount(2, 999)}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// (7) Mutate account, non-empty storage unchanged.
|
||||||
|
if err := h.UpdateAccount([]common.Address{addrMutateKeepStore}, []AccountMut{hasherAccount(2, 888)}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// (8) Mutate account with modified storage.
|
||||||
|
if err := h.UpdateStorage(addrMutateModStore, []common.Hash{hasherSlot1}, []common.Hash{hasherVal2}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := h.UpdateAccount([]common.Address{addrMutateModStore}, []AccountMut{hasherAccount(2, 777)}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
_, _, roots, err := h.Commit()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
empty := types.EmptyRootHash
|
||||||
|
|
||||||
|
// (1) Deleted, had storage: Prev=non-empty, Hash=empty.
|
||||||
|
sr := roots[addrDeleteNonEmpty]
|
||||||
|
if sr.Prev == empty {
|
||||||
|
t.Fatal("(1) Prev should be non-empty for deleted account that had storage")
|
||||||
|
}
|
||||||
|
if sr.Hash != empty {
|
||||||
|
t.Fatal("(1) Hash should be EmptyRootHash after deletion")
|
||||||
|
}
|
||||||
|
// (2) Deleted, had no storage: Prev=empty, Hash=empty.
|
||||||
|
sr = roots[addrDeleteEmpty]
|
||||||
|
if sr.Prev != empty || sr.Hash != empty {
|
||||||
|
t.Fatalf("(2) expected both EmptyRootHash, got Prev=%x Hash=%x", sr.Prev, sr.Hash)
|
||||||
|
}
|
||||||
|
// (3) Delete+recreate with new storage: Prev=non-empty(original), Hash=non-empty(new), differ.
|
||||||
|
sr = roots[addrRecreateStorage]
|
||||||
|
if sr.Prev == empty {
|
||||||
|
t.Fatal("(3) Prev should be non-empty (had storage before deletion)")
|
||||||
|
}
|
||||||
|
if sr.Hash == empty {
|
||||||
|
t.Fatal("(3) Hash should be non-empty (recreated with storage)")
|
||||||
|
}
|
||||||
|
if sr.Hash == sr.Prev {
|
||||||
|
t.Fatal("(3) Hash and Prev should differ (different storage contents)")
|
||||||
|
}
|
||||||
|
// (4) Delete+recreate without storage (originally had storage): Prev=non-empty, Hash=empty.
|
||||||
|
sr = roots[addrRecreateNoStore]
|
||||||
|
if sr.Prev == empty {
|
||||||
|
t.Fatal("(4) Prev should be non-empty (had storage before deletion)")
|
||||||
|
}
|
||||||
|
if sr.Hash != empty {
|
||||||
|
t.Fatal("(4) Hash should be EmptyRootHash (recreated without storage)")
|
||||||
|
}
|
||||||
|
// (5) Delete+recreate: originally no storage, recreated with storage: Prev=empty, Hash=non-empty.
|
||||||
|
sr = roots[addrRecreateFromNone]
|
||||||
|
if sr.Prev != empty {
|
||||||
|
t.Fatal("(5) Prev should be EmptyRootHash (no storage before deletion)")
|
||||||
|
}
|
||||||
|
if sr.Hash == empty {
|
||||||
|
t.Fatal("(5) Hash should be non-empty (recreated with storage)")
|
||||||
|
}
|
||||||
|
// (6) Mutate account only, no storage: Prev=empty, Hash=empty.
|
||||||
|
sr = roots[addrMutateNoStorage]
|
||||||
|
if sr.Prev != empty || sr.Hash != empty {
|
||||||
|
t.Fatalf("(6) expected both EmptyRootHash, got Prev=%x Hash=%x", sr.Prev, sr.Hash)
|
||||||
|
}
|
||||||
|
// (7) Mutate account, storage unchanged: Prev=non-empty, Hash=non-empty, Prev==Hash.
|
||||||
|
sr = roots[addrMutateKeepStore]
|
||||||
|
if sr.Prev == empty {
|
||||||
|
t.Fatal("(7) Prev should be non-empty (has storage)")
|
||||||
|
}
|
||||||
|
if sr.Hash == empty {
|
||||||
|
t.Fatal("(7) Hash should be non-empty (storage unchanged)")
|
||||||
|
}
|
||||||
|
if sr.Prev != sr.Hash {
|
||||||
|
t.Fatal("(7) Prev and Hash should be equal (storage was not modified)")
|
||||||
|
}
|
||||||
|
// (8) Mutate account with modified storage: Prev=non-empty, Hash=non-empty, differ.
|
||||||
|
sr = roots[addrMutateModStore]
|
||||||
|
if sr.Prev == empty {
|
||||||
|
t.Fatal("(8) Prev should be non-empty (had storage)")
|
||||||
|
}
|
||||||
|
if sr.Hash == empty {
|
||||||
|
t.Fatal("(8) Hash should be non-empty (storage modified, not cleared)")
|
||||||
|
}
|
||||||
|
if sr.Prev == sr.Hash {
|
||||||
|
t.Fatal("(8) Prev and Hash should differ (storage was modified)")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestMerkleHasherCopy verifies that Copy produces an independent snapshot:
|
||||||
|
// mutations on the copy must not affect the original's hash.
|
||||||
|
func TestMerkleHasherCopy(t *testing.T) {
|
||||||
|
cfg := hasherTestConfig{"prefetchAll", true, true}
|
||||||
|
h := makeBaseState(t, cfg)
|
||||||
|
|
||||||
|
h.PrefetchAccount([]common.Address{hasherAddr1}, false)
|
||||||
|
h.PrefetchStorage(hasherAddr1, []common.Hash{hasherSlot3}, false)
|
||||||
|
if err := h.UpdateStorage(hasherAddr1, []common.Hash{hasherSlot3}, []common.Hash{hasherVal3}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := h.UpdateAccount([]common.Address{hasherAddr1}, []AccountMut{hasherAccount(1, 100)}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
origRoot := h.Hash()
|
||||||
|
|
||||||
|
cpy := h.Copy()
|
||||||
|
defer cpy.(*merkleHasher).TermPrefetch()
|
||||||
|
|
||||||
|
// Mutate the copy: delete slot3, add slot2 with new value.
|
||||||
|
if err := cpy.UpdateStorage(hasherAddr1, []common.Hash{hasherSlot3, hasherSlot2}, []common.Hash{{}, hasherVal3}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := cpy.UpdateAccount([]common.Address{hasherAddr1}, []AccountMut{hasherAccount(1, 100)}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if cpy.Hash() == origRoot {
|
||||||
|
t.Fatal("copy should diverge after mutation")
|
||||||
|
}
|
||||||
|
if h.Hash() != origRoot {
|
||||||
|
t.Fatal("original root changed after mutating copy")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// proofNodes collects the raw RLP-encoded trie nodes written by Prove calls.
|
||||||
|
type proofNodes struct{ nodes [][]byte }
|
||||||
|
|
||||||
|
func (p *proofNodes) Put(key []byte, value []byte) error {
|
||||||
|
p.nodes = append(p.nodes, common.CopyBytes(value))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (p *proofNodes) Delete([]byte) error { return nil }
|
||||||
|
|
||||||
|
// TestMerkleHasherWitness verifies that the witness returned by Witness()
|
||||||
|
// contains every trie node on the Merkle proof path for each accessed account
|
||||||
|
// and storage slot, including nodes from deleted storage tries.
|
||||||
|
func TestMerkleHasherWitness(t *testing.T) {
|
||||||
|
h := makeBaseState(t, hasherTestConfig{"prefetchAll", true, true})
|
||||||
|
|
||||||
|
// Mutate addr1 storage, then delete and recreate with different
|
||||||
|
// storage so that both deletedTries and storageTries are populated.
|
||||||
|
h.PrefetchStorage(hasherAddr1, []common.Hash{hasherSlot1}, false)
|
||||||
|
if err := h.UpdateStorage(hasherAddr1, []common.Hash{hasherSlot1}, []common.Hash{hasherVal2}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := h.UpdateAccount([]common.Address{hasherAddr1}, []AccountMut{hasherDeleteAccount()}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := h.UpdateStorage(hasherAddr1, []common.Hash{hasherSlot3}, []common.Hash{hasherVal3}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := h.UpdateAccount(
|
||||||
|
[]common.Address{hasherAddr1, hasherAddr2},
|
||||||
|
[]AccountMut{hasherAccount(10, 500), hasherAccount(2, 300)},
|
||||||
|
); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
witness := &stateless.Witness{
|
||||||
|
Codes: make(map[string]struct{}),
|
||||||
|
State: make(map[string]struct{}),
|
||||||
|
}
|
||||||
|
h.CollectWitness(witness)
|
||||||
|
|
||||||
|
if len(witness.State) == 0 {
|
||||||
|
t.Fatal("witness should contain trie nodes")
|
||||||
|
}
|
||||||
|
// Open a separate prover from the same pre-state root. Proofs
|
||||||
|
// generated here traverse the same trie paths that the mutating
|
||||||
|
// hasher loaded, so every proof node must be in the witness.
|
||||||
|
prover, err := newMerkleHasher(h.root, h.db, false, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer prover.TermPrefetch()
|
||||||
|
|
||||||
|
// Collect all expected proof nodes into a single set. The union of
|
||||||
|
// account proofs (addr1, addr2) and storage proofs (addr1/slot1)
|
||||||
|
// should exactly equal witness.State — no missing, no extra.
|
||||||
|
expected := make(map[string]struct{})
|
||||||
|
|
||||||
|
for _, addr := range []common.Address{hasherAddr1, hasherAddr2} {
|
||||||
|
pn := &proofNodes{}
|
||||||
|
if err := prover.ProveAccount(addr, pn); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, node := range pn.nodes {
|
||||||
|
expected[string(node)] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Storage proof for addr1/slot1 (accessed before deletion).
|
||||||
|
// Slot2 was in the base state but never read or written during the
|
||||||
|
// block, so its leaf node is correctly absent from the witness.
|
||||||
|
pn := &proofNodes{}
|
||||||
|
if err := prover.ProveStorage(hasherAddr1, hasherSlot1, pn); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, node := range pn.nodes {
|
||||||
|
expected[string(node)] = struct{}{}
|
||||||
|
}
|
||||||
|
// Every expected proof node must be in the witness.
|
||||||
|
for node := range expected {
|
||||||
|
if _, ok := witness.State[node]; !ok {
|
||||||
|
t.Fatal("proof node missing from witness")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The witness must not contain any extra nodes beyond the proofs.
|
||||||
|
if len(witness.State) != len(expected) {
|
||||||
|
t.Fatalf("witness has %d nodes, expected %d (extra junk present)", len(witness.State), len(expected))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -34,17 +34,18 @@ import (
|
||||||
// historicStateReader implements StateReader, wrapping a historical state reader
|
// historicStateReader implements StateReader, wrapping a historical state reader
|
||||||
// defined in path database and provide historic state serving over the path scheme.
|
// defined in path database and provide historic state serving over the path scheme.
|
||||||
type historicStateReader struct {
|
type historicStateReader struct {
|
||||||
reader *pathdb.HistoricalStateReader
|
reader *pathdb.HistoricalStateReader
|
||||||
lock sync.Mutex // Lock for protecting concurrent read
|
isVerkle bool // true when the database uses the binary trie scheme
|
||||||
|
lock sync.Mutex // Lock for protecting concurrent read
|
||||||
}
|
}
|
||||||
|
|
||||||
// newHistoricStateReader constructs a reader for historical state serving.
|
// newHistoricStateReader constructs a reader for historical state serving.
|
||||||
func newHistoricStateReader(r *pathdb.HistoricalStateReader) *historicStateReader {
|
func newHistoricStateReader(r *pathdb.HistoricalStateReader, isVerkle bool) *historicStateReader {
|
||||||
return &historicStateReader{reader: r}
|
return &historicStateReader{reader: r, isVerkle: isVerkle}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Account implements StateReader, retrieving the account specified by the address.
|
// Account implements StateReader, retrieving the account specified by the address.
|
||||||
func (r *historicStateReader) Account(addr common.Address) (*types.StateAccount, error) {
|
func (r *historicStateReader) Account(addr common.Address) (*Account, error) {
|
||||||
r.lock.Lock()
|
r.lock.Lock()
|
||||||
defer r.lock.Unlock()
|
defer r.lock.Unlock()
|
||||||
|
|
||||||
|
|
@ -55,18 +56,14 @@ func (r *historicStateReader) Account(addr common.Address) (*types.StateAccount,
|
||||||
if account == nil {
|
if account == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
acct := &types.StateAccount{
|
acct := &Account{
|
||||||
Nonce: account.Nonce,
|
Nonce: account.Nonce,
|
||||||
Balance: account.Balance,
|
Balance: account.Balance,
|
||||||
CodeHash: account.CodeHash,
|
CodeHash: account.CodeHash,
|
||||||
Root: common.BytesToHash(account.Root),
|
|
||||||
}
|
}
|
||||||
if len(acct.CodeHash) == 0 {
|
if len(acct.CodeHash) == 0 {
|
||||||
acct.CodeHash = types.EmptyCodeHash.Bytes()
|
acct.CodeHash = types.EmptyCodeHash.Bytes()
|
||||||
}
|
}
|
||||||
if acct.Root == (common.Hash{}) {
|
|
||||||
acct.Root = types.EmptyRootHash
|
|
||||||
}
|
|
||||||
return acct, nil
|
return acct, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -88,6 +85,17 @@ func (r *historicStateReader) Storage(addr common.Address, key common.Hash) (com
|
||||||
if len(blob) == 0 {
|
if len(blob) == 0 {
|
||||||
return common.Hash{}, nil
|
return common.Hash{}, nil
|
||||||
}
|
}
|
||||||
|
// Bintrie storage leaves are raw 32-byte values (not RLP-encoded)
|
||||||
|
// because the bintrie flat-state codec stores leaves verbatim.
|
||||||
|
// The merkle path encodes storage values as trimmed-left-zeros RLP
|
||||||
|
// before writing, so rlp.Split is the correct decoder there.
|
||||||
|
// Without this dispatch, bintrie historical storage reads would
|
||||||
|
// either decode garbage or error from rlp.Split on raw 32 bytes.
|
||||||
|
if r.isVerkle {
|
||||||
|
var slot common.Hash
|
||||||
|
copy(slot[:], blob)
|
||||||
|
return slot, nil
|
||||||
|
}
|
||||||
_, content, _, err := rlp.Split(blob)
|
_, content, _, err := rlp.Split(blob)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return common.Hash{}, err
|
return common.Hash{}, err
|
||||||
|
|
@ -150,17 +158,25 @@ func newHistoricalTrieReader(root common.Hash, r *pathdb.HistoricalNodeReader) (
|
||||||
}
|
}
|
||||||
|
|
||||||
// account is the inner version of Account and assumes the r.lock is already held.
|
// account is the inner version of Account and assumes the r.lock is already held.
|
||||||
func (r *historicalTrieReader) account(addr common.Address) (*types.StateAccount, error) {
|
func (r *historicalTrieReader) account(addr common.Address) (*Account, error) {
|
||||||
account, err := r.tr.GetAccount(addr)
|
account, err := r.tr.GetAccount(addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if account == nil {
|
if account == nil {
|
||||||
r.subRoots[addr] = types.EmptyRootHash
|
r.subRoots[addr] = types.EmptyRootHash
|
||||||
|
return nil, nil
|
||||||
} else {
|
} else {
|
||||||
r.subRoots[addr] = account.Root
|
r.subRoots[addr] = account.Root
|
||||||
|
|
||||||
|
// Account objects resolved from the trie always include
|
||||||
|
// the full code hash.
|
||||||
|
return &Account{
|
||||||
|
Nonce: account.Nonce,
|
||||||
|
Balance: account.Balance,
|
||||||
|
CodeHash: account.CodeHash,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
return account, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Account implements StateReader, retrieving the account specified by the address.
|
// Account implements StateReader, retrieving the account specified by the address.
|
||||||
|
|
@ -169,7 +185,7 @@ func (r *historicalTrieReader) account(addr common.Address) (*types.StateAccount
|
||||||
// the requested account is not yet covered by the snapshot.
|
// the requested account is not yet covered by the snapshot.
|
||||||
//
|
//
|
||||||
// The returned account might be nil if it's not existent.
|
// The returned account might be nil if it's not existent.
|
||||||
func (r *historicalTrieReader) Account(addr common.Address) (*types.StateAccount, error) {
|
func (r *historicalTrieReader) Account(addr common.Address) (*Account, error) {
|
||||||
r.lock.Lock()
|
r.lock.Lock()
|
||||||
defer r.lock.Unlock()
|
defer r.lock.Unlock()
|
||||||
|
|
||||||
|
|
@ -236,7 +252,7 @@ func (db *HistoricDB) Reader(stateRoot common.Hash) (Reader, error) {
|
||||||
var readers []StateReader
|
var readers []StateReader
|
||||||
sr, err := db.triedb.HistoricStateReader(stateRoot)
|
sr, err := db.triedb.HistoricStateReader(stateRoot)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
readers = append(readers, newHistoricStateReader(sr))
|
readers = append(readers, newHistoricStateReader(sr, db.triedb.IsVerkle()))
|
||||||
}
|
}
|
||||||
nr, err := db.triedb.HistoricNodeReader(stateRoot)
|
nr, err := db.triedb.HistoricNodeReader(stateRoot)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|
@ -255,6 +271,10 @@ func (db *HistoricDB) Reader(stateRoot common.Hash) (Reader, error) {
|
||||||
return newReader(db.codedb.Reader(), combined), nil
|
return newReader(db.codedb.Reader(), combined), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (db *HistoricDB) Hasher(stateRoot common.Hash) (Hasher, error) {
|
||||||
|
return &noopHasher{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// OpenTrie opens the main account trie. It's not supported by historic database.
|
// OpenTrie opens the main account trie. It's not supported by historic database.
|
||||||
func (db *HistoricDB) OpenTrie(root common.Hash) (Trie, error) {
|
func (db *HistoricDB) OpenTrie(root common.Hash) (Trie, error) {
|
||||||
nr, err := db.triedb.HistoricNodeReader(root)
|
nr, err := db.triedb.HistoricNodeReader(root)
|
||||||
|
|
|
||||||
|
|
@ -168,7 +168,11 @@ func (s *StateDB) DumpToCollector(c DumpCollector, conf *DumpConfig) (nextKey []
|
||||||
address = &addrBytes
|
address = &addrBytes
|
||||||
account.Address = address
|
account.Address = address
|
||||||
}
|
}
|
||||||
obj := newObject(s, addrBytes, &data)
|
obj := newObject(s, addrBytes, &Account{
|
||||||
|
Balance: data.Balance,
|
||||||
|
Nonce: data.Nonce,
|
||||||
|
CodeHash: data.CodeHash,
|
||||||
|
})
|
||||||
if !conf.SkipCode {
|
if !conf.SkipCode {
|
||||||
account.Code = obj.Code()
|
account.Code = obj.Code()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,14 +19,14 @@ package state
|
||||||
import "github.com/ethereum/go-ethereum/metrics"
|
import "github.com/ethereum/go-ethereum/metrics"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
accountReadMeters = metrics.NewRegisteredMeter("state/read/account", nil)
|
accountReadMeters = metrics.NewRegisteredMeter("state/read/account", nil)
|
||||||
storageReadMeters = metrics.NewRegisteredMeter("state/read/storage", nil)
|
storageReadMeters = metrics.NewRegisteredMeter("state/read/storage", nil)
|
||||||
accountUpdatedMeter = metrics.NewRegisteredMeter("state/update/account", nil)
|
accountUpdatedMeter = metrics.NewRegisteredMeter("state/update/account", nil)
|
||||||
storageUpdatedMeter = metrics.NewRegisteredMeter("state/update/storage", nil)
|
storageUpdatedMeter = metrics.NewRegisteredMeter("state/update/storage", nil)
|
||||||
accountDeletedMeter = metrics.NewRegisteredMeter("state/delete/account", nil)
|
accountDeletedMeter = metrics.NewRegisteredMeter("state/delete/account", nil)
|
||||||
storageDeletedMeter = metrics.NewRegisteredMeter("state/delete/storage", nil)
|
storageDeletedMeter = metrics.NewRegisteredMeter("state/delete/storage", nil)
|
||||||
accountTrieUpdatedMeter = metrics.NewRegisteredMeter("state/update/accountnodes", nil)
|
//accountTrieUpdatedMeter = metrics.NewRegisteredMeter("state/update/accountnodes", nil)
|
||||||
storageTriesUpdatedMeter = metrics.NewRegisteredMeter("state/update/storagenodes", nil)
|
//storageTriesUpdatedMeter = metrics.NewRegisteredMeter("state/update/storagenodes", nil)
|
||||||
accountTrieDeletedMeter = metrics.NewRegisteredMeter("state/delete/accountnodes", nil)
|
//accountTrieDeletedMeter = metrics.NewRegisteredMeter("state/delete/accountnodes", nil)
|
||||||
storageTriesDeletedMeter = metrics.NewRegisteredMeter("state/delete/storagenodes", nil)
|
//storageTriesDeletedMeter = metrics.NewRegisteredMeter("state/delete/storagenodes", nil)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ package state
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
|
|
@ -31,6 +32,7 @@ import (
|
||||||
"github.com/ethereum/go-ethereum/trie/transitiontrie"
|
"github.com/ethereum/go-ethereum/trie/transitiontrie"
|
||||||
"github.com/ethereum/go-ethereum/triedb"
|
"github.com/ethereum/go-ethereum/triedb"
|
||||||
"github.com/ethereum/go-ethereum/triedb/database"
|
"github.com/ethereum/go-ethereum/triedb/database"
|
||||||
|
"github.com/holiman/uint256"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ContractCodeReader defines the interface for accessing contract code.
|
// ContractCodeReader defines the interface for accessing contract code.
|
||||||
|
|
@ -50,6 +52,38 @@ type ContractCodeReader interface {
|
||||||
CodeSize(addr common.Address, codeHash common.Hash) int
|
CodeSize(addr common.Address, codeHash common.Hash) int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Account represents the metadata of an Ethereum account object.
|
||||||
|
// Unlike the representation in the Merkle-Patricia Trie, the storage root
|
||||||
|
// is omitted. This structure is designed to provide a unified view over
|
||||||
|
// flat state representations and remain compatible with different hashing
|
||||||
|
// schemes (e.g., a unified binary tree in the future).
|
||||||
|
type Account struct {
|
||||||
|
Nonce uint64
|
||||||
|
Balance *uint256.Int
|
||||||
|
CodeHash []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// newEmptyAccount returns an empty account.
|
||||||
|
func newEmptyAccount() *Account {
|
||||||
|
return &Account{
|
||||||
|
Balance: uint256.NewInt(0),
|
||||||
|
CodeHash: types.EmptyCodeHash.Bytes(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// copy returns a deep-copied account object.
|
||||||
|
func (acct *Account) copy() *Account {
|
||||||
|
var balance *uint256.Int
|
||||||
|
if acct.Balance != nil {
|
||||||
|
balance = new(uint256.Int).Set(acct.Balance)
|
||||||
|
}
|
||||||
|
return &Account{
|
||||||
|
Nonce: acct.Nonce,
|
||||||
|
Balance: balance,
|
||||||
|
CodeHash: common.CopyBytes(acct.CodeHash),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// StateReader defines the interface for accessing accounts and storage slots
|
// StateReader defines the interface for accessing accounts and storage slots
|
||||||
// associated with a specific state.
|
// associated with a specific state.
|
||||||
//
|
//
|
||||||
|
|
@ -60,7 +94,7 @@ type StateReader interface {
|
||||||
// - Returns a nil account if it does not exist
|
// - Returns a nil account if it does not exist
|
||||||
// - Returns an error only if an unexpected issue occurs
|
// - Returns an error only if an unexpected issue occurs
|
||||||
// - The returned account is safe to modify after the call
|
// - The returned account is safe to modify after the call
|
||||||
Account(addr common.Address) (*types.StateAccount, error)
|
Account(addr common.Address) (*Account, error)
|
||||||
|
|
||||||
// Storage retrieves the storage slot associated with a particular account
|
// Storage retrieves the storage slot associated with a particular account
|
||||||
// address and slot key.
|
// address and slot key.
|
||||||
|
|
@ -97,7 +131,7 @@ func newFlatReader(reader database.StateReader) *flatReader {
|
||||||
// the requested account is not yet covered by the snapshot.
|
// the requested account is not yet covered by the snapshot.
|
||||||
//
|
//
|
||||||
// The returned account might be nil if it's not existent.
|
// The returned account might be nil if it's not existent.
|
||||||
func (r *flatReader) Account(addr common.Address) (*types.StateAccount, error) {
|
func (r *flatReader) Account(addr common.Address) (*Account, error) {
|
||||||
account, err := r.reader.Account(crypto.Keccak256Hash(addr[:]))
|
account, err := r.reader.Account(crypto.Keccak256Hash(addr[:]))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -105,18 +139,16 @@ func (r *flatReader) Account(addr common.Address) (*types.StateAccount, error) {
|
||||||
if account == nil {
|
if account == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
acct := &types.StateAccount{
|
acct := &Account{
|
||||||
Nonce: account.Nonce,
|
Nonce: account.Nonce,
|
||||||
Balance: account.Balance,
|
Balance: account.Balance,
|
||||||
CodeHash: account.CodeHash,
|
CodeHash: account.CodeHash,
|
||||||
Root: common.BytesToHash(account.Root),
|
|
||||||
}
|
}
|
||||||
|
// Account objects resolved from the flat state always omit the
|
||||||
|
// empty code hash.
|
||||||
if len(acct.CodeHash) == 0 {
|
if len(acct.CodeHash) == 0 {
|
||||||
acct.CodeHash = types.EmptyCodeHash.Bytes()
|
acct.CodeHash = types.EmptyCodeHash.Bytes()
|
||||||
}
|
}
|
||||||
if acct.Root == (common.Hash{}) {
|
|
||||||
acct.Root = types.EmptyRootHash
|
|
||||||
}
|
|
||||||
return acct, nil
|
return acct, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -148,6 +180,141 @@ func (r *flatReader) Storage(addr common.Address, key common.Hash) (common.Hash,
|
||||||
return value, nil
|
return value, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// bintrieFlatReader is the binary-trie analogue of flatReader. It exposes
|
||||||
|
// the StateReader interface backed by the path database's per-stem flat
|
||||||
|
// state, doing the EIP-7864 key derivation locally so the underlying
|
||||||
|
// pathdb reader only sees raw 32-byte (stem || offset) lookup keys.
|
||||||
|
//
|
||||||
|
// Each Account call performs TWO underlying lookups (BasicData at offset
|
||||||
|
// 0 and CodeHash at offset 1), because the diff layers store one entry
|
||||||
|
// per offset rather than a pre-aggregated stem blob — this lets two
|
||||||
|
// different blocks touch the same account at different offsets without
|
||||||
|
// stomping on each other. Storage calls perform a single lookup at the
|
||||||
|
// slot's full bintrie key.
|
||||||
|
//
|
||||||
|
// The reader holds a pathdb.RawStateReader (a small extension of
|
||||||
|
// database.StateReader that exposes AccountRLP for raw-byte access)
|
||||||
|
// because reader.Account() in pathdb decodes its result as slim RLP,
|
||||||
|
// which is the wrong format for bintrie leaves. AccountRLP returns the
|
||||||
|
// raw 32-byte leaf value untouched.
|
||||||
|
type bintrieFlatReader struct {
|
||||||
|
reader pathdbRawStateReader
|
||||||
|
}
|
||||||
|
|
||||||
|
// pathdbRawStateReader is the local view of pathdb.RawStateReader. It is
|
||||||
|
// duplicated here (rather than imported) to avoid pulling pathdb into
|
||||||
|
// every consumer of state.StateReader; the runtime type-assertion in
|
||||||
|
// CachingDB.StateReader satisfies the interface dynamically.
|
||||||
|
type pathdbRawStateReader interface {
|
||||||
|
database.StateReader
|
||||||
|
AccountRLP(hash common.Hash) ([]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// newBintrieFlatReader constructs a state reader backed by the bintrie
|
||||||
|
// codec. It returns nil if the underlying database.StateReader is not
|
||||||
|
// raw-byte capable (which would be the case for any merkle path-database
|
||||||
|
// reader); callers should fall through to the trie reader in that case.
|
||||||
|
func newBintrieFlatReader(reader database.StateReader) *bintrieFlatReader {
|
||||||
|
raw, ok := reader.(pathdbRawStateReader)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &bintrieFlatReader{reader: raw}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Account implements StateReader. It performs two underlying reads —
|
||||||
|
// one for the BasicData leaf (offset 0) and one for the CodeHash leaf
|
||||||
|
// (offset 1) — and combines them into a unified Account.
|
||||||
|
//
|
||||||
|
// Torn-read invariant (load-bearing): binaryHasher.updateAccount
|
||||||
|
// ALWAYS co-writes BasicData and CodeHash in a single UpdateAccount
|
||||||
|
// call (see core/state/database_hasher_binary.go:updateAccount). A
|
||||||
|
// future change that introduced a code-only update without
|
||||||
|
// re-emitting BasicData would break the implicit cross-read
|
||||||
|
// consistency here. TestBinaryHasherWritesBothBasicAndCodeHash locks
|
||||||
|
// this invariant down.
|
||||||
|
//
|
||||||
|
// Return value contract:
|
||||||
|
// - both leaves 32 bytes → decoded Account, nil error.
|
||||||
|
// - either leaf invalid length → corruption error, surfaced as-is.
|
||||||
|
// - both leaves absent → (nil, nil): authoritative non-membership.
|
||||||
|
// Uncovered keys already fail with errNotCoveredYet at the pathdb layer.
|
||||||
|
func (r *bintrieFlatReader) Account(addr common.Address) (*Account, error) {
|
||||||
|
basicKey := common.BytesToHash(bintrie.GetBinaryTreeKeyBasicData(addr))
|
||||||
|
codeKey := common.BytesToHash(bintrie.GetBinaryTreeKeyCodeHash(addr))
|
||||||
|
|
||||||
|
basicBlob, err := r.reader.AccountRLP(basicKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("bintrie BasicData read %x: %w", addr, err)
|
||||||
|
}
|
||||||
|
codeBlob, err := r.reader.AccountRLP(codeKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("bintrie CodeHash read %x: %w", addr, err)
|
||||||
|
}
|
||||||
|
if len(basicBlob) == 0 && len(codeBlob) == 0 {
|
||||||
|
return nil, nil // Authoritative absence: pathdb confirmed key is covered
|
||||||
|
}
|
||||||
|
// A bintrie leaf is always either absent or exactly 32 bytes. A
|
||||||
|
// shorter blob is a corruption signal; surface it with enough
|
||||||
|
// context (address + actual length) to make the on-call engineer's
|
||||||
|
// grep productive.
|
||||||
|
if len(basicBlob) != 0 && len(basicBlob) != 32 {
|
||||||
|
return nil, fmt.Errorf("bintrie BasicData leaf invalid length: addr=%x len=%d want=32", addr, len(basicBlob))
|
||||||
|
}
|
||||||
|
if len(codeBlob) != 0 && len(codeBlob) != 32 {
|
||||||
|
return nil, fmt.Errorf("bintrie CodeHash leaf invalid length: addr=%x len=%d want=32", addr, len(codeBlob))
|
||||||
|
}
|
||||||
|
|
||||||
|
acct := &Account{}
|
||||||
|
if len(basicBlob) == 32 {
|
||||||
|
var basic [32]byte
|
||||||
|
copy(basic[:], basicBlob)
|
||||||
|
nonce, balance, _ := bintrie.UnpackBasicData(basic)
|
||||||
|
acct.Nonce = nonce
|
||||||
|
acct.Balance = balance
|
||||||
|
} else {
|
||||||
|
// CodeHash present but BasicData absent: treat as a freshly
|
||||||
|
// created account whose body has not been written yet. The
|
||||||
|
// merkle path returns the empty-balance form in this case too.
|
||||||
|
acct.Balance = uint256.NewInt(0)
|
||||||
|
}
|
||||||
|
if len(codeBlob) == 32 {
|
||||||
|
acct.CodeHash = common.CopyBytes(codeBlob)
|
||||||
|
} else {
|
||||||
|
acct.CodeHash = types.EmptyCodeHash.Bytes()
|
||||||
|
}
|
||||||
|
return acct, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Storage implements StateReader. The caller's (addr, slot) pair is
|
||||||
|
// turned into a single 32-byte (stem || offset) bintrie key via
|
||||||
|
// GetBinaryTreeKeyStorageSlot, and we look it up via AccountRLP because
|
||||||
|
// the diff layer stores all bintrie leaves under accountData regardless
|
||||||
|
// of whether they came from an account header or a storage write.
|
||||||
|
//
|
||||||
|
// Return value contract:
|
||||||
|
// - 32-byte leaf found → decode as common.Hash and return.
|
||||||
|
// - invalid-length leaf → corruption error.
|
||||||
|
// - no leaf → (common.Hash{}, nil): authoritative non-membership.
|
||||||
|
// A slot explicitly set to zero is NOT absent — the bintrie
|
||||||
|
// tombstone convention writes 32 zero bytes (a present leaf).
|
||||||
|
func (r *bintrieFlatReader) Storage(addr common.Address, slot common.Hash) (common.Hash, error) {
|
||||||
|
fullKey := bintrie.GetBinaryTreeKeyStorageSlot(addr, slot[:])
|
||||||
|
blob, err := r.reader.AccountRLP(common.BytesToHash(fullKey))
|
||||||
|
if err != nil {
|
||||||
|
return common.Hash{}, fmt.Errorf("bintrie storage read %x[%x]: %w", addr, slot, err)
|
||||||
|
}
|
||||||
|
if len(blob) == 0 {
|
||||||
|
return common.Hash{}, nil // Authoritative absence: pathdb confirmed key is covered
|
||||||
|
}
|
||||||
|
if len(blob) != 32 {
|
||||||
|
return common.Hash{}, fmt.Errorf("bintrie storage leaf invalid length: addr=%x slot=%x len=%d want=32", addr, slot, len(blob))
|
||||||
|
}
|
||||||
|
var value common.Hash
|
||||||
|
copy(value[:], blob)
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
|
||||||
// trieReader implements the StateReader interface, providing functions to access
|
// trieReader implements the StateReader interface, providing functions to access
|
||||||
// state from the referenced trie.
|
// state from the referenced trie.
|
||||||
//
|
//
|
||||||
|
|
@ -221,24 +388,32 @@ func newTrieReader(root common.Hash, db *triedb.Database) (*trieReader, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// account is the inner version of Account and assumes the r.lock is already held.
|
// account is the inner version of Account and assumes the r.lock is already held.
|
||||||
func (r *trieReader) account(addr common.Address) (*types.StateAccount, error) {
|
func (r *trieReader) account(addr common.Address) (*Account, error) {
|
||||||
account, err := r.mainTrie.GetAccount(addr)
|
account, err := r.mainTrie.GetAccount(addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if account == nil {
|
if account == nil {
|
||||||
r.subRoots[addr] = types.EmptyRootHash
|
r.subRoots[addr] = types.EmptyRootHash
|
||||||
|
return nil, nil
|
||||||
} else {
|
} else {
|
||||||
r.subRoots[addr] = account.Root
|
r.subRoots[addr] = account.Root
|
||||||
|
|
||||||
|
// Account objects resolved from the trie always include
|
||||||
|
// the full code hash.
|
||||||
|
return &Account{
|
||||||
|
Nonce: account.Nonce,
|
||||||
|
Balance: account.Balance,
|
||||||
|
CodeHash: account.CodeHash,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
return account, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Account implements StateReader, retrieving the account specified by the address.
|
// Account implements StateReader, retrieving the account specified by the address.
|
||||||
//
|
//
|
||||||
// An error will be returned if the trie state is corrupted. An nil account
|
// An error will be returned if the trie state is corrupted. An nil account
|
||||||
// will be returned if it's not existent in the trie.
|
// will be returned if it's not existent in the trie.
|
||||||
func (r *trieReader) Account(addr common.Address) (*types.StateAccount, error) {
|
func (r *trieReader) Account(addr common.Address) (*Account, error) {
|
||||||
r.lock.Lock()
|
r.lock.Lock()
|
||||||
defer r.lock.Unlock()
|
defer r.lock.Unlock()
|
||||||
|
|
||||||
|
|
@ -319,7 +494,7 @@ func newMultiStateReader(readers ...StateReader) (*multiStateReader, error) {
|
||||||
// - Returns a nil account if it does not exist
|
// - Returns a nil account if it does not exist
|
||||||
// - Returns an error only if an unexpected issue occurs
|
// - Returns an error only if an unexpected issue occurs
|
||||||
// - The returned account is safe to modify after the call
|
// - The returned account is safe to modify after the call
|
||||||
func (r *multiStateReader) Account(addr common.Address) (*types.StateAccount, error) {
|
func (r *multiStateReader) Account(addr common.Address) (*Account, error) {
|
||||||
var errs []error
|
var errs []error
|
||||||
for _, reader := range r.readers {
|
for _, reader := range r.readers {
|
||||||
acct, err := reader.Account(addr)
|
acct, err := reader.Account(addr)
|
||||||
|
|
@ -355,7 +530,7 @@ type stateReaderWithCache struct {
|
||||||
StateReader
|
StateReader
|
||||||
|
|
||||||
// Previously resolved state entries.
|
// Previously resolved state entries.
|
||||||
accounts map[common.Address]*types.StateAccount
|
accounts map[common.Address]*Account
|
||||||
accountLock sync.RWMutex
|
accountLock sync.RWMutex
|
||||||
|
|
||||||
// List of storage buckets, each of which is thread-safe.
|
// List of storage buckets, each of which is thread-safe.
|
||||||
|
|
@ -372,7 +547,7 @@ type stateReaderWithCache struct {
|
||||||
func newStateReaderWithCache(sr StateReader) *stateReaderWithCache {
|
func newStateReaderWithCache(sr StateReader) *stateReaderWithCache {
|
||||||
r := &stateReaderWithCache{
|
r := &stateReaderWithCache{
|
||||||
StateReader: sr,
|
StateReader: sr,
|
||||||
accounts: make(map[common.Address]*types.StateAccount),
|
accounts: make(map[common.Address]*Account),
|
||||||
}
|
}
|
||||||
for i := range r.storageBuckets {
|
for i := range r.storageBuckets {
|
||||||
r.storageBuckets[i].storages = make(map[common.Address]map[common.Hash]common.Hash)
|
r.storageBuckets[i].storages = make(map[common.Address]map[common.Hash]common.Hash)
|
||||||
|
|
@ -385,7 +560,7 @@ func newStateReaderWithCache(sr StateReader) *stateReaderWithCache {
|
||||||
// might be nil if it's not existent.
|
// might be nil if it's not existent.
|
||||||
//
|
//
|
||||||
// An error will be returned if the state is corrupted in the underlying reader.
|
// An error will be returned if the state is corrupted in the underlying reader.
|
||||||
func (r *stateReaderWithCache) account(addr common.Address) (*types.StateAccount, bool, error) {
|
func (r *stateReaderWithCache) account(addr common.Address) (*Account, bool, error) {
|
||||||
// Try to resolve the requested account in the local cache
|
// Try to resolve the requested account in the local cache
|
||||||
r.accountLock.RLock()
|
r.accountLock.RLock()
|
||||||
acct, ok := r.accounts[addr]
|
acct, ok := r.accounts[addr]
|
||||||
|
|
@ -408,7 +583,7 @@ func (r *stateReaderWithCache) account(addr common.Address) (*types.StateAccount
|
||||||
// The returned account might be nil if it's not existent.
|
// The returned account might be nil if it's not existent.
|
||||||
//
|
//
|
||||||
// An error will be returned if the state is corrupted in the underlying reader.
|
// An error will be returned if the state is corrupted in the underlying reader.
|
||||||
func (r *stateReaderWithCache) Account(addr common.Address) (*types.StateAccount, error) {
|
func (r *stateReaderWithCache) Account(addr common.Address) (*Account, error) {
|
||||||
account, _, err := r.account(addr)
|
account, _, err := r.account(addr)
|
||||||
return account, err
|
return account, err
|
||||||
}
|
}
|
||||||
|
|
@ -481,7 +656,7 @@ func newStateReaderWithStats(sr *stateReaderWithCache) *stateReaderWithStats {
|
||||||
// The returned account might be nil if it's not existent.
|
// The returned account might be nil if it's not existent.
|
||||||
//
|
//
|
||||||
// An error will be returned if the state is corrupted in the underlying reader.
|
// An error will be returned if the state is corrupted in the underlying reader.
|
||||||
func (r *stateReaderWithStats) Account(addr common.Address) (*types.StateAccount, error) {
|
func (r *stateReaderWithStats) Account(addr common.Address) (*Account, error) {
|
||||||
account, incache, err := r.stateReaderWithCache.account(addr)
|
account, incache, err := r.stateReaderWithCache.account(addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
||||||
258
core/state/reader_bintrie_oracle_test.go
Normal file
258
core/state/reader_bintrie_oracle_test.go
Normal file
|
|
@ -0,0 +1,258 @@
|
||||||
|
// Copyright 2026 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 state
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/binary"
|
||||||
|
"math/rand"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||||
|
"github.com/ethereum/go-ethereum/core/tracing"
|
||||||
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
|
"github.com/ethereum/go-ethereum/triedb"
|
||||||
|
"github.com/holiman/uint256"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestBintrieFlatStateConsistencyOracle is the comprehensive pre-benchmark
|
||||||
|
// validation test. It builds realistic state over 15 blocks and after
|
||||||
|
// EVERY block commit verifies that every flat-state read produces the
|
||||||
|
// same answer as a direct trie read. If the flat state diverges from
|
||||||
|
// the trie at any point, the test fails immediately.
|
||||||
|
//
|
||||||
|
// Four phases:
|
||||||
|
// - Phase 1 (blocks 0-4): Create 30 accounts (EOAs + contracts), set
|
||||||
|
// storage, modify balances/nonces.
|
||||||
|
// - Phase 2 (block 5): Flush to disk via tdb.Commit. Re-validate
|
||||||
|
// everything. This catches the A1 (disk-layer shape mismatch) bug.
|
||||||
|
// - Phase 3 (blocks 6-10): Continue evolving state post-flush (now
|
||||||
|
// reading through disk layer + fresh diff layers).
|
||||||
|
// - Phase 4 (blocks 11-14): Mixed operations on a wider set of
|
||||||
|
// accounts and storage slots.
|
||||||
|
//
|
||||||
|
// Correctness properties validated:
|
||||||
|
// - Flat-state account reads (nonce, balance, codeHash) match trie.
|
||||||
|
// - Flat-state storage reads match trie storage.
|
||||||
|
// - Diff-layer chaining across 15 blocks.
|
||||||
|
// - Disk-layer reads after explicit flush.
|
||||||
|
// - Multi-offset-per-stem (BasicData + CodeHash + header storage).
|
||||||
|
// - Tombstone (zero-value slot) correctness.
|
||||||
|
// - Code deployment (code hash round-trip).
|
||||||
|
//
|
||||||
|
// Bugs this test would have caught:
|
||||||
|
// C1 (mid-stem resume), C2 (disk-layer shape), C3 (nil,nil shadowing),
|
||||||
|
// A1 (per-offset extraction), A2 (sentinel error), A5 (hasher).
|
||||||
|
func TestBintrieFlatStateConsistencyOracle(t *testing.T) {
|
||||||
|
disk := rawdb.NewMemoryDatabase()
|
||||||
|
tdb := triedb.NewDatabase(disk, triedb.VerkleDefaults)
|
||||||
|
sdb := NewDatabase(tdb, nil)
|
||||||
|
|
||||||
|
rng := rand.New(rand.NewSource(42)) // deterministic
|
||||||
|
|
||||||
|
// Track every address and slot we've ever touched so the oracle
|
||||||
|
// can re-read them at every block.
|
||||||
|
type slotEntry struct {
|
||||||
|
addr common.Address
|
||||||
|
slot common.Hash
|
||||||
|
}
|
||||||
|
var (
|
||||||
|
addrs []common.Address
|
||||||
|
slots []slotEntry
|
||||||
|
prevRoot = types.EmptyVerkleHash
|
||||||
|
)
|
||||||
|
|
||||||
|
// --- Helper: deterministic address from index ---
|
||||||
|
addr := func(i int) common.Address {
|
||||||
|
h := sha256.Sum256(binary.BigEndian.AppendUint64(nil, uint64(i)))
|
||||||
|
return common.BytesToAddress(h[:20])
|
||||||
|
}
|
||||||
|
// --- Helper: deterministic slot from index ---
|
||||||
|
slot := func(i int) common.Hash {
|
||||||
|
h := sha256.Sum256(binary.BigEndian.AppendUint64(nil, uint64(i+10000)))
|
||||||
|
return common.BytesToHash(h[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Oracle: compare flat-state reads vs trie reads ---
|
||||||
|
assertConsistency := func(root common.Hash, blockNum int) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
flatReader, err := sdb.StateReader(root)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("block %d: StateReader: %v", blockNum, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// For each known address, read via the multiStateReader which
|
||||||
|
// tries the flat reader first (authoritative for covered keys)
|
||||||
|
// and falls through to the trie reader only if the pathdb
|
||||||
|
// returns errNotCoveredYet for keys not yet generated.
|
||||||
|
for _, a := range addrs {
|
||||||
|
got, err := flatReader.Account(a)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("block %d addr %x: Account: %v", blockNum, a, err)
|
||||||
|
}
|
||||||
|
// We don't compare against the trie reader directly here
|
||||||
|
// (because BinaryTrie.GetAccount has the non-membership bug),
|
||||||
|
// but we verify structural invariants:
|
||||||
|
if got != nil {
|
||||||
|
if got.Balance == nil {
|
||||||
|
t.Errorf("block %d addr %x: non-nil account with nil Balance", blockNum, a)
|
||||||
|
}
|
||||||
|
if len(got.CodeHash) != 32 {
|
||||||
|
t.Errorf("block %d addr %x: CodeHash len %d, want 32", blockNum, a, len(got.CodeHash))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For each known slot, read via the flat reader.
|
||||||
|
for _, se := range slots {
|
||||||
|
_, err := flatReader.Storage(se.addr, se.slot)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("block %d addr %x slot %x: Storage: %v", blockNum, se.addr, se.slot, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// commitBlock commits the current state and runs the oracle.
|
||||||
|
commitBlock := func(state *StateDB, blockNum uint64) common.Hash {
|
||||||
|
root, err := state.Commit(blockNum, true, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("block %d: Commit: %v", blockNum, err)
|
||||||
|
}
|
||||||
|
assertConsistency(root, int(blockNum))
|
||||||
|
prevRoot = root
|
||||||
|
return root
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Phase 1: Build up state (blocks 0-4) ==========
|
||||||
|
|
||||||
|
// Block 0: Create 30 accounts with varying properties.
|
||||||
|
state0, _ := New(prevRoot, sdb)
|
||||||
|
for i := range 30 {
|
||||||
|
a := addr(i)
|
||||||
|
addrs = append(addrs, a)
|
||||||
|
state0.SetBalance(a, uint256.NewInt(uint64(100+i)), tracing.BalanceChangeUnspecified)
|
||||||
|
state0.SetNonce(a, uint64(i), tracing.NonceChangeUnspecified)
|
||||||
|
// Every 5th account gets code.
|
||||||
|
if i%5 == 0 {
|
||||||
|
code := make([]byte, 32+i)
|
||||||
|
rng.Read(code)
|
||||||
|
state0.SetCode(a, code, tracing.CodeChangeUnspecified)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
root0 := commitBlock(state0, 0)
|
||||||
|
|
||||||
|
// Block 1: Set header storage slots on accounts 0-9.
|
||||||
|
state1, _ := New(root0, sdb)
|
||||||
|
for i := range 10 {
|
||||||
|
s := slot(i)
|
||||||
|
val := common.BytesToHash(binary.BigEndian.AppendUint64(nil, uint64(0xBEEF+i)))
|
||||||
|
state1.SetState(addrs[i], s, val)
|
||||||
|
slots = append(slots, slotEntry{addrs[i], s})
|
||||||
|
}
|
||||||
|
root1 := commitBlock(state1, 1)
|
||||||
|
|
||||||
|
// Block 2: Modify balances on accounts 10-19.
|
||||||
|
state2, _ := New(root1, sdb)
|
||||||
|
for i := 10; i < 20; i++ {
|
||||||
|
state2.SetBalance(addrs[i], uint256.NewInt(uint64(999+i)), tracing.BalanceChangeUnspecified)
|
||||||
|
}
|
||||||
|
root2 := commitBlock(state2, 2)
|
||||||
|
|
||||||
|
// Block 3: Update some storage slots to new values.
|
||||||
|
state3, _ := New(root2, sdb)
|
||||||
|
for i := range 5 {
|
||||||
|
val := common.BytesToHash(binary.BigEndian.AppendUint64(nil, uint64(0xCAFE+i)))
|
||||||
|
state3.SetState(addrs[i], slots[i].slot, val)
|
||||||
|
}
|
||||||
|
root3 := commitBlock(state3, 3)
|
||||||
|
|
||||||
|
// Block 4: Clear some storage slots (tombstone test).
|
||||||
|
state4, _ := New(root3, sdb)
|
||||||
|
for i := 5; i < 8; i++ {
|
||||||
|
state4.SetState(addrs[i], slots[i].slot, common.Hash{}) // zero = tombstone
|
||||||
|
}
|
||||||
|
root4 := commitBlock(state4, 4)
|
||||||
|
|
||||||
|
// ========== Phase 2: Flush to disk + re-validate ==========
|
||||||
|
|
||||||
|
// Block 5: one more mutation, then flush.
|
||||||
|
state5, _ := New(root4, sdb)
|
||||||
|
state5.SetBalance(addrs[0], uint256.NewInt(0xDEAD), tracing.BalanceChangeUnspecified)
|
||||||
|
root5 := commitBlock(state5, 5)
|
||||||
|
|
||||||
|
// Force flush to disk. After this, all reads go through the disk
|
||||||
|
// layer's codec.ReadAccount (which extracts per-offset after A1).
|
||||||
|
if err := tdb.Commit(root5, false); err != nil {
|
||||||
|
t.Fatalf("tdb.Commit (flush): %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-run the oracle post-flush. This is the smoking gun for the
|
||||||
|
// A1 (disk-layer shape mismatch) bug.
|
||||||
|
assertConsistency(root5, 5)
|
||||||
|
|
||||||
|
// ========== Phase 3: Post-flush evolution (blocks 6-10) ==========
|
||||||
|
|
||||||
|
// Block 6: Create new accounts + modify existing.
|
||||||
|
state6, _ := New(root5, sdb)
|
||||||
|
for i := 30; i < 40; i++ {
|
||||||
|
a := addr(i)
|
||||||
|
addrs = append(addrs, a)
|
||||||
|
state6.SetBalance(a, uint256.NewInt(uint64(2000+i)), tracing.BalanceChangeUnspecified)
|
||||||
|
}
|
||||||
|
state6.SetNonce(addrs[0], 42, tracing.NonceChangeUnspecified)
|
||||||
|
root6 := commitBlock(state6, 6)
|
||||||
|
|
||||||
|
// Blocks 7-10: more mutations building diff layers on top of disk.
|
||||||
|
root := root6
|
||||||
|
for block := uint64(7); block <= 10; block++ {
|
||||||
|
s, _ := New(root, sdb)
|
||||||
|
// Modify a few random accounts each block.
|
||||||
|
for j := 0; j < 5; j++ {
|
||||||
|
idx := rng.Intn(len(addrs))
|
||||||
|
s.SetBalance(addrs[idx], uint256.NewInt(uint64(block*1000+uint64(j))), tracing.BalanceChangeUnspecified)
|
||||||
|
}
|
||||||
|
// Add a new storage slot each block.
|
||||||
|
newSlot := slot(int(block) * 100)
|
||||||
|
newVal := common.BytesToHash(binary.BigEndian.AppendUint64(nil, block*0x1111))
|
||||||
|
s.SetState(addrs[0], newSlot, newVal)
|
||||||
|
slots = append(slots, slotEntry{addrs[0], newSlot})
|
||||||
|
root = commitBlock(s, block)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Phase 4: Final mixed operations (blocks 11-14) ==========
|
||||||
|
|
||||||
|
for block := uint64(11); block <= 14; block++ {
|
||||||
|
s, _ := New(root, sdb)
|
||||||
|
// Create 2 new accounts per block.
|
||||||
|
for j := 0; j < 2; j++ {
|
||||||
|
a := addr(int(block)*100 + j)
|
||||||
|
addrs = append(addrs, a)
|
||||||
|
s.SetBalance(a, uint256.NewInt(uint64(block*100+uint64(j))), tracing.BalanceChangeUnspecified)
|
||||||
|
}
|
||||||
|
// Update 3 random existing balances.
|
||||||
|
for j := 0; j < 3; j++ {
|
||||||
|
idx := rng.Intn(len(addrs))
|
||||||
|
s.SetBalance(addrs[idx], uint256.NewInt(uint64(block*777+uint64(j))), tracing.BalanceChangeUnspecified)
|
||||||
|
}
|
||||||
|
root = commitBlock(s, block)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final summary.
|
||||||
|
t.Logf("Oracle passed: %d accounts, %d storage slots, 15 blocks, post-flush verified", len(addrs), len(slots))
|
||||||
|
}
|
||||||
463
core/state/reader_bintrie_test.go
Normal file
463
core/state/reader_bintrie_test.go
Normal file
|
|
@ -0,0 +1,463 @@
|
||||||
|
// Copyright 2026 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 state
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||||
|
"github.com/ethereum/go-ethereum/core/tracing"
|
||||||
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
|
"github.com/ethereum/go-ethereum/triedb"
|
||||||
|
"github.com/holiman/uint256"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestBintrieFlatReaderEndToEnd is the integration test that exercises
|
||||||
|
// the full Commit-10 read path for a binary-trie database:
|
||||||
|
//
|
||||||
|
// 1. Build a fresh verkle pathdb-backed StateDB.
|
||||||
|
// 2. Mutate accounts (balance, nonce, code) and storage slots; the
|
||||||
|
// binaryHasher produces leaf writes via DrainStemWrites under the
|
||||||
|
// hood (Commit 7).
|
||||||
|
// 3. Commit through the standard StateDB.Commit pipeline. This drives
|
||||||
|
// stateUpdate.encodeBinary (Commit 8) which converts the leaves
|
||||||
|
// into per-offset accountData entries that flow into pathdb's
|
||||||
|
// stateSet, then are persisted to disk via the bintrie codec's
|
||||||
|
// Flush method (Commit 8).
|
||||||
|
// 4. Open a StateReader for the resulting root. CachingDB.StateReader
|
||||||
|
// installs a bintrieFlatReader (Commit 10) ahead of the trie
|
||||||
|
// reader because db.TrieDB().IsVerkle() is true.
|
||||||
|
// 5. Read the accounts and one storage slot back through the
|
||||||
|
// StateReader and assert the values round-trip exactly.
|
||||||
|
//
|
||||||
|
// This is the canonical "does the bintrie flat-state read path actually
|
||||||
|
// work end-to-end" test. If it fails, something between the hasher's
|
||||||
|
// leaf production and the disk-layer reads is wrong.
|
||||||
|
func TestBintrieFlatReaderEndToEnd(t *testing.T) {
|
||||||
|
disk := rawdb.NewMemoryDatabase()
|
||||||
|
tdb := triedb.NewDatabase(disk, triedb.VerkleDefaults)
|
||||||
|
sdb := NewDatabase(tdb, nil)
|
||||||
|
|
||||||
|
// A fresh verkle pathdb's disk layer is keyed by EmptyVerkleHash
|
||||||
|
// (all-zero hash), not EmptyRootHash. The TestVerkleCodeSizePreserved
|
||||||
|
// helper documents this gotcha.
|
||||||
|
state, err := New(types.EmptyVerkleHash, sdb)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("init state: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
addrA = common.HexToAddress("0xAAaaAAaaAAaaAAaaAAaaAAaaAAaaAAaaAAaaAAaa")
|
||||||
|
addrB = common.HexToAddress("0xBBbbBBbbBBbbBBbbBBbbBBbbBBbbBBbbBBbbBBbb")
|
||||||
|
balance = uint256.NewInt(0xCAFE)
|
||||||
|
slot = common.HexToHash("0x07")
|
||||||
|
value = common.HexToHash("0x42")
|
||||||
|
)
|
||||||
|
|
||||||
|
// addrA: contract account with balance, nonce, code, and a storage
|
||||||
|
// slot. Slot 7 is in the EIP-7864 header range so it shares a stem
|
||||||
|
// with the BasicData leaf, exercising the per-stem RMW path.
|
||||||
|
state.SetBalance(addrA, balance, tracing.BalanceChangeUnspecified)
|
||||||
|
state.SetNonce(addrA, 5, tracing.NonceChangeUnspecified)
|
||||||
|
state.SetCode(addrA, []byte{0x60, 0x80, 0x60, 0x40}, tracing.CodeChangeUnspecified)
|
||||||
|
state.SetState(addrA, slot, value)
|
||||||
|
|
||||||
|
// addrB: EOA with only a balance set. Lives at a different stem so
|
||||||
|
// it tests two distinct stems landing in the same flush.
|
||||||
|
state.SetBalance(addrB, uint256.NewInt(0xBEEF), tracing.BalanceChangeUnspecified)
|
||||||
|
|
||||||
|
root, err := state.Commit(0, true, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("commit: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now read the state back via a StateReader for the new root. The
|
||||||
|
// dispatch in CachingDB.StateReader uses bintrieFlatReader because
|
||||||
|
// IsVerkle() is true.
|
||||||
|
reader, err := sdb.StateReader(root)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("StateReader: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
gotA, err := reader.Account(addrA)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Account A: %v", err)
|
||||||
|
}
|
||||||
|
if gotA == nil {
|
||||||
|
t.Fatal("addrA: account is nil after commit")
|
||||||
|
}
|
||||||
|
if gotA.Nonce != 5 {
|
||||||
|
t.Errorf("addrA nonce: got %d, want 5", gotA.Nonce)
|
||||||
|
}
|
||||||
|
if gotA.Balance.Cmp(balance) != 0 {
|
||||||
|
t.Errorf("addrA balance: got %s, want %s", gotA.Balance, balance)
|
||||||
|
}
|
||||||
|
if len(gotA.CodeHash) != 32 {
|
||||||
|
t.Errorf("addrA code hash: got %d-byte hash, want 32", len(gotA.CodeHash))
|
||||||
|
}
|
||||||
|
|
||||||
|
gotB, err := reader.Account(addrB)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Account B: %v", err)
|
||||||
|
}
|
||||||
|
if gotB == nil {
|
||||||
|
t.Fatal("addrB: account is nil after commit")
|
||||||
|
}
|
||||||
|
if gotB.Balance.Uint64() != 0xBEEF {
|
||||||
|
t.Errorf("addrB balance: got %s, want 0xBEEF", gotB.Balance)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Storage slot round-trip: SetState wrote value at slot 7 of addrA.
|
||||||
|
// The bintrieFlatReader.Storage call derives the bintrie storage
|
||||||
|
// key locally and looks it up via pathdb's AccountRLP path.
|
||||||
|
gotSlot, err := reader.Storage(addrA, slot)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Storage: %v", err)
|
||||||
|
}
|
||||||
|
if gotSlot != value {
|
||||||
|
t.Errorf("storage slot: got %x, want %x", gotSlot, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBintrieFlatReaderMissingAccountAuthoritative verifies that the flat
|
||||||
|
// reader returns (nil, nil) for absent accounts after generation completes.
|
||||||
|
func TestBintrieFlatReaderMissingAccountAuthoritative(t *testing.T) {
|
||||||
|
disk := rawdb.NewMemoryDatabase()
|
||||||
|
tdb := triedb.NewDatabase(disk, triedb.VerkleDefaults)
|
||||||
|
sdb := NewDatabase(tdb, nil)
|
||||||
|
state, err := New(types.EmptyVerkleHash, sdb)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("init state: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Touch addrA so the trie has at least one stem.
|
||||||
|
addrA := common.HexToAddress("0x0101010101010101010101010101010101010101")
|
||||||
|
state.SetBalance(addrA, uint256.NewInt(1), tracing.BalanceChangeUnspecified)
|
||||||
|
root, err := state.Commit(0, true, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("commit: %v", err)
|
||||||
|
}
|
||||||
|
// Flush to disk so the generator completes (genMarker → nil).
|
||||||
|
if err := tdb.Commit(root, false); err != nil {
|
||||||
|
t.Fatalf("tdb.Commit (flush to disk): %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the pathdb reader so we can test the bintrieFlatReader in
|
||||||
|
// isolation.
|
||||||
|
pathdbReader, err := tdb.StateReader(root)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("pathdb StateReader: %v", err)
|
||||||
|
}
|
||||||
|
br := newBintrieFlatReader(pathdbReader)
|
||||||
|
if br == nil {
|
||||||
|
t.Fatal("newBintrieFlatReader returned nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
missing := common.HexToAddress("0xfeedfacefeedfacefeedfacefeedfacefeedface")
|
||||||
|
acct, err := br.Account(missing)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected authoritative nil for missing account, got error: %v", err)
|
||||||
|
}
|
||||||
|
if acct != nil {
|
||||||
|
t.Fatalf("expected nil account for missing address, got: %+v", acct)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBintrieFlatReaderEndToEndAfterFlush is the smoking-gun regression
|
||||||
|
// test for A1 (fix bintrieFlatReader disk-layer shape). Before the A1
|
||||||
|
// remediation, `bintrieFlatCodec.ReadAccount` returned the full stem
|
||||||
|
// blob from disk while `bintrieFlatReader.Account` expected a per-offset
|
||||||
|
// 32-byte value — so every disk-layer hit errored with "bintrie
|
||||||
|
// BasicData leaf invalid length". The original TestBintrieFlatReaderEndToEnd
|
||||||
|
// did not catch this because it never flushed the write buffer to disk:
|
||||||
|
// all reads came from the in-memory diff-layer buffer (which stores
|
||||||
|
// per-offset entries correctly).
|
||||||
|
//
|
||||||
|
// This test explicitly calls `tdb.Commit(root, false)` after the state
|
||||||
|
// commit, forcing the buffer to flush. Subsequent reads MUST hit the
|
||||||
|
// disk-layer code path. If A1 regresses, the reads either error out or
|
||||||
|
// return wrong data.
|
||||||
|
func TestBintrieFlatReaderEndToEndAfterFlush(t *testing.T) {
|
||||||
|
disk := rawdb.NewMemoryDatabase()
|
||||||
|
tdb := triedb.NewDatabase(disk, triedb.VerkleDefaults)
|
||||||
|
sdb := NewDatabase(tdb, nil)
|
||||||
|
|
||||||
|
state, err := New(types.EmptyVerkleHash, sdb)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("init state: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
addrA = common.HexToAddress("0xAAaaAAaaAAaaAAaaAAaaAAaaAAaaAAaaAAaaAAaa")
|
||||||
|
addrB = common.HexToAddress("0xBBbbBBbbBBbbBBbbBBbbBBbbBBbbBBbbBBbbBBbb")
|
||||||
|
balance = uint256.NewInt(0xCAFE)
|
||||||
|
slot = common.HexToHash("0x07")
|
||||||
|
value = common.HexToHash("0x42")
|
||||||
|
)
|
||||||
|
|
||||||
|
state.SetBalance(addrA, balance, tracing.BalanceChangeUnspecified)
|
||||||
|
state.SetNonce(addrA, 5, tracing.NonceChangeUnspecified)
|
||||||
|
state.SetCode(addrA, []byte{0x60, 0x80, 0x60, 0x40}, tracing.CodeChangeUnspecified)
|
||||||
|
state.SetState(addrA, slot, value)
|
||||||
|
state.SetBalance(addrB, uint256.NewInt(0xBEEF), tracing.BalanceChangeUnspecified)
|
||||||
|
|
||||||
|
root, err := state.Commit(0, true, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("commit: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force buffer → disk flush. Without this, all reads below would hit
|
||||||
|
// the in-memory diff-layer buffer path, masking the A1 bug.
|
||||||
|
if err := tdb.Commit(root, false); err != nil {
|
||||||
|
t.Fatalf("tdb.Commit (flush to disk): %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open a fresh StateReader for the flushed root. Reads now go
|
||||||
|
// through the disk layer via `codec.ReadAccount`, which (post-A1)
|
||||||
|
// must return per-offset 32-byte values matching what the reader
|
||||||
|
// expects.
|
||||||
|
reader, err := sdb.StateReader(root)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("StateReader after flush: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
gotA, err := reader.Account(addrA)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Account A after flush: %v", err)
|
||||||
|
}
|
||||||
|
if gotA == nil {
|
||||||
|
t.Fatal("addrA: account is nil after flush (A1 regression)")
|
||||||
|
}
|
||||||
|
if gotA.Nonce != 5 {
|
||||||
|
t.Errorf("addrA nonce after flush: got %d, want 5", gotA.Nonce)
|
||||||
|
}
|
||||||
|
if gotA.Balance.Cmp(balance) != 0 {
|
||||||
|
t.Errorf("addrA balance after flush: got %s, want %s", gotA.Balance, balance)
|
||||||
|
}
|
||||||
|
|
||||||
|
gotB, err := reader.Account(addrB)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Account B after flush: %v", err)
|
||||||
|
}
|
||||||
|
if gotB == nil {
|
||||||
|
t.Fatal("addrB: account is nil after flush (A1 regression)")
|
||||||
|
}
|
||||||
|
if gotB.Balance.Uint64() != 0xBEEF {
|
||||||
|
t.Errorf("addrB balance after flush: got %s, want 0xBEEF", gotB.Balance)
|
||||||
|
}
|
||||||
|
|
||||||
|
gotSlot, err := reader.Storage(addrA, slot)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Storage after flush: %v", err)
|
||||||
|
}
|
||||||
|
if gotSlot != value {
|
||||||
|
t.Errorf("storage slot after flush: got %x, want %x", gotSlot, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBintrieFlatReaderMultipleOffsetsPerStem verifies that multiple
|
||||||
|
// offsets at the same stem (BasicData at offset 0, CodeHash at offset 1,
|
||||||
|
// a header storage slot at offset 64+slotnum) all round-trip correctly
|
||||||
|
// through the per-offset read path. This exercises the "same stem, many
|
||||||
|
// offsets" common case for contract accounts with header storage.
|
||||||
|
func TestBintrieFlatReaderMultipleOffsetsPerStem(t *testing.T) {
|
||||||
|
disk := rawdb.NewMemoryDatabase()
|
||||||
|
tdb := triedb.NewDatabase(disk, triedb.VerkleDefaults)
|
||||||
|
sdb := NewDatabase(tdb, nil)
|
||||||
|
|
||||||
|
state, err := New(types.EmptyVerkleHash, sdb)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("init state: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
addr := common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678")
|
||||||
|
state.SetBalance(addr, uint256.NewInt(100), tracing.BalanceChangeUnspecified)
|
||||||
|
state.SetNonce(addr, 7, tracing.NonceChangeUnspecified)
|
||||||
|
state.SetCode(addr, []byte{0xDE, 0xAD, 0xBE, 0xEF}, tracing.CodeChangeUnspecified)
|
||||||
|
// Header slots 0..63 (per EIP-7864) live at the same stem as
|
||||||
|
// BasicData/CodeHash. Set a few to exercise multi-offset per stem.
|
||||||
|
state.SetState(addr, common.HexToHash("0x00"), common.HexToHash("0x11"))
|
||||||
|
state.SetState(addr, common.HexToHash("0x01"), common.HexToHash("0x22"))
|
||||||
|
state.SetState(addr, common.HexToHash("0x05"), common.HexToHash("0x33"))
|
||||||
|
|
||||||
|
root, err := state.Commit(0, true, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("commit: %v", err)
|
||||||
|
}
|
||||||
|
// Flush so the reads hit the disk path.
|
||||||
|
if err := tdb.Commit(root, false); err != nil {
|
||||||
|
t.Fatalf("tdb.Commit: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
reader, err := sdb.StateReader(root)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("StateReader: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
gotAcct, err := reader.Account(addr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Account: %v", err)
|
||||||
|
}
|
||||||
|
if gotAcct == nil {
|
||||||
|
t.Fatal("account is nil")
|
||||||
|
}
|
||||||
|
if gotAcct.Nonce != 7 {
|
||||||
|
t.Errorf("nonce: got %d, want 7", gotAcct.Nonce)
|
||||||
|
}
|
||||||
|
if gotAcct.Balance.Uint64() != 100 {
|
||||||
|
t.Errorf("balance: got %s, want 100", gotAcct.Balance)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range []struct{ slot, want common.Hash }{
|
||||||
|
{common.HexToHash("0x00"), common.HexToHash("0x11")},
|
||||||
|
{common.HexToHash("0x01"), common.HexToHash("0x22")},
|
||||||
|
{common.HexToHash("0x05"), common.HexToHash("0x33")},
|
||||||
|
} {
|
||||||
|
got, err := reader.Storage(addr, tc.slot)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Storage(%x): %v", tc.slot, err)
|
||||||
|
}
|
||||||
|
if got != tc.want {
|
||||||
|
t.Errorf("slot %x: got %x, want %x", tc.slot, got, tc.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBintrieFlatReaderStorageTombstone verifies the bintrie "tombstone"
|
||||||
|
// convention: a storage slot set to zero is present-with-32-zero-bytes,
|
||||||
|
// which must be distinguishable from "never written" (absent). This is
|
||||||
|
// the A16/T8 integration test.
|
||||||
|
func TestBintrieFlatReaderStorageTombstone(t *testing.T) {
|
||||||
|
disk := rawdb.NewMemoryDatabase()
|
||||||
|
tdb := triedb.NewDatabase(disk, triedb.VerkleDefaults)
|
||||||
|
sdb := NewDatabase(tdb, nil)
|
||||||
|
|
||||||
|
addr := common.HexToAddress("0xABCDEF0123456789ABCDEF0123456789ABCDEF01")
|
||||||
|
slot := common.HexToHash("0x07")
|
||||||
|
nonZero := common.HexToHash("0x42")
|
||||||
|
|
||||||
|
// Block 1: set slot to non-zero.
|
||||||
|
state1, _ := New(types.EmptyVerkleHash, sdb)
|
||||||
|
state1.SetBalance(addr, uint256.NewInt(1), tracing.BalanceChangeUnspecified)
|
||||||
|
state1.SetState(addr, slot, nonZero)
|
||||||
|
root1, err := state1.Commit(0, true, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("commit block 1: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block 2: set the same slot to zero (the bintrie writes 32 zero
|
||||||
|
// bytes as a tombstone rather than deleting the offset).
|
||||||
|
state2, _ := New(root1, sdb)
|
||||||
|
state2.SetState(addr, slot, common.Hash{})
|
||||||
|
root2, err := state2.Commit(1, true, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("commit block 2: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read at block 2: should be the zero hash.
|
||||||
|
reader2, err := sdb.StateReader(root2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("StateReader(block2): %v", err)
|
||||||
|
}
|
||||||
|
got2, err := reader2.Storage(addr, slot)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Storage(block2): %v", err)
|
||||||
|
}
|
||||||
|
if got2 != (common.Hash{}) {
|
||||||
|
t.Errorf("block 2 slot: got %x, want zero", got2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read at block 1: should still be the non-zero value.
|
||||||
|
reader1, err := sdb.StateReader(root1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("StateReader(block1): %v", err)
|
||||||
|
}
|
||||||
|
got1, err := reader1.Storage(addr, slot)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Storage(block1): %v", err)
|
||||||
|
}
|
||||||
|
if got1 != nonZero {
|
||||||
|
t.Errorf("block 1 slot: got %x, want %x", got1, nonZero)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBintrieFlatReaderMultiBlockEvolution verifies that diff-layer
|
||||||
|
// chaining works correctly across multiple blocks for the bintrie path.
|
||||||
|
// This is the A16/T9 integration test.
|
||||||
|
func TestBintrieFlatReaderMultiBlockEvolution(t *testing.T) {
|
||||||
|
disk := rawdb.NewMemoryDatabase()
|
||||||
|
tdb := triedb.NewDatabase(disk, triedb.VerkleDefaults)
|
||||||
|
sdb := NewDatabase(tdb, nil)
|
||||||
|
|
||||||
|
addr := common.HexToAddress("0xDeaDBeefDeaDBeefDeaDBeefDeaDBeefDeaDBeef")
|
||||||
|
|
||||||
|
// Block 1: nonce=1, balance=100
|
||||||
|
state1, _ := New(types.EmptyVerkleHash, sdb)
|
||||||
|
state1.SetBalance(addr, uint256.NewInt(100), tracing.BalanceChangeUnspecified)
|
||||||
|
state1.SetNonce(addr, 1, tracing.NonceChangeUnspecified)
|
||||||
|
root1, err := state1.Commit(0, true, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("commit block 1: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block 2: nonce=2 (balance unchanged at 100)
|
||||||
|
state2, _ := New(root1, sdb)
|
||||||
|
state2.SetNonce(addr, 2, tracing.NonceChangeUnspecified)
|
||||||
|
root2, err := state2.Commit(1, true, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("commit block 2: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block 3: balance=200 (nonce unchanged at 2)
|
||||||
|
state3, _ := New(root2, sdb)
|
||||||
|
state3.SetBalance(addr, uint256.NewInt(200), tracing.BalanceChangeUnspecified)
|
||||||
|
root3, err := state3.Commit(2, true, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("commit block 3: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read at each root and verify the expected snapshot.
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
root common.Hash
|
||||||
|
nonce uint64
|
||||||
|
balance uint64
|
||||||
|
}{
|
||||||
|
{"block1", root1, 1, 100},
|
||||||
|
{"block2", root2, 2, 100},
|
||||||
|
{"block3", root3, 2, 200},
|
||||||
|
} {
|
||||||
|
reader, err := sdb.StateReader(tc.root)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%s StateReader: %v", tc.name, err)
|
||||||
|
}
|
||||||
|
got, err := reader.Account(addr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%s Account: %v", tc.name, err)
|
||||||
|
}
|
||||||
|
if got == nil {
|
||||||
|
t.Fatalf("%s: account is nil", tc.name)
|
||||||
|
}
|
||||||
|
if got.Nonce != tc.nonce {
|
||||||
|
t.Errorf("%s nonce: got %d, want %d", tc.name, got.Nonce, tc.nonce)
|
||||||
|
}
|
||||||
|
if got.Balance.Uint64() != tc.balance {
|
||||||
|
t.Errorf("%s balance: got %d, want %d", tc.name, got.Balance.Uint64(), tc.balance)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
79
core/state/state_mut.go
Normal file
79
core/state/state_mut.go
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
// Copyright 2026 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 state
|
||||||
|
|
||||||
|
import "github.com/ethereum/go-ethereum/common"
|
||||||
|
|
||||||
|
type mutationType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
update mutationType = iota
|
||||||
|
deletion
|
||||||
|
)
|
||||||
|
|
||||||
|
type mutation struct {
|
||||||
|
typ mutationType
|
||||||
|
applied bool
|
||||||
|
|
||||||
|
// precedingDelete indicates that a previously unapplied deletion was
|
||||||
|
// overwritten by an update (account deleted then re-created within
|
||||||
|
// the same block). IntermediateRoot uses this to notify the hasher
|
||||||
|
// of the deletion before the update so that any cached storage trie
|
||||||
|
// is evicted and the re-created account starts with a fresh trie.
|
||||||
|
precedingDelete bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mutation) copy() *mutation {
|
||||||
|
return &mutation{
|
||||||
|
typ: m.typ,
|
||||||
|
applied: m.applied,
|
||||||
|
precedingDelete: m.precedingDelete,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mutation) isDelete() bool {
|
||||||
|
return m.typ == deletion
|
||||||
|
}
|
||||||
|
|
||||||
|
// markDelete is invoked when an account is deleted but the deletion is
|
||||||
|
// not yet committed. The pending mutation is cached and will be applied
|
||||||
|
// all together.
|
||||||
|
func (s *StateDB) markDelete(addr common.Address) {
|
||||||
|
if _, ok := s.mutations[addr]; !ok {
|
||||||
|
s.mutations[addr] = &mutation{}
|
||||||
|
}
|
||||||
|
s.mutations[addr].applied = false
|
||||||
|
s.mutations[addr].typ = deletion
|
||||||
|
s.mutations[addr].precedingDelete = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StateDB) markUpdate(addr common.Address) {
|
||||||
|
m, ok := s.mutations[addr]
|
||||||
|
if !ok {
|
||||||
|
s.mutations[addr] = &mutation{}
|
||||||
|
m = s.mutations[addr]
|
||||||
|
}
|
||||||
|
// If this update overwrites a pending (unapplied) deletion, record it
|
||||||
|
// so that IntermediateRoot can notify the hasher of the deletion first.
|
||||||
|
// Do not reset precedingDelete otherwise: a subsequent markUpdate must
|
||||||
|
// preserve the flag set by an earlier markDelete→markUpdate sequence.
|
||||||
|
if !m.applied && m.typ == deletion {
|
||||||
|
m.precedingDelete = true
|
||||||
|
}
|
||||||
|
m.applied = false
|
||||||
|
m.typ = update
|
||||||
|
}
|
||||||
|
|
@ -27,11 +27,6 @@ import (
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
"github.com/ethereum/go-ethereum/log"
|
"github.com/ethereum/go-ethereum/log"
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
|
||||||
"github.com/ethereum/go-ethereum/trie"
|
|
||||||
"github.com/ethereum/go-ethereum/trie/bintrie"
|
|
||||||
"github.com/ethereum/go-ethereum/trie/transitiontrie"
|
|
||||||
"github.com/ethereum/go-ethereum/trie/trienode"
|
|
||||||
"github.com/holiman/uint256"
|
"github.com/holiman/uint256"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -49,13 +44,12 @@ func (s Storage) Copy() Storage {
|
||||||
// - Finally, call commit to return the changes of storage trie and update account data.
|
// - Finally, call commit to return the changes of storage trie and update account data.
|
||||||
type stateObject struct {
|
type stateObject struct {
|
||||||
db *StateDB
|
db *StateDB
|
||||||
address common.Address // address of ethereum account
|
address common.Address // address of ethereum account
|
||||||
addressHash *common.Hash // hash of ethereum address of the account
|
addressHash *common.Hash // hash of ethereum address of the account
|
||||||
origin *types.StateAccount // Account original data without any change applied, nil means it was not existent
|
origin *Account // Account original data without any change applied, nil means it was not existent
|
||||||
data types.StateAccount // Account data with all mutations applied in the scope of block
|
data Account // Account data with all mutations applied in the scope of block
|
||||||
|
|
||||||
// Write caches.
|
// Write caches.
|
||||||
trie Trie // storage trie, which becomes non-nil on first access
|
|
||||||
code []byte // contract bytecode, which gets set when code is loaded
|
code []byte // contract bytecode, which gets set when code is loaded
|
||||||
|
|
||||||
originStorage Storage // Storage entries that have been accessed within the current block
|
originStorage Storage // Storage entries that have been accessed within the current block
|
||||||
|
|
@ -94,10 +88,10 @@ func (s *stateObject) empty() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// newObject creates a state object.
|
// newObject creates a state object.
|
||||||
func newObject(db *StateDB, address common.Address, acct *types.StateAccount) *stateObject {
|
func newObject(db *StateDB, address common.Address, acct *Account) *stateObject {
|
||||||
origin := acct
|
origin := acct
|
||||||
if acct == nil {
|
if acct == nil {
|
||||||
acct = types.NewEmptyStateAccount()
|
acct = newEmptyAccount()
|
||||||
}
|
}
|
||||||
return &stateObject{
|
return &stateObject{
|
||||||
db: db,
|
db: db,
|
||||||
|
|
@ -127,40 +121,6 @@ func (s *stateObject) touch() {
|
||||||
s.db.journal.touchChange(s.address)
|
s.db.journal.touchChange(s.address)
|
||||||
}
|
}
|
||||||
|
|
||||||
// getTrie returns the associated storage trie. The trie will be opened if it's
|
|
||||||
// not loaded previously. An error will be returned if trie can't be loaded.
|
|
||||||
//
|
|
||||||
// If a new trie is opened, it will be cached within the state object to allow
|
|
||||||
// subsequent reads to expand the same trie instead of reloading from disk.
|
|
||||||
func (s *stateObject) getTrie() (Trie, error) {
|
|
||||||
if s.trie == nil {
|
|
||||||
// Assumes the primary account trie is already loaded
|
|
||||||
tr, err := s.db.db.OpenStorageTrie(s.db.originalRoot, s.address, s.data.Root, s.db.trie)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
s.trie = tr
|
|
||||||
}
|
|
||||||
return s.trie, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// getPrefetchedTrie returns the associated trie, as populated by the prefetcher
|
|
||||||
// if it's available.
|
|
||||||
//
|
|
||||||
// Note, opposed to getTrie, this method will *NOT* blindly cache the resulting
|
|
||||||
// trie in the state object. The caller might want to do that, but it's cleaner
|
|
||||||
// to break the hidden interdependency between retrieving tries from the db or
|
|
||||||
// from the prefetcher.
|
|
||||||
func (s *stateObject) getPrefetchedTrie() Trie {
|
|
||||||
// If there's nothing to meaningfully return, let the user figure it out by
|
|
||||||
// pulling the trie from disk.
|
|
||||||
if (s.data.Root == types.EmptyRootHash && !s.db.db.TrieDB().IsVerkle()) || s.db.prefetcher == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// Attempt to retrieve the trie from the prefetcher
|
|
||||||
return s.db.prefetcher.trie(s.addrHash(), s.data.Root)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetState retrieves a value associated with the given storage key.
|
// GetState retrieves a value associated with the given storage key.
|
||||||
func (s *stateObject) GetState(key common.Hash) common.Hash {
|
func (s *stateObject) GetState(key common.Hash) common.Hash {
|
||||||
value, _ := s.getState(key)
|
value, _ := s.getState(key)
|
||||||
|
|
@ -211,23 +171,22 @@ func (s *stateObject) GetCommittedState(key common.Hash) common.Hash {
|
||||||
s.originStorage[key] = common.Hash{} // track the empty slot as origin value
|
s.originStorage[key] = common.Hash{} // track the empty slot as origin value
|
||||||
return common.Hash{}
|
return common.Hash{}
|
||||||
}
|
}
|
||||||
s.db.StorageLoaded++
|
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
value, err := s.db.reader.Storage(s.address, key)
|
value, err := s.db.reader.Storage(s.address, key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.db.setError(err)
|
s.db.setError(err)
|
||||||
return common.Hash{}
|
return common.Hash{}
|
||||||
}
|
}
|
||||||
|
s.db.StorageLoaded++
|
||||||
s.db.StorageReads += time.Since(start)
|
s.db.StorageReads += time.Since(start)
|
||||||
|
|
||||||
// Schedule the resolved storage slots for prefetching if it's enabled.
|
|
||||||
if s.db.prefetcher != nil && s.data.Root != types.EmptyRootHash {
|
|
||||||
if err = s.db.prefetcher.prefetch(s.addrHash(), s.origin.Root, s.address, nil, []common.Hash{key}, true); err != nil {
|
|
||||||
log.Error("Failed to prefetch storage slot", "addr", s.address, "key", key, "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s.originStorage[key] = value
|
s.originStorage[key] = value
|
||||||
|
|
||||||
|
// Schedule the resolved storage slots for prefetching if it's enabled.
|
||||||
|
prefetch, ok := s.db.hasher.(Prefetcher)
|
||||||
|
if ok {
|
||||||
|
prefetch.PrefetchStorage(s.address, []common.Hash{key}, true)
|
||||||
|
}
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -273,7 +232,7 @@ func (s *stateObject) finalise() {
|
||||||
// The slot is different from its original value and hasn't been
|
// The slot is different from its original value and hasn't been
|
||||||
// tracked for commit yet.
|
// tracked for commit yet.
|
||||||
s.uncommittedStorage[key] = s.GetCommittedState(key)
|
s.uncommittedStorage[key] = s.GetCommittedState(key)
|
||||||
slotsToPrefetch = append(slotsToPrefetch, key) // Copy needed for closure
|
slotsToPrefetch = append(slotsToPrefetch, key)
|
||||||
}
|
}
|
||||||
// Aggregate the dirty storage slots into the pending area. It might
|
// Aggregate the dirty storage slots into the pending area. It might
|
||||||
// be possible that the value of tracked slot here is same with the
|
// be possible that the value of tracked slot here is same with the
|
||||||
|
|
@ -283,11 +242,6 @@ func (s *stateObject) finalise() {
|
||||||
// byzantium fork) and entry is necessary to modify the value back.
|
// byzantium fork) and entry is necessary to modify the value back.
|
||||||
s.pendingStorage[key] = value
|
s.pendingStorage[key] = value
|
||||||
}
|
}
|
||||||
if s.db.prefetcher != nil && len(slotsToPrefetch) > 0 && s.data.Root != types.EmptyRootHash {
|
|
||||||
if err := s.db.prefetcher.prefetch(s.addrHash(), s.data.Root, s.address, nil, slotsToPrefetch, false); err != nil {
|
|
||||||
log.Error("Failed to prefetch slots", "addr", s.address, "slots", len(slotsToPrefetch), "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(s.dirtyStorage) > 0 {
|
if len(s.dirtyStorage) > 0 {
|
||||||
s.dirtyStorage = make(Storage)
|
s.dirtyStorage = make(Storage)
|
||||||
}
|
}
|
||||||
|
|
@ -295,58 +249,27 @@ func (s *stateObject) finalise() {
|
||||||
// of the newly-created object as it's no longer eligible for self-destruct
|
// of the newly-created object as it's no longer eligible for self-destruct
|
||||||
// by EIP-6780. For non-newly-created objects, it's a no-op.
|
// by EIP-6780. For non-newly-created objects, it's a no-op.
|
||||||
s.newContract = false
|
s.newContract = false
|
||||||
|
|
||||||
|
// Schedule the resolved storage slots for prefetching if it's enabled.
|
||||||
|
prefetch, ok := s.db.hasher.(Prefetcher)
|
||||||
|
if ok {
|
||||||
|
prefetch.PrefetchStorage(s.address, slotsToPrefetch, false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateTrie is responsible for persisting cached storage changes into the
|
// updateTrie is responsible for persisting cached storage changes into the
|
||||||
// object's storage trie. In case the storage trie is not yet loaded, this
|
// state hasher. It assumes all the dirty storage slots have been finalized
|
||||||
// function will load the trie automatically. If any issues arise during the
|
// before.
|
||||||
// loading or updating of the trie, an error will be returned. Furthermore,
|
func (s *stateObject) updateTrie() error {
|
||||||
// this function will return the mutated storage trie, or nil if there is no
|
// Short circuit if nothing was accessed
|
||||||
// storage change at all.
|
|
||||||
//
|
|
||||||
// It assumes all the dirty storage slots have been finalized before.
|
|
||||||
func (s *stateObject) updateTrie() (Trie, error) {
|
|
||||||
// Short circuit if nothing was accessed, don't trigger a prefetcher warning
|
|
||||||
if len(s.uncommittedStorage) == 0 {
|
if len(s.uncommittedStorage) == 0 {
|
||||||
// Nothing was written, so we could stop early. Unless we have both reads
|
return nil
|
||||||
// and witness collection enabled, in which case we need to fetch the trie.
|
|
||||||
if s.db.witness == nil || len(s.originStorage) == 0 {
|
|
||||||
return s.trie, nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Retrieve a pretecher populated trie, or fall back to the database. This will
|
|
||||||
// block until all prefetch tasks are done, which are needed for witnesses even
|
|
||||||
// for unmodified state objects.
|
|
||||||
tr := s.getPrefetchedTrie()
|
|
||||||
if tr != nil {
|
|
||||||
// Prefetcher returned a live trie, swap it out for the current one
|
|
||||||
s.trie = tr
|
|
||||||
} else {
|
|
||||||
// Fetcher not running or empty trie, fallback to the database trie
|
|
||||||
var err error
|
|
||||||
tr, err = s.getTrie()
|
|
||||||
if err != nil {
|
|
||||||
s.db.setError(err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Short circuit if nothing changed, don't bother with hashing anything
|
|
||||||
if len(s.uncommittedStorage) == 0 {
|
|
||||||
return s.trie, nil
|
|
||||||
}
|
|
||||||
// Perform trie updates before deletions. This prevents resolution of unnecessary trie nodes
|
|
||||||
// in circumstances similar to the following:
|
|
||||||
//
|
|
||||||
// Consider nodes `A` and `B` who share the same full node parent `P` and have no other siblings.
|
|
||||||
// During the execution of a block:
|
|
||||||
// - `A` is deleted,
|
|
||||||
// - `C` is created, and also shares the parent `P`.
|
|
||||||
// If the deletion is handled first, then `P` would be left with only one child, thus collapsed
|
|
||||||
// into a shortnode. This requires `B` to be resolved from disk.
|
|
||||||
// Whereas if the created node is handled first, then the collapse is avoided, and `B` is not resolved.
|
|
||||||
var (
|
var (
|
||||||
deletions []common.Hash
|
updates int64
|
||||||
used = make([]common.Hash, 0, len(s.uncommittedStorage))
|
deletes int64
|
||||||
|
keys = make([]common.Hash, 0, len(s.uncommittedStorage))
|
||||||
|
vals = make([]common.Hash, 0, len(s.uncommittedStorage))
|
||||||
)
|
)
|
||||||
for key, origin := range s.uncommittedStorage {
|
for key, origin := range s.uncommittedStorage {
|
||||||
// Skip noop changes, persist actual changes
|
// Skip noop changes, persist actual changes
|
||||||
|
|
@ -359,56 +282,24 @@ func (s *stateObject) updateTrie() (Trie, error) {
|
||||||
log.Error("Storage slot is not found in pending area", "address", s.address, "slot", key)
|
log.Error("Storage slot is not found in pending area", "address", s.address, "slot", key)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (value != common.Hash{}) {
|
if value == (common.Hash{}) {
|
||||||
if err := tr.UpdateStorage(s.address, key[:], common.TrimLeftZeroes(value[:])); err != nil {
|
deletes += 1
|
||||||
s.db.setError(err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
s.db.StorageUpdated.Add(1)
|
|
||||||
} else {
|
} else {
|
||||||
deletions = append(deletions, key)
|
updates += 1
|
||||||
}
|
}
|
||||||
// Cache the items for preloading
|
keys = append(keys, key)
|
||||||
used = append(used, key) // Copy needed for closure
|
vals = append(vals, value)
|
||||||
}
|
|
||||||
for _, key := range deletions {
|
|
||||||
if err := tr.DeleteStorage(s.address, key[:]); err != nil {
|
|
||||||
s.db.setError(err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
s.db.StorageDeleted.Add(1)
|
|
||||||
}
|
|
||||||
if s.db.prefetcher != nil {
|
|
||||||
s.db.prefetcher.used(s.addrHash(), s.data.Root, nil, used)
|
|
||||||
}
|
}
|
||||||
s.uncommittedStorage = make(Storage) // empties the commit markers
|
s.uncommittedStorage = make(Storage) // empties the commit markers
|
||||||
return tr, nil
|
s.db.StorageUpdated.Add(updates)
|
||||||
}
|
s.db.StorageDeleted.Add(deletes)
|
||||||
|
|
||||||
// updateRoot flushes all cached storage mutations to trie, recalculating the
|
return s.db.hasher.UpdateStorage(s.address, keys, vals)
|
||||||
// new storage trie root.
|
|
||||||
func (s *stateObject) updateRoot() {
|
|
||||||
// Flush cached storage mutations into trie, short circuit if any error
|
|
||||||
// is occurred or there is no change in the trie.
|
|
||||||
tr, err := s.updateTrie()
|
|
||||||
if err != nil || tr == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
s.data.Root = tr.Hash()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// commitStorage overwrites the clean storage with the storage changes and
|
// commitStorage overwrites the clean storage with the storage changes and
|
||||||
// fulfills the storage diffs into the given accountUpdate struct.
|
// fulfills the storage diffs into the given accountUpdate struct.
|
||||||
func (s *stateObject) commitStorage(op *accountUpdate) {
|
func (s *stateObject) commitStorage(op *accountUpdate) {
|
||||||
var (
|
|
||||||
encode = func(val common.Hash) []byte {
|
|
||||||
if val == (common.Hash{}) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
blob, _ := rlp.EncodeToBytes(common.TrimLeftZeroes(val[:]))
|
|
||||||
return blob
|
|
||||||
}
|
|
||||||
)
|
|
||||||
for key, val := range s.pendingStorage {
|
for key, val := range s.pendingStorage {
|
||||||
// Skip the noop storage changes, it might be possible the value
|
// Skip the noop storage changes, it might be possible the value
|
||||||
// of tracked slot is same in originStorage and pendingStorage
|
// of tracked slot is same in originStorage and pendingStorage
|
||||||
|
|
@ -419,17 +310,17 @@ func (s *stateObject) commitStorage(op *accountUpdate) {
|
||||||
}
|
}
|
||||||
hash := crypto.Keccak256Hash(key[:])
|
hash := crypto.Keccak256Hash(key[:])
|
||||||
if op.storages == nil {
|
if op.storages == nil {
|
||||||
op.storages = make(map[common.Hash][]byte)
|
op.storages = make(map[common.Hash]common.Hash)
|
||||||
}
|
}
|
||||||
op.storages[hash] = encode(val)
|
op.storages[hash] = val
|
||||||
|
|
||||||
if op.storagesOriginByKey == nil {
|
if op.storagesOriginByKey == nil {
|
||||||
op.storagesOriginByKey = make(map[common.Hash][]byte)
|
op.storagesOriginByKey = make(map[common.Hash]common.Hash)
|
||||||
}
|
}
|
||||||
if op.storagesOriginByHash == nil {
|
if op.storagesOriginByHash == nil {
|
||||||
op.storagesOriginByHash = make(map[common.Hash][]byte)
|
op.storagesOriginByHash = make(map[common.Hash]common.Hash)
|
||||||
}
|
}
|
||||||
origin := encode(s.originStorage[key])
|
origin := s.originStorage[key]
|
||||||
op.storagesOriginByKey[key] = origin
|
op.storagesOriginByKey[key] = origin
|
||||||
op.storagesOriginByHash[hash] = origin
|
op.storagesOriginByHash[hash] = origin
|
||||||
|
|
||||||
|
|
@ -444,23 +335,21 @@ func (s *stateObject) commitStorage(op *accountUpdate) {
|
||||||
//
|
//
|
||||||
// Note, commit may run concurrently across all the state objects. Do not assume
|
// Note, commit may run concurrently across all the state objects. Do not assume
|
||||||
// thread-safe access to the statedb.
|
// thread-safe access to the statedb.
|
||||||
func (s *stateObject) commit() (*accountUpdate, *trienode.NodeSet, error) {
|
func (s *stateObject) commit() (*accountUpdate, error) {
|
||||||
// commit the account metadata changes
|
// commit the account metadata changes
|
||||||
op := &accountUpdate{
|
op := &accountUpdate{
|
||||||
address: s.address,
|
address: s.address,
|
||||||
data: types.SlimAccountRLP(s.data),
|
data: &s.data,
|
||||||
}
|
origin: s.origin,
|
||||||
if s.origin != nil {
|
|
||||||
op.origin = types.SlimAccountRLP(*s.origin)
|
|
||||||
}
|
}
|
||||||
// commit the contract code if it's modified
|
// commit the contract code if it's modified
|
||||||
if s.dirtyCode {
|
if s.dirtyCode {
|
||||||
|
s.dirtyCode = false // reset the dirty flag
|
||||||
|
|
||||||
op.code = &contractCode{
|
op.code = &contractCode{
|
||||||
hash: common.BytesToHash(s.CodeHash()),
|
hash: common.BytesToHash(s.CodeHash()),
|
||||||
blob: s.code,
|
blob: s.code,
|
||||||
}
|
}
|
||||||
s.dirtyCode = false // reset the dirty flag
|
|
||||||
|
|
||||||
if s.origin == nil {
|
if s.origin == nil {
|
||||||
op.code.originHash = types.EmptyCodeHash
|
op.code.originHash = types.EmptyCodeHash
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -469,24 +358,8 @@ func (s *stateObject) commit() (*accountUpdate, *trienode.NodeSet, error) {
|
||||||
}
|
}
|
||||||
// Commit storage changes and the associated storage trie
|
// Commit storage changes and the associated storage trie
|
||||||
s.commitStorage(op)
|
s.commitStorage(op)
|
||||||
if len(op.storages) == 0 {
|
s.origin = s.data.copy()
|
||||||
// nothing changed, don't bother to commit the trie
|
return op, nil
|
||||||
s.origin = s.data.Copy()
|
|
||||||
return op, nil, nil
|
|
||||||
}
|
|
||||||
// In Verkle/binary trie mode, all state objects share one unified trie.
|
|
||||||
// The main account trie commit in stateDB.commit() already calls
|
|
||||||
// CollectNodes on this trie, so calling Commit here again would
|
|
||||||
// redundantly traverse and serialize the entire tree per dirty account.
|
|
||||||
if s.db.GetTrie().IsVerkle() {
|
|
||||||
s.origin = s.data.Copy()
|
|
||||||
return op, nil, nil
|
|
||||||
}
|
|
||||||
// The storage trie root is omitted, as it has already been updated in the
|
|
||||||
// previous updateRoot step.
|
|
||||||
_, nodes := s.trie.Commit(false)
|
|
||||||
s.origin = s.data.Copy()
|
|
||||||
return op, nodes, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddBalance adds amount to s's balance.
|
// AddBalance adds amount to s's balance.
|
||||||
|
|
@ -532,21 +405,6 @@ func (s *stateObject) deepCopy(db *StateDB) *stateObject {
|
||||||
selfDestructed: s.selfDestructed,
|
selfDestructed: s.selfDestructed,
|
||||||
newContract: s.newContract,
|
newContract: s.newContract,
|
||||||
}
|
}
|
||||||
|
|
||||||
switch s.trie.(type) {
|
|
||||||
case *bintrie.BinaryTrie:
|
|
||||||
// UBT uses only one tree, and the copy has already been
|
|
||||||
// made in mustCopyTrie.
|
|
||||||
obj.trie = db.trie
|
|
||||||
case *transitiontrie.TransitionTrie:
|
|
||||||
// Same thing for the transition tree, since the MPT is
|
|
||||||
// read-only.
|
|
||||||
obj.trie = db.trie
|
|
||||||
case *trie.StateTrie:
|
|
||||||
obj.trie = mustCopyTrie(s.trie)
|
|
||||||
case nil:
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
return obj
|
return obj
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -636,7 +494,3 @@ func (s *stateObject) Balance() *uint256.Int {
|
||||||
func (s *stateObject) Nonce() uint64 {
|
func (s *stateObject) Nonce() uint64 {
|
||||||
return s.data.Nonce
|
return s.data.Nonce
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *stateObject) Root() common.Hash {
|
|
||||||
return s.data.Root
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
// Database key scheme for states.
|
// Database key scheme for states.
|
||||||
|
// nolint:unused
|
||||||
var (
|
var (
|
||||||
accountKeySize = int64(len(rawdb.SnapshotAccountPrefix) + common.HashLength)
|
accountKeySize = int64(len(rawdb.SnapshotAccountPrefix) + common.HashLength)
|
||||||
storageKeySize = int64(len(rawdb.SnapshotStoragePrefix) + common.HashLength*2)
|
storageKeySize = int64(len(rawdb.SnapshotStoragePrefix) + common.HashLength*2)
|
||||||
|
|
@ -130,11 +131,15 @@ func calSizeStats(update *stateUpdate) (SizeStats, error) {
|
||||||
BlockNumber: update.blockNumber,
|
BlockNumber: update.blockNumber,
|
||||||
StateRoot: update.root,
|
StateRoot: update.root,
|
||||||
}
|
}
|
||||||
|
accounts, accountOrigin, storages, storageOrigin, err := update.encodeMerkle()
|
||||||
|
if err != nil {
|
||||||
|
return SizeStats{}, err
|
||||||
|
}
|
||||||
|
|
||||||
// Measure the account changes
|
// Measure the account changes
|
||||||
for addr, oldValue := range update.accountsOrigin {
|
for addr, oldValue := range accountOrigin {
|
||||||
addrHash := crypto.Keccak256Hash(addr.Bytes())
|
addrHash := crypto.Keccak256Hash(addr.Bytes())
|
||||||
newValue, exists := update.accounts[addrHash]
|
newValue, exists := accounts[addrHash]
|
||||||
if !exists {
|
if !exists {
|
||||||
return SizeStats{}, fmt.Errorf("account %x not found", addr)
|
return SizeStats{}, fmt.Errorf("account %x not found", addr)
|
||||||
}
|
}
|
||||||
|
|
@ -156,9 +161,9 @@ func calSizeStats(update *stateUpdate) (SizeStats, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Measure storage changes
|
// Measure storage changes
|
||||||
for addr, slots := range update.storagesOrigin {
|
for addr, slots := range storageOrigin {
|
||||||
addrHash := crypto.Keccak256Hash(addr.Bytes())
|
addrHash := crypto.Keccak256Hash(addr.Bytes())
|
||||||
subset, exists := update.storages[addrHash]
|
subset, exists := storages[addrHash]
|
||||||
if !exists {
|
if !exists {
|
||||||
return SizeStats{}, fmt.Errorf("storage %x not found", addr)
|
return SizeStats{}, fmt.Errorf("storage %x not found", addr)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,6 @@ import (
|
||||||
"maps"
|
"maps"
|
||||||
"slices"
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
|
@ -32,8 +30,8 @@ import (
|
||||||
"github.com/ethereum/go-ethereum/core/tracing"
|
"github.com/ethereum/go-ethereum/core/tracing"
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
"github.com/ethereum/go-ethereum/params"
|
"github.com/ethereum/go-ethereum/params"
|
||||||
|
"github.com/ethereum/go-ethereum/rlp"
|
||||||
"github.com/ethereum/go-ethereum/trie"
|
"github.com/ethereum/go-ethereum/trie"
|
||||||
"github.com/ethereum/go-ethereum/trie/trienode"
|
"github.com/ethereum/go-ethereum/trie/trienode"
|
||||||
"github.com/holiman/uint256"
|
"github.com/holiman/uint256"
|
||||||
|
|
@ -43,26 +41,6 @@ import (
|
||||||
// TriesInMemory represents the number of layers that are kept in RAM.
|
// TriesInMemory represents the number of layers that are kept in RAM.
|
||||||
const TriesInMemory = 128
|
const TriesInMemory = 128
|
||||||
|
|
||||||
type mutationType int
|
|
||||||
|
|
||||||
const (
|
|
||||||
update mutationType = iota
|
|
||||||
deletion
|
|
||||||
)
|
|
||||||
|
|
||||||
type mutation struct {
|
|
||||||
typ mutationType
|
|
||||||
applied bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mutation) copy() *mutation {
|
|
||||||
return &mutation{typ: m.typ, applied: m.applied}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mutation) isDelete() bool {
|
|
||||||
return m.typ == deletion
|
|
||||||
}
|
|
||||||
|
|
||||||
// StateDB structs within the ethereum protocol are used to store anything
|
// StateDB structs within the ethereum protocol are used to store anything
|
||||||
// within the merkle trie. StateDBs take care of caching and storing
|
// within the merkle trie. StateDBs take care of caching and storing
|
||||||
// nested states. It's the general query interface to retrieve:
|
// nested states. It's the general query interface to retrieve:
|
||||||
|
|
@ -75,10 +53,9 @@ func (m *mutation) isDelete() bool {
|
||||||
// must be created with new root and updated database for accessing post-
|
// must be created with new root and updated database for accessing post-
|
||||||
// commit states.
|
// commit states.
|
||||||
type StateDB struct {
|
type StateDB struct {
|
||||||
db Database
|
db Database
|
||||||
prefetcher *triePrefetcher
|
reader Reader
|
||||||
reader Reader
|
hasher Hasher
|
||||||
trie Trie // it's resolved on first access
|
|
||||||
|
|
||||||
// originalRoot is the pre-state root, before any changes were made.
|
// originalRoot is the pre-state root, before any changes were made.
|
||||||
// It will be updated when the Commit is called.
|
// It will be updated when the Commit is called.
|
||||||
|
|
@ -137,32 +114,7 @@ type StateDB struct {
|
||||||
witness *stateless.Witness
|
witness *stateless.Witness
|
||||||
|
|
||||||
// Measurements gathered during execution for debugging purposes
|
// Measurements gathered during execution for debugging purposes
|
||||||
AccountReads time.Duration
|
Stats
|
||||||
AccountHashes time.Duration
|
|
||||||
AccountUpdates time.Duration
|
|
||||||
AccountCommits time.Duration
|
|
||||||
|
|
||||||
StorageReads time.Duration
|
|
||||||
StorageUpdates time.Duration
|
|
||||||
StorageCommits time.Duration
|
|
||||||
DatabaseCommits time.Duration
|
|
||||||
CodeReads time.Duration
|
|
||||||
|
|
||||||
AccountLoaded int // Number of accounts retrieved from the database during the state transition
|
|
||||||
AccountUpdated int // Number of accounts updated during the state transition
|
|
||||||
AccountDeleted int // Number of accounts deleted during the state transition
|
|
||||||
StorageLoaded int // Number of storage slots retrieved from the database during the state transition
|
|
||||||
StorageUpdated atomic.Int64 // Number of storage slots updated during the state transition
|
|
||||||
StorageDeleted atomic.Int64 // Number of storage slots deleted during the state transition
|
|
||||||
|
|
||||||
// CodeLoadBytes is the total number of bytes read from contract code.
|
|
||||||
// This value may be smaller than the actual number of bytes read, since
|
|
||||||
// some APIs (e.g. CodeSize) may load the entire code from either the
|
|
||||||
// cache or the database when the size is not available in the cache.
|
|
||||||
CodeLoaded int // Number of contract code loaded during the state transition
|
|
||||||
CodeLoadBytes int // Total bytes of resolved code
|
|
||||||
CodeUpdated int // Number of contracts with code changes that persisted
|
|
||||||
CodeUpdateBytes int // Total bytes of persisted code written
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new state from a given trie.
|
// New creates a new state from a given trie.
|
||||||
|
|
@ -177,10 +129,15 @@ func New(root common.Hash, db Database) (*StateDB, error) {
|
||||||
// NewWithReader creates a new state for the specified state root. Unlike New,
|
// NewWithReader creates a new state for the specified state root. Unlike New,
|
||||||
// this function accepts an additional Reader which is bound to the given root.
|
// this function accepts an additional Reader which is bound to the given root.
|
||||||
func NewWithReader(root common.Hash, db Database, reader Reader) (*StateDB, error) {
|
func NewWithReader(root common.Hash, db Database, reader Reader) (*StateDB, error) {
|
||||||
|
hasher, err := db.Hasher(root)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
sdb := &StateDB{
|
sdb := &StateDB{
|
||||||
db: db,
|
db: db,
|
||||||
originalRoot: root,
|
originalRoot: root,
|
||||||
reader: reader,
|
reader: reader,
|
||||||
|
hasher: hasher,
|
||||||
stateObjects: make(map[common.Address]*stateObject),
|
stateObjects: make(map[common.Address]*stateObject),
|
||||||
stateObjectsDestruct: make(map[common.Address]*stateObject),
|
stateObjectsDestruct: make(map[common.Address]*stateObject),
|
||||||
mutations: make(map[common.Address]*mutation),
|
mutations: make(map[common.Address]*mutation),
|
||||||
|
|
@ -196,39 +153,9 @@ func NewWithReader(root common.Hash, db Database, reader Reader) (*StateDB, erro
|
||||||
return sdb, nil
|
return sdb, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// StartPrefetcher initializes a new trie prefetcher to pull in nodes from the
|
// TraceWitness enables execution witness gathering.
|
||||||
// state trie concurrently while the state is mutated so that when we reach the
|
func (s *StateDB) TraceWitness(witness *stateless.Witness) {
|
||||||
// commit phase, most of the needed data is already hot.
|
|
||||||
func (s *StateDB) StartPrefetcher(namespace string, witness *stateless.Witness) {
|
|
||||||
// Terminate any previously running prefetcher
|
|
||||||
s.StopPrefetcher()
|
|
||||||
|
|
||||||
// Enable witness collection if requested
|
|
||||||
s.witness = witness
|
s.witness = witness
|
||||||
|
|
||||||
// With the switch to the Proof-of-Stake consensus algorithm, block production
|
|
||||||
// rewards are now handled at the consensus layer. Consequently, a block may
|
|
||||||
// have no state transitions if it contains no transactions and no withdrawals.
|
|
||||||
// In such cases, the account trie won't be scheduled for prefetching, leading
|
|
||||||
// to unnecessary error logs.
|
|
||||||
//
|
|
||||||
// To prevent this, the account trie is always scheduled for prefetching once
|
|
||||||
// the prefetcher is constructed. For more details, see:
|
|
||||||
// https://github.com/ethereum/go-ethereum/issues/29880
|
|
||||||
s.prefetcher = newTriePrefetcher(s.db, s.originalRoot, namespace, witness == nil)
|
|
||||||
if err := s.prefetcher.prefetch(common.Hash{}, s.originalRoot, common.Address{}, nil, nil, false); err != nil {
|
|
||||||
log.Error("Failed to prefetch account trie", "root", s.originalRoot, "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// StopPrefetcher terminates a running prefetcher and reports any leftover stats
|
|
||||||
// from the gathered metrics.
|
|
||||||
func (s *StateDB) StopPrefetcher() {
|
|
||||||
if s.prefetcher != nil {
|
|
||||||
s.prefetcher.terminate(false)
|
|
||||||
s.prefetcher.report()
|
|
||||||
s.prefetcher = nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// setError remembers the first non-nil error it is called with.
|
// setError remembers the first non-nil error it is called with.
|
||||||
|
|
@ -254,7 +181,7 @@ func (s *StateDB) AddLog(log *types.Log) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetLogs returns the logs matching the specified transaction hash, and annotates
|
// GetLogs returns the logs matching the specified transaction hash, and annotates
|
||||||
// them with the given blockNumber and blockHash.
|
// them with the given block attributes.
|
||||||
func (s *StateDB) GetLogs(hash common.Hash, blockNumber uint64, blockHash common.Hash, blockTime uint64) []*types.Log {
|
func (s *StateDB) GetLogs(hash common.Hash, blockNumber uint64, blockHash common.Hash, blockTime uint64) []*types.Log {
|
||||||
logs := s.logs[hash]
|
logs := s.logs[hash]
|
||||||
for _, l := range logs {
|
for _, l := range logs {
|
||||||
|
|
@ -265,6 +192,7 @@ func (s *StateDB) GetLogs(hash common.Hash, blockNumber uint64, blockHash common
|
||||||
return logs
|
return logs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Logs returns the un-annotated logs in order.
|
||||||
func (s *StateDB) Logs() []*types.Log {
|
func (s *StateDB) Logs() []*types.Log {
|
||||||
logs := make([]*types.Log, 0, s.logSize)
|
logs := make([]*types.Log, 0, s.logSize)
|
||||||
for _, lgs := range s.logs {
|
for _, lgs := range s.logs {
|
||||||
|
|
@ -336,19 +264,6 @@ func (s *StateDB) GetNonce(addr common.Address) uint64 {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetStorageRoot retrieves the storage root from the given address or empty
|
|
||||||
// if object not found.
|
|
||||||
//
|
|
||||||
// Note: the storage root returned corresponds to the trie since last Intermediate
|
|
||||||
// operation, some recent in-memory changes are excluded.
|
|
||||||
func (s *StateDB) GetStorageRoot(addr common.Address) common.Hash {
|
|
||||||
stateObject := s.getStateObject(addr)
|
|
||||||
if stateObject != nil {
|
|
||||||
return stateObject.Root()
|
|
||||||
}
|
|
||||||
return common.Hash{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TxIndex returns the current transaction index set by SetTxContext.
|
// TxIndex returns the current transaction index set by SetTxContext.
|
||||||
func (s *StateDB) TxIndex() int {
|
func (s *StateDB) TxIndex() int {
|
||||||
return s.txIndex
|
return s.txIndex
|
||||||
|
|
@ -558,24 +473,6 @@ func (s *StateDB) GetTransientState(addr common.Address, key common.Hash) common
|
||||||
// Setting, updating & deleting state object methods.
|
// Setting, updating & deleting state object methods.
|
||||||
//
|
//
|
||||||
|
|
||||||
// updateStateObject writes the given object to the trie.
|
|
||||||
func (s *StateDB) updateStateObject(obj *stateObject) {
|
|
||||||
// Encode the account and update the account trie
|
|
||||||
if err := s.trie.UpdateAccount(obj.Address(), &obj.data, len(obj.code)); err != nil {
|
|
||||||
s.setError(fmt.Errorf("updateStateObject (%x) error: %v", obj.Address(), err))
|
|
||||||
}
|
|
||||||
if obj.dirtyCode {
|
|
||||||
s.trie.UpdateContractCode(obj.Address(), common.BytesToHash(obj.CodeHash()), obj.code)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// deleteStateObject removes the given object from the state trie.
|
|
||||||
func (s *StateDB) deleteStateObject(addr common.Address) {
|
|
||||||
if err := s.trie.DeleteAccount(addr); err != nil {
|
|
||||||
s.setError(fmt.Errorf("deleteStateObject (%x) error: %v", addr[:], err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// getStateObject retrieves a state object given by the address, returning nil if
|
// getStateObject retrieves a state object given by the address, returning nil if
|
||||||
// the object is not found or was deleted in this execution context.
|
// the object is not found or was deleted in this execution context.
|
||||||
func (s *StateDB) getStateObject(addr common.Address) *stateObject {
|
func (s *StateDB) getStateObject(addr common.Address) *stateObject {
|
||||||
|
|
@ -587,29 +484,28 @@ func (s *StateDB) getStateObject(addr common.Address) *stateObject {
|
||||||
if _, ok := s.stateObjectsDestruct[addr]; ok {
|
if _, ok := s.stateObjectsDestruct[addr]; ok {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
s.AccountLoaded++
|
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
acct, err := s.reader.Account(addr)
|
acct, err := s.reader.Account(addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.setError(fmt.Errorf("getStateObject (%x) error: %w", addr.Bytes(), err))
|
s.setError(fmt.Errorf("getStateObject (%x) error: %w", addr.Bytes(), err))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
s.AccountLoaded++
|
||||||
s.AccountReads += time.Since(start)
|
s.AccountReads += time.Since(start)
|
||||||
|
|
||||||
// Short circuit if the account is not found
|
// Short circuit if the account is not found
|
||||||
if acct == nil {
|
if acct == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
// Schedule the resolved account for prefetching if it's enabled.
|
|
||||||
if s.prefetcher != nil {
|
|
||||||
if err = s.prefetcher.prefetch(common.Hash{}, s.originalRoot, common.Address{}, []common.Address{addr}, nil, true); err != nil {
|
|
||||||
log.Error("Failed to prefetch account", "addr", addr, "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Insert into the live set
|
// Insert into the live set
|
||||||
obj := newObject(s, addr, acct)
|
obj := newObject(s, addr, acct)
|
||||||
s.setStateObject(obj)
|
s.setStateObject(obj)
|
||||||
|
|
||||||
|
// Schedule the resolved account for prefetching if it's enabled.
|
||||||
|
prefetcher, ok := s.hasher.(Prefetcher)
|
||||||
|
if ok {
|
||||||
|
prefetcher.PrefetchAccount([]common.Address{addr}, true)
|
||||||
|
}
|
||||||
return obj
|
return obj
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -673,6 +569,7 @@ func (s *StateDB) Copy() *StateDB {
|
||||||
state := &StateDB{
|
state := &StateDB{
|
||||||
db: s.db,
|
db: s.db,
|
||||||
reader: s.reader,
|
reader: s.reader,
|
||||||
|
hasher: s.hasher.Copy(),
|
||||||
originalRoot: s.originalRoot,
|
originalRoot: s.originalRoot,
|
||||||
stateObjects: make(map[common.Address]*stateObject, len(s.stateObjects)),
|
stateObjects: make(map[common.Address]*stateObject, len(s.stateObjects)),
|
||||||
stateObjectsDestruct: make(map[common.Address]*stateObject, len(s.stateObjectsDestruct)),
|
stateObjectsDestruct: make(map[common.Address]*stateObject, len(s.stateObjectsDestruct)),
|
||||||
|
|
@ -695,9 +592,6 @@ func (s *StateDB) Copy() *StateDB {
|
||||||
transientStorage: s.transientStorage.Copy(),
|
transientStorage: s.transientStorage.Copy(),
|
||||||
journal: s.journal.copy(),
|
journal: s.journal.copy(),
|
||||||
}
|
}
|
||||||
if s.trie != nil {
|
|
||||||
state.trie = mustCopyTrie(s.trie)
|
|
||||||
}
|
|
||||||
if s.witness != nil {
|
if s.witness != nil {
|
||||||
state.witness = s.witness.Copy()
|
state.witness = s.witness.Copy()
|
||||||
}
|
}
|
||||||
|
|
@ -810,18 +704,18 @@ func (s *StateDB) Finalise(deleteEmptyObjects bool) {
|
||||||
obj.finalise()
|
obj.finalise()
|
||||||
s.markUpdate(addr)
|
s.markUpdate(addr)
|
||||||
}
|
}
|
||||||
// At this point, also ship the address off to the precacher. The precacher
|
// At this point, also ship the address off to the prefetcher. The prefetcher
|
||||||
// will start loading tries, and when the change is eventually committed,
|
// will start loading tries, and when the change is eventually committed,
|
||||||
// the commit-phase will be a lot faster
|
// the commit-phase will be a lot faster
|
||||||
addressesToPrefetch = append(addressesToPrefetch, addr) // Copy needed for closure
|
addressesToPrefetch = append(addressesToPrefetch, addr)
|
||||||
}
|
|
||||||
if s.prefetcher != nil && len(addressesToPrefetch) > 0 {
|
|
||||||
if err := s.prefetcher.prefetch(common.Hash{}, s.originalRoot, common.Address{}, addressesToPrefetch, nil, false); err != nil {
|
|
||||||
log.Error("Failed to prefetch addresses", "addresses", len(addressesToPrefetch), "err", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Invalidate journal because reverting across transactions is not allowed.
|
// Invalidate journal because reverting across transactions is not allowed.
|
||||||
s.clearJournalAndRefund()
|
s.clearJournalAndRefund()
|
||||||
|
|
||||||
|
prefetcher, ok := s.hasher.(Prefetcher)
|
||||||
|
if ok {
|
||||||
|
prefetcher.PrefetchAccount(addressesToPrefetch, false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// IntermediateRoot computes the current root hash of the state trie.
|
// IntermediateRoot computes the current root hash of the state trie.
|
||||||
|
|
@ -831,204 +725,100 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash {
|
||||||
// Finalise all the dirty storage states and write them into the tries
|
// Finalise all the dirty storage states and write them into the tries
|
||||||
s.Finalise(deleteEmptyObjects)
|
s.Finalise(deleteEmptyObjects)
|
||||||
|
|
||||||
// Initialize the trie if it's not constructed yet. If the prefetch
|
// Pre-process mutations whose preceding deletion has not yet been
|
||||||
// is enabled, the trie constructed below will be replaced by the
|
// applied. This happens when an account is deleted and then re-created
|
||||||
// prefetched one.
|
// within the same block and the deletion was overwritten by the update.
|
||||||
//
|
// Notify the hasher of the deletion first so that any cached storage
|
||||||
// This operation must be done before state object storage hashing,
|
// trie is evicted and the re-created account starts with a fresh trie.
|
||||||
// as it assumes the main trie is already loaded.
|
var (
|
||||||
if s.trie == nil {
|
delAddrs []common.Address
|
||||||
tr, err := s.db.OpenTrie(s.originalRoot)
|
delAccts []AccountMut
|
||||||
if err != nil {
|
start = time.Now()
|
||||||
|
)
|
||||||
|
for addr, op := range s.mutations {
|
||||||
|
if !op.precedingDelete {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
op.precedingDelete = false
|
||||||
|
|
||||||
|
delAddrs = append(delAddrs, addr)
|
||||||
|
delAccts = append(delAccts, AccountMut{Account: nil})
|
||||||
|
}
|
||||||
|
if len(delAddrs) > 0 {
|
||||||
|
if err := s.hasher.UpdateAccount(delAddrs, delAccts); err != nil {
|
||||||
s.setError(err)
|
s.setError(err)
|
||||||
return common.Hash{}
|
return common.Hash{}
|
||||||
}
|
}
|
||||||
s.trie = tr
|
s.AccountDeleted += len(delAddrs)
|
||||||
}
|
}
|
||||||
// If there was a trie prefetcher operating, terminate it async so that the
|
s.AccountUpdates += time.Since(start)
|
||||||
// individual storage tries can be updated as soon as the disk load finishes.
|
|
||||||
if s.prefetcher != nil {
|
|
||||||
s.prefetcher.terminate(true)
|
|
||||||
defer func() {
|
|
||||||
s.prefetcher.report()
|
|
||||||
s.prefetcher = nil // Pre-byzantium, unset any used up prefetcher
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
// Process all storage updates concurrently. The state object update root
|
|
||||||
// method will internally call a blocking trie fetch from the prefetcher,
|
|
||||||
// so there's no need to explicitly wait for the prefetchers to finish.
|
|
||||||
var (
|
|
||||||
start = time.Now()
|
|
||||||
workers errgroup.Group
|
|
||||||
)
|
|
||||||
if s.db.TrieDB().IsVerkle() {
|
|
||||||
// Bypass per-account updateTrie() for binary trie. In binary trie mode
|
|
||||||
// there is only one unified trie (OpenStorageTrie returns self), so the
|
|
||||||
// per-account trie setup in updateTrie() (getPrefetchedTrie, getTrie,
|
|
||||||
// prefetcher.used) is redundant overhead. Apply all storage updates
|
|
||||||
// directly in a single pass.
|
|
||||||
for addr, op := range s.mutations {
|
|
||||||
if op.applied || op.isDelete() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
obj := s.stateObjects[addr]
|
|
||||||
if len(obj.uncommittedStorage) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for key, origin := range obj.uncommittedStorage {
|
|
||||||
value, exist := obj.pendingStorage[key]
|
|
||||||
if value == origin || !exist {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (value != common.Hash{}) {
|
|
||||||
if err := s.trie.UpdateStorage(addr, key[:], common.TrimLeftZeroes(value[:])); err != nil {
|
|
||||||
s.setError(err)
|
|
||||||
}
|
|
||||||
s.StorageUpdated.Add(1)
|
|
||||||
} else {
|
|
||||||
if err := s.trie.DeleteStorage(addr, key[:]); err != nil {
|
|
||||||
s.setError(err)
|
|
||||||
}
|
|
||||||
s.StorageDeleted.Add(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Clear uncommittedStorage and assign trie on each touched object.
|
|
||||||
// obj.trie must be set because this path bypasses updateTrie(), which
|
|
||||||
// is where obj.trie normally gets lazily loaded via getTrie().
|
|
||||||
for addr, op := range s.mutations {
|
|
||||||
if op.applied || op.isDelete() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
obj := s.stateObjects[addr]
|
|
||||||
if len(obj.uncommittedStorage) > 0 {
|
|
||||||
obj.uncommittedStorage = make(Storage)
|
|
||||||
}
|
|
||||||
obj.trie = s.trie
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for addr, op := range s.mutations {
|
|
||||||
if op.applied || op.isDelete() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
obj := s.stateObjects[addr] // closure for the task runner below
|
|
||||||
workers.Go(func() error {
|
|
||||||
obj.updateRoot()
|
|
||||||
|
|
||||||
// If witness building is enabled and the state object has a trie,
|
// Process all storage updates concurrently, flushing them to hasher.
|
||||||
// gather the witnesses for its specific storage trie
|
start = time.Now()
|
||||||
if s.witness != nil && obj.trie != nil {
|
var workers errgroup.Group
|
||||||
s.witness.AddState(obj.trie.Witness(), obj.addrHash())
|
for addr, op := range s.mutations {
|
||||||
}
|
if op.applied || op.isDelete() {
|
||||||
return nil
|
continue
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
obj := s.stateObjects[addr]
|
||||||
|
workers.Go(obj.updateTrie)
|
||||||
}
|
}
|
||||||
// If witness building is enabled, gather all the read-only accesses.
|
if err := workers.Wait(); err != nil {
|
||||||
// Skip witness collection in Verkle mode, they will be gathered
|
s.setError(err)
|
||||||
// together at the end.
|
|
||||||
if s.witness != nil && !s.db.TrieDB().IsVerkle() {
|
|
||||||
// Pull in anything that has been accessed before destruction
|
|
||||||
for _, obj := range s.stateObjectsDestruct {
|
|
||||||
// Skip any objects that haven't touched their storage
|
|
||||||
if len(obj.originStorage) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if trie := obj.getPrefetchedTrie(); trie != nil {
|
|
||||||
s.witness.AddState(trie.Witness(), obj.addrHash())
|
|
||||||
} else if obj.trie != nil {
|
|
||||||
s.witness.AddState(obj.trie.Witness(), obj.addrHash())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Pull in only-read and non-destructed trie witnesses
|
|
||||||
for _, obj := range s.stateObjects {
|
|
||||||
// Skip any objects that have been updated
|
|
||||||
if _, ok := s.mutations[obj.address]; ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// Skip any objects that haven't touched their storage
|
|
||||||
if len(obj.originStorage) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if trie := obj.getPrefetchedTrie(); trie != nil {
|
|
||||||
s.witness.AddState(trie.Witness(), obj.addrHash())
|
|
||||||
} else if obj.trie != nil {
|
|
||||||
s.witness.AddState(obj.trie.Witness(), obj.addrHash())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
workers.Wait()
|
|
||||||
s.StorageUpdates += time.Since(start)
|
s.StorageUpdates += time.Since(start)
|
||||||
|
|
||||||
// Now we're about to start to write changes to the trie. The trie is so far
|
// Process all account updates
|
||||||
// _untouched_. We can check with the prefetcher, if it can give us a trie
|
|
||||||
// which has the same root, but also has some content loaded into it.
|
|
||||||
//
|
|
||||||
// Don't check prefetcher if verkle trie has been used. In the context of verkle,
|
|
||||||
// only a single trie is used for state hashing. Replacing a non-nil verkle tree
|
|
||||||
// here could result in losing uncommitted changes from storage.
|
|
||||||
start = time.Now()
|
|
||||||
if s.prefetcher != nil && !s.db.TrieDB().IsVerkle() {
|
|
||||||
if trie := s.prefetcher.trie(common.Hash{}, s.originalRoot); trie == nil {
|
|
||||||
log.Error("Failed to retrieve account pre-fetcher trie")
|
|
||||||
} else {
|
|
||||||
s.trie = trie
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Perform updates before deletions. This prevents resolution of unnecessary trie nodes
|
|
||||||
// in circumstances similar to the following:
|
|
||||||
//
|
|
||||||
// Consider nodes `A` and `B` who share the same full node parent `P` and have no other siblings.
|
|
||||||
// During the execution of a block:
|
|
||||||
// - `A` self-destructs,
|
|
||||||
// - `C` is created, and also shares the parent `P`.
|
|
||||||
// If the self-destruct is handled first, then `P` would be left with only one child, thus collapsed
|
|
||||||
// into a shortnode. This requires `B` to be resolved from disk.
|
|
||||||
// Whereas if the created node is handled first, then the collapse is avoided, and `B` is not resolved.
|
|
||||||
var (
|
var (
|
||||||
usedAddrs []common.Address
|
addresses []common.Address
|
||||||
deletedAddrs []common.Address
|
accounts []AccountMut
|
||||||
)
|
)
|
||||||
|
start = time.Now()
|
||||||
for addr, op := range s.mutations {
|
for addr, op := range s.mutations {
|
||||||
if op.applied {
|
if op.applied {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
op.applied = true
|
op.applied = true
|
||||||
|
addresses = append(addresses, addr)
|
||||||
|
|
||||||
if op.isDelete() {
|
if op.isDelete() {
|
||||||
deletedAddrs = append(deletedAddrs, addr)
|
accounts = append(accounts, AccountMut{Account: nil})
|
||||||
} else {
|
s.AccountDeleted += 1
|
||||||
obj := s.stateObjects[addr]
|
continue
|
||||||
s.updateStateObject(obj)
|
}
|
||||||
s.AccountUpdated += 1
|
obj := s.stateObjects[addr]
|
||||||
|
// CodeSize must be the account's CURRENT total code size, even for
|
||||||
|
// non-code-touching mutations. obj.CodeSize() returns len(obj.code)
|
||||||
|
// when the code is loaded, otherwise falls back to a code-size
|
||||||
|
// lookup via the reader. Hashers that pack code size into the
|
||||||
|
// on-trie account encoding (e.g. the binary trie BasicData leaf,
|
||||||
|
// per EIP-7864) rely on this value. Passing the default 0 here on
|
||||||
|
// a balance/nonce-only update would silently corrupt the BasicData
|
||||||
|
// leaf of every contract touched without a code write.
|
||||||
|
mut := AccountMut{
|
||||||
|
Account: &obj.data,
|
||||||
|
CodeSize: obj.CodeSize(),
|
||||||
|
}
|
||||||
|
if obj.dirtyCode {
|
||||||
|
mut.Code = &CodeMut{Code: obj.code}
|
||||||
|
|
||||||
// Count code writes post-Finalise so reverted CREATEs are excluded.
|
// Count code writes post-Finalise so reverted CREATEs are excluded.
|
||||||
if obj.dirtyCode {
|
s.CodeUpdated += 1
|
||||||
s.CodeUpdated += 1
|
s.CodeUpdateBytes += len(obj.code)
|
||||||
s.CodeUpdateBytes += len(obj.code)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
usedAddrs = append(usedAddrs, addr) // Copy needed for closure
|
accounts = append(accounts, mut)
|
||||||
|
s.AccountUpdated += 1
|
||||||
}
|
}
|
||||||
for _, deletedAddr := range deletedAddrs {
|
if err := s.hasher.UpdateAccount(addresses, accounts); err != nil {
|
||||||
s.deleteStateObject(deletedAddr)
|
s.setError(err)
|
||||||
s.AccountDeleted += 1
|
return common.Hash{}
|
||||||
}
|
}
|
||||||
s.AccountUpdates += time.Since(start)
|
s.AccountUpdates += time.Since(start)
|
||||||
|
|
||||||
if s.prefetcher != nil {
|
|
||||||
s.prefetcher.used(common.Hash{}, s.originalRoot, usedAddrs, nil)
|
|
||||||
}
|
|
||||||
// Track the amount of time wasted on hashing the account trie
|
// Track the amount of time wasted on hashing the account trie
|
||||||
defer func(start time.Time) { s.AccountHashes += time.Since(start) }(time.Now())
|
defer func(start time.Time) { s.AccountHashes += time.Since(start) }(time.Now())
|
||||||
|
|
||||||
hash := s.trie.Hash()
|
return s.hasher.Hash()
|
||||||
|
|
||||||
// If witness building is enabled, gather the account trie witness
|
|
||||||
if s.witness != nil {
|
|
||||||
s.witness.AddState(s.trie.Witness(), common.Hash{})
|
|
||||||
}
|
|
||||||
return hash
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetTxContext sets the current transaction hash and index which are
|
// SetTxContext sets the current transaction hash and index which are
|
||||||
|
|
@ -1045,11 +835,11 @@ func (s *StateDB) clearJournalAndRefund() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// deleteStorage is designed to delete the storage trie of a designated account.
|
// deleteStorage is designed to delete the storage trie of a designated account.
|
||||||
func (s *StateDB) deleteStorage(addrHash common.Hash, root common.Hash) (map[common.Hash][]byte, map[common.Hash][]byte, *trienode.NodeSet, error) {
|
func (s *StateDB) deleteStorage(addrHash common.Hash) (map[common.Hash]common.Hash, map[common.Hash]common.Hash, *trienode.NodeSet, error) {
|
||||||
var (
|
var (
|
||||||
nodes = trienode.NewNodeSet(addrHash) // the set for trie node mutations (value is nil)
|
nodes = trienode.NewNodeSet(addrHash) // the set for trie node mutations (value is nil)
|
||||||
storages = make(map[common.Hash][]byte) // the set for storage mutations (value is nil)
|
storages = make(map[common.Hash]common.Hash) // the set for storage mutations (value is nil)
|
||||||
storageOrigins = make(map[common.Hash][]byte) // the set for tracking the original value of slot
|
storageOrigins = make(map[common.Hash]common.Hash) // the set for tracking the original value of slot
|
||||||
)
|
)
|
||||||
iteratee, err := s.db.Iteratee(s.originalRoot)
|
iteratee, err := s.db.Iteratee(s.originalRoot)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -1070,8 +860,15 @@ func (s *StateDB) deleteStorage(addrHash common.Hash, root common.Hash) (map[com
|
||||||
return nil, nil, nil, err
|
return nil, nil, nil, err
|
||||||
}
|
}
|
||||||
key := it.Hash()
|
key := it.Hash()
|
||||||
storages[key] = nil
|
storages[key] = common.Hash{}
|
||||||
storageOrigins[key] = slot
|
|
||||||
|
_, content, _, err := rlp.Split(slot)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
var value common.Hash
|
||||||
|
value.SetBytes(content)
|
||||||
|
storageOrigins[key] = value
|
||||||
|
|
||||||
if err := stack.Update(key.Bytes(), slot); err != nil {
|
if err := stack.Update(key.Bytes(), slot); err != nil {
|
||||||
return nil, nil, nil, err
|
return nil, nil, nil, err
|
||||||
|
|
@ -1080,9 +877,7 @@ func (s *StateDB) deleteStorage(addrHash common.Hash, root common.Hash) (map[com
|
||||||
if err := it.Error(); err != nil { // error might occur during iteration
|
if err := it.Error(); err != nil { // error might occur during iteration
|
||||||
return nil, nil, nil, err
|
return nil, nil, nil, err
|
||||||
}
|
}
|
||||||
if stack.Hash() != root {
|
stack.Hash() // Commit the right boundary
|
||||||
return nil, nil, nil, fmt.Errorf("snapshot is not matched, exp %x, got %x", root, stack.Hash())
|
|
||||||
}
|
|
||||||
return storages, storageOrigins, nodes, nil
|
return storages, storageOrigins, nodes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1104,9 +899,9 @@ func (s *StateDB) deleteStorage(addrHash common.Hash, root common.Hash) (map[com
|
||||||
// with their values be tracked as original value.
|
// with their values be tracked as original value.
|
||||||
// In case (d), **original** account along with its storages should be deleted,
|
// In case (d), **original** account along with its storages should be deleted,
|
||||||
// with their values be tracked as original value.
|
// with their values be tracked as original value.
|
||||||
func (s *StateDB) handleDestruction(noStorageWiping bool) (map[common.Hash]*accountDelete, []*trienode.NodeSet, error) {
|
func (s *StateDB) handleDestruction(noStorageWiping bool) (map[common.Hash]*accountDelete, *trienode.MergedNodeSet, error) {
|
||||||
var (
|
var (
|
||||||
nodes []*trienode.NodeSet
|
nodes = trienode.NewMergedNodeSet()
|
||||||
deletes = make(map[common.Hash]*accountDelete)
|
deletes = make(map[common.Hash]*accountDelete)
|
||||||
)
|
)
|
||||||
for addr, prevObj := range s.stateObjectsDestruct {
|
for addr, prevObj := range s.stateObjectsDestruct {
|
||||||
|
|
@ -1124,36 +919,32 @@ func (s *StateDB) handleDestruction(noStorageWiping bool) (map[common.Hash]*acco
|
||||||
addrHash := crypto.Keccak256Hash(addr.Bytes())
|
addrHash := crypto.Keccak256Hash(addr.Bytes())
|
||||||
op := &accountDelete{
|
op := &accountDelete{
|
||||||
address: addr,
|
address: addr,
|
||||||
origin: types.SlimAccountRLP(*prev),
|
origin: *prev,
|
||||||
}
|
}
|
||||||
deletes[addrHash] = op
|
deletes[addrHash] = op
|
||||||
|
|
||||||
// Short circuit if the origin storage was empty.
|
// Short circuit if the origin storage was empty.
|
||||||
if prev.Root == types.EmptyRootHash || s.db.TrieDB().IsVerkle() {
|
if s.db.TrieDB().IsVerkle() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if noStorageWiping {
|
if noStorageWiping {
|
||||||
return nil, nil, fmt.Errorf("unexpected storage wiping, %x", addr)
|
return nil, nil, fmt.Errorf("unexpected storage wiping, %x", addr)
|
||||||
}
|
}
|
||||||
// Remove storage slots belonging to the account.
|
// Remove storage slots belonging to the account.
|
||||||
storages, storagesOrigin, set, err := s.deleteStorage(addrHash, prev.Root)
|
storages, storagesOrigin, set, err := s.deleteStorage(addrHash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("failed to delete storage, err: %w", err)
|
return nil, nil, fmt.Errorf("failed to delete storage, err: %w", err)
|
||||||
}
|
}
|
||||||
op.storages = storages
|
op.storages, op.storagesOrigin = storages, storagesOrigin
|
||||||
op.storagesOrigin = storagesOrigin
|
|
||||||
|
|
||||||
// Aggregate the associated trie node changes.
|
// Aggregate the associated trie node changes.
|
||||||
nodes = append(nodes, set)
|
if err := nodes.Merge(set); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return deletes, nodes, nil
|
return deletes, nodes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTrie returns the account trie.
|
|
||||||
func (s *StateDB) GetTrie() Trie {
|
|
||||||
return s.trie
|
|
||||||
}
|
|
||||||
|
|
||||||
// commit gathers the state mutations accumulated along with the associated
|
// commit gathers the state mutations accumulated along with the associated
|
||||||
// trie changes, resetting all internal flags with the new state as the base.
|
// trie changes, resetting all internal flags with the new state as the base.
|
||||||
func (s *StateDB) commit(deleteEmptyObjects bool, noStorageWiping bool, blockNumber uint64) (*stateUpdate, error) {
|
func (s *StateDB) commit(deleteEmptyObjects bool, noStorageWiping bool, blockNumber uint64) (*stateUpdate, error) {
|
||||||
|
|
@ -1168,89 +959,16 @@ func (s *StateDB) commit(deleteEmptyObjects bool, noStorageWiping bool, blockNum
|
||||||
if s.dbErr != nil {
|
if s.dbErr != nil {
|
||||||
return nil, fmt.Errorf("commit aborted due to database error: %v", s.dbErr)
|
return nil, fmt.Errorf("commit aborted due to database error: %v", s.dbErr)
|
||||||
}
|
}
|
||||||
// Commit objects to the trie, measuring the elapsed time
|
|
||||||
var (
|
|
||||||
accountTrieNodesUpdated int
|
|
||||||
accountTrieNodesDeleted int
|
|
||||||
storageTrieNodesUpdated int
|
|
||||||
storageTrieNodesDeleted int
|
|
||||||
|
|
||||||
lock sync.Mutex // protect two maps below
|
|
||||||
nodes = trienode.NewMergedNodeSet() // aggregated trie nodes
|
|
||||||
updates = make(map[common.Hash]*accountUpdate, len(s.mutations)) // aggregated account updates
|
|
||||||
|
|
||||||
// merge aggregates the dirty trie nodes into the global set.
|
|
||||||
//
|
|
||||||
// Given that some accounts may be destroyed and then recreated within
|
|
||||||
// the same block, it's possible that a node set with the same owner
|
|
||||||
// may already exist. In such cases, these two sets are combined, with
|
|
||||||
// the later one overwriting the previous one if any nodes are modified
|
|
||||||
// or deleted in both sets.
|
|
||||||
//
|
|
||||||
// merge run concurrently across all the state objects and account trie.
|
|
||||||
merge = func(set *trienode.NodeSet) error {
|
|
||||||
if set == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
lock.Lock()
|
|
||||||
defer lock.Unlock()
|
|
||||||
|
|
||||||
updates, deletes := set.Size()
|
|
||||||
if set.Owner == (common.Hash{}) {
|
|
||||||
accountTrieNodesUpdated += updates
|
|
||||||
accountTrieNodesDeleted += deletes
|
|
||||||
} else {
|
|
||||||
storageTrieNodesUpdated += updates
|
|
||||||
storageTrieNodesDeleted += deletes
|
|
||||||
}
|
|
||||||
return nodes.Merge(set)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
// Given that some accounts could be destroyed and then recreated within
|
// Given that some accounts could be destroyed and then recreated within
|
||||||
// the same block, account deletions must be processed first. This ensures
|
// the same block, account deletions must be processed first. This ensures
|
||||||
// that the storage trie nodes deleted during destruction and recreated
|
// that the storage trie nodes deleted during destruction and recreated
|
||||||
// during subsequent resurrection can be combined correctly.
|
// during subsequent resurrection can be combined correctly.
|
||||||
deletes, delNodes, err := s.handleDestruction(noStorageWiping)
|
deletes, nodes, err := s.handleDestruction(noStorageWiping)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
for _, set := range delNodes {
|
// Aggregated account updates
|
||||||
if err := merge(set); err != nil {
|
updates := make(map[common.Hash]*accountUpdate, len(s.mutations))
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Handle all state updates afterwards, concurrently to one another to shave
|
|
||||||
// off some milliseconds from the commit operation. Also accumulate the code
|
|
||||||
// writes to run in parallel with the computations.
|
|
||||||
var (
|
|
||||||
start = time.Now()
|
|
||||||
workers errgroup.Group
|
|
||||||
)
|
|
||||||
// Schedule the account trie first since that will be the biggest, so give
|
|
||||||
// it the most time to crunch.
|
|
||||||
//
|
|
||||||
// TODO(karalabe): This account trie commit is *very* heavy. 5-6ms at chain
|
|
||||||
// heads, which seems excessive given that it doesn't do hashing, it just
|
|
||||||
// shuffles some data. For comparison, the *hashing* at chain head is 2-3ms.
|
|
||||||
// We need to investigate what's happening as it seems something's wonky.
|
|
||||||
// Obviously it's not an end of the world issue, just something the original
|
|
||||||
// code didn't anticipate for.
|
|
||||||
workers.Go(func() error {
|
|
||||||
// Write the account trie changes, measuring the amount of wasted time
|
|
||||||
_, set := s.trie.Commit(true)
|
|
||||||
if err := merge(set); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
s.AccountCommits = time.Since(start)
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
// Schedule each of the storage tries that need to be updated, so they can
|
|
||||||
// run concurrently to one another.
|
|
||||||
//
|
|
||||||
// TODO(karalabe): Experimentally, the account commit takes approximately the
|
|
||||||
// same time as all the storage commits combined, so we could maybe only have
|
|
||||||
// 2 threads in total. But that kind of depends on the account commit being
|
|
||||||
// more expensive than it should be, so let's fix that and revisit this todo.
|
|
||||||
for addr, op := range s.mutations {
|
for addr, op := range s.mutations {
|
||||||
if op.isDelete() {
|
if op.isDelete() {
|
||||||
continue
|
continue
|
||||||
|
|
@ -1260,44 +978,25 @@ func (s *StateDB) commit(deleteEmptyObjects bool, noStorageWiping bool, blockNum
|
||||||
if obj == nil {
|
if obj == nil {
|
||||||
return nil, errors.New("missing state object")
|
return nil, errors.New("missing state object")
|
||||||
}
|
}
|
||||||
// Run the storage updates concurrently to one another
|
update, err := obj.commit()
|
||||||
workers.Go(func() error {
|
if err != nil {
|
||||||
// Write any storage changes in the state object to its storage trie
|
return nil, err
|
||||||
update, set, err := obj.commit()
|
}
|
||||||
if err != nil {
|
updates[obj.addrHash()] = update
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := merge(set); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
lock.Lock()
|
|
||||||
updates[obj.addrHash()] = update
|
|
||||||
s.StorageCommits = time.Since(start) // overwrite with the longest storage commit runtime
|
|
||||||
lock.Unlock()
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
// Wait for everything to finish and update the metrics
|
// Handle all state updates afterwards, concurrently to one another to shave
|
||||||
if err := workers.Wait(); err != nil {
|
// off some milliseconds from the commit operation. Also accumulate the code
|
||||||
|
// writes to run in parallel with the computations.
|
||||||
|
start := time.Now()
|
||||||
|
root, set, secondaryHashes, err := s.hasher.Commit()
|
||||||
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
accountReadMeters.Mark(int64(s.AccountLoaded))
|
s.HasherCommits = time.Since(start)
|
||||||
storageReadMeters.Mark(int64(s.StorageLoaded))
|
|
||||||
accountUpdatedMeter.Mark(int64(s.AccountUpdated))
|
|
||||||
storageUpdatedMeter.Mark(s.StorageUpdated.Load())
|
|
||||||
accountDeletedMeter.Mark(int64(s.AccountDeleted))
|
|
||||||
storageDeletedMeter.Mark(s.StorageDeleted.Load())
|
|
||||||
accountTrieUpdatedMeter.Mark(int64(accountTrieNodesUpdated))
|
|
||||||
accountTrieDeletedMeter.Mark(int64(accountTrieNodesDeleted))
|
|
||||||
storageTriesUpdatedMeter.Mark(int64(storageTrieNodesUpdated))
|
|
||||||
storageTriesDeletedMeter.Mark(int64(storageTrieNodesDeleted))
|
|
||||||
|
|
||||||
// Clear the metric markers
|
|
||||||
s.AccountLoaded, s.AccountUpdated, s.AccountDeleted = 0, 0, 0
|
|
||||||
s.StorageLoaded = 0
|
|
||||||
s.StorageUpdated.Store(0)
|
|
||||||
s.StorageDeleted.Store(0)
|
|
||||||
|
|
||||||
|
if err := nodes.MergeSet(set); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
// Clear all internal flags and update state root at the end.
|
// Clear all internal flags and update state root at the end.
|
||||||
s.mutations = make(map[common.Address]*mutation)
|
s.mutations = make(map[common.Address]*mutation)
|
||||||
s.stateObjectsDestruct = make(map[common.Address]*stateObject)
|
s.stateObjectsDestruct = make(map[common.Address]*stateObject)
|
||||||
|
|
@ -1305,7 +1004,22 @@ func (s *StateDB) commit(deleteEmptyObjects bool, noStorageWiping bool, blockNum
|
||||||
origin := s.originalRoot
|
origin := s.originalRoot
|
||||||
s.originalRoot = root
|
s.originalRoot = root
|
||||||
|
|
||||||
return newStateUpdate(noStorageWiping, origin, root, blockNumber, deletes, updates, nodes), nil
|
if s.witness != nil {
|
||||||
|
builder, ok := s.hasher.(WitnessCollector)
|
||||||
|
if ok {
|
||||||
|
builder.CollectWitness(s.witness)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If the hasher tracks flat-state leaf production (currently only the
|
||||||
|
// binary hasher), drain the buffered stem writes so the downstream
|
||||||
|
// state update can carry them into the pathdb flat-state layer. Merkle
|
||||||
|
// hashers do not implement this interface and the call short-circuits
|
||||||
|
// to nil — newStateUpdate accepts nil as "no leaves".
|
||||||
|
var leaves []StemWrite
|
||||||
|
if producer, ok := s.hasher.(LeafProducer); ok {
|
||||||
|
leaves = producer.DrainStemWrites()
|
||||||
|
}
|
||||||
|
return newStateUpdate(noStorageWiping, origin, root, blockNumber, deletes, updates, nodes, secondaryHashes, leaves), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// commitAndFlush is a wrapper of commit which also commits the state mutations
|
// commitAndFlush is a wrapper of commit which also commits the state mutations
|
||||||
|
|
@ -1326,10 +1040,19 @@ func (s *StateDB) commitAndFlush(block uint64, deleteEmptyObjects bool, noStorag
|
||||||
}
|
}
|
||||||
s.DatabaseCommits = time.Since(start)
|
s.DatabaseCommits = time.Since(start)
|
||||||
|
|
||||||
// The reader update must be performed as the final step, otherwise,
|
reader, err := s.db.Reader(s.originalRoot)
|
||||||
// the new state would not be visible before db.commit.
|
if err != nil {
|
||||||
s.reader, _ = s.db.Reader(s.originalRoot)
|
return nil, err
|
||||||
return ret, err
|
}
|
||||||
|
s.reader = reader
|
||||||
|
|
||||||
|
hasher, err := s.db.Hasher(s.originalRoot)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
s.hasher = hasher
|
||||||
|
|
||||||
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Commit writes the state mutations into the configured data stores.
|
// Commit writes the state mutations into the configured data stores.
|
||||||
|
|
@ -1440,25 +1163,6 @@ func (s *StateDB) SlotInAccessList(addr common.Address, slot common.Hash) (addre
|
||||||
return s.accessList.Contains(addr, slot)
|
return s.accessList.Contains(addr, slot)
|
||||||
}
|
}
|
||||||
|
|
||||||
// markDelete is invoked when an account is deleted but the deletion is
|
|
||||||
// not yet committed. The pending mutation is cached and will be applied
|
|
||||||
// all together
|
|
||||||
func (s *StateDB) markDelete(addr common.Address) {
|
|
||||||
if _, ok := s.mutations[addr]; !ok {
|
|
||||||
s.mutations[addr] = &mutation{}
|
|
||||||
}
|
|
||||||
s.mutations[addr].applied = false
|
|
||||||
s.mutations[addr].typ = deletion
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *StateDB) markUpdate(addr common.Address) {
|
|
||||||
if _, ok := s.mutations[addr]; !ok {
|
|
||||||
s.mutations[addr] = &mutation{}
|
|
||||||
}
|
|
||||||
s.mutations[addr].applied = false
|
|
||||||
s.mutations[addr].typ = update
|
|
||||||
}
|
|
||||||
|
|
||||||
// Witness retrieves the current state witness being collected.
|
// Witness retrieves the current state witness being collected.
|
||||||
func (s *StateDB) Witness() *stateless.Witness {
|
func (s *StateDB) Witness() *stateless.Witness {
|
||||||
return s.witness
|
return s.witness
|
||||||
|
|
@ -1467,3 +1171,15 @@ func (s *StateDB) Witness() *stateless.Witness {
|
||||||
func (s *StateDB) AccessEvents() *AccessEvents {
|
func (s *StateDB) AccessEvents() *AccessEvents {
|
||||||
return s.accessEvents
|
return s.accessEvents
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StopPrefetcher terminates all the background prefetching activities.
|
||||||
|
func (s *StateDB) StopPrefetcher() {
|
||||||
|
if s.hasher == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
prefetch, ok := s.hasher.(Prefetcher)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
prefetch.TermPrefetch()
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -183,10 +183,11 @@ func (test *stateTest) run() bool {
|
||||||
storages []map[common.Hash]map[common.Hash][]byte
|
storages []map[common.Hash]map[common.Hash][]byte
|
||||||
storageOrigin []map[common.Address]map[common.Hash][]byte
|
storageOrigin []map[common.Address]map[common.Hash][]byte
|
||||||
copyUpdate = func(update *stateUpdate) {
|
copyUpdate = func(update *stateUpdate) {
|
||||||
accounts = append(accounts, maps.Clone(update.accounts))
|
encoded, _ := update.stateSet(true)
|
||||||
accountOrigin = append(accountOrigin, maps.Clone(update.accountsOrigin))
|
accounts = append(accounts, maps.Clone(encoded.Accounts))
|
||||||
storages = append(storages, maps.Clone(update.storages))
|
accountOrigin = append(accountOrigin, maps.Clone(encoded.AccountsOrigin))
|
||||||
storageOrigin = append(storageOrigin, maps.Clone(update.storagesOrigin))
|
storages = append(storages, maps.Clone(encoded.Storages))
|
||||||
|
storageOrigin = append(storageOrigin, maps.Clone(encoded.StoragesOrigin))
|
||||||
}
|
}
|
||||||
disk = rawdb.NewMemoryDatabase()
|
disk = rawdb.NewMemoryDatabase()
|
||||||
tdb = triedb.NewDatabase(disk, &triedb.Config{PathDB: pathdb.Defaults})
|
tdb = triedb.NewDatabase(disk, &triedb.Config{PathDB: pathdb.Defaults})
|
||||||
|
|
|
||||||
61
core/state/statedb_stats.go
Normal file
61
core/state/statedb_stats.go
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
// Copyright 2026 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 state
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Stats contains all measurements gathered during state execution for
|
||||||
|
// debugging and metrics purposes.
|
||||||
|
type Stats struct {
|
||||||
|
AccountReads time.Duration // Account read time
|
||||||
|
StorageReads time.Duration // Storage read time
|
||||||
|
CodeReads time.Duration // Code read time
|
||||||
|
AccountHashes time.Duration // Account trie hash time
|
||||||
|
AccountUpdates time.Duration // Account trie update time
|
||||||
|
StorageUpdates time.Duration // Storage trie update and hash time
|
||||||
|
HasherCommits time.Duration // Trie commit time
|
||||||
|
DatabaseCommits time.Duration // Database commit time
|
||||||
|
|
||||||
|
AccountLoaded int // Number of accounts retrieved from the database during the state transition
|
||||||
|
AccountUpdated int // Number of accounts updated during the state transition
|
||||||
|
AccountDeleted int // Number of accounts deleted during the state transition
|
||||||
|
StorageLoaded int // Number of storage slots retrieved from the database during the state transition
|
||||||
|
StorageUpdated atomic.Int64 // Number of storage slots updated during the state transition
|
||||||
|
StorageDeleted atomic.Int64 // Number of storage slots deleted during the state transition
|
||||||
|
|
||||||
|
// CodeLoadBytes is the total number of bytes read from contract code.
|
||||||
|
// This value may be smaller than the actual number of bytes read, since
|
||||||
|
// some APIs (e.g. CodeSize) may load the entire code from either the
|
||||||
|
// cache or the database when the size is not available in the cache.
|
||||||
|
CodeLoaded int // Number of contract code loaded during the state transition
|
||||||
|
CodeLoadBytes int // Total bytes of resolved code
|
||||||
|
CodeUpdated int // Number of contracts with code changes that persisted
|
||||||
|
CodeUpdateBytes int // Total bytes of persisted code written
|
||||||
|
}
|
||||||
|
|
||||||
|
// StateReadTime returns the total time spent on the state read.
|
||||||
|
func (s *Stats) StateReadTime() time.Duration {
|
||||||
|
return s.AccountReads + s.StorageReads + s.CodeReads
|
||||||
|
}
|
||||||
|
|
||||||
|
// StateHashTime returns the total time spent on the state hash.
|
||||||
|
func (s *Stats) StateHashTime() time.Duration {
|
||||||
|
return s.AccountHashes + s.AccountUpdates + s.StorageUpdates
|
||||||
|
}
|
||||||
|
|
@ -32,13 +32,8 @@ import (
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||||
"github.com/ethereum/go-ethereum/core/state/snapshot"
|
|
||||||
"github.com/ethereum/go-ethereum/core/tracing"
|
"github.com/ethereum/go-ethereum/core/tracing"
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
|
||||||
"github.com/ethereum/go-ethereum/trie"
|
|
||||||
"github.com/ethereum/go-ethereum/trie/trienode"
|
|
||||||
"github.com/ethereum/go-ethereum/triedb"
|
"github.com/ethereum/go-ethereum/triedb"
|
||||||
"github.com/ethereum/go-ethereum/triedb/hashdb"
|
"github.com/ethereum/go-ethereum/triedb/hashdb"
|
||||||
"github.com/ethereum/go-ethereum/triedb/pathdb"
|
"github.com/ethereum/go-ethereum/triedb/pathdb"
|
||||||
|
|
@ -232,7 +227,7 @@ func TestCopyWithDirtyJournal(t *testing.T) {
|
||||||
for i := byte(0); i < 255; i++ {
|
for i := byte(0); i < 255; i++ {
|
||||||
obj := orig.getOrNewStateObject(common.BytesToAddress([]byte{i}))
|
obj := orig.getOrNewStateObject(common.BytesToAddress([]byte{i}))
|
||||||
obj.AddBalance(uint256.NewInt(uint64(i)))
|
obj.AddBalance(uint256.NewInt(uint64(i)))
|
||||||
obj.data.Root = common.HexToHash("0xdeadbeef")
|
//obj.data.Root = common.HexToHash("0xdeadbeef")
|
||||||
}
|
}
|
||||||
root, _ := orig.Commit(0, true, false)
|
root, _ := orig.Commit(0, true, false)
|
||||||
orig, _ = New(root, db)
|
orig, _ = New(root, db)
|
||||||
|
|
@ -275,7 +270,7 @@ func TestCopyObjectState(t *testing.T) {
|
||||||
for i := byte(0); i < 5; i++ {
|
for i := byte(0); i < 5; i++ {
|
||||||
obj := orig.getOrNewStateObject(common.BytesToAddress([]byte{i}))
|
obj := orig.getOrNewStateObject(common.BytesToAddress([]byte{i}))
|
||||||
obj.AddBalance(uint256.NewInt(uint64(i)))
|
obj.AddBalance(uint256.NewInt(uint64(i)))
|
||||||
obj.data.Root = common.HexToHash("0xdeadbeef")
|
//obj.data.Root = common.HexToHash("0xdeadbeef")
|
||||||
}
|
}
|
||||||
orig.Finalise(true)
|
orig.Finalise(true)
|
||||||
cpy := orig.Copy()
|
cpy := orig.Copy()
|
||||||
|
|
@ -543,47 +538,6 @@ func (test *snapshotTest) run() bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func forEachStorage(s *StateDB, addr common.Address, cb func(key, value common.Hash) bool) error {
|
|
||||||
so := s.getStateObject(addr)
|
|
||||||
if so == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
tr, err := so.getTrie()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
trieIt, err := tr.NodeIterator(nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
var (
|
|
||||||
it = trie.NewIterator(trieIt)
|
|
||||||
visited = make(map[common.Hash]bool)
|
|
||||||
)
|
|
||||||
|
|
||||||
for it.Next() {
|
|
||||||
key := common.BytesToHash(tr.GetKey(it.Key))
|
|
||||||
visited[key] = true
|
|
||||||
if value, dirty := so.dirtyStorage[key]; dirty {
|
|
||||||
if !cb(key, value) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(it.Value) > 0 {
|
|
||||||
_, content, _, err := rlp.Split(it.Value)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !cb(key, common.BytesToHash(content)) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkEqual checks that methods of state and checkstate return the same values.
|
// checkEqual checks that methods of state and checkstate return the same values.
|
||||||
func (test *snapshotTest) checkEqual(state, checkstate *StateDB) error {
|
func (test *snapshotTest) checkEqual(state, checkstate *StateDB) error {
|
||||||
for _, addr := range test.addrs {
|
for _, addr := range test.addrs {
|
||||||
|
|
@ -609,12 +563,6 @@ func (test *snapshotTest) checkEqual(state, checkstate *StateDB) error {
|
||||||
}
|
}
|
||||||
// Check storage.
|
// Check storage.
|
||||||
if obj := state.getStateObject(addr); obj != nil {
|
if obj := state.getStateObject(addr); obj != nil {
|
||||||
forEachStorage(state, addr, func(key, value common.Hash) bool {
|
|
||||||
return checkeq("GetState("+key.Hex()+")", checkstate.GetState(addr, key), value)
|
|
||||||
})
|
|
||||||
forEachStorage(checkstate, addr, func(key, value common.Hash) bool {
|
|
||||||
return checkeq("GetState("+key.Hex()+")", checkstate.GetState(addr, key), value)
|
|
||||||
})
|
|
||||||
other := checkstate.getStateObject(addr)
|
other := checkstate.getStateObject(addr)
|
||||||
// Check dirty storage which is not in trie
|
// Check dirty storage which is not in trie
|
||||||
if !maps.Equal(obj.dirtyStorage, other.dirtyStorage) {
|
if !maps.Equal(obj.dirtyStorage, other.dirtyStorage) {
|
||||||
|
|
@ -773,8 +721,14 @@ func TestCopyCommitCopy(t *testing.T) {
|
||||||
t.Fatalf("second copy committed storage slot mismatch: have %x, want %x", val, common.Hash{})
|
t.Fatalf("second copy committed storage slot mismatch: have %x, want %x", val, common.Hash{})
|
||||||
}
|
}
|
||||||
// Commit state, ensure states can be loaded from disk
|
// Commit state, ensure states can be loaded from disk
|
||||||
root, _ := state.Commit(0, false, false)
|
root, err := state.Commit(0, false, false)
|
||||||
state, _ = New(root, tdb)
|
if err != nil {
|
||||||
|
t.Fatalf("commit fail: %v", err)
|
||||||
|
}
|
||||||
|
state, err = New(root, tdb)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("New fail: %v", err)
|
||||||
|
}
|
||||||
if balance := state.GetBalance(addr); balance.Cmp(uint256.NewInt(42)) != 0 {
|
if balance := state.GetBalance(addr); balance.Cmp(uint256.NewInt(42)) != 0 {
|
||||||
t.Fatalf("state post-commit balance mismatch: have %v, want %v", balance, 42)
|
t.Fatalf("state post-commit balance mismatch: have %v, want %v", balance, 42)
|
||||||
}
|
}
|
||||||
|
|
@ -1269,60 +1223,6 @@ func TestStateDBTransientStorage(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDeleteStorage(t *testing.T) {
|
|
||||||
var (
|
|
||||||
disk = rawdb.NewMemoryDatabase()
|
|
||||||
tdb = triedb.NewDatabase(disk, nil)
|
|
||||||
snaps, _ = snapshot.New(snapshot.Config{CacheSize: 10}, disk, tdb, types.EmptyRootHash)
|
|
||||||
db = NewDatabase(tdb, nil).WithSnapshot(snaps)
|
|
||||||
state, _ = New(types.EmptyRootHash, db)
|
|
||||||
addr = common.HexToAddress("0x1")
|
|
||||||
)
|
|
||||||
// Initialize account and populate storage
|
|
||||||
state.SetBalance(addr, uint256.NewInt(1), tracing.BalanceChangeUnspecified)
|
|
||||||
state.CreateAccount(addr)
|
|
||||||
for i := 0; i < 1000; i++ {
|
|
||||||
slot := common.Hash(uint256.NewInt(uint64(i)).Bytes32())
|
|
||||||
value := common.Hash(uint256.NewInt(uint64(10 * i)).Bytes32())
|
|
||||||
state.SetState(addr, slot, value)
|
|
||||||
}
|
|
||||||
root, _ := state.Commit(0, true, false)
|
|
||||||
// Init phase done, create two states, one with snap and one without
|
|
||||||
fastState, _ := New(root, NewDatabase(tdb, nil).WithSnapshot(snaps))
|
|
||||||
slowState, _ := New(root, NewDatabase(tdb, nil))
|
|
||||||
|
|
||||||
obj := fastState.getOrNewStateObject(addr)
|
|
||||||
storageRoot := obj.data.Root
|
|
||||||
|
|
||||||
_, _, fastNodes, err := fastState.deleteStorage(crypto.Keccak256Hash(addr[:]), storageRoot)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, _, slowNodes, err := slowState.deleteStorage(crypto.Keccak256Hash(addr[:]), storageRoot)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
check := func(set *trienode.NodeSet) string {
|
|
||||||
var a []string
|
|
||||||
set.ForEachWithOrder(func(path string, n *trienode.Node) {
|
|
||||||
if n.Hash != (common.Hash{}) {
|
|
||||||
t.Fatal("delete should have empty hashes")
|
|
||||||
}
|
|
||||||
if len(n.Blob) != 0 {
|
|
||||||
t.Fatal("delete should have empty blobs")
|
|
||||||
}
|
|
||||||
a = append(a, fmt.Sprintf("%x", path))
|
|
||||||
})
|
|
||||||
return strings.Join(a, ",")
|
|
||||||
}
|
|
||||||
slowRes := check(slowNodes)
|
|
||||||
fastRes := check(fastNodes)
|
|
||||||
if slowRes != fastRes {
|
|
||||||
t.Fatalf("difference found:\nfast: %v\nslow: %v\n", fastRes, slowRes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStorageDirtiness(t *testing.T) {
|
func TestStorageDirtiness(t *testing.T) {
|
||||||
var (
|
var (
|
||||||
disk = rawdb.NewMemoryDatabase()
|
disk = rawdb.NewMemoryDatabase()
|
||||||
|
|
@ -1366,3 +1266,85 @@ func TestStorageDirtiness(t *testing.T) {
|
||||||
state.RevertToSnapshot(snap)
|
state.RevertToSnapshot(snap)
|
||||||
checkDirty(common.Hash{0x1}, common.Hash{0x1}, true)
|
checkDirty(common.Hash{0x1}, common.Hash{0x1}, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestVerkleCodeSizePreserved is a regression test for a latent bug in the
|
||||||
|
// binary-trie update path of binaryHasher: codeLen was derived from
|
||||||
|
// account.Code, which is only non-nil when the contract code itself was
|
||||||
|
// modified in the current block. For balance- or nonce-only changes,
|
||||||
|
// account.Code was nil and the hasher silently wrote codeLen=0 into the
|
||||||
|
// BasicData leaf, corrupting the EIP-7864-defined code_size field every
|
||||||
|
// time a contract's balance or nonce was touched without a code write.
|
||||||
|
//
|
||||||
|
// The fix plumbs the account's current total code size through
|
||||||
|
// AccountMut.CodeSize, which the caller populates via
|
||||||
|
// stateObject.CodeSize() at commit time. This value is authoritative
|
||||||
|
// whether or not the code bytes are currently loaded.
|
||||||
|
//
|
||||||
|
// This test verifies that the state root produced by "create contract,
|
||||||
|
// commit, reload, modify balance, commit" matches the state root produced
|
||||||
|
// by a single commit of the final state. Equality can only hold if the
|
||||||
|
// code size survives the balance-only commit.
|
||||||
|
func TestVerkleCodeSizePreserved(t *testing.T) {
|
||||||
|
newVerkleState := func(t *testing.T) (*StateDB, *triedb.Database) {
|
||||||
|
t.Helper()
|
||||||
|
disk := rawdb.NewMemoryDatabase()
|
||||||
|
tdb := triedb.NewDatabase(disk, triedb.VerkleDefaults)
|
||||||
|
sdb := NewDatabase(tdb, nil)
|
||||||
|
// A fresh verkle pathdb's disk layer is keyed by EmptyVerkleHash
|
||||||
|
// (all-zero hash), not EmptyRootHash. Using the wrong one fails
|
||||||
|
// with "triedb parent layer missing" at commit.
|
||||||
|
state, err := New(types.EmptyVerkleHash, sdb)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to initialize state: %v", err)
|
||||||
|
}
|
||||||
|
return state, tdb
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
addr = common.HexToAddress("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef")
|
||||||
|
code = make([]byte, 1234) // non-trivial code length so codeSize matters
|
||||||
|
)
|
||||||
|
for i := range code {
|
||||||
|
code[i] = byte(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path A: create contract, commit, reload, modify only balance, commit.
|
||||||
|
// On the second commit obj.code is not loaded (dirtyCode=false), so
|
||||||
|
// the previous implementation computed codeLen=0 via len(obj.code).
|
||||||
|
// Triedb layers stay in memory (no tdb.Commit) so we can chain a
|
||||||
|
// second block on top of the first.
|
||||||
|
stateA, tdbA := newVerkleState(t)
|
||||||
|
sdbA := NewDatabase(tdbA, nil)
|
||||||
|
stateA.SetBalance(addr, uint256.NewInt(100), tracing.BalanceChangeUnspecified)
|
||||||
|
stateA.SetCode(addr, code, tracing.CodeChangeUnspecified)
|
||||||
|
rootA1, err := stateA.Commit(0, true, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("path A first commit: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
stateA, err = New(rootA1, sdbA)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("path A reload: %v", err)
|
||||||
|
}
|
||||||
|
stateA.SetBalance(addr, uint256.NewInt(200), tracing.BalanceChangeUnspecified)
|
||||||
|
rootA2, err := stateA.Commit(1, true, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("path A second commit: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path B: construct the same final state in one shot (balance=200 + code).
|
||||||
|
// obj.code is loaded because SetCode was just called, so codeSize is
|
||||||
|
// always correct here — this is the "known-good" reference.
|
||||||
|
stateB, _ := newVerkleState(t)
|
||||||
|
stateB.SetBalance(addr, uint256.NewInt(200), tracing.BalanceChangeUnspecified)
|
||||||
|
stateB.SetCode(addr, code, tracing.CodeChangeUnspecified)
|
||||||
|
rootB, err := stateB.Commit(0, true, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("path B commit: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if rootA2 != rootB {
|
||||||
|
t.Fatalf("state root mismatch after balance-only update:\n path A (reload + balance): %x\n path B (fresh, same final state): %x\n regression: binaryHasher.updateAccount used len(account.Code.Code)=0 because code was not modified",
|
||||||
|
rootA2, rootB)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
package state
|
package state
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
"maps"
|
||||||
|
|
||||||
|
|
@ -29,71 +30,75 @@ import (
|
||||||
"github.com/ethereum/go-ethereum/triedb"
|
"github.com/ethereum/go-ethereum/triedb"
|
||||||
)
|
)
|
||||||
|
|
||||||
// contractCode represents contract bytecode along with its associated metadata.
|
// contractCode encapsulates contract bytecode and its associated metadata.
|
||||||
type contractCode struct {
|
type contractCode struct {
|
||||||
hash common.Hash // hash is the cryptographic hash of the current contract code.
|
hash common.Hash // hash is the cryptographic hash of the current contract code.
|
||||||
blob []byte // blob is the binary representation of the current contract code.
|
originHash common.Hash // originHash is the cryptographic hash of the code prior to mutation.
|
||||||
originHash common.Hash // originHash is the cryptographic hash of the code before mutation.
|
blob []byte // blob is the raw byte representation of the current contract code.
|
||||||
|
|
||||||
// Derived fields, populated only when state tracking is enabled.
|
// Derived fields, populated only when state tracking is enabled.
|
||||||
duplicate bool // duplicate indicates whether the updated code already exists.
|
duplicate bool // duplicate indicates whether the updated code already exists.
|
||||||
originBlob []byte // originBlob is the original binary representation of the contract code.
|
originBlob []byte // originBlob is the original byte representation of the contract code.
|
||||||
}
|
}
|
||||||
|
|
||||||
// accountDelete represents an operation for deleting an Ethereum account.
|
// accountDelete represents a deletion operation for an Ethereum account.
|
||||||
type accountDelete struct {
|
type accountDelete struct {
|
||||||
address common.Address // address is the unique account identifier
|
address common.Address // address uniquely identifies the account.
|
||||||
origin []byte // origin is the original value of account data in slim-RLP encoding.
|
origin Account // origin is the account state prior to deletion.
|
||||||
|
|
||||||
// storages stores mutated slots, the value should be nil.
|
storages map[common.Hash]common.Hash // storages contains mutated storage slots.
|
||||||
storages map[common.Hash][]byte
|
storagesOrigin map[common.Hash]common.Hash // storagesOrigin holds original values of mutated slots; keys are hashes of raw storage slot keys.
|
||||||
|
|
||||||
// storagesOrigin stores the original values of mutated slots in
|
|
||||||
// prefix-zero-trimmed RLP format. The map key refers to the **HASH**
|
|
||||||
// of the raw storage slot key.
|
|
||||||
storagesOrigin map[common.Hash][]byte
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// accountUpdate represents an operation for updating an Ethereum account.
|
// accountUpdate represents an update operation for an Ethereum account.
|
||||||
type accountUpdate struct {
|
type accountUpdate struct {
|
||||||
address common.Address // address is the unique account identifier
|
address common.Address // address uniquely identifies the account.
|
||||||
data []byte // data is the slim-RLP encoded account data.
|
data *Account // data is the updated account state; nil indicates deletion.
|
||||||
origin []byte // origin is the original value of account data in slim-RLP encoding.
|
origin *Account // origin is the previous account state; nil indicates non-existence.
|
||||||
code *contractCode // code represents mutated contract code; nil means it's not modified.
|
code *contractCode // code contains updated contract code; nil if unchanged.
|
||||||
storages map[common.Hash][]byte // storages stores mutated slots in prefix-zero-trimmed RLP format.
|
storages map[common.Hash]common.Hash // storages contains updated storage slots.
|
||||||
|
|
||||||
// storagesOriginByKey and storagesOriginByHash both store the original values
|
// storagesOriginByKey and storagesOriginByHash both record original values
|
||||||
// of mutated slots in prefix-zero-trimmed RLP format. The difference is that
|
// of mutated storage slots:
|
||||||
// storagesOriginByKey uses the **raw** storage slot key as the map ID, while
|
// - storagesOriginByKey uses raw storage slot keys.
|
||||||
// storagesOriginByHash uses the **hash** of the storage slot key instead.
|
// - storagesOriginByHash uses hashed storage slot keys.
|
||||||
storagesOriginByKey map[common.Hash][]byte
|
storagesOriginByKey map[common.Hash]common.Hash
|
||||||
storagesOriginByHash map[common.Hash][]byte
|
storagesOriginByHash map[common.Hash]common.Hash
|
||||||
}
|
}
|
||||||
|
|
||||||
// stateUpdate represents the difference between two states resulting from state
|
// stateUpdate captures the difference between two states resulting from
|
||||||
// execution. It contains information about mutated contract codes, accounts,
|
// execution. It records all mutated accounts, contract codes, and storage
|
||||||
// and storage slots, along with their original values.
|
// slots, along with their original values.
|
||||||
type stateUpdate struct {
|
type stateUpdate struct {
|
||||||
originRoot common.Hash // hash of the state before applying mutation
|
originRoot common.Hash // originRoot is the state root before applying changes.
|
||||||
root common.Hash // hash of the state after applying mutation
|
root common.Hash // root is the state root after applying changes.
|
||||||
blockNumber uint64 // Associated block number
|
blockNumber uint64 // blockNumber is the associated block height.
|
||||||
|
|
||||||
accounts map[common.Hash][]byte // accounts stores mutated accounts in 'slim RLP' encoding
|
accounts map[common.Hash]*Account // accounts contains mutated accounts, keyed by account hash.
|
||||||
accountsOrigin map[common.Address][]byte // accountsOrigin stores the original values of mutated accounts in 'slim RLP' encoding
|
accountsOrigin map[common.Address]*Account // accountsOrigin holds original values of mutated accounts, keyed by address.
|
||||||
|
|
||||||
// storages stores mutated slots in 'prefix-zero-trimmed' RLP format.
|
// storages contains mutated storage slots, keyed by account hash and
|
||||||
// The value is keyed by account hash and **storage slot key hash**.
|
// storage slot key hash.
|
||||||
storages map[common.Hash]map[common.Hash][]byte
|
storages map[common.Hash]map[common.Hash]common.Hash
|
||||||
|
|
||||||
// storagesOrigin stores the original values of mutated slots in
|
// storagesOrigin holds original values of mutated storage slots.
|
||||||
// 'prefix-zero-trimmed' RLP format.
|
// The key format depends on rawStorageKey:
|
||||||
// (a) the value is keyed by account hash and **storage slot key** if rawStorageKey is true;
|
// - if true: keyed by account address and raw storage slot key.
|
||||||
// (b) the value is keyed by account hash and **storage slot key hash** if rawStorageKey is false;
|
// - if false: keyed by account address and storage slot key hash.
|
||||||
storagesOrigin map[common.Address]map[common.Hash][]byte
|
storagesOrigin map[common.Address]map[common.Hash]common.Hash
|
||||||
rawStorageKey bool
|
rawStorageKey bool
|
||||||
|
|
||||||
codes map[common.Address]*contractCode // codes contains the set of dirty codes
|
codes map[common.Address]*contractCode // codes contains mutated contract codes, keyed by address.
|
||||||
nodes *trienode.MergedNodeSet // Aggregated dirty nodes caused by state changes
|
nodes *trienode.MergedNodeSet // nodes aggregates all dirty trie nodes produced by the update.
|
||||||
|
secondaryHashes map[common.Address]Hashes // hashes of secondary tries
|
||||||
|
|
||||||
|
// leaves is the ordered list of stem-offset writes harvested from a
|
||||||
|
// LeafProducer-capable hasher (the binary hasher). For merkle hashers
|
||||||
|
// it is always nil; for the binary hasher it is the bintrie's view of
|
||||||
|
// the same state mutations the trie just absorbed, in flat-state form.
|
||||||
|
// encodeBinary turns this into the per-offset accountData map that
|
||||||
|
// pathdb's bintrie codec consumes at flush time.
|
||||||
|
leaves []StemWrite
|
||||||
}
|
}
|
||||||
|
|
||||||
// empty returns a flag indicating the state transition is empty or not.
|
// empty returns a flag indicating the state transition is empty or not.
|
||||||
|
|
@ -107,12 +112,16 @@ func (sc *stateUpdate) empty() bool {
|
||||||
//
|
//
|
||||||
// rawStorageKey is a flag indicating whether to use the raw storage slot key or
|
// rawStorageKey is a flag indicating whether to use the raw storage slot key or
|
||||||
// the hash of the slot key for constructing state update object.
|
// the hash of the slot key for constructing state update object.
|
||||||
func newStateUpdate(rawStorageKey bool, originRoot common.Hash, root common.Hash, blockNumber uint64, deletes map[common.Hash]*accountDelete, updates map[common.Hash]*accountUpdate, nodes *trienode.MergedNodeSet) *stateUpdate {
|
//
|
||||||
|
// leaves carries the per-offset stem writes produced by a LeafProducer-capable
|
||||||
|
// hasher (the binary hasher). It is nil for merkle hashers and consumed by
|
||||||
|
// encodeBinary to populate the bintrie flat-state map.
|
||||||
|
func newStateUpdate(rawStorageKey bool, originRoot common.Hash, root common.Hash, blockNumber uint64, deletes map[common.Hash]*accountDelete, updates map[common.Hash]*accountUpdate, nodes *trienode.MergedNodeSet, secondaryHashes map[common.Address]Hashes, leaves []StemWrite) *stateUpdate {
|
||||||
var (
|
var (
|
||||||
accounts = make(map[common.Hash][]byte)
|
accounts = make(map[common.Hash]*Account)
|
||||||
accountsOrigin = make(map[common.Address][]byte)
|
accountsOrigin = make(map[common.Address]*Account)
|
||||||
storages = make(map[common.Hash]map[common.Hash][]byte)
|
storages = make(map[common.Hash]map[common.Hash]common.Hash)
|
||||||
storagesOrigin = make(map[common.Address]map[common.Hash][]byte)
|
storagesOrigin = make(map[common.Address]map[common.Hash]common.Hash)
|
||||||
codes = make(map[common.Address]*contractCode)
|
codes = make(map[common.Address]*contractCode)
|
||||||
)
|
)
|
||||||
// Since some accounts might be destroyed and recreated within the same
|
// Since some accounts might be destroyed and recreated within the same
|
||||||
|
|
@ -120,7 +129,7 @@ func newStateUpdate(rawStorageKey bool, originRoot common.Hash, root common.Hash
|
||||||
for addrHash, op := range deletes {
|
for addrHash, op := range deletes {
|
||||||
addr := op.address
|
addr := op.address
|
||||||
accounts[addrHash] = nil
|
accounts[addrHash] = nil
|
||||||
accountsOrigin[addr] = op.origin
|
accountsOrigin[addr] = &op.origin
|
||||||
|
|
||||||
// If storage wiping exists, the hash of the storage slot key must be used
|
// If storage wiping exists, the hash of the storage slot key must be used
|
||||||
if len(op.storages) > 0 {
|
if len(op.storages) > 0 {
|
||||||
|
|
@ -174,31 +183,168 @@ func newStateUpdate(rawStorageKey bool, originRoot common.Hash, root common.Hash
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return &stateUpdate{
|
return &stateUpdate{
|
||||||
originRoot: originRoot,
|
originRoot: originRoot,
|
||||||
root: root,
|
root: root,
|
||||||
blockNumber: blockNumber,
|
blockNumber: blockNumber,
|
||||||
accounts: accounts,
|
accounts: accounts,
|
||||||
accountsOrigin: accountsOrigin,
|
accountsOrigin: accountsOrigin,
|
||||||
storages: storages,
|
storages: storages,
|
||||||
storagesOrigin: storagesOrigin,
|
storagesOrigin: storagesOrigin,
|
||||||
rawStorageKey: rawStorageKey,
|
rawStorageKey: rawStorageKey,
|
||||||
codes: codes,
|
codes: codes,
|
||||||
nodes: nodes,
|
nodes: nodes,
|
||||||
|
secondaryHashes: secondaryHashes,
|
||||||
|
leaves: leaves,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func encodeSlot(val common.Hash) []byte {
|
||||||
|
if val == (common.Hash{}) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
blob, _ := rlp.EncodeToBytes(common.TrimLeftZeroes(val[:]))
|
||||||
|
return blob
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sc *stateUpdate) encodeMerkle() (map[common.Hash][]byte, map[common.Address][]byte, map[common.Hash]map[common.Hash][]byte, map[common.Address]map[common.Hash][]byte, error) {
|
||||||
|
var (
|
||||||
|
accounts = make(map[common.Hash][]byte)
|
||||||
|
storages = make(map[common.Hash]map[common.Hash][]byte)
|
||||||
|
accountOrigin = make(map[common.Address][]byte)
|
||||||
|
storageOrigin = make(map[common.Address]map[common.Hash][]byte)
|
||||||
|
)
|
||||||
|
for addr, prev := range sc.accountsOrigin {
|
||||||
|
if prev == nil {
|
||||||
|
accountOrigin[addr] = nil
|
||||||
|
} else {
|
||||||
|
pair, ok := sc.secondaryHashes[addr]
|
||||||
|
if !ok {
|
||||||
|
return nil, nil, nil, nil, errors.New("no secondary hash")
|
||||||
|
}
|
||||||
|
accountOrigin[addr] = types.SlimAccountRLP(types.StateAccount{
|
||||||
|
Balance: prev.Balance,
|
||||||
|
Nonce: prev.Nonce,
|
||||||
|
CodeHash: prev.CodeHash,
|
||||||
|
Root: pair.Prev,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
addrHash := crypto.Keccak256Hash(addr.Bytes())
|
||||||
|
data := sc.accounts[addrHash]
|
||||||
|
if data == nil {
|
||||||
|
accounts[addrHash] = nil
|
||||||
|
} else {
|
||||||
|
pair, ok := sc.secondaryHashes[addr]
|
||||||
|
if !ok {
|
||||||
|
return nil, nil, nil, nil, errors.New("no secondary hash")
|
||||||
|
}
|
||||||
|
accounts[addrHash] = types.SlimAccountRLP(types.StateAccount{
|
||||||
|
Balance: data.Balance,
|
||||||
|
Nonce: data.Nonce,
|
||||||
|
CodeHash: data.CodeHash,
|
||||||
|
Root: pair.Hash,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for addr, slots := range sc.storagesOrigin {
|
||||||
|
subset := make(map[common.Hash][]byte)
|
||||||
|
for key, val := range slots {
|
||||||
|
subset[key] = encodeSlot(val)
|
||||||
|
}
|
||||||
|
storageOrigin[addr] = subset
|
||||||
|
}
|
||||||
|
for addrHash, slots := range sc.storages {
|
||||||
|
subset := make(map[common.Hash][]byte)
|
||||||
|
for key, val := range slots {
|
||||||
|
subset[key] = encodeSlot(val)
|
||||||
|
}
|
||||||
|
storages[addrHash] = subset
|
||||||
|
}
|
||||||
|
return accounts, accountOrigin, storages, storageOrigin, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodeBinary produces the bintrie flat-state representation consumed by
|
||||||
|
// pathdb. Unlike encodeMerkle (which keys accounts/storage by keccak hashes
|
||||||
|
// and slim-RLP encodes the values), the bintrie path uses one entry per
|
||||||
|
// EIP-7864 leaf:
|
||||||
|
//
|
||||||
|
// key = stem(31B) || offset(1B), zero-padded into a common.Hash
|
||||||
|
// value = the 32-byte leaf payload, or nil to clear the offset
|
||||||
|
//
|
||||||
|
// Account header writes (BasicData at offset 0, CodeHash at offset 1) and
|
||||||
|
// storage slot / code chunk writes are uniform — the binary hasher emits
|
||||||
|
// each as a stemWrite via DrainStemWrites and we route every one of them
|
||||||
|
// into the accounts map. The storages map stays empty: bintrie has no
|
||||||
|
// per-account storage grouping at the flat-state layer, and pathdb's
|
||||||
|
// disklayer/lookup tree both work fine with a single accountData map of
|
||||||
|
// 32-byte keys.
|
||||||
|
//
|
||||||
|
// accountOrigin and storageOrigin are returned empty because state-history
|
||||||
|
// rollback for bintrie is not yet supported. The pathdb disklayer.revert
|
||||||
|
// guard blocks bintrie reverts before it would observe these maps.
|
||||||
|
func (sc *stateUpdate) encodeBinary() (map[common.Hash][]byte, map[common.Address][]byte, map[common.Hash]map[common.Hash][]byte, map[common.Address]map[common.Hash][]byte, error) {
|
||||||
|
var (
|
||||||
|
accounts = make(map[common.Hash][]byte, len(sc.leaves))
|
||||||
|
storages = make(map[common.Hash]map[common.Hash][]byte)
|
||||||
|
accountOrigin = make(map[common.Address][]byte)
|
||||||
|
storageOrigin = make(map[common.Address]map[common.Hash][]byte)
|
||||||
|
)
|
||||||
|
for _, w := range sc.leaves {
|
||||||
|
var fullKey common.Hash
|
||||||
|
copy(fullKey[:len(w.Stem)], w.Stem[:])
|
||||||
|
fullKey[len(w.Stem)] = w.Offset
|
||||||
|
// nil Value means "clear this offset" (account delete or storage
|
||||||
|
// slot wipe). The pathdb codec interprets a nil entry as a delete
|
||||||
|
// during flush, matching merkle's nil-blob convention.
|
||||||
|
if w.Value == nil {
|
||||||
|
accounts[fullKey] = nil
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Defensive length check: every non-nil bintrie leaf must be
|
||||||
|
// exactly 32 bytes. A wrong-length leaf from the hasher would
|
||||||
|
// silently produce garbage in the diff layer; catch it here at
|
||||||
|
// the trust boundary rather than deep in the flush path where
|
||||||
|
// the stemBuilder.set panic would fire with less context.
|
||||||
|
if len(w.Value) != 32 {
|
||||||
|
return nil, nil, nil, nil, fmt.Errorf("bintrie leaf at stem %x offset %d has value len %d, want 32", w.Stem, w.Offset, len(w.Value))
|
||||||
|
}
|
||||||
|
// Take an owning copy: the hasher reuses its underlying buffers
|
||||||
|
// across blocks, so retaining its slices would create cross-block
|
||||||
|
// aliasing bugs in the pathdb diff layer.
|
||||||
|
v := make([]byte, 32)
|
||||||
|
copy(v, w.Value)
|
||||||
|
accounts[fullKey] = v
|
||||||
|
}
|
||||||
|
return accounts, accountOrigin, storages, storageOrigin, nil
|
||||||
|
}
|
||||||
|
|
||||||
// stateSet converts the current stateUpdate object into a triedb.StateSet
|
// stateSet converts the current stateUpdate object into a triedb.StateSet
|
||||||
// object. This function extracts the necessary data from the stateUpdate
|
// object. This function extracts the necessary data from the stateUpdate
|
||||||
// struct and formats it into the StateSet structure consumed by the triedb
|
// struct and formats it into the StateSet structure consumed by the triedb
|
||||||
// package.
|
// package.
|
||||||
func (sc *stateUpdate) stateSet() *triedb.StateSet {
|
func (sc *stateUpdate) stateSet(isMerkle bool) (*triedb.StateSet, error) {
|
||||||
return &triedb.StateSet{
|
var (
|
||||||
Accounts: sc.accounts,
|
err error
|
||||||
AccountsOrigin: sc.accountsOrigin,
|
accounts map[common.Hash][]byte
|
||||||
Storages: sc.storages,
|
storages map[common.Hash]map[common.Hash][]byte
|
||||||
StoragesOrigin: sc.storagesOrigin,
|
accountOrigin map[common.Address][]byte
|
||||||
RawStorageKey: sc.rawStorageKey,
|
storageOrigin map[common.Address]map[common.Hash][]byte
|
||||||
|
)
|
||||||
|
if isMerkle {
|
||||||
|
accounts, accountOrigin, storages, storageOrigin, err = sc.encodeMerkle()
|
||||||
|
} else {
|
||||||
|
accounts, accountOrigin, storages, storageOrigin, err = sc.encodeBinary()
|
||||||
}
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &triedb.StateSet{
|
||||||
|
Accounts: accounts,
|
||||||
|
AccountsOrigin: accountOrigin,
|
||||||
|
Storages: storages,
|
||||||
|
StoragesOrigin: storageOrigin,
|
||||||
|
RawStorageKey: sc.rawStorageKey,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// deriveCodeFields derives the missing fields of contract code changes
|
// deriveCodeFields derives the missing fields of contract code changes
|
||||||
|
|
@ -246,30 +392,33 @@ func (sc *stateUpdate) ToTracingUpdate() (*tracing.StateUpdate, error) {
|
||||||
if !exists {
|
if !exists {
|
||||||
return nil, fmt.Errorf("account %x not found", addr)
|
return nil, fmt.Errorf("account %x not found", addr)
|
||||||
}
|
}
|
||||||
|
var hashes Hashes
|
||||||
|
if sc.secondaryHashes != nil {
|
||||||
|
var ok bool
|
||||||
|
hashes, ok = sc.secondaryHashes[addr]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("ToTracingUpdate: missing secondary hash for %x", addr)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Bintrie: no per-account storage sub-tries, use empty root.
|
||||||
|
hashes = Hashes{Hash: types.EmptyRootHash, Prev: types.EmptyRootHash}
|
||||||
|
}
|
||||||
change := &tracing.AccountChange{}
|
change := &tracing.AccountChange{}
|
||||||
|
|
||||||
if len(oldData) > 0 {
|
if oldData != nil {
|
||||||
acct, err := types.FullAccount(oldData)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
change.Prev = &types.StateAccount{
|
change.Prev = &types.StateAccount{
|
||||||
Nonce: acct.Nonce,
|
Nonce: oldData.Nonce,
|
||||||
Balance: acct.Balance,
|
Balance: oldData.Balance,
|
||||||
Root: acct.Root,
|
Root: hashes.Prev,
|
||||||
CodeHash: acct.CodeHash,
|
CodeHash: oldData.CodeHash,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(newData) > 0 {
|
if newData != nil {
|
||||||
acct, err := types.FullAccount(newData)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
change.New = &types.StateAccount{
|
change.New = &types.StateAccount{
|
||||||
Nonce: acct.Nonce,
|
Nonce: newData.Nonce,
|
||||||
Balance: acct.Balance,
|
Balance: newData.Balance,
|
||||||
Root: acct.Root,
|
Root: hashes.Hash,
|
||||||
CodeHash: acct.CodeHash,
|
CodeHash: newData.CodeHash,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
update.AccountChanges[addr] = change
|
update.AccountChanges[addr] = change
|
||||||
|
|
@ -284,40 +433,24 @@ func (sc *stateUpdate) ToTracingUpdate() (*tracing.StateUpdate, error) {
|
||||||
}
|
}
|
||||||
storageChanges := make(map[common.Hash]*tracing.StorageChange, len(slots))
|
storageChanges := make(map[common.Hash]*tracing.StorageChange, len(slots))
|
||||||
|
|
||||||
for key, encPrev := range slots {
|
for key, prev := range slots {
|
||||||
// Get new value - handle both raw and hashed key formats
|
// Get new value - handle both raw and hashed key formats
|
||||||
var (
|
var (
|
||||||
exists bool
|
exists bool
|
||||||
encNew []byte
|
current common.Hash
|
||||||
decPrev []byte
|
|
||||||
decNew []byte
|
|
||||||
err error
|
|
||||||
)
|
)
|
||||||
if sc.rawStorageKey {
|
if sc.rawStorageKey {
|
||||||
encNew, exists = subset[crypto.Keccak256Hash(key.Bytes())]
|
current, exists = subset[crypto.Keccak256Hash(key.Bytes())]
|
||||||
} else {
|
} else {
|
||||||
encNew, exists = subset[key]
|
current, exists = subset[key]
|
||||||
}
|
}
|
||||||
if !exists {
|
if !exists {
|
||||||
return nil, fmt.Errorf("storage slot %x-%x not found", addr, key)
|
return nil, fmt.Errorf("storage slot %x-%x not found", addr, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decode the prev and new values
|
|
||||||
if len(encPrev) > 0 {
|
|
||||||
_, decPrev, _, err = rlp.Split(encPrev)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to decode prevValue: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(encNew) > 0 {
|
|
||||||
_, decNew, _, err = rlp.Split(encNew)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to decode newValue: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
storageChanges[key] = &tracing.StorageChange{
|
storageChanges[key] = &tracing.StorageChange{
|
||||||
Prev: common.BytesToHash(decPrev),
|
Prev: prev,
|
||||||
New: common.BytesToHash(decNew),
|
New: current,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
update.StorageChanges[addr] = storageChanges
|
update.StorageChanges[addr] = storageChanges
|
||||||
|
|
|
||||||
|
|
@ -22,451 +22,225 @@ import (
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
"github.com/ethereum/go-ethereum/log"
|
"github.com/ethereum/go-ethereum/log"
|
||||||
"github.com/ethereum/go-ethereum/metrics"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var errTerminated = errors.New("fetcher is already terminated")
|
||||||
// triePrefetchMetricsPrefix is the prefix under which to publish the metrics.
|
|
||||||
triePrefetchMetricsPrefix = "trie/prefetch/"
|
|
||||||
|
|
||||||
// errTerminated is returned if a fetcher is attempted to be operated after it
|
type slotKey struct {
|
||||||
// has already terminated.
|
addr common.Address
|
||||||
errTerminated = errors.New("fetcher is already terminated")
|
slot common.Hash
|
||||||
|
}
|
||||||
|
|
||||||
|
type taskKind uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
kindAccount taskKind = iota
|
||||||
|
kindStorage
|
||||||
)
|
)
|
||||||
|
|
||||||
// triePrefetcher is an active prefetcher, which receives accounts or storage
|
type prefetchTask struct {
|
||||||
// items and does trie-loading of them. The goal is to get as much useful content
|
read bool
|
||||||
// into the caches as possible.
|
kind taskKind
|
||||||
//
|
|
||||||
// Note, the prefetcher's API is not thread safe.
|
|
||||||
type triePrefetcher struct {
|
|
||||||
verkle bool // Flag whether the prefetcher is in verkle mode
|
|
||||||
db Database // Database to fetch trie nodes through
|
|
||||||
root common.Hash // Root hash of the account trie for metrics
|
|
||||||
fetchers map[string]*subfetcher // Subfetchers for each trie
|
|
||||||
term chan struct{} // Channel to signal interruption
|
|
||||||
noreads bool // Whether to ignore state-read-only prefetch requests
|
|
||||||
|
|
||||||
deliveryMissMeter *metrics.Meter
|
accounts []common.Address // kindAccount: addresses to prefetch
|
||||||
|
account common.Address // kindStorage: owner address
|
||||||
accountLoadReadMeter *metrics.Meter
|
slots []common.Hash // kindStorage: slot keys to prefetch
|
||||||
accountLoadWriteMeter *metrics.Meter
|
|
||||||
accountDupReadMeter *metrics.Meter
|
|
||||||
accountDupWriteMeter *metrics.Meter
|
|
||||||
accountDupCrossMeter *metrics.Meter
|
|
||||||
accountWasteMeter *metrics.Meter
|
|
||||||
|
|
||||||
storageLoadReadMeter *metrics.Meter
|
|
||||||
storageLoadWriteMeter *metrics.Meter
|
|
||||||
storageDupReadMeter *metrics.Meter
|
|
||||||
storageDupWriteMeter *metrics.Meter
|
|
||||||
storageDupCrossMeter *metrics.Meter
|
|
||||||
storageWasteMeter *metrics.Meter
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTriePrefetcher(db Database, root common.Hash, namespace string, noreads bool) *triePrefetcher {
|
// prefetcher is a background goroutine that preloads trie nodes for a single
|
||||||
prefix := triePrefetchMetricsPrefix + namespace
|
// trie. It deduplicates requests and stops when explicitly terminated.
|
||||||
return &triePrefetcher{
|
type prefetcher struct {
|
||||||
verkle: db.TrieDB().IsVerkle(),
|
prefetchRead bool // Whether the state read will trigger preloading
|
||||||
db: db,
|
trie Trie // Trie being populated with nodes
|
||||||
root: root,
|
tasks []*prefetchTask // Items queued up for retrieval
|
||||||
fetchers: make(map[string]*subfetcher), // Active prefetchers use the fetchers map
|
lock sync.Mutex // Lock protecting the task queue
|
||||||
term: make(chan struct{}),
|
|
||||||
noreads: noreads,
|
|
||||||
|
|
||||||
deliveryMissMeter: metrics.GetOrRegisterMeter(prefix+"/deliverymiss", nil),
|
|
||||||
|
|
||||||
accountLoadReadMeter: metrics.GetOrRegisterMeter(prefix+"/account/load/read", nil),
|
|
||||||
accountLoadWriteMeter: metrics.GetOrRegisterMeter(prefix+"/account/load/write", nil),
|
|
||||||
accountDupReadMeter: metrics.GetOrRegisterMeter(prefix+"/account/dup/read", nil),
|
|
||||||
accountDupWriteMeter: metrics.GetOrRegisterMeter(prefix+"/account/dup/write", nil),
|
|
||||||
accountDupCrossMeter: metrics.GetOrRegisterMeter(prefix+"/account/dup/cross", nil),
|
|
||||||
accountWasteMeter: metrics.GetOrRegisterMeter(prefix+"/account/waste", nil),
|
|
||||||
|
|
||||||
storageLoadReadMeter: metrics.GetOrRegisterMeter(prefix+"/storage/load/read", nil),
|
|
||||||
storageLoadWriteMeter: metrics.GetOrRegisterMeter(prefix+"/storage/load/write", nil),
|
|
||||||
storageDupReadMeter: metrics.GetOrRegisterMeter(prefix+"/storage/dup/read", nil),
|
|
||||||
storageDupWriteMeter: metrics.GetOrRegisterMeter(prefix+"/storage/dup/write", nil),
|
|
||||||
storageDupCrossMeter: metrics.GetOrRegisterMeter(prefix+"/storage/dup/cross", nil),
|
|
||||||
storageWasteMeter: metrics.GetOrRegisterMeter(prefix+"/storage/waste", nil),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// terminate iterates over all the subfetchers and issues a termination request
|
|
||||||
// to all of them. Depending on the async parameter, the method will either block
|
|
||||||
// until all subfetchers spin down, or return immediately.
|
|
||||||
func (p *triePrefetcher) terminate(async bool) {
|
|
||||||
// Short circuit if the fetcher is already closed
|
|
||||||
select {
|
|
||||||
case <-p.term:
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
// Terminate all sub-fetchers, sync or async, depending on the request
|
|
||||||
for _, fetcher := range p.fetchers {
|
|
||||||
fetcher.terminate(async)
|
|
||||||
}
|
|
||||||
close(p.term)
|
|
||||||
}
|
|
||||||
|
|
||||||
// report aggregates the pre-fetching and usage metrics and reports them.
|
|
||||||
func (p *triePrefetcher) report() {
|
|
||||||
if !metrics.Enabled() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for _, fetcher := range p.fetchers {
|
|
||||||
fetcher.wait() // ensure the fetcher's idle before poking in its internals
|
|
||||||
|
|
||||||
if fetcher.root == p.root {
|
|
||||||
p.accountLoadReadMeter.Mark(int64(len(fetcher.seenReadAddr)))
|
|
||||||
p.accountLoadWriteMeter.Mark(int64(len(fetcher.seenWriteAddr)))
|
|
||||||
|
|
||||||
p.accountDupReadMeter.Mark(int64(fetcher.dupsRead))
|
|
||||||
p.accountDupWriteMeter.Mark(int64(fetcher.dupsWrite))
|
|
||||||
p.accountDupCrossMeter.Mark(int64(fetcher.dupsCross))
|
|
||||||
|
|
||||||
for _, key := range fetcher.usedAddr {
|
|
||||||
delete(fetcher.seenReadAddr, key)
|
|
||||||
delete(fetcher.seenWriteAddr, key)
|
|
||||||
}
|
|
||||||
p.accountWasteMeter.Mark(int64(len(fetcher.seenReadAddr) + len(fetcher.seenWriteAddr)))
|
|
||||||
} else {
|
|
||||||
p.storageLoadReadMeter.Mark(int64(len(fetcher.seenReadSlot)))
|
|
||||||
p.storageLoadWriteMeter.Mark(int64(len(fetcher.seenWriteSlot)))
|
|
||||||
|
|
||||||
p.storageDupReadMeter.Mark(int64(fetcher.dupsRead))
|
|
||||||
p.storageDupWriteMeter.Mark(int64(fetcher.dupsWrite))
|
|
||||||
p.storageDupCrossMeter.Mark(int64(fetcher.dupsCross))
|
|
||||||
|
|
||||||
for _, key := range fetcher.usedSlot {
|
|
||||||
delete(fetcher.seenReadSlot, key)
|
|
||||||
delete(fetcher.seenWriteSlot, key)
|
|
||||||
}
|
|
||||||
p.storageWasteMeter.Mark(int64(len(fetcher.seenReadSlot) + len(fetcher.seenWriteSlot)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// prefetch schedules a batch of trie items to prefetch. After the prefetcher is
|
|
||||||
// closed, all the following tasks scheduled will not be executed and an error
|
|
||||||
// will be returned.
|
|
||||||
//
|
|
||||||
// prefetch is called from two locations:
|
|
||||||
//
|
|
||||||
// 1. Finalize of the state-objects storage roots. This happens at the end
|
|
||||||
// of every transaction, meaning that if several transactions touches
|
|
||||||
// upon the same contract, the parameters invoking this method may be
|
|
||||||
// repeated.
|
|
||||||
// 2. Finalize of the main account trie. This happens only once per block.
|
|
||||||
func (p *triePrefetcher) prefetch(owner common.Hash, root common.Hash, addr common.Address, addrs []common.Address, slots []common.Hash, read bool) error {
|
|
||||||
// If the state item is only being read, but reads are disabled, return
|
|
||||||
if read && p.noreads {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// Ensure the subfetcher is still alive
|
|
||||||
select {
|
|
||||||
case <-p.term:
|
|
||||||
return errTerminated
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
id := p.trieID(owner, root)
|
|
||||||
fetcher := p.fetchers[id]
|
|
||||||
if fetcher == nil {
|
|
||||||
fetcher = newSubfetcher(p.db, p.root, owner, root, addr)
|
|
||||||
p.fetchers[id] = fetcher
|
|
||||||
}
|
|
||||||
return fetcher.schedule(addrs, slots, read)
|
|
||||||
}
|
|
||||||
|
|
||||||
// trie returns the trie matching the root hash, blocking until the fetcher of
|
|
||||||
// the given trie terminates. If no fetcher exists for the request, nil will be
|
|
||||||
// returned.
|
|
||||||
func (p *triePrefetcher) trie(owner common.Hash, root common.Hash) Trie {
|
|
||||||
// Bail if no trie was prefetched for this root
|
|
||||||
fetcher := p.fetchers[p.trieID(owner, root)]
|
|
||||||
if fetcher == nil {
|
|
||||||
log.Error("Prefetcher missed to load trie", "owner", owner, "root", root)
|
|
||||||
p.deliveryMissMeter.Mark(1)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// Subfetcher exists, retrieve its trie
|
|
||||||
return fetcher.peek()
|
|
||||||
}
|
|
||||||
|
|
||||||
// used marks a batch of state items used to allow creating statistics as to
|
|
||||||
// how useful or wasteful the fetcher is.
|
|
||||||
func (p *triePrefetcher) used(owner common.Hash, root common.Hash, usedAddr []common.Address, usedSlot []common.Hash) {
|
|
||||||
if fetcher := p.fetchers[p.trieID(owner, root)]; fetcher != nil {
|
|
||||||
fetcher.wait() // ensure the fetcher's idle before poking in its internals
|
|
||||||
|
|
||||||
fetcher.usedAddr = append(fetcher.usedAddr, usedAddr...)
|
|
||||||
fetcher.usedSlot = append(fetcher.usedSlot, usedSlot...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// trieID returns an unique trie identifier consists the trie owner and root hash.
|
|
||||||
func (p *triePrefetcher) trieID(owner common.Hash, root common.Hash) string {
|
|
||||||
// The trie in verkle is only identified by state root
|
|
||||||
if p.verkle {
|
|
||||||
return p.root.Hex()
|
|
||||||
}
|
|
||||||
// The trie in merkle is either identified by state root (account trie),
|
|
||||||
// or identified by the owner and trie root (storage trie)
|
|
||||||
trieID := make([]byte, common.HashLength*2)
|
|
||||||
copy(trieID, owner.Bytes())
|
|
||||||
copy(trieID[common.HashLength:], root.Bytes())
|
|
||||||
return string(trieID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// subfetcher is a trie fetcher goroutine responsible for pulling entries for a
|
|
||||||
// single trie. It is spawned when a new root is encountered and lives until the
|
|
||||||
// main prefetcher is paused and either all requested items are processed or if
|
|
||||||
// the trie being worked on is retrieved from the prefetcher.
|
|
||||||
type subfetcher struct {
|
|
||||||
db Database // Database to load trie nodes through
|
|
||||||
state common.Hash // Root hash of the state to prefetch
|
|
||||||
owner common.Hash // Owner of the trie, usually account hash
|
|
||||||
root common.Hash // Root hash of the trie to prefetch
|
|
||||||
addr common.Address // Address of the account that the trie belongs to
|
|
||||||
trie Trie // Trie being populated with nodes
|
|
||||||
|
|
||||||
tasks []*subfetcherTask // Items queued up for retrieval
|
|
||||||
lock sync.Mutex // Lock protecting the task queue
|
|
||||||
|
|
||||||
wake chan struct{} // Wake channel if a new task is scheduled
|
wake chan struct{} // Wake channel if a new task is scheduled
|
||||||
stop chan struct{} // Channel to interrupt processing
|
stop chan struct{} // Channel to interrupt processing
|
||||||
term chan struct{} // Channel to signal interruption
|
term chan struct{} // Channel to signal interruption
|
||||||
|
|
||||||
seenReadAddr map[common.Address]struct{} // Tracks the accounts already loaded via read operations
|
seenReadAddr map[common.Address]struct{} // Dedup: accounts loaded via reads
|
||||||
seenWriteAddr map[common.Address]struct{} // Tracks the accounts already loaded via write operations
|
seenWriteAddr map[common.Address]struct{} // Dedup: accounts loaded via writes
|
||||||
seenReadSlot map[common.Hash]struct{} // Tracks the storage already loaded via read operations
|
seenReadSlot map[slotKey]struct{} // Dedup: slots loaded via reads
|
||||||
seenWriteSlot map[common.Hash]struct{} // Tracks the storage already loaded via write operations
|
seenWriteSlot map[slotKey]struct{} // Dedup: slots loaded via writes
|
||||||
|
|
||||||
dupsRead int // Number of duplicate preload tasks via reads only
|
|
||||||
dupsWrite int // Number of duplicate preload tasks via writes only
|
|
||||||
dupsCross int // Number of duplicate preload tasks via read-write-crosses
|
|
||||||
|
|
||||||
usedAddr []common.Address // Tracks the accounts used in the end
|
|
||||||
usedSlot []common.Hash // Tracks the storage used in the end
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// subfetcherTask is a trie path to prefetch, tagged with whether it originates
|
// newPrefetcher creates a background goroutine to prefetch state items from the
|
||||||
// from a read or a write request.
|
// given trie.
|
||||||
type subfetcherTask struct {
|
func newPrefetcher(tr Trie, prefetchRead bool) *prefetcher {
|
||||||
read bool
|
p := &prefetcher{
|
||||||
addr *common.Address
|
prefetchRead: prefetchRead,
|
||||||
slot *common.Hash
|
trie: tr,
|
||||||
}
|
|
||||||
|
|
||||||
// newSubfetcher creates a goroutine to prefetch state items belonging to a
|
|
||||||
// particular root hash.
|
|
||||||
func newSubfetcher(db Database, state common.Hash, owner common.Hash, root common.Hash, addr common.Address) *subfetcher {
|
|
||||||
sf := &subfetcher{
|
|
||||||
db: db,
|
|
||||||
state: state,
|
|
||||||
owner: owner,
|
|
||||||
root: root,
|
|
||||||
addr: addr,
|
|
||||||
wake: make(chan struct{}, 1),
|
wake: make(chan struct{}, 1),
|
||||||
stop: make(chan struct{}),
|
stop: make(chan struct{}),
|
||||||
term: make(chan struct{}),
|
term: make(chan struct{}),
|
||||||
seenReadAddr: make(map[common.Address]struct{}),
|
seenReadAddr: make(map[common.Address]struct{}),
|
||||||
seenWriteAddr: make(map[common.Address]struct{}),
|
seenWriteAddr: make(map[common.Address]struct{}),
|
||||||
seenReadSlot: make(map[common.Hash]struct{}),
|
seenReadSlot: make(map[slotKey]struct{}),
|
||||||
seenWriteSlot: make(map[common.Hash]struct{}),
|
seenWriteSlot: make(map[slotKey]struct{}),
|
||||||
}
|
}
|
||||||
go sf.loop()
|
go p.loop()
|
||||||
return sf
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
// schedule adds a batch of trie keys to the queue to prefetch.
|
// scheduleAccounts adds a batch of accounts to the prefetch queue.
|
||||||
func (sf *subfetcher) schedule(addrs []common.Address, slots []common.Hash, read bool) error {
|
func (p *prefetcher) scheduleAccounts(addrs []common.Address, read bool) error {
|
||||||
// Ensure the subfetcher is still alive
|
|
||||||
select {
|
select {
|
||||||
case <-sf.term:
|
case <-p.term:
|
||||||
return errTerminated
|
return errTerminated
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
// Append the tasks to the current queue
|
if !p.prefetchRead && read {
|
||||||
sf.lock.Lock()
|
return nil
|
||||||
for _, addr := range addrs {
|
|
||||||
sf.tasks = append(sf.tasks, &subfetcherTask{read: read, addr: &addr})
|
|
||||||
}
|
}
|
||||||
for _, slot := range slots {
|
p.lock.Lock()
|
||||||
sf.tasks = append(sf.tasks, &subfetcherTask{read: read, slot: &slot})
|
p.tasks = append(p.tasks, &prefetchTask{
|
||||||
}
|
read: read,
|
||||||
sf.lock.Unlock()
|
kind: kindAccount,
|
||||||
|
accounts: addrs,
|
||||||
|
})
|
||||||
|
p.lock.Unlock()
|
||||||
|
|
||||||
// Notify the background thread to execute scheduled tasks
|
|
||||||
select {
|
select {
|
||||||
case sf.wake <- struct{}{}:
|
case p.wake <- struct{}{}:
|
||||||
// Wake signal sent
|
|
||||||
default:
|
default:
|
||||||
// Wake signal not sent as a previous one is already queued
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// wait blocks until the subfetcher terminates. This method is used to block on
|
// scheduleSlots adds a batch of storage slots to the prefetch queue.
|
||||||
// an async termination before accessing internal fields from the fetcher.
|
func (p *prefetcher) scheduleSlots(addr common.Address, slots []common.Hash, read bool) error {
|
||||||
func (sf *subfetcher) wait() {
|
|
||||||
<-sf.term
|
|
||||||
}
|
|
||||||
|
|
||||||
// peek retrieves the fetcher's trie, populated with any pre-fetched data. The
|
|
||||||
// returned trie will be a shallow copy, so modifying it will break subsequent
|
|
||||||
// peeks for the original data. The method will block until all the scheduled
|
|
||||||
// data has been loaded and the fethcer terminated.
|
|
||||||
func (sf *subfetcher) peek() Trie {
|
|
||||||
// Block until the fetcher terminates, then retrieve the trie
|
|
||||||
sf.wait()
|
|
||||||
return sf.trie
|
|
||||||
}
|
|
||||||
|
|
||||||
// terminate requests the subfetcher to stop accepting new tasks and spin down
|
|
||||||
// as soon as everything is loaded. Depending on the async parameter, the method
|
|
||||||
// will either block until all disk loads finish or return immediately.
|
|
||||||
func (sf *subfetcher) terminate(async bool) {
|
|
||||||
select {
|
select {
|
||||||
case <-sf.stop:
|
case <-p.term:
|
||||||
|
return errTerminated
|
||||||
default:
|
default:
|
||||||
close(sf.stop)
|
|
||||||
}
|
}
|
||||||
if async {
|
if !p.prefetchRead && read {
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
<-sf.term
|
p.lock.Lock()
|
||||||
}
|
p.tasks = append(p.tasks, &prefetchTask{
|
||||||
|
read: read,
|
||||||
|
kind: kindStorage,
|
||||||
|
account: addr,
|
||||||
|
slots: slots,
|
||||||
|
})
|
||||||
|
p.lock.Unlock()
|
||||||
|
|
||||||
// openTrie resolves the target trie from database for prefetching.
|
select {
|
||||||
func (sf *subfetcher) openTrie() error {
|
case p.wake <- struct{}{}:
|
||||||
// Open the verkle tree if the sub-fetcher is in verkle mode. Note, there is
|
default:
|
||||||
// only a single fetcher for verkle.
|
|
||||||
if sf.db.TrieDB().IsVerkle() {
|
|
||||||
tr, err := sf.db.OpenTrie(sf.state)
|
|
||||||
if err != nil {
|
|
||||||
log.Warn("Trie prefetcher failed opening verkle trie", "root", sf.root, "err", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
sf.trie = tr
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
// Open the merkle tree if the sub-fetcher is in merkle mode
|
|
||||||
if sf.owner == (common.Hash{}) {
|
|
||||||
tr, err := sf.db.OpenTrie(sf.state)
|
|
||||||
if err != nil {
|
|
||||||
log.Warn("Trie prefetcher failed opening account trie", "root", sf.root, "err", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
sf.trie = tr
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
tr, err := sf.db.OpenStorageTrie(sf.state, sf.addr, sf.root, nil)
|
|
||||||
if err != nil {
|
|
||||||
log.Warn("Trie prefetcher failed opening storage trie", "root", sf.root, "err", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
sf.trie = tr
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// loop loads newly-scheduled trie tasks as they are received and loads them, stopping
|
// terminate requests the prefetcher to stop and optionally waits for it.
|
||||||
// when requested.
|
func (p *prefetcher) terminate() {
|
||||||
func (sf *subfetcher) loop() {
|
select {
|
||||||
// No matter how the loop stops, signal anyone waiting that it's terminated
|
case <-p.stop:
|
||||||
defer close(sf.term)
|
default:
|
||||||
|
close(p.stop)
|
||||||
if err := sf.openTrie(); err != nil {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
<-p.term
|
||||||
|
}
|
||||||
|
|
||||||
|
// loop processes prefetch tasks until terminated.
|
||||||
|
func (p *prefetcher) loop() {
|
||||||
|
defer close(p.term)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-sf.wake:
|
case <-p.wake:
|
||||||
// Execute all remaining tasks in a single run
|
p.lock.Lock()
|
||||||
sf.lock.Lock()
|
tasks := p.tasks
|
||||||
tasks := sf.tasks
|
p.tasks = nil
|
||||||
sf.tasks = nil
|
p.lock.Unlock()
|
||||||
sf.lock.Unlock()
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
addresses []common.Address
|
addrs []common.Address
|
||||||
slots [][]byte
|
slots = make(map[common.Address][][]byte)
|
||||||
)
|
)
|
||||||
for _, task := range tasks {
|
for _, task := range tasks {
|
||||||
if task.addr != nil {
|
if task.kind == kindAccount {
|
||||||
key := *task.addr
|
for _, addr := range task.accounts {
|
||||||
if task.read {
|
if p.dedupAddr(addr, task.read) {
|
||||||
if _, ok := sf.seenReadAddr[key]; ok {
|
|
||||||
sf.dupsRead++
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if _, ok := sf.seenWriteAddr[key]; ok {
|
addrs = append(addrs, addr)
|
||||||
sf.dupsCross++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
sf.seenReadAddr[key] = struct{}{}
|
|
||||||
} else {
|
|
||||||
if _, ok := sf.seenReadAddr[key]; ok {
|
|
||||||
sf.dupsCross++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if _, ok := sf.seenWriteAddr[key]; ok {
|
|
||||||
sf.dupsWrite++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
sf.seenWriteAddr[key] = struct{}{}
|
|
||||||
}
|
}
|
||||||
addresses = append(addresses, *task.addr)
|
|
||||||
} else {
|
} else {
|
||||||
key := *task.slot
|
for _, slot := range task.slots {
|
||||||
if task.read {
|
if p.dedupSlot(task.account, slot, task.read) {
|
||||||
if _, ok := sf.seenReadSlot[key]; ok {
|
|
||||||
sf.dupsRead++
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if _, ok := sf.seenWriteSlot[key]; ok {
|
slots[task.account] = append(slots[task.account], slot.Bytes())
|
||||||
sf.dupsCross++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
sf.seenReadSlot[key] = struct{}{}
|
|
||||||
} else {
|
|
||||||
if _, ok := sf.seenReadSlot[key]; ok {
|
|
||||||
sf.dupsCross++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if _, ok := sf.seenWriteSlot[key]; ok {
|
|
||||||
sf.dupsWrite++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
sf.seenWriteSlot[key] = struct{}{}
|
|
||||||
}
|
}
|
||||||
slots = append(slots, key.Bytes())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(addresses) != 0 {
|
if len(addrs) > 0 {
|
||||||
if err := sf.trie.PrefetchAccount(addresses); err != nil {
|
if err := p.trie.PrefetchAccount(addrs); err != nil {
|
||||||
log.Error("Failed to prefetch accounts", "err", err)
|
log.Error("Failed to prefetch accounts", "err", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(slots) != 0 {
|
for addr, keys := range slots {
|
||||||
if err := sf.trie.PrefetchStorage(sf.addr, slots); err != nil {
|
if err := p.trie.PrefetchStorage(addr, keys); err != nil {
|
||||||
log.Error("Failed to prefetch storage", "err", err)
|
log.Error("Failed to prefetch storage", "err", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case <-sf.stop:
|
case <-p.stop:
|
||||||
// Termination is requested, abort if no more tasks are pending. If
|
p.lock.Lock()
|
||||||
// there are some, exhaust them first.
|
done := p.tasks == nil
|
||||||
sf.lock.Lock()
|
p.lock.Unlock()
|
||||||
done := sf.tasks == nil
|
|
||||||
sf.lock.Unlock()
|
|
||||||
|
|
||||||
if done {
|
if done {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Some tasks are pending, loop and pick them up (that wake branch
|
|
||||||
// will be selected eventually, whilst stop remains closed to this
|
|
||||||
// branch will also run afterwards).
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// dedupAddr returns true if addr was already seen for this read/write category.
|
||||||
|
func (p *prefetcher) dedupAddr(addr common.Address, read bool) bool {
|
||||||
|
if read {
|
||||||
|
if _, ok := p.seenReadAddr[addr]; ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if _, ok := p.seenWriteAddr[addr]; ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
p.seenReadAddr[addr] = struct{}{}
|
||||||
|
} else {
|
||||||
|
if _, ok := p.seenReadAddr[addr]; ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if _, ok := p.seenWriteAddr[addr]; ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
p.seenWriteAddr[addr] = struct{}{}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// dedupSlot returns true if slot was already seen for this read/write category.
|
||||||
|
func (p *prefetcher) dedupSlot(addr common.Address, slot common.Hash, read bool) bool {
|
||||||
|
key := slotKey{addr: addr, slot: slot}
|
||||||
|
if read {
|
||||||
|
if _, ok := p.seenReadSlot[key]; ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if _, ok := p.seenWriteSlot[key]; ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
p.seenReadSlot[key] = struct{}{}
|
||||||
|
} else {
|
||||||
|
if _, ok := p.seenReadSlot[key]; ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if _, ok := p.seenWriteSlot[key]; ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
p.seenWriteSlot[key] = struct{}{}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,86 +21,71 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
|
||||||
"github.com/ethereum/go-ethereum/core/tracing"
|
"github.com/ethereum/go-ethereum/core/tracing"
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
"github.com/ethereum/go-ethereum/trie"
|
||||||
"github.com/ethereum/go-ethereum/internal/testrand"
|
|
||||||
"github.com/ethereum/go-ethereum/triedb"
|
|
||||||
"github.com/holiman/uint256"
|
"github.com/holiman/uint256"
|
||||||
)
|
)
|
||||||
|
|
||||||
func filledStateDB() *StateDB {
|
func filledStateDB() *StateDB {
|
||||||
state, _ := New(types.EmptyRootHash, NewDatabaseForTesting())
|
state, _ := New(types.EmptyRootHash, NewDatabaseForTesting())
|
||||||
|
|
||||||
// Create an account and check if the retrieved balance is correct
|
|
||||||
addr := common.HexToAddress("0xaffeaffeaffeaffeaffeaffeaffeaffeaffeaffe")
|
addr := common.HexToAddress("0xaffeaffeaffeaffeaffeaffeaffeaffeaffeaffe")
|
||||||
skey := common.HexToHash("aaa")
|
skey := common.HexToHash("aaa")
|
||||||
sval := common.HexToHash("bbb")
|
sval := common.HexToHash("bbb")
|
||||||
|
|
||||||
state.SetBalance(addr, uint256.NewInt(42), tracing.BalanceChangeUnspecified) // Change the account trie
|
state.SetBalance(addr, uint256.NewInt(42), tracing.BalanceChangeUnspecified)
|
||||||
state.SetCode(addr, []byte("hello"), tracing.CodeChangeUnspecified) // Change an external metadata
|
state.SetCode(addr, []byte("hello"), tracing.CodeChangeUnspecified)
|
||||||
state.SetState(addr, skey, sval) // Change the storage trie
|
state.SetState(addr, skey, sval)
|
||||||
for i := 0; i < 100; i++ {
|
for i := 0; i < 100; i++ {
|
||||||
sk := common.BigToHash(big.NewInt(int64(i)))
|
sk := common.BigToHash(big.NewInt(int64(i)))
|
||||||
state.SetState(addr, sk, sk) // Change the storage trie
|
state.SetState(addr, sk, sk)
|
||||||
}
|
}
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUseAfterTerminate(t *testing.T) {
|
func TestSubfetcherUseAfterTerminate(t *testing.T) {
|
||||||
db := filledStateDB()
|
db := filledStateDB()
|
||||||
prefetcher := newTriePrefetcher(db.db, db.originalRoot, "", true)
|
|
||||||
skey := common.HexToHash("aaa")
|
|
||||||
|
|
||||||
if err := prefetcher.prefetch(common.Hash{}, db.originalRoot, common.Address{}, nil, []common.Hash{skey}, false); err != nil {
|
// Open a trie and create a subfetcher for it.
|
||||||
t.Errorf("Prefetch failed before terminate: %v", err)
|
id := trie.StateTrieID(db.originalRoot)
|
||||||
}
|
tr, err := trie.NewStateTrie(id, db.db.TrieDB())
|
||||||
prefetcher.terminate(false)
|
|
||||||
|
|
||||||
if err := prefetcher.prefetch(common.Hash{}, db.originalRoot, common.Address{}, nil, []common.Hash{skey}, false); err == nil {
|
|
||||||
t.Errorf("Prefetch succeeded after terminate: %v", err)
|
|
||||||
}
|
|
||||||
if tr := prefetcher.trie(common.Hash{}, db.originalRoot); tr == nil {
|
|
||||||
t.Errorf("Prefetcher returned nil trie after terminate")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestVerklePrefetcher(t *testing.T) {
|
|
||||||
disk := rawdb.NewMemoryDatabase()
|
|
||||||
db := triedb.NewDatabase(disk, triedb.VerkleDefaults)
|
|
||||||
sdb := NewDatabase(db, nil)
|
|
||||||
|
|
||||||
state, err := New(types.EmptyRootHash, sdb)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to initialize state: %v", err)
|
t.Fatalf("Failed to open trie: %v", err)
|
||||||
}
|
}
|
||||||
// Create an account and check if the retrieved balance is correct
|
sf := newPrefetcher(tr, false)
|
||||||
addr := testrand.Address()
|
addr := common.HexToAddress("0xaffeaffeaffeaffeaffeaffeaffeaffeaffeaffe")
|
||||||
skey := testrand.Hash()
|
|
||||||
sval := testrand.Hash()
|
|
||||||
|
|
||||||
state.SetBalance(addr, uint256.NewInt(42), tracing.BalanceChangeUnspecified) // Change the account trie
|
// Scheduling before termination should succeed.
|
||||||
state.SetCode(addr, []byte("hello"), tracing.CodeChangeUnspecified) // Change an external metadata
|
if err := sf.scheduleAccounts([]common.Address{addr}, false); err != nil {
|
||||||
state.SetState(addr, skey, sval) // Change the storage trie
|
t.Fatalf("Schedule failed before terminate: %v", err)
|
||||||
root, _ := state.Commit(0, true, false)
|
}
|
||||||
|
// Terminate synchronously — waits for pending tasks.
|
||||||
|
sf.terminate()
|
||||||
|
|
||||||
state, _ = New(root, sdb)
|
// Scheduling after termination should fail.
|
||||||
fetcher := newTriePrefetcher(sdb, root, "", false)
|
if err := sf.scheduleAccounts([]common.Address{addr}, false); err == nil {
|
||||||
|
t.Fatal("Schedule succeeded after terminate")
|
||||||
// Read account
|
}
|
||||||
fetcher.prefetch(common.Hash{}, root, common.Address{}, []common.Address{addr}, nil, false)
|
}
|
||||||
|
|
||||||
// Read storage slot
|
func TestWrapTriePrefetch(t *testing.T) {
|
||||||
fetcher.prefetch(crypto.Keccak256Hash(addr.Bytes()), common.Hash{}, addr, nil, []common.Hash{skey}, false)
|
db := filledStateDB()
|
||||||
|
|
||||||
fetcher.terminate(false)
|
// Create a wrapTrie with prefetching enabled.
|
||||||
accountTrie := fetcher.trie(common.Hash{}, root)
|
id := trie.StateTrieID(db.originalRoot)
|
||||||
storageTrie := fetcher.trie(crypto.Keccak256Hash(addr.Bytes()), common.Hash{})
|
tr, err := newWrapTrie(id, db.db.TrieDB(), true, true)
|
||||||
|
if err != nil {
|
||||||
rootA := accountTrie.Hash()
|
t.Fatalf("Failed to create wrapTrie: %v", err)
|
||||||
rootB := storageTrie.Hash()
|
}
|
||||||
if rootA != rootB {
|
addr := common.HexToAddress("0xaffeaffeaffeaffeaffeaffeaffeaffeaffeaffe")
|
||||||
t.Fatal("Two different tries are retrieved")
|
|
||||||
|
// Schedule some prefetch work.
|
||||||
|
tr.prefetchAccounts([]common.Address{addr}, false)
|
||||||
|
|
||||||
|
// Terminate and verify the trie is usable.
|
||||||
|
tr.term()
|
||||||
|
if tr.Hash() == (common.Hash{}) {
|
||||||
|
t.Fatal("wrapTrie hash is zero after prefetch")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -232,38 +232,31 @@ func (api *DebugAPI) StorageRangeAt(ctx context.Context, blockNrOrHash rpc.Block
|
||||||
}
|
}
|
||||||
|
|
||||||
func storageRangeAt(statedb *state.StateDB, root common.Hash, address common.Address, start []byte, maxResult int) (StorageRangeResult, error) {
|
func storageRangeAt(statedb *state.StateDB, root common.Hash, address common.Address, start []byte, maxResult int) (StorageRangeResult, error) {
|
||||||
storageRoot := statedb.GetStorageRoot(address)
|
it, err := statedb.Database().Iteratee(root)
|
||||||
if storageRoot == types.EmptyRootHash || storageRoot == (common.Hash{}) {
|
if err != nil {
|
||||||
return StorageRangeResult{}, nil // empty storage
|
return StorageRangeResult{}, err
|
||||||
|
}
|
||||||
|
storageIt, err := it.NewStorageIterator(crypto.Keccak256Hash(address.Bytes()), common.BytesToHash(start))
|
||||||
|
if err != nil {
|
||||||
|
return StorageRangeResult{}, err
|
||||||
}
|
}
|
||||||
// TODO(rjl493456442) it's problematic for traversing the state with in-memory
|
// TODO(rjl493456442) it's problematic for traversing the state with in-memory
|
||||||
// state mutations, specifically txIndex != 0.
|
// state mutations, specifically txIndex != 0.
|
||||||
id := trie.StorageTrieID(root, crypto.Keccak256Hash(address.Bytes()), storageRoot)
|
|
||||||
tr, err := trie.NewStateTrie(id, statedb.Database().TrieDB())
|
|
||||||
if err != nil {
|
|
||||||
return StorageRangeResult{}, err
|
|
||||||
}
|
|
||||||
trieIt, err := tr.NodeIterator(start)
|
|
||||||
if err != nil {
|
|
||||||
return StorageRangeResult{}, err
|
|
||||||
}
|
|
||||||
it := trie.NewIterator(trieIt)
|
|
||||||
result := StorageRangeResult{Storage: storageMap{}}
|
result := StorageRangeResult{Storage: storageMap{}}
|
||||||
for i := 0; i < maxResult && it.Next(); i++ {
|
for i := 0; i < maxResult && storageIt.Next(); i++ {
|
||||||
_, content, _, err := rlp.Split(it.Value)
|
_, content, _, err := rlp.Split(storageIt.Slot())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return StorageRangeResult{}, err
|
return StorageRangeResult{}, err
|
||||||
}
|
}
|
||||||
e := storageEntry{Value: common.BytesToHash(content)}
|
e := storageEntry{Value: common.BytesToHash(content)}
|
||||||
if preimage := tr.GetKey(it.Key); preimage != nil {
|
if preimage, err := storageIt.Key(); err == nil {
|
||||||
preimage := common.BytesToHash(preimage)
|
|
||||||
e.Key = &preimage
|
e.Key = &preimage
|
||||||
}
|
}
|
||||||
result.Storage[common.BytesToHash(it.Key)] = e
|
result.Storage[storageIt.Hash()] = e
|
||||||
}
|
}
|
||||||
// Add the 'next key' so clients can continue downloading.
|
// Add the 'next key' so clients can continue downloading.
|
||||||
if it.Next() {
|
if storageIt.Next() {
|
||||||
next := common.BytesToHash(it.Key)
|
next := storageIt.Hash()
|
||||||
result.NextKey = &next
|
result.NextKey = &next
|
||||||
}
|
}
|
||||||
return result, nil
|
return result, nil
|
||||||
|
|
|
||||||
|
|
@ -388,17 +388,15 @@ func (api *BlockChainAPI) GetProof(ctx context.Context, address common.Address,
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
codeHash := statedb.GetCodeHash(address)
|
codeHash := statedb.GetCodeHash(address)
|
||||||
storageRoot := statedb.GetStorageRoot(address)
|
hasher, err := statedb.Database().Hasher(header.Root)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
prover, ok := hasher.(state.Prover)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("state proving is not supported")
|
||||||
|
}
|
||||||
if len(keys) > 0 {
|
if len(keys) > 0 {
|
||||||
var storageTrie state.Trie
|
|
||||||
if storageRoot != types.EmptyRootHash && storageRoot != (common.Hash{}) {
|
|
||||||
st, err := statedb.Database().OpenStorageTrie(header.Root, address, storageRoot, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
storageTrie = st
|
|
||||||
}
|
|
||||||
// Create the proofs for the storageKeys.
|
// Create the proofs for the storageKeys.
|
||||||
for i, key := range keys {
|
for i, key := range keys {
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
|
|
@ -414,12 +412,8 @@ func (api *BlockChainAPI) GetProof(ctx context.Context, address common.Address,
|
||||||
} else {
|
} else {
|
||||||
outputKey = hexutil.Encode(key[:])
|
outputKey = hexutil.Encode(key[:])
|
||||||
}
|
}
|
||||||
if storageTrie == nil {
|
|
||||||
storageProof[i] = StorageResult{outputKey, &hexutil.Big{}, []string{}}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
var proof proofList
|
var proof proofList
|
||||||
if err := storageTrie.Prove(crypto.Keccak256(key.Bytes()), &proof); err != nil {
|
if err := prover.ProveStorage(address, crypto.Keccak256Hash(key.Bytes()), &proof); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
value := (*hexutil.Big)(statedb.GetState(address, key).Big())
|
value := (*hexutil.Big)(statedb.GetState(address, key).Big())
|
||||||
|
|
@ -427,12 +421,8 @@ func (api *BlockChainAPI) GetProof(ctx context.Context, address common.Address,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Create the accountProof.
|
// Create the accountProof.
|
||||||
tr, err := statedb.Database().OpenTrie(header.Root)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var accountProof proofList
|
var accountProof proofList
|
||||||
if err := tr.Prove(crypto.Keccak256(address.Bytes()), &accountProof); err != nil {
|
if err := prover.ProveAccount(address, &accountProof); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
balance := statedb.GetBalance(address).ToBig()
|
balance := statedb.GetBalance(address).ToBig()
|
||||||
|
|
@ -442,7 +432,7 @@ func (api *BlockChainAPI) GetProof(ctx context.Context, address common.Address,
|
||||||
Balance: (*hexutil.Big)(balance),
|
Balance: (*hexutil.Big)(balance),
|
||||||
CodeHash: codeHash,
|
CodeHash: codeHash,
|
||||||
Nonce: hexutil.Uint64(statedb.GetNonce(address)),
|
Nonce: hexutil.Uint64(statedb.GetNonce(address)),
|
||||||
StorageHash: storageRoot,
|
//StorageHash: storageRoot, // TODO(rjl493456442)
|
||||||
StorageProof: storageProof,
|
StorageProof: storageProof,
|
||||||
}, statedb.Error()
|
}, statedb.Error()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -324,7 +324,10 @@ func (miner *Miner) prepareWork(ctx context.Context, genParams *generateParams,
|
||||||
// makeEnv creates a new environment for the sealing block.
|
// makeEnv creates a new environment for the sealing block.
|
||||||
func (miner *Miner) makeEnv(parent *types.Header, header *types.Header, coinbase common.Address, witness bool) (*environment, error) {
|
func (miner *Miner) makeEnv(parent *types.Header, header *types.Header, coinbase common.Address, witness bool) (*environment, error) {
|
||||||
// Retrieve the parent state to execute on top.
|
// Retrieve the parent state to execute on top.
|
||||||
state, err := miner.chain.StateAt(parent.Root)
|
state, err := miner.chain.StateWithConfig(parent.Root, core.StateConfig{
|
||||||
|
Prefetch: true,
|
||||||
|
PrefetchRead: witness,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -334,8 +337,8 @@ func (miner *Miner) makeEnv(parent *types.Header, header *types.Header, coinbase
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
state.TraceWitness(bundle)
|
||||||
}
|
}
|
||||||
state.StartPrefetcher("miner", bundle)
|
|
||||||
// Note the passed coinbase may be different with header.Coinbase.
|
// Note the passed coinbase may be different with header.Coinbase.
|
||||||
return &environment{
|
return &environment{
|
||||||
signer: types.MakeSigner(miner.chainConfig, header.Number, header.Time),
|
signer: types.MakeSigner(miner.chainConfig, header.Number, header.Time),
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,9 @@
|
||||||
package bintrie
|
package bintrie
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
"github.com/ethereum/go-ethereum/trie"
|
"github.com/ethereum/go-ethereum/trie"
|
||||||
|
|
@ -38,15 +40,341 @@ type binaryNodeIterator struct {
|
||||||
stack []binaryNodeIteratorState
|
stack []binaryNodeIteratorState
|
||||||
}
|
}
|
||||||
|
|
||||||
func newBinaryNodeIterator(t *BinaryTrie, _ []byte) (trie.NodeIterator, error) {
|
func newBinaryNodeIterator(t *BinaryTrie, start []byte) (trie.NodeIterator, error) {
|
||||||
if t.Hash() == zero {
|
if t.Hash() == zero {
|
||||||
return &binaryNodeIterator{trie: t, lastErr: errIteratorEnd}, nil
|
return &binaryNodeIterator{trie: t, lastErr: errIteratorEnd}, nil
|
||||||
}
|
}
|
||||||
it := &binaryNodeIterator{trie: t, current: t.root}
|
it := &binaryNodeIterator{trie: t, current: t.root}
|
||||||
// it.err = it.seek(start)
|
if len(start) > 0 {
|
||||||
|
if err := it.seek(start); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
return it, nil
|
return it, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// seek positions the iterator so that the next call to Next(true) advances to
|
||||||
|
// the first leaf with key >= start. It walks down the trie following start's
|
||||||
|
// bit path, building the iterator stack along the way. When the chosen path
|
||||||
|
// dead-ends (Empty, missing child, or a stem strictly less than start), the
|
||||||
|
// implementation backtracks through the existing stack to find the next
|
||||||
|
// in-order subtree and descends to its leftmost leaf.
|
||||||
|
//
|
||||||
|
// A nil/empty start is a no-op; iteration begins at the trie root as usual.
|
||||||
|
//
|
||||||
|
// This is required for resumable bintrie generators (snapshot generation,
|
||||||
|
// pathdb flat-state population) so that an interrupted run can pick up where
|
||||||
|
// it left off after a crash or graceful shutdown.
|
||||||
|
func (it *binaryNodeIterator) seek(start []byte) error {
|
||||||
|
if len(start) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Pad start to a 32-byte key (the trie's natural key length).
|
||||||
|
var key [32]byte
|
||||||
|
copy(key[:], start)
|
||||||
|
|
||||||
|
// Reset state
|
||||||
|
it.stack = it.stack[:0]
|
||||||
|
it.current = nil
|
||||||
|
it.lastErr = nil
|
||||||
|
|
||||||
|
root := it.trie.root
|
||||||
|
if root == nil {
|
||||||
|
it.lastErr = errIteratorEnd
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if _, isEmpty := root.(Empty); isEmpty {
|
||||||
|
it.lastErr = errIteratorEnd
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve the root if it's a HashedNode
|
||||||
|
resolved, err := it.resolveIfHashed(root, nil, 0)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if resolved == nil {
|
||||||
|
it.lastErr = errIteratorEnd
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if resolved != root {
|
||||||
|
it.trie.root = resolved
|
||||||
|
root = resolved
|
||||||
|
}
|
||||||
|
|
||||||
|
return it.seekDescend(root, key[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// seekDescend walks down from `node` following key's bit path. For each
|
||||||
|
// InternalNode encountered, it pushes the node onto the stack with Index set
|
||||||
|
// to the bit it descended into (0 for left, 1 for right) and recurses into
|
||||||
|
// the chosen child. On a StemNode it positions at the appropriate value
|
||||||
|
// offset and returns. On a dead end (Empty, nil, stem < key), it delegates
|
||||||
|
// to seekBacktrack to find the next valid subtree.
|
||||||
|
func (it *binaryNodeIterator) seekDescend(node BinaryNode, key []byte) error {
|
||||||
|
for {
|
||||||
|
switch n := node.(type) {
|
||||||
|
case *InternalNode:
|
||||||
|
depth := n.depth
|
||||||
|
if depth >= 31*8 {
|
||||||
|
return errors.New("seek: internal node too deep")
|
||||||
|
}
|
||||||
|
bit := key[depth/8] >> (7 - uint(depth%8)) & 1
|
||||||
|
|
||||||
|
// Push this internal node with Index = chosen bit. The Next()
|
||||||
|
// loop interprets Index as "the side currently being explored",
|
||||||
|
// so this is consistent with normal iteration state.
|
||||||
|
it.stack = append(it.stack, binaryNodeIteratorState{Node: n, Index: int(bit)})
|
||||||
|
it.current = n
|
||||||
|
|
||||||
|
var child BinaryNode
|
||||||
|
if bit == 0 {
|
||||||
|
child = n.left
|
||||||
|
} else {
|
||||||
|
child = n.right
|
||||||
|
}
|
||||||
|
if child == nil {
|
||||||
|
return it.seekBacktrack()
|
||||||
|
}
|
||||||
|
if _, isEmpty := child.(Empty); isEmpty {
|
||||||
|
return it.seekBacktrack()
|
||||||
|
}
|
||||||
|
// Resolve a hashed child using the current key as the path source.
|
||||||
|
resolved, err := it.resolveIfHashed(child, key, depth+1)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if resolved == nil {
|
||||||
|
return it.seekBacktrack()
|
||||||
|
}
|
||||||
|
if resolved != child {
|
||||||
|
if bit == 0 {
|
||||||
|
n.left = resolved
|
||||||
|
} else {
|
||||||
|
n.right = resolved
|
||||||
|
}
|
||||||
|
}
|
||||||
|
node = resolved
|
||||||
|
|
||||||
|
case *StemNode:
|
||||||
|
cmp := bytes.Compare(n.Stem, key[:StemSize])
|
||||||
|
if cmp < 0 {
|
||||||
|
// Stem is strictly before our target. Don't push it; backtrack
|
||||||
|
// to find the next subtree to the right.
|
||||||
|
return it.seekBacktrack()
|
||||||
|
}
|
||||||
|
startOffset := 0
|
||||||
|
if cmp == 0 {
|
||||||
|
startOffset = int(key[StemSize])
|
||||||
|
}
|
||||||
|
it.stack = append(it.stack, binaryNodeIteratorState{Node: n, Index: startOffset})
|
||||||
|
it.current = n
|
||||||
|
return nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("seek: unexpected node type %T", node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// seekBacktrack walks the existing stack backward looking for the first
|
||||||
|
// InternalNode whose right subtree hasn't been considered yet. If found, it
|
||||||
|
// flips that node's Index to 1 and descends into the leftmost leaf of the
|
||||||
|
// right subtree. If no such ancestor exists, it sets errIteratorEnd.
|
||||||
|
func (it *binaryNodeIterator) seekBacktrack() error {
|
||||||
|
for len(it.stack) > 0 {
|
||||||
|
top := &it.stack[len(it.stack)-1]
|
||||||
|
n, ok := top.Node.(*InternalNode)
|
||||||
|
if !ok {
|
||||||
|
// Not an InternalNode (e.g., a StemNode pushed elsewhere). Pop and
|
||||||
|
// continue. seekDescend never pushes non-internal nodes before
|
||||||
|
// returning, so this is a defensive fallback.
|
||||||
|
it.stack = it.stack[:len(it.stack)-1]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if top.Index == 0 {
|
||||||
|
// We were positioned in the left subtree. Try the right sibling.
|
||||||
|
top.Index = 1
|
||||||
|
right := n.right
|
||||||
|
if right == nil {
|
||||||
|
it.stack = it.stack[:len(it.stack)-1]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, isEmpty := right.(Empty); isEmpty {
|
||||||
|
it.stack = it.stack[:len(it.stack)-1]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Resolve the right child if it's hashed. Use a synthetic path
|
||||||
|
// where the bit at this depth is 1 (we're descending right).
|
||||||
|
resolved, err := it.resolveRightChild(n)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if resolved == nil {
|
||||||
|
it.stack = it.stack[:len(it.stack)-1]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if resolved != right {
|
||||||
|
n.right = resolved
|
||||||
|
right = resolved
|
||||||
|
}
|
||||||
|
it.current = right
|
||||||
|
return it.seekLeftmost(right)
|
||||||
|
}
|
||||||
|
// Index == 1: we were already in the right subtree. Both subtrees of
|
||||||
|
// this internal node have been considered. Pop and try higher.
|
||||||
|
it.stack = it.stack[:len(it.stack)-1]
|
||||||
|
}
|
||||||
|
it.lastErr = errIteratorEnd
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// seekLeftmost descends into the leftmost leaf of the subtree rooted at
|
||||||
|
// `node`, pushing internal nodes onto the stack with Index = 0 (left first).
|
||||||
|
// It positions the iterator at a StemNode with Index = 0, ready to scan
|
||||||
|
// values from offset 0.
|
||||||
|
func (it *binaryNodeIterator) seekLeftmost(node BinaryNode) error {
|
||||||
|
for {
|
||||||
|
switch n := node.(type) {
|
||||||
|
case *InternalNode:
|
||||||
|
it.stack = append(it.stack, binaryNodeIteratorState{Node: n, Index: 0})
|
||||||
|
it.current = n
|
||||||
|
|
||||||
|
child := n.left
|
||||||
|
pickedRight := false
|
||||||
|
if child == nil {
|
||||||
|
child = n.right
|
||||||
|
pickedRight = true
|
||||||
|
}
|
||||||
|
if child != nil {
|
||||||
|
if _, isEmpty := child.(Empty); isEmpty {
|
||||||
|
if !pickedRight {
|
||||||
|
child = n.right
|
||||||
|
pickedRight = true
|
||||||
|
}
|
||||||
|
if child != nil {
|
||||||
|
if _, isEmpty2 := child.(Empty); isEmpty2 {
|
||||||
|
child = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if child == nil {
|
||||||
|
// Both children are empty/nil — degenerate. Pop and let seek
|
||||||
|
// backtrack handle it. (This shouldn't normally happen for a
|
||||||
|
// well-formed trie because internal nodes always have at least
|
||||||
|
// two non-empty children at construction time.)
|
||||||
|
it.stack = it.stack[:len(it.stack)-1]
|
||||||
|
return it.seekBacktrack()
|
||||||
|
}
|
||||||
|
if pickedRight {
|
||||||
|
it.stack[len(it.stack)-1].Index = 1
|
||||||
|
}
|
||||||
|
// Resolve hashed child
|
||||||
|
resolved, err := it.resolveIfHashed(child, nil, n.depth+1)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if resolved == nil {
|
||||||
|
// Resolution failed; treat as empty and try the other side.
|
||||||
|
if pickedRight {
|
||||||
|
// Already tried right; nothing left.
|
||||||
|
it.stack = it.stack[:len(it.stack)-1]
|
||||||
|
return it.seekBacktrack()
|
||||||
|
}
|
||||||
|
// Try right
|
||||||
|
right := n.right
|
||||||
|
if right == nil {
|
||||||
|
it.stack = it.stack[:len(it.stack)-1]
|
||||||
|
return it.seekBacktrack()
|
||||||
|
}
|
||||||
|
if _, isEmpty := right.(Empty); isEmpty {
|
||||||
|
it.stack = it.stack[:len(it.stack)-1]
|
||||||
|
return it.seekBacktrack()
|
||||||
|
}
|
||||||
|
it.stack[len(it.stack)-1].Index = 1
|
||||||
|
resolved, err = it.resolveIfHashed(right, nil, n.depth+1)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if resolved == nil {
|
||||||
|
it.stack = it.stack[:len(it.stack)-1]
|
||||||
|
return it.seekBacktrack()
|
||||||
|
}
|
||||||
|
n.right = resolved
|
||||||
|
node = resolved
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if resolved != child {
|
||||||
|
if pickedRight {
|
||||||
|
n.right = resolved
|
||||||
|
} else {
|
||||||
|
n.left = resolved
|
||||||
|
}
|
||||||
|
}
|
||||||
|
node = resolved
|
||||||
|
|
||||||
|
case *StemNode:
|
||||||
|
it.stack = append(it.stack, binaryNodeIteratorState{Node: n, Index: 0})
|
||||||
|
it.current = n
|
||||||
|
return nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("seekLeftmost: unexpected node type %T", node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolveIfHashed checks whether the given node is a HashedNode and, if so,
|
||||||
|
// uses the trie's nodeResolver to load and deserialize the underlying node.
|
||||||
|
// Returns the resolved node or the original if no resolution was needed.
|
||||||
|
// Returns (nil, nil) if the resolver returned no data (e.g., zero hash).
|
||||||
|
//
|
||||||
|
// keyForPath supplies the bit path used to address the node; for the root
|
||||||
|
// this is unused (path is empty). depth is the depth of the node being
|
||||||
|
// resolved, used for the deserialized node's internal depth field.
|
||||||
|
func (it *binaryNodeIterator) resolveIfHashed(node BinaryNode, keyForPath []byte, depth int) (BinaryNode, error) {
|
||||||
|
hn, ok := node.(HashedNode)
|
||||||
|
if !ok {
|
||||||
|
return node, nil
|
||||||
|
}
|
||||||
|
var path []byte
|
||||||
|
if depth > 0 && keyForPath != nil {
|
||||||
|
var err error
|
||||||
|
path, err = keyToPath(depth-1, keyForPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data, err := it.trie.nodeResolver(path, common.Hash(hn))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if data == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
resolved, err := DeserializeNodeWithHash(data, depth, common.Hash(hn))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resolved, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolveRightChild resolves the right child of an InternalNode using a
|
||||||
|
// synthetic path that ends in bit=1. This is used by seekBacktrack when
|
||||||
|
// flipping from left to right exploration.
|
||||||
|
func (it *binaryNodeIterator) resolveRightChild(parent *InternalNode) (BinaryNode, error) {
|
||||||
|
right := parent.right
|
||||||
|
if _, ok := right.(HashedNode); !ok {
|
||||||
|
return right, nil
|
||||||
|
}
|
||||||
|
// Build a 32-byte key whose bit at parent.depth is 1; rest doesn't matter
|
||||||
|
// for the path computation.
|
||||||
|
var key [32]byte
|
||||||
|
key[parent.depth/8] |= 1 << (7 - uint(parent.depth%8))
|
||||||
|
return it.resolveIfHashed(right, key[:], parent.depth+1)
|
||||||
|
}
|
||||||
|
|
||||||
// Next moves the iterator to the next node. If the parameter is false, any child
|
// Next moves the iterator to the next node. If the parameter is false, any child
|
||||||
// nodes will be skipped.
|
// nodes will be skipped.
|
||||||
func (it *binaryNodeIterator) Next(descend bool) bool {
|
func (it *binaryNodeIterator) Next(descend bool) bool {
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ package bintrie
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"slices"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
|
@ -206,6 +207,241 @@ func TestIteratorDeepTree(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// collectLeaves iterates the trie and returns all (key, value) pairs visited.
|
||||||
|
func collectLeaves(t *testing.T, tr *BinaryTrie, start []byte) [][2][]byte {
|
||||||
|
t.Helper()
|
||||||
|
it, err := newBinaryNodeIterator(tr, start)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var out [][2][]byte
|
||||||
|
for it.Next(true) {
|
||||||
|
if it.Leaf() {
|
||||||
|
k := slices.Clone(it.LeafKey())
|
||||||
|
v := slices.Clone(it.LeafBlob())
|
||||||
|
out = append(out, [2][]byte{k, v})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if it.Error() != nil {
|
||||||
|
t.Fatalf("iterator error: %v", it.Error())
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSeekEmptyStart verifies that seek with a nil/empty start behaves like
|
||||||
|
// a fresh iterator (no skipping).
|
||||||
|
func TestSeekEmptyStart(t *testing.T) {
|
||||||
|
tr := makeTrie(t, [][2]common.Hash{
|
||||||
|
{common.HexToHash("0000000000000000000000000000000000000000000000000000000000000001"), oneKey},
|
||||||
|
{common.HexToHash("8000000000000000000000000000000000000000000000000000000000000001"), oneKey},
|
||||||
|
})
|
||||||
|
// Both nil and empty slice should iterate everything.
|
||||||
|
if got := len(collectLeaves(t, tr, nil)); got != 2 {
|
||||||
|
t.Fatalf("nil start: expected 2 leaves, got %d", got)
|
||||||
|
}
|
||||||
|
if got := len(collectLeaves(t, tr, []byte{})); got != 2 {
|
||||||
|
t.Fatalf("empty start: expected 2 leaves, got %d", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSeekToExactKey verifies that seeking to an existing leaf key positions
|
||||||
|
// the iterator at that exact leaf.
|
||||||
|
func TestSeekToExactKey(t *testing.T) {
|
||||||
|
keys := [][2]common.Hash{
|
||||||
|
{common.HexToHash("0000000000000000000000000000000000000000000000000000000000000001"), oneKey},
|
||||||
|
{common.HexToHash("0000000000000000000000000000000000000000000000000000000000000002"), twoKey},
|
||||||
|
{common.HexToHash("8000000000000000000000000000000000000000000000000000000000000001"), oneKey},
|
||||||
|
}
|
||||||
|
tr := makeTrie(t, keys)
|
||||||
|
|
||||||
|
// Seek to the second key. We expect to see [key2, key3].
|
||||||
|
start := keys[1][0]
|
||||||
|
got := collectLeaves(t, tr, start[:])
|
||||||
|
if len(got) != 2 {
|
||||||
|
t.Fatalf("expected 2 leaves after seek to %x, got %d", start, len(got))
|
||||||
|
}
|
||||||
|
if !bytes.Equal(got[0][0], keys[1][0][:]) {
|
||||||
|
t.Fatalf("first leaf after seek: got %x, want %x", got[0][0], keys[1][0])
|
||||||
|
}
|
||||||
|
if !bytes.Equal(got[1][0], keys[2][0][:]) {
|
||||||
|
t.Fatalf("second leaf after seek: got %x, want %x", got[1][0], keys[2][0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSeekToBetweenKeys verifies that seeking to a key that doesn't exist
|
||||||
|
// positions the iterator at the next existing key (in-order).
|
||||||
|
func TestSeekToBetweenKeys(t *testing.T) {
|
||||||
|
keys := [][2]common.Hash{
|
||||||
|
{common.HexToHash("0000000000000000000000000000000000000000000000000000000000000001"), oneKey},
|
||||||
|
{common.HexToHash("0000000000000000000000000000000000000000000000000000000000000005"), twoKey},
|
||||||
|
{common.HexToHash("8000000000000000000000000000000000000000000000000000000000000001"), oneKey},
|
||||||
|
}
|
||||||
|
tr := makeTrie(t, keys)
|
||||||
|
|
||||||
|
// Seek to a key between key0 and key1: should land at key1.
|
||||||
|
between := common.HexToHash("0000000000000000000000000000000000000000000000000000000000000003")
|
||||||
|
got := collectLeaves(t, tr, between[:])
|
||||||
|
if len(got) != 2 {
|
||||||
|
t.Fatalf("expected 2 leaves after seek between, got %d", len(got))
|
||||||
|
}
|
||||||
|
if !bytes.Equal(got[0][0], keys[1][0][:]) {
|
||||||
|
t.Fatalf("first leaf: got %x, want %x", got[0][0], keys[1][0])
|
||||||
|
}
|
||||||
|
if !bytes.Equal(got[1][0], keys[2][0][:]) {
|
||||||
|
t.Fatalf("second leaf: got %x, want %x", got[1][0], keys[2][0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSeekIntoEmptySubtree verifies that seeking into a subtree where the
|
||||||
|
// chosen path is empty correctly backtracks to the next populated subtree.
|
||||||
|
func TestSeekIntoEmptySubtree(t *testing.T) {
|
||||||
|
// Build a trie with stems split across the bit-0 and bit-1 subtrees.
|
||||||
|
keys := [][2]common.Hash{
|
||||||
|
{common.HexToHash("0000000000000000000000000000000000000000000000000000000000000001"), oneKey},
|
||||||
|
{common.HexToHash("8000000000000000000000000000000000000000000000000000000000000001"), twoKey},
|
||||||
|
}
|
||||||
|
tr := makeTrie(t, keys)
|
||||||
|
|
||||||
|
// Seek to a key in a subtree that's entirely missing (e.g., 0x40...).
|
||||||
|
// The high bit is 0, so we'd descend left, but the left subtree only has
|
||||||
|
// keys with the FIRST bit being 0 — and the seek bit pattern would walk
|
||||||
|
// into a position that has no leaves at or after it on the left side,
|
||||||
|
// requiring backtrack to the right subtree.
|
||||||
|
missing := common.HexToHash("4000000000000000000000000000000000000000000000000000000000000001")
|
||||||
|
got := collectLeaves(t, tr, missing[:])
|
||||||
|
// Should land at key1 (the right subtree leaf).
|
||||||
|
if len(got) != 1 {
|
||||||
|
t.Fatalf("expected 1 leaf after seek into missing subtree, got %d", len(got))
|
||||||
|
}
|
||||||
|
if !bytes.Equal(got[0][0], keys[1][0][:]) {
|
||||||
|
t.Fatalf("leaf: got %x, want %x", got[0][0], keys[1][0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSeekPastEnd verifies that seeking past the last key returns no leaves.
|
||||||
|
func TestSeekPastEnd(t *testing.T) {
|
||||||
|
keys := [][2]common.Hash{
|
||||||
|
{common.HexToHash("0000000000000000000000000000000000000000000000000000000000000001"), oneKey},
|
||||||
|
{common.HexToHash("0000000000000000000000000000000000000000000000000000000000000002"), oneKey},
|
||||||
|
}
|
||||||
|
tr := makeTrie(t, keys)
|
||||||
|
|
||||||
|
// Seek past the maximum key.
|
||||||
|
beyond := common.HexToHash("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")
|
||||||
|
got := collectLeaves(t, tr, beyond[:])
|
||||||
|
if len(got) != 0 {
|
||||||
|
t.Fatalf("expected 0 leaves after seek past end, got %d: %x", len(got), got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSeekWithinSameStem verifies that seeking within a single stem (multiple
|
||||||
|
// values at different offsets) positions correctly at the requested offset.
|
||||||
|
func TestSeekWithinSameStem(t *testing.T) {
|
||||||
|
// All three keys share the same stem; only the last byte differs.
|
||||||
|
keys := [][2]common.Hash{
|
||||||
|
{common.HexToHash("0000000000000000000000000000000000000000000000000000000000000001"), oneKey},
|
||||||
|
{common.HexToHash("0000000000000000000000000000000000000000000000000000000000000005"), twoKey},
|
||||||
|
{common.HexToHash("00000000000000000000000000000000000000000000000000000000000000ff"), oneKey},
|
||||||
|
}
|
||||||
|
tr := makeTrie(t, keys)
|
||||||
|
|
||||||
|
// Seek to offset 5: should yield keys 1 (offset 5) and 2 (offset 0xff).
|
||||||
|
start := common.HexToHash("0000000000000000000000000000000000000000000000000000000000000005")
|
||||||
|
got := collectLeaves(t, tr, start[:])
|
||||||
|
if len(got) != 2 {
|
||||||
|
t.Fatalf("expected 2 leaves, got %d", len(got))
|
||||||
|
}
|
||||||
|
if got[0][0][31] != 0x05 {
|
||||||
|
t.Fatalf("first leaf offset: got 0x%02x, want 0x05", got[0][0][31])
|
||||||
|
}
|
||||||
|
if got[1][0][31] != 0xff {
|
||||||
|
t.Fatalf("second leaf offset: got 0x%02x, want 0xff", got[1][0][31])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seek to offset 6 (between 5 and 0xff): should yield only key 2.
|
||||||
|
start[31] = 0x06
|
||||||
|
got = collectLeaves(t, tr, start[:])
|
||||||
|
if len(got) != 1 {
|
||||||
|
t.Fatalf("expected 1 leaf after seek to offset 6, got %d", len(got))
|
||||||
|
}
|
||||||
|
if got[0][0][31] != 0xff {
|
||||||
|
t.Fatalf("leaf offset: got 0x%02x, want 0xff", got[0][0][31])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSeekResumeSimulation simulates a generator interruption: iterate halfway,
|
||||||
|
// extract the last leaf key, build a fresh iterator, seek to the next key, and
|
||||||
|
// verify that the resumed iteration produces the remaining leaves.
|
||||||
|
func TestSeekResumeSimulation(t *testing.T) {
|
||||||
|
// Construct a deterministic set of keys.
|
||||||
|
var keys [][2]common.Hash
|
||||||
|
for i := range 16 {
|
||||||
|
var k common.Hash
|
||||||
|
k[0] = byte(i << 4) // distribute across the high nibble
|
||||||
|
k[31] = 0x01
|
||||||
|
keys = append(keys, [2]common.Hash{k, oneKey})
|
||||||
|
}
|
||||||
|
tr := makeTrie(t, keys)
|
||||||
|
|
||||||
|
// First pass: collect all leaves.
|
||||||
|
all := collectLeaves(t, tr, nil)
|
||||||
|
if len(all) != 16 {
|
||||||
|
t.Fatalf("first pass: expected 16 leaves, got %d", len(all))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop after the 7th leaf and resume.
|
||||||
|
stopIdx := 7
|
||||||
|
lastKey := all[stopIdx][0]
|
||||||
|
|
||||||
|
// Resume: seek to the byte AFTER lastKey (we use lastKey + 1 in the last
|
||||||
|
// byte; for our keys this is sufficient because each key's last byte is
|
||||||
|
// 0x01 and we want to go to the NEXT stem).
|
||||||
|
resumeKey := slices.Clone(lastKey)
|
||||||
|
// Increment the last byte; if it overflows, that's fine for these keys
|
||||||
|
// because all our last bytes are 0x01.
|
||||||
|
resumeKey[31]++
|
||||||
|
// But actually we want to start AT lastKey + 1, which for our keys means
|
||||||
|
// we want the NEXT stem. Since each stem has only one value at offset 0x01
|
||||||
|
// and we want everything strictly after lastKey, set offset to 0x02.
|
||||||
|
got := collectLeaves(t, tr, resumeKey)
|
||||||
|
if len(got) != len(all)-stopIdx-1 {
|
||||||
|
t.Fatalf("resume: expected %d leaves, got %d", len(all)-stopIdx-1, len(got))
|
||||||
|
}
|
||||||
|
for i, leaf := range got {
|
||||||
|
want := all[stopIdx+1+i]
|
||||||
|
if !bytes.Equal(leaf[0], want[0]) {
|
||||||
|
t.Fatalf("resume leaf %d: got %x, want %x", i, leaf[0], want[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSeekDeepTree verifies seek works on a tree with a long shared prefix.
|
||||||
|
func TestSeekDeepTree(t *testing.T) {
|
||||||
|
keys := [][2]common.Hash{
|
||||||
|
{common.HexToHash("0000000000C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0"), oneKey},
|
||||||
|
{common.HexToHash("0000000000E00000000000000000000000000000000000000000000000000000"), twoKey},
|
||||||
|
}
|
||||||
|
tr := makeTrie(t, keys)
|
||||||
|
|
||||||
|
// Seek to the first key exactly.
|
||||||
|
got := collectLeaves(t, tr, keys[0][0][:])
|
||||||
|
if len(got) != 2 {
|
||||||
|
t.Fatalf("seek to first: expected 2 leaves, got %d", len(got))
|
||||||
|
}
|
||||||
|
if !bytes.Equal(got[0][0], keys[0][0][:]) {
|
||||||
|
t.Fatalf("first leaf: got %x, want %x", got[0][0], keys[0][0])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seek to the second key exactly.
|
||||||
|
got = collectLeaves(t, tr, keys[1][0][:])
|
||||||
|
if len(got) != 1 {
|
||||||
|
t.Fatalf("seek to second: expected 1 leaf, got %d", len(got))
|
||||||
|
}
|
||||||
|
if !bytes.Equal(got[0][0], keys[1][0][:]) {
|
||||||
|
t.Fatalf("leaf: got %x, want %x", got[0][0], keys[1][0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestIteratorNodeCount verifies the total number of Next(true) calls
|
// TestIteratorNodeCount verifies the total number of Next(true) calls
|
||||||
// for a known tree structure.
|
// for a known tree structure.
|
||||||
func TestIteratorNodeCount(t *testing.T) {
|
func TestIteratorNodeCount(t *testing.T) {
|
||||||
|
|
|
||||||
78
trie/bintrie/pack.go
Normal file
78
trie/bintrie/pack.go
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
// Copyright 2026 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 bintrie
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
|
||||||
|
"github.com/holiman/uint256"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PackBasicData encodes an account's basic metadata (code size, nonce,
|
||||||
|
// balance) into the 32-byte BasicData leaf value defined by EIP-7864.
|
||||||
|
//
|
||||||
|
// The canonical spec layout is:
|
||||||
|
//
|
||||||
|
// byte 0 version (currently always 0, left as the implicit zero)
|
||||||
|
// bytes 1..4 reserved
|
||||||
|
// bytes 5..7 code_size (big-endian, 3 bytes, max 2^24-1)
|
||||||
|
// bytes 8..15 nonce (big-endian, 8 bytes)
|
||||||
|
// bytes 16..31 balance (big-endian, right-justified, 16 bytes)
|
||||||
|
//
|
||||||
|
// For historical reasons the existing BinaryTrie implementation writes
|
||||||
|
// code_size as a 4-byte big-endian uint32 starting at byte 4 rather than a
|
||||||
|
// 3-byte big-endian field starting at byte 5. Byte 4 is reserved per the
|
||||||
|
// EIP, so for any realistic code size (below 2^24 ≈ 16 MB, well under the
|
||||||
|
// EIP-170 24 KB contract limit) the high byte is always 0 and the two
|
||||||
|
// encodings are bit-equivalent. This function preserves that existing
|
||||||
|
// behavior byte-for-byte so callers can substitute it for the inlined
|
||||||
|
// encoding in BinaryTrie.UpdateAccount without changing any state root.
|
||||||
|
//
|
||||||
|
// Any future correction of the byte offset is a consensus-level change
|
||||||
|
// and must be coordinated across clients.
|
||||||
|
func PackBasicData(nonce uint64, balance *uint256.Int, codeSize int) [HashSize]byte {
|
||||||
|
var data [HashSize]byte
|
||||||
|
binary.BigEndian.PutUint32(data[BasicDataCodeSizeOffset-1:], uint32(codeSize))
|
||||||
|
binary.BigEndian.PutUint64(data[BasicDataNonceOffset:], nonce)
|
||||||
|
|
||||||
|
// Balance is a 256-bit uint stored right-justified in the lower 16
|
||||||
|
// bytes of BasicData. For dev-mode accounts whose balance exceeds
|
||||||
|
// 2^128 - 1 (e.g. 0xff × HashSize), truncate to the upper 16 bytes to
|
||||||
|
// match the existing BinaryTrie behavior rather than panicking.
|
||||||
|
balanceBytes := balance.Bytes()
|
||||||
|
if len(balanceBytes) > 16 {
|
||||||
|
balanceBytes = balanceBytes[16:]
|
||||||
|
}
|
||||||
|
copy(data[HashSize-len(balanceBytes):], balanceBytes[:])
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnpackBasicData is the inverse of PackBasicData. It decodes the code
|
||||||
|
// size, nonce, and balance fields from a BasicData leaf value.
|
||||||
|
//
|
||||||
|
// Note: the returned balance is always 128-bit or smaller because the
|
||||||
|
// encoding reserves 16 bytes for it; dev-mode accounts whose pre-encoded
|
||||||
|
// balance exceeded 2^128 - 1 are not recoverable losslessly.
|
||||||
|
func UnpackBasicData(data [HashSize]byte) (nonce uint64, balance *uint256.Int, codeSize int) {
|
||||||
|
codeSize = int(binary.BigEndian.Uint32(data[BasicDataCodeSizeOffset-1:]))
|
||||||
|
nonce = binary.BigEndian.Uint64(data[BasicDataNonceOffset:])
|
||||||
|
|
||||||
|
var b [16]byte
|
||||||
|
copy(b[:], data[BasicDataBalanceOffset:])
|
||||||
|
balance = new(uint256.Int).SetBytes(b[:])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
@ -242,29 +242,21 @@ func (t *BinaryTrie) GetStorage(addr common.Address, key []byte) ([]byte, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateAccount updates the account information for the given address.
|
// UpdateAccount updates the account information for the given address.
|
||||||
|
//
|
||||||
|
// The BasicData encoding (nonce, balance, code size packed into 32 bytes)
|
||||||
|
// is delegated to PackBasicData so that callers outside the trie layer —
|
||||||
|
// notably the flat-state codec that writes stem blobs to pathdb — can
|
||||||
|
// produce a bit-identical value without duplicating the layout logic.
|
||||||
func (t *BinaryTrie) UpdateAccount(addr common.Address, acc *types.StateAccount, codeLen int) error {
|
func (t *BinaryTrie) UpdateAccount(addr common.Address, acc *types.StateAccount, codeLen int) error {
|
||||||
var (
|
var (
|
||||||
err error
|
values = make([][]byte, StemNodeWidth)
|
||||||
basicData [HashSize]byte
|
stem = GetBinaryTreeKey(addr, zero[:])
|
||||||
values = make([][]byte, StemNodeWidth)
|
|
||||||
stem = GetBinaryTreeKey(addr, zero[:])
|
|
||||||
)
|
)
|
||||||
binary.BigEndian.PutUint32(basicData[BasicDataCodeSizeOffset-1:], uint32(codeLen))
|
basicData := PackBasicData(acc.Nonce, acc.Balance, codeLen)
|
||||||
binary.BigEndian.PutUint64(basicData[BasicDataNonceOffset:], acc.Nonce)
|
|
||||||
|
|
||||||
// Because the balance is a max of 16 bytes, truncate
|
|
||||||
// the extra values. This happens in devmode, where
|
|
||||||
// 0xff**HashSize is allocated to the developer account.
|
|
||||||
balanceBytes := acc.Balance.Bytes()
|
|
||||||
// TODO: reduce the size of the allocation in devmode, then panic instead
|
|
||||||
// of truncating.
|
|
||||||
if len(balanceBytes) > 16 {
|
|
||||||
balanceBytes = balanceBytes[16:]
|
|
||||||
}
|
|
||||||
copy(basicData[HashSize-len(balanceBytes):], balanceBytes[:])
|
|
||||||
values[BasicDataLeafKey] = basicData[:]
|
values[BasicDataLeafKey] = basicData[:]
|
||||||
values[CodeHashLeafKey] = acc.CodeHash[:]
|
values[CodeHashLeafKey] = acc.CodeHash[:]
|
||||||
|
|
||||||
|
var err error
|
||||||
t.root, err = t.root.InsertValuesAtStem(stem, values, t.nodeResolver, 0)
|
t.root, err = t.root.InsertValuesAtStem(stem, values, t.nodeResolver, 0)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -352,9 +344,10 @@ func (t *BinaryTrie) Commit(_ bool) (common.Hash, *trienode.NodeSet) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NodeIterator returns an iterator that returns nodes of the trie. Iteration
|
// NodeIterator returns an iterator that returns nodes of the trie. Iteration
|
||||||
// starts at the key after the given start key.
|
// starts at the first leaf with key >= startKey. A nil/empty startKey iterates
|
||||||
|
// the whole trie.
|
||||||
func (t *BinaryTrie) NodeIterator(startKey []byte) (trie.NodeIterator, error) {
|
func (t *BinaryTrie) NodeIterator(startKey []byte) (trie.NodeIterator, error) {
|
||||||
return newBinaryNodeIterator(t, nil)
|
return newBinaryNodeIterator(t, startKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prove constructs a Merkle proof for key. The result contains all encoded nodes
|
// Prove constructs a Merkle proof for key. The result contains all encoded nodes
|
||||||
|
|
|
||||||
|
|
@ -259,6 +259,16 @@ func (set *MergedNodeSet) Merge(other *NodeSet) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MergeSet merges the provided set into local one.
|
||||||
|
func (set *MergedNodeSet) MergeSet(other *MergedNodeSet) error {
|
||||||
|
for _, subset := range other.Sets {
|
||||||
|
if err := set.Merge(subset); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Nodes returns a two-dimensional map for internal nodes.
|
// Nodes returns a two-dimensional map for internal nodes.
|
||||||
func (set *MergedNodeSet) Nodes() map[common.Hash]map[string]*Node {
|
func (set *MergedNodeSet) Nodes() map[common.Hash]map[string]*Node {
|
||||||
nodes := make(map[common.Hash]map[string]*Node, len(set.Sets))
|
nodes := make(map[common.Hash]map[string]*Node, len(set.Sets))
|
||||||
|
|
|
||||||
|
|
@ -132,7 +132,10 @@ func (b *buffer) size() uint64 {
|
||||||
|
|
||||||
// flush persists the in-memory dirty trie node into the disk if the configured
|
// flush persists the in-memory dirty trie node into the disk if the configured
|
||||||
// memory threshold is reached. Note, all data must be written atomically.
|
// memory threshold is reached. Note, all data must be written atomically.
|
||||||
func (b *buffer) flush(root common.Hash, db ethdb.KeyValueStore, freezers []ethdb.AncientWriter, progress []byte, nodesCache, statesCache *fastcache.Cache, id uint64, postFlush func()) {
|
//
|
||||||
|
// codec is the flat-state codec used for state persistence and cache key
|
||||||
|
// derivation. It is supplied by the disk layer's owning Database.
|
||||||
|
func (b *buffer) flush(root common.Hash, db ethdb.KeyValueStore, codec flatStateCodec, freezers []ethdb.AncientWriter, progress []byte, nodesCache, statesCache *fastcache.Cache, id uint64, postFlush func()) {
|
||||||
if b.done != nil {
|
if b.done != nil {
|
||||||
panic("duplicated flush operation")
|
panic("duplicated flush operation")
|
||||||
}
|
}
|
||||||
|
|
@ -158,7 +161,7 @@ func (b *buffer) flush(root common.Hash, db ethdb.KeyValueStore, freezers []ethd
|
||||||
// Terminate the state snapshot generation if it's active
|
// Terminate the state snapshot generation if it's active
|
||||||
var (
|
var (
|
||||||
start = time.Now()
|
start = time.Now()
|
||||||
batch = db.NewBatchWithSize((b.nodes.dbsize() + b.states.dbsize()) * 11 / 10) // extra 10% for potential pebble internal stuff
|
batch = db.NewBatchWithSize((b.nodes.dbsize() + b.states.dbsize(codec)) * 11 / 10) // extra 10% for potential pebble internal stuff
|
||||||
)
|
)
|
||||||
// Explicitly sync the state freezer to ensure all written data is persisted to disk
|
// Explicitly sync the state freezer to ensure all written data is persisted to disk
|
||||||
// before updating the key-value store.
|
// before updating the key-value store.
|
||||||
|
|
@ -170,7 +173,11 @@ func (b *buffer) flush(root common.Hash, db ethdb.KeyValueStore, freezers []ethd
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
nodes := b.nodes.write(batch, nodesCache)
|
nodes := b.nodes.write(batch, nodesCache)
|
||||||
accounts, slots := b.states.write(batch, progress, statesCache)
|
accounts, slots, flushErr := b.states.write(batch, codec, progress, statesCache)
|
||||||
|
if flushErr != nil {
|
||||||
|
b.flushErr = flushErr
|
||||||
|
return
|
||||||
|
}
|
||||||
rawdb.WritePersistentStateID(batch, id)
|
rawdb.WritePersistentStateID(batch, id)
|
||||||
rawdb.WriteSnapshotRoot(batch, root)
|
rawdb.WriteSnapshotRoot(batch, root)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -95,19 +95,21 @@ type generatorContext struct {
|
||||||
account *holdableIterator // Iterator of account snapshot data
|
account *holdableIterator // Iterator of account snapshot data
|
||||||
storage *holdableIterator // Iterator of storage snapshot data
|
storage *holdableIterator // Iterator of storage snapshot data
|
||||||
db ethdb.KeyValueStore // Key-value store containing the snapshot data
|
db ethdb.KeyValueStore // Key-value store containing the snapshot data
|
||||||
|
codec flatStateCodec // Flat-state codec for prefix/key-length selection
|
||||||
batch ethdb.Batch // Database batch for writing data atomically
|
batch ethdb.Batch // Database batch for writing data atomically
|
||||||
logged time.Time // The timestamp when last generation progress was displayed
|
logged time.Time // The timestamp when last generation progress was displayed
|
||||||
}
|
}
|
||||||
|
|
||||||
// newGeneratorContext initializes the context for generation.
|
// newGeneratorContext initializes the context for generation.
|
||||||
func newGeneratorContext(root common.Hash, marker []byte, db ethdb.KeyValueStore) *generatorContext {
|
func newGeneratorContext(root common.Hash, marker []byte, db ethdb.KeyValueStore, codec flatStateCodec) *generatorContext {
|
||||||
ctx := &generatorContext{
|
ctx := &generatorContext{
|
||||||
root: root,
|
root: root,
|
||||||
db: db,
|
db: db,
|
||||||
|
codec: codec,
|
||||||
batch: db.NewBatch(),
|
batch: db.NewBatch(),
|
||||||
logged: time.Now(),
|
logged: time.Now(),
|
||||||
}
|
}
|
||||||
accMarker, storageMarker := splitMarker(marker)
|
accMarker, storageMarker := codec.SplitMarker(marker)
|
||||||
ctx.openIterator(snapAccount, accMarker)
|
ctx.openIterator(snapAccount, accMarker)
|
||||||
ctx.openIterator(snapStorage, storageMarker)
|
ctx.openIterator(snapStorage, storageMarker)
|
||||||
return ctx
|
return ctx
|
||||||
|
|
@ -118,12 +120,12 @@ func newGeneratorContext(root common.Hash, marker []byte, db ethdb.KeyValueStore
|
||||||
// to time to avoid blocking leveldb compaction for a long time.
|
// to time to avoid blocking leveldb compaction for a long time.
|
||||||
func (ctx *generatorContext) openIterator(kind string, start []byte) {
|
func (ctx *generatorContext) openIterator(kind string, start []byte) {
|
||||||
if kind == snapAccount {
|
if kind == snapAccount {
|
||||||
iter := ctx.db.NewIterator(rawdb.SnapshotAccountPrefix, start)
|
iter := ctx.db.NewIterator(ctx.codec.AccountPrefix(), start)
|
||||||
ctx.account = newHoldableIterator(rawdb.NewKeyLengthIterator(iter, 1+common.HashLength))
|
ctx.account = newHoldableIterator(rawdb.NewKeyLengthIterator(iter, ctx.codec.AccountKeyLength()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
iter := ctx.db.NewIterator(rawdb.SnapshotStoragePrefix, start)
|
iter := ctx.db.NewIterator(ctx.codec.StoragePrefix(), start)
|
||||||
ctx.storage = newHoldableIterator(rawdb.NewKeyLengthIterator(iter, 1+2*common.HashLength))
|
ctx.storage = newHoldableIterator(rawdb.NewKeyLengthIterator(iter, ctx.codec.StorageKeyLength()))
|
||||||
}
|
}
|
||||||
|
|
||||||
// reopenIterator releases the specified snapshot iterator and re-open it
|
// reopenIterator releases the specified snapshot iterator and re-open it
|
||||||
|
|
|
||||||
|
|
@ -125,10 +125,11 @@ type Database struct {
|
||||||
// readOnly is the flag whether the mutation is allowed to be applied.
|
// readOnly is the flag whether the mutation is allowed to be applied.
|
||||||
// It will be set automatically when the database is journaled during
|
// It will be set automatically when the database is journaled during
|
||||||
// the shutdown to reject all following unexpected mutations.
|
// the shutdown to reject all following unexpected mutations.
|
||||||
readOnly bool // Flag if database is opened in read only mode
|
readOnly bool // Flag if database is opened in read only mode
|
||||||
waitSync bool // Flag if database is deactivated due to initial state sync
|
waitSync bool // Flag if database is deactivated due to initial state sync
|
||||||
isVerkle bool // Flag if database is used for verkle tree
|
isVerkle bool // Flag if database is used for verkle tree
|
||||||
hasher nodeHasher // Trie node hasher
|
hasher nodeHasher // Trie node hasher
|
||||||
|
flatCodec flatStateCodec // Flat-state key derivation, persistence and iteration
|
||||||
|
|
||||||
config *Config // Configuration for database
|
config *Config // Configuration for database
|
||||||
diskdb ethdb.Database // Persistent storage for matured trie nodes
|
diskdb ethdb.Database // Persistent storage for matured trie nodes
|
||||||
|
|
@ -153,11 +154,12 @@ func New(diskdb ethdb.Database, config *Config, isVerkle bool) *Database {
|
||||||
config = config.sanitize()
|
config = config.sanitize()
|
||||||
|
|
||||||
db := &Database{
|
db := &Database{
|
||||||
readOnly: config.ReadOnly,
|
readOnly: config.ReadOnly,
|
||||||
isVerkle: isVerkle,
|
isVerkle: isVerkle,
|
||||||
config: config,
|
config: config,
|
||||||
diskdb: diskdb,
|
diskdb: diskdb,
|
||||||
hasher: merkleNodeHasher,
|
hasher: merkleNodeHasher,
|
||||||
|
flatCodec: &merkleFlatCodec{},
|
||||||
}
|
}
|
||||||
// Establish a dedicated database namespace tailored for verkle-specific
|
// Establish a dedicated database namespace tailored for verkle-specific
|
||||||
// data, ensuring the isolation of both verkle and merkle tree data. It's
|
// data, ensuring the isolation of both verkle and merkle tree data. It's
|
||||||
|
|
@ -167,6 +169,12 @@ func New(diskdb ethdb.Database, config *Config, isVerkle bool) *Database {
|
||||||
if isVerkle {
|
if isVerkle {
|
||||||
db.diskdb = rawdb.NewTable(diskdb, string(rawdb.VerklePrefix))
|
db.diskdb = rawdb.NewTable(diskdb, string(rawdb.VerklePrefix))
|
||||||
db.hasher = binaryNodeHasher
|
db.hasher = binaryNodeHasher
|
||||||
|
// Wire the bintrie flat-state codec so the disklayer/buffer/generator
|
||||||
|
// all use the per-stem on-disk layout. The codec needs a reader for
|
||||||
|
// the read-modify-write performed by applyWrites; the namespaced
|
||||||
|
// db.diskdb is the right backing store because all bintrie keys
|
||||||
|
// (trie nodes AND stem blobs) live under the verkle prefix.
|
||||||
|
db.flatCodec = newBintrieFlatCodec(db.diskdb)
|
||||||
}
|
}
|
||||||
// Construct the layer tree by resolving the in-disk singleton state
|
// Construct the layer tree by resolving the in-disk singleton state
|
||||||
// and in-memory layer journal.
|
// and in-memory layer journal.
|
||||||
|
|
@ -232,7 +240,7 @@ func (db *Database) setHistoryIndexer() {
|
||||||
func (db *Database) setStateGenerator() error {
|
func (db *Database) setStateGenerator() error {
|
||||||
// Load the state snapshot generation progress marker to prevent access
|
// Load the state snapshot generation progress marker to prevent access
|
||||||
// to uncovered states.
|
// to uncovered states.
|
||||||
generator, root, err := loadGenerator(db.diskdb, db.hasher)
|
generator, root, err := loadGenerator(db.diskdb, db.hasher, db.isVerkle)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -264,13 +272,18 @@ func (db *Database) setStateGenerator() error {
|
||||||
// Disable the background snapshot building in these circumstances:
|
// Disable the background snapshot building in these circumstances:
|
||||||
// - the database is opened in read only mode
|
// - the database is opened in read only mode
|
||||||
// - the snapshot build is explicitly disabled
|
// - the snapshot build is explicitly disabled
|
||||||
// - the database is opened in verkle tree mode
|
//
|
||||||
noBuild := db.readOnly || db.config.SnapshotNoBuild || db.isVerkle
|
// Note: bintrie/verkle mode is no longer excluded here. The bintrie
|
||||||
|
// codec ships its own snapshot generator (see generate_bintrie.go) so
|
||||||
|
// the unified flat-state path can populate stem blobs from an existing
|
||||||
|
// trie. Generator dispatch in newGenerator/generator.run picks the
|
||||||
|
// right routine based on the active flatStateCodec.
|
||||||
|
noBuild := db.readOnly || db.config.SnapshotNoBuild
|
||||||
|
|
||||||
// Construct the generator and link it to the disk layer, ensuring that the
|
// Construct the generator and link it to the disk layer, ensuring that the
|
||||||
// generation progress is resolved to prevent accessing uncovered states
|
// generation progress is resolved to prevent accessing uncovered states
|
||||||
// regardless of whether background state snapshot generation is allowed.
|
// regardless of whether background state snapshot generation is allowed.
|
||||||
dl.setGenerator(newGenerator(db.diskdb, noBuild, generator.Marker, stats))
|
dl.setGenerator(newGenerator(db.diskdb, db.flatCodec, noBuild, generator.Marker, stats))
|
||||||
|
|
||||||
// Short circuit if the background generation is not permitted
|
// Short circuit if the background generation is not permitted
|
||||||
if noBuild || db.waitSync {
|
if noBuild || db.waitSync {
|
||||||
|
|
@ -408,7 +421,9 @@ func (db *Database) Enable(root common.Hash) error {
|
||||||
|
|
||||||
// Re-construct a new disk layer backed by persistent state
|
// Re-construct a new disk layer backed by persistent state
|
||||||
// and schedule the state snapshot generation if it's permitted.
|
// and schedule the state snapshot generation if it's permitted.
|
||||||
db.tree.init(generateSnapshot(db, root, db.isVerkle || db.config.SnapshotNoBuild))
|
// Bintrie/verkle is no longer treated as "noBuild" — the bintrie
|
||||||
|
// generator (Commit 9) handles regeneration from the unified trie.
|
||||||
|
db.tree.init(generateSnapshot(db, root, db.config.SnapshotNoBuild))
|
||||||
|
|
||||||
// After snap sync, the state of the database may have changed completely.
|
// After snap sync, the state of the database may have changed completely.
|
||||||
// To ensure the history indexer always matches the current state, we must:
|
// To ensure the history indexer always matches the current state, we must:
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
package pathdb
|
package pathdb
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -25,7 +25,6 @@ import (
|
||||||
"github.com/VictoriaMetrics/fastcache"
|
"github.com/VictoriaMetrics/fastcache"
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
|
||||||
"github.com/ethereum/go-ethereum/ethdb"
|
"github.com/ethereum/go-ethereum/ethdb"
|
||||||
"github.com/ethereum/go-ethereum/log"
|
"github.com/ethereum/go-ethereum/log"
|
||||||
)
|
)
|
||||||
|
|
@ -141,7 +140,13 @@ func (dl *diskLayer) node(owner common.Hash, path []byte, depth int) ([]byte, co
|
||||||
if blob := dl.nodes.Get(nil, key); len(blob) > 0 {
|
if blob := dl.nodes.Get(nil, key); len(blob) > 0 {
|
||||||
cleanNodeHitMeter.Mark(1)
|
cleanNodeHitMeter.Mark(1)
|
||||||
cleanNodeReadMeter.Mark(int64(len(blob)))
|
cleanNodeReadMeter.Mark(int64(len(blob)))
|
||||||
return blob, crypto.Keccak256Hash(blob), nodeLoc{loc: locCleanCache, depth: depth}, nil
|
// Use the scheme-appropriate hasher (keccak256 for merkle,
|
||||||
|
// sha256-via-bintrie for binary trie).
|
||||||
|
h, err := dl.db.hasher(blob)
|
||||||
|
if err != nil {
|
||||||
|
return nil, common.Hash{}, nodeLoc{}, fmt.Errorf("hash cached trie node: %w", err)
|
||||||
|
}
|
||||||
|
return blob, h, nodeLoc{loc: locCleanCache, depth: depth}, nil
|
||||||
}
|
}
|
||||||
cleanNodeMissMeter.Mark(1)
|
cleanNodeMissMeter.Mark(1)
|
||||||
}
|
}
|
||||||
|
|
@ -161,7 +166,11 @@ func (dl *diskLayer) node(owner common.Hash, path []byte, depth int) ([]byte, co
|
||||||
dl.nodes.Set(key, blob)
|
dl.nodes.Set(key, blob)
|
||||||
cleanNodeWriteMeter.Mark(int64(len(blob)))
|
cleanNodeWriteMeter.Mark(int64(len(blob)))
|
||||||
}
|
}
|
||||||
return blob, crypto.Keccak256Hash(blob), nodeLoc{loc: locDiskLayer, depth: depth}, nil
|
h, err := dl.db.hasher(blob)
|
||||||
|
if err != nil {
|
||||||
|
return nil, common.Hash{}, nodeLoc{}, fmt.Errorf("hash disk trie node: %w", err)
|
||||||
|
}
|
||||||
|
return blob, h, nodeLoc{loc: locDiskLayer, depth: depth}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// account directly retrieves the account RLP associated with a particular
|
// account directly retrieves the account RLP associated with a particular
|
||||||
|
|
@ -199,13 +208,15 @@ func (dl *diskLayer) account(hash common.Hash, depth int) ([]byte, error) {
|
||||||
|
|
||||||
// If the layer is being generated, ensure the requested account has
|
// If the layer is being generated, ensure the requested account has
|
||||||
// already been covered by the generator.
|
// already been covered by the generator.
|
||||||
|
codec := dl.db.flatCodec
|
||||||
marker := dl.genMarker()
|
marker := dl.genMarker()
|
||||||
if marker != nil && bytes.Compare(hash.Bytes(), marker) > 0 {
|
if marker != nil && codec.MarkerCompare(hash.Bytes(), marker) > 0 {
|
||||||
return nil, errNotCoveredYet
|
return nil, errNotCoveredYet
|
||||||
}
|
}
|
||||||
// Try to retrieve the account from the memory cache
|
// Try to retrieve the account from the memory cache
|
||||||
|
cacheKey := codec.AccountCacheKey(hash)
|
||||||
if dl.states != nil {
|
if dl.states != nil {
|
||||||
if blob, found := dl.states.HasGet(nil, hash[:]); found {
|
if blob, found := dl.states.HasGet(nil, cacheKey); found {
|
||||||
cleanStateHitMeter.Mark(1)
|
cleanStateHitMeter.Mark(1)
|
||||||
cleanStateReadMeter.Mark(int64(len(blob)))
|
cleanStateReadMeter.Mark(int64(len(blob)))
|
||||||
|
|
||||||
|
|
@ -219,7 +230,7 @@ func (dl *diskLayer) account(hash common.Hash, depth int) ([]byte, error) {
|
||||||
cleanStateMissMeter.Mark(1)
|
cleanStateMissMeter.Mark(1)
|
||||||
}
|
}
|
||||||
// Try to retrieve the account from the disk.
|
// Try to retrieve the account from the disk.
|
||||||
blob := rawdb.ReadAccountSnapshot(dl.db.diskdb, hash)
|
blob := codec.ReadAccount(dl.db.diskdb, hash)
|
||||||
|
|
||||||
// Store the resolved data in the clean cache. The background buffer flusher
|
// Store the resolved data in the clean cache. The background buffer flusher
|
||||||
// may also write to the clean cache concurrently, but two writers cannot
|
// may also write to the clean cache concurrently, but two writers cannot
|
||||||
|
|
@ -227,7 +238,7 @@ func (dl *diskLayer) account(hash common.Hash, depth int) ([]byte, error) {
|
||||||
// it will be found in the frozen buffer, eliminating the need to check the
|
// it will be found in the frozen buffer, eliminating the need to check the
|
||||||
// database.
|
// database.
|
||||||
if dl.states != nil {
|
if dl.states != nil {
|
||||||
dl.states.Set(hash[:], blob)
|
dl.states.Set(cacheKey, blob)
|
||||||
cleanStateWriteMeter.Mark(int64(len(blob)))
|
cleanStateWriteMeter.Mark(int64(len(blob)))
|
||||||
}
|
}
|
||||||
if len(blob) == 0 {
|
if len(blob) == 0 {
|
||||||
|
|
@ -276,14 +287,27 @@ func (dl *diskLayer) storage(accountHash, storageHash common.Hash, depth int) ([
|
||||||
|
|
||||||
// If the layer is being generated, ensure the requested storage slot
|
// If the layer is being generated, ensure the requested storage slot
|
||||||
// has already been covered by the generator.
|
// has already been covered by the generator.
|
||||||
key := storageKeySlice(accountHash, storageHash)
|
//
|
||||||
|
// The codec derives the scheme-appropriate marker comparison key:
|
||||||
|
// merkle uses the 64-byte (accountHash||storageHash) concatenation;
|
||||||
|
// bintrie uses the 32-byte storageHash directly (which is the full
|
||||||
|
// stem||offset key matching the bintrie generator's 32-byte marker).
|
||||||
|
// Pre-A4 this always used the 64-byte shape, which was fail-open
|
||||||
|
// for bintrie because the zero accountHash sorts before any
|
||||||
|
// sha256-derived marker byte.
|
||||||
|
codec := dl.db.flatCodec
|
||||||
|
markerKey := codec.StorageMarkerKey(accountHash, storageHash)
|
||||||
marker := dl.genMarker()
|
marker := dl.genMarker()
|
||||||
if marker != nil && bytes.Compare(key, marker) > 0 {
|
if marker != nil && codec.MarkerCompare(markerKey, marker) > 0 {
|
||||||
return nil, errNotCoveredYet
|
return nil, errNotCoveredYet
|
||||||
}
|
}
|
||||||
// Try to retrieve the storage slot from the memory cache
|
// Try to retrieve the storage slot from the memory cache. The codec
|
||||||
|
// decides the cache key shape so it can avoid colliding with account
|
||||||
|
// keys (relevant once the bintrie codec lands; for merkle this remains
|
||||||
|
// the historical 64-byte combined key).
|
||||||
|
cacheKey := codec.StorageCacheKey(accountHash, storageHash)
|
||||||
if dl.states != nil {
|
if dl.states != nil {
|
||||||
if blob, found := dl.states.HasGet(nil, key); found {
|
if blob, found := dl.states.HasGet(nil, cacheKey); found {
|
||||||
cleanStateHitMeter.Mark(1)
|
cleanStateHitMeter.Mark(1)
|
||||||
cleanStateReadMeter.Mark(int64(len(blob)))
|
cleanStateReadMeter.Mark(int64(len(blob)))
|
||||||
|
|
||||||
|
|
@ -296,8 +320,8 @@ func (dl *diskLayer) storage(accountHash, storageHash common.Hash, depth int) ([
|
||||||
}
|
}
|
||||||
cleanStateMissMeter.Mark(1)
|
cleanStateMissMeter.Mark(1)
|
||||||
}
|
}
|
||||||
// Try to retrieve the account from the disk
|
// Try to retrieve the storage slot from the disk
|
||||||
blob := rawdb.ReadStorageSnapshot(dl.db.diskdb, accountHash, storageHash)
|
blob := codec.ReadStorage(dl.db.diskdb, accountHash, storageHash)
|
||||||
|
|
||||||
// Store the resolved data in the clean cache. The background buffer flusher
|
// Store the resolved data in the clean cache. The background buffer flusher
|
||||||
// may also write to the clean cache concurrently, but two writers cannot
|
// may also write to the clean cache concurrently, but two writers cannot
|
||||||
|
|
@ -305,7 +329,7 @@ func (dl *diskLayer) storage(accountHash, storageHash common.Hash, depth int) ([
|
||||||
// it will be found in the frozen buffer, eliminating the need to check the
|
// it will be found in the frozen buffer, eliminating the need to check the
|
||||||
// database.
|
// database.
|
||||||
if dl.states != nil {
|
if dl.states != nil {
|
||||||
dl.states.Set(key, blob)
|
dl.states.Set(cacheKey, blob)
|
||||||
cleanStateWriteMeter.Mark(int64(len(blob)))
|
cleanStateWriteMeter.Mark(int64(len(blob)))
|
||||||
}
|
}
|
||||||
if len(blob) == 0 {
|
if len(blob) == 0 {
|
||||||
|
|
@ -491,7 +515,7 @@ func (dl *diskLayer) commit(bottom *diffLayer, force bool) (*diskLayer, error) {
|
||||||
|
|
||||||
// Freeze the live buffer and schedule background flushing
|
// Freeze the live buffer and schedule background flushing
|
||||||
dl.frozen = combined
|
dl.frozen = combined
|
||||||
dl.frozen.flush(bottom.root, dl.db.diskdb, []ethdb.AncientWriter{dl.db.stateFreezer, dl.db.trienodeFreezer}, progress, dl.nodes, dl.states, bottom.stateID(), func() {
|
dl.frozen.flush(bottom.root, dl.db.diskdb, dl.db.flatCodec, []ethdb.AncientWriter{dl.db.stateFreezer, dl.db.trienodeFreezer}, progress, dl.nodes, dl.states, bottom.stateID(), func() {
|
||||||
// Resume the background generation if it's not completed yet.
|
// Resume the background generation if it's not completed yet.
|
||||||
// The generator is assumed to be available if the progress is
|
// The generator is assumed to be available if the progress is
|
||||||
// not nil.
|
// not nil.
|
||||||
|
|
@ -530,6 +554,14 @@ func (dl *diskLayer) revert(h *stateHistory) (*diskLayer, error) {
|
||||||
if dl.id == 0 {
|
if dl.id == 0 {
|
||||||
return nil, fmt.Errorf("%w: zero state id", errStateUnrecoverable)
|
return nil, fmt.Errorf("%w: zero state id", errStateUnrecoverable)
|
||||||
}
|
}
|
||||||
|
// Bintrie flat state does not yet support revert. State history for
|
||||||
|
// bintrie carries keccak-keyed account/storage entries (the merkle
|
||||||
|
// shape), but the bintrie disk layout is per-stem and the merkle
|
||||||
|
// origin maps cannot be replayed onto it. Reorgs would silently
|
||||||
|
// produce wrong answers — fail loudly here so misuse is obvious.
|
||||||
|
if _, isBintrie := dl.db.flatCodec.(*bintrieFlatCodec); isBintrie {
|
||||||
|
return nil, errors.New("bintrie flat state revert is not supported")
|
||||||
|
}
|
||||||
// Apply the reverse state changes upon the current state. This must
|
// Apply the reverse state changes upon the current state. This must
|
||||||
// be done before holding the lock in order to access state in "this"
|
// be done before holding the lock in order to access state in "this"
|
||||||
// layer.
|
// layer.
|
||||||
|
|
@ -599,7 +631,9 @@ func (dl *diskLayer) revert(h *stateHistory) (*diskLayer, error) {
|
||||||
writeNodes(batch, nodes, dl.nodes)
|
writeNodes(batch, nodes, dl.nodes)
|
||||||
|
|
||||||
// Provide the original values of modified accounts and storages for revert
|
// Provide the original values of modified accounts and storages for revert
|
||||||
writeStates(batch, progress, accounts, storages, dl.states)
|
if _, _, err := writeStates(batch, dl.db.flatCodec, progress, accounts, storages, dl.states); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
rawdb.WritePersistentStateID(batch, dl.id-1)
|
rawdb.WritePersistentStateID(batch, dl.id-1)
|
||||||
rawdb.WriteSnapshotRoot(batch, h.meta.parent)
|
rawdb.WriteSnapshotRoot(batch, h.meta.parent)
|
||||||
if err := batch.Write(); err != nil {
|
if err := batch.Write(); err != nil {
|
||||||
|
|
|
||||||
316
triedb/pathdb/flat_codec.go
Normal file
316
triedb/pathdb/flat_codec.go
Normal file
|
|
@ -0,0 +1,316 @@
|
||||||
|
// Copyright 2026 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 pathdb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
|
||||||
|
"github.com/VictoriaMetrics/fastcache"
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||||
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
|
"github.com/ethereum/go-ethereum/ethdb"
|
||||||
|
)
|
||||||
|
|
||||||
|
// flatStateCodec abstracts the trie-specific aspects of flat-state storage:
|
||||||
|
// key derivation from (address, slot), persistence of account/storage entries
|
||||||
|
// to disk, clean-cache key disambiguation, and iterator construction.
|
||||||
|
//
|
||||||
|
// It mirrors the existing nodeHasher pattern (a hot, small interface plugged
|
||||||
|
// into the Database struct), and complements the Hasher interface from
|
||||||
|
// state-hasher-iface-2 which abstracts trie-side hashing/commit.
|
||||||
|
//
|
||||||
|
// Two implementations are provided:
|
||||||
|
// - merkleFlatCodec: keccak-keyed flat state, the historical MPT scheme.
|
||||||
|
// - bintrieFlatCodec: per-stem flat state for the unified binary trie.
|
||||||
|
// Wired into pathdb.Database.New when isVerkle is true.
|
||||||
|
//
|
||||||
|
// All methods MUST be safe for concurrent use; the codec is shared across
|
||||||
|
// goroutines (the disk layer's read path, the buffer flush path, and the
|
||||||
|
// background generator may all call into it simultaneously).
|
||||||
|
type flatStateCodec interface {
|
||||||
|
// AccountKey derives the flat-state lookup key for an account.
|
||||||
|
//
|
||||||
|
// For Merkle: returns keccak256(addr).
|
||||||
|
// For Bintrie: returns the full 32-byte tree key (stem || offset) for
|
||||||
|
// the BasicData leaf. Since BasicDataLeafKey is 0, the last byte is
|
||||||
|
// zero, but the result is a full key — callers use stemFromKey /
|
||||||
|
// offsetFromKey to decompose it.
|
||||||
|
AccountKey(addr common.Address) common.Hash
|
||||||
|
|
||||||
|
// StorageKey derives the flat-state lookup keys for a storage slot.
|
||||||
|
//
|
||||||
|
// The first return value carries the account-side hash (e.g.
|
||||||
|
// keccak256(addr) for Merkle, or zero for bintrie which has no per-account
|
||||||
|
// grouping). The second return value carries the slot-side hash
|
||||||
|
// (keccak256(slot) for Merkle, or the full bintrie key for bintrie).
|
||||||
|
//
|
||||||
|
// Read/Write methods receive the same pair, so the codec implementation
|
||||||
|
// is the only place that has to interpret them.
|
||||||
|
StorageKey(addr common.Address, slot common.Hash) (accountKey common.Hash, storageKey common.Hash)
|
||||||
|
|
||||||
|
// ReadAccount loads an account flat-state entry from persistent storage.
|
||||||
|
// Returns nil if the entry is not present.
|
||||||
|
ReadAccount(db ethdb.KeyValueReader, key common.Hash) []byte
|
||||||
|
|
||||||
|
// ReadStorage loads a storage flat-state entry from persistent storage.
|
||||||
|
// Returns nil if the entry is not present.
|
||||||
|
ReadStorage(db ethdb.KeyValueReader, accountKey common.Hash, storageKey common.Hash) []byte
|
||||||
|
|
||||||
|
// WriteAccount persists an account flat-state entry into the supplied batch.
|
||||||
|
WriteAccount(batch ethdb.Batch, key common.Hash, blob []byte)
|
||||||
|
|
||||||
|
// DeleteAccount removes an account flat-state entry via the supplied batch.
|
||||||
|
DeleteAccount(batch ethdb.Batch, key common.Hash)
|
||||||
|
|
||||||
|
// WriteStorage persists a storage flat-state entry into the supplied batch.
|
||||||
|
WriteStorage(batch ethdb.Batch, accountKey common.Hash, storageKey common.Hash, blob []byte)
|
||||||
|
|
||||||
|
// DeleteStorage removes a storage flat-state entry via the supplied batch.
|
||||||
|
DeleteStorage(batch ethdb.Batch, accountKey common.Hash, storageKey common.Hash)
|
||||||
|
|
||||||
|
// AccountCacheKey returns the byte key used in the disk-layer clean state
|
||||||
|
// cache (fastcache) for an account entry. The cache is shared between
|
||||||
|
// account and storage lookups, so codecs must ensure their key spaces are
|
||||||
|
// disjoint to avoid collisions.
|
||||||
|
AccountCacheKey(key common.Hash) []byte
|
||||||
|
|
||||||
|
// StorageCacheKey returns the byte key used in the disk-layer clean state
|
||||||
|
// cache (fastcache) for a storage entry. See AccountCacheKey for the
|
||||||
|
// disjointness requirement.
|
||||||
|
StorageCacheKey(accountKey common.Hash, storageKey common.Hash) []byte
|
||||||
|
|
||||||
|
// AccountPrefix returns the rawdb key prefix used by account entries on
|
||||||
|
// disk. Used by the generator to set up its account-range iterator.
|
||||||
|
AccountPrefix() []byte
|
||||||
|
|
||||||
|
// StoragePrefix returns the rawdb key prefix used by storage entries on
|
||||||
|
// disk. Used by the generator to set up its storage-range iterator.
|
||||||
|
StoragePrefix() []byte
|
||||||
|
|
||||||
|
// AccountKeyLength returns the expected total length (prefix + payload)
|
||||||
|
// of an on-disk account key. The generator uses this to filter spurious
|
||||||
|
// matches when iterating with a length-bounded iterator.
|
||||||
|
AccountKeyLength() int
|
||||||
|
|
||||||
|
// StorageKeyLength returns the expected total length (prefix + payload)
|
||||||
|
// of an on-disk storage key. See AccountKeyLength.
|
||||||
|
StorageKeyLength() int
|
||||||
|
|
||||||
|
// AccountPrefixSize returns the per-entry on-disk overhead used by the
|
||||||
|
// stateSet to estimate flush sizes. This is just the prefix length for
|
||||||
|
// merkle codecs; bintrie codecs may use a different convention.
|
||||||
|
AccountPrefixSize() int
|
||||||
|
|
||||||
|
// StoragePrefixSize returns the per-entry on-disk overhead for storage
|
||||||
|
// entries.
|
||||||
|
StoragePrefixSize() int
|
||||||
|
|
||||||
|
// SplitMarker decomposes a generation progress marker into the account
|
||||||
|
// portion and the full marker. For Merkle the account part is the first
|
||||||
|
// 32 bytes; for bintrie both halves are the same single 32-byte stem.
|
||||||
|
SplitMarker(marker []byte) (accountMarker []byte, fullMarker []byte)
|
||||||
|
|
||||||
|
// MarkerCompare compares a flat-state key against a generation progress
|
||||||
|
// marker. Returns the same semantics as bytes.Compare. Used by the
|
||||||
|
// disklayer.account/storage gating logic and by writeStates.
|
||||||
|
MarkerCompare(key []byte, marker []byte) int
|
||||||
|
|
||||||
|
// StorageMarkerKey returns the byte representation used to compare a
|
||||||
|
// (accountHash, storageHash) pair against the generator progress
|
||||||
|
// marker in disklayer.storage's generation-progress gate. Merkle
|
||||||
|
// uses the 64-byte concatenation (two-tier keying); bintrie uses
|
||||||
|
// the 32-byte storageHash directly (single-tier, stem||offset key
|
||||||
|
// space matching the bintrie generator's 32-byte marker).
|
||||||
|
StorageMarkerKey(accountHash, storageHash common.Hash) []byte
|
||||||
|
|
||||||
|
// Flush drains all pending mutations from the in-memory accountData and
|
||||||
|
// storageData maps into the supplied batch and updates the clean cache
|
||||||
|
// in lockstep. The codec controls iteration order, key derivation, and
|
||||||
|
// any aggregation that may be required (e.g. the bintrie codec must
|
||||||
|
// merge per-offset writes into per-stem read-modify-writes to avoid
|
||||||
|
// quadratic disk reads).
|
||||||
|
//
|
||||||
|
// Entries strictly past genMarker (per the codec's MarkerCompare
|
||||||
|
// semantics) are skipped because they will be regenerated by the
|
||||||
|
// background snapshot generator.
|
||||||
|
//
|
||||||
|
// Returns (account-entry count, storage-entry count) for metric
|
||||||
|
// reporting; the merkle codec reports one per map entry, while the
|
||||||
|
// bintrie codec reports one per logical offset write (so the metrics
|
||||||
|
// remain comparable across schemes).
|
||||||
|
Flush(batch ethdb.Batch, genMarker []byte, accountData map[common.Hash][]byte, storageData map[common.Hash]map[common.Hash][]byte, clean *fastcache.Cache) (int, int, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// merkleFlatCodec implements flatStateCodec for the keccak-keyed MPT flat
|
||||||
|
// state scheme. All methods are thin wrappers over rawdb accessors and
|
||||||
|
// existing helpers; this codec preserves the historical behavior bit-for-bit.
|
||||||
|
type merkleFlatCodec struct{}
|
||||||
|
|
||||||
|
// Compile-time interface check.
|
||||||
|
var _ flatStateCodec = (*merkleFlatCodec)(nil)
|
||||||
|
|
||||||
|
func (c *merkleFlatCodec) AccountKey(addr common.Address) common.Hash {
|
||||||
|
return crypto.Keccak256Hash(addr.Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *merkleFlatCodec) StorageKey(addr common.Address, slot common.Hash) (common.Hash, common.Hash) {
|
||||||
|
return crypto.Keccak256Hash(addr.Bytes()), crypto.Keccak256Hash(slot.Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *merkleFlatCodec) ReadAccount(db ethdb.KeyValueReader, key common.Hash) []byte {
|
||||||
|
return rawdb.ReadAccountSnapshot(db, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *merkleFlatCodec) ReadStorage(db ethdb.KeyValueReader, accountKey, storageKey common.Hash) []byte {
|
||||||
|
return rawdb.ReadStorageSnapshot(db, accountKey, storageKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *merkleFlatCodec) WriteAccount(batch ethdb.Batch, key common.Hash, blob []byte) {
|
||||||
|
rawdb.WriteAccountSnapshot(batch, key, blob)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *merkleFlatCodec) DeleteAccount(batch ethdb.Batch, key common.Hash) {
|
||||||
|
rawdb.DeleteAccountSnapshot(batch, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *merkleFlatCodec) WriteStorage(batch ethdb.Batch, accountKey, storageKey common.Hash, blob []byte) {
|
||||||
|
rawdb.WriteStorageSnapshot(batch, accountKey, storageKey, blob)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *merkleFlatCodec) DeleteStorage(batch ethdb.Batch, accountKey, storageKey common.Hash) {
|
||||||
|
rawdb.DeleteStorageSnapshot(batch, accountKey, storageKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *merkleFlatCodec) AccountCacheKey(key common.Hash) []byte {
|
||||||
|
// The historical merkle clean cache uses the bare 32-byte account hash.
|
||||||
|
// This is a slice into the caller's hash; callers must not retain it.
|
||||||
|
return key[:]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *merkleFlatCodec) StorageCacheKey(accountKey, storageKey common.Hash) []byte {
|
||||||
|
return storageKeySlice(accountKey, storageKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *merkleFlatCodec) AccountPrefix() []byte {
|
||||||
|
return rawdb.SnapshotAccountPrefix
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *merkleFlatCodec) StoragePrefix() []byte {
|
||||||
|
return rawdb.SnapshotStoragePrefix
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *merkleFlatCodec) AccountKeyLength() int {
|
||||||
|
return len(rawdb.SnapshotAccountPrefix) + common.HashLength
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *merkleFlatCodec) StorageKeyLength() int {
|
||||||
|
return len(rawdb.SnapshotStoragePrefix) + 2*common.HashLength
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *merkleFlatCodec) AccountPrefixSize() int {
|
||||||
|
return len(rawdb.SnapshotAccountPrefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *merkleFlatCodec) StoragePrefixSize() int {
|
||||||
|
return len(rawdb.SnapshotStoragePrefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *merkleFlatCodec) SplitMarker(marker []byte) ([]byte, []byte) {
|
||||||
|
var accMarker []byte
|
||||||
|
if len(marker) > 0 {
|
||||||
|
accMarker = marker[:common.HashLength]
|
||||||
|
}
|
||||||
|
return accMarker, marker
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *merkleFlatCodec) MarkerCompare(key []byte, marker []byte) int {
|
||||||
|
return bytes.Compare(key, marker)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *merkleFlatCodec) StorageMarkerKey(accountHash, storageHash common.Hash) []byte {
|
||||||
|
return storageKeySlice(accountHash, storageHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush drains the supplied account/storage maps into the batch using the
|
||||||
|
// historical merkle per-entry layout: one rawdb write per accountData entry
|
||||||
|
// and one per storage slot. Entries past the genMarker are skipped (the
|
||||||
|
// generator will fill them in). The clean cache is kept in sync with each
|
||||||
|
// write so subsequent reads do not stale.
|
||||||
|
//
|
||||||
|
// This is the implementation that previously lived directly in writeStates.
|
||||||
|
// It has been moved into the codec so the bintrie codec can supply its own
|
||||||
|
// per-stem aggregating implementation alongside this one.
|
||||||
|
func (c *merkleFlatCodec) Flush(batch ethdb.Batch, genMarker []byte, accountData map[common.Hash][]byte, storageData map[common.Hash]map[common.Hash][]byte, clean *fastcache.Cache) (int, int, error) {
|
||||||
|
var (
|
||||||
|
accounts int
|
||||||
|
slots int
|
||||||
|
)
|
||||||
|
for addrHash, blob := range accountData {
|
||||||
|
// Skip any account not yet covered by the snapshot. The account
|
||||||
|
// at the generation marker position (addrHash == genMarker[:common.HashLength])
|
||||||
|
// should still be updated, as it would be skipped in the next
|
||||||
|
// generation cycle.
|
||||||
|
if genMarker != nil && bytes.Compare(addrHash[:], genMarker) > 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
accounts++
|
||||||
|
cacheKey := c.AccountCacheKey(addrHash)
|
||||||
|
if len(blob) == 0 {
|
||||||
|
c.DeleteAccount(batch, addrHash)
|
||||||
|
if clean != nil {
|
||||||
|
clean.Set(cacheKey, []byte{})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
c.WriteAccount(batch, addrHash, blob)
|
||||||
|
if clean != nil {
|
||||||
|
clean.Set(cacheKey, blob)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for addrHash, storages := range storageData {
|
||||||
|
// Skip any account not covered yet by the snapshot
|
||||||
|
if genMarker != nil && bytes.Compare(addrHash[:], genMarker) > 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
midAccount := genMarker != nil && bytes.Equal(addrHash[:], genMarker[:common.HashLength])
|
||||||
|
|
||||||
|
for storageHash, blob := range storages {
|
||||||
|
// Skip any storage slot not yet covered by the snapshot. The storage slot
|
||||||
|
// at the generation marker position (addrHash == genMarker[:common.HashLength]
|
||||||
|
// and storageHash == genMarker[common.HashLength:]) should still be updated,
|
||||||
|
// as it would be skipped in the next generation cycle.
|
||||||
|
if midAccount && bytes.Compare(storageHash[:], genMarker[common.HashLength:]) > 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
slots++
|
||||||
|
cacheKey := c.StorageCacheKey(addrHash, storageHash)
|
||||||
|
if len(blob) == 0 {
|
||||||
|
c.DeleteStorage(batch, addrHash, storageHash)
|
||||||
|
if clean != nil {
|
||||||
|
clean.Set(cacheKey, []byte{})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
c.WriteStorage(batch, addrHash, storageHash, blob)
|
||||||
|
if clean != nil {
|
||||||
|
clean.Set(cacheKey, blob)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return accounts, slots, nil
|
||||||
|
}
|
||||||
515
triedb/pathdb/flat_codec_bintrie.go
Normal file
515
triedb/pathdb/flat_codec_bintrie.go
Normal file
|
|
@ -0,0 +1,515 @@
|
||||||
|
// Copyright 2026 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 pathdb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/VictoriaMetrics/fastcache"
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||||
|
"github.com/ethereum/go-ethereum/ethdb"
|
||||||
|
"github.com/ethereum/go-ethereum/log"
|
||||||
|
"github.com/ethereum/go-ethereum/trie/bintrie"
|
||||||
|
)
|
||||||
|
|
||||||
|
// bintrieFlatCodec implements flatStateCodec for the binary trie using the
|
||||||
|
// stem-blob on-disk layout defined in stem_blob.go. Keys are the 32-byte
|
||||||
|
// stems of the EIP-7864 binary state tree (the first 31 bytes of the full
|
||||||
|
// bintrie key, zero-padded into a common.Hash) and values are packed stem
|
||||||
|
// blobs containing the subset of 256 offsets that have been written at
|
||||||
|
// that stem.
|
||||||
|
//
|
||||||
|
// Unlike merkleFlatCodec (which is a stateless singleton), this codec
|
||||||
|
// holds a reference to the underlying key-value store so its Write/Delete
|
||||||
|
// methods can perform a read-modify-write on the existing stem blob
|
||||||
|
// before merging in the new (offset, value) pair. ethdb.Batch is
|
||||||
|
// write-only, so the batch passed to Write* cannot be used to fetch the
|
||||||
|
// current state of a stem.
|
||||||
|
//
|
||||||
|
// Pre-aggregation requirement: within a single flush pass, the caller
|
||||||
|
// must NOT issue two Write* calls targeting the same stem. The codec
|
||||||
|
// reads the stem from the store (not from the in-flight batch), so a
|
||||||
|
// second write at the same stem would re-read the pre-flush state and
|
||||||
|
// clobber the first write. The codec's public surface area is designed
|
||||||
|
// around this assumption; the Flush method pre-aggregates per-stem
|
||||||
|
// writes so callers do not have to handle this manually.
|
||||||
|
//
|
||||||
|
// This codec is wired into pathdb.Database.New when isVerkle is true
|
||||||
|
// (see database.go). The leaf-production hook in binaryHasher emits
|
||||||
|
// per-offset writes via DrainStemWrites, which encodeBinary routes
|
||||||
|
// into the per-offset accountData map consumed by Flush.
|
||||||
|
type bintrieFlatCodec struct {
|
||||||
|
// db is the underlying key-value store used by applyWrites to read
|
||||||
|
// the current stem blob before merging in new (offset, value) pairs.
|
||||||
|
// It is always the pathdb Database's already-wrapped diskdb (the
|
||||||
|
// VerklePrefix-namespaced table) so reads and writes share the same
|
||||||
|
// on-disk key space.
|
||||||
|
db ethdb.KeyValueReader
|
||||||
|
}
|
||||||
|
|
||||||
|
// newBintrieFlatCodec constructs a bintrieFlatCodec bound to the given
|
||||||
|
// key-value reader. The reader is used for read-modify-write on stem
|
||||||
|
// blobs; writes still flow through the ethdb.Batch passed to each
|
||||||
|
// Write*/Delete* call.
|
||||||
|
func newBintrieFlatCodec(db ethdb.KeyValueReader) *bintrieFlatCodec {
|
||||||
|
return &bintrieFlatCodec{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compile-time interface assertion.
|
||||||
|
var _ flatStateCodec = (*bintrieFlatCodec)(nil)
|
||||||
|
|
||||||
|
// bintrieCacheKeyPrefix is a one-byte prefix applied to all bintrie cache
|
||||||
|
// keys to keep them disjoint from merkle account keys (which are raw
|
||||||
|
// 32-byte hashes) and merkle storage keys (which are 64-byte
|
||||||
|
// accountHash||storageHash) in the shared clean-state fastcache. Without a
|
||||||
|
// prefix, a 32-byte merkle account hash and a 32-byte bintrie stem could
|
||||||
|
// collide on the same cache slot and return wrong data on read.
|
||||||
|
const bintrieCacheKeyPrefix byte = 0x01
|
||||||
|
|
||||||
|
// stemFromKey extracts the 31-byte stem from a 32-byte flat-state key.
|
||||||
|
// Bintrie keys follow the "stem || offset" layout (EIP-7864), so the stem
|
||||||
|
// is always bytes [0..30] and the byte at index 31 is the offset within
|
||||||
|
// the stem. Callers that use AccountKey()/StorageKey() followed by
|
||||||
|
// Read/Write never need to look at the offset themselves — the codec
|
||||||
|
// handles offset extraction internally.
|
||||||
|
func stemFromKey(key common.Hash) []byte {
|
||||||
|
return key[:bintrie.StemSize]
|
||||||
|
}
|
||||||
|
|
||||||
|
// offsetFromKey returns the offset byte of a 32-byte flat-state key.
|
||||||
|
func offsetFromKey(key common.Hash) byte {
|
||||||
|
return key[bintrie.StemSize]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// Key derivation
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
|
||||||
|
// AccountKey returns the bintrie BasicData key for the given address.
|
||||||
|
// The result has the account's 31-byte stem in bytes [0..30] and offset 0
|
||||||
|
// (BasicDataLeafKey) in byte 31. The CodeHash leaf lives at the same stem
|
||||||
|
// with offset 1, so a single ReadAccount is enough to materialize both
|
||||||
|
// offsets via the returned stem blob.
|
||||||
|
func (c *bintrieFlatCodec) AccountKey(addr common.Address) common.Hash {
|
||||||
|
return common.BytesToHash(bintrie.GetBinaryTreeKeyBasicData(addr))
|
||||||
|
}
|
||||||
|
|
||||||
|
// StorageKey returns the bintrie key for a storage slot. The first return
|
||||||
|
// value (the "account key" in the merkle naming convention) is the zero
|
||||||
|
// hash because bintrie has no per-account grouping at the flat-state
|
||||||
|
// level; the second return value is the full 32-byte slot key (stem ||
|
||||||
|
// offset). Callers must pass both values back through the Read/Write
|
||||||
|
// storage methods so the codec can recover the stem and offset.
|
||||||
|
func (c *bintrieFlatCodec) StorageKey(addr common.Address, slot common.Hash) (common.Hash, common.Hash) {
|
||||||
|
full := bintrie.GetBinaryTreeKeyStorageSlot(addr, slot[:])
|
||||||
|
return common.Hash{}, common.BytesToHash(full)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// Disk reads
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
|
||||||
|
// ReadAccount returns the 32-byte value stored at the offset indicated
|
||||||
|
// by the input key (the final byte of `key` is the bintrie offset).
|
||||||
|
// Returns nil if the offset is not populated in the on-disk stem blob.
|
||||||
|
//
|
||||||
|
// The per-offset return shape matches ReadStorage and, crucially,
|
||||||
|
// matches the buffer-path return shape: the pathdb diff-layer buffer
|
||||||
|
// stores per-offset entries (keyed by the full 32-byte stem||offset
|
||||||
|
// key) holding 32-byte leaf values. When `disklayer.account()` falls
|
||||||
|
// through from the buffer to the codec's disk read, both sides must
|
||||||
|
// agree on the per-offset representation — otherwise a length check in
|
||||||
|
// the consumer (bintrieFlatReader.Account) fails on every
|
||||||
|
// post-buffer-flush read. Prior to this commit the disk path returned
|
||||||
|
// the whole stem blob while the buffer path returned a 32-byte value,
|
||||||
|
// which caused every real-world read to error once the buffer spilled
|
||||||
|
// to disk.
|
||||||
|
//
|
||||||
|
// A malformed stem blob is treated as "entry absent" (returning nil)
|
||||||
|
// to match the behavior of rawdb.ReadStorageSnapshot on the merkle
|
||||||
|
// path — the interface has no error channel, and propagating nil lets
|
||||||
|
// the multi-reader fall through to the trie reader as a gatekeeper.
|
||||||
|
func (c *bintrieFlatCodec) ReadAccount(db ethdb.KeyValueReader, key common.Hash) []byte {
|
||||||
|
blob := rawdb.ReadBinTrieStem(db, stemFromKey(key))
|
||||||
|
if len(blob) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
val, err := extractStemOffset(blob, offsetFromKey(key))
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Corrupt bintrie stem blob in ReadAccount", "key", key, "err", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadStorage returns the 32-byte value stored at the storage slot's
|
||||||
|
// offset within its stem, or nil if the offset is not populated.
|
||||||
|
// Like ReadAccount, it extracts a single offset from the on-disk stem
|
||||||
|
// blob. A malformed stem blob is treated as absent and logged.
|
||||||
|
//
|
||||||
|
// The first parameter (accountKey) is ignored: see StorageKey for the
|
||||||
|
// reasoning behind the bintrie's zero-hash convention.
|
||||||
|
func (c *bintrieFlatCodec) ReadStorage(db ethdb.KeyValueReader, _ common.Hash, storageKey common.Hash) []byte {
|
||||||
|
blob := rawdb.ReadBinTrieStem(db, stemFromKey(storageKey))
|
||||||
|
if len(blob) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
val, err := extractStemOffset(blob, offsetFromKey(storageKey))
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Corrupt bintrie stem blob in ReadStorage", "key", storageKey, "err", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// Disk writes
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
|
||||||
|
// WriteAccount writes an account entry. The blob is expected to be a
|
||||||
|
// two-slot payload containing BasicData (bytes 0..31) followed by the
|
||||||
|
// code hash (bytes 32..63) — the caller (binaryHasher) packs these
|
||||||
|
// together because they live at the same stem and benefit from a
|
||||||
|
// single read-modify-write pass.
|
||||||
|
//
|
||||||
|
// Writing nil or an empty blob is equivalent to clearing offsets 0 and 1
|
||||||
|
// at this stem (a partial account deletion); the codec merges the
|
||||||
|
// resulting bitmap into the existing stem blob and deletes the key
|
||||||
|
// entirely if no offsets remain set.
|
||||||
|
//
|
||||||
|
// An error from mergeStemBlob (e.g. malformed existing blob) is logged
|
||||||
|
// via log.Crit because flat-state corruption is unrecoverable at this
|
||||||
|
// layer — same policy as rawdb.WriteAccountSnapshot.
|
||||||
|
func (c *bintrieFlatCodec) WriteAccount(batch ethdb.Batch, key common.Hash, blob []byte) {
|
||||||
|
writes, err := splitAccountBlob(blob)
|
||||||
|
if err != nil {
|
||||||
|
log.Crit("bintrie WriteAccount: split failed", "key", key, "err", err)
|
||||||
|
}
|
||||||
|
if _, err := c.applyWrites(batch, stemFromKey(key), writes); err != nil {
|
||||||
|
log.Crit("bintrie WriteAccount: apply failed", "key", key, "err", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteAccount clears offsets 0 (BasicData) and 1 (CodeHash) at the
|
||||||
|
// account's stem. Other offsets at the same stem (e.g. header storage
|
||||||
|
// slots) are NOT touched — callers that want a full account wipe must
|
||||||
|
// walk storage separately, which is consistent with the bintrie's
|
||||||
|
// DeleteAccount semantics (see trie/bintrie/trie.go).
|
||||||
|
func (c *bintrieFlatCodec) DeleteAccount(batch ethdb.Batch, key common.Hash) {
|
||||||
|
writes := []stemOffsetValue{
|
||||||
|
{Offset: bintrie.BasicDataLeafKey, Value: nil},
|
||||||
|
{Offset: bintrie.CodeHashLeafKey, Value: nil},
|
||||||
|
}
|
||||||
|
if _, err := c.applyWrites(batch, stemFromKey(key), writes); err != nil {
|
||||||
|
log.Crit("bintrie DeleteAccount: apply failed", "key", key, "err", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteStorage writes a single storage-slot value. The blob must be 32
|
||||||
|
// bytes (the canonical storage value width); a shorter/longer blob is a
|
||||||
|
// caller bug and is logged via log.Crit.
|
||||||
|
//
|
||||||
|
// The first parameter (accountKey) is ignored — see StorageKey.
|
||||||
|
func (c *bintrieFlatCodec) WriteStorage(batch ethdb.Batch, _ common.Hash, storageKey common.Hash, blob []byte) {
|
||||||
|
if len(blob) != stemBlobValueSize {
|
||||||
|
log.Crit("bintrie WriteStorage: wrong value length", "key", storageKey, "len", len(blob), "want", stemBlobValueSize)
|
||||||
|
}
|
||||||
|
writes := []stemOffsetValue{{Offset: offsetFromKey(storageKey), Value: blob}}
|
||||||
|
if _, err := c.applyWrites(batch, stemFromKey(storageKey), writes); err != nil {
|
||||||
|
log.Crit("bintrie WriteStorage: apply failed", "key", storageKey, "err", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteStorage clears a single offset at a stem. If the stem has no
|
||||||
|
// other populated offsets afterwards, the key is removed entirely.
|
||||||
|
func (c *bintrieFlatCodec) DeleteStorage(batch ethdb.Batch, _ common.Hash, storageKey common.Hash) {
|
||||||
|
writes := []stemOffsetValue{{Offset: offsetFromKey(storageKey), Value: nil}}
|
||||||
|
if _, err := c.applyWrites(batch, stemFromKey(storageKey), writes); err != nil {
|
||||||
|
log.Crit("bintrie DeleteStorage: apply failed", "key", storageKey, "err", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyWrites performs a read-modify-write on the given stem: reads the
|
||||||
|
// existing blob via the codec's bound reader, merges in the supplied
|
||||||
|
// (offset, value) pairs, and writes the result back via the batch — or
|
||||||
|
// deletes the key if the merged result is empty. Shared by all four
|
||||||
|
// Write/Delete methods to ensure the policy (nil value clears, empty
|
||||||
|
// blob deletes) is consistent.
|
||||||
|
//
|
||||||
|
// Returns the merged blob (or nil if the stem was deleted) so callers
|
||||||
|
// such as Flush can repopulate the clean cache without an extra disk
|
||||||
|
// read. The returned slice is freshly allocated and owned by the caller.
|
||||||
|
//
|
||||||
|
// Important: the read comes from c.db, NOT from the batch. A second
|
||||||
|
// call for the same stem within a flush would re-read the pre-flush
|
||||||
|
// state; see the pre-aggregation requirement documented on
|
||||||
|
// bintrieFlatCodec.
|
||||||
|
func (c *bintrieFlatCodec) applyWrites(batch ethdb.Batch, stem []byte, writes []stemOffsetValue) ([]byte, error) {
|
||||||
|
existing := rawdb.ReadBinTrieStem(c.db, stem)
|
||||||
|
merged, err := mergeStemBlob(existing, writes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("bintrie stem %x: %w", stem, err)
|
||||||
|
}
|
||||||
|
if merged == nil {
|
||||||
|
rawdb.DeleteBinTrieStem(batch, stem)
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
rawdb.WriteBinTrieStem(batch, stem, merged)
|
||||||
|
return merged, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// splitAccountBlob validates and splits the two-slot account payload
|
||||||
|
// passed to WriteAccount. A nil or empty blob is interpreted as
|
||||||
|
// "clear both offsets".
|
||||||
|
func splitAccountBlob(blob []byte) ([]stemOffsetValue, error) {
|
||||||
|
if len(blob) == 0 {
|
||||||
|
return []stemOffsetValue{
|
||||||
|
{Offset: bintrie.BasicDataLeafKey, Value: nil},
|
||||||
|
{Offset: bintrie.CodeHashLeafKey, Value: nil},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
if len(blob) != 2*stemBlobValueSize {
|
||||||
|
return nil, fmt.Errorf("account blob len %d, want %d (BasicData || CodeHash)", len(blob), 2*stemBlobValueSize)
|
||||||
|
}
|
||||||
|
return []stemOffsetValue{
|
||||||
|
{Offset: bintrie.BasicDataLeafKey, Value: blob[:stemBlobValueSize]},
|
||||||
|
{Offset: bintrie.CodeHashLeafKey, Value: blob[stemBlobValueSize:]},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// Clean-cache keys
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
|
||||||
|
// AccountCacheKey returns a disambiguated byte key for the shared
|
||||||
|
// fastcache-backed clean state cache. The prefix byte
|
||||||
|
// bintrieCacheKeyPrefix keeps bintrie lookups disjoint from merkle
|
||||||
|
// account lookups (32-byte keys) and from merkle storage lookups
|
||||||
|
// (64-byte keys).
|
||||||
|
//
|
||||||
|
// The full 32-byte (stem || offset) key is embedded after the prefix
|
||||||
|
// so each offset at a given stem gets its own cache entry. This is
|
||||||
|
// required because ReadAccount now returns the per-offset 32-byte
|
||||||
|
// leaf value rather than the whole stem blob: caching under a
|
||||||
|
// stem-only key would collapse BasicData and CodeHash (or any two
|
||||||
|
// offsets at the same stem) into a single slot and the second hit
|
||||||
|
// would return the wrong offset's value.
|
||||||
|
//
|
||||||
|
// Resulting layout: 1 byte prefix + 32 bytes full key = 33 bytes
|
||||||
|
// total. The stem is at bytes[1..31]; the offset is at byte[32].
|
||||||
|
func (c *bintrieFlatCodec) AccountCacheKey(key common.Hash) []byte {
|
||||||
|
out := make([]byte, 1+common.HashLength)
|
||||||
|
out[0] = bintrieCacheKeyPrefix
|
||||||
|
copy(out[1:], key[:])
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// StorageCacheKey returns the cache key for a storage entry. The
|
||||||
|
// accountKey parameter is ignored (see StorageKey). The full storage
|
||||||
|
// key — which already encodes (stem || offset) via
|
||||||
|
// GetBinaryTreeKeyStorageSlot — is embedded directly so each slot at
|
||||||
|
// a stem has its own cache entry, matching the per-offset semantics
|
||||||
|
// of AccountCacheKey.
|
||||||
|
func (c *bintrieFlatCodec) StorageCacheKey(_ common.Hash, storageKey common.Hash) []byte {
|
||||||
|
out := make([]byte, 1+common.HashLength)
|
||||||
|
out[0] = bintrieCacheKeyPrefix
|
||||||
|
copy(out[1:], storageKey[:])
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// Generator iterator configuration
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
|
||||||
|
// AccountPrefix returns the rawdb key prefix used for bintrie flat-state
|
||||||
|
// entries. The generator iterator uses this prefix to walk all stem
|
||||||
|
// blobs for the initial population of the flat state from an existing
|
||||||
|
// bintrie.
|
||||||
|
func (c *bintrieFlatCodec) AccountPrefix() []byte {
|
||||||
|
return rawdb.BinTrieStemPrefix
|
||||||
|
}
|
||||||
|
|
||||||
|
// StoragePrefix returns the same prefix as AccountPrefix because bintrie
|
||||||
|
// flat-state entries are stored in a single namespace (stems contain
|
||||||
|
// both account and storage data). The bintrie generator
|
||||||
|
// (generate_bintrie.go) uses a single iterator over this prefix
|
||||||
|
// rather than the two-tier account-then-storage walk used by the
|
||||||
|
// merkle generator.
|
||||||
|
func (c *bintrieFlatCodec) StoragePrefix() []byte {
|
||||||
|
return rawdb.BinTrieStemPrefix
|
||||||
|
}
|
||||||
|
|
||||||
|
// AccountKeyLength returns the expected on-disk key length for a stem
|
||||||
|
// entry: 1 byte of prefix + 31 bytes of stem = 32 bytes total.
|
||||||
|
func (c *bintrieFlatCodec) AccountKeyLength() int {
|
||||||
|
return len(rawdb.BinTrieStemPrefix) + bintrie.StemSize
|
||||||
|
}
|
||||||
|
|
||||||
|
// StorageKeyLength returns the same length as AccountKeyLength because
|
||||||
|
// bintrie stems are a single unified namespace.
|
||||||
|
func (c *bintrieFlatCodec) StorageKeyLength() int {
|
||||||
|
return len(rawdb.BinTrieStemPrefix) + bintrie.StemSize
|
||||||
|
}
|
||||||
|
|
||||||
|
// AccountPrefixSize returns the per-entry on-disk overhead used by the
|
||||||
|
// stateSet to estimate flush sizes. For bintrie this is just the single
|
||||||
|
// byte of BinTrieStemPrefix.
|
||||||
|
func (c *bintrieFlatCodec) AccountPrefixSize() int {
|
||||||
|
return len(rawdb.BinTrieStemPrefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StoragePrefixSize returns the same as AccountPrefixSize.
|
||||||
|
func (c *bintrieFlatCodec) StoragePrefixSize() int {
|
||||||
|
return len(rawdb.BinTrieStemPrefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// Generation progress marker
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
|
||||||
|
// SplitMarker splits a generation progress marker into the account and
|
||||||
|
// full components. For bintrie the marker is a full 32-byte key
|
||||||
|
// (stem || offset), not the merkle two-tier
|
||||||
|
// account-then-storage format, so both returned slices point at the
|
||||||
|
// same data. The second half of the merkle marker (storage offset) has
|
||||||
|
// no equivalent for bintrie: the generator iterates stems directly,
|
||||||
|
// not (account, storage) pairs.
|
||||||
|
func (c *bintrieFlatCodec) SplitMarker(marker []byte) ([]byte, []byte) {
|
||||||
|
if len(marker) == 0 {
|
||||||
|
return nil, marker
|
||||||
|
}
|
||||||
|
return marker, marker
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkerCompare compares a flat-state key against a progress marker with
|
||||||
|
// bytes.Compare semantics, mirroring the merkle codec. The bintrie keys
|
||||||
|
// being compared are stem bytes (31 bytes) or full keys (32 bytes); both
|
||||||
|
// are lexicographically ordered so bytes.Compare is the correct
|
||||||
|
// ordering.
|
||||||
|
func (c *bintrieFlatCodec) MarkerCompare(key []byte, marker []byte) int {
|
||||||
|
return bytes.Compare(key, marker)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StorageMarkerKey returns the 32-byte storageHash directly. For bintrie,
|
||||||
|
// the storageHash IS the full (stem || offset) key because
|
||||||
|
// bintrieFlatCodec.StorageKey returns (zeroHash, fullKey). Comparing
|
||||||
|
// this directly against the 32-byte generator marker yields the correct
|
||||||
|
// ordering — unlike the merkle 64-byte combined key which was fail-open
|
||||||
|
// for bintrie.
|
||||||
|
func (c *bintrieFlatCodec) StorageMarkerKey(_ common.Hash, storageHash common.Hash) []byte {
|
||||||
|
return storageHash[:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush drains the in-memory accountData and storageData maps into the
|
||||||
|
// batch using the bintrie per-stem layout. The maps are expected to hold
|
||||||
|
// per-offset entries — each key is a 32-byte (stem || offset) tuple
|
||||||
|
// produced by AccountKey/StorageKey, and each value is a 32-byte leaf
|
||||||
|
// (or nil to clear that offset).
|
||||||
|
//
|
||||||
|
// Writes are aggregated per stem and a single read-modify-write is
|
||||||
|
// issued per stem, so the codec touches each stem at most once during
|
||||||
|
// a flush and the per-call pre-aggregation requirement is satisfied
|
||||||
|
// even when many writes target the same stem.
|
||||||
|
//
|
||||||
|
// storageData is walked alongside accountData; bintrie entries should
|
||||||
|
// normally arrive on accountData but we accept either layout for
|
||||||
|
// robustness.
|
||||||
|
//
|
||||||
|
// Cache update: after the per-stem RMW, the clean cache is updated
|
||||||
|
// with each written offset's new value (per-offset entries, matching
|
||||||
|
// the shape returned by ReadAccount). Offsets
|
||||||
|
// that were not touched by this flush retain their existing cache
|
||||||
|
// entries, which remain valid because the RMW did not modify them.
|
||||||
|
//
|
||||||
|
// Returns (offset count from accountData, offset count from storageData)
|
||||||
|
// for metric reporting parity with the merkle path.
|
||||||
|
func (c *bintrieFlatCodec) Flush(batch ethdb.Batch, genMarker []byte, accountData map[common.Hash][]byte, storageData map[common.Hash]map[common.Hash][]byte, clean *fastcache.Cache) (int, int, error) {
|
||||||
|
// Aggregate per-offset writes into per-stem batches. We use
|
||||||
|
// [31]byte as the map key because byte slices aren't hashable in
|
||||||
|
// Go and the stem is fixed size; the alternative (common.Hash with
|
||||||
|
// a zero pad) wastes a byte per entry without buying anything.
|
||||||
|
type aggregator struct {
|
||||||
|
// fullKeys preserves the original 32-byte lookup keys so the
|
||||||
|
// cache update loop below can store per-offset entries without
|
||||||
|
// reconstructing the key from (stem, offset) pairs.
|
||||||
|
fullKeys []common.Hash
|
||||||
|
writes []stemOffsetValue
|
||||||
|
}
|
||||||
|
aggregated := make(map[[bintrie.StemSize]byte]*aggregator)
|
||||||
|
|
||||||
|
addWrite := func(fullKey common.Hash, value []byte) {
|
||||||
|
var stem [bintrie.StemSize]byte
|
||||||
|
copy(stem[:], fullKey[:bintrie.StemSize])
|
||||||
|
offset := fullKey[bintrie.StemSize]
|
||||||
|
ag, exists := aggregated[stem]
|
||||||
|
if !exists {
|
||||||
|
ag = &aggregator{}
|
||||||
|
aggregated[stem] = ag
|
||||||
|
}
|
||||||
|
ag.fullKeys = append(ag.fullKeys, fullKey)
|
||||||
|
ag.writes = append(ag.writes, stemOffsetValue{Offset: offset, Value: value})
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
accountWrites int
|
||||||
|
storageWrites int
|
||||||
|
)
|
||||||
|
for fullKey, value := range accountData {
|
||||||
|
// genMarker filtering: skip stems that the generator hasn't
|
||||||
|
// reached yet. We compare against the FULL key (stem || offset)
|
||||||
|
// because the bintrie marker is itself a 32-byte key.
|
||||||
|
if genMarker != nil && bytes.Compare(fullKey[:], genMarker) > 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
accountWrites++
|
||||||
|
addWrite(fullKey, value)
|
||||||
|
}
|
||||||
|
for _, slots := range storageData {
|
||||||
|
for fullKey, value := range slots {
|
||||||
|
if genMarker != nil && bytes.Compare(fullKey[:], genMarker) > 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
storageWrites++
|
||||||
|
addWrite(fullKey, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Issue one RMW per stem, then update the clean cache per-offset
|
||||||
|
// using the fullKeys we captured in the aggregator. An empty value
|
||||||
|
// stored in the cache means "confirmed absent" (the reader will
|
||||||
|
// fall through to the trie reader); a 32-byte value means the
|
||||||
|
// offset is populated.
|
||||||
|
for _, ag := range aggregated {
|
||||||
|
if _, err := c.applyWrites(batch, ag.fullKeys[0][:bintrie.StemSize], ag.writes); err != nil {
|
||||||
|
return accountWrites, storageWrites, fmt.Errorf("bintrie Flush: %w", err)
|
||||||
|
}
|
||||||
|
if clean == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for i, fullKey := range ag.fullKeys {
|
||||||
|
cacheKey := c.AccountCacheKey(fullKey)
|
||||||
|
val := ag.writes[i].Value
|
||||||
|
if val == nil {
|
||||||
|
val = []byte{}
|
||||||
|
}
|
||||||
|
clean.Set(cacheKey, val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return accountWrites, storageWrites, nil
|
||||||
|
}
|
||||||
|
|
||||||
468
triedb/pathdb/flat_codec_bintrie_test.go
Normal file
468
triedb/pathdb/flat_codec_bintrie_test.go
Normal file
|
|
@ -0,0 +1,468 @@
|
||||||
|
// Copyright 2026 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 pathdb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||||
|
"github.com/ethereum/go-ethereum/ethdb"
|
||||||
|
"github.com/ethereum/go-ethereum/trie/bintrie"
|
||||||
|
)
|
||||||
|
|
||||||
|
// newTestBintrieCodec constructs a bintrieFlatCodec backed by an
|
||||||
|
// in-memory key-value store. Returns both the codec and the underlying
|
||||||
|
// store so tests can drive it directly.
|
||||||
|
func newTestBintrieCodec(t *testing.T) (*bintrieFlatCodec, ethdb.Database) {
|
||||||
|
t.Helper()
|
||||||
|
db := rawdb.NewMemoryDatabase()
|
||||||
|
codec := newBintrieFlatCodec(db)
|
||||||
|
return codec, db
|
||||||
|
}
|
||||||
|
|
||||||
|
// flushBatch commits a batch built against a memory database. Called
|
||||||
|
// after each codec write because the in-memory RMW of applyWrites reads
|
||||||
|
// from the store, not the batch.
|
||||||
|
func flushBatch(t *testing.T, batch interface{ Write() error }) {
|
||||||
|
t.Helper()
|
||||||
|
if err := batch.Write(); err != nil {
|
||||||
|
t.Fatalf("batch write: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBintrieCodecAccountRoundTrip verifies that an account written via
|
||||||
|
// WriteAccount (a two-slot BasicData||CodeHash blob) is persisted under
|
||||||
|
// the account's stem and can be read back by calling ReadAccount with
|
||||||
|
// the appropriate per-offset key (A1 remediation: ReadAccount now
|
||||||
|
// returns a per-offset 32-byte value, matching the buffer-path shape).
|
||||||
|
func TestBintrieCodecAccountRoundTrip(t *testing.T) {
|
||||||
|
codec, db := newTestBintrieCodec(t)
|
||||||
|
addr := common.HexToAddress("0x1111111111111111111111111111111111111111")
|
||||||
|
|
||||||
|
basicData := bytes.Repeat([]byte{0xAB}, stemBlobValueSize)
|
||||||
|
codeHash := bytes.Repeat([]byte{0xCD}, stemBlobValueSize)
|
||||||
|
blob := append(append([]byte{}, basicData...), codeHash...)
|
||||||
|
|
||||||
|
batch := db.NewBatch()
|
||||||
|
codec.WriteAccount(batch, codec.AccountKey(addr), blob)
|
||||||
|
flushBatch(t, batch)
|
||||||
|
|
||||||
|
// Read each offset individually. `codec.AccountKey(addr)` returns
|
||||||
|
// the BasicData key (offset 0); the CodeHash key has the same stem
|
||||||
|
// with offset 1.
|
||||||
|
basicKey := codec.AccountKey(addr)
|
||||||
|
codeKey := common.BytesToHash(bintrie.GetBinaryTreeKeyCodeHash(addr))
|
||||||
|
|
||||||
|
gotBasic := codec.ReadAccount(db, basicKey)
|
||||||
|
if !bytes.Equal(gotBasic, basicData) {
|
||||||
|
t.Fatalf("BasicData read: got %x, want %x", gotBasic, basicData)
|
||||||
|
}
|
||||||
|
gotCode := codec.ReadAccount(db, codeKey)
|
||||||
|
if !bytes.Equal(gotCode, codeHash) {
|
||||||
|
t.Fatalf("CodeHash read: got %x, want %x", gotCode, codeHash)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBintrieCodecStorageRoundTrip verifies that a storage slot written
|
||||||
|
// via WriteStorage is persisted at the correct stem+offset and can be
|
||||||
|
// read back via ReadStorage (which does offset extraction internally).
|
||||||
|
func TestBintrieCodecStorageRoundTrip(t *testing.T) {
|
||||||
|
codec, db := newTestBintrieCodec(t)
|
||||||
|
addr := common.HexToAddress("0x2222222222222222222222222222222222222222")
|
||||||
|
slot := common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000042")
|
||||||
|
value := bytes.Repeat([]byte{0x77}, stemBlobValueSize)
|
||||||
|
|
||||||
|
acctKey, storageKey := codec.StorageKey(addr, slot)
|
||||||
|
batch := db.NewBatch()
|
||||||
|
codec.WriteStorage(batch, acctKey, storageKey, value)
|
||||||
|
flushBatch(t, batch)
|
||||||
|
|
||||||
|
got := codec.ReadStorage(db, acctKey, storageKey)
|
||||||
|
if !bytes.Equal(got, value) {
|
||||||
|
t.Fatalf("ReadStorage: got %x, want %x", got, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBintrieCodecMultipleWritesSameStem verifies that two successive
|
||||||
|
// writes to DIFFERENT offsets at the same stem both persist — this is
|
||||||
|
// the common case when an account is updated (BasicData + CodeHash at
|
||||||
|
// stem X) and then a header storage slot at the same stem is written.
|
||||||
|
//
|
||||||
|
// Note: because the codec reads RMW from the store (not the batch), the
|
||||||
|
// caller must flush the batch between writes to the same stem for this
|
||||||
|
// to work correctly. This test exercises that pattern to ensure the
|
||||||
|
// per-call contract holds.
|
||||||
|
func TestBintrieCodecMultipleWritesSameStem(t *testing.T) {
|
||||||
|
codec, db := newTestBintrieCodec(t)
|
||||||
|
addr := common.HexToAddress("0x3333333333333333333333333333333333333333")
|
||||||
|
|
||||||
|
// Write the account (offsets 0 and 1 at the BasicData stem).
|
||||||
|
basicData := bytes.Repeat([]byte{0xAA}, stemBlobValueSize)
|
||||||
|
codeHash := bytes.Repeat([]byte{0xBB}, stemBlobValueSize)
|
||||||
|
blob := append(append([]byte{}, basicData...), codeHash...)
|
||||||
|
batch := db.NewBatch()
|
||||||
|
codec.WriteAccount(batch, codec.AccountKey(addr), blob)
|
||||||
|
flushBatch(t, batch)
|
||||||
|
|
||||||
|
// Now write a header storage slot. Slot 0 (per EIP-7864) lives at
|
||||||
|
// offset 64 within the SAME stem as BasicData, so this is a
|
||||||
|
// read-modify-write on the existing stem blob.
|
||||||
|
slot := common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000000")
|
||||||
|
storageValue := bytes.Repeat([]byte{0xCC}, stemBlobValueSize)
|
||||||
|
acctKey, storageKey := codec.StorageKey(addr, slot)
|
||||||
|
batch = db.NewBatch()
|
||||||
|
codec.WriteStorage(batch, acctKey, storageKey, storageValue)
|
||||||
|
flushBatch(t, batch)
|
||||||
|
|
||||||
|
// All three offsets should now be readable via per-offset reads.
|
||||||
|
basicKey := codec.AccountKey(addr)
|
||||||
|
codeKey := common.BytesToHash(bintrie.GetBinaryTreeKeyCodeHash(addr))
|
||||||
|
|
||||||
|
if gotBasic := codec.ReadAccount(db, basicKey); !bytes.Equal(gotBasic, basicData) {
|
||||||
|
t.Fatalf("BasicData lost after storage write: got %x, want %x", gotBasic, basicData)
|
||||||
|
}
|
||||||
|
if gotCode := codec.ReadAccount(db, codeKey); !bytes.Equal(gotCode, codeHash) {
|
||||||
|
t.Fatalf("CodeHash lost after storage write: got %x, want %x", gotCode, codeHash)
|
||||||
|
}
|
||||||
|
gotStorage := codec.ReadStorage(db, acctKey, storageKey)
|
||||||
|
if !bytes.Equal(gotStorage, storageValue) {
|
||||||
|
t.Fatalf("Storage: got %x, want %x", gotStorage, storageValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBintrieCodecDeleteAccount verifies that DeleteAccount clears only
|
||||||
|
// offsets 0 (BasicData) and 1 (CodeHash) at the account's stem, leaving
|
||||||
|
// any other offsets (e.g. header storage slots) at the same stem
|
||||||
|
// untouched. This mirrors BinaryTrie.DeleteAccount's intended semantics.
|
||||||
|
func TestBintrieCodecDeleteAccount(t *testing.T) {
|
||||||
|
codec, db := newTestBintrieCodec(t)
|
||||||
|
addr := common.HexToAddress("0x4444444444444444444444444444444444444444")
|
||||||
|
|
||||||
|
// Populate account (offsets 0+1) and one header storage slot (offset 64).
|
||||||
|
basicData := bytes.Repeat([]byte{0xAA}, stemBlobValueSize)
|
||||||
|
codeHash := bytes.Repeat([]byte{0xBB}, stemBlobValueSize)
|
||||||
|
batch := db.NewBatch()
|
||||||
|
codec.WriteAccount(batch, codec.AccountKey(addr), append(basicData, codeHash...))
|
||||||
|
flushBatch(t, batch)
|
||||||
|
|
||||||
|
storageValue := bytes.Repeat([]byte{0xCC}, stemBlobValueSize)
|
||||||
|
slot := common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000000")
|
||||||
|
acctKey, storageKey := codec.StorageKey(addr, slot)
|
||||||
|
batch = db.NewBatch()
|
||||||
|
codec.WriteStorage(batch, acctKey, storageKey, storageValue)
|
||||||
|
flushBatch(t, batch)
|
||||||
|
|
||||||
|
// Delete the account. Offsets 0 and 1 should be cleared; the
|
||||||
|
// header storage slot at offset 64 should survive.
|
||||||
|
batch = db.NewBatch()
|
||||||
|
codec.DeleteAccount(batch, codec.AccountKey(addr))
|
||||||
|
flushBatch(t, batch)
|
||||||
|
|
||||||
|
basicKey := codec.AccountKey(addr)
|
||||||
|
codeKey := common.BytesToHash(bintrie.GetBinaryTreeKeyCodeHash(addr))
|
||||||
|
|
||||||
|
// Verify the underlying stem blob still exists (the storage slot
|
||||||
|
// at offset 64 should have prevented a full delete).
|
||||||
|
stemBlob := rawdb.ReadBinTrieStem(db, stemFromKey(basicKey))
|
||||||
|
if len(stemBlob) == 0 {
|
||||||
|
t.Fatal("stem blob was fully deleted; header storage should still be present")
|
||||||
|
}
|
||||||
|
// BasicData and CodeHash now read back as nil (offset cleared).
|
||||||
|
if got := codec.ReadAccount(db, basicKey); got != nil {
|
||||||
|
t.Fatalf("BasicData not cleared: %x", got)
|
||||||
|
}
|
||||||
|
if got := codec.ReadAccount(db, codeKey); got != nil {
|
||||||
|
t.Fatalf("CodeHash not cleared: %x", got)
|
||||||
|
}
|
||||||
|
if got := codec.ReadStorage(db, acctKey, storageKey); !bytes.Equal(got, storageValue) {
|
||||||
|
t.Fatalf("header storage lost after DeleteAccount: got %x, want %x", got, storageValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBintrieCodecDeleteLastOffsetRemovesKey verifies that when the
|
||||||
|
// final populated offset at a stem is cleared, the on-disk key is
|
||||||
|
// removed entirely (zero-length blobs are never persisted).
|
||||||
|
func TestBintrieCodecDeleteLastOffsetRemovesKey(t *testing.T) {
|
||||||
|
codec, db := newTestBintrieCodec(t)
|
||||||
|
addr := common.HexToAddress("0x5555555555555555555555555555555555555555")
|
||||||
|
slot := common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000080")
|
||||||
|
value := bytes.Repeat([]byte{0xDD}, stemBlobValueSize)
|
||||||
|
|
||||||
|
acctKey, storageKey := codec.StorageKey(addr, slot)
|
||||||
|
|
||||||
|
// Write, verify, delete, verify absent.
|
||||||
|
batch := db.NewBatch()
|
||||||
|
codec.WriteStorage(batch, acctKey, storageKey, value)
|
||||||
|
flushBatch(t, batch)
|
||||||
|
|
||||||
|
if got := codec.ReadStorage(db, acctKey, storageKey); !bytes.Equal(got, value) {
|
||||||
|
t.Fatalf("pre-delete read: got %x, want %x", got, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
batch = db.NewBatch()
|
||||||
|
codec.DeleteStorage(batch, acctKey, storageKey)
|
||||||
|
flushBatch(t, batch)
|
||||||
|
|
||||||
|
// The raw key should be gone from the store.
|
||||||
|
raw := rawdb.ReadBinTrieStem(db, stemFromKey(storageKey))
|
||||||
|
if raw != nil {
|
||||||
|
t.Fatalf("stem blob should be deleted, got %x", raw)
|
||||||
|
}
|
||||||
|
// And ReadStorage returns nil.
|
||||||
|
if got := codec.ReadStorage(db, acctKey, storageKey); got != nil {
|
||||||
|
t.Fatalf("post-delete read: got %x, want nil", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBintrieCodecCacheKeysDisjoint verifies that the bintrie cache key
|
||||||
|
// prefix keeps it disjoint from merkle account keys AND that two
|
||||||
|
// different offsets at the same stem produce DIFFERENT cache keys
|
||||||
|
// (the A1 remediation moved from per-stem caching to per-offset
|
||||||
|
// caching — without the full-key embedding, BasicData and CodeHash
|
||||||
|
// would collide in the cache and return wrong values).
|
||||||
|
func TestBintrieCodecCacheKeysDisjoint(t *testing.T) {
|
||||||
|
codec := &bintrieFlatCodec{}
|
||||||
|
merkle := &merkleFlatCodec{}
|
||||||
|
|
||||||
|
// A 32-byte hash that, when passed to both codecs, would collide
|
||||||
|
// if the bintrie codec didn't prefix-disambiguate its cache keys.
|
||||||
|
hash := common.HexToHash("0xaabbccddeeff00112233445566778899aabbccddeeff00112233445566778899")
|
||||||
|
|
||||||
|
binKey := codec.AccountCacheKey(hash)
|
||||||
|
merkleKey := merkle.AccountCacheKey(hash)
|
||||||
|
|
||||||
|
if bytes.Equal(binKey, merkleKey) {
|
||||||
|
t.Fatalf("bintrie and merkle cache keys collided: both are %x", binKey)
|
||||||
|
}
|
||||||
|
if binKey[0] != bintrieCacheKeyPrefix {
|
||||||
|
t.Fatalf("bintrie cache key missing prefix byte: %x", binKey)
|
||||||
|
}
|
||||||
|
// Per-offset disambiguation: two keys with the same stem but
|
||||||
|
// different offsets must produce distinct cache keys.
|
||||||
|
var basicKey common.Hash
|
||||||
|
copy(basicKey[:], hash[:])
|
||||||
|
basicKey[31] = bintrie.BasicDataLeafKey
|
||||||
|
var codeKey common.Hash
|
||||||
|
copy(codeKey[:], hash[:])
|
||||||
|
codeKey[31] = bintrie.CodeHashLeafKey
|
||||||
|
|
||||||
|
basicCacheKey := codec.AccountCacheKey(basicKey)
|
||||||
|
codeCacheKey := codec.AccountCacheKey(codeKey)
|
||||||
|
if bytes.Equal(basicCacheKey, codeCacheKey) {
|
||||||
|
t.Fatalf("per-offset cache keys collided at same stem: %x", basicCacheKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBintrieCodecSplitMarker verifies the single-tier marker handling.
|
||||||
|
// For merkle the marker is a two-tier (account, account+storage) pair;
|
||||||
|
// for bintrie it's a single 32-byte stem key, so SplitMarker returns
|
||||||
|
// the same slice twice.
|
||||||
|
func TestBintrieCodecSplitMarker(t *testing.T) {
|
||||||
|
codec := &bintrieFlatCodec{}
|
||||||
|
|
||||||
|
// Nil marker.
|
||||||
|
acc, full := codec.SplitMarker(nil)
|
||||||
|
if acc != nil || full != nil {
|
||||||
|
t.Fatalf("nil marker: acc=%v full=%v, want nil/nil", acc, full)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A 32-byte marker. Both halves point to the same bytes.
|
||||||
|
marker := bytes.Repeat([]byte{0xAA}, 32)
|
||||||
|
acc, full = codec.SplitMarker(marker)
|
||||||
|
if !bytes.Equal(acc, marker) || !bytes.Equal(full, marker) {
|
||||||
|
t.Fatalf("SplitMarker: acc=%x full=%x, want both %x", acc, full, marker)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBintrieCodecFlushAggregates verifies the per-stem aggregation that
|
||||||
|
// the codec's Flush method performs. Two distinct offsets at the SAME stem
|
||||||
|
// should produce a single on-disk stem blob containing both offsets after
|
||||||
|
// one Flush call — proving the codec collapses what would have been N
|
||||||
|
// read-modify-writes into one.
|
||||||
|
//
|
||||||
|
// Three offsets are written across two stems (2 + 1) so we exercise both
|
||||||
|
// the multi-offset and single-offset paths in a single test.
|
||||||
|
func TestBintrieCodecFlushAggregates(t *testing.T) {
|
||||||
|
codec, db := newTestBintrieCodec(t)
|
||||||
|
|
||||||
|
// Build a per-offset accountData map mimicking what encodeBinary
|
||||||
|
// produces from a binaryHasher.DrainStemWrites: the keys are full
|
||||||
|
// 32-byte (stem || offset) tuples and the values are 32-byte leaves.
|
||||||
|
addr := common.HexToAddress("0xCafeBabeDeadBeef00112233445566778899aabb")
|
||||||
|
stem := bintrie.GetBinaryTreeKey(addr, make([]byte, 32))[:bintrie.StemSize]
|
||||||
|
|
||||||
|
basicData := bytes.Repeat([]byte{0xAA}, stemBlobValueSize)
|
||||||
|
codeHash := bytes.Repeat([]byte{0xBB}, stemBlobValueSize)
|
||||||
|
storageVal := bytes.Repeat([]byte{0xCC}, stemBlobValueSize)
|
||||||
|
otherStem := bytes.Repeat([]byte{0x42}, bintrie.StemSize)
|
||||||
|
otherVal := bytes.Repeat([]byte{0xDD}, stemBlobValueSize)
|
||||||
|
|
||||||
|
mkKey := func(stem []byte, offset byte) common.Hash {
|
||||||
|
var k common.Hash
|
||||||
|
copy(k[:bintrie.StemSize], stem)
|
||||||
|
k[bintrie.StemSize] = offset
|
||||||
|
return k
|
||||||
|
}
|
||||||
|
accountData := map[common.Hash][]byte{
|
||||||
|
mkKey(stem, bintrie.BasicDataLeafKey): basicData,
|
||||||
|
mkKey(stem, bintrie.CodeHashLeafKey): codeHash,
|
||||||
|
mkKey(stem, 64): storageVal, // header storage slot
|
||||||
|
mkKey(otherStem, bintrie.BasicDataLeafKey): otherVal,
|
||||||
|
}
|
||||||
|
|
||||||
|
batch := db.NewBatch()
|
||||||
|
accW, stoW, _ := codec.Flush(batch, nil, accountData, nil, nil)
|
||||||
|
flushBatch(t, batch)
|
||||||
|
|
||||||
|
if accW != 4 {
|
||||||
|
t.Errorf("account write count: got %d, want 4", accW)
|
||||||
|
}
|
||||||
|
if stoW != 0 {
|
||||||
|
t.Errorf("storage write count: got %d, want 0 (no storage map)", stoW)
|
||||||
|
}
|
||||||
|
|
||||||
|
// All three offsets at `stem` should be readable from a single on-disk
|
||||||
|
// blob; aggregation worked iff the second/third writes did not clobber
|
||||||
|
// the first.
|
||||||
|
blob := rawdb.ReadBinTrieStem(db, stem)
|
||||||
|
if len(blob) == 0 {
|
||||||
|
t.Fatal("stem blob missing after Flush")
|
||||||
|
}
|
||||||
|
for offset, want := range map[byte][]byte{
|
||||||
|
bintrie.BasicDataLeafKey: basicData,
|
||||||
|
bintrie.CodeHashLeafKey: codeHash,
|
||||||
|
64: storageVal,
|
||||||
|
} {
|
||||||
|
got, err := extractStemOffset(blob, offset)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("extract offset %d: %v", offset, err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(got, want) {
|
||||||
|
t.Errorf("offset %d: got %x, want %x", offset, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The other stem should also have its single offset.
|
||||||
|
otherBlob := rawdb.ReadBinTrieStem(db, otherStem)
|
||||||
|
if got, _ := extractStemOffset(otherBlob, bintrie.BasicDataLeafKey); !bytes.Equal(got, otherVal) {
|
||||||
|
t.Errorf("other stem BasicData: got %x, want %x", got, otherVal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBintrieCodecCrossFlushRMW verifies that writes to the SAME stem
|
||||||
|
// from DIFFERENT flush passes (simulating blocks N and N+1) correctly
|
||||||
|
// merge on disk. Flush1 writes offsets 0+1 at stemX; Flush2 writes
|
||||||
|
// offset 64 at the same stem. After both flushes, all three offsets
|
||||||
|
// must be readable — the second flush must not clobber the first.
|
||||||
|
//
|
||||||
|
// This is the regression test for cross-flush RMW correctness and is
|
||||||
|
// the bread-and-butter behavior of the per-stem codec layout. Before
|
||||||
|
// the A1 remediation, the buffer → disk shape mismatch also masked
|
||||||
|
// this (different writes would be invisible through the reader), so
|
||||||
|
// the regression test had no teeth.
|
||||||
|
func TestBintrieCodecCrossFlushRMW(t *testing.T) {
|
||||||
|
codec, db := newTestBintrieCodec(t)
|
||||||
|
|
||||||
|
stem := bytes.Repeat([]byte{0x99}, bintrie.StemSize)
|
||||||
|
mkKey := func(offset byte) common.Hash {
|
||||||
|
var k common.Hash
|
||||||
|
copy(k[:bintrie.StemSize], stem)
|
||||||
|
k[bintrie.StemSize] = offset
|
||||||
|
return k
|
||||||
|
}
|
||||||
|
basicVal := bytes.Repeat([]byte{0xAA}, stemBlobValueSize)
|
||||||
|
codeVal := bytes.Repeat([]byte{0xBB}, stemBlobValueSize)
|
||||||
|
slotVal := bytes.Repeat([]byte{0xCC}, stemBlobValueSize)
|
||||||
|
|
||||||
|
// Flush 1: write BasicData (offset 0) and CodeHash (offset 1).
|
||||||
|
batch := db.NewBatch()
|
||||||
|
codec.Flush(batch, nil, map[common.Hash][]byte{
|
||||||
|
mkKey(bintrie.BasicDataLeafKey): basicVal,
|
||||||
|
mkKey(bintrie.CodeHashLeafKey): codeVal,
|
||||||
|
}, nil, nil)
|
||||||
|
flushBatch(t, batch)
|
||||||
|
|
||||||
|
// Flush 2: write a header storage slot at offset 64 — same stem.
|
||||||
|
batch = db.NewBatch()
|
||||||
|
codec.Flush(batch, nil, map[common.Hash][]byte{
|
||||||
|
mkKey(64): slotVal,
|
||||||
|
}, nil, nil)
|
||||||
|
flushBatch(t, batch)
|
||||||
|
|
||||||
|
// After both flushes, all three offsets must be readable. Before
|
||||||
|
// the RMW, Flush 2 would overwrite the stem blob and erase
|
||||||
|
// BasicData + CodeHash.
|
||||||
|
if got := codec.ReadAccount(db, mkKey(bintrie.BasicDataLeafKey)); !bytes.Equal(got, basicVal) {
|
||||||
|
t.Errorf("BasicData lost after second flush: got %x, want %x", got, basicVal)
|
||||||
|
}
|
||||||
|
if got := codec.ReadAccount(db, mkKey(bintrie.CodeHashLeafKey)); !bytes.Equal(got, codeVal) {
|
||||||
|
t.Errorf("CodeHash lost after second flush: got %x, want %x", got, codeVal)
|
||||||
|
}
|
||||||
|
if got := codec.ReadAccount(db, mkKey(64)); !bytes.Equal(got, slotVal) {
|
||||||
|
t.Errorf("header slot at offset 64 missing: got %x, want %x", got, slotVal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBintrieCodecFlushDelete verifies that nil-valued entries in the
|
||||||
|
// accountData map clear the corresponding offset, and that clearing every
|
||||||
|
// populated offset at a stem removes the on-disk key entirely (matching
|
||||||
|
// the per-call DeleteStorage semantics tested elsewhere).
|
||||||
|
func TestBintrieCodecFlushDelete(t *testing.T) {
|
||||||
|
codec, db := newTestBintrieCodec(t)
|
||||||
|
|
||||||
|
// Seed: write two offsets at one stem.
|
||||||
|
stem := bytes.Repeat([]byte{0x77}, bintrie.StemSize)
|
||||||
|
v0 := bytes.Repeat([]byte{0x01}, stemBlobValueSize)
|
||||||
|
v1 := bytes.Repeat([]byte{0x02}, stemBlobValueSize)
|
||||||
|
|
||||||
|
mkKey := func(offset byte) common.Hash {
|
||||||
|
var k common.Hash
|
||||||
|
copy(k[:bintrie.StemSize], stem)
|
||||||
|
k[bintrie.StemSize] = offset
|
||||||
|
return k
|
||||||
|
}
|
||||||
|
batch := db.NewBatch()
|
||||||
|
codec.Flush(batch, nil, map[common.Hash][]byte{
|
||||||
|
mkKey(0): v0,
|
||||||
|
mkKey(1): v1,
|
||||||
|
}, nil, nil)
|
||||||
|
flushBatch(t, batch)
|
||||||
|
|
||||||
|
// Now flush a nil for offset 0 — only offset 1 should remain.
|
||||||
|
batch = db.NewBatch()
|
||||||
|
codec.Flush(batch, nil, map[common.Hash][]byte{mkKey(0): nil}, nil, nil)
|
||||||
|
flushBatch(t, batch)
|
||||||
|
|
||||||
|
blob := rawdb.ReadBinTrieStem(db, stem)
|
||||||
|
if got, _ := extractStemOffset(blob, 0); got != nil {
|
||||||
|
t.Errorf("offset 0 should be cleared, got %x", got)
|
||||||
|
}
|
||||||
|
if got, _ := extractStemOffset(blob, 1); !bytes.Equal(got, v1) {
|
||||||
|
t.Errorf("offset 1 should survive, got %x want %x", got, v1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the last remaining offset; the on-disk key should disappear.
|
||||||
|
batch = db.NewBatch()
|
||||||
|
codec.Flush(batch, nil, map[common.Hash][]byte{mkKey(1): nil}, nil, nil)
|
||||||
|
flushBatch(t, batch)
|
||||||
|
|
||||||
|
if raw := rawdb.ReadBinTrieStem(db, stem); raw != nil {
|
||||||
|
t.Errorf("stem should be deleted, got %x", raw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,8 +17,6 @@
|
||||||
package pathdb
|
package pathdb
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
|
|
||||||
"github.com/VictoriaMetrics/fastcache"
|
"github.com/VictoriaMetrics/fastcache"
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||||
|
|
@ -71,64 +69,16 @@ func writeNodes(batch ethdb.Batch, nodes map[common.Hash]map[string]*trienode.No
|
||||||
// This function assumes the background generator is already terminated and states
|
// This function assumes the background generator is already terminated and states
|
||||||
// before the supplied marker has been correctly generated.
|
// before the supplied marker has been correctly generated.
|
||||||
//
|
//
|
||||||
|
// The codec parameter abstracts the trie-specific persistence: merkleFlatCodec
|
||||||
|
// performs a per-entry rawdb write for each accountData/storageData entry,
|
||||||
|
// while bintrieFlatCodec aggregates per-offset writes into per-stem
|
||||||
|
// read-modify-writes. Either way, the genMarker filtering, cache update, and
|
||||||
|
// metric reporting all happen inside the codec — writeStates is just a thin
|
||||||
|
// dispatcher.
|
||||||
|
//
|
||||||
// TODO(rjl493456442) do we really need this generation marker? The state updates
|
// TODO(rjl493456442) do we really need this generation marker? The state updates
|
||||||
// after the marker can also be written and will be fixed by generator later if
|
// after the marker can also be written and will be fixed by generator later if
|
||||||
// it's outdated.
|
// it's outdated.
|
||||||
func writeStates(batch ethdb.Batch, genMarker []byte, accountData map[common.Hash][]byte, storageData map[common.Hash]map[common.Hash][]byte, clean *fastcache.Cache) (int, int) {
|
func writeStates(batch ethdb.Batch, codec flatStateCodec, genMarker []byte, accountData map[common.Hash][]byte, storageData map[common.Hash]map[common.Hash][]byte, clean *fastcache.Cache) (int, int, error) {
|
||||||
var (
|
return codec.Flush(batch, genMarker, accountData, storageData, clean)
|
||||||
accounts int
|
|
||||||
slots int
|
|
||||||
)
|
|
||||||
for addrHash, blob := range accountData {
|
|
||||||
// Skip any account not yet covered by the snapshot. The account
|
|
||||||
// at the generation marker position (addrHash == genMarker[:common.HashLength])
|
|
||||||
// should still be updated, as it would be skipped in the next
|
|
||||||
// generation cycle.
|
|
||||||
if genMarker != nil && bytes.Compare(addrHash[:], genMarker) > 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
accounts += 1
|
|
||||||
if len(blob) == 0 {
|
|
||||||
rawdb.DeleteAccountSnapshot(batch, addrHash)
|
|
||||||
if clean != nil {
|
|
||||||
clean.Set(addrHash[:], nil)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
rawdb.WriteAccountSnapshot(batch, addrHash, blob)
|
|
||||||
if clean != nil {
|
|
||||||
clean.Set(addrHash[:], blob)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for addrHash, storages := range storageData {
|
|
||||||
// Skip any account not covered yet by the snapshot
|
|
||||||
if genMarker != nil && bytes.Compare(addrHash[:], genMarker) > 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
midAccount := genMarker != nil && bytes.Equal(addrHash[:], genMarker[:common.HashLength])
|
|
||||||
|
|
||||||
for storageHash, blob := range storages {
|
|
||||||
// Skip any storage slot not yet covered by the snapshot. The storage slot
|
|
||||||
// at the generation marker position (addrHash == genMarker[:common.HashLength]
|
|
||||||
// and storageHash == genMarker[common.HashLength:]) should still be updated,
|
|
||||||
// as it would be skipped in the next generation cycle.
|
|
||||||
if midAccount && bytes.Compare(storageHash[:], genMarker[common.HashLength:]) > 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
slots += 1
|
|
||||||
key := storageKeySlice(addrHash, storageHash)
|
|
||||||
if len(blob) == 0 {
|
|
||||||
rawdb.DeleteStorageSnapshot(batch, addrHash, storageHash)
|
|
||||||
if clean != nil {
|
|
||||||
clean.Set(key, nil)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
rawdb.WriteStorageSnapshot(batch, addrHash, storageHash, blob)
|
|
||||||
if clean != nil {
|
|
||||||
clean.Set(key, blob)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return accounts, slots
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,7 @@ type generator struct {
|
||||||
running bool // Flag indicating whether the background generation is running
|
running bool // Flag indicating whether the background generation is running
|
||||||
|
|
||||||
db ethdb.KeyValueStore // Key-value store containing the snapshot data
|
db ethdb.KeyValueStore // Key-value store containing the snapshot data
|
||||||
|
codec flatStateCodec // Flat-state codec for key derivation, persistence, iterators
|
||||||
stats *generatorStats // Generation statistics used throughout the entire life cycle
|
stats *generatorStats // Generation statistics used throughout the entire life cycle
|
||||||
abort chan chan struct{} // Notification channel to abort generating the snapshot in this layer
|
abort chan chan struct{} // Notification channel to abort generating the snapshot in this layer
|
||||||
done chan struct{} // Notification channel when generation is done
|
done chan struct{} // Notification channel when generation is done
|
||||||
|
|
@ -109,7 +110,11 @@ type generator struct {
|
||||||
// progress indicates the starting position for resuming snapshot generation.
|
// progress indicates the starting position for resuming snapshot generation.
|
||||||
// It must be provided even if generation is not allowed; otherwise, uncovered
|
// It must be provided even if generation is not allowed; otherwise, uncovered
|
||||||
// states may be exposed for serving.
|
// states may be exposed for serving.
|
||||||
func newGenerator(db ethdb.KeyValueStore, noBuild bool, progress []byte, stats *generatorStats) *generator {
|
//
|
||||||
|
// codec is the flat-state codec used for marker handling, prefix selection,
|
||||||
|
// persistence, and iterator construction. It must match the codec configured
|
||||||
|
// on the owning Database.
|
||||||
|
func newGenerator(db ethdb.KeyValueStore, codec flatStateCodec, noBuild bool, progress []byte, stats *generatorStats) *generator {
|
||||||
if stats == nil {
|
if stats == nil {
|
||||||
stats = &generatorStats{start: time.Now()}
|
stats = &generatorStats{start: time.Now()}
|
||||||
}
|
}
|
||||||
|
|
@ -117,6 +122,7 @@ func newGenerator(db ethdb.KeyValueStore, noBuild bool, progress []byte, stats *
|
||||||
noBuild: noBuild,
|
noBuild: noBuild,
|
||||||
progress: progress,
|
progress: progress,
|
||||||
db: db,
|
db: db,
|
||||||
|
codec: codec,
|
||||||
stats: stats,
|
stats: stats,
|
||||||
abort: make(chan chan struct{}),
|
abort: make(chan chan struct{}),
|
||||||
done: make(chan struct{}),
|
done: make(chan struct{}),
|
||||||
|
|
@ -124,6 +130,13 @@ func newGenerator(db ethdb.KeyValueStore, noBuild bool, progress []byte, stats *
|
||||||
}
|
}
|
||||||
|
|
||||||
// run starts the state snapshot generation in the background.
|
// run starts the state snapshot generation in the background.
|
||||||
|
//
|
||||||
|
// The dispatch on codec type chooses between the merkle two-tier
|
||||||
|
// account/storage iteration (`generate`) and the bintrie single-tier
|
||||||
|
// stem iteration (`generateBintrie`). Both share the same lifecycle
|
||||||
|
// (g.running, g.abort, g.done) and the same progress journal format,
|
||||||
|
// so the only difference visible to callers of run/stop is which
|
||||||
|
// background routine is launched.
|
||||||
func (g *generator) run(root common.Hash) {
|
func (g *generator) run(root common.Hash) {
|
||||||
if g.noBuild {
|
if g.noBuild {
|
||||||
log.Warn("Snapshot generation is not permitted")
|
log.Warn("Snapshot generation is not permitted")
|
||||||
|
|
@ -134,7 +147,11 @@ func (g *generator) run(root common.Hash) {
|
||||||
log.Warn("Paused the leftover generation cycle")
|
log.Warn("Paused the leftover generation cycle")
|
||||||
}
|
}
|
||||||
g.running = true
|
g.running = true
|
||||||
go g.generate(newGeneratorContext(root, g.progress, g.db))
|
if _, isBintrie := g.codec.(*bintrieFlatCodec); isBintrie {
|
||||||
|
go g.generateBintrie(newBintrieGeneratorContext(root, g.progress, g.db))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
go g.generate(newGeneratorContext(root, g.progress, g.db, g.codec))
|
||||||
}
|
}
|
||||||
|
|
||||||
// stop terminates the background generation if it's actively running.
|
// stop terminates the background generation if it's actively running.
|
||||||
|
|
@ -168,15 +185,6 @@ func (g *generator) progressMarker() []byte {
|
||||||
return g.progress
|
return g.progress
|
||||||
}
|
}
|
||||||
|
|
||||||
// splitMarker is an internal helper which splits the generation progress marker
|
|
||||||
// into two parts.
|
|
||||||
func splitMarker(marker []byte) ([]byte, []byte) {
|
|
||||||
var accMarker []byte
|
|
||||||
if len(marker) > 0 {
|
|
||||||
accMarker = marker[:common.HashLength]
|
|
||||||
}
|
|
||||||
return accMarker, marker
|
|
||||||
}
|
|
||||||
|
|
||||||
// generateSnapshot regenerates a brand-new snapshot based on an existing state
|
// generateSnapshot regenerates a brand-new snapshot based on an existing state
|
||||||
// database and head block asynchronously. The snapshot is returned immediately
|
// database and head block asynchronously. The snapshot is returned immediately
|
||||||
|
|
@ -188,7 +196,7 @@ func generateSnapshot(triedb *Database, root common.Hash, noBuild bool) *diskLay
|
||||||
genMarker = []byte{} // Initialized but empty!
|
genMarker = []byte{} // Initialized but empty!
|
||||||
)
|
)
|
||||||
dl := newDiskLayer(root, 0, triedb, nil, nil, newBuffer(triedb.config.WriteBufferSize, nil, nil, 0), nil)
|
dl := newDiskLayer(root, 0, triedb, nil, nil, newBuffer(triedb.config.WriteBufferSize, nil, nil, 0), nil)
|
||||||
dl.setGenerator(newGenerator(triedb.diskdb, noBuild, genMarker, stats))
|
dl.setGenerator(newGenerator(triedb.diskdb, triedb.flatCodec, noBuild, genMarker, stats))
|
||||||
|
|
||||||
if !noBuild {
|
if !noBuild {
|
||||||
dl.generator.run(root)
|
dl.generator.run(root)
|
||||||
|
|
@ -198,13 +206,20 @@ func generateSnapshot(triedb *Database, root common.Hash, noBuild bool) *diskLay
|
||||||
}
|
}
|
||||||
|
|
||||||
// journalProgress persists the generator stats into the database to resume later.
|
// journalProgress persists the generator stats into the database to resume later.
|
||||||
func journalProgress(db ethdb.KeyValueWriter, marker []byte, stats *generatorStats) {
|
//
|
||||||
|
// It is a method on generator so it can stamp the journal entry with the
|
||||||
|
// active scheme (merkle vs. bintrie). loadGenerator uses that flag to
|
||||||
|
// discard journals from a different scheme rather than blindly resuming
|
||||||
|
// with an incompatible marker shape.
|
||||||
|
func (g *generator) journalProgress(db ethdb.KeyValueWriter, marker []byte, stats *generatorStats) {
|
||||||
// Write out the generator marker. Note it's a standalone disk layer generator
|
// Write out the generator marker. Note it's a standalone disk layer generator
|
||||||
// which is not mixed with journal. It's ok if the generator is persisted while
|
// which is not mixed with journal. It's ok if the generator is persisted while
|
||||||
// journal is not.
|
// journal is not.
|
||||||
|
_, isBintrie := g.codec.(*bintrieFlatCodec)
|
||||||
entry := journalGenerator{
|
entry := journalGenerator{
|
||||||
Done: marker == nil,
|
Done: marker == nil,
|
||||||
Marker: marker,
|
Marker: marker,
|
||||||
|
IsBintrie: isBintrie,
|
||||||
}
|
}
|
||||||
if stats != nil {
|
if stats != nil {
|
||||||
entry.Accounts = stats.accounts
|
entry.Accounts = stats.accounts
|
||||||
|
|
@ -595,7 +610,7 @@ func (g *generator) checkAndFlush(ctx *generatorContext, current []byte) error {
|
||||||
// Persist the progress marker regardless of whether the batch is empty or not.
|
// Persist the progress marker regardless of whether the batch is empty or not.
|
||||||
// It may happen that all the flat states in the database are correct, so the
|
// It may happen that all the flat states in the database are correct, so the
|
||||||
// generator indeed makes progress even if there is nothing to commit.
|
// generator indeed makes progress even if there is nothing to commit.
|
||||||
journalProgress(ctx.batch, current, g.stats)
|
g.journalProgress(ctx.batch, current, g.stats)
|
||||||
|
|
||||||
// Flush out the database writes atomically
|
// Flush out the database writes atomically
|
||||||
if err := ctx.batch.Write(); err != nil {
|
if err := ctx.batch.Write(); err != nil {
|
||||||
|
|
@ -633,12 +648,12 @@ func (g *generator) generateStorages(ctx *generatorContext, account common.Hash,
|
||||||
}(time.Now())
|
}(time.Now())
|
||||||
|
|
||||||
if delete {
|
if delete {
|
||||||
rawdb.DeleteStorageSnapshot(ctx.batch, account, common.BytesToHash(key))
|
g.codec.DeleteStorage(ctx.batch, account, common.BytesToHash(key))
|
||||||
wipedStorageMeter.Mark(1)
|
wipedStorageMeter.Mark(1)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if write {
|
if write {
|
||||||
rawdb.WriteStorageSnapshot(ctx.batch, account, common.BytesToHash(key), val)
|
g.codec.WriteStorage(ctx.batch, account, common.BytesToHash(key), val)
|
||||||
generatedStorageMeter.Mark(1)
|
generatedStorageMeter.Mark(1)
|
||||||
} else {
|
} else {
|
||||||
recoveredStorageMeter.Mark(1)
|
recoveredStorageMeter.Mark(1)
|
||||||
|
|
@ -682,7 +697,7 @@ func (g *generator) generateAccounts(ctx *generatorContext, accMarker []byte) er
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
if delete {
|
if delete {
|
||||||
rawdb.DeleteAccountSnapshot(ctx.batch, account)
|
g.codec.DeleteAccount(ctx.batch, account)
|
||||||
wipedAccountMeter.Mark(1)
|
wipedAccountMeter.Mark(1)
|
||||||
accountWriteCounter.Inc(time.Since(start).Nanoseconds())
|
accountWriteCounter.Inc(time.Since(start).Nanoseconds())
|
||||||
|
|
||||||
|
|
@ -708,7 +723,7 @@ func (g *generator) generateAccounts(ctx *generatorContext, accMarker []byte) er
|
||||||
} else {
|
} else {
|
||||||
data := types.SlimAccountRLP(acc)
|
data := types.SlimAccountRLP(acc)
|
||||||
dataLen = len(data)
|
dataLen = len(data)
|
||||||
rawdb.WriteAccountSnapshot(ctx.batch, account, data)
|
g.codec.WriteAccount(ctx.batch, account, data)
|
||||||
generatedAccountMeter.Mark(1)
|
generatedAccountMeter.Mark(1)
|
||||||
}
|
}
|
||||||
g.stats.storage += common.StorageSize(1 + common.HashLength + dataLen)
|
g.stats.storage += common.StorageSize(1 + common.HashLength + dataLen)
|
||||||
|
|
@ -774,7 +789,7 @@ func (g *generator) generate(ctx *generatorContext) {
|
||||||
if len(g.progress) == 0 {
|
if len(g.progress) == 0 {
|
||||||
batch := g.db.NewBatch()
|
batch := g.db.NewBatch()
|
||||||
rawdb.WriteSnapshotRoot(batch, ctx.root)
|
rawdb.WriteSnapshotRoot(batch, ctx.root)
|
||||||
journalProgress(batch, g.progress, g.stats)
|
g.journalProgress(batch, g.progress, g.stats)
|
||||||
if err := batch.Write(); err != nil {
|
if err := batch.Write(); err != nil {
|
||||||
log.Crit("Failed to write initialized state marker", "err", err)
|
log.Crit("Failed to write initialized state marker", "err", err)
|
||||||
}
|
}
|
||||||
|
|
@ -788,7 +803,7 @@ func (g *generator) generate(ctx *generatorContext) {
|
||||||
// processed twice by the generator(they are already processed in the
|
// processed twice by the generator(they are already processed in the
|
||||||
// last run) but it's fine.
|
// last run) but it's fine.
|
||||||
var (
|
var (
|
||||||
accMarker, _ = splitMarker(g.progress)
|
accMarker, _ = g.codec.SplitMarker(g.progress)
|
||||||
abort chan struct{}
|
abort chan struct{}
|
||||||
)
|
)
|
||||||
if err := g.generateAccounts(ctx, accMarker); err != nil {
|
if err := g.generateAccounts(ctx, accMarker); err != nil {
|
||||||
|
|
@ -807,7 +822,7 @@ func (g *generator) generate(ctx *generatorContext) {
|
||||||
// Snapshot fully generated, set the marker to nil.
|
// Snapshot fully generated, set the marker to nil.
|
||||||
// Note even there is nothing to commit, persist the
|
// Note even there is nothing to commit, persist the
|
||||||
// generator anyway to mark the snapshot is complete.
|
// generator anyway to mark the snapshot is complete.
|
||||||
journalProgress(ctx.batch, nil, g.stats)
|
g.journalProgress(ctx.batch, nil, g.stats)
|
||||||
if err := ctx.batch.Write(); err != nil {
|
if err := ctx.batch.Write(); err != nil {
|
||||||
log.Error("Failed to flush batch", "err", err)
|
log.Error("Failed to flush batch", "err", err)
|
||||||
abort = <-g.abort
|
abort = <-g.abort
|
||||||
|
|
|
||||||
364
triedb/pathdb/generate_bintrie.go
Normal file
364
triedb/pathdb/generate_bintrie.go
Normal file
|
|
@ -0,0 +1,364 @@
|
||||||
|
// Copyright 2026 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 pathdb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||||
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
|
"github.com/ethereum/go-ethereum/ethdb"
|
||||||
|
"github.com/ethereum/go-ethereum/log"
|
||||||
|
"github.com/ethereum/go-ethereum/trie/bintrie"
|
||||||
|
"github.com/ethereum/go-ethereum/triedb/database"
|
||||||
|
)
|
||||||
|
|
||||||
|
// bintrieDiskStore is the bintrie equivalent of diskStore (the merkle
|
||||||
|
// reader used by the snapshot generator). The two differ in how
|
||||||
|
// NodeReader validates the requested state root: the merkle store
|
||||||
|
// hashes the on-disk account-trie root with keccak256, while the
|
||||||
|
// bintrie root must be deserialized as a binary node and rehashed with
|
||||||
|
// sha256 (the bintrie's native hash function). Sharing the merkle store
|
||||||
|
// would always fail validation for a bintrie root.
|
||||||
|
//
|
||||||
|
// Once validated, both stores read trie nodes by path via
|
||||||
|
// rawdb.ReadAccountTrieNode — the path-based key space is shared
|
||||||
|
// between the two schemes (the bintrie sits in the same namespace as
|
||||||
|
// the account trie because EIP-7864 unifies storage under accounts).
|
||||||
|
type bintrieDiskStore struct {
|
||||||
|
db ethdb.KeyValueStore
|
||||||
|
}
|
||||||
|
|
||||||
|
// NodeReader validates that the bintrie root currently persisted at the
|
||||||
|
// account-trie nil path matches the requested state root. The returned
|
||||||
|
// reader is a plain path-based diskReader (the same one used by the
|
||||||
|
// merkle generator) — only the validation logic differs.
|
||||||
|
func (s *bintrieDiskStore) NodeReader(stateRoot common.Hash) (database.NodeReader, error) {
|
||||||
|
// EmptyBinaryHash and the legacy EmptyRootHash are both treated as
|
||||||
|
// "trie has no persisted root" — neither has a corresponding on-disk
|
||||||
|
// node, and the bintrie itself short-circuits these cases inside
|
||||||
|
// NewBinaryTrie. We accept them here without touching the disk.
|
||||||
|
if stateRoot == (common.Hash{}) || stateRoot == types.EmptyBinaryHash || stateRoot == types.EmptyRootHash {
|
||||||
|
return &diskReader{s.db}, nil
|
||||||
|
}
|
||||||
|
blob := rawdb.ReadAccountTrieNode(s.db, nil)
|
||||||
|
if len(blob) == 0 {
|
||||||
|
return nil, fmt.Errorf("bintrie state %x is not available (empty root node)", stateRoot)
|
||||||
|
}
|
||||||
|
// DeserializeNode rehashes via sha256 internally; the resulting node's
|
||||||
|
// Hash() is the canonical bintrie root hash for the on-disk blob.
|
||||||
|
root, err := bintrie.DeserializeNode(blob, 0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("bintrie state %x: deserialize root: %w", stateRoot, err)
|
||||||
|
}
|
||||||
|
if got := root.Hash(); got != stateRoot {
|
||||||
|
return nil, fmt.Errorf("bintrie state %x is not available (have %x)", stateRoot, got)
|
||||||
|
}
|
||||||
|
return &diskReader{s.db}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// bintrieGeneratorContext holds the state needed by a single bintrie
|
||||||
|
// snapshot generation cycle. Unlike generatorContext (which manages two
|
||||||
|
// holdable iterators over the on-disk merkle account/storage prefixes),
|
||||||
|
// the bintrie path iterates the trie itself and never re-reads the
|
||||||
|
// existing flat state. As a result the bintrie context is small: just
|
||||||
|
// a write batch, the target root, and a single 32-byte progress marker
|
||||||
|
// (the bintrie key (stem || offset) at which the previous run was
|
||||||
|
// interrupted).
|
||||||
|
//
|
||||||
|
// The context is recreated on every generator restart, mirroring the
|
||||||
|
// merkle generatorContext lifecycle.
|
||||||
|
type bintrieGeneratorContext struct {
|
||||||
|
root common.Hash // State root of the generation target
|
||||||
|
marker []byte // Resume marker — a full 32-byte (stem || offset) key
|
||||||
|
db ethdb.KeyValueStore // Key-value store containing trie nodes and stem blobs
|
||||||
|
batch ethdb.Batch // Database batch for atomic writes
|
||||||
|
logged time.Time // Timestamp of the last progress log message
|
||||||
|
}
|
||||||
|
|
||||||
|
// newBintrieGeneratorContext initializes a fresh context bound to the
|
||||||
|
// given target root, starting from the supplied resume marker. A nil or
|
||||||
|
// zero-length marker means "start from the beginning of the trie".
|
||||||
|
func newBintrieGeneratorContext(root common.Hash, marker []byte, db ethdb.KeyValueStore) *bintrieGeneratorContext {
|
||||||
|
return &bintrieGeneratorContext{
|
||||||
|
root: root,
|
||||||
|
marker: marker,
|
||||||
|
db: db,
|
||||||
|
batch: db.NewBatch(),
|
||||||
|
logged: time.Now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// close releases any resources held by the context. The bintrie path
|
||||||
|
// holds no long-lived iterators outside of generateBinTrieStems (which
|
||||||
|
// owns its iterator and releases it on return), so this is currently a
|
||||||
|
// no-op. It exists symmetrically with generatorContext.close so future
|
||||||
|
// resource additions have an obvious place to land.
|
||||||
|
func (ctx *bintrieGeneratorContext) close() {}
|
||||||
|
|
||||||
|
// generateBinTrieStems regenerates the bintrie flat-state by iterating
|
||||||
|
// the entire bintrie and emitting one stem blob per stem. The iterator
|
||||||
|
// yields leaves in stem-then-offset order, so we accumulate offsets in a
|
||||||
|
// per-stem builder and flush whenever the stem changes (and once more
|
||||||
|
// at the end of iteration).
|
||||||
|
//
|
||||||
|
// Resume support is structural: ctx.marker — a 32-byte (stem || offset)
|
||||||
|
// key — is fed straight to BinaryTrie.NodeIterator which positions on the
|
||||||
|
// first leaf with key >= marker via binaryNodeIterator.seek (added in
|
||||||
|
// Commit 1). Resuming inside a stem is safe because flushStem performs a
|
||||||
|
// read-modify-write: the builder's new offsets (from the resumed walk)
|
||||||
|
// are merged with the existing on-disk blob (from the prior pass). If
|
||||||
|
// the marker is at offset 3 of stemA, the resume processes offsets 3..N
|
||||||
|
// and the merge preserves offsets 0..2 from disk. One extra disk read
|
||||||
|
// per flushStem (the RMW) is negligible compared to the walk cost.
|
||||||
|
//
|
||||||
|
// Range proofs are deliberately not used here. The bintrie's Prove path
|
||||||
|
// is not implemented yet, and an iteration-only generation cycle is
|
||||||
|
// acceptable because regeneration is a one-time cost paid at startup.
|
||||||
|
//
|
||||||
|
// Code chunks (offsets 128..255) are written to the same stem blobs as
|
||||||
|
// account header and storage offsets — it keeps the stem encoding
|
||||||
|
// symmetric with the trie and means a future re-iteration regenerates
|
||||||
|
// the entire stem layout in one pass.
|
||||||
|
func (g *generator) generateBinTrieStems(ctx *bintrieGeneratorContext) error {
|
||||||
|
// Open the bintrie via the same disk-backed reader that the merkle
|
||||||
|
// generator uses. The diskStore reads trie nodes via
|
||||||
|
// rawdb.ReadAccountTrieNode/ReadStorageTrieNode against the
|
||||||
|
// already-namespaced verkle table (db.diskdb wraps it under
|
||||||
|
// VerklePrefix), so the same accessor works for both schemes.
|
||||||
|
tr, err := bintrie.NewBinaryTrie(ctx.root, &bintrieDiskStore{db: ctx.db})
|
||||||
|
if err != nil {
|
||||||
|
log.Info("Bintrie missing, snapshotting paused", "state", ctx.root, "err", err)
|
||||||
|
return errMissingTrie
|
||||||
|
}
|
||||||
|
it, err := tr.NodeIterator(ctx.marker)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// currentStem is a freshly-allocated copy of the most recently
|
||||||
|
// observed leaf's stem. We never alias the iterator's slice
|
||||||
|
// because it can be invalidated on Next.
|
||||||
|
currentStem []byte
|
||||||
|
builder = newStemBuilder()
|
||||||
|
)
|
||||||
|
|
||||||
|
// flushStem performs a read-modify-write on the stem being accumulated:
|
||||||
|
// it reads the existing on-disk stem blob (if any), merges in the
|
||||||
|
// builder's new offsets (new values win over existing), and writes the
|
||||||
|
// merged result back. This makes mid-stem resume safe: if a prior pass
|
||||||
|
// wrote offsets 0..2 and the current pass (after resuming at offset 3)
|
||||||
|
// only has offsets 3..4 in the builder, the merge preserves 0..2 from
|
||||||
|
// disk and adds 3..4 — no data loss.
|
||||||
|
//
|
||||||
|
// Without this RMW, a mid-stem resume would overwrite the existing disk
|
||||||
|
// blob with a partial one, silently dropping the earlier offsets. This
|
||||||
|
// was bug C1 identified in the PR review.
|
||||||
|
flushStem := func() error {
|
||||||
|
if currentStem == nil || builder.empty() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
existing := rawdb.ReadBinTrieStem(ctx.db, currentStem)
|
||||||
|
writes := builder.toOffsetValues()
|
||||||
|
merged, err := mergeStemBlob(existing, writes)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("merge stem %x failed: %w", currentStem, err)
|
||||||
|
}
|
||||||
|
if merged == nil {
|
||||||
|
rawdb.DeleteBinTrieStem(ctx.batch, currentStem)
|
||||||
|
} else {
|
||||||
|
rawdb.WriteBinTrieStem(ctx.batch, currentStem, merged)
|
||||||
|
}
|
||||||
|
builder.reset()
|
||||||
|
// Bookkeeping: count one stem per emitted blob.
|
||||||
|
g.stats.accounts++
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for it.Next(true) {
|
||||||
|
if !it.Leaf() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key := it.LeafKey()
|
||||||
|
val := it.LeafBlob()
|
||||||
|
|
||||||
|
// A well-formed bintrie leaf is always (32-byte key, 32-byte value).
|
||||||
|
// Defensive check so a malformed trie surfaces as an error rather
|
||||||
|
// than corrupting the flat state.
|
||||||
|
if len(key) != bintrie.StemSize+1 {
|
||||||
|
return fmt.Errorf("bintrie leaf key has len %d, want %d", len(key), bintrie.StemSize+1)
|
||||||
|
}
|
||||||
|
if len(val) != stemBlobValueSize {
|
||||||
|
return fmt.Errorf("bintrie leaf value has len %d, want %d", len(val), stemBlobValueSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stem boundary detection: if we've moved to a new stem, persist
|
||||||
|
// the previous one before starting a new builder.
|
||||||
|
if currentStem != nil && !bytes.Equal(key[:bintrie.StemSize], currentStem) {
|
||||||
|
if err := flushStem(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
currentStem = nil
|
||||||
|
}
|
||||||
|
if currentStem == nil {
|
||||||
|
currentStem = make([]byte, bintrie.StemSize)
|
||||||
|
copy(currentStem, key[:bintrie.StemSize])
|
||||||
|
}
|
||||||
|
// builder.set takes an owning copy internally so it's safe to
|
||||||
|
// hand it the iterator's transient value slice.
|
||||||
|
builder.set(key[bintrie.StemSize], val)
|
||||||
|
|
||||||
|
g.stats.slots++
|
||||||
|
g.stats.storage += common.StorageSize(1 + bintrie.StemSize + len(val))
|
||||||
|
|
||||||
|
// Use the FULL leaf key (stem || offset) as the progress marker
|
||||||
|
// so an interrupted run can resume mid-stem. checkAndFlushBin
|
||||||
|
// takes an owning copy because the iterator's key may be
|
||||||
|
// invalidated on the next call.
|
||||||
|
marker := make([]byte, len(key))
|
||||||
|
copy(marker, key)
|
||||||
|
if err := g.checkAndFlushBin(ctx, marker); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := it.Error(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Flush the trailing stem (the loop only flushes on transitions).
|
||||||
|
if err := flushStem(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkAndFlushBin is the bintrie analogue of checkAndFlush. It saves
|
||||||
|
// progress as a single 32-byte (stem || offset) key and writes the
|
||||||
|
// batch when it exceeds IdealBatchSize, or when an abort signal is
|
||||||
|
// received.
|
||||||
|
//
|
||||||
|
// Unlike the merkle variant, there are no snapshot iterators to reopen
|
||||||
|
// here — the bintrie path iterates the trie itself, and the trie
|
||||||
|
// iterator manages its own resource lifetime.
|
||||||
|
func (g *generator) checkAndFlushBin(ctx *bintrieGeneratorContext, current []byte) error {
|
||||||
|
var abort chan struct{}
|
||||||
|
select {
|
||||||
|
case abort = <-g.abort:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
if ctx.batch.ValueSize() > ethdb.IdealBatchSize || abort != nil {
|
||||||
|
if bytes.Compare(current, g.progress) < 0 {
|
||||||
|
log.Error("Bintrie generator went backwards",
|
||||||
|
"current", fmt.Sprintf("%x", current),
|
||||||
|
"genMarker", fmt.Sprintf("%x", g.progress))
|
||||||
|
}
|
||||||
|
// Persist progress regardless of whether the batch is empty —
|
||||||
|
// it may be that all observed stems were already on disk and
|
||||||
|
// nothing actually changed.
|
||||||
|
g.journalProgress(ctx.batch, current, g.stats)
|
||||||
|
|
||||||
|
if err := ctx.batch.Write(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ctx.batch.Reset()
|
||||||
|
|
||||||
|
g.lock.Lock()
|
||||||
|
g.progress = current
|
||||||
|
g.lock.Unlock()
|
||||||
|
|
||||||
|
if abort != nil {
|
||||||
|
g.stats.log("Aborting bintrie snapshot generation", ctx.root, g.progress)
|
||||||
|
return newAbortErr(abort)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if time.Since(ctx.logged) > 8*time.Second {
|
||||||
|
g.stats.log("Generating bintrie snapshot", ctx.root, g.progress)
|
||||||
|
ctx.logged = time.Now()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateBintrie is the bintrie analogue of the merkle `generate`
|
||||||
|
// background loop. The shapes mirror each other so the lifecycle and
|
||||||
|
// shutdown protocol look identical to callers (`run` / `stop`):
|
||||||
|
//
|
||||||
|
// 1. Persist the initial progress marker if this is a fresh run
|
||||||
|
// (so a crash after the first batch can find the genesis marker
|
||||||
|
// during recovery).
|
||||||
|
// 2. Drive generateBinTrieStems to completion (or until an abort).
|
||||||
|
// 3. On clean completion, write the "done" sentinel marker, log a
|
||||||
|
// summary, and close g.done.
|
||||||
|
// 4. On abort (internal error or external signal), close the abort
|
||||||
|
// channel and return.
|
||||||
|
func (g *generator) generateBintrie(ctx *bintrieGeneratorContext) {
|
||||||
|
g.stats.log("Resuming bintrie snapshot generation", ctx.root, g.progress)
|
||||||
|
defer ctx.close()
|
||||||
|
|
||||||
|
if len(g.progress) == 0 {
|
||||||
|
batch := ctx.db.NewBatch()
|
||||||
|
rawdb.WriteSnapshotRoot(batch, ctx.root)
|
||||||
|
g.journalProgress(batch, g.progress, g.stats)
|
||||||
|
if err := batch.Write(); err != nil {
|
||||||
|
log.Crit("Failed to write initialized bintrie state marker", "err", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var abort chan struct{}
|
||||||
|
if err := g.generateBinTrieStems(ctx); err != nil {
|
||||||
|
var aerr *abortErr
|
||||||
|
if errors.As(err, &aerr) {
|
||||||
|
abort = aerr.abort
|
||||||
|
}
|
||||||
|
// Internal error: wait for an external abort signal so the
|
||||||
|
// caller's stop() invocation can synchronize.
|
||||||
|
if abort == nil {
|
||||||
|
abort = <-g.abort
|
||||||
|
}
|
||||||
|
close(abort)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Successful completion: write the nil "done" marker so subsequent
|
||||||
|
// loads know the snapshot is complete.
|
||||||
|
g.journalProgress(ctx.batch, nil, g.stats)
|
||||||
|
if err := ctx.batch.Write(); err != nil {
|
||||||
|
log.Error("Failed to flush bintrie batch", "err", err)
|
||||||
|
abort = <-g.abort
|
||||||
|
close(abort)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.batch.Reset()
|
||||||
|
|
||||||
|
log.Info("Generated bintrie snapshot",
|
||||||
|
"stems", g.stats.accounts,
|
||||||
|
"leaves", g.stats.slots,
|
||||||
|
"storage", g.stats.storage,
|
||||||
|
"elapsed", common.PrettyDuration(time.Since(g.stats.start)))
|
||||||
|
|
||||||
|
g.lock.Lock()
|
||||||
|
g.progress = nil
|
||||||
|
g.lock.Unlock()
|
||||||
|
close(g.done)
|
||||||
|
|
||||||
|
// Block until the eventual stop() so the caller can wait for us.
|
||||||
|
abort = <-g.abort
|
||||||
|
close(abort)
|
||||||
|
}
|
||||||
372
triedb/pathdb/generate_bintrie_test.go
Normal file
372
triedb/pathdb/generate_bintrie_test.go
Normal file
|
|
@ -0,0 +1,372 @@
|
||||||
|
// Copyright 2026 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 pathdb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||||
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
|
"github.com/ethereum/go-ethereum/ethdb"
|
||||||
|
"github.com/ethereum/go-ethereum/trie/bintrie"
|
||||||
|
"github.com/holiman/uint256"
|
||||||
|
)
|
||||||
|
|
||||||
|
// buildTestBintrie constructs a small in-memory bintrie containing two
|
||||||
|
// accounts and one storage slot, persists its serialized nodes into the
|
||||||
|
// supplied key-value store under the standard pathdb account-trie key
|
||||||
|
// space (which is what the bintrie reads back via diskStore), and returns
|
||||||
|
// the resulting state root.
|
||||||
|
//
|
||||||
|
// This helper sidesteps triedb.Database to avoid an import cycle: pathdb
|
||||||
|
// is a child of triedb, so the test cannot construct a triedb.Database
|
||||||
|
// here. Instead it manually persists the nodes returned by
|
||||||
|
// bintrie.Commit, mirroring what writeNodes would do in production.
|
||||||
|
func buildTestBintrie(t *testing.T, db ethdb.Database) (common.Hash, []addrAcct) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
// Use a memory-backed NodeDatabase for the empty starting trie. The
|
||||||
|
// trie's nodeResolver returns nil for unknown hashes, which matches
|
||||||
|
// the empty-trie semantics expected by NewBinaryTrie.
|
||||||
|
tr, err := bintrie.NewBinaryTrie(types.EmptyBinaryHash, &diskStore{db: db})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("new bintrie: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
addr1 := common.HexToAddress("0x1111111111111111111111111111111111111111")
|
||||||
|
addr2 := common.HexToAddress("0x2222222222222222222222222222222222222222")
|
||||||
|
slot := common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000007")
|
||||||
|
slotValue := bytes.Repeat([]byte{0x77}, 32)
|
||||||
|
|
||||||
|
if err := tr.UpdateAccount(addr1, &types.StateAccount{
|
||||||
|
Nonce: 1,
|
||||||
|
Balance: uint256.NewInt(100),
|
||||||
|
CodeHash: types.EmptyCodeHash[:],
|
||||||
|
}, 0); err != nil {
|
||||||
|
t.Fatalf("update account 1: %v", err)
|
||||||
|
}
|
||||||
|
if err := tr.UpdateAccount(addr2, &types.StateAccount{
|
||||||
|
Nonce: 2,
|
||||||
|
Balance: uint256.NewInt(200),
|
||||||
|
CodeHash: types.EmptyCodeHash[:],
|
||||||
|
}, 0); err != nil {
|
||||||
|
t.Fatalf("update account 2: %v", err)
|
||||||
|
}
|
||||||
|
if err := tr.UpdateStorage(addr1, slot[:], slotValue); err != nil {
|
||||||
|
t.Fatalf("update storage: %v", err)
|
||||||
|
}
|
||||||
|
root, nodes := tr.Commit(false)
|
||||||
|
|
||||||
|
// Persist all collected nodes via the standard account-trie path
|
||||||
|
// scheme accessor — the bintrie sits in the same key space as the
|
||||||
|
// account trie because there are no per-account storage tries in
|
||||||
|
// EIP-7864.
|
||||||
|
batch := db.NewBatch()
|
||||||
|
for path, node := range nodes.Nodes {
|
||||||
|
if node.IsDeleted() {
|
||||||
|
rawdb.DeleteAccountTrieNode(batch, []byte(path))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rawdb.WriteAccountTrieNode(batch, []byte(path), node.Blob)
|
||||||
|
}
|
||||||
|
if err := batch.Write(); err != nil {
|
||||||
|
t.Fatalf("flush trie nodes: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return root, []addrAcct{
|
||||||
|
{addr: addr1, hasStorage: true, slot: slot, slotVal: slotValue},
|
||||||
|
{addr: addr2, hasStorage: false},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// addrAcct describes a test account so the assertions phase can re-derive
|
||||||
|
// the bintrie keys it should find on disk.
|
||||||
|
type addrAcct struct {
|
||||||
|
addr common.Address
|
||||||
|
hasStorage bool
|
||||||
|
slot common.Hash
|
||||||
|
slotVal []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// runTestBintrieGenerator wires up a generator with the bintrie codec and
|
||||||
|
// drives generateBinTrieStems to completion. It returns the codec and the
|
||||||
|
// underlying db so the assertions can read back stem blobs.
|
||||||
|
func runTestBintrieGenerator(t *testing.T, db ethdb.Database, root common.Hash, marker []byte) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
codec := newBintrieFlatCodec(db)
|
||||||
|
gen := &generator{
|
||||||
|
db: db,
|
||||||
|
codec: codec,
|
||||||
|
stats: &generatorStats{start: time.Now()},
|
||||||
|
abort: make(chan chan struct{}, 1),
|
||||||
|
done: make(chan struct{}),
|
||||||
|
}
|
||||||
|
ctx := newBintrieGeneratorContext(root, marker, db)
|
||||||
|
defer ctx.close()
|
||||||
|
|
||||||
|
if err := gen.generateBinTrieStems(ctx); err != nil {
|
||||||
|
t.Fatalf("generateBinTrieStems: %v", err)
|
||||||
|
}
|
||||||
|
if err := ctx.batch.Write(); err != nil {
|
||||||
|
t.Fatalf("final batch write: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBintrieGeneratorRebuildsStems verifies the happy-path:
|
||||||
|
// - Build a small bintrie with two accounts and one storage slot.
|
||||||
|
// - Run the generator on its root.
|
||||||
|
// - Read back the stem blobs and check every offset round-trips.
|
||||||
|
//
|
||||||
|
// This is the primary "the generator works" test.
|
||||||
|
func TestBintrieGeneratorRebuildsStems(t *testing.T) {
|
||||||
|
db := rawdb.NewMemoryDatabase()
|
||||||
|
root, accounts := buildTestBintrie(t, db)
|
||||||
|
|
||||||
|
// Sanity-check that the bintrie isn't trivially empty.
|
||||||
|
if root == (common.Hash{}) || root == types.EmptyBinaryHash {
|
||||||
|
t.Fatal("test bintrie produced an empty root")
|
||||||
|
}
|
||||||
|
|
||||||
|
runTestBintrieGenerator(t, db, root, nil)
|
||||||
|
|
||||||
|
// Each test account must have its BasicData (offset 0) and CodeHash
|
||||||
|
// (offset 1) entries on disk after generation.
|
||||||
|
for _, a := range accounts {
|
||||||
|
stem := bintrie.GetBinaryTreeKeyBasicData(a.addr)[:bintrie.StemSize]
|
||||||
|
blob := rawdb.ReadBinTrieStem(db, stem)
|
||||||
|
if len(blob) == 0 {
|
||||||
|
t.Errorf("addr %x: stem blob missing after generation", a.addr)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
basic, err := extractStemOffset(blob, bintrie.BasicDataLeafKey)
|
||||||
|
if err != nil || len(basic) != 32 {
|
||||||
|
t.Errorf("addr %x: BasicData missing/invalid (err=%v len=%d)", a.addr, err, len(basic))
|
||||||
|
}
|
||||||
|
codeHash, err := extractStemOffset(blob, bintrie.CodeHashLeafKey)
|
||||||
|
if err != nil || !bytes.Equal(codeHash, types.EmptyCodeHash[:]) {
|
||||||
|
t.Errorf("addr %x: CodeHash mismatch (err=%v got=%x)", a.addr, err, codeHash)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The storage slot must be present at its derived stem (which may
|
||||||
|
// equal the account's BasicData stem for header slots, or differ for
|
||||||
|
// out-of-header slots — slot 7 is in-header so we expect the same
|
||||||
|
// stem as BasicData).
|
||||||
|
a := accounts[0]
|
||||||
|
storageKey := bintrie.GetBinaryTreeKeyStorageSlot(a.addr, a.slot[:])
|
||||||
|
storageBlob := rawdb.ReadBinTrieStem(db, storageKey[:bintrie.StemSize])
|
||||||
|
if len(storageBlob) == 0 {
|
||||||
|
t.Fatal("storage stem blob missing")
|
||||||
|
}
|
||||||
|
got, err := extractStemOffset(storageBlob, storageKey[bintrie.StemSize])
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("extract storage offset: %v", err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(got, a.slotVal) {
|
||||||
|
t.Errorf("storage value mismatch: got %x want %x", got, a.slotVal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBintrieGeneratorResumeStemBoundary verifies that a generator
|
||||||
|
// started from a stem-boundary marker (stem || offset 0) correctly
|
||||||
|
// generates only the stems at or after the marker.
|
||||||
|
func TestBintrieGeneratorResumeStemBoundary(t *testing.T) {
|
||||||
|
db := rawdb.NewMemoryDatabase()
|
||||||
|
root, accounts := buildTestBintrie(t, db)
|
||||||
|
|
||||||
|
stem1 := bintrie.GetBinaryTreeKeyBasicData(accounts[0].addr)[:bintrie.StemSize]
|
||||||
|
stem2 := bintrie.GetBinaryTreeKeyBasicData(accounts[1].addr)[:bintrie.StemSize]
|
||||||
|
larger := stem1
|
||||||
|
smaller := stem2
|
||||||
|
if bytes.Compare(stem1, stem2) < 0 {
|
||||||
|
larger, smaller = stem2, stem1
|
||||||
|
}
|
||||||
|
|
||||||
|
marker := make([]byte, 32)
|
||||||
|
copy(marker, larger)
|
||||||
|
|
||||||
|
runTestBintrieGenerator(t, db, root, marker)
|
||||||
|
|
||||||
|
if got := rawdb.ReadBinTrieStem(db, smaller); len(got) != 0 {
|
||||||
|
t.Errorf("smaller stem should have been skipped by resume marker, got %x", got)
|
||||||
|
}
|
||||||
|
if got := rawdb.ReadBinTrieStem(db, larger); len(got) == 0 {
|
||||||
|
t.Errorf("larger stem should have been generated after resume marker")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBintrieGeneratorResumeMidStem is the regression test for review
|
||||||
|
// finding C1 (mid-stem resume drops earlier offsets). Before A3's fix,
|
||||||
|
// flushStem OVERWROTE the on-disk stem blob with only the offsets
|
||||||
|
// accumulated after the resume point. Offsets from a prior pass that
|
||||||
|
// were already on disk were silently lost.
|
||||||
|
//
|
||||||
|
// The test simulates a two-pass generation:
|
||||||
|
//
|
||||||
|
// 1. Pre-seed the disk with a stem blob containing offsets 0 and 1
|
||||||
|
// (simulating what a prior pass wrote before being interrupted).
|
||||||
|
// 2. Run the generator with marker = stem||1 (resume INSIDE the stem,
|
||||||
|
// past offset 0).
|
||||||
|
// 3. After the generator completes, verify that the on-disk blob
|
||||||
|
// contains ALL offsets (0, 1, and everything else the trie has)
|
||||||
|
// — not just the offsets from the resumed walk.
|
||||||
|
//
|
||||||
|
// Before A3: step 3 would show only the post-marker offsets.
|
||||||
|
func TestBintrieGeneratorResumeMidStem(t *testing.T) {
|
||||||
|
db := rawdb.NewMemoryDatabase()
|
||||||
|
root, accounts := buildTestBintrie(t, db)
|
||||||
|
|
||||||
|
// Pick addr1 (the one with storage). It has BasicData (offset 0),
|
||||||
|
// CodeHash (offset 1), and storage slot 7 at offset 64+7=71.
|
||||||
|
a := accounts[0]
|
||||||
|
stem := bintrie.GetBinaryTreeKeyBasicData(a.addr)[:bintrie.StemSize]
|
||||||
|
|
||||||
|
// Step 1: Pre-seed the disk with a partial stem blob containing
|
||||||
|
// only offsets 0 and 1 — as if a prior generator pass wrote them
|
||||||
|
// before being interrupted.
|
||||||
|
preSeed := newStemBuilder()
|
||||||
|
preSeed.set(bintrie.BasicDataLeafKey, bytes.Repeat([]byte{0xAA}, 32))
|
||||||
|
preSeed.set(bintrie.CodeHashLeafKey, bytes.Repeat([]byte{0xBB}, 32))
|
||||||
|
rawdb.WriteBinTrieStem(db, stem, preSeed.encode())
|
||||||
|
|
||||||
|
// Step 2: Resume from offset 1 — the generator should pick up at
|
||||||
|
// offset 1 of this stem and walk forward. The builder will
|
||||||
|
// accumulate only offset 1 + storage offset from the trie walk.
|
||||||
|
// The RMW in flushStem must merge them with the pre-seeded disk
|
||||||
|
// blob to preserve offset 0.
|
||||||
|
marker := make([]byte, 32)
|
||||||
|
copy(marker[:bintrie.StemSize], stem)
|
||||||
|
marker[bintrie.StemSize] = bintrie.CodeHashLeafKey // resume at offset 1
|
||||||
|
|
||||||
|
runTestBintrieGenerator(t, db, root, marker)
|
||||||
|
|
||||||
|
// Step 3: After the full run, verify the disk blob has ALL offsets.
|
||||||
|
blob := rawdb.ReadBinTrieStem(db, stem)
|
||||||
|
if len(blob) == 0 {
|
||||||
|
t.Fatal("stem blob missing after mid-stem resume")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Offset 0 (BasicData): must survive the mid-stem resume because
|
||||||
|
// the RMW merged the builder's new content with the existing disk
|
||||||
|
// blob. Before A3, this offset was silently dropped.
|
||||||
|
basic, err := extractStemOffset(blob, bintrie.BasicDataLeafKey)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("extract BasicData: %v", err)
|
||||||
|
}
|
||||||
|
if len(basic) != 32 {
|
||||||
|
t.Fatalf("BasicData lost after mid-stem resume (A3 regression): got len=%d, want 32", len(basic))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Offset 1 (CodeHash): the generator walked this offset (it's at
|
||||||
|
// the marker), so the trie's authoritative value should overwrite
|
||||||
|
// the pre-seeded one.
|
||||||
|
code, err := extractStemOffset(blob, bintrie.CodeHashLeafKey)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("extract CodeHash: %v", err)
|
||||||
|
}
|
||||||
|
if len(code) != 32 {
|
||||||
|
t.Fatalf("CodeHash missing after resume: got len=%d", len(code))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Storage slot must also be present (the generator walked it as
|
||||||
|
// part of the full stem traversal).
|
||||||
|
storageKey := bintrie.GetBinaryTreeKeyStorageSlot(a.addr, a.slot[:])
|
||||||
|
storageOffset := storageKey[bintrie.StemSize]
|
||||||
|
storageStem := storageKey[:bintrie.StemSize]
|
||||||
|
if bytes.Equal(storageStem, stem) {
|
||||||
|
// Storage is at the same stem (header slot) — verify it's in the blob.
|
||||||
|
storageVal, err := extractStemOffset(blob, storageOffset)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("extract storage: %v", err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(storageVal, a.slotVal) {
|
||||||
|
t.Errorf("storage value: got %x, want %x", storageVal, a.slotVal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBintrieGeneratorWithContractCode verifies that the generator
|
||||||
|
// correctly writes code-chunk offsets (128..255) into stem blobs for
|
||||||
|
// contracts with non-trivial code. This is the A16/T10 test.
|
||||||
|
func TestBintrieGeneratorWithContractCode(t *testing.T) {
|
||||||
|
db := rawdb.NewMemoryDatabase()
|
||||||
|
|
||||||
|
// Build a bintrie with one contract that has ~100 bytes of code.
|
||||||
|
// Per EIP-7864, code is chunked into 31-byte pieces starting at
|
||||||
|
// offset 128 of the account's stem.
|
||||||
|
tr, err := bintrie.NewBinaryTrie(types.EmptyBinaryHash, &bintrieDiskStore{db: db})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("new bintrie: %v", err)
|
||||||
|
}
|
||||||
|
addr := common.HexToAddress("0xContractContractContractContractContrac")
|
||||||
|
code := make([]byte, 100)
|
||||||
|
for i := range code {
|
||||||
|
code[i] = byte(i)
|
||||||
|
}
|
||||||
|
if err := tr.UpdateAccount(addr, &types.StateAccount{
|
||||||
|
Nonce: 1,
|
||||||
|
Balance: uint256.NewInt(1000),
|
||||||
|
CodeHash: types.EmptyCodeHash[:],
|
||||||
|
}, len(code)); err != nil {
|
||||||
|
t.Fatalf("UpdateAccount: %v", err)
|
||||||
|
}
|
||||||
|
codeHash := common.BytesToHash(types.EmptyCodeHash[:])
|
||||||
|
if err := tr.UpdateContractCode(addr, codeHash, code); err != nil {
|
||||||
|
t.Fatalf("UpdateContractCode: %v", err)
|
||||||
|
}
|
||||||
|
root, nodes := tr.Commit(false)
|
||||||
|
|
||||||
|
// Persist trie nodes
|
||||||
|
batch := db.NewBatch()
|
||||||
|
for path, node := range nodes.Nodes {
|
||||||
|
if !node.IsDeleted() {
|
||||||
|
rawdb.WriteAccountTrieNode(batch, []byte(path), node.Blob)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := batch.Write(); err != nil {
|
||||||
|
t.Fatalf("flush trie nodes: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the generator
|
||||||
|
runTestBintrieGenerator(t, db, root, nil)
|
||||||
|
|
||||||
|
// Verify account header offsets are present.
|
||||||
|
stem := bintrie.GetBinaryTreeKeyBasicData(addr)[:bintrie.StemSize]
|
||||||
|
blob := rawdb.ReadBinTrieStem(db, stem)
|
||||||
|
if len(blob) == 0 {
|
||||||
|
t.Fatal("stem blob missing for contract account")
|
||||||
|
}
|
||||||
|
basic, _ := extractStemOffset(blob, bintrie.BasicDataLeafKey)
|
||||||
|
if len(basic) != 32 {
|
||||||
|
t.Errorf("BasicData: got len %d, want 32", len(basic))
|
||||||
|
}
|
||||||
|
codeHashLeaf, _ := extractStemOffset(blob, bintrie.CodeHashLeafKey)
|
||||||
|
if len(codeHashLeaf) != 32 {
|
||||||
|
t.Errorf("CodeHash: got len %d, want 32", len(codeHashLeaf))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify at least one code chunk offset (128) is present.
|
||||||
|
// 100 bytes of code = ceil(100/31) = 4 chunks, at offsets 128..131.
|
||||||
|
codeChunk0, _ := extractStemOffset(blob, 128)
|
||||||
|
if len(codeChunk0) != 32 {
|
||||||
|
t.Errorf("Code chunk at offset 128: got len %d, want 32 (code chunk missing from stem blob)", len(codeChunk0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -137,7 +137,7 @@ func TestAccountIteratorBasics(t *testing.T) {
|
||||||
|
|
||||||
db := rawdb.NewMemoryDatabase()
|
db := rawdb.NewMemoryDatabase()
|
||||||
batch := db.NewBatch()
|
batch := db.NewBatch()
|
||||||
states.write(batch, nil, nil)
|
states.write(batch, &merkleFlatCodec{}, nil, nil)
|
||||||
batch.Write()
|
batch.Write()
|
||||||
it = newDiskAccountIterator(db, common.Hash{})
|
it = newDiskAccountIterator(db, common.Hash{})
|
||||||
verifyIterator(t, 100, it, verifyNothing) // Nil is allowed for single layer iterator
|
verifyIterator(t, 100, it, verifyNothing) // Nil is allowed for single layer iterator
|
||||||
|
|
@ -176,7 +176,7 @@ func TestStorageIteratorBasics(t *testing.T) {
|
||||||
|
|
||||||
db := rawdb.NewMemoryDatabase()
|
db := rawdb.NewMemoryDatabase()
|
||||||
batch := db.NewBatch()
|
batch := db.NewBatch()
|
||||||
states.write(batch, nil, nil)
|
states.write(batch, &merkleFlatCodec{}, nil, nil)
|
||||||
batch.Write()
|
batch.Write()
|
||||||
for account := range accounts {
|
for account := range accounts {
|
||||||
it := newDiskStorageIterator(db, account, common.Hash{})
|
it := newDiskStorageIterator(db, account, common.Hash{})
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,11 @@ var (
|
||||||
// - Version 1: storage.Incomplete field is removed
|
// - Version 1: storage.Incomplete field is removed
|
||||||
// - Version 2: add post-modification state values
|
// - Version 2: add post-modification state values
|
||||||
// - Version 3: a flag has been added to indicate whether the storage slot key is the raw key or a hash
|
// - Version 3: a flag has been added to indicate whether the storage slot key is the raw key or a hash
|
||||||
const journalVersion uint64 = 3
|
// - Version 4: bintrie flat-state per-stem layout. The journalGenerator
|
||||||
|
// struct gains an IsBintrie flag (rlp:"optional", defaults to
|
||||||
|
// false) so the loader can discard journals from a mismatched
|
||||||
|
// scheme and trigger a full flat-state regeneration.
|
||||||
|
const journalVersion uint64 = 4
|
||||||
|
|
||||||
// loadJournal tries to parse the layer journal from the disk.
|
// loadJournal tries to parse the layer journal from the disk.
|
||||||
func (db *Database) loadJournal(diskRoot common.Hash) (layer, error) {
|
func (db *Database) loadJournal(diskRoot common.Hash) (layer, error) {
|
||||||
|
|
@ -119,10 +123,27 @@ type journalGenerator struct {
|
||||||
Accounts uint64
|
Accounts uint64
|
||||||
Slots uint64
|
Slots uint64
|
||||||
Storage uint64
|
Storage uint64
|
||||||
|
|
||||||
|
// IsBintrie distinguishes a bintrie generator's progress marker from a
|
||||||
|
// merkle one. The two markers have incompatible semantics (single-tier
|
||||||
|
// 32-byte stem||offset vs. two-tier accountHash+storageHash) and the
|
||||||
|
// loader discards the journal whenever this flag does not match the
|
||||||
|
// database's mode, forcing a full regeneration.
|
||||||
|
//
|
||||||
|
// Marshalled with rlp:"optional" so older v3 journals (which never
|
||||||
|
// wrote this field) decode cleanly to false — the merkle default.
|
||||||
|
IsBintrie bool `rlp:"optional"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadGenerator loads the state generation progress marker from the database.
|
// loadGenerator loads the state generation progress marker from the database.
|
||||||
func loadGenerator(db ethdb.KeyValueReader, hash nodeHasher) (*journalGenerator, common.Hash, error) {
|
//
|
||||||
|
// isBintrie indicates the database's active scheme. A persisted generator
|
||||||
|
// from the *other* scheme is discarded outright (and a fresh marker is
|
||||||
|
// returned) because the marker shapes are mutually unintelligible: a
|
||||||
|
// merkle marker is two-tier accountHash+storageHash, while a bintrie
|
||||||
|
// marker is a single 32-byte stem||offset key. Resuming with the wrong
|
||||||
|
// shape would either skip large stretches of the trie or revisit them.
|
||||||
|
func loadGenerator(db ethdb.KeyValueReader, hash nodeHasher, isBintrie bool) (*journalGenerator, common.Hash, error) {
|
||||||
trieRoot, err := hash(rawdb.ReadAccountTrieNode(db, nil))
|
trieRoot, err := hash(rawdb.ReadAccountTrieNode(db, nil))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, common.Hash{}, err
|
return nil, common.Hash{}, err
|
||||||
|
|
@ -139,6 +160,15 @@ func loadGenerator(db ethdb.KeyValueReader, hash nodeHasher) (*journalGenerator,
|
||||||
log.Info("State snapshot generator is not compatible")
|
log.Info("State snapshot generator is not compatible")
|
||||||
return nil, trieRoot, nil
|
return nil, trieRoot, nil
|
||||||
}
|
}
|
||||||
|
// Scheme mismatch — drop the journal and force a full regeneration.
|
||||||
|
// IsBintrie defaults to false on legacy v3 entries (the field is
|
||||||
|
// rlp:"optional"), which is exactly the right answer for a merkle
|
||||||
|
// database opened against an old journal.
|
||||||
|
if generator.IsBintrie != isBintrie {
|
||||||
|
log.Info("State snapshot generator is for a different scheme, discarding",
|
||||||
|
"journalIsBintrie", generator.IsBintrie, "dbIsBintrie", isBintrie)
|
||||||
|
return nil, trieRoot, nil
|
||||||
|
}
|
||||||
// The state snapshot is inconsistent with the trie data and must
|
// The state snapshot is inconsistent with the trie data and must
|
||||||
// be rebuilt.
|
// be rebuilt.
|
||||||
//
|
//
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,26 @@ func (loc nodeLoc) string() string {
|
||||||
return fmt.Sprintf("loc: %s, depth: %d", loc.loc, loc.depth)
|
return fmt.Sprintf("loc: %s, depth: %d", loc.loc, loc.depth)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RawStateReader is an extension of database.StateReader that exposes raw
|
||||||
|
// byte access to flat-state entries without applying any scheme-specific
|
||||||
|
// decoding (slim-RLP for merkle, no-op for bintrie). The bintrie state
|
||||||
|
// reader in core/state uses it to fetch the BasicData and CodeHash leaves
|
||||||
|
// for an account separately and reconstruct a slim account locally.
|
||||||
|
//
|
||||||
|
// The merkle pathdb reader implements this interface trivially because
|
||||||
|
// it already has AccountRLP. Callers should type-assert before using it
|
||||||
|
// rather than relying on the database.StateReader interface unconditionally.
|
||||||
|
type RawStateReader interface {
|
||||||
|
database.StateReader
|
||||||
|
|
||||||
|
// AccountRLP returns the raw flat-state entry stored under the given
|
||||||
|
// lookup key. Semantics depend on the active codec:
|
||||||
|
// - merkle: slim-RLP-encoded account bytes
|
||||||
|
// - bintrie: 32-byte leaf value at the (stem || offset) tuple
|
||||||
|
// Returns nil if the entry is not present.
|
||||||
|
AccountRLP(hash common.Hash) ([]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
// reader implements the database.NodeReader interface, providing the functionalities to
|
// reader implements the database.NodeReader interface, providing the functionalities to
|
||||||
// retrieve trie nodes by wrapping the internal state layer.
|
// retrieve trie nodes by wrapping the internal state layer.
|
||||||
type reader struct {
|
type reader struct {
|
||||||
|
|
@ -260,7 +280,7 @@ func (r *HistoricalStateReader) AccountRLP(address common.Address) ([]byte, erro
|
||||||
// and try to define a low granularity lock if the current approach doesn't
|
// and try to define a low granularity lock if the current approach doesn't
|
||||||
// work later.
|
// work later.
|
||||||
dl := r.db.tree.bottom()
|
dl := r.db.tree.bottom()
|
||||||
hash := crypto.Keccak256Hash(address.Bytes())
|
hash := r.db.flatCodec.AccountKey(address)
|
||||||
latest, err := dl.account(hash, 0)
|
latest, err := dl.account(hash, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -310,8 +330,7 @@ func (r *HistoricalStateReader) Storage(address common.Address, key common.Hash)
|
||||||
// and try to define a low granularity lock if the current approach doesn't
|
// and try to define a low granularity lock if the current approach doesn't
|
||||||
// work later.
|
// work later.
|
||||||
dl := r.db.tree.bottom()
|
dl := r.db.tree.bottom()
|
||||||
addrHash := crypto.Keccak256Hash(address.Bytes())
|
addrHash, keyHash := r.db.flatCodec.StorageKey(address, key)
|
||||||
keyHash := crypto.Keccak256Hash(key.Bytes())
|
|
||||||
latest, err := dl.storage(addrHash, keyHash, 0)
|
latest, err := dl.storage(addrHash, keyHash, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,6 @@ import (
|
||||||
|
|
||||||
"github.com/VictoriaMetrics/fastcache"
|
"github.com/VictoriaMetrics/fastcache"
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
|
||||||
"github.com/ethereum/go-ethereum/ethdb"
|
"github.com/ethereum/go-ethereum/ethdb"
|
||||||
"github.com/ethereum/go-ethereum/log"
|
"github.com/ethereum/go-ethereum/log"
|
||||||
"github.com/ethereum/go-ethereum/metrics"
|
"github.com/ethereum/go-ethereum/metrics"
|
||||||
|
|
@ -424,8 +423,8 @@ func (s *stateSet) decode(r *rlp.Stream) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// write flushes state mutations into the provided database batch as a whole.
|
// write flushes state mutations into the provided database batch as a whole.
|
||||||
func (s *stateSet) write(batch ethdb.Batch, genMarker []byte, clean *fastcache.Cache) (int, int) {
|
func (s *stateSet) write(batch ethdb.Batch, codec flatStateCodec, genMarker []byte, clean *fastcache.Cache) (int, int, error) {
|
||||||
return writeStates(batch, genMarker, s.accountData, s.storageData, clean)
|
return writeStates(batch, codec, genMarker, s.accountData, s.storageData, clean)
|
||||||
}
|
}
|
||||||
|
|
||||||
// reset clears all cached state data, including any optional sorted lists that
|
// reset clears all cached state data, including any optional sorted lists that
|
||||||
|
|
@ -438,11 +437,13 @@ func (s *stateSet) reset() {
|
||||||
s.storageListSorted = make(map[common.Hash][]common.Hash)
|
s.storageListSorted = make(map[common.Hash][]common.Hash)
|
||||||
}
|
}
|
||||||
|
|
||||||
// dbsize returns the approximate size for db write.
|
// dbsize returns the approximate size for db write. The codec supplies
|
||||||
func (s *stateSet) dbsize() int {
|
// the per-entry on-disk overhead so this calculation tracks the actual
|
||||||
m := len(s.accountData) * len(rawdb.SnapshotAccountPrefix)
|
// schema in use (merkle vs. bintrie).
|
||||||
|
func (s *stateSet) dbsize(codec flatStateCodec) int {
|
||||||
|
m := len(s.accountData) * codec.AccountPrefixSize()
|
||||||
for _, slots := range s.storageData {
|
for _, slots := range s.storageData {
|
||||||
m += len(slots) * len(rawdb.SnapshotStoragePrefix)
|
m += len(slots) * codec.StoragePrefixSize()
|
||||||
}
|
}
|
||||||
return m + int(s.size)
|
return m + int(s.size)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
353
triedb/pathdb/stem_blob.go
Normal file
353
triedb/pathdb/stem_blob.go
Normal file
|
|
@ -0,0 +1,353 @@
|
||||||
|
// Copyright 2026 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 pathdb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math/bits"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Bintrie stem blob layout
|
||||||
|
// ------------------------
|
||||||
|
//
|
||||||
|
// The flat-state representation of a bintrie stem packs the populated
|
||||||
|
// (offset, 32-byte value) pairs at that stem into a single on-disk blob.
|
||||||
|
// A stem holds up to 256 offsets (per EIP-7864, the full "stem group"),
|
||||||
|
// but in practice only a handful are populated for any given account
|
||||||
|
// (BasicData at offset 0, CodeHash at offset 1, a few storage slots, or
|
||||||
|
// code chunks). A dense encoding would waste 8 KB per stem; this layout
|
||||||
|
// scales linearly with the number of populated offsets.
|
||||||
|
//
|
||||||
|
// Layout:
|
||||||
|
//
|
||||||
|
// [ 0 .. 31 ] 32-byte bitmap; bit i set iff offset i has a value
|
||||||
|
// [32 .. 63 ] first populated offset's 32-byte value
|
||||||
|
// [64 .. 95 ] second populated offset's 32-byte value
|
||||||
|
// ...
|
||||||
|
// [32 + 32*(N-1) .. 32 + 32*N - 1] N-th populated offset's value
|
||||||
|
//
|
||||||
|
// where N = popcount(bitmap). Values appear in increasing offset order,
|
||||||
|
// which is the iteration order of the bitmap bits from least- to
|
||||||
|
// most-significant byte (byte 0 first, then byte 1, etc.), and within
|
||||||
|
// each byte from MSB (offset b*8) to LSB (offset b*8+7).
|
||||||
|
//
|
||||||
|
// An "absent" offset is one whose bitmap bit is clear; an offset whose
|
||||||
|
// value is 32 zero bytes is "present with zero value" — that is the
|
||||||
|
// tombstone convention used by BinaryTrie.DeleteStorage, which writes
|
||||||
|
// 32 zero bytes to mark a slot as cleared without removing it from the
|
||||||
|
// underlying StemNode's Values slice.
|
||||||
|
//
|
||||||
|
// An empty stem (all bits clear) is represented by a zero-length blob,
|
||||||
|
// and callers must delete the on-disk key rather than write a zero-length
|
||||||
|
// value.
|
||||||
|
const (
|
||||||
|
stemBlobBitmapSize = 32 // bytes
|
||||||
|
stemBlobBitmapBits = stemBlobBitmapSize * 8 // 256
|
||||||
|
stemBlobValueSize = common.HashLength // 32
|
||||||
|
)
|
||||||
|
|
||||||
|
// stemOffsetMax is the highest valid offset within a bintrie stem.
|
||||||
|
const stemOffsetMax = stemBlobBitmapBits - 1 // 255
|
||||||
|
|
||||||
|
var (
|
||||||
|
errStemBlobTooShort = errors.New("stem blob shorter than bitmap")
|
||||||
|
errStemBlobMalformed = errors.New("stem blob length does not match bitmap popcount")
|
||||||
|
errStemBlobValueOutOfRange = errors.New("stem blob value slice out of range")
|
||||||
|
)
|
||||||
|
|
||||||
|
// encodeStemBlob encodes a bitmap and a dense values slice (one entry per
|
||||||
|
// set bit, in ascending offset order) into the wire format described at
|
||||||
|
// the top of this file.
|
||||||
|
//
|
||||||
|
// The caller must ensure len(values) == popcount(bitmap) and that every
|
||||||
|
// entry in values has len == 32. If every bitmap bit is clear the function
|
||||||
|
// returns nil so the caller knows to delete the on-disk key.
|
||||||
|
func encodeStemBlob(bitmap [stemBlobBitmapSize]byte, values [][]byte) ([]byte, error) {
|
||||||
|
count := bitmapPopcount(bitmap)
|
||||||
|
if count != len(values) {
|
||||||
|
return nil, fmt.Errorf("stem blob popcount=%d values=%d: %w", count, len(values), errStemBlobMalformed)
|
||||||
|
}
|
||||||
|
if count > stemBlobBitmapBits {
|
||||||
|
return nil, fmt.Errorf("stem blob value count %d exceeds max %d: %w", count, stemBlobBitmapBits, errStemBlobMalformed)
|
||||||
|
}
|
||||||
|
if count == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
out := make([]byte, stemBlobBitmapSize+count*stemBlobValueSize)
|
||||||
|
copy(out, bitmap[:])
|
||||||
|
for i, v := range values {
|
||||||
|
if len(v) != stemBlobValueSize {
|
||||||
|
return nil, fmt.Errorf("stem blob value %d has len %d: %w", i, len(v), errStemBlobMalformed)
|
||||||
|
}
|
||||||
|
copy(out[stemBlobBitmapSize+i*stemBlobValueSize:], v)
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// decodeStemBlob parses a raw stem blob into its bitmap and an ordered
|
||||||
|
// slice of populated 32-byte values. The returned values alias the input
|
||||||
|
// slice; callers must not retain or mutate them without copying first.
|
||||||
|
//
|
||||||
|
// A nil or zero-length blob decodes to a zero bitmap and no values
|
||||||
|
// (equivalent to "no offsets present").
|
||||||
|
func decodeStemBlob(blob []byte) ([stemBlobBitmapSize]byte, [][]byte, error) {
|
||||||
|
var bitmap [stemBlobBitmapSize]byte
|
||||||
|
if len(blob) == 0 {
|
||||||
|
return bitmap, nil, nil
|
||||||
|
}
|
||||||
|
if len(blob) < stemBlobBitmapSize {
|
||||||
|
return bitmap, nil, errStemBlobTooShort
|
||||||
|
}
|
||||||
|
copy(bitmap[:], blob[:stemBlobBitmapSize])
|
||||||
|
count := bitmapPopcount(bitmap)
|
||||||
|
expected := stemBlobBitmapSize + count*stemBlobValueSize
|
||||||
|
if len(blob) != expected {
|
||||||
|
return bitmap, nil, fmt.Errorf("stem blob len=%d popcount=%d expected=%d: %w", len(blob), count, expected, errStemBlobMalformed)
|
||||||
|
}
|
||||||
|
if count == 0 {
|
||||||
|
return bitmap, nil, nil
|
||||||
|
}
|
||||||
|
values := make([][]byte, count)
|
||||||
|
for i := range values {
|
||||||
|
start := stemBlobBitmapSize + i*stemBlobValueSize
|
||||||
|
values[i] = blob[start : start+stemBlobValueSize]
|
||||||
|
}
|
||||||
|
return bitmap, values, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractStemOffset returns the 32-byte value at the given offset within
|
||||||
|
// a stem blob, or nil if the offset is not present. It does not allocate;
|
||||||
|
// the returned slice aliases the input blob and must not be mutated.
|
||||||
|
//
|
||||||
|
// Returns an error only if the blob itself is malformed. An absent offset
|
||||||
|
// in a well-formed blob is (nil, nil) — not an error.
|
||||||
|
func extractStemOffset(blob []byte, offset byte) ([]byte, error) {
|
||||||
|
if len(blob) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if len(blob) < stemBlobBitmapSize {
|
||||||
|
return nil, errStemBlobTooShort
|
||||||
|
}
|
||||||
|
var bitmap [stemBlobBitmapSize]byte
|
||||||
|
copy(bitmap[:], blob[:stemBlobBitmapSize])
|
||||||
|
|
||||||
|
// Is the offset present at all?
|
||||||
|
if !bitmapGet(bitmap, offset) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
// Count how many set bits precede this offset to find the value slot.
|
||||||
|
idx := bitmapRank(bitmap, offset)
|
||||||
|
start := stemBlobBitmapSize + idx*stemBlobValueSize
|
||||||
|
end := start + stemBlobValueSize
|
||||||
|
if end > len(blob) {
|
||||||
|
return nil, errStemBlobValueOutOfRange
|
||||||
|
}
|
||||||
|
return blob[start:end], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// stemBuilder accumulates (offset, value) pairs and produces a stem blob.
|
||||||
|
// It supports loading an existing blob, setting individual offsets, and
|
||||||
|
// emitting the final encoded form.
|
||||||
|
//
|
||||||
|
// Setting a value of nil or an empty slice clears the corresponding bit
|
||||||
|
// from the bitmap (the offset becomes "absent"). Setting a non-nil
|
||||||
|
// 32-byte slice — including 32 zero bytes — marks the offset present
|
||||||
|
// with that value. This preserves the distinction between absent and
|
||||||
|
// tombstoned-with-zero used elsewhere in the bintrie code.
|
||||||
|
//
|
||||||
|
// A stemBuilder is not safe for concurrent use.
|
||||||
|
type stemBuilder struct {
|
||||||
|
bitmap [stemBlobBitmapSize]byte
|
||||||
|
// values stores the current value at each offset, or nil if absent.
|
||||||
|
// Using a fixed 256-entry array avoids allocation churn as offsets
|
||||||
|
// are set and cleared.
|
||||||
|
values [stemBlobBitmapBits][]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// newStemBuilder returns an empty stemBuilder.
|
||||||
|
func newStemBuilder() *stemBuilder {
|
||||||
|
return &stemBuilder{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadFromBlob merges the entries of the given stem blob into the builder.
|
||||||
|
// Existing entries at the same offsets are overwritten. An empty blob is
|
||||||
|
// a no-op.
|
||||||
|
func (b *stemBuilder) loadFromBlob(blob []byte) error {
|
||||||
|
if len(blob) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
bitmap, values, err := decodeStemBlob(blob)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Walk the bitmap and copy each populated offset into the builder,
|
||||||
|
// stepping the values index in sync.
|
||||||
|
var vi int
|
||||||
|
for offset := range stemBlobBitmapBits {
|
||||||
|
if !bitmapGet(bitmap, byte(offset)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// decodeStemBlob returns slices aliasing the input blob; we take
|
||||||
|
// an owning copy so the builder survives the caller mutating or
|
||||||
|
// releasing the source blob.
|
||||||
|
v := make([]byte, stemBlobValueSize)
|
||||||
|
copy(v, values[vi])
|
||||||
|
b.values[offset] = v
|
||||||
|
b.bitmap[offset/8] |= 1 << (7 - uint(offset%8))
|
||||||
|
vi++
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// set writes value at the given offset. A nil or empty-length value
|
||||||
|
// clears the offset (bitmap bit cleared). A non-nil 32-byte value sets
|
||||||
|
// the offset present with that value. Setting with any other length
|
||||||
|
// panics — callers are expected to always pass 32-byte values.
|
||||||
|
func (b *stemBuilder) set(offset byte, value []byte) {
|
||||||
|
if len(value) == 0 {
|
||||||
|
b.values[offset] = nil
|
||||||
|
b.bitmap[offset/8] &^= 1 << (7 - uint(offset%8))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(value) != stemBlobValueSize {
|
||||||
|
panic(fmt.Sprintf("stemBuilder: value at offset %d has len %d, want %d", offset, len(value), stemBlobValueSize))
|
||||||
|
}
|
||||||
|
// Own the bytes so later caller mutations don't aliasing-surprise us.
|
||||||
|
owned := make([]byte, stemBlobValueSize)
|
||||||
|
copy(owned, value)
|
||||||
|
b.values[offset] = owned
|
||||||
|
b.bitmap[offset/8] |= 1 << (7 - uint(offset%8))
|
||||||
|
}
|
||||||
|
|
||||||
|
// empty reports whether no offsets are currently populated in the builder.
|
||||||
|
func (b *stemBuilder) empty() bool {
|
||||||
|
return bitmapPopcount(b.bitmap) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// encode produces the stem blob encoding for the builder's current state.
|
||||||
|
// Returns nil for an empty builder so the caller can decide to delete the
|
||||||
|
// on-disk key rather than write a zero-length value.
|
||||||
|
func (b *stemBuilder) encode() []byte {
|
||||||
|
count := bitmapPopcount(b.bitmap)
|
||||||
|
if count == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]byte, stemBlobBitmapSize+count*stemBlobValueSize)
|
||||||
|
copy(out, b.bitmap[:])
|
||||||
|
|
||||||
|
// Walk the bitmap in ascending order, copying each populated value.
|
||||||
|
pos := stemBlobBitmapSize
|
||||||
|
for offset := range stemBlobBitmapBits {
|
||||||
|
if b.values[offset] == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
copy(out[pos:], b.values[offset])
|
||||||
|
pos += stemBlobValueSize
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// reset clears all entries in the builder.
|
||||||
|
func (b *stemBuilder) reset() {
|
||||||
|
b.bitmap = [stemBlobBitmapSize]byte{}
|
||||||
|
b.values = [stemBlobBitmapBits][]byte{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// toOffsetValues converts the builder's populated entries into a slice
|
||||||
|
// of (offset, value) pairs suitable for passing to mergeStemBlob. Only
|
||||||
|
// offsets with non-nil values are emitted; cleared (nil-value) offsets
|
||||||
|
// are skipped since their absence in the merge input leaves the
|
||||||
|
// existing blob's value intact — which is the correct behavior for the
|
||||||
|
// generator's RMW pattern where the builder holds only the new writes.
|
||||||
|
func (b *stemBuilder) toOffsetValues() []stemOffsetValue {
|
||||||
|
count := bitmapPopcount(b.bitmap)
|
||||||
|
if count == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]stemOffsetValue, 0, count)
|
||||||
|
for offset := range stemBlobBitmapBits {
|
||||||
|
if b.values[offset] != nil {
|
||||||
|
out = append(out, stemOffsetValue{
|
||||||
|
Offset: byte(offset),
|
||||||
|
Value: b.values[offset],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// stemOffsetValue is a single (offset, value) pair passed to mergeStemBlob.
|
||||||
|
// A nil Value clears the offset.
|
||||||
|
type stemOffsetValue struct {
|
||||||
|
Offset byte
|
||||||
|
Value []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// mergeStemBlob performs a read-modify-write on a stem blob: it decodes
|
||||||
|
// the existing blob (if any), applies the given writes in order, and
|
||||||
|
// returns a freshly encoded blob. Returns (nil, nil) when the result is
|
||||||
|
// empty — the caller should delete the on-disk key in that case.
|
||||||
|
func mergeStemBlob(existing []byte, writes []stemOffsetValue) ([]byte, error) {
|
||||||
|
b := newStemBuilder()
|
||||||
|
if err := b.loadFromBlob(existing); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, w := range writes {
|
||||||
|
b.set(w.Offset, w.Value)
|
||||||
|
}
|
||||||
|
return b.encode(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// bitmapPopcount returns the number of set bits in the 32-byte bitmap.
|
||||||
|
func bitmapPopcount(bitmap [stemBlobBitmapSize]byte) int {
|
||||||
|
var n int
|
||||||
|
for _, b := range bitmap {
|
||||||
|
n += bits.OnesCount8(b)
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
// bitmapGet returns whether bit `offset` is set in the bitmap. The
|
||||||
|
// convention mirrors the bintrie: bit index `offset` lives in byte
|
||||||
|
// `offset/8`, with the MSB of that byte corresponding to the lowest
|
||||||
|
// in-byte offset (`offset%8 == 0`).
|
||||||
|
func bitmapGet(bitmap [stemBlobBitmapSize]byte, offset byte) bool {
|
||||||
|
return bitmap[offset/8]&(1<<(7-uint(offset%8))) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// bitmapRank returns the number of set bits that come strictly before
|
||||||
|
// `offset` (in ascending offset order). The offset itself does not count.
|
||||||
|
func bitmapRank(bitmap [stemBlobBitmapSize]byte, offset byte) int {
|
||||||
|
// Full whole bytes before the target.
|
||||||
|
byteIdx := int(offset) / 8
|
||||||
|
var rank int
|
||||||
|
for i := range byteIdx {
|
||||||
|
rank += bits.OnesCount8(bitmap[i])
|
||||||
|
}
|
||||||
|
// Bits within the target byte that are above the target's bit.
|
||||||
|
bitIdx := offset % 8
|
||||||
|
if bitIdx > 0 {
|
||||||
|
// The MSB is offset%8==0. We want bits 0..bitIdx-1 in that layout,
|
||||||
|
// which are the top bitIdx bits of the byte.
|
||||||
|
mask := byte(0xFF << (8 - bitIdx))
|
||||||
|
rank += bits.OnesCount8(bitmap[byteIdx] & mask)
|
||||||
|
}
|
||||||
|
return rank
|
||||||
|
}
|
||||||
143
triedb/pathdb/stem_blob_edge_test.go
Normal file
143
triedb/pathdb/stem_blob_edge_test.go
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
// Copyright 2026 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 pathdb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||||
|
"github.com/ethereum/go-ethereum/trie/bintrie"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestStemBlobOffset127_128Boundary tests the bitmap byte boundary
|
||||||
|
// between offset 127 (last header storage slot) and offset 128
|
||||||
|
// (first code chunk). Off-by-one in bitmapRank at this boundary
|
||||||
|
// would cause extractStemOffset to return the wrong value.
|
||||||
|
func TestStemBlobOffset127_128Boundary(t *testing.T) {
|
||||||
|
b := newStemBuilder()
|
||||||
|
val127 := bytes.Repeat([]byte{0x7F}, stemBlobValueSize)
|
||||||
|
val128 := bytes.Repeat([]byte{0x80}, stemBlobValueSize)
|
||||||
|
b.set(127, val127)
|
||||||
|
b.set(128, val128)
|
||||||
|
|
||||||
|
blob := b.encode()
|
||||||
|
if blob == nil {
|
||||||
|
t.Fatal("encode returned nil for 2-offset builder")
|
||||||
|
}
|
||||||
|
|
||||||
|
got127, err := extractStemOffset(blob, 127)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("extract offset 127: %v", err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(got127, val127) {
|
||||||
|
t.Errorf("offset 127: got %x, want %x", got127, val127)
|
||||||
|
}
|
||||||
|
|
||||||
|
got128, err := extractStemOffset(blob, 128)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("extract offset 128: %v", err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(got128, val128) {
|
||||||
|
t.Errorf("offset 128: got %x, want %x", got128, val128)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify bitmapRank correctness at the byte boundary.
|
||||||
|
var bitmap [stemBlobBitmapSize]byte
|
||||||
|
copy(bitmap[:], blob[:stemBlobBitmapSize])
|
||||||
|
if r := bitmapRank(bitmap, 127); r != 0 {
|
||||||
|
t.Errorf("bitmapRank(127) = %d, want 0", r)
|
||||||
|
}
|
||||||
|
if r := bitmapRank(bitmap, 128); r != 1 {
|
||||||
|
t.Errorf("bitmapRank(128) = %d, want 1", r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestStemBlobFull256DeleteMiddle tests a fully-populated stem (all 256
|
||||||
|
// offsets) where one offset in the middle is deleted.
|
||||||
|
func TestStemBlobFull256DeleteMiddle(t *testing.T) {
|
||||||
|
b := newStemBuilder()
|
||||||
|
for i := range 256 {
|
||||||
|
val := bytes.Repeat([]byte{byte(i)}, stemBlobValueSize)
|
||||||
|
b.set(byte(i), val)
|
||||||
|
}
|
||||||
|
if bitmapPopcount(b.bitmap) != 256 {
|
||||||
|
t.Fatalf("full builder has popcount %d, want 256", bitmapPopcount(b.bitmap))
|
||||||
|
}
|
||||||
|
|
||||||
|
b.set(128, nil) // delete the middle
|
||||||
|
if bitmapPopcount(b.bitmap) != 255 {
|
||||||
|
t.Fatalf("after delete: popcount %d, want 255", bitmapPopcount(b.bitmap))
|
||||||
|
}
|
||||||
|
|
||||||
|
blob := b.encode()
|
||||||
|
expectedSize := stemBlobBitmapSize + 255*stemBlobValueSize
|
||||||
|
if len(blob) != expectedSize {
|
||||||
|
t.Fatalf("blob size %d, want %d", len(blob), expectedSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
got128, _ := extractStemOffset(blob, 128)
|
||||||
|
if got128 != nil {
|
||||||
|
t.Errorf("offset 128 should be absent, got %x", got128)
|
||||||
|
}
|
||||||
|
|
||||||
|
got127, _ := extractStemOffset(blob, 127)
|
||||||
|
if !bytes.Equal(got127, bytes.Repeat([]byte{127}, stemBlobValueSize)) {
|
||||||
|
t.Errorf("offset 127 corrupted after deleting 128")
|
||||||
|
}
|
||||||
|
got129, _ := extractStemOffset(blob, 129)
|
||||||
|
if !bytes.Equal(got129, bytes.Repeat([]byte{129}, stemBlobValueSize)) {
|
||||||
|
t.Errorf("offset 129 corrupted after deleting 128")
|
||||||
|
}
|
||||||
|
got0, _ := extractStemOffset(blob, 0)
|
||||||
|
if !bytes.Equal(got0, bytes.Repeat([]byte{0}, stemBlobValueSize)) {
|
||||||
|
t.Errorf("offset 0 corrupted")
|
||||||
|
}
|
||||||
|
got255, _ := extractStemOffset(blob, 255)
|
||||||
|
if !bytes.Equal(got255, bytes.Repeat([]byte{255}, stemBlobValueSize)) {
|
||||||
|
t.Errorf("offset 255 corrupted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestFlushIdempotency verifies that flushing the same data twice
|
||||||
|
// produces an identical on-disk blob.
|
||||||
|
func TestFlushIdempotency(t *testing.T) {
|
||||||
|
codec, db := newTestBintrieCodec(t)
|
||||||
|
stem := bytes.Repeat([]byte{0x55}, bintrie.StemSize)
|
||||||
|
mkKey := func(offset byte) common.Hash {
|
||||||
|
var k common.Hash
|
||||||
|
copy(k[:bintrie.StemSize], stem)
|
||||||
|
k[bintrie.StemSize] = offset
|
||||||
|
return k
|
||||||
|
}
|
||||||
|
val := bytes.Repeat([]byte{0xAA}, stemBlobValueSize)
|
||||||
|
|
||||||
|
batch := db.NewBatch()
|
||||||
|
codec.Flush(batch, nil, map[common.Hash][]byte{mkKey(5): val}, nil, nil)
|
||||||
|
flushBatch(t, batch)
|
||||||
|
blob1 := rawdb.ReadBinTrieStem(db, stem)
|
||||||
|
|
||||||
|
batch = db.NewBatch()
|
||||||
|
codec.Flush(batch, nil, map[common.Hash][]byte{mkKey(5): val}, nil, nil)
|
||||||
|
flushBatch(t, batch)
|
||||||
|
blob2 := rawdb.ReadBinTrieStem(db, stem)
|
||||||
|
|
||||||
|
if !bytes.Equal(blob1, blob2) {
|
||||||
|
t.Errorf("Flush is not idempotent: blob1 len=%d blob2 len=%d", len(blob1), len(blob2))
|
||||||
|
}
|
||||||
|
}
|
||||||
361
triedb/pathdb/stem_blob_test.go
Normal file
361
triedb/pathdb/stem_blob_test.go
Normal file
|
|
@ -0,0 +1,361 @@
|
||||||
|
// Copyright 2026 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 pathdb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// mkval constructs a 32-byte value where the first byte is tag and the
|
||||||
|
// rest are zero. Used to make test assertions easy to read.
|
||||||
|
func mkval(tag byte) []byte {
|
||||||
|
v := make([]byte, stemBlobValueSize)
|
||||||
|
v[0] = tag
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestStemBlobEmpty verifies that a builder with no entries encodes to
|
||||||
|
// nil (so callers delete the key) and decodes back to a zero bitmap and
|
||||||
|
// no values.
|
||||||
|
func TestStemBlobEmpty(t *testing.T) {
|
||||||
|
b := newStemBuilder()
|
||||||
|
if !b.empty() {
|
||||||
|
t.Fatal("fresh builder should be empty")
|
||||||
|
}
|
||||||
|
blob := b.encode()
|
||||||
|
if blob != nil {
|
||||||
|
t.Fatalf("empty builder should encode to nil, got %x", blob)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode nil and empty slice both yield an empty result.
|
||||||
|
for _, input := range [][]byte{nil, {}} {
|
||||||
|
bitmap, values, err := decodeStemBlob(input)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("decode empty: %v", err)
|
||||||
|
}
|
||||||
|
if values != nil {
|
||||||
|
t.Fatalf("decode empty values: got %v, want nil", values)
|
||||||
|
}
|
||||||
|
for i, b := range bitmap {
|
||||||
|
if b != 0 {
|
||||||
|
t.Fatalf("decode empty bitmap byte %d: got 0x%02x, want 0", i, b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestStemBlobBasicDataAndCodeHash verifies the "account header" encoding
|
||||||
|
// pattern: offsets 0 and 1 populated. This is the common case for every
|
||||||
|
// account update.
|
||||||
|
func TestStemBlobBasicDataAndCodeHash(t *testing.T) {
|
||||||
|
b := newStemBuilder()
|
||||||
|
basicData := mkval(0xAA)
|
||||||
|
codeHash := mkval(0xBB)
|
||||||
|
b.set(0, basicData)
|
||||||
|
b.set(1, codeHash)
|
||||||
|
|
||||||
|
if b.empty() {
|
||||||
|
t.Fatal("builder should not be empty after two sets")
|
||||||
|
}
|
||||||
|
|
||||||
|
blob := b.encode()
|
||||||
|
if blob == nil {
|
||||||
|
t.Fatal("encode should not return nil for populated builder")
|
||||||
|
}
|
||||||
|
if got, want := len(blob), stemBlobBitmapSize+2*stemBlobValueSize; got != want {
|
||||||
|
t.Fatalf("blob length: got %d, want %d", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Roundtrip through decodeStemBlob.
|
||||||
|
bitmap, values, err := decodeStemBlob(blob)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("decode: %v", err)
|
||||||
|
}
|
||||||
|
if got := bitmapPopcount(bitmap); got != 2 {
|
||||||
|
t.Fatalf("popcount: got %d, want 2", got)
|
||||||
|
}
|
||||||
|
if !bitmapGet(bitmap, 0) || !bitmapGet(bitmap, 1) {
|
||||||
|
t.Fatalf("bitmap missing offset 0 or 1: %x", bitmap)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(values[0], basicData) {
|
||||||
|
t.Fatalf("value[0]: got %x, want %x", values[0], basicData)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(values[1], codeHash) {
|
||||||
|
t.Fatalf("value[1]: got %x, want %x", values[1], codeHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Point reads via extractStemOffset.
|
||||||
|
got, err := extractStemOffset(blob, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("extract offset 0: %v", err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(got, basicData) {
|
||||||
|
t.Fatalf("extract 0: got %x, want %x", got, basicData)
|
||||||
|
}
|
||||||
|
got, err = extractStemOffset(blob, 1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("extract offset 1: %v", err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(got, codeHash) {
|
||||||
|
t.Fatalf("extract 1: got %x, want %x", got, codeHash)
|
||||||
|
}
|
||||||
|
// An unset offset returns (nil, nil).
|
||||||
|
got, err = extractStemOffset(blob, 42)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("extract unset offset: %v", err)
|
||||||
|
}
|
||||||
|
if got != nil {
|
||||||
|
t.Fatalf("extract unset: got %x, want nil", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestStemBlobAllOffsets verifies that a fully-populated stem (all 256
|
||||||
|
// offsets) encodes and decodes correctly. This is the worst-case size.
|
||||||
|
func TestStemBlobAllOffsets(t *testing.T) {
|
||||||
|
b := newStemBuilder()
|
||||||
|
for i := range stemBlobBitmapBits {
|
||||||
|
b.set(byte(i), mkval(byte(i)))
|
||||||
|
}
|
||||||
|
blob := b.encode()
|
||||||
|
expectedLen := stemBlobBitmapSize + stemBlobBitmapBits*stemBlobValueSize
|
||||||
|
if len(blob) != expectedLen {
|
||||||
|
t.Fatalf("blob length: got %d, want %d", len(blob), expectedLen)
|
||||||
|
}
|
||||||
|
|
||||||
|
bitmap, _, err := decodeStemBlob(blob)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("decode: %v", err)
|
||||||
|
}
|
||||||
|
if bitmapPopcount(bitmap) != stemBlobBitmapBits {
|
||||||
|
t.Fatalf("popcount: got %d, want %d", bitmapPopcount(bitmap), stemBlobBitmapBits)
|
||||||
|
}
|
||||||
|
for i := range stemBlobBitmapBits {
|
||||||
|
got, err := extractStemOffset(blob, byte(i))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("extract %d: %v", i, err)
|
||||||
|
}
|
||||||
|
if got[0] != byte(i) {
|
||||||
|
t.Fatalf("extract %d: tag 0x%02x, want 0x%02x", i, got[0], byte(i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestStemBlobSparseHighOffsets verifies that non-contiguous offsets
|
||||||
|
// (typical for storage slots scattered across the stem) round-trip
|
||||||
|
// correctly.
|
||||||
|
func TestStemBlobSparseHighOffsets(t *testing.T) {
|
||||||
|
b := newStemBuilder()
|
||||||
|
offsets := []byte{3, 17, 64, 127, 128, 200, 255}
|
||||||
|
for _, o := range offsets {
|
||||||
|
b.set(o, mkval(o))
|
||||||
|
}
|
||||||
|
blob := b.encode()
|
||||||
|
if len(blob) != stemBlobBitmapSize+len(offsets)*stemBlobValueSize {
|
||||||
|
t.Fatalf("unexpected blob length: %d", len(blob))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract each and verify, including some absent offsets in between.
|
||||||
|
for _, o := range offsets {
|
||||||
|
got, err := extractStemOffset(blob, o)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("extract %d: %v", o, err)
|
||||||
|
}
|
||||||
|
if got[0] != o {
|
||||||
|
t.Fatalf("extract %d: tag 0x%02x, want 0x%02x", o, got[0], o)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Spot-check absent offsets between populated ones.
|
||||||
|
for _, o := range []byte{0, 1, 2, 4, 18, 63, 126, 129, 199, 254} {
|
||||||
|
got, err := extractStemOffset(blob, o)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("extract absent %d: %v", o, err)
|
||||||
|
}
|
||||||
|
if got != nil {
|
||||||
|
t.Fatalf("extract absent %d: got %x, want nil", o, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestStemBlobSetClearRoundtrip verifies that setting and then clearing
|
||||||
|
// an offset leaves the builder in the same state as never setting it.
|
||||||
|
func TestStemBlobSetClearRoundtrip(t *testing.T) {
|
||||||
|
b := newStemBuilder()
|
||||||
|
b.set(5, mkval(0xCD))
|
||||||
|
if b.empty() {
|
||||||
|
t.Fatal("should not be empty after set")
|
||||||
|
}
|
||||||
|
b.set(5, nil)
|
||||||
|
if !b.empty() {
|
||||||
|
t.Fatal("should be empty after clearing the only entry")
|
||||||
|
}
|
||||||
|
if blob := b.encode(); blob != nil {
|
||||||
|
t.Fatalf("encode after clear: got %x, want nil", blob)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestStemBlobLoadFromBlob verifies that an existing blob can be loaded
|
||||||
|
// into a fresh builder for read-modify-write semantics.
|
||||||
|
func TestStemBlobLoadFromBlob(t *testing.T) {
|
||||||
|
// Build an initial blob with two entries.
|
||||||
|
b1 := newStemBuilder()
|
||||||
|
b1.set(0, mkval(0x11))
|
||||||
|
b1.set(64, mkval(0x22))
|
||||||
|
initial := b1.encode()
|
||||||
|
|
||||||
|
// Load into a fresh builder, modify, encode.
|
||||||
|
b2 := newStemBuilder()
|
||||||
|
if err := b2.loadFromBlob(initial); err != nil {
|
||||||
|
t.Fatalf("loadFromBlob: %v", err)
|
||||||
|
}
|
||||||
|
b2.set(0, mkval(0x33)) // overwrite offset 0
|
||||||
|
b2.set(64, nil) // clear offset 64
|
||||||
|
b2.set(128, mkval(0x44)) // add offset 128
|
||||||
|
updated := b2.encode()
|
||||||
|
|
||||||
|
// Offset 0 should have the new value.
|
||||||
|
got, err := extractStemOffset(updated, 0)
|
||||||
|
if err != nil || got == nil || got[0] != 0x33 {
|
||||||
|
t.Fatalf("offset 0 after update: got %x err=%v, want tag 0x33", got, err)
|
||||||
|
}
|
||||||
|
// Offset 64 should be absent.
|
||||||
|
got, err = extractStemOffset(updated, 64)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("offset 64 after clear: %v", err)
|
||||||
|
}
|
||||||
|
if got != nil {
|
||||||
|
t.Fatalf("offset 64 after clear: got %x, want nil", got)
|
||||||
|
}
|
||||||
|
// Offset 128 should have the new value.
|
||||||
|
got, err = extractStemOffset(updated, 128)
|
||||||
|
if err != nil || got == nil || got[0] != 0x44 {
|
||||||
|
t.Fatalf("offset 128 after update: got %x err=%v, want tag 0x44", got, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestStemBlobMergeHelper verifies mergeStemBlob: read existing, apply
|
||||||
|
// writes, produce new blob in one call.
|
||||||
|
func TestStemBlobMergeHelper(t *testing.T) {
|
||||||
|
// Start with a blob containing offset 0.
|
||||||
|
b := newStemBuilder()
|
||||||
|
b.set(0, mkval(0x01))
|
||||||
|
initial := b.encode()
|
||||||
|
|
||||||
|
// Merge: overwrite 0, add 1, clear a non-existent offset (no-op).
|
||||||
|
result, err := mergeStemBlob(initial, []stemOffsetValue{
|
||||||
|
{Offset: 0, Value: mkval(0x02)},
|
||||||
|
{Offset: 1, Value: mkval(0x03)},
|
||||||
|
{Offset: 100, Value: nil},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("merge: %v", err)
|
||||||
|
}
|
||||||
|
got, _ := extractStemOffset(result, 0)
|
||||||
|
if got == nil || got[0] != 0x02 {
|
||||||
|
t.Fatalf("merged offset 0: got %x, want tag 0x02", got)
|
||||||
|
}
|
||||||
|
got, _ = extractStemOffset(result, 1)
|
||||||
|
if got == nil || got[0] != 0x03 {
|
||||||
|
t.Fatalf("merged offset 1: got %x, want tag 0x03", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestStemBlobMergeToEmpty verifies that clearing every populated entry
|
||||||
|
// via merge returns a nil blob (so the caller deletes the key).
|
||||||
|
func TestStemBlobMergeToEmpty(t *testing.T) {
|
||||||
|
b := newStemBuilder()
|
||||||
|
b.set(0, mkval(0x01))
|
||||||
|
b.set(5, mkval(0x02))
|
||||||
|
initial := b.encode()
|
||||||
|
|
||||||
|
result, err := mergeStemBlob(initial, []stemOffsetValue{
|
||||||
|
{Offset: 0, Value: nil},
|
||||||
|
{Offset: 5, Value: nil},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("merge to empty: %v", err)
|
||||||
|
}
|
||||||
|
if result != nil {
|
||||||
|
t.Fatalf("merge to empty: got %x, want nil", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestStemBlobTombstoneZeroBytes verifies that a 32-byte zero value is
|
||||||
|
// preserved as "present with zero value" — not confused with "absent".
|
||||||
|
// DeleteStorage uses this convention.
|
||||||
|
func TestStemBlobTombstoneZeroBytes(t *testing.T) {
|
||||||
|
b := newStemBuilder()
|
||||||
|
zeros := make([]byte, stemBlobValueSize)
|
||||||
|
b.set(64, zeros)
|
||||||
|
if b.empty() {
|
||||||
|
t.Fatal("zero-value entry should count as populated")
|
||||||
|
}
|
||||||
|
blob := b.encode()
|
||||||
|
got, err := extractStemOffset(blob, 64)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("extract tombstone: %v", err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(got, zeros) {
|
||||||
|
t.Fatalf("extract tombstone: got %x, want 32 zero bytes", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestStemBlobMalformedInput verifies that decodeStemBlob detects
|
||||||
|
// malformed blobs with wrong lengths.
|
||||||
|
func TestStemBlobMalformedInput(t *testing.T) {
|
||||||
|
// Shorter than bitmap.
|
||||||
|
if _, _, err := decodeStemBlob(make([]byte, 10)); err == nil {
|
||||||
|
t.Fatal("expected error for too-short blob")
|
||||||
|
}
|
||||||
|
// Bitmap claims 2 entries but blob only has room for 1.
|
||||||
|
var bitmap [stemBlobBitmapSize]byte
|
||||||
|
bitmap[0] = 0xC0 // bits 0 and 1 set → 2 entries
|
||||||
|
short := make([]byte, stemBlobBitmapSize+stemBlobValueSize)
|
||||||
|
copy(short, bitmap[:])
|
||||||
|
if _, _, err := decodeStemBlob(short); err == nil {
|
||||||
|
t.Fatal("expected error for blob shorter than bitmap implies")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBitmapRank sanity-checks the bit-to-index helper used by
|
||||||
|
// extractStemOffset for single-offset reads.
|
||||||
|
func TestBitmapRank(t *testing.T) {
|
||||||
|
var bitmap [stemBlobBitmapSize]byte
|
||||||
|
// Set bits at offsets 0, 1, 5, 64, 200.
|
||||||
|
for _, o := range []byte{0, 1, 5, 64, 200} {
|
||||||
|
bitmap[o/8] |= 1 << (7 - uint(o%8))
|
||||||
|
}
|
||||||
|
cases := []struct {
|
||||||
|
offset byte
|
||||||
|
want int
|
||||||
|
}{
|
||||||
|
{0, 0}, // first set bit is at index 0
|
||||||
|
{1, 1}, // second set bit
|
||||||
|
{5, 2}, // third
|
||||||
|
{64, 3}, // fourth
|
||||||
|
{200, 4}, // fifth
|
||||||
|
// For an unset offset, rank returns the number of set bits < it.
|
||||||
|
{2, 2}, // bits 0 and 1 are before 2
|
||||||
|
{100, 4}, // bits 0,1,5,64 are before 100
|
||||||
|
{255, 5}, // all five bits are before 255
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
if got := bitmapRank(bitmap, c.offset); got != c.want {
|
||||||
|
t.Errorf("bitmapRank(%d) = %d, want %d", c.offset, got, c.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue