Files
evm/consensus/dummy/consensus.go
zeekay bf4f4a4328 fix: atomic acceptedBlockDB updates and import resilience
- admin_api.go: add afterCommit callback to importBlocksFromFile so
  acceptedBlockDB is updated atomically with each state commit,
  eliminating the crash window between state persist and DB update
- admin_api.go: resume import from current head (skip already-imported
  blocks) for restart-safety after partial imports
- vm.go: simplify readLastAccepted to return genesis on ErrNotFound
  without recovery hacks
- consensus.go: simplify Cancun/beacon validation — Lux networks do
  not use Ethereum beacon chain; only reject non-zero BlobGasUsed
- blockchain.go: chunked batch deletes in reorg to avoid BadgerDB
  ErrTxnTooBig on large chains
- blockchain.go: tolerate "not supported" from snapshot flatten
  (pathdb backend) instead of log.Crit crash
- genesis.go: fix duplicate comment
2026-03-09 23:36:08 -07:00

463 lines
14 KiB
Go

// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package dummy
import (
"errors"
"fmt"
"math/big"
"time"
"github.com/luxfi/evm/consensus"
"github.com/luxfi/evm/core/state"
"github.com/luxfi/evm/params"
"github.com/luxfi/evm/params/extras"
"github.com/luxfi/evm/plugin/evm/customtypes"
"github.com/luxfi/evm/plugin/evm/vmerrors"
"github.com/luxfi/evm/utils"
"github.com/luxfi/geth/common"
"github.com/luxfi/geth/core/types"
"github.com/luxfi/geth/trie"
"github.com/luxfi/timer/mockable"
customheader "github.com/luxfi/evm/plugin/evm/header"
)
var (
allowedFutureBlockTime = 10 * time.Second // Max time from current time allowed for blocks, before they're considered future blocks
ErrInsufficientBlockGas = errors.New("insufficient gas to cover the block cost")
errInvalidBlockTime = errors.New("timestamp less than parent's")
errUnclesUnsupported = errors.New("uncles unsupported")
)
type Mode struct {
ModeSkipHeader bool
ModeSkipBlockFee bool
ModeSkipCoinbase bool
}
type (
DummyEngine struct {
clock *mockable.Clock
consensusMode Mode
}
)
func NewDummyEngine(
mode Mode,
clock *mockable.Clock,
) *DummyEngine {
return &DummyEngine{
clock: clock,
consensusMode: mode,
}
}
func NewETHFaker() *DummyEngine {
return &DummyEngine{
clock: &mockable.Clock{},
consensusMode: Mode{ModeSkipBlockFee: true},
}
}
func NewFaker() *DummyEngine {
return &DummyEngine{
clock: &mockable.Clock{},
}
}
func NewFakerWithClock(clock *mockable.Clock) *DummyEngine {
return &DummyEngine{
clock: clock,
}
}
func NewFakerWithMode(mode Mode) *DummyEngine {
return &DummyEngine{
clock: &mockable.Clock{},
consensusMode: mode,
}
}
func NewFakerWithModeAndClock(mode Mode, clock *mockable.Clock) *DummyEngine {
return &DummyEngine{
clock: clock,
consensusMode: mode,
}
}
func NewCoinbaseFaker() *DummyEngine {
return &DummyEngine{
clock: &mockable.Clock{},
consensusMode: Mode{ModeSkipCoinbase: true},
}
}
func NewFullFaker() *DummyEngine {
return &DummyEngine{
clock: &mockable.Clock{},
consensusMode: Mode{ModeSkipHeader: true},
}
}
// verifyCoinbase checks that the coinbase is valid for the given [header] and [parent].
func (eng *DummyEngine) verifyCoinbase(header *types.Header, parent *types.Header, chain consensus.ChainHeaderReader) error {
if eng.consensusMode.ModeSkipCoinbase {
return nil
}
// get the coinbase configured at parent
configuredAddressAtParent, isAllowFeeRecipients, err := chain.GetCoinbaseAt(parent)
if err != nil {
return fmt.Errorf("failed to get coinbase at %v: %w", header.Hash(), err)
}
if isAllowFeeRecipients {
// if fee recipients are allowed we don't need to check the coinbase
return nil
}
// we fetch the configured coinbase at the parent's state
// to check against the coinbase in [header].
if configuredAddressAtParent != header.Coinbase {
return fmt.Errorf("%w: %v does not match required coinbase address %v", vmerrors.ErrInvalidCoinbase, header.Coinbase, configuredAddressAtParent)
}
return nil
}
func verifyHeaderGasFields(config *extras.ChainConfig, header *types.Header, parent *types.Header, chain consensus.ChainHeaderReader, mode Mode) error {
// We verify the current block by checking the parent fee config
// this is because the current block cannot set the fee config for itself
// Fee config might depend on the state when precompile is activated
// but we don't know the final state while forming the block.
// See worker package for more details.
feeConfig, _, err := chain.GetFeeConfigAt(parent)
if err != nil {
return err
}
if err := customheader.VerifyGasUsed(config, feeConfig, parent, header); err != nil {
return err
}
if err := customheader.VerifyGasLimit(config, feeConfig, parent, header); err != nil {
return err
}
if err := customheader.VerifyExtraPrefix(config, parent, header); err != nil {
return err
}
// Verify header.BaseFee matches the expected value.
// Skip base fee verification if ModeSkipBlockFee is set
if !mode.ModeSkipBlockFee {
expectedBaseFee, err := customheader.BaseFee(config, feeConfig, parent, header.Time)
if err != nil {
return fmt.Errorf("failed to calculate base fee: %w", err)
}
if !utils.BigEqual(header.BaseFee, expectedBaseFee) {
return fmt.Errorf("expected base fee (%d), found (%d)", expectedBaseFee, header.BaseFee)
}
}
// Enforce BlockGasCost constraints (skip if ModeSkipBlockFee is set)
if !mode.ModeSkipBlockFee {
expectedBlockGasCost := customheader.BlockGasCost(
config,
feeConfig,
parent,
header.Time,
)
headerExtra := customtypes.GetHeaderExtra(header)
// Accept nil or zero BlockGasCost as equivalent
if headerExtra.BlockGasCost == nil {
headerExtra.BlockGasCost = big.NewInt(0)
}
if !blockGasCostEqual(headerExtra.BlockGasCost, expectedBlockGasCost) {
return fmt.Errorf("invalid block gas cost: have %v, want %v", headerExtra.BlockGasCost, expectedBlockGasCost)
}
}
return nil
}
// modified from consensus.go
func (eng *DummyEngine) verifyHeader(chain consensus.ChainHeaderReader, header *types.Header, parent *types.Header, uncle bool) error {
// Ensure that we do not verify an uncle
if uncle {
return errUnclesUnsupported
}
// Verify the extra data is well-formed.
config := params.GetExtra(chain.Config())
rules := config.GetLuxRules(header.Time)
if err := customheader.VerifyExtra(rules, header.Extra); err != nil {
return err
}
// Ensure gas-related header fields are correct
if err := verifyHeaderGasFields(config, header, parent, chain, eng.consensusMode); err != nil {
return err
}
// Ensure that coinbase is valid
if err := eng.verifyCoinbase(header, parent, chain); err != nil {
return err
}
// Verify the header's timestamp
if header.Time > uint64(eng.clock.Time().Add(allowedFutureBlockTime).Unix()) {
return consensus.ErrFutureBlock
}
// Verify the header's timestamp is not earlier than parent's
// it does include equality(==), so multiple blocks per second is ok
if header.Time < parent.Time {
return errInvalidBlockTime
}
// Verify that the block number is parent's +1
if diff := new(big.Int).Sub(header.Number, parent.Number); diff.Cmp(big.NewInt(1)) != 0 {
return consensus.ErrInvalidNumber
}
// Verify excessBlobGas / blobGasUsed / parentBeaconRoot
// NOTE: Lux networks do NOT use Ethereum beacon chain or blob transactions.
// Allow ExcessBlobGas=0 and BlobGasUsed=0 regardless of Cancun activation,
// since Lux EVM blocks may include these zero-value fields.
// Only reject if blob gas was actually USED (non-zero), which should never happen.
if header.BlobGasUsed != nil && *header.BlobGasUsed > 0 {
return fmt.Errorf("blobs not enabled on lux networks: used %d blob gas, expected 0", *header.BlobGasUsed)
}
return nil
}
func (*DummyEngine) Author(header *types.Header) (common.Address, error) {
return header.Coinbase, nil
}
func (eng *DummyEngine) VerifyHeader(chain consensus.ChainHeaderReader, header *types.Header) error {
// If we're running a full engine faking, accept any input as valid
if eng.consensusMode.ModeSkipHeader {
return nil
}
// Short circuit if the header is known, or it's parent not
number := header.Number.Uint64()
if chain.GetHeader(header.Hash(), number) != nil {
return nil
}
parent := chain.GetHeader(header.ParentHash, number-1)
if parent == nil {
return consensus.ErrUnknownAncestor
}
// Sanity checks passed, do a proper verification
return eng.verifyHeader(chain, header, parent, false)
}
func (*DummyEngine) VerifyUncles(chain consensus.ChainReader, block *types.Block) error {
if len(block.Uncles()) > 0 {
return errUnclesUnsupported
}
return nil
}
func (*DummyEngine) Prepare(chain consensus.ChainHeaderReader, header *types.Header) error {
header.Difficulty = big.NewInt(1)
return nil
}
func (eng *DummyEngine) verifyBlockFee(
baseFee *big.Int,
requiredBlockGasCost *big.Int,
txs []*types.Transaction,
receipts []*types.Receipt,
) error {
if eng.consensusMode.ModeSkipBlockFee {
return nil
}
if baseFee == nil || baseFee.Sign() <= 0 {
return fmt.Errorf("invalid base fee (%d) in EVM", baseFee)
}
if requiredBlockGasCost == nil || !requiredBlockGasCost.IsUint64() {
return fmt.Errorf("invalid block gas cost (%d) in EVM", requiredBlockGasCost)
}
var (
gasUsed = new(big.Int)
blockFeeContribution = new(big.Int)
totalBlockFee = new(big.Int)
)
// Calculate the total excess over the base fee that was paid towards the block fee
for i, receipt := range receipts {
// Each transaction contributes the excess over the baseFee towards the totalBlockFee
// This should be equivalent to the sum of the "priority fees" within EIP-1559.
txFeePremium, err := txs[i].EffectiveGasTip(baseFee)
if err != nil {
return err
}
// Multiply the [txFeePremium] by the gasUsed in the transaction since this gives the total coin that was paid
// above the amount required if the transaction had simply paid the minimum base fee for the block.
//
// Ex. LegacyTx paying a gas price of 100 gwei for 1M gas in a block with a base fee of 10 gwei.
// Total Fee = 100 gwei * 1M gas
// Minimum Fee = 10 gwei * 1M gas (minimum fee that would have been accepted for this transaction)
// Fee Premium = 90 gwei
// Total Overpaid = 90 gwei * 1M gas
blockFeeContribution.Mul(txFeePremium, gasUsed.SetUint64(receipt.GasUsed))
totalBlockFee.Add(totalBlockFee, blockFeeContribution)
}
// Calculate how much gas the [totalBlockFee] would purchase at the price level
// set by the base fee of this block.
blockGas := new(big.Int).Div(totalBlockFee, baseFee)
// Require that the amount of gas purchased by the effective tips within the
// block covers at least `requiredBlockGasCost`.
//
// NOTE: To determine the required block fee, multiply
// `requiredBlockGasCost` by `baseFee`.
if blockGas.Cmp(requiredBlockGasCost) < 0 {
return fmt.Errorf("%w: expected %d but got %d",
ErrInsufficientBlockGas,
requiredBlockGasCost,
blockGas,
)
}
return nil
}
func (eng *DummyEngine) Finalize(chain consensus.ChainHeaderReader, block *types.Block, parent *types.Header, state *state.StateDB, receipts []*types.Receipt) error {
// If we're running a full engine faking, skip finalization checks
if eng.consensusMode.ModeSkipHeader {
return nil
}
config := params.GetExtra(chain.Config())
timestamp := block.Time()
// we use the parent to determine the fee config
// since the current block has not been finalized yet.
feeConfig, _, err := chain.GetFeeConfigAt(parent)
if err != nil {
return err
}
// Verify the BlockGasCost set in the header matches the expected value.
blockGasCost := customtypes.BlockGasCost(block)
expectedBlockGasCost := customheader.BlockGasCost(
config,
feeConfig,
parent,
timestamp,
)
// For genesis and test blocks, treat nil as 0 for blockGasCost
if blockGasCost == nil && expectedBlockGasCost != nil && expectedBlockGasCost.Sign() == 0 {
blockGasCost = big.NewInt(0)
}
if !utils.BigEqual(blockGasCost, expectedBlockGasCost) {
return fmt.Errorf("invalid blockGasCost: have %d, want %d", blockGasCost, expectedBlockGasCost)
}
if config.IsEVM(timestamp) {
// Verify the block fee was paid.
if err := eng.verifyBlockFee(
block.BaseFee(),
blockGasCost,
block.Transactions(),
receipts,
); err != nil {
return err
}
}
return nil
}
func (eng *DummyEngine) FinalizeAndAssemble(chain consensus.ChainHeaderReader, header *types.Header, parent *types.Header, state *state.StateDB, txs []*types.Transaction,
uncles []*types.Header, receipts []*types.Receipt,
) (*types.Block, error) {
// we use the parent to determine the fee config
// since the current block has not been finalized yet.
feeConfig, _, err := chain.GetFeeConfigAt(parent)
if err != nil {
return nil, err
}
config := params.GetExtra(chain.Config())
// Calculate the required block gas cost for this block.
// Note: We create a new HeaderExtra struct and store it AFTER the header
// is fully finalized (after Root is set) so the hash-based storage works.
headerExtra := &customtypes.HeaderExtra{
BlockGasCost: customheader.BlockGasCost(
config,
feeConfig,
parent,
header.Time,
),
}
if config.IsEVM(header.Time) {
// Verify that this block covers the block fee.
if err := eng.verifyBlockFee(
header.BaseFee,
headerExtra.BlockGasCost,
txs,
receipts,
); err != nil {
return nil, err
}
}
// finalize the header.Extra
extraPrefix, err := customheader.ExtraPrefix(config, parent, header)
if err != nil {
return nil, fmt.Errorf("failed to calculate new header.Extra: %w", err)
}
// In the miner, predicate bytes are appended to an empty header.Extra (not after prefix).
// So header.Extra at this point IS the predicate bytes (if any were added).
// We need to preserve them and append after the computed prefix.
predicateBytes := header.Extra // The raw predicate bytes from miner
// Set header.Extra to prefix + predicate bytes
header.Extra = customheader.SetPredicateBytesInExtra(extraPrefix, predicateBytes)
// commit the final state root
header.Root = state.IntermediateRoot(chain.Config().IsEIP158(header.Number))
// Header seems complete, assemble into a block and return
body := &types.Body{
Transactions: txs,
Uncles: uncles,
}
// EIP-4895: Include empty withdrawals for Shanghai to preserve WithdrawalsHash
if chain.Config().IsShanghai(header.Number, header.Time) {
body.Withdrawals = make([]*types.Withdrawal, 0)
}
block := types.NewBlock(
header, body, receipts, trie.NewStackTrie(nil),
)
// Store the header extras AFTER NewBlock because NewBlock recomputes
// TxHash, ReceiptHash, Bloom, etc., which changes the header hash.
// We must store using the block's final header hash for hash-based lookup to work.
customtypes.SetHeaderExtra(block.Header(), headerExtra)
return block, nil
}
func (*DummyEngine) CalcDifficulty(chain consensus.ChainHeaderReader, time uint64, parent *types.Header) *big.Int {
return big.NewInt(1)
}
func (*DummyEngine) Close() error {
return nil
}
// blockGasCostEqual compares two block gas costs, treating nil and zero as equivalent.
// This handles the case where test blocks may have nil gas cost but expected gas cost is 0.
func blockGasCostEqual(actual, expected *big.Int) bool {
// If both are nil, they're equal
if actual == nil && expected == nil {
return true
}
// If one is nil and the other is zero, they're equal
if actual == nil && expected != nil && expected.Sign() == 0 {
return true
}
if expected == nil && actual != nil && actual.Sign() == 0 {
return true
}
// Otherwise use normal big.Int comparison
return utils.BigEqual(actual, expected)
}