mirror of
https://github.com/luxfi/evm.git
synced 2026-09-24 00:27:44 +00:00
2846 lines
116 KiB
Go
2846 lines
116 KiB
Go
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||
// See the file LICENSE for licensing terms.
|
||
//
|
||
// This file is a derived work, based on the go-ethereum library whose original
|
||
// notices appear below.
|
||
//
|
||
// It is distributed under a license compatible with the licensing terms of the
|
||
// original code from which it is derived.
|
||
//
|
||
// Much love to the original authors for their work.
|
||
// **********
|
||
// Copyright 2014 The go-ethereum Authors
|
||
// This file is part of the go-ethereum library.
|
||
//
|
||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||
// it under the terms of the GNU Lesser General Public License as published by
|
||
// the Free Software Foundation, either version 3 of the License, or
|
||
// (at your option) any later version.
|
||
//
|
||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
// GNU Lesser General Public License for more details.
|
||
//
|
||
// You should have received a copy of the GNU Lesser General Public License
|
||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||
|
||
// Package core implements the Ethereum consensus protocol.
|
||
package core
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"io"
|
||
"math/big"
|
||
stdruntime "runtime"
|
||
"strings"
|
||
"sync"
|
||
"sync/atomic"
|
||
"time"
|
||
|
||
"github.com/holiman/uint256"
|
||
|
||
"github.com/luxfi/evm/commontype"
|
||
"github.com/luxfi/evm/consensus"
|
||
"github.com/luxfi/evm/core/state"
|
||
"github.com/luxfi/evm/core/state/snapshot"
|
||
"github.com/luxfi/evm/internal/version"
|
||
"github.com/luxfi/evm/params"
|
||
"github.com/luxfi/evm/plugin/evm/customlogs"
|
||
"github.com/luxfi/evm/plugin/evm/customrawdb"
|
||
"github.com/luxfi/evm/plugin/evm/customtypes"
|
||
"github.com/luxfi/geth/common"
|
||
"github.com/luxfi/geth/common/lru"
|
||
"github.com/luxfi/geth/consensus/misc/eip4844"
|
||
"github.com/luxfi/geth/core/rawdb"
|
||
"github.com/luxfi/geth/core/tracing"
|
||
"github.com/luxfi/geth/core/types"
|
||
"github.com/luxfi/geth/core/vm"
|
||
"github.com/luxfi/geth/ethdb"
|
||
"github.com/luxfi/geth/event"
|
||
"github.com/luxfi/geth/metrics"
|
||
"github.com/luxfi/geth/triedb"
|
||
log "github.com/luxfi/log"
|
||
"github.com/luxfi/runtime"
|
||
|
||
// "github.com/luxfi/evm/triedb/firewood"
|
||
"github.com/luxfi/geth/triedb/hashdb"
|
||
"github.com/luxfi/geth/triedb/pathdb"
|
||
|
||
// Force geth metrics of the same name to be registered first.
|
||
_ "github.com/luxfi/geth/core"
|
||
// ffi "github.com/luxfi/firewood-go-ethhash/ffi"
|
||
)
|
||
|
||
// ====== If resolving merge conflicts ======
|
||
//
|
||
// All calls to metrics.NewRegistered*() for metrics also defined in geth/core have been
|
||
// replaced either with:
|
||
// - metrics.GetOrRegister*() to get a metric already registered in geth/core, or register it
|
||
// here otherwise
|
||
// - [getOrOverrideAsRegisteredCounter] to get a metric already registered in geth/core
|
||
// only if it is a [metrics.Counter]. If it is not, the metric is unregistered and registered
|
||
// as a [metrics.Counter] here.
|
||
//
|
||
// These replacements ensure the same metrics are shared between the two packages.
|
||
var (
|
||
accountReadTimer = getOrOverrideAsRegisteredCounter("chain/account/reads", nil)
|
||
accountHashTimer = getOrOverrideAsRegisteredCounter("chain/account/hashes", nil)
|
||
accountUpdateTimer = getOrOverrideAsRegisteredCounter("chain/account/updates", nil)
|
||
accountCommitTimer = getOrOverrideAsRegisteredCounter("chain/account/commits", nil)
|
||
storageReadTimer = getOrOverrideAsRegisteredCounter("chain/storage/reads", nil)
|
||
storageHashTimer = getOrOverrideAsRegisteredCounter("chain/storage/hashes", nil)
|
||
storageUpdateTimer = getOrOverrideAsRegisteredCounter("chain/storage/updates", nil)
|
||
storageCommitTimer = getOrOverrideAsRegisteredCounter("chain/storage/commits", nil)
|
||
snapshotAccountReadTimer = getOrOverrideAsRegisteredCounter("chain/snapshot/account/reads", nil)
|
||
snapshotStorageReadTimer = getOrOverrideAsRegisteredCounter("chain/snapshot/storage/reads", nil)
|
||
snapshotCommitTimer = getOrOverrideAsRegisteredCounter("chain/snapshot/commits", nil)
|
||
|
||
triedbCommitTimer = getOrOverrideAsRegisteredCounter("chain/triedb/commits", nil)
|
||
|
||
blockInsertTimer = metrics.GetOrRegisterCounter("chain/block/inserts", nil)
|
||
blockSignatureRecoveryTimer = metrics.GetOrRegisterCounter("chain/block/signature/recovery", nil)
|
||
blockInsertCount = metrics.GetOrRegisterCounter("chain/block/inserts/count", nil)
|
||
blockContentValidationTimer = metrics.GetOrRegisterCounter("chain/block/validations/content", nil)
|
||
blockStateInitTimer = metrics.GetOrRegisterCounter("chain/block/inits/state", nil)
|
||
blockExecutionTimer = metrics.GetOrRegisterCounter("chain/block/executions", nil)
|
||
blockTrieOpsTimer = metrics.GetOrRegisterCounter("chain/block/trie", nil)
|
||
blockValidationTimer = metrics.GetOrRegisterCounter("chain/block/validations/state", nil)
|
||
blockWriteTimer = metrics.GetOrRegisterCounter("chain/block/writes", nil)
|
||
|
||
acceptorQueueGauge = metrics.GetOrRegisterGauge("chain/acceptor/queue/size", nil)
|
||
acceptorWorkTimer = metrics.GetOrRegisterCounter("chain/acceptor/work", nil)
|
||
acceptorWorkCount = metrics.GetOrRegisterCounter("chain/acceptor/work/count", nil)
|
||
processedBlockGasUsedCounter = metrics.GetOrRegisterCounter("chain/block/gas/used/processed", nil)
|
||
lastAcceptedBlockBaseFeeGauge = metrics.NewRegisteredGauge("chain/block/fee/basefee", nil)
|
||
blockTotalFeesGauge = metrics.NewRegisteredGauge("chain/block/fee/total", nil)
|
||
acceptedBlockGasUsedCounter = metrics.GetOrRegisterCounter("chain/block/gas/used/accepted", nil)
|
||
badBlockCounter = metrics.GetOrRegisterCounter("chain/block/bad/count", nil)
|
||
|
||
txUnindexTimer = metrics.GetOrRegisterCounter("chain/txs/unindex", nil)
|
||
acceptedTxsCounter = metrics.GetOrRegisterCounter("chain/txs/accepted", nil)
|
||
processedTxsCounter = metrics.GetOrRegisterCounter("chain/txs/processed", nil)
|
||
|
||
acceptedLogsCounter = metrics.GetOrRegisterCounter("chain/logs/accepted", nil)
|
||
processedLogsCounter = metrics.GetOrRegisterCounter("chain/logs/processed", nil)
|
||
|
||
ErrRefuseToCorruptArchiver = errors.New("node has operated with pruning disabled, shutting down to prevent missing tries")
|
||
|
||
errFutureBlockUnsupported = errors.New("future block insertion not supported")
|
||
errCacheConfigNotSpecified = errors.New("must specify cache config")
|
||
errInvalidOldChain = errors.New("invalid old chain")
|
||
errInvalidNewChain = errors.New("invalid new chain")
|
||
)
|
||
|
||
const (
|
||
bodyCacheLimit = 256
|
||
blockCacheLimit = 256
|
||
receiptsCacheLimit = 32
|
||
txLookupCacheLimit = 1024
|
||
feeConfigCacheLimit = 256
|
||
coinbaseConfigCacheLimit = 256
|
||
badBlockLimit = 10
|
||
|
||
// BlockChainVersion ensures that an incompatible database forces a resync from scratch.
|
||
//
|
||
// Changelog:
|
||
//
|
||
// - Version 4
|
||
// The following incompatible database changes were added:
|
||
// * the `BlockNumber`, `TxHash`, `TxIndex`, `BlockHash` and `Index` fields of log are deleted
|
||
// * the `Bloom` field of receipt is deleted
|
||
// * the `BlockIndex` and `TxIndex` fields of txlookup are deleted
|
||
// - Version 5
|
||
// The following incompatible database changes were added:
|
||
// * the `TxHash`, `GasCost`, and `ContractAddress` fields are no longer stored for a receipt
|
||
// * the `TxHash`, `GasCost`, and `ContractAddress` fields are computed by looking up the
|
||
// receipts' corresponding block
|
||
// - Version 6
|
||
// The following incompatible database changes were added:
|
||
// * Transaction lookup information stores the corresponding block number instead of block hash
|
||
// - Version 7
|
||
// The following incompatible database changes were added:
|
||
// * Use freezer as the ancient database to maintain all ancient data
|
||
// - Version 8
|
||
// The following incompatible database changes were added:
|
||
// * New scheme for contract code in order to separate the codes and trie nodes
|
||
BlockChainVersion uint64 = 8
|
||
|
||
// statsReportLimit is the time limit during import and export after which we
|
||
// always print out progress. This avoids the user wondering what's going on.
|
||
statsReportLimit = 8 * time.Second
|
||
|
||
// trieCleanCacheStatsNamespace is the namespace to surface stats from the trie
|
||
// clean cache's underlying cache.
|
||
trieCleanCacheStatsNamespace = "hashdb/memcache/clean/fastcache"
|
||
|
||
firewoodFileName = "firewood_state"
|
||
)
|
||
|
||
// cacheableFeeConfig encapsulates fee configuration itself and the block number that it has changed at,
|
||
// in order to cache them together.
|
||
type cacheableFeeConfig struct {
|
||
feeConfig commontype.FeeConfig
|
||
lastChangedAt *big.Int
|
||
}
|
||
|
||
// cacheableCoinbaseConfig encapsulates coinbase address itself and allowFeeRecipient flag,
|
||
// in order to cache them together.
|
||
type cacheableCoinbaseConfig struct {
|
||
coinbaseAddress common.Address
|
||
allowFeeRecipients bool
|
||
}
|
||
|
||
// CacheConfig contains the configuration values for the trie database
|
||
// and state snapshot these are resident in a blockchain.
|
||
type CacheConfig struct {
|
||
TrieCleanLimit int // Memory allowance (MB) to use for caching trie nodes in memory
|
||
TrieDirtyLimit int // Memory limit (MB) at which to block on insert and force a flush of dirty trie nodes to disk
|
||
TrieDirtyCommitTarget int // Memory limit (MB) to target for the dirties cache before invoking commit
|
||
TriePrefetcherParallelism int // Max concurrent disk reads trie prefetcher should perform at once
|
||
CommitInterval uint64 // Commit the trie every [CommitInterval] blocks.
|
||
Pruning bool // Whether to disable trie write caching and GC altogether (archive node)
|
||
AcceptorQueueLimit int // Blocks to queue before blocking during acceptance
|
||
PopulateMissingTries *uint64 // If non-nil, sets the starting height for re-generating historical tries.
|
||
PopulateMissingTriesParallelism int // Number of readers to use when trying to populate missing tries.
|
||
AllowMissingTries bool // Whether to allow an archive node to run with pruning enabled
|
||
SnapshotDelayInit bool // Whether to initialize snapshots on startup or wait for external call (= StateSyncEnabled)
|
||
SnapshotLimit int // Memory allowance (MB) to use for caching snapshot entries in memory
|
||
SnapshotVerify bool // Verify generated snapshots
|
||
Preimages bool // Whether to store preimage of trie key to the disk
|
||
AcceptedCacheSize int // Depth of accepted headers cache and accepted logs cache at the accepted tip
|
||
TransactionHistory uint64 // Number of recent blocks for which to maintain transaction lookup indices
|
||
SkipTxIndexing bool // Whether to skip transaction indexing
|
||
StateHistory uint64 // Number of blocks from head whose state histories are reserved.
|
||
StateScheme string // Scheme used to store ethereum states and merkle tree nodes on top
|
||
|
||
ChainDataDir string // Directory to store chain data in (used by Firewood)
|
||
SnapshotNoBuild bool // Whether the background generation is allowed
|
||
SnapshotWait bool // Wait for snapshot construction on startup (used by tests)
|
||
}
|
||
|
||
// triedbConfig derives the configures for trie database.
|
||
func (c *CacheConfig) triedbConfig() *triedb.Config {
|
||
config := &triedb.Config{Preimages: c.Preimages}
|
||
if c.StateScheme == rawdb.HashScheme || c.StateScheme == "" {
|
||
config.HashDB = &hashdb.Config{
|
||
CleanCacheSize: c.TrieCleanLimit * 1024 * 1024,
|
||
}
|
||
}
|
||
if c.StateScheme == rawdb.PathScheme {
|
||
config.PathDB = &pathdb.Config{
|
||
StateHistory: c.StateHistory,
|
||
TrieCleanSize: c.TrieCleanLimit * 1024 * 1024,
|
||
StateCleanSize: c.SnapshotLimit * 1024 * 1024,
|
||
WriteBufferSize: c.TrieDirtyLimit * 1024 * 1024,
|
||
}
|
||
}
|
||
// if c.StateScheme == customrawdb.FirewoodScheme {
|
||
// // ChainDataDir may not be set during some tests, where this path won't be called.
|
||
// if c.ChainDataDir == "" {
|
||
// log.Crit("Chain data directory must be specified for Firewood")
|
||
// }
|
||
// config.DBOverride = firewood.Config{
|
||
// FilePath: filepath.Join(c.ChainDataDir, firewoodFileName),
|
||
// CleanCacheSize: c.TrieCleanLimit * 1024 * 1024,
|
||
// FreeListCacheEntries: firewood.Defaults.FreeListCacheEntries,
|
||
// Revisions: uint(c.StateHistory), // must be at least 2
|
||
// ReadCacheStrategy: ffi.CacheAllReads,
|
||
// }.BackendConstructor
|
||
// }
|
||
return config
|
||
}
|
||
|
||
// DefaultCacheConfig are the default caching values if none are specified by the
|
||
// user (also used during testing).
|
||
var DefaultCacheConfig = &CacheConfig{
|
||
TrieCleanLimit: 256,
|
||
TrieDirtyLimit: 256,
|
||
TrieDirtyCommitTarget: 20, // 20% overhead in memory counting (this targets 16 MB)
|
||
TriePrefetcherParallelism: 16,
|
||
Pruning: true,
|
||
CommitInterval: 4096,
|
||
AcceptorQueueLimit: 64, // Provides 2 minutes of buffer (2s block target) for a commit delay
|
||
SnapshotLimit: 256,
|
||
AcceptedCacheSize: 32,
|
||
StateHistory: 32, // Default state history size
|
||
StateScheme: rawdb.HashScheme,
|
||
}
|
||
|
||
// DefaultCacheConfigWithScheme returns a deep copied default cache config with
|
||
// a provided trie node scheme.
|
||
func DefaultCacheConfigWithScheme(scheme string) *CacheConfig {
|
||
config := *DefaultCacheConfig
|
||
config.StateScheme = scheme
|
||
// Firewood does not support snapshots yet; disable them.
|
||
if config.StateScheme == customrawdb.FirewoodScheme {
|
||
config.SnapshotLimit = 0 // no snapshot allowed for firewood
|
||
}
|
||
return &config
|
||
}
|
||
|
||
// txLookup is wrapper over transaction lookup along with the corresponding
|
||
// transaction object.
|
||
type txLookup struct {
|
||
lookup *rawdb.LegacyTxLookupEntry
|
||
transaction *types.Transaction
|
||
}
|
||
|
||
// BlockChain represents the canonical chain given a database with a genesis
|
||
// block. The Blockchain manages chain imports, reverts, chain reorganisations.
|
||
//
|
||
// Importing blocks in to the block chain happens according to the set of rules
|
||
// defined by the two stage Validator. Processing of blocks is done using the
|
||
// Processor which processes the included transaction. The validation of the state
|
||
// is done in the second part of the Validator. Failing results in aborting of
|
||
// the import.
|
||
//
|
||
// The BlockChain also helps in returning blocks from **any** chain included
|
||
// in the database as well as blocks that represents the canonical chain. It's
|
||
// important to note that GetBlock can return any block and does not need to be
|
||
// included in the canonical one where as GetBlockByNumber always represents the
|
||
// canonical chain.
|
||
type BlockChain struct {
|
||
chainConfig *params.ChainConfig // Chain & network configuration
|
||
cacheConfig *CacheConfig // Cache configuration for pruning
|
||
|
||
consensusCtx context.Context // Consensus context carrying chain identity (transport for the geth precompile boundary + warp); bound from the chain Runtime before reprocess, upgraded with SharedMemory by SetConsensusContext
|
||
|
||
// atomicImportVerifier authenticates a block's DECLARED cross-chain object
|
||
// consumptions against shared memory, at verify time. Installed by the VM (which
|
||
// owns the bootstrap gate); nil disables the check. See SetAtomicImportVerifier.
|
||
atomicImportVerifier AtomicImportVerifier
|
||
|
||
db ethdb.Database // Low level persistent database to store final content in
|
||
snaps *snapshot.Tree // Snapshot tree for fast trie leaf access
|
||
triedb *triedb.Database // The database handler for maintaining trie nodes.
|
||
stateCache state.Database // State database to reuse between imports (contains state cache)
|
||
txIndexer *txIndexer // Transaction indexer, might be nil if not enabled
|
||
stateManager TrieWriter
|
||
|
||
hc *HeaderChain
|
||
rmLogsFeed event.Feed
|
||
chainFeed event.Feed
|
||
chainSideFeed event.Feed
|
||
chainHeadFeed event.Feed
|
||
chainAcceptedFeed event.Feed
|
||
logsFeed event.Feed
|
||
logsAcceptedFeed event.Feed
|
||
blockProcFeed event.Feed
|
||
txAcceptedFeed event.Feed
|
||
scope event.SubscriptionScope
|
||
genesisBlock *types.Block
|
||
|
||
// This mutex synchronizes chain write operations.
|
||
// Readers don't need to take it, they can just read the database.
|
||
chainmu sync.RWMutex
|
||
|
||
currentBlock atomic.Pointer[types.Header] // Current head of the block chain
|
||
|
||
bodyCache *lru.Cache[common.Hash, *types.Body] // Cache for the most recent block bodies
|
||
receiptsCache *lru.Cache[common.Hash, []*types.Receipt] // Cache for the most recent receipts per block
|
||
blockCache *lru.Cache[common.Hash, *types.Block] // Cache for the most recent entire blocks
|
||
txLookupCache *lru.Cache[common.Hash, txLookup] // Cache for the most recent transaction lookup data.
|
||
badBlocks *lru.Cache[common.Hash, *badBlock] // Cache for bad blocks
|
||
feeConfigCache *lru.Cache[common.Hash, *cacheableFeeConfig] // Cache for the most recent feeConfig lookup data.
|
||
coinbaseConfigCache *lru.Cache[common.Hash, *cacheableCoinbaseConfig] // Cache for the most recent coinbaseConfig lookup data.
|
||
|
||
stopping atomic.Bool // false if chain is running, true when stopped
|
||
|
||
engine consensus.Engine
|
||
validator Validator // Block and state validator interface
|
||
processor Processor // Block transaction processor interface
|
||
vmConfig vm.Config
|
||
|
||
lastAccepted *types.Block // Prevents reorgs past this height
|
||
|
||
// [headStatePinRoot] is a dedicated, UNBALANCED GC reference held on the
|
||
// accepted head's state root so it can never be evicted from the trie dirty
|
||
// cache while it is the head — even across an arbitrarily long idle window
|
||
// (no block production), duplicate empty-block roots, cold snapshot/cache
|
||
// layers, or a small StateHistory. It is transferred head-to-head in Accept
|
||
// (Reference the new head, then Dereference the previous head) and
|
||
// re-established on the loaded head at startup, so exactly one live head pin
|
||
// exists at all times, on bc.lastAccepted.Root(). This is distinct from the
|
||
// insert-time Reference in writeBlockAndSetHead, which is refcount-BALANCED
|
||
// (consumed as the block ages through the StateHistory-deep tipBuffer) and so
|
||
// does NOT protect an idle head. Empty on pathdb (Reference/Dereference are
|
||
// no-ops there); the idle-head-eviction only affects the hashdb dirty cache.
|
||
headStatePinRoot common.Hash
|
||
|
||
senderCacher *TxSenderCacher
|
||
|
||
// [acceptorQueue] is a processing queue for the Acceptor. This is
|
||
// different than [chainAcceptedFeed], which is sent an event after an accepted
|
||
// block is processed (after each loop of the accepted worker). If there is a
|
||
// clean shutdown, all items inserted into the [acceptorQueue] will be processed.
|
||
acceptorQueue chan *types.Block
|
||
|
||
// [acceptorClosingLock], and [acceptorClosed] are used
|
||
// to synchronize the closing of the [acceptorQueue] channel.
|
||
//
|
||
// Because we can't check if a channel is closed without reading from it
|
||
// (which we don't want to do as we may remove a processing block), we need
|
||
// to use a second variable to ensure we don't close a closed channel.
|
||
acceptorClosingLock sync.RWMutex
|
||
acceptorClosed bool
|
||
|
||
// [acceptorWg] is used to wait for the acceptorQueue to clear. This is used
|
||
// during shutdown and in tests.
|
||
acceptorWg sync.WaitGroup
|
||
|
||
// [wg] is used to wait for the async blockchain processes to finish on shutdown.
|
||
wg sync.WaitGroup
|
||
|
||
// quit channel is used to listen for when the blockchain is shut down to close
|
||
// async processes.
|
||
// WaitGroups are used to ensure that async processes have finished during shutdown.
|
||
quit chan struct{}
|
||
|
||
// [acceptorTip] is the last block processed by the acceptor. This is
|
||
// returned as the LastAcceptedBlock() to ensure clients get only fully
|
||
// processed blocks. This may be equal to [lastAccepted].
|
||
acceptorTip *types.Block
|
||
acceptorTipLock sync.Mutex
|
||
|
||
// [flattenLock] prevents the [acceptor] from flattening snapshots while
|
||
// a block is being verified.
|
||
flattenLock sync.Mutex
|
||
|
||
// [acceptedLogsCache] stores recently accepted logs to improve the performance of eth_getLogs.
|
||
acceptedLogsCache FIFOCache[common.Hash, [][]*types.Log]
|
||
|
||
// [txIndexTailLock] is used to synchronize the updating of the tx index tail.
|
||
txIndexTailLock sync.Mutex
|
||
}
|
||
|
||
// NewBlockChain returns a fully initialised block chain using information
|
||
// available in the database. It initialises the default Ethereum Validator and
|
||
// Processor.
|
||
//
|
||
// It binds [chainRuntime] (the chain's consensus identity — networkID / X / C chain ids —
|
||
// which the VM already holds) onto the chain BEFORE loadLastState and populateMissingTries
|
||
// run, so any block re-executed at construction (unclean-shutdown recovery via reprocessState,
|
||
// or archive backfill via populateMissingTries) sees the SAME identity it was built and
|
||
// accepted under. An identity-gated stateful precompile (the 0x9999 DEX value path) therefore
|
||
// re-executes deterministically; without this, reprocess would run with networkID 0 / C-Chain
|
||
// Empty, a committed swap would revert on re-execution, ValidateState would reject the block,
|
||
// and the node could not boot. [chainRuntime] is nil for callers that run no identity-gated
|
||
// precompiles (tests, the simulated backend); that is the unchanged default.
|
||
func NewBlockChain(
|
||
db ethdb.Database, cacheConfig *CacheConfig, genesis *Genesis, engine consensus.Engine,
|
||
vmConfig vm.Config, lastAcceptedHash common.Hash, skipChainConfigCheckCompatible bool,
|
||
chainRuntime *runtime.Runtime,
|
||
) (*BlockChain, error) {
|
||
if cacheConfig == nil {
|
||
return nil, errCacheConfigNotSpecified
|
||
}
|
||
// Open trie database with provided config
|
||
triedb := triedb.NewDatabase(db, cacheConfig.triedbConfig())
|
||
|
||
// Setup the genesis block, commit the provided genesis specification
|
||
// to database if the genesis block is not present yet, or load the
|
||
// stored one from database.
|
||
// Note: In go-ethereum, the code rewinds the chain on an incompatible config upgrade.
|
||
// We don't do this and expect the node operator to always update their node's configuration
|
||
// before network upgrades take effect.
|
||
chainConfig, _, err := SetupGenesisBlock(db, triedb, genesis, lastAcceptedHash, skipChainConfigCheckCompatible)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
log.Info("")
|
||
log.Info(strings.Repeat("-", 153))
|
||
for _, line := range strings.Split(chainConfig.Description(), "\n") {
|
||
log.Info(line)
|
||
}
|
||
log.Info(strings.Repeat("-", 153))
|
||
log.Info("")
|
||
|
||
// Bind the chain's consensus identity BEFORE the bc is used for loadLastState /
|
||
// populateMissingTries reprocess. The consensus context is the last-mile transport the
|
||
// geth precompile boundary reads (runtime.FromContext); the explicit input is the typed
|
||
// [chainRuntime] the VM already holds — nil for chains with no identity-gated precompiles.
|
||
var consensusCtx context.Context
|
||
if chainRuntime != nil {
|
||
consensusCtx = runtime.WithContext(context.Background(), chainRuntime)
|
||
}
|
||
|
||
bc := &BlockChain{
|
||
chainConfig: chainConfig,
|
||
cacheConfig: cacheConfig,
|
||
consensusCtx: consensusCtx,
|
||
db: db,
|
||
triedb: triedb,
|
||
bodyCache: lru.NewCache[common.Hash, *types.Body](bodyCacheLimit),
|
||
receiptsCache: lru.NewCache[common.Hash, []*types.Receipt](receiptsCacheLimit),
|
||
blockCache: lru.NewCache[common.Hash, *types.Block](blockCacheLimit),
|
||
txLookupCache: lru.NewCache[common.Hash, txLookup](txLookupCacheLimit),
|
||
badBlocks: lru.NewCache[common.Hash, *badBlock](badBlockLimit),
|
||
feeConfigCache: lru.NewCache[common.Hash, *cacheableFeeConfig](feeConfigCacheLimit),
|
||
coinbaseConfigCache: lru.NewCache[common.Hash, *cacheableCoinbaseConfig](coinbaseConfigCacheLimit),
|
||
engine: engine,
|
||
vmConfig: vmConfig,
|
||
senderCacher: NewTxSenderCacher(stdruntime.NumCPU()),
|
||
acceptorQueue: make(chan *types.Block, cacheConfig.AcceptorQueueLimit),
|
||
quit: make(chan struct{}),
|
||
acceptedLogsCache: NewFIFOCache[common.Hash, [][]*types.Log](cacheConfig.AcceptedCacheSize),
|
||
}
|
||
bc.stateCache = state.NewDatabaseWithNodeDB(bc.db, bc.triedb)
|
||
bc.validator = NewBlockValidator(chainConfig, bc, engine)
|
||
bc.processor = NewStateProcessor(chainConfig, bc, engine)
|
||
|
||
bc.hc, err = NewHeaderChain(db, chainConfig, cacheConfig, engine)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
bc.genesisBlock = bc.GetBlockByNumber(0)
|
||
if bc.genesisBlock == nil {
|
||
// Workaround for rawdb issue where genesis block can't be read
|
||
// If we have a genesis configuration, recreate the genesis block
|
||
if genesis != nil {
|
||
bc.genesisBlock = genesis.ToBlock()
|
||
// Cache it for future use
|
||
bc.blockCache.Add(bc.genesisBlock.Hash(), bc.genesisBlock)
|
||
} else {
|
||
return nil, ErrNoGenesis
|
||
}
|
||
}
|
||
|
||
bc.currentBlock.Store(nil)
|
||
|
||
// Create the state manager
|
||
bc.stateManager = NewTrieWriter(bc.triedb, cacheConfig)
|
||
|
||
// Re-generate current block state if it is missing
|
||
if err := bc.loadLastState(lastAcceptedHash); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// After loading the last state (and reprocessing if necessary), we are
|
||
// guaranteed that [acceptorTip] is equal to [lastAccepted].
|
||
//
|
||
// It is critical to update this vaue before performing any state repairs so
|
||
// that all accepted blocks can be considered.
|
||
bc.acceptorTip = bc.lastAccepted
|
||
|
||
// [HEAD-STATE-PIN] Establish the dedicated head pin on the loaded accepted root
|
||
// at startup, so a node that restarts and then sits idle (produces no block)
|
||
// still cannot GC its head state — closing the restart-then-idle variant of the
|
||
// freeze. pinAcceptedHead is a no-op if the state is not yet present (a
|
||
// bootstrapping/state-syncing node), where the first Accept establishes it.
|
||
// This is why "restart fixes it" is recovery evidence only — the pin, not the
|
||
// restart, is what makes the head GC-ineligible from here on.
|
||
if bc.lastAccepted != nil {
|
||
if err := bc.pinAcceptedHead(bc.lastAccepted.Root()); err != nil {
|
||
// Non-fatal (backend may not support refcounting, e.g. pathdb):
|
||
// degrade to pre-fix behavior rather than blocking startup.
|
||
log.Warn("[HEAD-STATE-PIN] failed to pin loaded accepted head", "root", bc.lastAccepted.Root(), "err", err)
|
||
}
|
||
}
|
||
|
||
// Make sure the state associated with the block is available
|
||
head := bc.CurrentBlock()
|
||
if !bc.HasState(head.Root) {
|
||
return nil, fmt.Errorf("head state missing %d:%s", head.Number, head.Hash())
|
||
}
|
||
|
||
if err := bc.protectTrieIndex(); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// Populate missing tries if required
|
||
if err := bc.populateMissingTries(); err != nil {
|
||
return nil, fmt.Errorf("could not populate missing tries: %v", err)
|
||
}
|
||
|
||
// If snapshot initialization is delayed for fast sync, skip initializing it here.
|
||
// This assumes that no blocks will be processed until ResetState is called to initialize
|
||
// the state of fast sync.
|
||
if !bc.cacheConfig.SnapshotDelayInit {
|
||
// Load any existing snapshot, regenerating it if loading failed (if not
|
||
// already initialized in recovery)
|
||
bc.initSnapshot(head)
|
||
}
|
||
|
||
// Warm up [hc.acceptedNumberCache] and [acceptedLogsCache]
|
||
bc.warmAcceptedCaches()
|
||
|
||
// if txlookup limit is 0 (uindexing disabled), we don't need to repair the tx index tail.
|
||
if bc.cacheConfig.TransactionHistory != 0 {
|
||
latestStateSynced := customrawdb.GetLatestSyncPerformed(bc.db)
|
||
bc.repairTxIndexTail(latestStateSynced)
|
||
}
|
||
|
||
// Start processing accepted blocks effects in the background
|
||
go bc.startAcceptor()
|
||
|
||
// Start tx indexer if it's enabled.
|
||
if bc.cacheConfig.TransactionHistory != 0 {
|
||
bc.txIndexer = newTxIndexer(bc.cacheConfig.TransactionHistory, bc)
|
||
}
|
||
return bc, nil
|
||
}
|
||
|
||
// writeBlockAcceptedIndices writes any indices that must be persisted for accepted block.
|
||
// This includes the following:
|
||
// - transaction lookup indices
|
||
// - updating the acceptor tip index
|
||
func (bc *BlockChain) writeBlockAcceptedIndices(b *types.Block) error {
|
||
batch := bc.db.NewBatch()
|
||
if err := bc.batchBlockAcceptedIndices(batch, b); err != nil {
|
||
return err
|
||
}
|
||
if err := batch.Write(); err != nil {
|
||
return fmt.Errorf("%w: failed to write accepted indices entries batch", err)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (bc *BlockChain) batchBlockAcceptedIndices(batch ethdb.Batch, b *types.Block) error {
|
||
if !bc.cacheConfig.SkipTxIndexing {
|
||
rawdb.WriteTxLookupEntriesByBlock(batch, b)
|
||
}
|
||
if err := customrawdb.WriteAcceptorTip(batch, b.Hash()); err != nil {
|
||
return fmt.Errorf("%w: failed to write acceptor tip key", err)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// flattenSnapshot attempts to flatten a block of [hash] to disk.
|
||
func (bc *BlockChain) flattenSnapshot(postAbortWork func() error, hash common.Hash) error {
|
||
// If snapshots are not initialized, perform [postAbortWork] immediately.
|
||
if bc.snaps == nil {
|
||
return postAbortWork()
|
||
}
|
||
|
||
// Abort snapshot generation before pruning anything from trie database
|
||
// (could occur in AcceptTrie)
|
||
bc.snaps.AbortGeneration()
|
||
|
||
// Perform work after snapshot generation is aborted (typically trie updates)
|
||
if err := postAbortWork(); err != nil {
|
||
return err
|
||
}
|
||
|
||
// Check if the snapshot layer exists for this block hash.
|
||
// When SnapshotNoBuild is true, snapshot layers may not be created for new blocks,
|
||
// so we skip flattening if the layer doesn't exist.
|
||
if !bc.snaps.HasBlockLayer(hash) {
|
||
return nil
|
||
}
|
||
|
||
// Ensure we avoid flattening the snapshot while we are processing a block, or
|
||
// block execution will fallback to reading from the trie (which is much
|
||
// slower).
|
||
bc.flattenLock.Lock()
|
||
defer bc.flattenLock.Unlock()
|
||
|
||
// Flatten the entire snap Trie to disk
|
||
//
|
||
// Note: This resumes snapshot generation.
|
||
return bc.snaps.Flatten(hash)
|
||
}
|
||
|
||
// warmAcceptedCaches fetches previously accepted headers and logs from disk to
|
||
// pre-populate [hc.acceptedNumberCache] and [acceptedLogsCache].
|
||
func (bc *BlockChain) warmAcceptedCaches() {
|
||
var (
|
||
startTime = time.Now()
|
||
lastAccepted = bc.LastAcceptedBlock().NumberU64()
|
||
startIndex = uint64(1)
|
||
targetCacheSize = uint64(bc.cacheConfig.AcceptedCacheSize)
|
||
)
|
||
if targetCacheSize == 0 {
|
||
log.Info("Not warming accepted cache because disabled")
|
||
return
|
||
}
|
||
if lastAccepted < startIndex {
|
||
// This could occur if we haven't accepted any blocks yet
|
||
log.Info("Not warming accepted cache because there are no accepted blocks")
|
||
return
|
||
}
|
||
cacheDiff := targetCacheSize - 1 // last accepted lookback is inclusive, so we reduce size by 1
|
||
if cacheDiff < lastAccepted {
|
||
startIndex = lastAccepted - cacheDiff
|
||
}
|
||
for i := startIndex; i <= lastAccepted; i++ {
|
||
block := bc.GetBlockByNumber(i)
|
||
if block == nil {
|
||
// This could happen if a node state-synced
|
||
log.Info("Exiting accepted cache warming early because header is nil", "height", i, "t", time.Since(startTime))
|
||
break
|
||
}
|
||
// Blocks written to disk during state sync are skipped here; the cache
|
||
// warming only covers blocks already fully validated.
|
||
bc.hc.acceptedNumberCache.Put(block.NumberU64(), block.Header())
|
||
logs := bc.collectUnflattenedLogs(block, false)
|
||
bc.acceptedLogsCache.Put(block.Hash(), logs)
|
||
}
|
||
log.Info("Warmed accepted caches", "start", startIndex, "end", lastAccepted, "t", time.Since(startTime))
|
||
}
|
||
|
||
// startAcceptor starts processing items on the [acceptorQueue]. If a [nil]
|
||
// object is placed on the [acceptorQueue], the [startAcceptor] will exit.
|
||
func (bc *BlockChain) startAcceptor() {
|
||
log.Info("Starting Acceptor", "queue length", bc.cacheConfig.AcceptorQueueLimit)
|
||
|
||
for next := range bc.acceptorQueue {
|
||
start := time.Now()
|
||
acceptorQueueGauge.Dec(1)
|
||
|
||
// Update acceptor tip and transaction lookup index
|
||
// Write this prior to state changes to allow easier reconstruction in `reprocessState`.
|
||
if err := bc.writeBlockAcceptedIndices(next); err != nil {
|
||
log.Crit("failed to write accepted block effects", "err", err)
|
||
}
|
||
|
||
if err := bc.flattenSnapshot(func() error {
|
||
return bc.stateManager.AcceptTrie(next)
|
||
}, next.Hash()); err != nil {
|
||
// "not supported" means the triedb backend (e.g. pathdb) doesn't implement Cap().
|
||
// This is non-fatal during fast-follow sync — log a warning and continue.
|
||
if strings.Contains(err.Error(), "not supported") {
|
||
log.Warn("snapshot flatten not supported by backend, skipping", "blockHash", next.Hash(), "err", err)
|
||
} else {
|
||
log.Crit("unable to flatten snapshot from acceptor", "blockHash", next.Hash(), "err", err)
|
||
}
|
||
}
|
||
|
||
// Ensure [hc.acceptedNumberCache] and [acceptedLogsCache] have latest content
|
||
bc.hc.acceptedNumberCache.Put(next.NumberU64(), next.Header())
|
||
logs := bc.collectUnflattenedLogs(next, false)
|
||
bc.acceptedLogsCache.Put(next.Hash(), logs)
|
||
|
||
// Update the acceptor tip before sending events to ensure that any client acting based off of
|
||
// the events observes the updated acceptorTip on subsequent requests
|
||
bc.acceptorTipLock.Lock()
|
||
bc.acceptorTip = next
|
||
bc.acceptorTipLock.Unlock()
|
||
|
||
// Update accepted feeds
|
||
flattenedLogs := customlogs.FlattenLogs(logs)
|
||
bc.chainAcceptedFeed.Send(ChainEvent{Block: next, Hash: next.Hash(), Logs: flattenedLogs})
|
||
if len(flattenedLogs) > 0 {
|
||
bc.logsAcceptedFeed.Send(flattenedLogs)
|
||
}
|
||
if len(next.Transactions()) != 0 {
|
||
bc.txAcceptedFeed.Send(NewTxsEvent{next.Transactions()})
|
||
}
|
||
|
||
bc.acceptorWg.Done()
|
||
|
||
acceptorWorkTimer.Inc(time.Since(start).Milliseconds())
|
||
acceptorWorkCount.Inc(1)
|
||
// Note: in contrast to most accepted metrics, we increment the accepted log metrics in the acceptor queue because
|
||
// the logs are already processed in the acceptor queue.
|
||
acceptedLogsCounter.Inc(int64(len(logs)))
|
||
}
|
||
}
|
||
|
||
// addAcceptorQueue adds a new *types.Block to the [acceptorQueue]. This will
|
||
// block if there are [AcceptorQueueLimit] items in [acceptorQueue].
|
||
func (bc *BlockChain) addAcceptorQueue(b *types.Block) {
|
||
// We only acquire a read lock here because it is ok to add items to the
|
||
// [acceptorQueue] concurrently.
|
||
bc.acceptorClosingLock.RLock()
|
||
defer bc.acceptorClosingLock.RUnlock()
|
||
|
||
if bc.acceptorClosed {
|
||
return
|
||
}
|
||
|
||
acceptorQueueGauge.Inc(1)
|
||
bc.acceptorWg.Add(1)
|
||
bc.acceptorQueue <- b
|
||
}
|
||
|
||
// DrainAcceptorQueue blocks until all items in [acceptorQueue] have been
|
||
// processed.
|
||
func (bc *BlockChain) DrainAcceptorQueue() {
|
||
bc.acceptorClosingLock.RLock()
|
||
defer bc.acceptorClosingLock.RUnlock()
|
||
|
||
if bc.acceptorClosed {
|
||
return
|
||
}
|
||
|
||
bc.acceptorWg.Wait()
|
||
}
|
||
|
||
// stopAcceptor sends a signal to the Acceptor to stop processing accepted
|
||
// blocks. The Acceptor will exit once all items in [acceptorQueue] have been
|
||
// processed.
|
||
func (bc *BlockChain) stopAcceptor() {
|
||
bc.acceptorClosingLock.Lock()
|
||
defer bc.acceptorClosingLock.Unlock()
|
||
|
||
// If [acceptorClosed] is already false, we should just return here instead
|
||
// of attempting to close [acceptorQueue] more than once (will cause
|
||
// a panic).
|
||
//
|
||
// This typically happens when a test calls [stopAcceptor] directly (prior to
|
||
// shutdown) and then [stopAcceptor] is called again in shutdown.
|
||
if bc.acceptorClosed {
|
||
return
|
||
}
|
||
|
||
// Although nothing should be added to [acceptorQueue] after
|
||
// [acceptorClosed] is updated, we close the channel so the Acceptor
|
||
// goroutine exits.
|
||
bc.acceptorWg.Wait()
|
||
bc.acceptorClosed = true
|
||
close(bc.acceptorQueue)
|
||
}
|
||
|
||
func (bc *BlockChain) InitializeSnapshots() {
|
||
bc.chainmu.Lock()
|
||
defer bc.chainmu.Unlock()
|
||
|
||
head := bc.CurrentBlock()
|
||
bc.initSnapshot(head)
|
||
}
|
||
|
||
// SenderCacher returns the *TxSenderCacher used within the core package.
|
||
func (bc *BlockChain) SenderCacher() *TxSenderCacher {
|
||
return bc.senderCacher
|
||
}
|
||
|
||
// SetConsensusContext re-affirms / upgrades the consensus context after construction — the VM
|
||
// calls it once the full chain Runtime (carrying the cross-chain atomic SharedMemory capability)
|
||
// is available. The chain identity is already bound at construction from chainRuntime (so
|
||
// reprocess is deterministic); this is the live (non-reprocess) execution context.
|
||
func (bc *BlockChain) SetConsensusContext(ctx context.Context) {
|
||
bc.consensusCtx = ctx
|
||
}
|
||
|
||
// ConsensusContext returns the consensus context (the transport carrying chain identity, and
|
||
// after SetConsensusContext the cross-chain SharedMemory) read at the geth precompile boundary.
|
||
func (bc *BlockChain) ConsensusContext() context.Context {
|
||
return bc.consensusCtx
|
||
}
|
||
|
||
// AtomicImportVerifier authenticates the cross-chain object consumptions a block
|
||
// DECLARES in its logs against the peer chain's atomic shared memory. Returning an
|
||
// error makes the block INVALID (verification fails and the block is rejected) —
|
||
// which is the whole point: execution can no longer read shared memory without
|
||
// making the block unreplayable, so the read moved here, where a mismatch costs the
|
||
// producer a block instead of forking a receipt root.
|
||
//
|
||
// It is installed by the VM because the VM owns the engine-state gate: a
|
||
// bootstrapping node must skip the check (it may not have applied the exporting
|
||
// chain's block yet), exactly as the shared-memory layer requires of any chain that
|
||
// consumes cross-chain state.
|
||
type AtomicImportVerifier func(block *types.Block, logs []*types.Log) error
|
||
|
||
// SetAtomicImportVerifier installs the cross-chain import authentication hook. It is
|
||
// called once at VM initialization, before any block is verified.
|
||
func (bc *BlockChain) SetAtomicImportVerifier(v AtomicImportVerifier) {
|
||
bc.atomicImportVerifier = v
|
||
}
|
||
|
||
// loadLastState loads the last known chain state from the database. This method
|
||
// assumes that the chain manager mutex is held.
|
||
func (bc *BlockChain) loadLastState(lastAcceptedHash common.Hash) error {
|
||
// Initialize genesis state
|
||
if lastAcceptedHash == (common.Hash{}) {
|
||
return bc.loadGenesisState()
|
||
}
|
||
|
||
// The consensus-owned accepted pointer is the durable recovery anchor. Load
|
||
// it before the optimistic processing head so a crash after rejecting a
|
||
// processing block can be repaired even if the old head body was deleted.
|
||
bc.lastAccepted = bc.GetBlockByHash(lastAcceptedHash)
|
||
if bc.lastAccepted == nil {
|
||
return fmt.Errorf("could not load last accepted block %s", lastAcceptedHash.Hex())
|
||
}
|
||
|
||
// Restore the last known head block
|
||
head := rawdb.ReadHeadBlockHash(bc.db)
|
||
if head == (common.Hash{}) {
|
||
return errors.New("could not read head block hash")
|
||
}
|
||
// Make sure the entire head block is available
|
||
headBlock := bc.GetBlockByHash(head)
|
||
repairedMissingHead := false
|
||
if headBlock == nil {
|
||
// Older releases could delete a rejected canonical processing block
|
||
// without first rewinding the head markers. Recover only from the exact
|
||
// block authenticated by the consensus last-accepted pointer; never guess
|
||
// another ancestor or silently fall back to genesis.
|
||
log.Warn("[HEAD-POINTER-REPAIR] processing head body is missing; recovering from last accepted block",
|
||
"missingHead", head,
|
||
"accepted", bc.lastAccepted.Hash(),
|
||
"acceptedHeight", bc.lastAccepted.NumberU64(),
|
||
)
|
||
if err := bc.deleteCanonicalHashesAbove(bc.lastAccepted.NumberU64()); err != nil {
|
||
return fmt.Errorf("could not remove canonical assignments above repaired head %s:%d: %w",
|
||
bc.lastAccepted.Hash(), bc.lastAccepted.NumberU64(), err)
|
||
}
|
||
bc.writeHeadBlock(bc.lastAccepted)
|
||
headBlock = bc.lastAccepted
|
||
repairedMissingHead = true
|
||
}
|
||
// Everything seems to be fine, set as the head block
|
||
bc.currentBlock.Store(headBlock.Header())
|
||
|
||
// Restore the last known head header
|
||
currentHeader := headBlock.Header()
|
||
if head := rawdb.ReadHeadHeaderHash(bc.db); head != (common.Hash{}) {
|
||
if header := bc.GetHeaderByHash(head); header != nil {
|
||
currentHeader = header
|
||
}
|
||
}
|
||
bc.hc.SetCurrentHeader(currentHeader)
|
||
|
||
log.Info("Loaded most recent local header", "number", currentHeader.Number, "hash", currentHeader.Hash(), "age", common.PrettyAge(time.Unix(int64(currentHeader.Time), 0)))
|
||
log.Info("Loaded most recent local full block", "number", headBlock.Number(), "hash", headBlock.Hash(), "age", common.PrettyAge(time.Unix(int64(headBlock.Time()), 0)))
|
||
|
||
// This ensures that the head block is updated to the last accepted block on startup.
|
||
if err := bc.setPreference(bc.lastAccepted); err != nil {
|
||
return fmt.Errorf("failed to set preference to last accepted block while loading last state: %w", err)
|
||
}
|
||
|
||
// reprocessState is necessary to ensure that the last accepted state is
|
||
// available. The state may not be available if it was not committed due
|
||
// to an unclean shutdown.
|
||
// v1.104.47 repaired the stale head pointer before this state-suffix repair
|
||
// existed. Such a node no longer presents a missing head on its next restart,
|
||
// but the live failure leaves one deterministic fingerprint: the accepted tip
|
||
// is immediately before the commit boundary whose processing block was
|
||
// deleted. Re-run the same bounded repair for that already-rewound shape.
|
||
boundaryRepair := bc.cacheConfig.CommitInterval > 0 &&
|
||
(bc.lastAccepted.NumberU64()+1)%bc.cacheConfig.CommitInterval == 0
|
||
if repairedMissingHead || boundaryRepair {
|
||
// A legacy reject could leave more than stale head pointers behind. In
|
||
// hash-scheme pruning mode the rejected processing block may have crossed
|
||
// a commit boundary and garbage-collected descendants of the accepted
|
||
// parent's trie while leaving the root node itself on disk. HasState only
|
||
// proves that root node can be opened; it cannot prove all descendants are
|
||
// present, so the normal reprocessState fast path would incorrectly skip
|
||
// recovery and the next block would fail part-way through execution with a
|
||
// bare "not found".
|
||
//
|
||
// Rebuild the accepted suffix from the preceding durable commit boundary.
|
||
// Every transition is validated against its historical header and the
|
||
// final accepted root is force-committed, making this both deterministic
|
||
// and restart-safe. This runs only after the exact legacy-damage signature
|
||
// (head pointer names a missing block body, or a prior release already
|
||
// rewound it at the affected commit boundary) has been observed.
|
||
if err := bc.rebuildAcceptedStateAfterHeadRepair(bc.lastAccepted); err != nil {
|
||
return fmt.Errorf("could not rebuild accepted state after repairing missing processing head: %w", err)
|
||
}
|
||
}
|
||
return bc.reprocessState(bc.lastAccepted, 2*bc.cacheConfig.CommitInterval)
|
||
}
|
||
|
||
// rebuildAcceptedStateAfterHeadRepair force-reexecutes the accepted suffix from
|
||
// the preceding on-disk commit boundary. It intentionally does not use HasState
|
||
// for the target: a partially persisted hash trie can contain its root node while
|
||
// missing a descendant, which is indistinguishable from a complete trie through
|
||
// OpenTrie alone.
|
||
func (bc *BlockChain) rebuildAcceptedStateAfterHeadRepair(target *types.Block) error {
|
||
if target == nil || target.NumberU64() == 0 {
|
||
return nil
|
||
}
|
||
interval := bc.cacheConfig.CommitInterval
|
||
if interval == 0 {
|
||
return errors.New("cannot rebuild accepted state with zero commit interval")
|
||
}
|
||
|
||
targetHeight := target.NumberU64()
|
||
startHeight := targetHeight - targetHeight%interval
|
||
// If the accepted target itself is a commit boundary, begin at the prior
|
||
// boundary so the target is still re-executed and proven complete.
|
||
if startHeight == targetHeight && startHeight >= interval {
|
||
startHeight -= interval
|
||
}
|
||
start := bc.GetBlockByNumber(startHeight)
|
||
if start == nil {
|
||
return fmt.Errorf("durable commit-boundary block %d is unavailable", startHeight)
|
||
}
|
||
|
||
log.Warn("[HEAD-STATE-REPAIR] rebuilding accepted state from durable commit boundary",
|
||
"fromHeight", startHeight,
|
||
"toHeight", targetHeight,
|
||
"blocks", targetHeight-startHeight,
|
||
"acceptedHash", target.Hash(),
|
||
"acceptedRoot", target.Root(),
|
||
)
|
||
|
||
parent := start
|
||
var previousRoot common.Hash
|
||
for height := startHeight + 1; height <= targetHeight; height++ {
|
||
current := bc.GetBlockByNumber(height)
|
||
if current == nil {
|
||
return fmt.Errorf("accepted canonical block %d is unavailable", height)
|
||
}
|
||
root, err := bc.reprocessBlock(parent, current)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to re-execute accepted block %s:%d: %w", current.Hash(), height, err)
|
||
}
|
||
if root != current.Root() {
|
||
return fmt.Errorf("re-executed root %s != accepted header root %s for block %s:%d",
|
||
root, current.Root(), current.Hash(), height)
|
||
}
|
||
if previousRoot != (common.Hash{}) && previousRoot != root {
|
||
if err := bc.triedb.Dereference(previousRoot); err != nil {
|
||
return fmt.Errorf("failed to release intermediate rebuilt root %s at height %d: %w",
|
||
previousRoot, height-1, err)
|
||
}
|
||
}
|
||
previousRoot = root
|
||
parent = current
|
||
}
|
||
if previousRoot == (common.Hash{}) {
|
||
return errors.New("accepted-state rebuild did not execute any blocks")
|
||
}
|
||
if err := bc.triedb.Commit(previousRoot, true); err != nil {
|
||
return fmt.Errorf("failed to commit rebuilt accepted root %s: %w", previousRoot, err)
|
||
}
|
||
log.Warn("[HEAD-STATE-REPAIR] rebuilt and committed accepted state",
|
||
"height", targetHeight,
|
||
"hash", target.Hash(),
|
||
"root", previousRoot,
|
||
)
|
||
return nil
|
||
}
|
||
|
||
func (bc *BlockChain) loadGenesisState() error {
|
||
// Prepare the genesis block and reinitialise the chain
|
||
batch := bc.db.NewBatch()
|
||
customrawdb.WriteBlock(batch, bc.genesisBlock)
|
||
if err := batch.Write(); err != nil {
|
||
log.Crit("Failed to write genesis block", "err", err)
|
||
}
|
||
bc.writeHeadBlock(bc.genesisBlock)
|
||
|
||
// Last update all in-memory chain markers
|
||
bc.lastAccepted = bc.genesisBlock
|
||
bc.currentBlock.Store(bc.genesisBlock.Header())
|
||
bc.hc.SetGenesis(bc.genesisBlock.Header())
|
||
bc.hc.SetCurrentHeader(bc.genesisBlock.Header())
|
||
return nil
|
||
}
|
||
|
||
// Export writes the active chain to the given writer.
|
||
func (bc *BlockChain) Export(w io.Writer) error {
|
||
return bc.ExportN(w, uint64(0), bc.CurrentBlock().Number.Uint64())
|
||
}
|
||
|
||
// ExportN writes a subset of the active chain to the given writer.
|
||
func (bc *BlockChain) ExportN(w io.Writer, first uint64, last uint64) error {
|
||
return bc.ExportCallback(func(block *types.Block) error {
|
||
return block.EncodeRLP(w)
|
||
}, first, last)
|
||
}
|
||
|
||
// ExportCallback invokes [callback] for every block from [first] to [last] in order.
|
||
func (bc *BlockChain) ExportCallback(callback func(block *types.Block) error, first uint64, last uint64) error {
|
||
if first > last {
|
||
return fmt.Errorf("export failed: first (%d) is greater than last (%d)", first, last)
|
||
}
|
||
log.Info("Exporting batch of blocks", "count", last-first+1)
|
||
|
||
var (
|
||
parentHash common.Hash
|
||
start = time.Now()
|
||
reported = time.Now()
|
||
)
|
||
for nr := first; nr <= last; nr++ {
|
||
block := bc.GetBlockByNumber(nr)
|
||
if block == nil {
|
||
return fmt.Errorf("export failed on #%d: not found", nr)
|
||
}
|
||
if nr > first && block.ParentHash() != parentHash {
|
||
return errors.New("export failed: chain reorg during export")
|
||
}
|
||
parentHash = block.Hash()
|
||
if err := callback(block); err != nil {
|
||
return err
|
||
}
|
||
if time.Since(reported) >= statsReportLimit {
|
||
log.Info("Exporting blocks", "exported", block.NumberU64()-first, "elapsed", common.PrettyDuration(time.Since(start)))
|
||
reported = time.Now()
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// writeHeadBlock injects a new head block into the current block chain. This method
|
||
// assumes that the block is indeed a true head. It will also reset the head
|
||
// header to this very same block if they are older or if they are on a different side chain.
|
||
//
|
||
// Note, this function assumes that the `mu` mutex is held!
|
||
func (bc *BlockChain) writeHeadBlock(block *types.Block) {
|
||
// If the block is on a side chain or an unknown one, force other heads onto it too
|
||
// Add the block to the canonical chain number scheme and mark as the head
|
||
batch := bc.db.NewBatch()
|
||
rawdb.WriteCanonicalHash(batch, block.Hash(), block.NumberU64())
|
||
|
||
rawdb.WriteHeadBlockHash(batch, block.Hash())
|
||
rawdb.WriteHeadHeaderHash(batch, block.Hash())
|
||
|
||
// Flush the whole batch into the disk, exit the node if failed
|
||
if err := batch.Write(); err != nil {
|
||
log.Crit("Failed to update chain indexes and markers", "err", err)
|
||
}
|
||
// Update all in-memory chain markers in the last step
|
||
bc.hc.SetCurrentHeader(block.Header())
|
||
bc.currentBlock.Store(block.Header())
|
||
}
|
||
|
||
// deleteCanonicalHashesAbove removes stale canonical number assignments above
|
||
// a retained head. It is shared by normal reorgs and startup head-pointer repair
|
||
// so both paths leave the same durable canonical index shape.
|
||
func (bc *BlockChain) deleteCanonicalHashesAbove(head uint64) error {
|
||
// Chunk writes to avoid BadgerDB ErrTxnTooBig for large rewinds.
|
||
const indexBatchSize = 1000
|
||
batch := bc.db.NewBatch()
|
||
batchCount := 0
|
||
for height := head + 1; ; height++ {
|
||
if rawdb.ReadCanonicalHash(bc.db, height) == (common.Hash{}) {
|
||
break
|
||
}
|
||
rawdb.DeleteCanonicalHash(batch, height)
|
||
batchCount++
|
||
if batchCount >= indexBatchSize {
|
||
if err := batch.Write(); err != nil {
|
||
return err
|
||
}
|
||
batch = bc.db.NewBatch()
|
||
batchCount = 0
|
||
}
|
||
}
|
||
if batchCount > 0 {
|
||
return batch.Write()
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// ValidateCanonicalChain confirms a canonical chain is well-formed.
|
||
func (bc *BlockChain) ValidateCanonicalChain() error {
|
||
// Ensure all accepted blocks are fully processed
|
||
bc.DrainAcceptorQueue()
|
||
|
||
current := bc.CurrentBlock()
|
||
i := 0
|
||
log.Info("Beginning to validate canonical chain", "startBlock", current.Number)
|
||
|
||
for current.Hash() != bc.genesisBlock.Hash() {
|
||
blkByHash := bc.GetBlockByHash(current.Hash())
|
||
if blkByHash == nil {
|
||
return fmt.Errorf("couldn't find block by hash %s at height %d", current.Hash().String(), current.Number)
|
||
}
|
||
if blkByHash.Hash() != current.Hash() {
|
||
return fmt.Errorf("blockByHash returned a block with an unexpected hash: %s, expected: %s", blkByHash.Hash().String(), current.Hash().String())
|
||
}
|
||
blkByNumber := bc.GetBlockByNumber(current.Number.Uint64())
|
||
if blkByNumber == nil {
|
||
return fmt.Errorf("couldn't find block by number at height %d", current.Number)
|
||
}
|
||
if blkByNumber.Hash() != current.Hash() {
|
||
return fmt.Errorf("blockByNumber returned a block with unexpected hash: %s, expected: %s", blkByNumber.Hash().String(), current.Hash().String())
|
||
}
|
||
|
||
hdrByHash := bc.GetHeaderByHash(current.Hash())
|
||
if hdrByHash == nil {
|
||
return fmt.Errorf("couldn't find block header by hash %s at height %d", current.Hash().String(), current.Number)
|
||
}
|
||
if hdrByHash.Hash() != current.Hash() {
|
||
return fmt.Errorf("hdrByHash returned a block header with an unexpected hash: %s, expected: %s", hdrByHash.Hash().String(), current.Hash().String())
|
||
}
|
||
hdrByNumber := bc.GetHeaderByNumber(current.Number.Uint64())
|
||
if hdrByNumber == nil {
|
||
return fmt.Errorf("couldn't find block header by number at height %d", current.Number)
|
||
}
|
||
if hdrByNumber.Hash() != current.Hash() {
|
||
return fmt.Errorf("hdrByNumber returned a block header with unexpected hash: %s, expected: %s", hdrByNumber.Hash().String(), current.Hash().String())
|
||
}
|
||
|
||
// Lookup the full block to get the transactions
|
||
block := bc.GetBlock(current.Hash(), current.Number.Uint64())
|
||
if block == nil {
|
||
log.Error("Current block not found in database", "block", current.Number, "hash", current.Hash())
|
||
return fmt.Errorf("current block missing: #%d [%x..]", current.Number, current.Hash().Bytes()[:4])
|
||
}
|
||
txs := block.Transactions()
|
||
|
||
// Transactions are only indexed beneath the last accepted block, so we only check
|
||
// that the transactions have been indexed, if we are checking below the last accepted
|
||
// block.
|
||
shouldIndexTxs := !bc.cacheConfig.SkipTxIndexing &&
|
||
(bc.cacheConfig.TransactionHistory == 0 || bc.lastAccepted.NumberU64() < current.Number.Uint64()+bc.cacheConfig.TransactionHistory)
|
||
if current.Number.Uint64() <= bc.lastAccepted.NumberU64() && shouldIndexTxs {
|
||
// Ensure that all of the transactions have been stored correctly in the canonical
|
||
// chain
|
||
for txIndex, tx := range txs {
|
||
txLookup, _, _ := bc.GetTransactionLookup(tx.Hash())
|
||
if txLookup == nil {
|
||
return fmt.Errorf("failed to find transaction %s", tx.Hash().String())
|
||
}
|
||
if txLookup.BlockHash != current.Hash() {
|
||
return fmt.Errorf("tx lookup returned with incorrect block hash: %s, expected: %s", txLookup.BlockHash.String(), current.Hash().String())
|
||
}
|
||
if txLookup.BlockIndex != current.Number.Uint64() {
|
||
return fmt.Errorf("tx lookup returned with incorrect block index: %d, expected: %d", txLookup.BlockIndex, current.Number)
|
||
}
|
||
if txLookup.Index != uint64(txIndex) {
|
||
return fmt.Errorf("tx lookup returned with incorrect transaction index: %d, expected: %d", txLookup.Index, txIndex)
|
||
}
|
||
}
|
||
}
|
||
|
||
blkReceipts := bc.GetReceiptsByHash(current.Hash())
|
||
if blkReceipts.Len() != len(txs) {
|
||
return fmt.Errorf("found %d transaction receipts, expected %d", blkReceipts.Len(), len(txs))
|
||
}
|
||
for index, txReceipt := range blkReceipts {
|
||
if txReceipt.TxHash != txs[index].Hash() {
|
||
return fmt.Errorf("transaction receipt mismatch, expected %s, but found: %s", txs[index].Hash().String(), txReceipt.TxHash.String())
|
||
}
|
||
if txReceipt.BlockHash != current.Hash() {
|
||
return fmt.Errorf("transaction receipt had block hash %s, but expected %s", txReceipt.BlockHash.String(), current.Hash().String())
|
||
}
|
||
if txReceipt.BlockNumber.Uint64() != current.Number.Uint64() {
|
||
return fmt.Errorf("transaction receipt had block number %d, but expected %d", txReceipt.BlockNumber.Uint64(), current.Number)
|
||
}
|
||
}
|
||
|
||
i += 1
|
||
if i%1000 == 0 {
|
||
log.Info("Validate Canonical Chain Update", "totalBlocks", i)
|
||
}
|
||
|
||
parent := bc.GetHeaderByHash(current.ParentHash)
|
||
if parent.Hash() != current.ParentHash {
|
||
return fmt.Errorf("getBlockByHash retrieved parent block with incorrect hash, found %s, expected: %s", parent.Hash().String(), current.ParentHash.String())
|
||
}
|
||
current = parent
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// stopWithoutSaving stops the blockchain service. If any imports are currently in progress
|
||
// it will abort them using the procInterrupt. This method stops all running
|
||
// goroutines, but does not do all the post-stop work of persisting data.
|
||
// OBS! It is generally recommended to use the Stop method!
|
||
// This method has been exposed to allow tests to stop the blockchain while simulating
|
||
// a crash.
|
||
func (bc *BlockChain) stopWithoutSaving() {
|
||
if !bc.stopping.CompareAndSwap(false, true) {
|
||
return
|
||
}
|
||
// Signal shutdown tx indexer.
|
||
if bc.txIndexer != nil {
|
||
bc.txIndexer.close()
|
||
}
|
||
|
||
log.Info("Closing quit channel")
|
||
close(bc.quit)
|
||
// Wait for accepted feed to process all remaining items
|
||
log.Info("Stopping Acceptor")
|
||
start := time.Now()
|
||
bc.stopAcceptor()
|
||
log.Info("Acceptor queue drained", "t", time.Since(start))
|
||
|
||
// Stop senderCacher's goroutines
|
||
log.Info("Shutting down sender cacher")
|
||
bc.senderCacher.Shutdown()
|
||
|
||
// Unsubscribe all subscriptions registered from blockchain.
|
||
log.Info("Closing scope")
|
||
bc.scope.Close()
|
||
|
||
// Waiting for background processes to complete
|
||
log.Info("Waiting for background processes to complete")
|
||
bc.wg.Wait()
|
||
}
|
||
|
||
// Stop stops the blockchain service. If any imports are currently in progress
|
||
// it will abort them using the procInterrupt.
|
||
func (bc *BlockChain) Stop() {
|
||
bc.stopWithoutSaving()
|
||
|
||
// Ensure that the entirety of the state snapshot is journaled to disk.
|
||
if bc.snaps != nil {
|
||
bc.snaps.Release()
|
||
}
|
||
if bc.triedb.Scheme() == rawdb.PathScheme {
|
||
// Ensure that the in-memory trie nodes are journaled to disk properly.
|
||
if err := bc.triedb.Journal(bc.CurrentBlock().Root); err != nil {
|
||
log.Info("Failed to journal in-memory trie nodes", "err", err)
|
||
}
|
||
}
|
||
log.Info("Shutting down state manager")
|
||
start := time.Now()
|
||
if err := bc.stateManager.Shutdown(); err != nil {
|
||
log.Error("Failed to Shutdown state manager", "err", err)
|
||
}
|
||
log.Info("State manager shut down", "t", time.Since(start))
|
||
// Close the trie database, release all the held resources as the last step.
|
||
if err := bc.triedb.Close(); err != nil {
|
||
log.Error("Failed to close trie database", "err", err)
|
||
}
|
||
log.Info("Blockchain stopped")
|
||
}
|
||
|
||
// SetPreference attempts to update the head block to be the provided block and
|
||
// emits a ChainHeadEvent if successful. This function will handle all reorg
|
||
// side effects, if necessary.
|
||
//
|
||
// Note: This function should ONLY be called on blocks that have already been
|
||
// inserted into the chain.
|
||
//
|
||
// Assumes [bc.chainmu] is not held by the caller.
|
||
func (bc *BlockChain) SetPreference(block *types.Block) error {
|
||
bc.chainmu.Lock()
|
||
defer bc.chainmu.Unlock()
|
||
|
||
return bc.setPreference(block)
|
||
}
|
||
|
||
// setPreference attempts to update the head block to be the provided block and
|
||
// emits a ChainHeadEvent if successful. This function will handle all reorg
|
||
// side effects, if necessary.
|
||
//
|
||
// Assumes [bc.chainmu] is held by the caller.
|
||
func (bc *BlockChain) setPreference(block *types.Block) error {
|
||
current := bc.CurrentBlock()
|
||
|
||
// Return early if the current block is already the block
|
||
// we are trying to write.
|
||
if current.Hash() == block.Hash() {
|
||
return nil
|
||
}
|
||
|
||
log.Debug("Setting preference", "number", block.Number(), "hash", block.Hash())
|
||
|
||
// writeKnownBlock updates the head block and will handle any reorg side
|
||
// effects automatically.
|
||
if err := bc.writeKnownBlock(block); err != nil {
|
||
return fmt.Errorf("unable to invoke writeKnownBlock: %w", err)
|
||
}
|
||
|
||
// Send a ChainHeadEvent if we end up altering
|
||
// the head block. Many internal aysnc processes rely on
|
||
// receiving these events (i.e. the TxPool).
|
||
bc.chainHeadFeed.Send(ChainHeadEvent{Block: block})
|
||
return nil
|
||
}
|
||
|
||
// LastConsensusAcceptedBlock returns the last block to be marked as accepted. It may or
|
||
// may not yet be processed.
|
||
func (bc *BlockChain) LastConsensusAcceptedBlock() *types.Block {
|
||
bc.chainmu.Lock()
|
||
defer bc.chainmu.Unlock()
|
||
|
||
return bc.lastAccepted
|
||
}
|
||
|
||
// LastAcceptedBlock returns the last block to be marked as accepted and is
|
||
// processed.
|
||
//
|
||
// Note: During initialization, [acceptorTip] is equal to [lastAccepted].
|
||
func (bc *BlockChain) LastAcceptedBlock() *types.Block {
|
||
bc.acceptorTipLock.Lock()
|
||
defer bc.acceptorTipLock.Unlock()
|
||
|
||
return bc.acceptorTip
|
||
}
|
||
|
||
// MaterializeState guarantees the state trie for [target] is present in the trie
|
||
// database, rebuilding it on demand from the nearest ancestor whose state is
|
||
// available if it was pruned. It is a cheap HasState no-op when the state is
|
||
// already present.
|
||
//
|
||
// This is the recovery half of the fresh-multi-node block-production halt: on a
|
||
// fresh chain the reorg/reject churn that equivocation produces can dereference
|
||
// an accepted block's trie AFTER it becomes the head (the state is present at
|
||
// accept — so the accept-path guard is a no-op — but pruned before the next
|
||
// BuildBlock reads it), wedging the chain with "missing trie node". Called from
|
||
// the miner's createCurrentEnvironment on a StateAt miss, this re-executes the
|
||
// pruned range so BuildBlock can always load its parent state. reprocessBlock's
|
||
// ValidateState re-checks each rebuilt root against its header, so a
|
||
// non-deterministic transition surfaces loudly rather than silently.
|
||
func (bc *BlockChain) MaterializeState(target *types.Block) error {
|
||
if bc.HasState(target.Root()) {
|
||
return nil
|
||
}
|
||
bc.chainmu.Lock()
|
||
defer bc.chainmu.Unlock()
|
||
// Re-check under the lock — a concurrent insert may have materialized it.
|
||
if bc.HasState(target.Root()) {
|
||
return nil
|
||
}
|
||
|
||
// Walk back to the nearest ancestor whose state is available, collecting the
|
||
// missing blocks (target first, oldest-missing last).
|
||
var missing []*types.Block
|
||
cur := target
|
||
for !bc.HasState(cur.Root()) {
|
||
if cur.NumberU64() == 0 {
|
||
return fmt.Errorf("cannot materialize state for %s:%d: genesis state missing", target.Hash().Hex(), target.NumberU64())
|
||
}
|
||
missing = append(missing, cur)
|
||
parent := bc.GetBlock(cur.ParentHash(), cur.NumberU64()-1)
|
||
if parent == nil {
|
||
return fmt.Errorf("cannot materialize state for %s:%d: missing ancestor %s:%d", target.Hash().Hex(), target.NumberU64(), cur.ParentHash().Hex(), cur.NumberU64()-1)
|
||
}
|
||
cur = parent
|
||
}
|
||
|
||
log.Warn("[STATE-MATERIALIZE] rebuilding pruned state on demand",
|
||
"target", target.Hash(), "targetHeight", target.NumberU64(), "targetRoot", target.Root(),
|
||
"fromHeight", cur.NumberU64(), "blocks", len(missing),
|
||
)
|
||
|
||
// Re-execute forward from [cur] (which has state) through the missing range.
|
||
parent := cur
|
||
for i := len(missing) - 1; i >= 0; i-- {
|
||
child := missing[i]
|
||
root, err := bc.reprocessBlock(parent, child)
|
||
if err != nil {
|
||
return fmt.Errorf("materialize: failed to re-execute %s:%d: %w", child.Hash().Hex(), child.NumberU64(), err)
|
||
}
|
||
if root != child.Root() {
|
||
return fmt.Errorf("materialize: re-executed root %s != header root %s for %s:%d (non-deterministic state transition)",
|
||
root.Hex(), child.Root().Hex(), child.Hash().Hex(), child.NumberU64())
|
||
}
|
||
parent = child
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// SetLastAcceptedBlockDirect sets the last accepted block directly without
|
||
// validation. This is used during block import to update the canonical chain
|
||
// head after InsertChain has verified and inserted the blocks.
|
||
// IMPORTANT: Only use this for imported blocks that have already been validated.
|
||
func (bc *BlockChain) SetLastAcceptedBlockDirect(block *types.Block) error {
|
||
bc.chainmu.Lock()
|
||
defer bc.chainmu.Unlock()
|
||
|
||
// Update database pointers
|
||
batch := bc.db.NewBatch()
|
||
if err := bc.batchBlockAcceptedIndices(batch, block); err != nil {
|
||
return err
|
||
}
|
||
rawdb.WriteHeadBlockHash(batch, block.Hash())
|
||
rawdb.WriteHeadHeaderHash(batch, block.Hash())
|
||
if err := batch.Write(); err != nil {
|
||
return err
|
||
}
|
||
|
||
// Update in-memory chain markers
|
||
bc.lastAccepted = block
|
||
bc.acceptorTip = block
|
||
bc.currentBlock.Store(block.Header())
|
||
bc.hc.SetCurrentHeader(block.Header())
|
||
|
||
// [HEAD-STATE-PIN] Keep the head pin on the directly-set accepted head so a
|
||
// repair/resurrection path that sits idle afterward cannot GC its head state.
|
||
// Best-effort: this is a recovery path and normal Accept re-establishes the pin.
|
||
if err := bc.pinAcceptedHead(block.Root()); err != nil {
|
||
log.Warn("[HEAD-STATE-PIN] failed to pin directly-set accepted head", "root", block.Root(), "err", err)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// AcceptImportedState accepts and commits the state for imported blocks.
|
||
// This should be called after importing blocks via InsertChain to ensure
|
||
// the state trie is persisted to disk. Without calling this, imported
|
||
// blocks will have their state in memory but it won't be committed,
|
||
// causing "required historical state unavailable" errors on restart.
|
||
func (bc *BlockChain) AcceptImportedState(block *types.Block) error {
|
||
log.Info("Accepting imported state", "block", block.NumberU64(), "root", block.Root())
|
||
|
||
// Accept the trie for the imported block
|
||
if err := bc.stateManager.AcceptTrie(block); err != nil {
|
||
return fmt.Errorf("failed to accept trie for block %d: %w", block.NumberU64(), err)
|
||
}
|
||
|
||
log.Info("Successfully committed imported state", "block", block.NumberU64())
|
||
return nil
|
||
}
|
||
|
||
// ForceCommitState commits the state trie for the given block directly to disk.
|
||
// This bypasses the state manager's commit interval logic (cappedMemoryTrieWriter
|
||
// only commits at commitInterval boundaries via modulo, which would miss most
|
||
// commits during import). Used during block import to periodically persist state
|
||
// so the node can safely restart without "required historical state unavailable" errors.
|
||
func (bc *BlockChain) ForceCommitState(block *types.Block) error {
|
||
return bc.triedb.Commit(block.Root(), true)
|
||
}
|
||
|
||
// CommitInterval returns the configured commit interval for the blockchain.
|
||
func (bc *BlockChain) CommitInterval() uint64 {
|
||
return bc.cacheConfig.CommitInterval
|
||
}
|
||
|
||
// EnsureGenesisState ensures that the genesis block state is accessible.
|
||
// This is needed before importing blocks because the import validation
|
||
// requires parent state to be accessible.
|
||
// For RLP imports, if the genesis state is not accessible but allocations are stored,
|
||
// this function will regenerate and commit the genesis state from the allocations.
|
||
func (bc *BlockChain) EnsureGenesisState() error {
|
||
genesis := bc.genesisBlock
|
||
if genesis == nil {
|
||
return errors.New("genesis block not set")
|
||
}
|
||
|
||
// Check if genesis state is already accessible
|
||
if bc.HasState(genesis.Root()) {
|
||
log.Debug("Genesis state is accessible", "root", genesis.Root().Hex())
|
||
return nil
|
||
}
|
||
|
||
log.Warn("Genesis state not accessible, attempting recovery", "root", genesis.Root().Hex())
|
||
|
||
// For PathDB, the genesis state was committed at startup but the in-memory layer
|
||
// may have been evicted. Try to recover it by loading from the journal/disk.
|
||
if bc.triedb.Scheme() == rawdb.PathScheme {
|
||
log.Info("Genesis state not in memory, using PathDB scheme",
|
||
"root", genesis.Root().Hex())
|
||
|
||
// For PathDB with allow-missing-tries, we can proceed without the genesis
|
||
// state in memory. The import will use the database scheme's fallback
|
||
// mechanism to read state from disk.
|
||
if bc.cacheConfig.AllowMissingTries {
|
||
log.Info("AllowMissingTries enabled, proceeding without genesis state in memory",
|
||
"root", genesis.Root().Hex())
|
||
return nil
|
||
}
|
||
|
||
// PathDB: attempt to read the genesis state from the journal/disk
|
||
return nil
|
||
}
|
||
|
||
// For HashDB, if we don't have the genesis state, we need the allocations
|
||
// stored in the database to regenerate it.
|
||
log.Info("Attempting to regenerate genesis state from stored allocations")
|
||
allocJSON := rawdb.ReadGenesisStateSpec(bc.db, genesis.Hash())
|
||
if allocJSON == nil {
|
||
return fmt.Errorf("genesis state not accessible and no allocations stored in database for genesis hash %s; "+
|
||
"use admin_writeGenesisStateSpec to store the genesis allocations before importing blocks", genesis.Hash().Hex())
|
||
}
|
||
|
||
log.Info("Found genesis allocations in database, regenerating state",
|
||
"genesisHash", genesis.Hash().Hex(), "allocSize", len(allocJSON))
|
||
|
||
// Parse the stored allocations
|
||
var alloc types.GenesisAlloc
|
||
if err := json.Unmarshal(allocJSON, &alloc); err != nil {
|
||
return fmt.Errorf("failed to parse genesis allocations: %w", err)
|
||
}
|
||
|
||
// Create a new state from empty root
|
||
statedb, err := state.New(types.EmptyRootHash, bc.stateCache, nil)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to create state for genesis regeneration: %w", err)
|
||
}
|
||
|
||
// Apply all allocations to create accounts
|
||
for addr, account := range alloc {
|
||
statedb.SetBalance(addr, uint256.MustFromBig(account.Balance), tracing.BalanceIncreaseGenesisBalance)
|
||
_ = statedb.SetCode(addr, account.Code, tracing.CodeChangeGenesis)
|
||
statedb.SetNonce(addr, account.Nonce, tracing.NonceChangeGenesis)
|
||
for key, value := range account.Storage {
|
||
statedb.SetState(addr, key, value)
|
||
}
|
||
}
|
||
|
||
// Compute and verify the root matches genesis
|
||
computedRoot := statedb.IntermediateRoot(false)
|
||
if computedRoot != genesis.Root() {
|
||
return fmt.Errorf("regenerated state root %s does not match genesis root %s",
|
||
computedRoot.Hex(), genesis.Root().Hex())
|
||
}
|
||
|
||
// Commit the state to the database
|
||
// Args: block number, delete empty objects, no storage wiping
|
||
root, err := statedb.Commit(0, false, false)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to commit regenerated genesis state: %w", err)
|
||
}
|
||
|
||
// Flush the trie to persistent storage
|
||
if err := bc.triedb.Commit(root, false); err != nil {
|
||
return fmt.Errorf("failed to flush regenerated genesis state to disk: %w", err)
|
||
}
|
||
|
||
log.Info("Successfully regenerated genesis state",
|
||
"genesisHash", genesis.Hash().Hex(),
|
||
"stateRoot", root.Hex(),
|
||
"accounts", len(alloc))
|
||
|
||
return nil
|
||
}
|
||
|
||
// pinAcceptedHead transfers the dedicated, unbalanced GC pin (see the
|
||
// [headStatePinRoot] field doc) onto root, releasing the previous head's pin so
|
||
// exactly one live reference is held on the current accepted head's state root.
|
||
// That reference makes the head GC-ineligible: the tipBuffer/Cap can never evict
|
||
// it while it is the head, regardless of idle duration, duplicate empty-block
|
||
// roots, cold caches, or a small StateHistory. This is the durable freeze fix —
|
||
// the standalone lifetime "is the accepted head", NOT balanced against the
|
||
// InsertTrie/AcceptTrie/RejectTrie reference counting.
|
||
//
|
||
// No-op when: root is already the pinned head (duplicate roots — keeps exactly
|
||
// one reference), or its state is not yet materialized (a bootstrapping /
|
||
// state-syncing node — the first Accept with present state then establishes it).
|
||
// Reference/Dereference are no-ops on pathdb, so this is inert there. Callers
|
||
// must hold bc.chainmu (the state markers it reads/writes are chainmu-guarded).
|
||
func (bc *BlockChain) pinAcceptedHead(root common.Hash) error {
|
||
if root == bc.headStatePinRoot || !bc.HasState(root) {
|
||
return nil
|
||
}
|
||
if err := bc.triedb.Reference(root, common.Hash{}); err != nil {
|
||
return err
|
||
}
|
||
if bc.headStatePinRoot != (common.Hash{}) {
|
||
bc.triedb.Dereference(bc.headStatePinRoot)
|
||
}
|
||
bc.headStatePinRoot = root
|
||
return nil
|
||
}
|
||
|
||
// Accept sets a minimum height at which no reorg can pass. Additionally,
|
||
// this function may trigger a reorg if the block being accepted is not in the
|
||
// canonical chain.
|
||
//
|
||
// Assumes [bc.chainmu] is not held by the caller.
|
||
func (bc *BlockChain) Accept(block *types.Block) error {
|
||
bc.chainmu.Lock()
|
||
defer bc.chainmu.Unlock()
|
||
|
||
// The parent of [block] must be the last accepted block.
|
||
if bc.lastAccepted.Hash() != block.ParentHash() {
|
||
return fmt.Errorf(
|
||
"expected accepted block to have parent %s:%d but got %s:%d",
|
||
bc.lastAccepted.Hash().Hex(),
|
||
bc.lastAccepted.NumberU64(),
|
||
block.ParentHash().Hex(),
|
||
block.NumberU64()-1,
|
||
)
|
||
}
|
||
|
||
// If the canonical hash at the block height does not match the block we are
|
||
// accepting, we need to trigger a reorg.
|
||
canonical := bc.GetCanonicalHash(block.NumberU64())
|
||
if canonical != block.Hash() {
|
||
log.Debug("Accepting block in non-canonical chain", "number", block.Number(), "hash", block.Hash())
|
||
if err := bc.setPreference(block); err != nil {
|
||
return fmt.Errorf("could not set new preferred block %d:%s as preferred: %w", block.Number(), block.Hash(), err)
|
||
}
|
||
}
|
||
|
||
// [ACCEPT-BACKSTOP] Guarantee the accepted block's state is materialized before
|
||
// it becomes the head. Under equivocation the consensus engine can finalize (via
|
||
// the α-of-K cert / catch-up path) a block that this node verified only as a
|
||
// transient non-canonical sibling — whose state trie was then dereferenced when
|
||
// that sibling was rejected — and the finalize path does NOT re-verify it. The
|
||
// result is a head whose state root is absent from the trie database, so the next
|
||
// BuildBlock fails with "missing trie node" and the chain wedges (the fresh
|
||
// re-genesised testnet/devnet block-2 halt). Re-executing here on the parent
|
||
// (== last accepted, whose state is present by construction) rematerializes the
|
||
// exact accepted root; commitWithSnap's ValidateState re-checks the computed root
|
||
// against the header, so a mismatch surfaces loudly instead of silently. When the
|
||
// state is already present (the common, non-equivocation path) this is a cheap
|
||
// HasState no-op. The WARN fires ONLY on the equivocation-commit-gap, so it doubles
|
||
// as the confirming instrumentation for that root cause.
|
||
if !bc.HasState(block.Root()) {
|
||
parent := bc.lastAccepted // parent == block.ParentHash(), asserted above
|
||
log.Warn("[ACCEPT-BACKSTOP] accepted block state not materialized; re-executing on parent",
|
||
"height", block.NumberU64(),
|
||
"acceptedHash", block.Hash(),
|
||
"acceptedRoot", block.Root(),
|
||
"parentHash", parent.Hash(),
|
||
"parentRoot", parent.Root(),
|
||
"canonicalAtHeight", canonical,
|
||
)
|
||
reRoot, err := bc.reprocessBlock(parent, block)
|
||
if err != nil {
|
||
return fmt.Errorf("accept: failed to materialize state for accepted block %s:%d: %w", block.Hash(), block.NumberU64(), err)
|
||
}
|
||
if reRoot != block.Root() {
|
||
return fmt.Errorf("accept: re-executed root %s != accepted header root %s for block %s:%d (non-deterministic state transition)",
|
||
reRoot.Hex(), block.Root().Hex(), block.Hash(), block.NumberU64())
|
||
}
|
||
log.Warn("[ACCEPT-BACKSTOP] re-materialized accepted block state",
|
||
"height", block.NumberU64(), "root", block.Root())
|
||
}
|
||
|
||
// Enqueue block in the acceptor
|
||
bc.lastAccepted = block
|
||
|
||
// [HEAD-STATE-PIN] Transfer the dedicated head pin to the new accepted head so
|
||
// its state root can never be GC'd/capped while it is the head — the durable
|
||
// fix for the idle-tip "missing trie node at head" wedge. Non-fatal by design
|
||
// (matching the acceptor's tolerance of backend "not supported"): a failed pin
|
||
// degrades to the pre-fix behavior instead of wedging Accept itself.
|
||
if err := bc.pinAcceptedHead(block.Root()); err != nil {
|
||
log.Warn("[HEAD-STATE-PIN] failed to pin accepted head state root", "hash", block.Hash(), "height", block.NumberU64(), "err", err)
|
||
}
|
||
|
||
bc.addAcceptorQueue(block)
|
||
acceptedBlockGasUsedCounter.Inc(int64(block.GasUsed()))
|
||
acceptedTxsCounter.Inc(int64(len(block.Transactions())))
|
||
if baseFee := block.BaseFee(); baseFee != nil {
|
||
lastAcceptedBlockBaseFeeGauge.Update(baseFee.Int64())
|
||
}
|
||
total, err := TotalFees(block, bc.GetReceiptsByHash(block.Hash()))
|
||
if err != nil {
|
||
log.Error(fmt.Sprintf("TotalFees error: %s", err))
|
||
} else {
|
||
blockTotalFeesGauge.Update(total.Int64())
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// TotalFees computes total consumed fees in wei. Block transactions and receipts have to have the same order.
|
||
func TotalFees(block *types.Block, receipts []*types.Receipt) (*big.Int, error) {
|
||
baseFee := block.BaseFee()
|
||
feesWei := new(big.Int)
|
||
if len(block.Transactions()) != len(receipts) {
|
||
return nil, errors.New("mismatch between total number of transactions and receipts")
|
||
}
|
||
for i, tx := range block.Transactions() {
|
||
var minerFee *big.Int
|
||
if baseFee == nil {
|
||
// legacy block, no baseFee
|
||
minerFee = tx.GasPrice()
|
||
} else {
|
||
effectiveTip, _ := tx.EffectiveGasTip(baseFee)
|
||
minerFee = new(big.Int).Add(baseFee, effectiveTip)
|
||
}
|
||
feesWei.Add(feesWei, new(big.Int).Mul(new(big.Int).SetUint64(receipts[i].GasUsed), minerFee))
|
||
}
|
||
return feesWei, nil
|
||
}
|
||
|
||
// TotalFees computes total consumed fees in ether. Block transactions and receipts have to have the same order.
|
||
func TotalFeesFloat(block *types.Block, receipts []*types.Receipt) (*big.Float, error) {
|
||
total, err := TotalFees(block, receipts)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return new(big.Float).Quo(new(big.Float).SetInt(total), new(big.Float).SetInt(big.NewInt(params.Ether))), nil
|
||
}
|
||
|
||
func (bc *BlockChain) Reject(block *types.Block) error {
|
||
bc.chainmu.Lock()
|
||
defer bc.chainmu.Unlock()
|
||
|
||
// A processing block may be the current canonical head even though consensus
|
||
// later rejects it. Deleting that block without first moving the durable head
|
||
// markers leaves ReadHeadBlockHash pointing at a body that no longer exists;
|
||
// the next restart then fails in loadLastState with "could not load head
|
||
// block". If the rejected block is canonical at its height, rewind the full
|
||
// processing head to the retained parent before deleting any data. This also
|
||
// removes canonical assignments for descendants of the rejected block.
|
||
if bc.GetCanonicalHash(block.NumberU64()) == block.Hash() {
|
||
if block.NumberU64() == 0 {
|
||
return errors.New("cannot reject the canonical genesis block")
|
||
}
|
||
if block.NumberU64() <= bc.lastAccepted.NumberU64() {
|
||
return fmt.Errorf("cannot reject canonical block %s:%d at or below last accepted height %d",
|
||
block.Hash(), block.NumberU64(), bc.lastAccepted.NumberU64())
|
||
}
|
||
parent := bc.GetBlock(block.ParentHash(), block.NumberU64()-1)
|
||
if parent == nil {
|
||
return fmt.Errorf("cannot reject canonical block %s:%d: parent %s is unavailable",
|
||
block.Hash(), block.NumberU64(), block.ParentHash())
|
||
}
|
||
if err := bc.setPreference(parent); err != nil {
|
||
return fmt.Errorf("cannot rewind canonical head before rejecting %s:%d: %w",
|
||
block.Hash(), block.NumberU64(), err)
|
||
}
|
||
}
|
||
|
||
// Reject Trie
|
||
if err := bc.stateManager.RejectTrie(block); err != nil {
|
||
return fmt.Errorf("unable to reject trie: %w", err)
|
||
}
|
||
|
||
if bc.snaps != nil {
|
||
if err := bc.snaps.Discard(block.Hash()); err != nil {
|
||
log.Error("unable to discard snap from rejected block", "block", block.Hash(), "number", block.NumberU64(), "root", block.Root())
|
||
}
|
||
}
|
||
|
||
// Remove the block since its data is no longer needed
|
||
batch := bc.db.NewBatch()
|
||
rawdb.DeleteBlock(batch, block.Hash(), block.NumberU64())
|
||
if err := batch.Write(); err != nil {
|
||
return fmt.Errorf("failed to write delete block batch: %w", err)
|
||
}
|
||
|
||
// Remove the block from the block cache (ignore return value of whether it was in the cache)
|
||
_ = bc.blockCache.Remove(block.Hash())
|
||
|
||
return nil
|
||
}
|
||
|
||
// writeKnownBlock updates the head block flag with a known block
|
||
// and introduces chain reorg if necessary.
|
||
func (bc *BlockChain) writeKnownBlock(block *types.Block) error {
|
||
current := bc.CurrentBlock()
|
||
if block.ParentHash() != current.Hash() {
|
||
if err := bc.reorg(current, block); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
bc.writeHeadBlock(block)
|
||
return nil
|
||
}
|
||
|
||
// writeCanonicalBlockWithLogs writes the new head [block] and emits events
|
||
// for the new head block.
|
||
func (bc *BlockChain) writeCanonicalBlockWithLogs(block *types.Block, logs []*types.Log) {
|
||
bc.writeHeadBlock(block)
|
||
bc.chainFeed.Send(ChainEvent{Block: block, Hash: block.Hash(), Logs: logs})
|
||
if len(logs) > 0 {
|
||
bc.logsFeed.Send(logs)
|
||
}
|
||
bc.chainHeadFeed.Send(ChainHeadEvent{Block: block})
|
||
}
|
||
|
||
// newTip returns a boolean indicating if the block should be appended to
|
||
// the canonical chain.
|
||
func (bc *BlockChain) newTip(block *types.Block) bool {
|
||
return block.ParentHash() == bc.CurrentBlock().Hash()
|
||
}
|
||
|
||
// writeBlockAndSetHead persists the block and associated state to the database
|
||
// and optimistically updates the canonical chain if [block] extends the current
|
||
// canonical chain.
|
||
// writeBlockAndSetHead expects to be the last verification step during InsertBlock
|
||
// since it creates a reference that will only be cleaned up by Accept/Reject.
|
||
func (bc *BlockChain) writeBlockAndSetHead(block *types.Block, parentRoot common.Hash, receipts []*types.Receipt, logs []*types.Log, state *state.StateDB) error {
|
||
if err := bc.writeBlockWithState(block, parentRoot, receipts, state); err != nil {
|
||
return err
|
||
}
|
||
|
||
// If [block] represents a new tip of the canonical chain, we optimistically add it before
|
||
// setPreference is called. Otherwise, we consider it a side chain block.
|
||
if bc.newTip(block) {
|
||
bc.writeCanonicalBlockWithLogs(block, logs)
|
||
} else {
|
||
bc.chainSideFeed.Send(ChainSideEvent{Block: block})
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// writeBlockWithState writes the block and all associated state to the database,
|
||
// but it expects the chain mutex to be held.
|
||
func (bc *BlockChain) writeBlockWithState(block *types.Block, parentRoot common.Hash, receipts []*types.Receipt, state *state.StateDB) error {
|
||
// Irrelevant of the canonical status, write the block itself to the database.
|
||
//
|
||
// Note all the components of block(hash->number map, header, body, receipts)
|
||
// should be written atomically. BlockBatch is used for containing all components.
|
||
blockBatch := bc.db.NewBatch()
|
||
customrawdb.WriteBlock(blockBatch, block)
|
||
rawdb.WriteReceipts(blockBatch, block.Hash(), block.NumberU64(), receipts)
|
||
rawdb.WritePreimages(blockBatch, state.Preimages())
|
||
if err := blockBatch.Write(); err != nil {
|
||
log.Crit("Failed to write block into disk", "err", err)
|
||
}
|
||
|
||
// Commit all cached state changes into underlying memory database.
|
||
_, err := bc.commitWithSnap(block, parentRoot, state)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
// If node is running in path mode, skip explicit gc operation
|
||
// which is unnecessary in this mode.
|
||
if bc.triedb.Scheme() == rawdb.PathScheme {
|
||
return nil
|
||
}
|
||
|
||
// Hash scheme: pin this block's state root in the triedb dirty cache.
|
||
// hashdb.Update references storage-trie roots but never the state root, so
|
||
// every processed root has parents==0. The StateHistory-deep tipBuffer
|
||
// Dereferences roots as they age out (and RejectTrie on reject); with
|
||
// duplicate roots on an idle chain that Dereference lands on the live head
|
||
// and evicts it — and at a non-CommitInterval height the head was never
|
||
// flushed to disk, yielding "missing trie node" at tip. This Reference is
|
||
// refcount-balanced (+1 here per processed block, -1 on the matching
|
||
// AcceptTrie eviction or RejectTrie) and restores the reference invariant
|
||
// documented at writeBlockAndSetHead's call site. It touches only in-memory
|
||
// GC pinning — never a state root, block hash, gas value, or on-disk byte.
|
||
if err := bc.triedb.Reference(block.Root(), common.Hash{}); err != nil {
|
||
return fmt.Errorf("failed to reference state root for block %s: %w", block.Hash().Hex(), err)
|
||
}
|
||
|
||
// Note: if InsertTrie must be the last step in verification that can return an error.
|
||
// This allows [stateManager] to assume that if it inserts a trie without returning an
|
||
// error then the block has passed verification and either AcceptTrie/RejectTrie will
|
||
// eventually be called on [root] unless a fatal error occurs. It does not assume that
|
||
// the node will not shutdown before either AcceptTrie/RejectTrie is called.
|
||
if err := bc.stateManager.InsertTrie(block); err != nil {
|
||
if bc.snaps != nil {
|
||
discardErr := bc.snaps.Discard(block.Hash())
|
||
if discardErr != nil {
|
||
log.Debug("failed to discard snapshot after being unable to insert block trie", "block", block.Hash(), "root", block.Root())
|
||
}
|
||
}
|
||
return err
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// InsertChain attempts to insert the given batch of blocks in to the canonical
|
||
// chain or, otherwise, create a fork. If an error is returned it will return
|
||
// the index number of the failing block as well an error describing what went
|
||
// wrong.
|
||
//
|
||
// After insertion is done, all accumulated events will be fired.
|
||
func (bc *BlockChain) InsertChain(chain types.Blocks) (int, error) {
|
||
// Sanity check that we have something meaningful to import
|
||
if len(chain) == 0 {
|
||
return 0, nil
|
||
}
|
||
|
||
bc.blockProcFeed.Send(true)
|
||
defer bc.blockProcFeed.Send(false)
|
||
|
||
// Do a sanity check that the provided chain is actually ordered and linked.
|
||
for i := 1; i < len(chain); i++ {
|
||
block, prev := chain[i], chain[i-1]
|
||
if block.NumberU64() != prev.NumberU64()+1 || block.ParentHash() != prev.Hash() {
|
||
log.Error("Non contiguous block insert",
|
||
"number", block.Number(),
|
||
"hash", block.Hash(),
|
||
"parent", block.ParentHash(),
|
||
"prevnumber", prev.Number(),
|
||
"prevhash", prev.Hash(),
|
||
)
|
||
return 0, fmt.Errorf("non contiguous insert: item %d is #%d [%x…], item %d is #%d [%x…] (parent [%x…])", i-1, prev.NumberU64(),
|
||
prev.Hash().Bytes()[:4], i, block.NumberU64(), block.Hash().Bytes()[:4], block.ParentHash().Bytes()[:4])
|
||
}
|
||
}
|
||
// Pre-checks passed, start the full block imports
|
||
bc.chainmu.Lock()
|
||
defer bc.chainmu.Unlock()
|
||
for n, block := range chain {
|
||
if err := bc.insertBlock(block, true); err != nil {
|
||
return n, err
|
||
}
|
||
}
|
||
|
||
return len(chain), nil
|
||
}
|
||
|
||
func (bc *BlockChain) InsertBlock(block *types.Block) error {
|
||
return bc.InsertBlockManual(block, true)
|
||
}
|
||
|
||
func (bc *BlockChain) InsertBlockManual(block *types.Block, writes bool) error {
|
||
bc.blockProcFeed.Send(true)
|
||
defer bc.blockProcFeed.Send(false)
|
||
|
||
bc.chainmu.Lock()
|
||
err := bc.insertBlock(block, writes)
|
||
bc.chainmu.Unlock()
|
||
|
||
return err
|
||
}
|
||
|
||
func (bc *BlockChain) insertBlock(block *types.Block, writes bool) error {
|
||
start := time.Now()
|
||
bc.senderCacher.Recover(types.MakeSigner(bc.chainConfig, block.Number(), block.Time()), block.Transactions())
|
||
blockSignatureRecoveryTimer.Inc(time.Since(start).Milliseconds())
|
||
|
||
substart := time.Now()
|
||
err := bc.engine.VerifyHeader(bc, block.Header())
|
||
if err == nil {
|
||
err = bc.validator.ValidateBody(block)
|
||
}
|
||
|
||
switch {
|
||
case errors.Is(err, ErrKnownBlock):
|
||
// even if the block is already known, we still need to generate the
|
||
// snapshot layer and add a reference to the triedb, so we re-execute
|
||
// the block. Note that insertBlock should only be called on a block
|
||
// once if it returns nil
|
||
if bc.newTip(block) {
|
||
log.Debug("Setting head to be known block", "number", block.Number(), "hash", block.Hash())
|
||
} else {
|
||
log.Debug("Reprocessing already known block", "number", block.Number(), "hash", block.Hash())
|
||
}
|
||
|
||
// If an ancestor has been pruned, then this block cannot be acceptable.
|
||
case errors.Is(err, consensus.ErrPrunedAncestor):
|
||
return errors.New("side chain insertion is not supported")
|
||
|
||
// Future blocks are not supported, but should not be reported, so we return an error
|
||
// early here
|
||
case errors.Is(err, consensus.ErrFutureBlock):
|
||
return errFutureBlockUnsupported
|
||
|
||
// Some other error occurred, abort
|
||
case err != nil:
|
||
bc.reportBlock(block, nil, err)
|
||
return err
|
||
}
|
||
blockContentValidationTimer.Inc(time.Since(substart).Milliseconds())
|
||
|
||
// No validation errors for the block
|
||
|
||
// Retrieve the parent block to determine which root to build state on
|
||
substart = time.Now()
|
||
parent := bc.GetHeader(block.ParentHash(), block.NumberU64()-1)
|
||
|
||
// Instantiate the statedb to use for processing transactions
|
||
//
|
||
// NOTE: Flattening a snapshot during block execution requires fetching state
|
||
// entries directly from the trie (much slower).
|
||
bc.flattenLock.Lock()
|
||
defer bc.flattenLock.Unlock()
|
||
statedb, err := state.New(parent.Root, bc.stateCache, bc.snaps)
|
||
if err != nil {
|
||
return fmt.Errorf("insert block %s:%d: initialize parent state %s: %w",
|
||
block.Hash(), block.NumberU64(), parent.Root, err)
|
||
}
|
||
blockStateInitTimer.Inc(time.Since(substart).Milliseconds())
|
||
|
||
// Enable prefetching to pull in trie node paths while processing transactions
|
||
statedb.StartPrefetcher("chain", nil, nil)
|
||
defer statedb.StopPrefetcher()
|
||
|
||
// Process block using the parent state as reference point
|
||
pstart := time.Now()
|
||
receipts, logs, usedGas, err := bc.processor.Process(block, parent, statedb, bc.vmConfig)
|
||
if serr := statedb.Error(); serr != nil {
|
||
log.Error("statedb error encountered", "err", serr, "number", block.Number(), "hash", block.Hash())
|
||
err := fmt.Errorf("insert block %s:%d: execute against parent state %s: %w",
|
||
block.Hash(), block.NumberU64(), parent.Root, serr)
|
||
bc.reportBlock(block, receipts, err)
|
||
return err
|
||
}
|
||
if err != nil {
|
||
bc.reportBlock(block, receipts, err)
|
||
return err
|
||
}
|
||
ptime := time.Since(pstart)
|
||
|
||
// Validate the state using the default validator
|
||
vstart := time.Now()
|
||
if err := bc.validator.ValidateState(block, statedb, receipts, usedGas); err != nil {
|
||
bc.reportBlock(block, receipts, err)
|
||
return err
|
||
}
|
||
vtime := time.Since(vstart)
|
||
|
||
// CROSS-CHAIN IMPORT AUTHENTICATION. A block may DECLARE, in its logs, that it
|
||
// consumed atomic shared-memory objects exported by a peer chain (the 0x9999 C<->D
|
||
// settlement seam). Execution binds value to the object bytes the TRANSACTION
|
||
// carried — it never reads shared memory, which is what makes the block replay
|
||
// byte-identically on a bootstrapping node. Proving those bytes are the real
|
||
// recorded object is therefore a BLOCK rule, and this is where it is enforced: a
|
||
// declaration that does not match shared memory REJECTS THE BLOCK rather than
|
||
// diverging a receipt.
|
||
//
|
||
// It runs on BOTH verify paths (writes=true from consensus, writes=false from the
|
||
// producer verifying its own block), so a forged import is refused before it is
|
||
// ever proposed. The hook is installed by the VM, which owns the engine-state gate
|
||
// — during bootstrap the read MUST be skipped, because a syncing node legitimately
|
||
// has not yet applied the peer chain's export (the same constraint the shared-memory
|
||
// layer documents on RemoveValue). Nil hook (no VM installed / no shared memory)
|
||
// disables the check.
|
||
if bc.atomicImportVerifier != nil {
|
||
if err := bc.atomicImportVerifier(block, logs); err != nil {
|
||
bc.reportBlock(block, receipts, err)
|
||
return err
|
||
}
|
||
}
|
||
|
||
// Metrics for per-operation StateDB timing (AccountReads, StorageReads, etc.)
|
||
// are not exposed by the current luxfi/geth StateDB and are omitted here.
|
||
// storageHashTimer.Inc(statedb.StorageHashes.Milliseconds()) // Storage hashes are complete(in validation)
|
||
// triehash := statedb.AccountHashes + statedb.StorageHashes // The time spent on tries hashing
|
||
// trieUpdate := statedb.AccountUpdates + statedb.StorageUpdates // The time spent on tries update
|
||
// trieRead := statedb.SnapshotAccountReads + statedb.AccountReads // The time spent on account read
|
||
var triehash, trieUpdate, trieRead time.Duration
|
||
// trieRead += statedb.SnapshotStorageReads + statedb.StorageReads // The time spent on storage read
|
||
blockExecutionTimer.Inc(ptime.Milliseconds()) // The time spent on EVM processing (approximate)
|
||
blockValidationTimer.Inc(vtime.Milliseconds()) // The time spent on block validation (approximate)
|
||
blockTrieOpsTimer.Inc((triehash + trieUpdate + trieRead).Milliseconds()) // The time spent on trie operations
|
||
|
||
// If [writes] are disabled, skip [writeBlockWithState] so that we do not write the block
|
||
// or the state trie to disk.
|
||
// Note: in pruning mode, this prevents us from generating a reference to the state root.
|
||
if !writes {
|
||
return nil
|
||
}
|
||
|
||
// Write the block to the chain and get the status.
|
||
// writeBlockWithState (called within writeBlockAndSethead) creates a reference that
|
||
// will be cleaned up in Accept/Reject so we need to ensure an error cannot occur
|
||
// later in verification, since that would cause the referenced root to never be dereferenced.
|
||
wstart := time.Now()
|
||
if err := bc.writeBlockAndSetHead(block, parent.Root, receipts, logs, statedb); err != nil {
|
||
return err
|
||
}
|
||
// Update the metrics touched during block commit
|
||
accountCommitTimer.Inc(statedb.AccountCommits.Milliseconds()) // Account commits are complete, we can mark them
|
||
storageCommitTimer.Inc(statedb.StorageCommits.Milliseconds()) // Storage commits are complete, we can mark them
|
||
snapshotCommitTimer.Inc(statedb.SnapshotCommits.Milliseconds()) // Snapshot commits are complete, we can mark them
|
||
triedbCommitTimer.Inc(statedb.TrieDBCommits.Milliseconds()) // Trie database commits are complete, we can mark them
|
||
blockWriteTimer.Inc((time.Since(wstart) - statedb.AccountCommits - statedb.StorageCommits - statedb.SnapshotCommits - statedb.TrieDBCommits).Milliseconds())
|
||
blockInsertTimer.Inc(time.Since(start).Milliseconds())
|
||
|
||
log.Debug("Inserted new block", "number", block.Number(), "hash", block.Hash(),
|
||
"parentHash", block.ParentHash(),
|
||
"uncles", len(block.Uncles()), "txs", len(block.Transactions()), "gas", block.GasUsed(),
|
||
"elapsed", common.PrettyDuration(time.Since(start)),
|
||
"root", block.Root(), "baseFeePerGas", block.BaseFee(), "blockGasCost", customtypes.BlockGasCost(block),
|
||
)
|
||
|
||
processedBlockGasUsedCounter.Inc(int64(block.GasUsed()))
|
||
processedTxsCounter.Inc(int64(block.Transactions().Len()))
|
||
processedLogsCounter.Inc(int64(len(logs)))
|
||
blockInsertCount.Inc(1)
|
||
return nil
|
||
}
|
||
|
||
// collectUnflattenedLogs collects the logs that were generated or removed during
|
||
// the processing of a block.
|
||
func (bc *BlockChain) collectUnflattenedLogs(b *types.Block, removed bool) [][]*types.Log {
|
||
var blobGasPrice *big.Int
|
||
excessBlobGas := b.ExcessBlobGas()
|
||
// Only calculate blob fee if Cancun fork is active AND block has ExcessBlobGas.
|
||
// Without the IsCancun check, CalcBlobFee panics with "calculating blob fee on unsupported fork"
|
||
// when blocks have ExcessBlobGas set but the chain config doesn't have Cancun enabled.
|
||
if excessBlobGas != nil && bc.chainConfig.IsCancun(b.Number(), b.Time()) {
|
||
blobGasPrice = eip4844.CalcBlobFee(bc.chainConfig, b.Header())
|
||
}
|
||
receipts := rawdb.ReadRawReceipts(bc.db, b.Hash(), b.NumberU64())
|
||
if err := receipts.DeriveFields(bc.chainConfig, b.Hash(), b.NumberU64(), b.Time(), b.BaseFee(), blobGasPrice, b.Transactions()); err != nil {
|
||
log.Error("Failed to derive block receipts fields", "hash", b.Hash(), "number", b.NumberU64(), "err", err)
|
||
}
|
||
|
||
// Note: gross but this needs to be initialized here because returning nil will be treated specially as an incorrect
|
||
// error case downstream.
|
||
logs := make([][]*types.Log, len(receipts))
|
||
for i, receipt := range receipts {
|
||
receiptLogs := make([]*types.Log, len(receipt.Logs))
|
||
for i, log := range receipt.Logs {
|
||
if removed {
|
||
log.Removed = true
|
||
}
|
||
receiptLogs[i] = log
|
||
}
|
||
logs[i] = receiptLogs
|
||
}
|
||
return logs
|
||
}
|
||
|
||
// collectLogs collects the logs that were generated or removed during
|
||
// the processing of a block. These logs are later announced as deleted or reborn.
|
||
func (bc *BlockChain) collectLogs(b *types.Block, removed bool) []*types.Log {
|
||
unflattenedLogs := bc.collectUnflattenedLogs(b, removed)
|
||
return customlogs.FlattenLogs(unflattenedLogs)
|
||
}
|
||
|
||
// reorg takes two blocks, an old chain and a new chain and will reconstruct the
|
||
// blocks and inserts them to be part of the new canonical chain and accumulates
|
||
// potential missing transactions and post an event about them.
|
||
func (bc *BlockChain) reorg(oldHead *types.Header, newHead *types.Block) error {
|
||
var (
|
||
newChain types.Blocks
|
||
oldChain types.Blocks
|
||
commonBlock *types.Block
|
||
)
|
||
oldBlock := bc.GetBlock(oldHead.Hash(), oldHead.Number.Uint64())
|
||
if oldBlock == nil {
|
||
return errors.New("current head block missing")
|
||
}
|
||
newBlock := newHead
|
||
|
||
// Reduce the longer chain to the same number as the shorter one
|
||
if oldBlock.NumberU64() > newBlock.NumberU64() {
|
||
// Old chain is longer, gather all transactions and logs as deleted ones
|
||
for ; oldBlock != nil && oldBlock.NumberU64() != newBlock.NumberU64(); oldBlock = bc.GetBlock(oldBlock.ParentHash(), oldBlock.NumberU64()-1) {
|
||
oldChain = append(oldChain, oldBlock)
|
||
}
|
||
} else {
|
||
// New chain is longer, stash all blocks away for subsequent insertion
|
||
for ; newBlock != nil && newBlock.NumberU64() != oldBlock.NumberU64(); newBlock = bc.GetBlock(newBlock.ParentHash(), newBlock.NumberU64()-1) {
|
||
newChain = append(newChain, newBlock)
|
||
}
|
||
}
|
||
if oldBlock == nil {
|
||
return errInvalidOldChain
|
||
}
|
||
if newBlock == nil {
|
||
return errInvalidNewChain
|
||
}
|
||
// Both sides of the reorg are at the same number, reduce both until the common
|
||
// ancestor is found
|
||
for {
|
||
// If the common ancestor was found, bail out
|
||
if oldBlock.Hash() == newBlock.Hash() {
|
||
commonBlock = oldBlock
|
||
break
|
||
}
|
||
// Remove an old block as well as stash away a new block
|
||
oldChain = append(oldChain, oldBlock)
|
||
newChain = append(newChain, newBlock)
|
||
|
||
// Step back with both chains
|
||
oldBlock = bc.GetBlock(oldBlock.ParentHash(), oldBlock.NumberU64()-1)
|
||
if oldBlock == nil {
|
||
return errInvalidOldChain
|
||
}
|
||
newBlock = bc.GetBlock(newBlock.ParentHash(), newBlock.NumberU64()-1)
|
||
if newBlock == nil {
|
||
return errInvalidNewChain
|
||
}
|
||
}
|
||
|
||
// If the commonBlock is less than the last accepted height, we return an error
|
||
// because performing a reorg would mean removing an accepted block from the
|
||
// canonical chain.
|
||
if commonBlock.NumberU64() < bc.lastAccepted.NumberU64() {
|
||
return fmt.Errorf("cannot orphan finalized block at height: %d to common block at height: %d", bc.lastAccepted.NumberU64(), commonBlock.NumberU64())
|
||
}
|
||
|
||
// Ensure the user sees large reorgs
|
||
if len(oldChain) > 0 && len(newChain) > 0 {
|
||
logFn := log.Info
|
||
msg := "Resetting chain preference"
|
||
if len(oldChain) > 63 {
|
||
msg = "Large chain preference change detected"
|
||
logFn = log.Warn
|
||
}
|
||
logFn(msg, "number", commonBlock.Number(), "hash", commonBlock.Hash(),
|
||
"drop", len(oldChain), "dropfrom", oldChain[0].Hash(), "add", len(newChain), "addfrom", newChain[0].Hash())
|
||
} else {
|
||
log.Debug("Preference change (rewind to ancestor) occurred", "oldnum", oldHead.Number, "oldhash", oldHead.Hash(), "newnum", newHead.Number(), "newhash", newHead.Hash())
|
||
}
|
||
// Reset the tx lookup cache in case to clear stale txlookups.
|
||
// This is done before writing any new chain data to avoid the
|
||
// weird scenario that canonical chain is changed while the
|
||
// stale lookups are still cached.
|
||
bc.txLookupCache.Purge()
|
||
|
||
// Insert the new chain(except the head block(reverse order)),
|
||
// taking care of the proper incremental order.
|
||
for i := len(newChain) - 1; i >= 1; i-- {
|
||
// Insert the block in the canonical way, re-writing history
|
||
bc.writeHeadBlock(newChain[i])
|
||
}
|
||
|
||
// Use the height of [newHead] to remove assignments invalidated when
|
||
// rewinding to a shorter chain.
|
||
if err := bc.deleteCanonicalHashesAbove(newHead.NumberU64()); err != nil {
|
||
return fmt.Errorf("failed to delete canonical assignments above %d: %w", newHead.NumberU64(), err)
|
||
}
|
||
|
||
// Send out events for logs from the old canon chain, and 'reborn'
|
||
// logs from the new canon chain. The number of logs can be very
|
||
// high, so the events are sent in batches of size around 512.
|
||
|
||
// Deleted logs + blocks:
|
||
var deletedLogs []*types.Log
|
||
for i := len(oldChain) - 1; i >= 0; i-- {
|
||
// Also send event for blocks removed from the canon chain.
|
||
bc.chainSideFeed.Send(ChainSideEvent{Block: oldChain[i]})
|
||
|
||
// Collect deleted logs for notification
|
||
if logs := bc.collectLogs(oldChain[i], true); len(logs) > 0 {
|
||
deletedLogs = append(deletedLogs, logs...)
|
||
}
|
||
if len(deletedLogs) > 512 {
|
||
bc.rmLogsFeed.Send(RemovedLogsEvent{deletedLogs})
|
||
deletedLogs = nil
|
||
}
|
||
}
|
||
if len(deletedLogs) > 0 {
|
||
bc.rmLogsFeed.Send(RemovedLogsEvent{deletedLogs})
|
||
}
|
||
|
||
// New logs:
|
||
var rebirthLogs []*types.Log
|
||
for i := len(newChain) - 1; i >= 1; i-- {
|
||
if logs := bc.collectLogs(newChain[i], false); len(logs) > 0 {
|
||
rebirthLogs = append(rebirthLogs, logs...)
|
||
}
|
||
if len(rebirthLogs) > 512 {
|
||
bc.logsFeed.Send(rebirthLogs)
|
||
rebirthLogs = nil
|
||
}
|
||
}
|
||
if len(rebirthLogs) > 0 {
|
||
bc.logsFeed.Send(rebirthLogs)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
type badBlock struct {
|
||
block *types.Block
|
||
reason *BadBlockReason
|
||
}
|
||
|
||
type BadBlockReason struct {
|
||
ChainConfig *params.ChainConfig `json:"chainConfig"`
|
||
Receipts types.Receipts `json:"receipts"`
|
||
Number uint64 `json:"number"`
|
||
Hash common.Hash `json:"hash"`
|
||
Error string `json:"error"`
|
||
}
|
||
|
||
func (b *BadBlockReason) String() string {
|
||
var receiptString string
|
||
for i, receipt := range b.Receipts {
|
||
receiptString += fmt.Sprintf("\n %d: cumulative: %v gas: %v contract: %v status: %v tx: %v logs: %v bloom: %x state: %x",
|
||
i, receipt.CumulativeGasUsed, receipt.GasUsed, receipt.ContractAddress.Hex(),
|
||
receipt.Status, receipt.TxHash.Hex(), receipt.Logs, receipt.Bloom, receipt.PostState)
|
||
}
|
||
version, vcs := version.Info()
|
||
platform := fmt.Sprintf("%s %s %s %s", version, stdruntime.Version(), stdruntime.GOARCH, stdruntime.GOOS)
|
||
if vcs != "" {
|
||
vcs = fmt.Sprintf("\nVCS: %s", vcs)
|
||
}
|
||
return fmt.Sprintf(`
|
||
########## BAD BLOCK #########
|
||
Block: %v (%#x)
|
||
Error: %v
|
||
Platform: %v%v
|
||
Chain config: %#v
|
||
Receipts: %v
|
||
##############################
|
||
`, b.Number, b.Hash, b.Error, platform, vcs, b.ChainConfig, receiptString)
|
||
}
|
||
|
||
// BadBlocks returns a list of the last 'bad blocks' that the client has seen on the network and the BadBlockReason
|
||
// that caused each to be reported as a bad block.
|
||
// BadBlocks ensures that the length of the blocks and the BadBlockReason slice have the same length.
|
||
func (bc *BlockChain) BadBlocks() ([]*types.Block, []*BadBlockReason) {
|
||
blocks := make([]*types.Block, 0, bc.badBlocks.Len())
|
||
reasons := make([]*BadBlockReason, 0, bc.badBlocks.Len())
|
||
for _, hash := range bc.badBlocks.Keys() {
|
||
if badBlk, exist := bc.badBlocks.Peek(hash); exist {
|
||
blocks = append(blocks, badBlk.block)
|
||
reasons = append(reasons, badBlk.reason)
|
||
}
|
||
}
|
||
return blocks, reasons
|
||
}
|
||
|
||
// addBadBlock adds a bad block to the bad-block LRU cache
|
||
func (bc *BlockChain) addBadBlock(block *types.Block, reason *BadBlockReason) {
|
||
bc.badBlocks.Add(block.Hash(), &badBlock{
|
||
block: block,
|
||
reason: reason,
|
||
})
|
||
}
|
||
|
||
// reportBlock logs a bad block error.
|
||
func (bc *BlockChain) reportBlock(block *types.Block, receipts types.Receipts, err error) {
|
||
reason := &BadBlockReason{
|
||
ChainConfig: bc.chainConfig,
|
||
Receipts: receipts,
|
||
Number: block.NumberU64(),
|
||
Hash: block.Hash(),
|
||
Error: err.Error(),
|
||
}
|
||
|
||
badBlockCounter.Inc(1)
|
||
bc.addBadBlock(block, reason)
|
||
log.Debug(reason.String())
|
||
}
|
||
|
||
// reprocessBlock reprocesses a previously accepted block. This is often used
|
||
// to regenerate previously pruned state tries.
|
||
func (bc *BlockChain) reprocessBlock(parent *types.Block, current *types.Block) (common.Hash, error) {
|
||
// Retrieve the parent block and its state to execute block
|
||
var (
|
||
statedb *state.StateDB
|
||
err error
|
||
parentRoot = parent.Root()
|
||
)
|
||
// We don't simply use [NewWithSnapshot] here because it doesn't return an
|
||
// error if [bc.snaps != nil] and [bc.snaps.Snapshot(parentRoot) == nil].
|
||
if bc.snaps == nil {
|
||
statedb, err = state.New(parentRoot, bc.stateCache, nil)
|
||
} else {
|
||
snap := bc.snaps.Snapshot(parentRoot)
|
||
if snap == nil {
|
||
return common.Hash{}, fmt.Errorf("failed to get snapshot for parent root: %s", parentRoot)
|
||
}
|
||
statedb, err = state.New(parentRoot, bc.stateCache, bc.snaps)
|
||
}
|
||
if err != nil {
|
||
return common.Hash{}, fmt.Errorf("could not fetch state for (%s: %d): %v", parent.Hash().Hex(), parent.NumberU64(), err)
|
||
}
|
||
|
||
// Enable prefetching to pull in trie node paths while processing transactions
|
||
statedb.StartPrefetcher("chain", nil, nil)
|
||
defer statedb.StopPrefetcher()
|
||
|
||
// Process previously stored block
|
||
receipts, _, usedGas, err := bc.processor.Process(current, parent.Header(), statedb, vm.Config{})
|
||
if err != nil {
|
||
return common.Hash{}, fmt.Errorf("failed to re-process block (%s: %d): %v", current.Hash().Hex(), current.NumberU64(), err)
|
||
}
|
||
|
||
// Validate the state using the default validator
|
||
if err := bc.validator.ValidateState(current, statedb, receipts, usedGas); err != nil {
|
||
return common.Hash{}, fmt.Errorf("failed to validate state while re-processing block (%s: %d): %v", current.Hash().Hex(), current.NumberU64(), err)
|
||
}
|
||
log.Debug("Processed block", "block", current.Hash(), "number", current.NumberU64())
|
||
|
||
// Commit all cached state changes into underlying memory database.
|
||
return bc.commitWithSnap(current, parentRoot, statedb)
|
||
}
|
||
|
||
func (bc *BlockChain) commitWithSnap(
|
||
current *types.Block, parentRoot common.Hash, statedb *state.StateDB,
|
||
) (common.Hash, error) {
|
||
// We pass through block hashes to the Update calls to ensure that we can uniquely
|
||
// identify states despite identical state roots.
|
||
snapshotOpt := snapshot.WithBlockHashes(current.Hash(), current.ParentHash())
|
||
root, err := statedb.Commit(
|
||
current.NumberU64(),
|
||
bc.chainConfig.IsEIP158(current.Number()),
|
||
false, // noStorageWiping
|
||
)
|
||
if err != nil {
|
||
return common.Hash{}, err
|
||
}
|
||
// Upstream does not perform a snapshot update if the root is the same as the
|
||
// parent root, however here the snapshots are based on the block hash, so
|
||
// this update is necessary. Note blockHashes are passed here as well.
|
||
if bc.snaps != nil && root == parentRoot {
|
||
if err := bc.snaps.Update(root, parentRoot, nil, nil, nil, snapshotOpt); err != nil {
|
||
return common.Hash{}, err
|
||
}
|
||
}
|
||
|
||
// Because Firewood relies on tracking block hashes in a tree, we need to notify the
|
||
// database that this block is empty.
|
||
if bc.CacheConfig().StateScheme == customrawdb.FirewoodScheme && root == parentRoot {
|
||
if err := bc.triedb.Update(root, parentRoot, current.NumberU64(), nil, nil); err != nil {
|
||
return common.Hash{}, fmt.Errorf("failed to update trie for block %s: %w", current.Hash(), err)
|
||
}
|
||
}
|
||
return root, nil
|
||
}
|
||
|
||
// initSnapshot instantiates a Snapshot instance and adds it to [bc]
|
||
func (bc *BlockChain) initSnapshot(b *types.Header) {
|
||
if bc.cacheConfig.SnapshotLimit <= 0 || bc.snaps != nil {
|
||
return
|
||
}
|
||
|
||
// If we are starting from genesis, generate the original snapshot disk layer
|
||
// up front, so we can use it while executing blocks in bootstrapping. This
|
||
// also avoids a costly async generation process when reaching tip.
|
||
//
|
||
// Additionally, we should always repair a snapshot if starting at genesis
|
||
// if [SnapshotLimit] > 0.
|
||
asyncBuild := !bc.cacheConfig.SnapshotWait && b.Number.Uint64() > 0
|
||
noBuild := bc.cacheConfig.SnapshotNoBuild && b.Number.Uint64() > 0
|
||
log.Info("Initializing snapshots", "async", asyncBuild, "rebuild", !noBuild, "headHash", b.Hash(), "headRoot", b.Root)
|
||
snapconfig := snapshot.Config{
|
||
CacheSize: bc.cacheConfig.SnapshotLimit,
|
||
NoBuild: noBuild,
|
||
AsyncBuild: asyncBuild,
|
||
SkipVerify: !bc.cacheConfig.SnapshotVerify,
|
||
}
|
||
var err error
|
||
bc.snaps, err = snapshot.New(snapconfig, bc.db, bc.triedb, b.Hash(), b.Root)
|
||
if err != nil {
|
||
log.Error("failed to initialize snapshots", "headHash", b.Hash(), "headRoot", b.Root, "err", err, "async", asyncBuild)
|
||
}
|
||
}
|
||
|
||
// reprocessState reprocesses the state up to [block], iterating through its ancestors until
|
||
// it reaches a block with a state committed to the database. reprocessState does not use
|
||
// snapshots since the disk layer for snapshots will most likely be above the last committed
|
||
// state that reprocessing will start from.
|
||
func (bc *BlockChain) reprocessState(current *types.Block, reexec uint64) error {
|
||
origin := current.NumberU64()
|
||
acceptorTip, err := customrawdb.ReadAcceptorTip(bc.db)
|
||
if err != nil {
|
||
return fmt.Errorf("%w: unable to get Acceptor tip", err)
|
||
}
|
||
log.Info("Loaded Acceptor tip", "hash", acceptorTip)
|
||
|
||
// The acceptor tip is up to date either if it matches the current hash, or it has not been
|
||
// initialized (i.e., this node has not accepted any blocks asynchronously).
|
||
acceptorTipUpToDate := acceptorTip == (common.Hash{}) || acceptorTip == current.Hash()
|
||
|
||
// If the state is already available and the acceptor tip is up to date, skip re-processing.
|
||
if bc.HasState(current.Root()) && acceptorTipUpToDate {
|
||
log.Info("Skipping state reprocessing", "root", current.Root())
|
||
return nil
|
||
}
|
||
|
||
// If the acceptorTip is a non-empty hash, jump re-processing back to the acceptor tip to ensure that
|
||
// we re-process at a minimum from the last processed accepted block.
|
||
// Note: we do not have a guarantee that the last trie on disk will be at a height <= acceptorTip.
|
||
// Since we need to re-process from at least the acceptorTip to ensure indices are updated correctly
|
||
// we must start searching for the block to start re-processing at the acceptorTip.
|
||
// This may occur if we are running in archive mode where every block's trie is committed on insertion
|
||
// or during an unclean shutdown.
|
||
if acceptorTip != (common.Hash{}) {
|
||
current = bc.GetBlockByHash(acceptorTip)
|
||
if current == nil {
|
||
return fmt.Errorf("failed to get block for acceptor tip %s", acceptorTip)
|
||
}
|
||
}
|
||
|
||
// Find a historic available state root
|
||
var hasState bool
|
||
for i := 0; i <= int(reexec); i++ {
|
||
if bc.HasState(current.Root()) {
|
||
hasState = true
|
||
break
|
||
}
|
||
|
||
if current.NumberU64() == 0 {
|
||
return errors.New("genesis state is missing")
|
||
}
|
||
parent := bc.GetBlock(current.ParentHash(), current.NumberU64()-1)
|
||
if parent == nil {
|
||
return fmt.Errorf("missing block %s:%d", current.ParentHash().Hex(), current.NumberU64()-1)
|
||
}
|
||
current = parent
|
||
}
|
||
if !hasState {
|
||
return fmt.Errorf("required historical state unavailable (reexec=%d)", reexec)
|
||
}
|
||
|
||
// State was available at historical point, regenerate
|
||
var (
|
||
start = time.Now()
|
||
logged time.Time
|
||
previousRoot common.Hash
|
||
triedb = bc.triedb
|
||
writeIndices bool
|
||
)
|
||
// Note: we add 1 since in each iteration, we attempt to re-execute the next block.
|
||
log.Info("Re-executing blocks to generate state for last accepted block", "from", current.NumberU64()+1, "to", origin)
|
||
var roots []common.Hash
|
||
for current.NumberU64() < origin {
|
||
// Print progress logs if long enough time elapsed
|
||
if time.Since(logged) > 8*time.Second {
|
||
log.Info("Regenerating historical state", "block", current.NumberU64()+1, "target", origin, "remaining", origin-current.NumberU64(), "elapsed", time.Since(start))
|
||
logged = time.Now()
|
||
}
|
||
|
||
// Retrieve the next block to regenerate and process it
|
||
parent := current
|
||
next := current.NumberU64() + 1
|
||
if current = bc.GetBlockByNumber(next); current == nil {
|
||
return fmt.Errorf("failed to retrieve block %d while re-generating state", next)
|
||
}
|
||
|
||
// Initialize snapshot if required (prevents full snapshot re-generation in
|
||
// the case of unclean shutdown)
|
||
if parent.Hash() == acceptorTip {
|
||
log.Info("Recovering snapshot", "hash", parent.Hash(), "index", parent.NumberU64())
|
||
// If snapshot initialization is delayed due to state sync, skip initializing snaps here
|
||
if !bc.cacheConfig.SnapshotDelayInit {
|
||
bc.initSnapshot(parent.Header())
|
||
}
|
||
writeIndices = true // Set [writeIndices] to true, so that the indices will be updated from the last accepted tip onwards.
|
||
}
|
||
|
||
// Reprocess next block using previously fetched data
|
||
root, err := bc.reprocessBlock(parent, current)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
roots = append(roots, root)
|
||
|
||
// Write any unsaved indices to disk
|
||
if writeIndices {
|
||
if err := bc.writeBlockAcceptedIndices(current); err != nil {
|
||
return fmt.Errorf("%w: failed to process accepted block indices", err)
|
||
}
|
||
}
|
||
|
||
// Flatten snapshot if initialized, holding a reference to the state root until the next block
|
||
// is processed.
|
||
if err := bc.flattenSnapshot(func() error {
|
||
if previousRoot != (common.Hash{}) && previousRoot != root {
|
||
triedb.Dereference(previousRoot)
|
||
}
|
||
previousRoot = root
|
||
return nil
|
||
}, current.Hash()); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
|
||
_, nodes, imgs := triedb.Size()
|
||
log.Info("Historical state regenerated", "block", current.NumberU64(), "elapsed", time.Since(start), "nodes", nodes, "preimages", imgs)
|
||
|
||
// Firewood requires processing each root individually.
|
||
if bc.CacheConfig().StateScheme == customrawdb.FirewoodScheme {
|
||
for _, root := range roots {
|
||
if err := triedb.Commit(root, true); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
if previousRoot != (common.Hash{}) {
|
||
return triedb.Commit(previousRoot, true)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (bc *BlockChain) protectTrieIndex() error {
|
||
if !bc.cacheConfig.Pruning {
|
||
return customrawdb.WritePruningDisabled(bc.db)
|
||
}
|
||
pruningDisabled, err := customrawdb.HasPruningDisabled(bc.db)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to check if the chain has been run with pruning disabled: %w", err)
|
||
}
|
||
if !pruningDisabled {
|
||
return nil
|
||
}
|
||
if !bc.cacheConfig.AllowMissingTries {
|
||
return ErrRefuseToCorruptArchiver
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// populateMissingTries iterates from [bc.cacheConfig.PopulateMissingTries] (defaults to 0)
|
||
// to [LastAcceptedBlock] and persists all tries to disk that are not already on disk. This is
|
||
// used to fill trie index gaps in an "archive" node without resyncing from scratch.
|
||
//
|
||
// NOTE: Assumes the genesis root and last accepted root are written to disk
|
||
func (bc *BlockChain) populateMissingTries() error {
|
||
if bc.cacheConfig.PopulateMissingTries == nil {
|
||
return nil
|
||
}
|
||
|
||
var (
|
||
lastAccepted = bc.LastAcceptedBlock().NumberU64()
|
||
startHeight = *bc.cacheConfig.PopulateMissingTries
|
||
startTime = time.Now()
|
||
logged time.Time
|
||
triedb = bc.triedb
|
||
missing = 0
|
||
)
|
||
|
||
// Do not allow the config to specify a starting point above the last accepted block.
|
||
if startHeight > lastAccepted {
|
||
return fmt.Errorf("cannot populate missing tries from a starting point (%d) > last accepted block (%d)", startHeight, lastAccepted)
|
||
}
|
||
|
||
// If we are starting from the genesis, increment the start height by 1 so we don't attempt to re-process
|
||
// the genesis block.
|
||
if startHeight == 0 {
|
||
startHeight += 1
|
||
}
|
||
parent := bc.GetBlockByNumber(startHeight - 1)
|
||
if parent == nil {
|
||
return fmt.Errorf("failed to fetch initial parent block for re-populate missing tries at height %d", startHeight-1)
|
||
}
|
||
|
||
it := newBlockChainIterator(bc, startHeight, bc.cacheConfig.PopulateMissingTriesParallelism)
|
||
defer it.Stop()
|
||
|
||
for i := startHeight; i < lastAccepted; i++ {
|
||
// Print progress logs if long enough time elapsed
|
||
if time.Since(logged) > 8*time.Second {
|
||
log.Info("Populating missing tries", "missing", missing, "block", i, "remaining", lastAccepted-i, "elapsed", time.Since(startTime))
|
||
logged = time.Now()
|
||
}
|
||
|
||
current, hasState, err := it.Next(context.TODO())
|
||
if err != nil {
|
||
return fmt.Errorf("error while populating missing tries: %w", err)
|
||
}
|
||
|
||
if hasState {
|
||
parent = current
|
||
continue
|
||
}
|
||
|
||
root, err := bc.reprocessBlock(parent, current)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// Commit root to disk so that it can be accessed directly
|
||
if err := triedb.Commit(root, false); err != nil {
|
||
return err
|
||
}
|
||
parent = current
|
||
log.Debug("Populated missing trie", "block", current.NumberU64(), "root", root)
|
||
missing++
|
||
}
|
||
|
||
// Write marker to DB to indicate populate missing tries finished successfully.
|
||
// Note: writing the marker here means that we do allow consecutive runs of re-populating
|
||
// missing tries if it does not finish during the prior run.
|
||
if err := customrawdb.WritePopulateMissingTries(bc.db); err != nil {
|
||
return fmt.Errorf("failed to write offline pruning success marker: %w", err)
|
||
}
|
||
|
||
_, nodes, imgs := triedb.Size()
|
||
log.Info("All missing tries populated", "startHeight", startHeight, "lastAcceptedHeight", lastAccepted, "missing", missing, "elapsed", time.Since(startTime), "nodes", nodes, "preimages", imgs)
|
||
return nil
|
||
}
|
||
|
||
// CleanBlockRootsAboveLastAccepted gathers the blocks that may have previously been in processing above the
|
||
// last accepted block and wipes their block roots from disk to mark their tries as inaccessible.
|
||
// This is used prior to pruning to ensure that all of the tries that may still be in processing are marked
|
||
// as inaccessible and mirrors the handling of middle roots in the geth offline pruning implementation.
|
||
// This is not strictly necessary, but maintains a soft assumption.
|
||
func (bc *BlockChain) CleanBlockRootsAboveLastAccepted() error {
|
||
targetRoot := bc.LastAcceptedBlock().Root()
|
||
|
||
// Clean up any block roots above the last accepted block before we start pruning.
|
||
// Note: this takes the place of middleRoots in the geth implementation since we do not
|
||
// track processing block roots via snapshot journals in the same way.
|
||
processingRoots := bc.gatherBlockRootsAboveLastAccepted()
|
||
// If there is a block above the last accepted block with an identical state root, we
|
||
// explicitly remove it from the set to ensure we do not corrupt the last accepted trie.
|
||
delete(processingRoots, targetRoot)
|
||
for processingRoot := range processingRoots {
|
||
// Delete the processing root from disk to mark the trie as inaccessible (no need to handle this in a batch).
|
||
if err := bc.db.Delete(processingRoot[:]); err != nil {
|
||
return fmt.Errorf("failed to remove processing root (%s) preparing for offline pruning: %w", processingRoot, err)
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// gatherBlockRootsAboveLastAccepted iterates forward from the last accepted block and returns a list of all block roots
|
||
// for any blocks that were inserted above the last accepted block.
|
||
// Given that we never insert a block into the chain unless all of its ancestors have been inserted, this should gather
|
||
// all of the block roots for blocks inserted above the last accepted block that may have been in processing at some point
|
||
// in the past and are therefore potentially still acceptable.
|
||
// Note: there is an edge case where the node dies while the consensus engine is rejecting a branch of blocks since the
|
||
// consensus engine will reject the lowest ancestor first. In this case, these blocks will not be considered acceptable in
|
||
// the future.
|
||
// Ex.
|
||
//
|
||
// A
|
||
// / \
|
||
// B C
|
||
// |
|
||
// D
|
||
// |
|
||
// E
|
||
// |
|
||
// F
|
||
//
|
||
// The consensus engine accepts block C and proceeds to reject the other branch in order (B, D, E, F).
|
||
// If the consensus engine dies after rejecting block D, block D will be deleted, such that the forward iteration
|
||
// may not find any blocks at this height and will not reach the previously processing blocks E and F.
|
||
func (bc *BlockChain) gatherBlockRootsAboveLastAccepted() map[common.Hash]struct{} {
|
||
blockRoots := make(map[common.Hash]struct{})
|
||
for height := bc.lastAccepted.NumberU64() + 1; ; height++ {
|
||
blockHashes := rawdb.ReadAllHashes(bc.db, height)
|
||
// If there are no block hashes at [height], then there should be no further acceptable blocks
|
||
// past this point.
|
||
if len(blockHashes) == 0 {
|
||
break
|
||
}
|
||
|
||
// Fetch the blocks and append their roots.
|
||
for _, blockHash := range blockHashes {
|
||
block := bc.GetBlockByHash(blockHash)
|
||
if block == nil {
|
||
continue
|
||
}
|
||
|
||
blockRoots[block.Root()] = struct{}{}
|
||
}
|
||
}
|
||
|
||
return blockRoots
|
||
}
|
||
|
||
// ResetToStateSyncedBlock reinitializes the state of the blockchain
|
||
// to the trie represented by [block.Root()] after updating
|
||
// in-memory and on disk current block pointers to [block].
|
||
// Only should be called after state sync has completed.
|
||
func (bc *BlockChain) ResetToStateSyncedBlock(block *types.Block) error {
|
||
bc.chainmu.Lock()
|
||
defer bc.chainmu.Unlock()
|
||
|
||
// Update head block and snapshot pointers on disk
|
||
batch := bc.db.NewBatch()
|
||
if err := bc.batchBlockAcceptedIndices(batch, block); err != nil {
|
||
return err
|
||
}
|
||
rawdb.WriteHeadBlockHash(batch, block.Hash())
|
||
rawdb.WriteHeadHeaderHash(batch, block.Hash())
|
||
customrawdb.WriteSnapshotBlockHash(batch, block.Hash())
|
||
rawdb.WriteSnapshotRoot(batch, block.Root())
|
||
if err := customrawdb.WriteSyncPerformed(batch, block.NumberU64()); err != nil {
|
||
return err
|
||
}
|
||
|
||
if err := batch.Write(); err != nil {
|
||
return err
|
||
}
|
||
|
||
// if txlookup limit is 0 (uindexing disabled), we don't need to repair the tx index tail.
|
||
if bc.cacheConfig.TransactionHistory != 0 {
|
||
bc.repairTxIndexTail(block.NumberU64())
|
||
}
|
||
|
||
// Update all in-memory chain markers
|
||
bc.lastAccepted = block
|
||
bc.acceptorTip = block
|
||
bc.currentBlock.Store(block.Header())
|
||
bc.hc.SetCurrentHeader(block.Header())
|
||
|
||
lastAcceptedHash := block.Hash()
|
||
bc.stateCache = state.NewDatabaseWithNodeDB(bc.db, bc.triedb)
|
||
|
||
if err := bc.loadLastState(lastAcceptedHash); err != nil {
|
||
return err
|
||
}
|
||
// Create the state manager
|
||
bc.stateManager = NewTrieWriter(bc.triedb, bc.cacheConfig)
|
||
|
||
// Make sure the state associated with the block is available
|
||
head := bc.CurrentBlock()
|
||
if !bc.HasState(head.Root) {
|
||
return fmt.Errorf("head state missing %d:%s", head.Number, head.Hash())
|
||
}
|
||
|
||
bc.initSnapshot(head)
|
||
return nil
|
||
}
|
||
|
||
// CacheConfig returns a reference to [bc.cacheConfig]
|
||
//
|
||
// This is used by [miner] to set prefetch parallelism
|
||
// during block building.
|
||
func (bc *BlockChain) CacheConfig() *CacheConfig {
|
||
return bc.cacheConfig
|
||
}
|
||
|
||
func (bc *BlockChain) repairTxIndexTail(newTail uint64) error {
|
||
bc.txIndexTailLock.Lock()
|
||
defer bc.txIndexTailLock.Unlock()
|
||
|
||
if curr := rawdb.ReadTxIndexTail(bc.db); curr == nil || *curr < newTail {
|
||
log.Info("Repairing tx index tail", "old", curr, "new", newTail)
|
||
rawdb.WriteTxIndexTail(bc.db, newTail)
|
||
}
|
||
return nil
|
||
}
|