mirror of
https://github.com/luxfi/evm.git
synced 2026-09-24 00:27:44 +00:00
Track the upstream rename across the evm warp backend
(warp/{service,backend,verifier_backend}) and the warp precompile
(precompile/contracts/warp/{config,contract,contract_warp_handler},
precompileconfig): SignedCore->Core, WarpEnvelope->Envelope.
Identifiers only — the digest D, ZAP c14n bytes and BLS Beam / Corona Pulse /
ML-DSA signing domains are unchanged. ./warp/... and
./precompile/contracts/warp/... green, including the 10k-validator predicate
sign->aggregate->verify.
2225 lines
84 KiB
Go
2225 lines
84 KiB
Go
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||
// See the file LICENSE for licensing terms.
|
||
|
||
package evm
|
||
|
||
import (
|
||
"bytes"
|
||
"context"
|
||
"encoding/binary"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"math/big"
|
||
"net/http"
|
||
"os"
|
||
"path/filepath"
|
||
goruntime "runtime"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
|
||
"github.com/luxfi/precompile/dex"
|
||
|
||
"github.com/luxfi/cache/lru"
|
||
"github.com/luxfi/cache/metercacher"
|
||
"github.com/luxfi/p2p"
|
||
"github.com/luxfi/p2p/gossip"
|
||
luxwarp "github.com/luxfi/warp"
|
||
|
||
// "github.com/luxfi/firewood-go-ethhash/ffi"
|
||
"github.com/luxfi/metric"
|
||
|
||
"github.com/luxfi/constants"
|
||
"github.com/luxfi/evm/commontype"
|
||
"github.com/luxfi/evm/consensus/dummy"
|
||
"github.com/luxfi/evm/core"
|
||
"github.com/luxfi/evm/core/txpool"
|
||
"github.com/luxfi/evm/eth"
|
||
"github.com/luxfi/evm/eth/ethconfig"
|
||
evmmetrics "github.com/luxfi/evm/metrics/gatherer"
|
||
"github.com/luxfi/evm/miner"
|
||
"github.com/luxfi/evm/network"
|
||
"github.com/luxfi/evm/node"
|
||
"github.com/luxfi/evm/params"
|
||
"github.com/luxfi/evm/params/extras"
|
||
"github.com/luxfi/evm/plugin/evm/config"
|
||
gossipHandler "github.com/luxfi/evm/plugin/evm/gossip"
|
||
"github.com/luxfi/evm/plugin/evm/message"
|
||
"github.com/luxfi/evm/plugin/evm/validators"
|
||
"github.com/luxfi/evm/plugin/evm/validators/interfaces"
|
||
"github.com/luxfi/geth/core/rawdb"
|
||
"github.com/luxfi/geth/core/types"
|
||
"github.com/luxfi/geth/metrics"
|
||
"github.com/luxfi/geth/triedb"
|
||
"github.com/luxfi/geth/triedb/hashdb"
|
||
|
||
warpcontract "github.com/luxfi/evm/precompile/contracts/warp"
|
||
"github.com/luxfi/evm/precompile/modules"
|
||
"github.com/luxfi/evm/rpc"
|
||
statesyncclient "github.com/luxfi/evm/sync/client"
|
||
"github.com/luxfi/evm/sync/client/stats"
|
||
"github.com/luxfi/evm/utils"
|
||
"github.com/luxfi/evm/warp"
|
||
|
||
"github.com/luxfi/evm/plugin/evm/customtypes"
|
||
customheader "github.com/luxfi/evm/plugin/evm/header"
|
||
|
||
// Force-load tracer engine to trigger registration
|
||
//
|
||
// We must import this package (not referenced elsewhere) so that the native "callTracer"
|
||
// is added to a map of client-accessible tracers. In geth, this is done
|
||
// inside of cmd/geth.
|
||
_ "github.com/luxfi/geth/eth/tracers/js"
|
||
_ "github.com/luxfi/geth/eth/tracers/native"
|
||
|
||
"github.com/luxfi/evm/precompile/precompileconfig"
|
||
// Force-load precompiles to trigger registration
|
||
_ "github.com/luxfi/evm/precompile/registry"
|
||
|
||
"github.com/luxfi/geth/common"
|
||
"github.com/luxfi/geth/ethdb"
|
||
"github.com/luxfi/geth/rlp"
|
||
log "github.com/luxfi/log"
|
||
|
||
luxRPC "github.com/gorilla/rpc/v2"
|
||
|
||
"github.com/luxfi/consensus/engine/chain/block"
|
||
consensusmockable "github.com/luxfi/consensus/utils/timer/mockable"
|
||
"github.com/luxfi/database/versiondb"
|
||
"github.com/luxfi/filesystem/perms"
|
||
"github.com/luxfi/ids"
|
||
"github.com/luxfi/metric/profiler"
|
||
"github.com/luxfi/runtime"
|
||
nodemockable "github.com/luxfi/timer/mockable"
|
||
"github.com/luxfi/upgrade"
|
||
p2pversion "github.com/luxfi/version"
|
||
nodeblock "github.com/luxfi/vm/chain"
|
||
"github.com/luxfi/vm/chains/atomic"
|
||
nodeChain "github.com/luxfi/vm/components/chain"
|
||
|
||
"github.com/luxfi/database"
|
||
luxJSON "github.com/luxfi/evm/utils/json"
|
||
luxUtils "github.com/luxfi/utils"
|
||
)
|
||
|
||
var (
|
||
// Interface compatibility resolved with node v1.16.15
|
||
_ nodeblock.ChainVM = (*VM)(nil)
|
||
_ nodeblock.BuildBlockWithContextChainVM = (*VM)(nil)
|
||
_ nodeblock.StateSyncableVM = (*VM)(nil)
|
||
_ statesyncclient.EthBlockParser = (*VM)(nil)
|
||
)
|
||
|
||
const (
|
||
// Max time from current time allowed for blocks, before they're considered future blocks
|
||
// and fail verification
|
||
maxFutureBlockTime = 10 * time.Second
|
||
decidedCacheSize = 10 * constants.MiB
|
||
missingCacheSize = 50
|
||
unverifiedCacheSize = 5 * constants.MiB
|
||
bytesToIDCacheSize = 5 * constants.MiB
|
||
warpSignatureCacheSize = 500
|
||
|
||
// Prefixes for metrics gatherers
|
||
ethMetricsPrefix = "eth"
|
||
sdkMetricsPrefix = "sdk"
|
||
chainStateMetricsPrefix = "chain_state"
|
||
|
||
// TxGossipHandlerID is the handler ID for transaction gossip
|
||
TxGossipHandlerID = uint64(0x1)
|
||
)
|
||
|
||
// Define the API endpoints for the VM
|
||
const (
|
||
adminEndpoint = "/admin"
|
||
ethRPCEndpoint = "/rpc"
|
||
ethWSEndpoint = "/ws"
|
||
validatorsEndpoint = "/validators"
|
||
ethTxGossipNamespace = "eth_tx_gossip"
|
||
)
|
||
|
||
var (
|
||
// Set last accepted key to be longer than the keys used to store accepted block IDs.
|
||
lastAcceptedKey = []byte("last_accepted_key")
|
||
acceptedPrefix = []byte("chain_accepted")
|
||
metadataPrefix = []byte("metadata")
|
||
warpPrefix = []byte("warp")
|
||
// dexAtomicSeqKey stores the node-local high-water staged-atomic seq the DEX
|
||
// native seam has flushed to shared memory. It is a crash/idempotency optimization
|
||
// ONLY — the deterministic flush set is derived from consensus state (parent ->
|
||
// accepted staged-seq); this marker just lets a restarted node skip already-applied
|
||
// windows. Written in the SAME versiondb batch as the block accept.
|
||
dexAtomicSeqKey = []byte("dex_atomic_flushed_seq")
|
||
ethDBPrefix = []byte("ethdb")
|
||
validatorsDBPrefix = []byte("validators")
|
||
)
|
||
|
||
var (
|
||
errEmptyBlock = errors.New("empty block")
|
||
errUnsupportedFXs = errors.New("unsupported feature extensions")
|
||
errInvalidBlock = errors.New("invalid block")
|
||
errInvalidNonce = errors.New("invalid nonce")
|
||
errUnclesUnsupported = errors.New("uncles unsupported")
|
||
errNilBaseFeeEVM = errors.New("nil base fee is invalid after EVM activation")
|
||
errNilBlockGasCostEVM = errors.New("nil blockGasCost is invalid after EVM activation")
|
||
errInvalidHeaderPredicateResults = errors.New("invalid header predicate results")
|
||
errInitializingLogger = errors.New("failed to initialize logger")
|
||
errShuttingDownVM = errors.New("shutting down VM")
|
||
)
|
||
|
||
// Package-level debug logging — routes through the canonical luxfi/log
|
||
// logger at Debug level. No more /tmp/evm-debug.log sidecar.
|
||
func debugLog(msg string, args ...interface{}) {
|
||
log.Debug(fmt.Sprintf(msg, args...))
|
||
}
|
||
|
||
// resolveStateScheme normalises the operator-supplied state scheme into the
|
||
// concrete scheme this VM will run with, refusing schemes it does not support.
|
||
//
|
||
// History (lux-mainnet 2026-06-02): when the operator does not pick a scheme,
|
||
// geth's rawdb.ParseStateScheme (called via customrawdb.ParseStateSchemeExt
|
||
// from eth/backend.go) silently returns "path" for any database with no
|
||
// previously-stored scheme — which is the case for every freshly-created L2
|
||
// EVM chain. That collided with this VM's hard refusal of path mode, but only
|
||
// AFTER chain creation had already plumbed pathdb into the genesis-commit
|
||
// path. The chain then panicked with:
|
||
//
|
||
// panic in eth.New: unable to commit genesis block to statedb:
|
||
// triedb parent [0x56e81f17…] layer missing
|
||
//
|
||
// taking down hanzo/zoo/spc/pars L2s. The fix is to lock the default to
|
||
// HashScheme HERE — before backend.go can inherit the upstream "path"
|
||
// default — so the VM-level refusal is enforced and chain creation succeeds.
|
||
func resolveStateScheme(provided string) (string, error) {
|
||
switch provided {
|
||
case rawdb.PathScheme:
|
||
return "", errors.New("Path state scheme is not supported. Please use HashDB state scheme instead")
|
||
case "":
|
||
// No scheme requested → use HashScheme. Do not rely on geth's empty-DB
|
||
// default, which is path-by-default and breaks fresh L2 chain creation.
|
||
return rawdb.HashScheme, nil
|
||
default:
|
||
return provided, nil
|
||
}
|
||
}
|
||
|
||
// legacyApiNames maps pre geth v1.10.20 api names to their updated counterparts.
|
||
// used in attachEthService for backward configuration compatibility.
|
||
var legacyApiNames = map[string]string{
|
||
"internal-public-eth": "internal-eth",
|
||
"internal-public-blockchain": "internal-blockchain",
|
||
"internal-public-transaction-pool": "internal-transaction",
|
||
"internal-public-tx-pool": "internal-tx-pool",
|
||
"internal-public-debug": "internal-debug",
|
||
"internal-private-debug": "internal-debug",
|
||
"internal-public-account": "internal-account",
|
||
"internal-private-personal": "internal-personal",
|
||
|
||
"public-eth": "eth",
|
||
"public-eth-filter": "eth-filter",
|
||
"private-admin": "admin",
|
||
"public-debug": "debug",
|
||
"private-debug": "debug",
|
||
}
|
||
|
||
// Note: AppSender is now an alias for p2p.Sender, so no wrapper is needed.
|
||
// The node passes a p2p.Sender directly via RPC.
|
||
// warp.Signer is used directly from luxfi/warp - no adapters needed.
|
||
|
||
type VM struct {
|
||
ctx context.Context // stdlib context for cancellation
|
||
runtime *runtime.Runtime // chain wiring (IDs, validators, etc.)
|
||
// contextLock is used to coordinate global VM operations.
|
||
// This can be used safely instead of context.Context.Lock which is deprecated and should not be used in rpcchainvm.
|
||
vmLock sync.RWMutex
|
||
// [cancel] may be nil until [consensus.NormalOp] starts
|
||
cancel context.CancelFunc
|
||
// *nodeChain.State helps to implement the VM interface by wrapping blocks
|
||
// with an efficient caching layer.
|
||
*nodeChain.State
|
||
|
||
config config.Config
|
||
|
||
genesisHash common.Hash
|
||
chainConfig *params.ChainConfig
|
||
ethConfig ethconfig.Config
|
||
|
||
// pointers to eth constructs
|
||
eth *eth.Ethereum
|
||
txPool *txpool.TxPool
|
||
blockChain *core.BlockChain
|
||
miner *miner.Miner
|
||
|
||
// [versiondb] is the VM's current versioned database
|
||
versiondb *versiondb.Database
|
||
|
||
// [db] is the VM's current database
|
||
db database.Database
|
||
|
||
// metadataDB is used to store one off keys.
|
||
metadataDB database.Database
|
||
|
||
// [chaindb] is the database supplied to the Ethereum backend
|
||
chaindb ethdb.Database
|
||
|
||
usingStandaloneDB bool
|
||
|
||
// [acceptedBlockDB] is the database to store the last accepted
|
||
// block.
|
||
acceptedBlockDB database.Database
|
||
// [warpDB] is used to store warp message signatures
|
||
// set to a prefixDB with the prefix [warpPrefix]
|
||
warpDB database.Database
|
||
|
||
validatorsDB database.Database
|
||
|
||
syntacticBlockValidator BlockValidator
|
||
|
||
// builderLock is used to synchronize access to the block builder,
|
||
// as it is uninitialized at first and is only initialized when onNormalOperationsStarted is called.
|
||
builderLock sync.Mutex
|
||
builder *blockBuilder
|
||
|
||
clock nodemockable.Clock
|
||
consensusClock consensusmockable.Clock
|
||
|
||
shutdownChan chan struct{}
|
||
shutdownWg sync.WaitGroup
|
||
|
||
// Continuous Profiler
|
||
profiler profiler.ContinuousProfiler
|
||
|
||
Network *network.Network
|
||
networkCodec message.Manager
|
||
|
||
// Metrics
|
||
sdkMetrics metric.Registry
|
||
|
||
bootstrapped luxUtils.Atomic[bool]
|
||
|
||
stateSyncDone chan struct{}
|
||
|
||
logger log.Logger
|
||
// State sync server and client
|
||
StateSyncServer
|
||
StateSyncClient
|
||
|
||
// Lux Warp Messaging backend
|
||
// Used to serve BLS signatures of warp messages over RPC
|
||
warpBackend warp.Backend
|
||
|
||
// Initialize only sets these if nil so they can be overridden in tests
|
||
p2pValidators *p2p.Validators
|
||
ethTxGossipHandler p2p.Handler
|
||
ethTxPushGossiper luxUtils.Atomic[*gossip.PushGossiper[*GossipEthTx]]
|
||
ethTxPullGossiper gossip.Gossiper
|
||
|
||
validatorsManager interfaces.Manager
|
||
|
||
chainAlias string
|
||
// RPC handlers (should be stopped before closing chaindb)
|
||
rpcHandlers []interface{ Stop() }
|
||
}
|
||
|
||
// ParseBlock implements nodeblock.ChainVM interface
|
||
func (vm *VM) ParseBlock(ctx context.Context, b []byte) (nodeblock.Block, error) {
|
||
// Call the embedded State's ParseBlock and convert the result
|
||
blk, err := vm.State.ParseBlock(ctx, b)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return blk, nil
|
||
}
|
||
|
||
// Initialize implements the chain.ChainVM interface
|
||
func (vm *VM) Initialize(ctx context.Context, init block.Init) error {
|
||
// Store runtime for chain wiring (IDs, validators, etc.)
|
||
vm.runtime = init.Runtime
|
||
vm.ctx = ctx
|
||
|
||
// Get database and other init params
|
||
db := init.DB
|
||
genesisBytes := init.Genesis
|
||
upgradeBytes := init.Upgrade
|
||
configBytes := init.Config
|
||
vm.stateSyncDone = make(chan struct{})
|
||
vm.config.SetDefaults(defaultTxPoolConfig)
|
||
if len(configBytes) > 0 {
|
||
if err := json.Unmarshal(configBytes, &vm.config); err != nil {
|
||
return fmt.Errorf("failed to unmarshal config %s: %w", string(configBytes), err)
|
||
}
|
||
}
|
||
if err := vm.config.Validate(); err != nil {
|
||
return err
|
||
}
|
||
|
||
// The 0x9999 native DEX is ALWAYS-ON (live since the Dec 25 2025 precompile activation).
|
||
// There is no value-activation GATE to bind here: a swap's fail-closed controls (real-
|
||
// asset registry admission, the live-code verifier, the per-swap min-out / price floor,
|
||
// the halt switches, the non-reentrant custody mutex) are intrinsic to the swap and apply
|
||
// on every call regardless of consensus mode. No runtime flag turns the money path on or
|
||
// off. The LIVE WIRING of the value path's trust root (the registry-backed asset resolver
|
||
// + the local in-process D-Chain client) is installed below, once the chain context is
|
||
// known (after vm.chainConfig is set) — see installDEXValuePath in dex_value_wiring.go.
|
||
|
||
// We should deprecate config flags as the first thing, before we do anything else
|
||
// because this can set old flags to new flags. log the message after we have
|
||
// initialized the logger.
|
||
deprecateMsg := vm.config.Deprecate()
|
||
|
||
// Use ChainID from runtime for alias
|
||
var alias string
|
||
if vm.runtime != nil && vm.runtime.ChainID != ids.Empty {
|
||
alias = vm.runtime.ChainID.String()
|
||
} else {
|
||
alias = "evm"
|
||
}
|
||
vm.chainAlias = alias
|
||
|
||
// Create a logger since consensus Context doesn't expose a Log field.
|
||
contextLogger := log.New()
|
||
logWriter := newLoggerWriter(contextLogger)
|
||
evmLogger, err := log.InitLogger(vm.chainAlias, vm.config.LogLevel, vm.config.LogJSONFormat, logWriter)
|
||
if err != nil {
|
||
return fmt.Errorf("%w: %w ", errInitializingLogger, err)
|
||
}
|
||
vm.logger = evmLogger
|
||
|
||
log.Info("Initializing Chain EVM VM", "Version", Version, "geth version", params.VersionWithMeta, "Config", vm.config)
|
||
|
||
if deprecateMsg != "" {
|
||
log.Warn("Deprecation Warning", "msg", deprecateMsg)
|
||
}
|
||
|
||
if len(init.Fx) > 0 {
|
||
return errUnsupportedFXs
|
||
}
|
||
|
||
// Enable debug-level metrics that might impact runtime performance
|
||
// Note: metrics.EnabledExpensive is not a global in our geth fork
|
||
// Expensive metrics configuration handled via config
|
||
|
||
vm.shutdownChan = make(chan struct{}, 1)
|
||
|
||
if err := vm.initializeMetrics(); err != nil {
|
||
return fmt.Errorf("failed to initialize metrics: %w", err)
|
||
}
|
||
|
||
// Initialize the database
|
||
if err := vm.initializeDBs(db); err != nil {
|
||
return fmt.Errorf("failed to initialize databases: %w", err)
|
||
}
|
||
|
||
if vm.config.InspectDatabase {
|
||
if err := vm.inspectDatabases(); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
|
||
// DEX settlement model (settled architecture): the C-Chain DEX precompile
|
||
// SETTLES certified D-Chain fill receipts at 0x9999; it does NOT query a live
|
||
// matcher backend. The deprecated DexZapEndpoint -> SetBackend(NewZAPEngine())
|
||
// wiring is REMOVED — a synchronous in-block ZAP query against the D-Chain's
|
||
// moving order book forks consensus (each validator observes differently-timed
|
||
// fills => divergent StateRoot, proven by chains/dexvm TestRED_PerValidator
|
||
// Relay_SplitsConsensus). ZAP survives ONLY as the dexvm's own build-path
|
||
// transport (chains/dexvm), never as a C-Chain live backend. Receipts ride in
|
||
// the V4 swap hookData and are verified inline (deterministic BLS), so there is
|
||
// no backend to install here. See precompile/dex/settle9999.go.
|
||
|
||
debugLog("Calling parseGenesis with genesisAllocFile=%q, upgradeBytes len=%d", vm.config.GenesisAllocFile, len(upgradeBytes))
|
||
fmt.Fprintf(os.Stderr, "[GENESIS-BYTES-DEBUG] len=%d first500=%s\n", len(genesisBytes), string(genesisBytes[:min(500, len(genesisBytes))]))
|
||
if len(upgradeBytes) > 0 {
|
||
debugLog("upgradeBytes content: %s", string(upgradeBytes))
|
||
}
|
||
g, err := parseGenesis(vm.ctx, genesisBytes, upgradeBytes, vm.config.AirdropFile, vm.config.GenesisAllocFile)
|
||
if err != nil {
|
||
debugLog("parseGenesis failed: %v", err)
|
||
return err
|
||
}
|
||
debugLog("parseGenesis succeeded: chainID=%v alloc=%d accounts", g.Config.ChainID, len(g.Alloc))
|
||
|
||
// The Lux EVM is PQ-native: pin strict-PQ from genesis on the extras config,
|
||
// UNCONDITIONALLY, for every chain this plugin boots. From block 0 the Lux
|
||
// stateful precompiles that call contract.RefuseUnderStrictPQ (KZG, Groth16,
|
||
// PLONK, fflonk, Halo2, BN254-Pedersen, BabyJubJub, Pallas/Vesta, BLS12-381
|
||
// modules, etc.) refuse classical pairing/discrete-log primitives — the
|
||
// ecosystem uses the post-quantum verifiers instead. This is gated on being a
|
||
// Lux-ecosystem chain (i.e. running this plugin), NOT on a per-VM config flag;
|
||
// the nil StrictPQTimestamp default (extras) remains only for non-Lux hosts that
|
||
// embed the precompiles classical-permissively.
|
||
zero := uint64(0)
|
||
params.GetExtra(g.Config).StrictPQTimestamp = &zero
|
||
|
||
vm.syntacticBlockValidator = NewBlockValidator()
|
||
debugLog("Creating ethConfig")
|
||
|
||
vm.ethConfig = ethconfig.NewDefaultConfig()
|
||
vm.ethConfig.Genesis = g
|
||
debugLog("ethConfig.Genesis set")
|
||
// NetworkID here is different than Lux's NetworkID.
|
||
// Lux's NetworkID represents the Lux network is running on
|
||
// like Testnet, Mainnet, Local, etc.
|
||
// The NetworkId here is kept same as ChainID to be compatible with
|
||
// Ethereum tooling.
|
||
vm.ethConfig.NetworkId = g.Config.ChainID.Uint64()
|
||
|
||
// Set minimum price for mining and default gas price oracle value to the min
|
||
// gas price to prevent so transactions and blocks all use the correct fees
|
||
vm.ethConfig.RPCGasCap = vm.config.RPCGasCap
|
||
vm.ethConfig.RPCEVMTimeout = vm.config.APIMaxDuration.Duration
|
||
vm.ethConfig.RPCTxFeeCap = vm.config.RPCTxFeeCap
|
||
|
||
vm.ethConfig.TxPool.Locals = vm.config.PriorityRegossipAddresses
|
||
vm.ethConfig.TxPool.NoLocals = !vm.config.LocalTxsEnabled
|
||
vm.ethConfig.TxPool.PriceLimit = vm.config.TxPoolPriceLimit
|
||
vm.ethConfig.TxPool.PriceBump = vm.config.TxPoolPriceBump
|
||
vm.ethConfig.TxPool.AccountSlots = vm.config.TxPoolAccountSlots
|
||
vm.ethConfig.TxPool.GlobalSlots = vm.config.TxPoolGlobalSlots
|
||
vm.ethConfig.TxPool.AccountQueue = vm.config.TxPoolAccountQueue
|
||
vm.ethConfig.TxPool.GlobalQueue = vm.config.TxPoolGlobalQueue
|
||
vm.ethConfig.TxPool.Lifetime = vm.config.TxPoolLifetime.Duration
|
||
|
||
vm.ethConfig.AllowUnfinalizedQueries = vm.config.AllowUnfinalizedQueries
|
||
vm.ethConfig.AllowUnprotectedTxs = vm.config.AllowUnprotectedTxs
|
||
vm.ethConfig.AllowUnprotectedTxHashes = vm.config.AllowUnprotectedTxHashes
|
||
vm.ethConfig.Preimages = vm.config.Preimages
|
||
vm.ethConfig.Pruning = vm.config.Pruning
|
||
vm.ethConfig.TrieCleanCache = vm.config.TrieCleanCache
|
||
vm.ethConfig.TrieDirtyCache = vm.config.TrieDirtyCache
|
||
vm.ethConfig.TrieDirtyCommitTarget = vm.config.TrieDirtyCommitTarget
|
||
vm.ethConfig.TriePrefetcherParallelism = vm.config.TriePrefetcherParallelism
|
||
vm.ethConfig.SnapshotCache = vm.config.SnapshotCache
|
||
vm.ethConfig.AcceptorQueueLimit = vm.config.AcceptorQueueLimit
|
||
vm.ethConfig.PopulateMissingTries = vm.config.PopulateMissingTries
|
||
vm.ethConfig.PopulateMissingTriesParallelism = vm.config.PopulateMissingTriesParallelism
|
||
vm.ethConfig.AllowMissingTries = vm.config.AllowMissingTries
|
||
vm.ethConfig.SnapshotDelayInit = vm.config.StateSyncEnabled
|
||
vm.ethConfig.SnapshotWait = vm.config.SnapshotWait
|
||
vm.ethConfig.SnapshotVerify = vm.config.SnapshotVerify
|
||
vm.ethConfig.HistoricalProofQueryWindow = vm.config.HistoricalProofQueryWindow
|
||
vm.ethConfig.OfflinePruning = vm.config.OfflinePruning
|
||
vm.ethConfig.OfflinePruningBloomFilterSize = vm.config.OfflinePruningBloomFilterSize
|
||
vm.ethConfig.OfflinePruningDataDirectory = vm.config.OfflinePruningDataDirectory
|
||
vm.ethConfig.CommitInterval = vm.config.CommitInterval
|
||
vm.ethConfig.SkipUpgradeCheck = vm.config.SkipUpgradeCheck
|
||
vm.ethConfig.AcceptedCacheSize = vm.config.AcceptedCacheSize
|
||
vm.ethConfig.StateHistory = vm.config.StateHistory
|
||
vm.ethConfig.TransactionHistory = vm.config.TransactionHistory
|
||
vm.ethConfig.SkipTxIndexing = vm.config.SkipTxIndexing
|
||
resolvedScheme, err := resolveStateScheme(vm.config.StateScheme)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
vm.ethConfig.StateScheme = resolvedScheme
|
||
|
||
// Create directory for offline pruning
|
||
if len(vm.ethConfig.OfflinePruningDataDirectory) != 0 {
|
||
if err := os.MkdirAll(vm.ethConfig.OfflinePruningDataDirectory, perms.ReadWriteExecute); err != nil {
|
||
log.Error("failed to create offline pruning data directory", "error", err)
|
||
return err
|
||
}
|
||
}
|
||
|
||
// Handle custom fee recipient
|
||
if common.IsHexAddress(vm.config.FeeRecipient) {
|
||
address := common.HexToAddress(vm.config.FeeRecipient)
|
||
log.Info("Setting fee recipient", "address", address)
|
||
vm.ethConfig.Miner.Etherbase = address
|
||
} else {
|
||
log.Info("Config has not specified any coinbase address. Defaulting to the blackhole address.")
|
||
vm.ethConfig.Miner.Etherbase = constants.BlackholeAddr
|
||
}
|
||
|
||
vm.chainConfig = g.Config
|
||
|
||
// Install the 0x9999 native DEX value-path trust root (registry-backed asset resolver +
|
||
// local in-process D-Chain client), now that the EVM chainID (g.Config.ChainID) and the
|
||
// chain runtime identity are both known. This is the SOLE production install of the
|
||
// resolver the always-on precompile consults; without it the value path is fail-closed
|
||
// (every swap reverts ErrNoAssetResolver). It is wired for ALL networks and derives the
|
||
// resolver's identity ENTIRELY from the node's real chain context, never a constant.
|
||
//
|
||
// FAIL-CLOSED, NOT FATAL: a construction failure (missing/mismatched manifest, no chain
|
||
// context) is LOGGED and the resolver is left UNINSTALLED — the money path stays closed
|
||
// (swaps revert), it never opens. The node still boots for all non-DEX usage. We do not
|
||
// return the error: a node that cannot serve the DEX must still serve everything else.
|
||
if err := installDEXValuePath(vm.runtime, g.Config.ChainID.Uint64()); err != nil {
|
||
log.Warn("0x9999 native DEX value path NOT installed (value swaps will revert, fail-closed)", "err", err)
|
||
}
|
||
|
||
// F102 close-out — geth-layer PQ profile for the STANDARD precompiles (0x01–0x09).
|
||
// One concept, one place: ChainConfig.PQ holds the profile;
|
||
// (*EVM).runPrecompile reads it via chainConfig.PQ.RefuseUnder(op).
|
||
//
|
||
// Install the LUX strict-PQ profile, NOT AllForbidden(): the standard
|
||
// alt_bn128 (BN254) precompiles at 0x06–0x08 stay available for Ethereum-compat
|
||
// dapps. This geth-layer profile is the ONLY PQ surface still opt-in
|
||
// (vm.config.PQ), because it governs the EVM-compat-sensitive standard
|
||
// precompiles (ecrecover/sha256/ripemd/blake2f) that ordinary dapps depend on.
|
||
// Lux's security-critical pairing/DLOG usage lives in the CUSTOM precompiles
|
||
// (precompile/zk @ 0x0900, 0x22 Pedersen, the Z-Chain verifiers) and is ALREADY
|
||
// strict from genesis UNCONDITIONALLY via StrictPQTimestamp=0 pinned above
|
||
// (contract.RefuseUnderStrictPQ). See LuxStrictPQ (pq_profile.go).
|
||
if vm.config.PQ {
|
||
vm.chainConfig.PQ = LuxStrictPQ()
|
||
log.Info("EVM PQ mode active: standard classical precompiles refuse (bn256 0x06-0x08 kept for EVM-compat; Lux custom precompiles strict from genesis)")
|
||
}
|
||
|
||
// create genesisHash after applying upgradeBytes in case
|
||
// upgradeBytes modifies genesis.
|
||
vm.genesisHash = vm.ethConfig.Genesis.ToBlock().Hash() // must create genesis hash before [vm.readLastAccepted]
|
||
lastAcceptedHash, lastAcceptedHeight, err := vm.readLastAccepted()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
log.Info("read last accepted",
|
||
"hash", lastAcceptedHash,
|
||
"height", lastAcceptedHeight,
|
||
)
|
||
|
||
vm.networkCodec = message.Codec
|
||
// p2p.Sender is passed directly to network
|
||
debugLog("Creating network")
|
||
vm.Network, err = network.NewNetwork(context.Background(), init.Sender, vm.networkCodec, vm.config.MaxOutboundActiveRequests, vm.sdkMetrics)
|
||
if err != nil {
|
||
debugLog("Failed to create network: %v", err)
|
||
return fmt.Errorf("failed to create network: %w", err)
|
||
}
|
||
debugLog("Network created successfully")
|
||
// P2PValidators might be nil in test environments
|
||
p2pValidatorsInterface := vm.Network.P2PValidators()
|
||
if p2pValidatorsInterface != nil {
|
||
vm.p2pValidators = p2pValidatorsInterface
|
||
}
|
||
|
||
debugLog("Creating validators manager")
|
||
vm.validatorsManager, err = validators.NewManager(vm.runtime, vm.validatorsDB, &vm.clock)
|
||
if err != nil {
|
||
debugLog("Failed to create validators manager: %v", err)
|
||
return fmt.Errorf("failed to initialize validators manager: %w", err)
|
||
}
|
||
debugLog("Validators manager created successfully")
|
||
|
||
// Initialize warp backend
|
||
offchainWarpMessages := make([][]byte, len(vm.config.WarpOffChainMessages))
|
||
for i, hexMsg := range vm.config.WarpOffChainMessages {
|
||
offchainWarpMessages[i] = []byte(hexMsg)
|
||
}
|
||
warpSignatureCache := lru.NewCache[ids.ID, []byte](warpSignatureCacheSize)
|
||
meteredCache, err := metercacher.New("warp_signature_cache", vm.sdkMetrics, warpSignatureCache)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to create warp signature cache: %w", err)
|
||
}
|
||
|
||
// clear warpdb on initialization if config enabled
|
||
if vm.config.PruneWarpDB {
|
||
if err := database.Clear(vm.warpDB, ethdb.IdealBatchSize); err != nil {
|
||
return fmt.Errorf("failed to prune warpDB: %w", err)
|
||
}
|
||
}
|
||
|
||
// VM implements warp.BlockClient directly
|
||
|
||
// Get warp signer from context - use luxwarp.Signer directly, no adapters
|
||
debugLog("Getting warp signer from context (WarpSigner=%v)", vm.runtime.WarpSigner != nil)
|
||
var warpSigner luxwarp.Signer
|
||
if vm.runtime.WarpSigner != nil {
|
||
var ok bool
|
||
warpSigner, ok = vm.runtime.WarpSigner.(luxwarp.Signer)
|
||
if !ok {
|
||
debugLog("Invalid warp signer type: %T", vm.runtime.WarpSigner)
|
||
return fmt.Errorf("invalid warp signer type: %T", vm.runtime.WarpSigner)
|
||
}
|
||
debugLog("Got warp signer successfully")
|
||
}
|
||
|
||
// Only create warp backend if we have a signer
|
||
if warpSigner != nil {
|
||
debugLog("Creating warp backend")
|
||
vm.warpBackend, err = warp.NewBackend(
|
||
vm.runtime.NetworkID, // Use NetworkID from consensus Context
|
||
vm.runtime.ChainID,
|
||
warpSigner,
|
||
&warpBlockClient{vm: vm}, // Wrapper that implements warp.BlockClient
|
||
validators.NewLockedValidatorReader(vm.validatorsManager, &vm.vmLock),
|
||
vm.warpDB,
|
||
meteredCache,
|
||
offchainWarpMessages,
|
||
)
|
||
if err != nil {
|
||
debugLog("Failed to create warp backend: %v", err)
|
||
return err
|
||
}
|
||
debugLog("Warp backend created successfully")
|
||
}
|
||
|
||
debugLog("Initializing chain with lastAcceptedHash=%s", lastAcceptedHash)
|
||
if err := vm.initializeChain(lastAcceptedHash, vm.ethConfig); err != nil {
|
||
debugLog("Failed to initialize chain: %v", err)
|
||
return err
|
||
}
|
||
debugLog("Chain initialized successfully")
|
||
|
||
// Auto-import RLP chain data if configured. Runs the same logic as admin_importChain
|
||
// but at startup, allowing luxd's --import-chain-data flag to drive bulk historical
|
||
// imports without requiring the RPC server to be up first.
|
||
if vm.config.ImportChainData != "" {
|
||
log.Info("Auto-importing chain data at startup", "path", vm.config.ImportChainData)
|
||
chain := vm.eth.BlockChain()
|
||
persistAccepted := func(hash common.Hash, height uint64) error {
|
||
var blkID ids.ID
|
||
copy(blkID[:], hash[:])
|
||
if err := vm.acceptedBlockDB.Put(lastAcceptedKey, blkID[:]); err != nil {
|
||
return fmt.Errorf("failed to update acceptedBlockDB: %w", err)
|
||
}
|
||
if err := vm.versiondb.Commit(); err != nil {
|
||
return fmt.Errorf("failed to commit versiondb: %w", err)
|
||
}
|
||
return nil
|
||
}
|
||
imported, lastHash, lastHeight, err := importBlocksFromFile(chain, vm.config.ImportChainData, persistAccepted)
|
||
if err != nil {
|
||
return fmt.Errorf("startup import failed: %w", err)
|
||
}
|
||
log.Info("Startup import complete", "blocks", imported, "lastHash", lastHash.Hex(), "height", lastHeight)
|
||
}
|
||
|
||
// Start continuous profiler in a goroutine with recovery
|
||
go func() {
|
||
defer func() {
|
||
if r := recover(); r != nil {
|
||
contextLogger.Error("continuous profiler panicked", "panic", r)
|
||
}
|
||
}()
|
||
vm.startContinuousProfiler()
|
||
}()
|
||
|
||
// Add p2p warp message handler
|
||
warpVerifier := &warpVerifierAdapter{backend: vm.warpBackend}
|
||
warpHandler := luxwarp.NewCachedSignatureHandler(meteredCache, warpVerifier, warpSigner)
|
||
p2pHandler := luxwarp.NewSignatureHandlerAdapter(warpHandler)
|
||
vm.Network.AddHandler(luxwarp.SignatureHandlerID, p2pHandler)
|
||
|
||
vm.setAppRequestHandlers()
|
||
|
||
vm.StateSyncServer = NewStateSyncServer(&stateSyncServerConfig{
|
||
Chain: vm.blockChain,
|
||
SyncableInterval: vm.config.StateSyncCommitInterval,
|
||
})
|
||
return vm.initializeStateSyncClient(lastAcceptedHeight)
|
||
}
|
||
|
||
func parseGenesis(ctx context.Context, genesisBytes []byte, upgradeBytes []byte, airdropFile string, genesisAllocFile string) (*core.Genesis, error) {
|
||
// First check if this is a database replay genesis
|
||
var genesisMap map[string]interface{}
|
||
if err := json.Unmarshal(genesisBytes, &genesisMap); err == nil {
|
||
if replay, ok := genesisMap["replay"].(bool); ok && replay {
|
||
// This is a database replay genesis
|
||
dbPath, _ := genesisMap["dbPath"].(string)
|
||
dbType, _ := genesisMap["dbType"].(string)
|
||
log.Info("Database replay genesis detected", "dbPath", dbPath, "dbType", dbType)
|
||
|
||
// Return a special genesis that signals database replay
|
||
g := &core.Genesis{
|
||
Config: ¶ms.ChainConfig{},
|
||
}
|
||
|
||
// Extract the chain config from the genesis map
|
||
if configData, ok := genesisMap["config"].(map[string]interface{}); ok {
|
||
configBytes, _ := json.Marshal(configData)
|
||
if err := json.Unmarshal(configBytes, g.Config); err != nil {
|
||
return nil, fmt.Errorf("failed to parse chain config: %w", err)
|
||
}
|
||
}
|
||
|
||
// Note: Database replay fields are not present in our ChainConfig
|
||
// These would need to be added if database replay functionality is needed
|
||
|
||
return g, nil
|
||
}
|
||
}
|
||
|
||
// Normal genesis parsing
|
||
g := new(core.Genesis)
|
||
if err := json.Unmarshal(genesisBytes, g); err != nil {
|
||
return nil, fmt.Errorf("parsing genesis: %w", err)
|
||
}
|
||
|
||
// Set the default chain config if not provided
|
||
if g.Config == nil {
|
||
g.Config = params.EVMDefaultChainConfig
|
||
}
|
||
|
||
// Ensure all EVM upgrades are active from genesis for Lux chains.
|
||
// Shanghai: PUSH0
|
||
// Cancun: MCOPY, TSTORE, TLOAD, BLOBHASH
|
||
{
|
||
zero := uint64(0)
|
||
if g.Config.ShanghaiTime == nil {
|
||
g.Config.ShanghaiTime = &zero
|
||
debugLog("ShanghaiTime was nil, forced to 0")
|
||
}
|
||
if g.Config.CancunTime == nil {
|
||
g.Config.CancunTime = &zero
|
||
debugLog("CancunTime was nil, forced to 0")
|
||
}
|
||
debugLog("EVM forks: shanghaiTime=%v cancunTime=%v", *g.Config.ShanghaiTime, *g.Config.CancunTime)
|
||
fmt.Fprintf(os.Stderr, "[EVM-FORK-DEBUG] genesisTimestamp=%v shanghaiTime=%v cancunTime=%v isShanghaiAtGenesis=%v\n",
|
||
g.Timestamp, *g.Config.ShanghaiTime, *g.Config.CancunTime,
|
||
g.Config.IsShanghai(big.NewInt(0), g.Timestamp))
|
||
}
|
||
|
||
// Populate the Lux config extras.
|
||
configExtra := params.GetExtra(g.Config)
|
||
configExtra.LuxContext = extras.LuxContext{
|
||
ConsensusCtx: ctx,
|
||
}
|
||
|
||
if configExtra.FeeConfig == commontype.EmptyFeeConfig {
|
||
log.Info("No fee config given in genesis, setting default fee config", "DefaultFeeConfig", params.DefaultFeeConfig)
|
||
configExtra.FeeConfig = params.DefaultFeeConfig
|
||
}
|
||
|
||
// Load airdrop file if provided
|
||
if airdropFile != "" {
|
||
var err error
|
||
g.AirdropData, err = os.ReadFile(airdropFile)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("could not read airdrop file '%s': %w", airdropFile, err)
|
||
}
|
||
}
|
||
|
||
// Load genesis allocations from external file if provided
|
||
// This allows bypassing P-chain transaction size limits for large genesis states
|
||
if genesisAllocFile != "" {
|
||
fmt.Fprintf(os.Stderr, "[EVM-DEBUG] Loading genesis allocations from file: %s\n", genesisAllocFile)
|
||
log.Info("Loading genesis allocations from file", "file", genesisAllocFile)
|
||
allocData, err := os.ReadFile(genesisAllocFile)
|
||
if err != nil {
|
||
fmt.Fprintf(os.Stderr, "[EVM-DEBUG] ERROR reading genesis-alloc-file: %v\n", err)
|
||
return nil, fmt.Errorf("could not read genesis-alloc-file '%s': %w", genesisAllocFile, err)
|
||
}
|
||
fmt.Fprintf(os.Stderr, "[EVM-DEBUG] Read %d bytes from genesis-alloc-file\n", len(allocData))
|
||
|
||
// Parse the allocations file - it should contain an "alloc" object
|
||
var allocGenesis struct {
|
||
Alloc core.GenesisAlloc `json:"alloc"`
|
||
}
|
||
fmt.Fprintf(os.Stderr, "[EVM-DEBUG] Starting JSON unmarshal of genesis-alloc-file\n")
|
||
if err := json.Unmarshal(allocData, &allocGenesis); err != nil {
|
||
fmt.Fprintf(os.Stderr, "[EVM-DEBUG] ERROR parsing genesis-alloc-file: %v\n", err)
|
||
return nil, fmt.Errorf("could not parse genesis-alloc-file '%s': %w", genesisAllocFile, err)
|
||
}
|
||
fmt.Fprintf(os.Stderr, "[EVM-DEBUG] Successfully parsed %d accounts from genesis-alloc-file\n", len(allocGenesis.Alloc))
|
||
|
||
// Merge allocations into genesis
|
||
if g.Alloc == nil {
|
||
g.Alloc = make(core.GenesisAlloc)
|
||
}
|
||
fmt.Fprintf(os.Stderr, "[EVM-DEBUG] Starting merge into genesis (current: %d accounts)\n", len(g.Alloc))
|
||
for addr, account := range allocGenesis.Alloc {
|
||
if _, exists := g.Alloc[addr]; !exists {
|
||
g.Alloc[addr] = account
|
||
}
|
||
}
|
||
fmt.Fprintf(os.Stderr, "[EVM-DEBUG] Merge complete: %d total accounts\n", len(g.Alloc))
|
||
log.Info("Loaded genesis allocations from file", "accounts", len(allocGenesis.Alloc), "total", len(g.Alloc))
|
||
}
|
||
|
||
// Set network upgrade defaults
|
||
// Network upgrades are managed through chain config
|
||
configExtra.SetDefaults(upgrade.Config{})
|
||
|
||
// Parse network upgrades from the genesis JSON if present
|
||
// They won't be in g.Config because geth's ChainConfig doesn't know about them
|
||
// This must be done after SetDefaults to override defaults with genesis values
|
||
genesisMap = make(map[string]interface{})
|
||
if err := json.Unmarshal(genesisBytes, &genesisMap); err == nil {
|
||
if configData, ok := genesisMap["config"].(map[string]interface{}); ok {
|
||
// Extract network upgrade timestamps
|
||
if val, ok := configData["evmTimestamp"]; ok {
|
||
if ts, ok := val.(float64); ok {
|
||
configExtra.EVMTimestamp = utils.NewUint64(uint64(ts))
|
||
}
|
||
}
|
||
if val, ok := configData["durangoTimestamp"]; ok {
|
||
log.Info("DEBUG: Found durangoTimestamp in genesis", "val", val, "type", fmt.Sprintf("%T", val))
|
||
fmt.Fprintf(os.Stderr, "[DURANGO-DEBUG] val=%v type=%T\n", val, val)
|
||
if ts, ok := val.(float64); ok {
|
||
log.Info("DEBUG: Setting DurangoTimestamp from float64", "ts", ts, "uint64", uint64(ts))
|
||
fmt.Fprintf(os.Stderr, "[DURANGO-DEBUG] setting durango=%d\n", uint64(ts))
|
||
configExtra.DurangoTimestamp = utils.NewUint64(uint64(ts))
|
||
} else {
|
||
log.Info("DEBUG: durangoTimestamp is not float64")
|
||
}
|
||
} else {
|
||
// Default DurangoTimestamp to 0 (activated at genesis) when not specified
|
||
log.Info("DEBUG: durangoTimestamp NOT found in genesis config, defaulting to 0")
|
||
configExtra.DurangoTimestamp = utils.NewUint64(0)
|
||
}
|
||
if val, ok := configData["quasarTimestamp"]; ok {
|
||
if ts, ok := val.(float64); ok {
|
||
configExtra.QuasarTimestamp = utils.NewUint64(uint64(ts))
|
||
}
|
||
}
|
||
if val, ok := configData["fortunaTimestamp"]; ok {
|
||
if ts, ok := val.(float64); ok {
|
||
configExtra.FortunaTimestamp = utils.NewUint64(uint64(ts))
|
||
}
|
||
}
|
||
if val, ok := configData["graniteTimestamp"]; ok {
|
||
if ts, ok := val.(float64); ok {
|
||
configExtra.GraniteTimestamp = utils.NewUint64(uint64(ts))
|
||
}
|
||
}
|
||
|
||
// Parse genesis precompiles from config JSON
|
||
// They are stored at the top level of config, not under "genesisPrecompiles"
|
||
for _, module := range modules.RegisteredModules() {
|
||
key := module.ConfigKey
|
||
if val, ok := configData[key]; ok {
|
||
// Re-marshal the precompile config to parse it properly
|
||
precompileBytes, err := json.Marshal(val)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to marshal precompile config %s: %w", key, err)
|
||
}
|
||
conf := module.MakeConfig()
|
||
if err := json.Unmarshal(precompileBytes, conf); err != nil {
|
||
return nil, fmt.Errorf("failed to parse precompile config %s: %w", key, err)
|
||
}
|
||
if configExtra.GenesisPrecompiles == nil {
|
||
configExtra.GenesisPrecompiles = make(extras.Precompiles)
|
||
}
|
||
configExtra.GenesisPrecompiles[key] = conf
|
||
log.Info("DEBUG: Parsed genesis precompile from config", "key", key)
|
||
}
|
||
}
|
||
|
||
// Parse feeConfig from genesis JSON
|
||
if val, ok := configData["feeConfig"]; ok {
|
||
feeConfigBytes, err := json.Marshal(val)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to marshal feeConfig: %w", err)
|
||
}
|
||
var feeConfig commontype.FeeConfig
|
||
if err := json.Unmarshal(feeConfigBytes, &feeConfig); err != nil {
|
||
return nil, fmt.Errorf("failed to parse feeConfig: %w", err)
|
||
}
|
||
configExtra.FeeConfig = feeConfig
|
||
log.Info("DEBUG: Parsed feeConfig from genesis", "gasLimit", feeConfig.GasLimit)
|
||
}
|
||
|
||
// Parse allowFeeRecipients from genesis JSON
|
||
if val, ok := configData["allowFeeRecipients"]; ok {
|
||
if allow, ok := val.(bool); ok {
|
||
configExtra.AllowFeeRecipients = allow
|
||
log.Info("DEBUG: Parsed allowFeeRecipients from genesis", "value", allow)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Apply upgradeBytes (if any) by unmarshalling them into [chainConfig.UpgradeConfig].
|
||
// Initializing the chain will verify upgradeBytes are compatible with existing values.
|
||
// This should be called before g.Verify().
|
||
//
|
||
// DisallowUnknownFields is mandatory: upgrade keys (e.g. quasarTimestamp) are
|
||
// wire-identified by name. A typo or stale alias (e.g. etnaTimestamp) silently
|
||
// becomes a zero-value field with default encoding/json — muting forks. The
|
||
// strict decoder forces parse-time failure so renames surface during config
|
||
// roll, not at activation.
|
||
if len(upgradeBytes) > 0 {
|
||
log.Info("DEBUG: Parsing upgrade bytes", "len", len(upgradeBytes), "content", string(upgradeBytes))
|
||
var upgradeConfig extras.UpgradeConfig
|
||
dec := json.NewDecoder(bytes.NewReader(upgradeBytes))
|
||
dec.DisallowUnknownFields()
|
||
if err := dec.Decode(&upgradeConfig); err != nil {
|
||
log.Error("DEBUG: Failed to parse upgrade bytes", "error", err)
|
||
return nil, fmt.Errorf("failed to parse upgrade bytes: %w", err)
|
||
}
|
||
log.Info("DEBUG: Parsed upgrade config", "numPrecompileUpgrades", len(upgradeConfig.PrecompileUpgrades))
|
||
for i, pu := range upgradeConfig.PrecompileUpgrades {
|
||
if pu.Config != nil {
|
||
log.Info("DEBUG: Precompile upgrade", "index", i, "key", pu.Config.Key(), "timestamp", pu.Config.Timestamp())
|
||
} else {
|
||
log.Warn("DEBUG: Precompile upgrade has nil config", "index", i)
|
||
}
|
||
}
|
||
configExtra.UpgradeConfig = upgradeConfig
|
||
}
|
||
|
||
if configExtra.NetworkUpgradeOverrides != nil {
|
||
overrides := configExtra.NetworkUpgradeOverrides
|
||
marshaled, err := json.Marshal(overrides)
|
||
if err != nil {
|
||
log.Warn("Failed to marshal network upgrade overrides", "error", err, "overrides", overrides)
|
||
} else {
|
||
log.Info("Applying network upgrade overrides", "overrides", string(marshaled))
|
||
}
|
||
configExtra.Override(overrides)
|
||
}
|
||
|
||
if err := configExtra.Verify(); err != nil {
|
||
return nil, fmt.Errorf("invalid chain config: %w", err)
|
||
}
|
||
|
||
// Align all the Ethereum upgrades to the Lux upgrades
|
||
if err := params.SetEthUpgrades(g.Config); err != nil {
|
||
return nil, fmt.Errorf("setting eth upgrades: %w", err)
|
||
}
|
||
return g, nil
|
||
}
|
||
|
||
func (vm *VM) initializeMetrics() error {
|
||
// Enable metrics collection using our geth's Enable function
|
||
metrics.Enable()
|
||
vm.sdkMetrics = metric.NewRegistry()
|
||
gatherer := evmmetrics.NewGatherer(metrics.DefaultRegistry)
|
||
// Metrics are handled through sdkMetrics parameter
|
||
_ = gatherer
|
||
|
||
// if vm.config.MetricsExpensiveEnabled && vm.config.StateScheme == customrawdb.FirewoodScheme {
|
||
// if err := ffi.StartMetrics(); err != nil {
|
||
// return fmt.Errorf("failed to start firewood metrics collection: %w", err)
|
||
// }
|
||
// // Firewood metrics registration deferred
|
||
// }
|
||
// SDK metrics registered via sdkMetrics parameter
|
||
return nil
|
||
}
|
||
|
||
func (vm *VM) initializeChain(lastAcceptedHash common.Hash, ethConfig ethconfig.Config) error {
|
||
debugLog("initializeChain: starting with lastAcceptedHash=%s", lastAcceptedHash.Hex())
|
||
nodecfg := &node.Config{
|
||
EVMVersion: Version,
|
||
KeyStoreDir: vm.config.KeystoreDirectory,
|
||
ExternalSigner: vm.config.KeystoreExternalSigner,
|
||
InsecureUnlockAllowed: vm.config.KeystoreInsecureUnlockAllowed,
|
||
}
|
||
debugLog("initializeChain: creating node")
|
||
node, err := node.New(nodecfg)
|
||
if err != nil {
|
||
debugLog("initializeChain: node.New failed: %v", err)
|
||
return err
|
||
}
|
||
debugLog("initializeChain: node created successfully")
|
||
// Create consensus engine with appropriate mode
|
||
// When automining is enabled, skip block fee validation to allow instant block production
|
||
consensusMode := dummy.Mode{}
|
||
if vm.config.EnableAutomining {
|
||
consensusMode.ModeSkipBlockFee = true
|
||
log.Info("Automining enabled: setting ModeSkipBlockFee=true on consensus engine")
|
||
}
|
||
consensusEngine := dummy.NewFakerWithModeAndClock(consensusMode, &vm.clock)
|
||
// Hand eth.New the chain Runtime the VM already holds (vm.runtime: networkID / X / C chain
|
||
// ids + cross-chain SharedMemory). It flows eth.Settings.ChainRuntime → core.NewBlockChain,
|
||
// which binds the chain identity onto the BlockChain BEFORE any block is re-executed during
|
||
// startup recovery. Otherwise an unclean shutdown after a 0x9999 swap would re-execute that
|
||
// block with identity (networkID 0 / C-Chain Empty), revert the committed swap, fail
|
||
// ValidateState, and the node could not boot. The identity is the runtime the VM already
|
||
// knows — passed explicitly, not smuggled through context.Context. vm.runtime is nil on
|
||
// single-chain dev, which is fine (no identity-gated precompiles there).
|
||
debugLog("initializeChain: calling eth.New")
|
||
// Wrap eth.New in panic recovery to capture any panics
|
||
func() {
|
||
defer func() {
|
||
if r := recover(); r != nil {
|
||
debugLog("PANIC in eth.New: %v", r)
|
||
buf := make([]byte, 4096)
|
||
n := goruntime.Stack(buf, false)
|
||
debugLog("Stack trace:\n%s", string(buf[:n]))
|
||
// CRITICAL: Set err so the caller doesn't continue with nil vm.eth
|
||
err = fmt.Errorf("panic in eth.New: %v", r)
|
||
}
|
||
}()
|
||
vm.eth, err = eth.New(
|
||
node,
|
||
&vm.ethConfig,
|
||
&EthPushGossiper{vm: vm},
|
||
vm.chaindb,
|
||
eth.Settings{
|
||
MaxBlocksPerRequest: vm.config.MaxBlocksPerRequest,
|
||
ChainRuntime: vm.runtime,
|
||
},
|
||
lastAcceptedHash,
|
||
consensusEngine,
|
||
&vm.clock,
|
||
)
|
||
}()
|
||
if err != nil {
|
||
debugLog("initializeChain: eth.New failed: %v", err)
|
||
return err
|
||
}
|
||
debugLog("initializeChain: eth.New succeeded")
|
||
vm.eth.SetEtherbase(ethConfig.Miner.Etherbase)
|
||
vm.txPool = vm.eth.TxPool()
|
||
vm.blockChain = vm.eth.BlockChain()
|
||
debugLog("initializeChain: got txPool and blockChain")
|
||
// Upgrade the consensus context on the live chain to the FULL chain Runtime, which adds
|
||
// the cross-chain atomic SharedMemory capability (the DEX 0x9999 C<->D shared-memory seam)
|
||
// on top of the identity already bound at construction. Also covers the warp message
|
||
// chain ID / network ID path. When vm.runtime is nil (single-chain dev) this is vm.ctx and
|
||
// FromContext stays nil, so a cross-chain settle reverts rather than fabricating value.
|
||
consensusCtx := vm.ctx
|
||
if vm.runtime != nil {
|
||
consensusCtx = runtime.WithContext(vm.ctx, vm.runtime)
|
||
}
|
||
vm.blockChain.SetConsensusContext(consensusCtx)
|
||
vm.miner = vm.eth.Miner()
|
||
lastAccepted := vm.blockChain.LastAcceptedBlock()
|
||
debugLog("initializeChain: lastAccepted block number=%d", lastAccepted.NumberU64())
|
||
feeConfig, _, err := vm.blockChain.GetFeeConfigAt(lastAccepted.Header())
|
||
if err != nil {
|
||
debugLog("initializeChain: GetFeeConfigAt failed: %v", err)
|
||
return err
|
||
}
|
||
vm.txPool.SetMinFee(feeConfig.MinBaseFee)
|
||
vm.txPool.SetGasTip(big.NewInt(0))
|
||
debugLog("initializeChain: calling eth.Start")
|
||
vm.eth.Start()
|
||
|
||
// Register callback for admin_importChain to update acceptedBlockDB.
|
||
// This is critical: when blocks are imported via the admin API, the VM layer's
|
||
// acceptedBlockDB must be updated so ReadLastAccepted() returns the correct hash on restart.
|
||
vm.eth.SetPostImportCallback(func(lastBlockHash common.Hash, lastBlockHeight uint64) error {
|
||
log.Info("PostImportCallback: updating acceptedBlockDB", "hash", lastBlockHash, "height", lastBlockHeight)
|
||
var blkID ids.ID
|
||
copy(blkID[:], lastBlockHash[:])
|
||
if err := vm.acceptedBlockDB.Put(lastAcceptedKey, blkID[:]); err != nil {
|
||
return fmt.Errorf("failed to update acceptedBlockDB: %w", err)
|
||
}
|
||
if err := vm.versiondb.Commit(); err != nil {
|
||
return fmt.Errorf("failed to commit versiondb: %w", err)
|
||
}
|
||
// CRITICAL: Force sync to disk to ensure persistence across restarts.
|
||
// Without this, async writes (e.g., badgerdb with SyncWrites=false) may be
|
||
// lost if the network stops before the background flush completes.
|
||
if syncer, ok := vm.db.(interface{ Sync() error }); ok {
|
||
if err := syncer.Sync(); err != nil {
|
||
return fmt.Errorf("failed to sync database: %w", err)
|
||
}
|
||
log.Info("PostImportCallback: database synced to disk")
|
||
}
|
||
log.Info("PostImportCallback: acceptedBlockDB updated successfully", "hash", lastBlockHash.Hex(), "height", lastBlockHeight)
|
||
return nil
|
||
})
|
||
|
||
return vm.initChainState(lastAccepted)
|
||
}
|
||
|
||
// initializeStateSyncClient initializes the client for performing state sync.
|
||
// If state sync is disabled, this function will wipe any ongoing summary from
|
||
// disk to ensure that we do not continue syncing from an invalid snapshot.
|
||
func (vm *VM) initializeStateSyncClient(lastAcceptedHeight uint64) error {
|
||
// parse nodeIDs from state sync IDs in vm config
|
||
var stateSyncIDs []ids.NodeID
|
||
if vm.config.StateSyncEnabled && len(vm.config.StateSyncIDs) > 0 {
|
||
nodeIDs := strings.Split(vm.config.StateSyncIDs, ",")
|
||
stateSyncIDs = make([]ids.NodeID, len(nodeIDs))
|
||
for i, nodeIDString := range nodeIDs {
|
||
nodeID, err := ids.NodeIDFromString(nodeIDString)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to parse %s as NodeID: %w", nodeIDString, err)
|
||
}
|
||
stateSyncIDs[i] = nodeID
|
||
}
|
||
}
|
||
|
||
vm.StateSyncClient = NewStateSyncClient(&stateSyncClientConfig{
|
||
chain: vm.eth,
|
||
state: vm.State,
|
||
stateSyncDone: vm.stateSyncDone,
|
||
client: statesyncclient.NewClient(
|
||
&statesyncclient.ClientConfig{
|
||
NetworkClient: vm.Network,
|
||
Codec: vm.networkCodec,
|
||
Stats: stats.NewClientSyncerStats(),
|
||
StateSyncNodeIDs: stateSyncIDs,
|
||
BlockParser: vm,
|
||
},
|
||
),
|
||
enabled: vm.config.StateSyncEnabled,
|
||
skipResume: vm.config.StateSyncSkipResume,
|
||
stateSyncMinBlocks: vm.config.StateSyncMinBlocks,
|
||
stateSyncRequestSize: vm.config.StateSyncRequestSize,
|
||
lastAcceptedHeight: lastAcceptedHeight,
|
||
chaindb: vm.chaindb,
|
||
metadataDB: vm.metadataDB,
|
||
acceptedBlockDB: vm.acceptedBlockDB,
|
||
db: vm.versiondb,
|
||
})
|
||
|
||
// If StateSync is disabled, clear any ongoing summary so that we will not attempt to resume
|
||
// sync using a snapshot that has been modified by the node running normal operations.
|
||
if !vm.config.StateSyncEnabled {
|
||
return vm.ClearOngoingSummary()
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func (vm *VM) initChainState(lastAcceptedBlock *types.Block) error {
|
||
block := vm.newBlock(lastAcceptedBlock)
|
||
|
||
config := &nodeChain.Config{
|
||
DecidedCacheSize: int(decidedCacheSize),
|
||
MissingCacheSize: missingCacheSize,
|
||
UnverifiedCacheSize: int(unverifiedCacheSize),
|
||
BytesToIDCacheSize: int(bytesToIDCacheSize),
|
||
// Our vm methods return *Block which needs to implement the node's chain.Block
|
||
GetBlock: func(ctx context.Context, id ids.ID) (nodeblock.Block, error) {
|
||
// getBlock returns our block
|
||
ethBlock := vm.blockChain.GetBlockByHash(common.Hash(id))
|
||
if ethBlock == nil {
|
||
return nil, database.ErrNotFound
|
||
}
|
||
return vm.newBlock(ethBlock), nil
|
||
},
|
||
UnmarshalBlock: func(ctx context.Context, b []byte) (nodeblock.Block, error) {
|
||
// parseBlock returns our block
|
||
ethBlock := &types.Block{}
|
||
if err := rlp.DecodeBytes(b, ethBlock); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// CRITICAL: Populate BlockGasCost on blocks received via P2P.
|
||
// The BlockGasCost is stored in an in-memory map on the block producer,
|
||
// but this data is NOT included in RLP encoding. We must calculate it
|
||
// from the parent header before verification.
|
||
header := ethBlock.Header()
|
||
if header.Number.Uint64() > 0 {
|
||
// Non-genesis block: calculate BlockGasCost from parent
|
||
parent := vm.blockChain.GetHeaderByHash(header.ParentHash)
|
||
if parent != nil {
|
||
config := params.GetExtra(vm.chainConfig)
|
||
// Get fee config at parent height
|
||
feeConfig, _, err := vm.blockChain.GetFeeConfigAt(parent)
|
||
if err == nil && config.IsEVM(header.Time) {
|
||
blockGasCost := customheader.BlockGasCost(
|
||
config,
|
||
feeConfig,
|
||
parent,
|
||
header.Time,
|
||
)
|
||
customtypes.SetHeaderExtra(header, &customtypes.HeaderExtra{
|
||
BlockGasCost: blockGasCost,
|
||
})
|
||
}
|
||
}
|
||
} else {
|
||
// Genesis block: BlockGasCost is always 0
|
||
customtypes.SetHeaderExtra(header, &customtypes.HeaderExtra{
|
||
BlockGasCost: big.NewInt(0),
|
||
})
|
||
}
|
||
|
||
block := vm.newBlock(ethBlock)
|
||
// Performing syntactic verification in ParseBlock allows for
|
||
// short-circuiting bad blocks before they are processed by the VM.
|
||
if err := block.syntacticVerify(); err != nil {
|
||
return nil, fmt.Errorf("syntactic block verification failed: %w", err)
|
||
}
|
||
return block, nil
|
||
},
|
||
BuildBlock: func(ctx context.Context) (nodeblock.Block, error) {
|
||
// Call VM's buildBlock directly which returns the right type
|
||
return vm.buildBlock(ctx)
|
||
},
|
||
// BuildBlockWithContext: func(ctx context.Context, proposerVMBlockCtx *nodeblock.Context) (nodechain.Block, error) {
|
||
// // Call VM's BuildBlockWithContext directly which returns the right type
|
||
// return vm.BuildBlockWithContext(ctx, proposerVMBlockCtx)
|
||
// },
|
||
LastAcceptedBlock: block,
|
||
}
|
||
|
||
// Register chain state metrics
|
||
chainStateRegisterer := metric.NewRegistry()
|
||
state, err := nodeChain.NewMeteredState(chainStateRegisterer, config)
|
||
if err != nil {
|
||
return fmt.Errorf("could not create metered state: %w", err)
|
||
}
|
||
vm.State = state
|
||
|
||
if !metrics.Enabled() {
|
||
return nil
|
||
}
|
||
|
||
// Chain state metrics registered through initializeMetrics
|
||
_ = chainStateRegisterer
|
||
return nil
|
||
}
|
||
|
||
func (vm *VM) SetState(_ context.Context, state uint32) error {
|
||
vm.vmLock.Lock()
|
||
defer vm.vmLock.Unlock()
|
||
|
||
switch VMState(state) {
|
||
case VMStateSyncing:
|
||
vm.bootstrapped.Set(false)
|
||
return nil
|
||
case VMBootstrapping:
|
||
return vm.onBootstrapStarted()
|
||
case VMNormalOp:
|
||
return vm.onNormalOperationsStarted()
|
||
default:
|
||
return fmt.Errorf("unknown state: %d", state)
|
||
}
|
||
}
|
||
|
||
// onBootstrapStarted marks this VM as bootstrapping
|
||
func (vm *VM) onBootstrapStarted() error {
|
||
vm.bootstrapped.Set(false)
|
||
if err := vm.Error(); err != nil {
|
||
return err
|
||
}
|
||
// After starting bootstrapping, do not attempt to resume a previous state sync.
|
||
if err := vm.ClearOngoingSummary(); err != nil {
|
||
return err
|
||
}
|
||
// Ensure snapshots are initialized before bootstrapping (i.e., if state sync is skipped).
|
||
// Note calling this function has no effect if snapshots are already initialized.
|
||
vm.blockChain.InitializeSnapshots()
|
||
|
||
return nil
|
||
}
|
||
|
||
// onNormalOperationsStarted marks this VM as bootstrapped
|
||
func (vm *VM) onNormalOperationsStarted() error {
|
||
if vm.bootstrapped.Get() {
|
||
return nil
|
||
}
|
||
vm.bootstrapped.Set(true)
|
||
|
||
ctx, cancel := context.WithCancel(context.TODO())
|
||
vm.cancel = cancel
|
||
|
||
// Start the validators manager
|
||
if err := vm.validatorsManager.Initialize(ctx); err != nil {
|
||
return fmt.Errorf("failed to initialize validators manager: %w", err)
|
||
}
|
||
|
||
// dispatch validator set update
|
||
vm.shutdownWg.Add(1)
|
||
go func() {
|
||
vm.validatorsManager.DispatchSync(ctx, &vm.vmLock)
|
||
vm.shutdownWg.Done()
|
||
}()
|
||
|
||
// Initialize goroutines related to block building
|
||
// once we enter normal operation as there is no need to handle mempool gossip before this point.
|
||
ethTxGossipMarshaller := GossipEthTxMarshaller{}
|
||
|
||
// P2PValidators might be nil in test environments
|
||
p2pValidators := vm.Network.P2PValidators()
|
||
var ethTxGossipClient *p2p.Client
|
||
if p2pValidators != nil {
|
||
ethTxGossipClient = vm.Network.NewClient(TxGossipHandlerID, p2p.WithValidatorSampling(p2pValidators))
|
||
} else {
|
||
// In test mode, use a client without validator sampling
|
||
ethTxGossipClient = vm.Network.NewClient(TxGossipHandlerID)
|
||
}
|
||
ethTxGossipMetrics, err := gossip.NewMetrics(vm.sdkMetrics, ethTxGossipNamespace)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to initialize eth tx gossip metrics: %w", err)
|
||
}
|
||
ethTxPool, err := NewGossipEthTxPool(vm.txPool, vm.sdkMetrics)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to initialize gossip eth tx pool: %w", err)
|
||
}
|
||
vm.shutdownWg.Add(1)
|
||
go func() {
|
||
ethTxPool.Subscribe(ctx)
|
||
vm.shutdownWg.Done()
|
||
}()
|
||
|
||
pushGossipParams := gossip.BranchingFactor{
|
||
StakePercentage: vm.config.PushGossipPercentStake,
|
||
Validators: vm.config.PushGossipNumValidators,
|
||
Peers: vm.config.PushGossipNumPeers,
|
||
}
|
||
pushRegossipParams := gossip.BranchingFactor{
|
||
Validators: vm.config.PushRegossipNumValidators,
|
||
Peers: vm.config.PushRegossipNumPeers,
|
||
}
|
||
|
||
ethTxPushGossiper := vm.ethTxPushGossiper.Get()
|
||
if ethTxPushGossiper == nil && p2pValidators != nil {
|
||
// Only create push gossiper if we have P2P validators
|
||
ethTxPushGossiper, err = gossip.NewPushGossiper[*GossipEthTx](
|
||
ethTxGossipMarshaller,
|
||
ethTxPool,
|
||
p2pValidators,
|
||
ethTxGossipClient,
|
||
ethTxGossipMetrics,
|
||
pushGossipParams,
|
||
pushRegossipParams,
|
||
config.PushGossipDiscardedElements,
|
||
int(config.TxGossipTargetMessageSize),
|
||
vm.config.RegossipFrequency.Duration,
|
||
)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to initialize eth tx push gossiper: %w", err)
|
||
}
|
||
vm.ethTxPushGossiper.Set(ethTxPushGossiper)
|
||
}
|
||
|
||
// NOTE: gossip network must be initialized first otherwise ETH tx gossip will not work.
|
||
vm.builderLock.Lock()
|
||
vm.builder = vm.NewBlockBuilder()
|
||
vm.builder.awaitSubmittedTxs()
|
||
// Start automining if enabled (dev mode)
|
||
log.Info("C-Chain automining config check", "enableAutomining", vm.config.EnableAutomining)
|
||
if vm.config.EnableAutomining {
|
||
log.Info("C-Chain automining ENABLED, starting automining loop")
|
||
vm.builder.startAutomining(AutominingConfig{
|
||
BuildBlock: func(ctx context.Context) (interface {
|
||
Verify(context.Context) error
|
||
Accept(context.Context) error
|
||
}, error) {
|
||
return vm.BuildBlock(ctx)
|
||
},
|
||
Interval: 100 * time.Millisecond,
|
||
})
|
||
}
|
||
vm.builderLock.Unlock()
|
||
|
||
if vm.ethTxGossipHandler == nil {
|
||
// Get logger from context for gossip handler
|
||
// Use VM's logger instead of consensus logger
|
||
handler, err := gossipHandler.NewTxGossipHandler[*GossipEthTx](
|
||
log.Root(),
|
||
ethTxGossipMarshaller,
|
||
ethTxPool,
|
||
ethTxGossipMetrics,
|
||
int(config.TxGossipTargetMessageSize),
|
||
config.TxGossipThrottlingPeriod,
|
||
float64(config.TxGossipThrottlingLimit),
|
||
p2pValidators,
|
||
vm.sdkMetrics,
|
||
ethTxGossipNamespace,
|
||
)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to create tx gossip handler: %w", err)
|
||
}
|
||
vm.ethTxGossipHandler = handler
|
||
}
|
||
|
||
if err := vm.Network.AddHandler(TxGossipHandlerID, vm.ethTxGossipHandler); err != nil {
|
||
return fmt.Errorf("failed to add eth tx gossip handler: %w", err)
|
||
}
|
||
|
||
if vm.ethTxPullGossiper == nil && p2pValidators != nil {
|
||
// Only create pull gossiper if we have P2P validators
|
||
// Create a log.Logger for gossip (required by gossip package)
|
||
gossipLogger := log.New("component", "gossip", "chain", vm.chainAlias)
|
||
ethTxPullGossiper := gossip.NewPullGossiper[*GossipEthTx](
|
||
gossipLogger,
|
||
ethTxGossipMarshaller,
|
||
ethTxPool,
|
||
ethTxGossipClient,
|
||
ethTxGossipMetrics,
|
||
config.TxGossipPollSize,
|
||
)
|
||
|
||
vm.ethTxPullGossiper = gossip.ValidatorGossiper{
|
||
Gossiper: ethTxPullGossiper,
|
||
NodeID: vm.runtime.NodeID,
|
||
Validators: p2pValidators,
|
||
}
|
||
}
|
||
|
||
// Create a log.Logger for gossip routines (required by gossip package)
|
||
gossipEveryLogger := log.New("component", "gossip", "chain", vm.chainAlias)
|
||
|
||
// Only start gossip routines if gossipers are initialized (requires P2P validators)
|
||
if ethTxPushGossiper != nil {
|
||
vm.shutdownWg.Add(1)
|
||
go func() {
|
||
gossip.Every(ctx, gossipEveryLogger, ethTxPushGossiper, vm.config.PushGossipFrequency.Duration)
|
||
vm.shutdownWg.Done()
|
||
}()
|
||
}
|
||
if vm.ethTxPullGossiper != nil {
|
||
vm.shutdownWg.Add(1)
|
||
go func() {
|
||
gossip.Every(ctx, gossipEveryLogger, vm.ethTxPullGossiper, vm.config.PullGossipFrequency.Duration)
|
||
vm.shutdownWg.Done()
|
||
}()
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// setAppRequestHandlers sets the request handlers for the VM to serve state sync
|
||
// requests.
|
||
func (vm *VM) setAppRequestHandlers() {
|
||
// Create standalone EVM TrieDB (read only) for serving leafs requests.
|
||
// We create a standalone TrieDB here, so that it has a standalone cache from the one
|
||
// used by the node when processing blocks.
|
||
evmTrieDB := triedb.NewDatabase(
|
||
vm.chaindb,
|
||
&triedb.Config{
|
||
HashDB: &hashdb.Config{
|
||
CleanCacheSize: vm.config.StateSyncServerTrieCache * int(constants.MiB),
|
||
},
|
||
},
|
||
)
|
||
|
||
networkHandler := newNetworkHandler(vm.blockChain, vm.chaindb, evmTrieDB, vm.warpBackend, vm.networkCodec)
|
||
vm.Network.SetRequestHandler(networkHandler)
|
||
}
|
||
|
||
func (vm *VM) WaitForEvent(ctx context.Context) (block.Message, error) {
|
||
vm.builderLock.Lock()
|
||
builder := vm.builder
|
||
vm.builderLock.Unlock()
|
||
|
||
// Block building is not initialized yet, so we haven't finished syncing or bootstrapping.
|
||
if builder == nil {
|
||
select {
|
||
case <-ctx.Done():
|
||
return block.Message{}, ctx.Err()
|
||
case <-vm.stateSyncDone:
|
||
return block.Message{Type: block.StateSyncDone}, nil
|
||
case <-vm.shutdownChan:
|
||
return block.Message{}, errShuttingDownVM
|
||
}
|
||
}
|
||
|
||
return builder.waitForEvent(ctx)
|
||
}
|
||
|
||
// Shutdown implements the chain.ChainVM interface
|
||
func (vm *VM) Shutdown(context.Context) error {
|
||
vm.vmLock.Lock()
|
||
defer vm.vmLock.Unlock()
|
||
if vm.ctx == nil {
|
||
return nil
|
||
}
|
||
if vm.cancel != nil {
|
||
vm.cancel()
|
||
}
|
||
if vm.bootstrapped.Get() {
|
||
if err := vm.validatorsManager.Shutdown(); err != nil {
|
||
return fmt.Errorf("failed to shutdown validators manager: %w", err)
|
||
}
|
||
}
|
||
vm.Network.Shutdown()
|
||
if err := vm.StateSyncClient.Shutdown(); err != nil {
|
||
log.Error("error stopping state syncer", "err", err)
|
||
}
|
||
close(vm.shutdownChan)
|
||
// Stop RPC handlers before eth.Stop which will close the database
|
||
for _, handler := range vm.rpcHandlers {
|
||
handler.Stop()
|
||
}
|
||
vm.eth.Stop()
|
||
log.Info("Ethereum backend stop completed")
|
||
if vm.usingStandaloneDB {
|
||
if err := vm.db.Close(); err != nil {
|
||
log.Error("failed to close database: %w", err)
|
||
} else {
|
||
log.Info("Database closed")
|
||
}
|
||
}
|
||
vm.shutdownWg.Wait()
|
||
log.Info("EVM Shutdown completed")
|
||
return nil
|
||
}
|
||
|
||
// BuildBlock implements the ChainVM interface
|
||
// Uses State.BuildBlock to ensure proper block tracking (for LastAccepted, etc.)
|
||
func (vm *VM) BuildBlock(ctx context.Context) (nodeblock.Block, error) {
|
||
// Use State's BuildBlock which properly wraps and tracks blocks
|
||
// The State's BuildBlock callback calls vm.buildBlock() and wraps the result
|
||
// so Accept() updates the State's lastAccepted
|
||
log.Info("[EVM DEBUG] BuildBlock called, calling vm.State.BuildBlock")
|
||
blk, err := vm.State.BuildBlock(ctx)
|
||
if err != nil {
|
||
log.Error("[EVM DEBUG] BuildBlock error", "error", err)
|
||
return nil, err
|
||
}
|
||
log.Info("[EVM DEBUG] BuildBlock returned", "id", blk.ID(), "height", blk.Height())
|
||
return blk, nil
|
||
}
|
||
|
||
// BuildBlockWithContext implements the BuildBlockWithContextChainVM interface
|
||
func (vm *VM) BuildBlockWithContext(ctx context.Context, proposerVMBlockCtx *nodeblock.Context) (nodeblock.Block, error) {
|
||
// Convert node context to consensus context
|
||
var consensusCtx *nodeblock.Context
|
||
if proposerVMBlockCtx != nil {
|
||
consensusCtx = &nodeblock.Context{
|
||
PChainHeight: proposerVMBlockCtx.PChainHeight,
|
||
}
|
||
}
|
||
return vm.buildBlockWithContext(ctx, consensusCtx)
|
||
}
|
||
|
||
// buildBlock builds a block to be wrapped by ChainState
|
||
func (vm *VM) buildBlock(ctx context.Context) (nodeblock.Block, error) {
|
||
return vm.buildBlockWithContext(ctx, nil)
|
||
}
|
||
|
||
func (vm *VM) buildBlockWithContext(ctx context.Context, proposerVMBlockCtx *nodeblock.Context) (nodeblock.Block, error) {
|
||
if proposerVMBlockCtx != nil {
|
||
log.Debug("Building block with context", "pChainBlockHeight", proposerVMBlockCtx.PChainHeight)
|
||
} else {
|
||
log.Debug("Building block without context")
|
||
}
|
||
predicateCtx := &precompileconfig.PredicateContext{
|
||
ConsensusCtx: vm.ctx,
|
||
ProposerVMBlockCtx: proposerVMBlockCtx,
|
||
}
|
||
|
||
block, err := vm.miner.GenerateBlock(predicateCtx)
|
||
vm.builder.handleGenerateBlock()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// Note: the status of block is set by ChainState
|
||
blk := vm.newBlock(block)
|
||
log.Info("[EVM DEBUG] buildBlock: created block", "ethHash", block.Hash(), "id", blk.ID(), "height", blk.Height())
|
||
|
||
// Verify is called on a non-wrapped block here, such that this
|
||
// does not add [blk] to the processing blocks map in ChainState.
|
||
//
|
||
// Verification is repeated by the consensus engine; caching the result
|
||
// would only benefit high-throughput block producers.
|
||
// We call verify without writes here to avoid generating a reference
|
||
// to the blk state root in the triedb when we are going to call verify
|
||
// again from the consensus engine with writes enabled.
|
||
if err := blk.verify(predicateCtx, false /*=writes*/); err != nil {
|
||
return nil, fmt.Errorf("block failed verification due to: %w", err)
|
||
}
|
||
|
||
log.Debug("built block",
|
||
"id", blk.ID(),
|
||
)
|
||
// Marks the current transactions from the mempool as being successfully issued
|
||
// into a block.
|
||
return blk, nil
|
||
}
|
||
|
||
// parseBlock parses [b] into a block to be wrapped by ChainState.
|
||
func (vm *VM) parseBlock(_ context.Context, b []byte) (nodeblock.Block, error) {
|
||
ethBlock := new(types.Block)
|
||
if err := rlp.DecodeBytes(b, ethBlock); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// Note: the status of block is set by ChainState
|
||
block := vm.newBlock(ethBlock)
|
||
// Performing syntactic verification in ParseBlock allows for
|
||
// short-circuiting bad blocks before they are processed by the VM.
|
||
if err := block.syntacticVerify(); err != nil {
|
||
return nil, fmt.Errorf("syntactic block verification failed: %w", err)
|
||
}
|
||
return block, nil
|
||
}
|
||
|
||
func (vm *VM) ParseEthBlock(b []byte) (*types.Block, error) {
|
||
// Parse the raw bytes into an Ethereum block
|
||
ethBlock := new(types.Block)
|
||
if err := rlp.DecodeBytes(b, ethBlock); err != nil {
|
||
return nil, err
|
||
}
|
||
return ethBlock, nil
|
||
}
|
||
|
||
// getBlock attempts to retrieve block [id] from the VM to be wrapped
|
||
// by ChainState.
|
||
// GetBlock implements the ChainVM interface
|
||
func (vm *VM) GetBlock(ctx context.Context, id ids.ID) (nodeblock.Block, error) {
|
||
blk, err := vm.getBlock(ctx, id)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return blk, nil
|
||
}
|
||
|
||
func (vm *VM) getBlock(_ context.Context, id ids.ID) (nodeblock.Block, error) {
|
||
ethBlock := vm.blockChain.GetBlockByHash(common.Hash(id))
|
||
// If [ethBlock] is nil, return [database.ErrNotFound] here
|
||
// so that the miss is considered cacheable.
|
||
if ethBlock == nil {
|
||
return nil, database.ErrNotFound
|
||
}
|
||
// Note: the status of block is set by ChainState
|
||
return vm.newBlock(ethBlock), nil
|
||
}
|
||
|
||
// GetAcceptedBlock attempts to retrieve block [blkID] from the VM. This method
|
||
// only returns accepted blocks.
|
||
func (vm *VM) GetAcceptedBlock(ctx context.Context, blkID ids.ID) (nodeblock.Block, error) {
|
||
// First verify the block is accepted
|
||
ethBlock := vm.blockChain.GetBlockByHash(common.BytesToHash(blkID[:]))
|
||
if ethBlock == nil {
|
||
return nil, database.ErrNotFound
|
||
}
|
||
|
||
// Check if this block is accepted by comparing with canonical chain
|
||
acceptedHash := vm.blockChain.GetCanonicalHash(ethBlock.NumberU64())
|
||
if acceptedHash != ethBlock.Hash() {
|
||
return nil, database.ErrNotFound
|
||
}
|
||
|
||
// Get the block from our State
|
||
blk, err := vm.State.GetBlock(ctx, blkID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// Extract the actual Block from the nodeChain.BlockWrapper
|
||
switch b := blk.(type) {
|
||
case *nodeChain.BlockWrapper:
|
||
if evmBlock, ok := b.Block.(*Block); ok {
|
||
return evmBlock, nil
|
||
}
|
||
return nil, fmt.Errorf("unexpected block type in wrapper: %T", b.Block)
|
||
case *Block:
|
||
return b, nil
|
||
default:
|
||
return nil, fmt.Errorf("unexpected block type: %T", blk)
|
||
}
|
||
}
|
||
|
||
// SetPreference sets what the current tail of the chain is
|
||
func (vm *VM) SetPreference(ctx context.Context, blkID ids.ID) error {
|
||
// Since each internal handler used by [vm.State] always returns a block
|
||
// with non-nil ethBlock value, GetBlockInternal should never return a
|
||
// (*Block) with a nil ethBlock value.
|
||
block, err := vm.GetBlockInternal(ctx, blkID)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to set preference to %s: %w", blkID, err)
|
||
}
|
||
|
||
return vm.blockChain.SetPreference(block.(*Block).ethBlock)
|
||
}
|
||
|
||
// GetBlockIDAtHeight returns the canonical block at [height].
|
||
// Note: the engine assumes that if a block is not found at [height], then
|
||
// [database.ErrNotFound] will be returned. This indicates that the VM has state
|
||
// synced and does not have all historical blocks available.
|
||
func (vm *VM) GetBlockIDAtHeight(_ context.Context, height uint64) (ids.ID, error) {
|
||
// Use LastConsensusAcceptedBlock() instead of LastAcceptedBlock() because
|
||
// the engine expects the block to be visible immediately after Accept() is called.
|
||
// LastAcceptedBlock() returns acceptorTip which is updated asynchronously by the
|
||
// acceptor queue, while LastConsensusAcceptedBlock() returns lastAccepted which
|
||
// is set synchronously by Accept().
|
||
lastAcceptedBlock := vm.blockChain.LastConsensusAcceptedBlock()
|
||
if lastAcceptedBlock.NumberU64() < height {
|
||
return ids.ID{}, database.ErrNotFound
|
||
}
|
||
|
||
hash := vm.blockChain.GetCanonicalHash(height)
|
||
if hash == (common.Hash{}) {
|
||
return ids.ID{}, database.ErrNotFound
|
||
}
|
||
return ids.ID(hash), nil
|
||
}
|
||
|
||
func (vm *VM) Version(context.Context) (string, error) {
|
||
return Version, nil
|
||
}
|
||
|
||
// NewHandler returns a new Handler for a service where:
|
||
// - The handler's functionality is defined by [service]
|
||
// [service] should be a gorilla RPC service (see https://www.gorillatoolkit.org/pkg/rpc/v2)
|
||
// - The name of the service is [name]
|
||
func newHandler(name string, service interface{}) (http.Handler, error) {
|
||
server := luxRPC.NewServer()
|
||
server.RegisterCodec(luxJSON.NewCodec(), "application/json")
|
||
server.RegisterCodec(luxJSON.NewCodec(), "application/json;charset=UTF-8")
|
||
return server, server.RegisterService(service, name)
|
||
}
|
||
|
||
// CreateHandlers makes new http handlers that can handle API calls
|
||
func (vm *VM) CreateHandlers(context.Context) (map[string]http.Handler, error) {
|
||
log.Info("CreateHandlers called", "chainID", vm.chainConfig.ChainID, "adminAPIEnabled", vm.config.AdminAPIEnabled)
|
||
|
||
handler := rpc.NewServer(vm.config.APIMaxDuration.Duration)
|
||
if vm.config.BatchRequestLimit > 0 && vm.config.BatchResponseMaxSize > 0 {
|
||
handler.SetBatchLimits(int(vm.config.BatchRequestLimit), int(vm.config.BatchResponseMaxSize))
|
||
}
|
||
if vm.config.HttpBodyLimit > 0 {
|
||
handler.SetHTTPBodyLimit(int(vm.config.HttpBodyLimit))
|
||
}
|
||
|
||
enabledAPIs := vm.config.EthAPIs()
|
||
log.Info("CreateHandlers attaching eth service", "enabledAPIs", enabledAPIs)
|
||
if err := attachEthService(handler, vm.eth.APIs(), enabledAPIs); err != nil {
|
||
log.Error("CreateHandlers failed to attach eth service", "error", err)
|
||
return nil, err
|
||
}
|
||
log.Info("CreateHandlers eth service attached successfully")
|
||
|
||
// Register admin API with geth RPC server for underscore notation (admin_importChain)
|
||
if vm.config.AdminAPIEnabled {
|
||
log.Info("CreateHandlers registering admin API with geth RPC server")
|
||
adminAPI := NewAdminAPI(vm, os.ExpandEnv(fmt.Sprintf("%s_chain_evm_performance_%s", vm.config.AdminAPIDir, vm.chainAlias)))
|
||
if err := handler.RegisterName("admin", adminAPI); err != nil {
|
||
log.Error("CreateHandlers failed to register admin API", "error", err)
|
||
return nil, fmt.Errorf("failed to register admin API: %w", err)
|
||
}
|
||
log.Info("CreateHandlers admin API registered (admin_importChain enabled)")
|
||
}
|
||
|
||
apis := make(map[string]http.Handler)
|
||
log.Info("CreateHandlers checking admin API", "adminAPIEnabled", vm.config.AdminAPIEnabled)
|
||
if vm.config.AdminAPIEnabled {
|
||
log.Info("CreateHandlers creating admin API handler (Gorilla RPC for backward compat)")
|
||
adminAPI, err := newHandler("admin", NewAdminService(vm, os.ExpandEnv(fmt.Sprintf("%s_chain_evm_performance_%s", vm.config.AdminAPIDir, vm.chainAlias))))
|
||
if err != nil {
|
||
log.Error("CreateHandlers failed to create admin API handler", "error", err)
|
||
return nil, fmt.Errorf("failed to register service for admin API due to %w", err)
|
||
}
|
||
apis[adminEndpoint] = adminAPI
|
||
enabledAPIs = append(enabledAPIs, "evm-admin")
|
||
log.Info("CreateHandlers admin API handler created successfully")
|
||
}
|
||
|
||
log.Info("CreateHandlers checking validators API", "validatorsAPIEnabled", vm.config.ValidatorsAPIEnabled)
|
||
if vm.config.ValidatorsAPIEnabled {
|
||
log.Info("CreateHandlers creating validators API handler")
|
||
validatorsAPI, err := newHandler("validators", &ValidatorsAPI{vm})
|
||
if err != nil {
|
||
log.Error("CreateHandlers failed to create validators API handler", "error", err)
|
||
return nil, fmt.Errorf("failed to register service for validators API due to %w", err)
|
||
}
|
||
apis[validatorsEndpoint] = validatorsAPI
|
||
enabledAPIs = append(enabledAPIs, "validators")
|
||
log.Info("CreateHandlers validators API handler created successfully")
|
||
}
|
||
|
||
// Register evm namespace (evm_backend, evm_backends, evm_setBackend)
|
||
if err := handler.RegisterName("evm", NewEvmBackendAPI(vm)); err != nil {
|
||
return nil, fmt.Errorf("failed to register evm backend API: %w", err)
|
||
}
|
||
enabledAPIs = append(enabledAPIs, "lux")
|
||
|
||
if vm.config.WarpAPIEnabled {
|
||
warpSDKClient := vm.Network.NewClient(luxwarp.SignatureHandlerID)
|
||
signatureAggregator := luxwarp.NewSignatureAggregator(log.Noop(), warpSDKClient)
|
||
|
||
if err := handler.RegisterName("warp", warp.NewAPI(vm.runtime, vm.warpBackend, signatureAggregator, vm.requirePrimaryNetworkSigners)); err != nil {
|
||
return nil, err
|
||
}
|
||
enabledAPIs = append(enabledAPIs, "warp")
|
||
}
|
||
|
||
log.Info("CreateHandlers adding RPC/WS endpoints", "apis", enabledAPIs)
|
||
apis[ethRPCEndpoint] = handler
|
||
apis[ethWSEndpoint] = handler.WebsocketHandlerWithDuration(
|
||
[]string{"*"},
|
||
vm.config.APIMaxDuration.Duration,
|
||
vm.config.WSCPURefillRate.Duration,
|
||
vm.config.WSCPUMaxStored.Duration,
|
||
)
|
||
|
||
vm.rpcHandlers = append(vm.rpcHandlers, handler)
|
||
log.Info("CreateHandlers completed successfully", "numHandlers", len(apis))
|
||
return apis, nil
|
||
}
|
||
|
||
// NewHTTPHandler implements the chain.ChainVM interface
|
||
func (vm *VM) NewHTTPHandler(ctx context.Context) (http.Handler, error) {
|
||
handlers, err := vm.CreateHandlers(ctx)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// Return the main RPC handler as the primary HTTP handler
|
||
if handler, exists := handlers[ethRPCEndpoint]; exists {
|
||
return handler, nil
|
||
}
|
||
|
||
// Fallback to a default handler if no RPC handler exists
|
||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
http.Error(w, "No HTTP handler available", http.StatusNotFound)
|
||
}), nil
|
||
}
|
||
|
||
func (vm *VM) CreateHTTP2Handler(context.Context) (http.Handler, error) {
|
||
return nil, nil
|
||
}
|
||
|
||
/*
|
||
******************************************************************************
|
||
*********************************** Helpers **********************************
|
||
******************************************************************************
|
||
*/
|
||
|
||
// GetCurrentNonce returns the nonce associated with the address at the
|
||
// preferred block
|
||
func (vm *VM) GetCurrentNonce(address common.Address) (uint64, error) {
|
||
// Note: current state uses the state of the preferred block.
|
||
state, err := vm.blockChain.State()
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
return state.GetNonce(address), nil
|
||
}
|
||
|
||
// stageDexAtomic is the host side of the DEX native C<->D atomic seam's cross-domain
|
||
// commit (called from Block.Accept after blockChain.Accept). It derives the flush
|
||
// window from CONSENSUS STATE — the parent block's staged-atomic seq and the accepted
|
||
// block's staged-atomic seq — collects exactly that window's staged ops, and STAGES
|
||
// the advanced node-local marker (dexAtomicSeqKey) into the versiondb's in-memory
|
||
// layer (acceptedBlockDB sits on versiondb). It returns the collected requests and
|
||
// this chain's shared memory so Block.Accept can apply them in the SAME database
|
||
// batch as the marker — the platformvm acceptor pattern (state commit and shared-
|
||
// memory Apply are ONE atomic write). It does NOT call sm.Apply and does NOT commit.
|
||
//
|
||
// THE EXACTLY-ONCE / CHAIN-HALT INVARIANT (the bug this closes): the SM mutation and
|
||
// the seq-marker advance MUST land in one atomic commit. The old code applied to
|
||
// shared memory FIRST and advanced the marker LATER in a separate versiondb.Commit();
|
||
// a crash between them left shared memory mutated but the marker UNADVANCED, so on
|
||
// restart the same window re-collected and sm.Apply hit a duplicate Put (Apply is NOT
|
||
// idempotent) -> Accept fatal -> chain halt. By returning reqs+sm and letting Accept
|
||
// pass the versiondb commit batch to sm.Apply(reqs, batch), the marker and the SM
|
||
// mutation commit all-or-nothing: a crash applies-and-marks both or neither, and a
|
||
// re-accept of an already-applied window sees an advanced marker -> empty window ->
|
||
// no duplicate Put -> no halt.
|
||
//
|
||
// SHARED MEMORY ABSENT (single-chain dev): runtime/SM nil -> returns (nil, nil) so
|
||
// Accept takes the plain versiondb.Commit() path. A malformed staged op or a seq
|
||
// regression is FATAL (returns an error that fails block accept) — value must never
|
||
// move inconsistently with consensus.
|
||
func (vm *VM) stageDexAtomic(b *Block) (map[ids.ID]*atomic.Requests, atomic.SharedMemory, error) {
|
||
if vm.runtime == nil {
|
||
return nil, nil, nil
|
||
}
|
||
sm := vm.runtime.GetSharedMemory()
|
||
if sm == nil {
|
||
return nil, nil, nil // single-chain dev: no atomic peer wired.
|
||
}
|
||
|
||
// Parent and accepted block state roots define the consensus flush window.
|
||
parentRoot := b.ethBlock.ParentHash() // header parent hash != state root; resolve state via parent header.
|
||
parentHeader := vm.blockChain.GetHeaderByHash(common.Hash(b.ethBlock.ParentHash()))
|
||
if parentHeader != nil {
|
||
parentRoot = parentHeader.Root
|
||
}
|
||
parentState, err := vm.blockChain.StateAt(parentRoot)
|
||
if err != nil {
|
||
// Parent state may be pruned on a deep restart; fall back to the node-local
|
||
// marker as the window's lower bound (the optimization path).
|
||
parentState = nil
|
||
}
|
||
currentState, err := vm.blockChain.StateAt(b.ethBlock.Root())
|
||
if err != nil {
|
||
return nil, nil, fmt.Errorf("dex atomic flush: accepted state unavailable: %w", err)
|
||
}
|
||
|
||
var fromSeq uint64
|
||
if parentState != nil {
|
||
fromSeq = dex.ReadStagedAtomicSeq(parentState)
|
||
} else {
|
||
fromSeq = vm.readDexAtomicMarker()
|
||
}
|
||
// Never re-flush below the node-local marker (skip windows already applied). This
|
||
// is the crash-recovery guard: after a clean atomic commit the marker == toSeq, so
|
||
// a re-accept of the same block collects an EMPTY window.
|
||
if m := vm.readDexAtomicMarker(); m > fromSeq {
|
||
fromSeq = m
|
||
}
|
||
toSeq := dex.ReadStagedAtomicSeq(currentState)
|
||
if toSeq < fromSeq {
|
||
return nil, nil, dex.ErrAtomicSeqRegression
|
||
}
|
||
if toSeq == fromSeq {
|
||
return nil, nil, nil // no new staged ops this block.
|
||
}
|
||
|
||
reqs, cerr := dex.CollectStagedAtomicRange(currentState, fromSeq, toSeq)
|
||
if cerr != nil {
|
||
return nil, nil, cerr // malformed staged op -> fatal.
|
||
}
|
||
// STAGE the advanced marker into the versiondb in-memory layer (acceptedBlockDB ->
|
||
// versiondb). Block.Accept snapshots versiondb into a batch and passes it to
|
||
// sm.Apply(reqs, batch), so this marker write and the shared-memory mutation commit
|
||
// in ONE atomic database write.
|
||
var buf [8]byte
|
||
binary.BigEndian.PutUint64(buf[:], toSeq)
|
||
if perr := vm.acceptedBlockDB.Put(dexAtomicSeqKey, buf[:]); perr != nil {
|
||
return nil, nil, fmt.Errorf("dex atomic marker persist: %w", perr)
|
||
}
|
||
if len(reqs) == 0 {
|
||
return nil, nil, nil
|
||
}
|
||
return reqs, sm, nil
|
||
}
|
||
|
||
// readDexAtomicMarker reads the node-local last-flushed staged-atomic seq (0 if unset).
|
||
func (vm *VM) readDexAtomicMarker() uint64 {
|
||
b, err := vm.acceptedBlockDB.Get(dexAtomicSeqKey)
|
||
if err != nil || len(b) != 8 {
|
||
return 0
|
||
}
|
||
return binary.BigEndian.Uint64(b)
|
||
}
|
||
|
||
func (vm *VM) chainConfigExtra() *extras.ChainConfig {
|
||
return params.GetExtra(vm.chainConfig)
|
||
}
|
||
|
||
func (vm *VM) rules(number *big.Int, time uint64) extras.Rules {
|
||
ethrules := vm.chainConfig.Rules(number, params.IsMergeTODO, time)
|
||
return *params.GetExtrasRules(ethrules, vm.chainConfig, time)
|
||
}
|
||
|
||
// currentRules returns the chain rules for the current block.
|
||
func (vm *VM) currentRules() extras.Rules {
|
||
header := vm.eth.APIBackend.CurrentHeader()
|
||
return vm.rules(header.Number, header.Time)
|
||
}
|
||
|
||
// requirePrimaryNetworkSigners returns true if warp messages from the primary
|
||
// network must be signed by the primary network validators.
|
||
// This is necessary when the chain is not validating the primary network.
|
||
func (vm *VM) requirePrimaryNetworkSigners() bool {
|
||
switch c := vm.currentRules().Precompiles[warpcontract.ContractAddress].(type) {
|
||
case *warpcontract.Config:
|
||
return c.RequirePrimaryNetworkSigners
|
||
default: // includes nil due to non-presence
|
||
return false
|
||
}
|
||
}
|
||
|
||
func (vm *VM) startContinuousProfiler() {
|
||
// If the profiler directory is empty, return immediately
|
||
// without creating or starting a continuous profiler.
|
||
if vm.config.ContinuousProfilerDir == "" {
|
||
return
|
||
}
|
||
vm.profiler = profiler.NewContinuous(
|
||
filepath.Join(vm.config.ContinuousProfilerDir),
|
||
vm.config.ContinuousProfilerFrequency.Duration,
|
||
vm.config.ContinuousProfilerMaxFiles,
|
||
)
|
||
defer vm.profiler.Shutdown()
|
||
|
||
vm.shutdownWg.Add(1)
|
||
go func() {
|
||
defer vm.shutdownWg.Done()
|
||
log.Info("Dispatching continuous profiler", "dir", vm.config.ContinuousProfilerDir, "freq", vm.config.ContinuousProfilerFrequency, "maxFiles", vm.config.ContinuousProfilerMaxFiles)
|
||
err := vm.profiler.Dispatch()
|
||
if err != nil {
|
||
log.Error("continuous profiler failed", "err", err)
|
||
}
|
||
}()
|
||
// Wait for shutdownChan to be closed
|
||
<-vm.shutdownChan
|
||
}
|
||
|
||
// readLastAccepted reads the last accepted hash from [acceptedBlockDB] and returns the
|
||
// last accepted block hash and height by reading directly from [vm.chaindb] instead of relying
|
||
// on [chain].
|
||
// Note: assumes [vm.chaindb] and [vm.genesisHash] have been initialized.
|
||
func (vm *VM) readLastAccepted() (common.Hash, uint64, error) {
|
||
// Attempt to load last accepted block to determine if it is necessary to
|
||
// initialize state with the genesis block.
|
||
lastAcceptedBytes, lastAcceptedErr := vm.acceptedBlockDB.Get(lastAcceptedKey)
|
||
switch {
|
||
case lastAcceptedErr == database.ErrNotFound:
|
||
return vm.genesisHash, 0, nil
|
||
case lastAcceptedErr != nil:
|
||
return common.Hash{}, 0, fmt.Errorf("failed to get last accepted block ID: %w", lastAcceptedErr)
|
||
case len(lastAcceptedBytes) != common.HashLength:
|
||
return common.Hash{}, 0, fmt.Errorf("last accepted bytes should have been length %d, but found %d", common.HashLength, len(lastAcceptedBytes))
|
||
default:
|
||
lastAcceptedHash := common.BytesToHash(lastAcceptedBytes)
|
||
height, found := rawdb.ReadHeaderNumber(vm.chaindb, lastAcceptedHash)
|
||
if !found {
|
||
return common.Hash{}, 0, fmt.Errorf("failed to retrieve header number of last accepted block: %s", lastAcceptedHash)
|
||
}
|
||
return lastAcceptedHash, height, nil
|
||
}
|
||
}
|
||
|
||
// attachEthService registers the backend RPC services provided by Ethereum
|
||
// to the provided handler under their assigned namespaces.
|
||
func attachEthService(handler *rpc.Server, apis []rpc.API, names []string) error {
|
||
enabledServicesSet := make(map[string]struct{})
|
||
for _, ns := range names {
|
||
// handle pre geth v1.10.20 api names as aliases for their updated values
|
||
// to allow configurations to be backwards compatible.
|
||
if newName, isLegacy := legacyApiNames[ns]; isLegacy {
|
||
log.Info("deprecated api name referenced in configuration.", "deprecated", ns, "new", newName)
|
||
enabledServicesSet[newName] = struct{}{}
|
||
continue
|
||
}
|
||
|
||
enabledServicesSet[ns] = struct{}{}
|
||
}
|
||
|
||
apiSet := make(map[string]rpc.API)
|
||
for _, api := range apis {
|
||
if existingAPI, exists := apiSet[api.Name]; exists {
|
||
return fmt.Errorf("duplicated API name: %s, namespaces %s and %s", api.Name, api.Namespace, existingAPI.Namespace)
|
||
}
|
||
apiSet[api.Name] = api
|
||
}
|
||
|
||
for name := range enabledServicesSet {
|
||
api, exists := apiSet[name]
|
||
if !exists {
|
||
return fmt.Errorf("API service %s not found", name)
|
||
}
|
||
if err := handler.RegisterName(api.Namespace, api.Service); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func (vm *VM) Connected(ctx context.Context, nodeID ids.NodeID, ver *nodeblock.VersionInfo) error {
|
||
vm.vmLock.Lock()
|
||
defer vm.vmLock.Unlock()
|
||
|
||
// Convert VersionInfo to the network's expected version type (p2p.Network uses github.com/luxfi/version)
|
||
// VersionInfo is an alias for version.Application, so ver already has Name, Major, Minor, Patch fields
|
||
var nodeVersion *p2pversion.Application
|
||
if ver != nil {
|
||
nodeVersion = &p2pversion.Application{
|
||
Name: ver.Name,
|
||
Major: ver.Major,
|
||
Minor: ver.Minor,
|
||
Patch: ver.Patch,
|
||
}
|
||
}
|
||
return vm.Network.Connected(ctx, nodeID, nodeVersion)
|
||
}
|
||
|
||
func (vm *VM) Disconnected(ctx context.Context, nodeID ids.NodeID) error {
|
||
vm.vmLock.Lock()
|
||
defer vm.vmLock.Unlock()
|
||
|
||
// Validator manager does not yet expose a Disconnect method.
|
||
|
||
return vm.Network.Disconnected(ctx, nodeID)
|
||
}
|
||
|
||
// AppRequestFailed implements the VM interface
|
||
func (vm *VM) AppRequestFailed(ctx context.Context, nodeID ids.NodeID, requestID uint32, appErr *warp.AppError) error {
|
||
// The Network interface doesn't expose AppRequestFailed directly
|
||
// We need to handle this at the VM level by logging the error
|
||
log.Debug("AppRequestFailed", "nodeID", nodeID, "requestID", requestID, "error", appErr)
|
||
// The network's response handler will handle the timeout internally
|
||
return nil
|
||
}
|
||
|
||
// CrossChainAppRequestFailed implements the VM interface
|
||
func (vm *VM) CrossChainAppRequestFailed(ctx context.Context, chainID ids.ID, requestID uint32, appErr *warp.AppError) error {
|
||
// Cross-chain app requests are not currently supported
|
||
// Just log and return nil to satisfy the interface
|
||
log.Debug("CrossChainAppRequestFailed called", "chainID", chainID, "requestID", requestID, "error", appErr.Message)
|
||
return nil
|
||
}
|
||
|
||
// StateSyncEnabled implements the StateSyncableVM interface
|
||
func (vm *VM) StateSyncEnabled(ctx context.Context) (bool, error) {
|
||
return vm.config.StateSyncEnabled, nil
|
||
}
|
||
|
||
// GetOngoingSyncStateSummary implements the StateSyncableVM interface
|
||
func (vm *VM) GetOngoingSyncStateSummary(ctx context.Context) (nodeblock.StateSummary, error) {
|
||
// Ongoing sync resumption is not implemented; return ErrNotFound to
|
||
// signal that no in-progress summary exists.
|
||
return nil, database.ErrNotFound
|
||
}
|
||
|
||
// GetLastStateSummary implements the StateSyncableVM interface
|
||
func (vm *VM) GetLastStateSummary(ctx context.Context) (nodeblock.StateSummary, error) {
|
||
summary, err := vm.StateSyncServer.GetLastStateSummary(ctx)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
// Cast and wrap the summary to match node's interface
|
||
if syncSummary, ok := summary.(message.SyncSummary); ok {
|
||
return &stateSummaryWrapper{summary: syncSummary}, nil
|
||
}
|
||
return nil, fmt.Errorf("summary does not implement message.SyncSummary")
|
||
}
|
||
|
||
// stateSummaryWrapper wraps message.SyncSummary to implement nodeblock.StateSummary
|
||
type stateSummaryWrapper struct {
|
||
summary message.SyncSummary
|
||
}
|
||
|
||
func (s *stateSummaryWrapper) Accept(ctx context.Context) (nodeblock.StateSyncMode, error) {
|
||
consensusMode, err := s.summary.Accept(ctx)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
// Convert consensus StateSyncMode to node StateSyncMode
|
||
// The values should be the same, just different types
|
||
return nodeblock.StateSyncMode(consensusMode), nil
|
||
}
|
||
|
||
func (s *stateSummaryWrapper) Bytes() []byte {
|
||
return s.summary.Bytes()
|
||
}
|
||
|
||
func (s *stateSummaryWrapper) Height() uint64 {
|
||
return s.summary.Height()
|
||
}
|
||
|
||
func (s *stateSummaryWrapper) ID() ids.ID {
|
||
return s.summary.ID()
|
||
}
|
||
|
||
// ParseStateSummary implements the StateSyncableVM interface
|
||
func (vm *VM) ParseStateSummary(ctx context.Context, summaryBytes []byte) (nodeblock.StateSummary, error) {
|
||
// Delegate to StateSyncClient which has the proper acceptImpl callback
|
||
if vm.StateSyncClient != nil {
|
||
summary, err := vm.StateSyncClient.ParseStateSummary(ctx, summaryBytes)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
// The StateSyncClient returns a message.SyncSummary which we need to wrap
|
||
if syncSummary, ok := summary.(message.SyncSummary); ok {
|
||
return &stateSummaryWrapper{summary: syncSummary}, nil
|
||
}
|
||
// If not a SyncSummary, try to cast directly to our wrapper
|
||
if wrapper, ok := summary.(*stateSummaryWrapper); ok {
|
||
return wrapper, nil
|
||
}
|
||
return nil, fmt.Errorf("unexpected summary type from StateSyncClient: %T", summary)
|
||
}
|
||
|
||
// Fallback: Parse without acceptImpl (for server-side parsing)
|
||
summary, err := message.NewSyncSummaryFromBytes(summaryBytes, nil)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return &stateSummaryWrapper{summary: summary}, nil
|
||
}
|
||
|
||
// GetStateSummary implements the StateSyncableVM interface
|
||
func (vm *VM) GetStateSummary(ctx context.Context, height uint64) (nodeblock.StateSummary, error) {
|
||
summary, err := vm.StateSyncServer.GetStateSummary(ctx, height)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
// Cast and wrap the summary to match node's interface
|
||
if syncSummary, ok := summary.(message.SyncSummary); ok {
|
||
return &stateSummaryWrapper{summary: syncSummary}, nil
|
||
}
|
||
return nil, fmt.Errorf("summary does not implement message.SyncSummary")
|
||
}
|
||
|
||
// warpVerifierAdapter adapts our warp.Backend to warp.Verifier
|
||
type warpVerifierAdapter struct {
|
||
backend warp.Backend
|
||
}
|
||
|
||
// Verify implements warp.Verifier interface
|
||
func (w *warpVerifierAdapter) Verify(ctx context.Context, core *luxwarp.Core, justification []byte) error {
|
||
return w.backend.Verify(ctx, core, justification)
|
||
}
|