Files

506 lines
16 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/coreth/consensus"
"github.com/luxfi/coreth/consensus/misc/eip4844"
"github.com/luxfi/coreth/params"
"github.com/luxfi/coreth/params/extras"
"github.com/luxfi/coreth/plugin/evm/customtypes"
"github.com/luxfi/coreth/utils"
"github.com/luxfi/geth/common"
"github.com/luxfi/geth/core/state"
"github.com/luxfi/geth/core/types"
"github.com/luxfi/geth/trie"
"github.com/luxfi/timer/mockable"
"github.com/luxfi/vm/components/gas"
customheader "github.com/luxfi/coreth/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")
errExtDataGasUsedNil = errors.New("extDataGasUsed is nil")
errExtDataGasUsedTooLarge = errors.New("extDataGasUsed is not uint64")
)
type Mode struct {
ModeSkipHeader bool
ModeSkipBlockFee bool
ModeSkipCoinbase bool
}
type (
OnFinalizeAndAssembleCallbackType = func(
header *types.Header,
parent *types.Header,
state *state.StateDB,
txs []*types.Transaction,
) (
extraData []byte,
blockFeeContribution *big.Int,
extDataGasUsed *big.Int,
err error,
)
OnExtraStateChangeType = func(
block *types.Block,
parent *types.Header,
statedb *state.StateDB,
) (
blockFeeContribution *big.Int,
extDataGasUsed *big.Int,
err error,
)
ConsensusCallbacks struct {
OnFinalizeAndAssemble OnFinalizeAndAssembleCallbackType
OnExtraStateChange OnExtraStateChangeType
}
DummyEngine struct {
cb ConsensusCallbacks
clock *mockable.Clock
consensusMode Mode
desiredTargetExcess *gas.Gas
}
)
func NewDummyEngine(
cb ConsensusCallbacks,
mode Mode,
clock *mockable.Clock,
desiredTargetExcess *gas.Gas,
) *DummyEngine {
return &DummyEngine{
cb: cb,
clock: clock,
consensusMode: mode,
desiredTargetExcess: desiredTargetExcess,
}
}
func NewETHFaker() *DummyEngine {
return &DummyEngine{
clock: &mockable.Clock{},
consensusMode: Mode{ModeSkipBlockFee: true},
}
}
func NewFaker() *DummyEngine {
return &DummyEngine{
clock: &mockable.Clock{},
}
}
func NewFakerWithClock(cb ConsensusCallbacks, clock *mockable.Clock) *DummyEngine {
return &DummyEngine{
cb: cb,
clock: clock,
}
}
func NewFakerWithCallbacks(cb ConsensusCallbacks) *DummyEngine {
return &DummyEngine{
cb: cb,
clock: &mockable.Clock{},
}
}
func NewFakerWithMode(cb ConsensusCallbacks, mode Mode) *DummyEngine {
return &DummyEngine{
cb: cb,
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},
}
}
func verifyHeaderGasFields(config *extras.ChainConfig, header *types.Header, parent *types.Header) error {
// Activate-all-implicitly: every Lux chain runs the dynamic-fee gas
// pipeline. There is no opt-out path.
if err := customheader.VerifyGasUsed(config, parent, header); err != nil {
return err
}
if err := customheader.VerifyGasLimit(config, parent, header); err != nil {
return err
}
if err := customheader.VerifyExtraPrefix(config, parent, header); err != nil {
return err
}
// Verify header.BaseFee matches the expected value.
expectedBaseFee, err := customheader.BaseFee(config, 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)
}
headerExtra := customtypes.GetHeaderExtra(header)
// Enforce BlockGasCost constraints
expectedBlockGasCost := customheader.BlockGasCost(
config,
parent,
header.Time,
)
// may be nil in historic blocks. Accept nil as equivalent to 0 for these blocks.
blockGasCostValid := utils.BigEqual(headerExtra.BlockGasCost, expectedBlockGasCost)
if !blockGasCostValid {
// Check if this is a Genesis EVM block with nil BlockGasCost where 0 is expected
if headerExtra.BlockGasCost == nil && expectedBlockGasCost != nil && expectedBlockGasCost.Sign() == 0 {
blockGasCostValid = true
}
}
if !blockGasCostValid {
return fmt.Errorf("invalid block gas cost: have %d, want %d", headerExtra.BlockGasCost, expectedBlockGasCost)
}
// historical "Genesis EVM" carve-out for missing ExtDataGasUsed is gone.
// ExtDataGasUsed correctness is checked during block validation
// (when the validator has access to the block contents)
if headerExtra.ExtDataGasUsed == nil {
return errExtDataGasUsedNil
}
if !headerExtra.ExtDataGasUsed.IsUint64() {
return errExtDataGasUsedTooLarge
}
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 := chain.Config()
configExtra := params.GetExtra(config)
rules := configExtra.GetLuxRules(header.Time)
if err := customheader.VerifyExtra(rules, header.Extra); err != nil {
return err
}
// Ensure gas-related header fields are correct
if err := verifyHeaderGasFields(configExtra, header, parent); 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 the existence / non-existence of excessBlobGas
cancun := config.IsCancun(header.Number, header.Time)
if !cancun {
switch {
case header.ExcessBlobGas != nil:
return fmt.Errorf("invalid excessBlobGas: have %d, expected nil", *header.ExcessBlobGas)
case header.BlobGasUsed != nil:
return fmt.Errorf("invalid blobGasUsed: have %d, expected nil", *header.BlobGasUsed)
case header.ParentBeaconRoot != nil:
return fmt.Errorf("invalid parentBeaconRoot, have %#x, expected nil", *header.ParentBeaconRoot)
}
} else {
if header.ParentBeaconRoot == nil {
return errors.New("header is missing beaconRoot")
}
if *header.ParentBeaconRoot != (common.Hash{}) {
return fmt.Errorf("invalid parentBeaconRoot, have %#x, expected empty", *header.ParentBeaconRoot)
}
if err := eip4844.VerifyEIP4844Header(parent, header); err != nil {
return err
}
if *header.BlobGasUsed > 0 { // VerifyEIP4844Header ensures BlobGasUsed is non-nil
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,
extraStateChangeContribution *big.Int,
) error {
if eng.consensusMode.ModeSkipBlockFee {
return nil
}
if baseFee == nil || baseFee.Sign() <= 0 {
return fmt.Errorf("invalid base fee (%d)", baseFee)
}
if requiredBlockGasCost == nil || !requiredBlockGasCost.IsUint64() {
return fmt.Errorf("invalid block gas cost (%d)", requiredBlockGasCost)
}
var (
gasUsed = new(big.Int)
blockFeeContribution = new(big.Int)
totalBlockFee = new(big.Int)
)
// Add in the external contribution
if extraStateChangeContribution != nil {
if extraStateChangeContribution.Cmp(common.Big0) < 0 {
return fmt.Errorf("invalid extra state change contribution: %d", extraStateChangeContribution)
}
totalBlockFee.Add(totalBlockFee, extraStateChangeContribution)
}
// Calculate the total excess (denominated in LUX) 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 LUX 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 {
// Perform extra state change while finalizing the block
var (
contribution, extDataGasUsed *big.Int
err error
)
if eng.cb.OnExtraStateChange != nil {
contribution, extDataGasUsed, err = eng.cb.OnExtraStateChange(block, parent, state)
if err != nil {
return err
}
}
config := params.GetExtra(chain.Config())
timestamp := block.Time()
// Verify the BlockGasCost set in the header matches the expected value.
blockGasCost := customtypes.BlockGasCost(block)
expectedBlockGasCost := customheader.BlockGasCost(
config,
parent,
timestamp,
)
if !utils.BigEqual(blockGasCost, expectedBlockGasCost) {
return fmt.Errorf("invalid blockGasCost: have %d, want %d", blockGasCost, expectedBlockGasCost)
}
// checks always run when a valid base fee is present.
baseFee := block.BaseFee()
hasValidBaseFee := baseFee != nil && baseFee.Sign() > 0
if hasValidBaseFee {
// Validate extDataGasUsed and BlockGasCost match expectations
//
// NOTE: This is a duplicate check of what is already performed in
// blockValidator but is done here for symmetry with FinalizeAndAssemble.
if extDataGasUsed == nil {
extDataGasUsed = new(big.Int).Set(common.Big0)
}
blockExtDataGasUsed := customtypes.BlockExtDataGasUsed(block)
if !blockExtDataGasUsed.IsUint64() || blockExtDataGasUsed.Cmp(extDataGasUsed) != 0 {
return fmt.Errorf("invalid extDataGasUsed: have %d, want %d", blockExtDataGasUsed, extDataGasUsed)
}
// Verify the block fee was paid.
if err := eng.verifyBlockFee(
baseFee,
blockGasCost,
block.Transactions(),
receipts,
contribution,
); 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) {
var (
contribution, extDataGasUsed *big.Int
extraData []byte
err error
)
if eng.cb.OnFinalizeAndAssemble != nil {
extraData, contribution, extDataGasUsed, err = eng.cb.OnFinalizeAndAssemble(header, parent, state, txs)
if err != nil {
return nil, err
}
}
configExtra := params.GetExtra(chain.Config())
headerExtra := customtypes.GetHeaderExtra(header)
// Calculate the required block gas cost for this block.
headerExtra.BlockGasCost = customheader.BlockGasCost(
configExtra,
parent,
header.Time,
)
// Set the native header field for BlockGasCost (used by ExtraPrefix -> GetHeaderExtra fallback)
header.BlockGasCost = headerExtra.BlockGasCost
// Under activate-all-implicitly dynamic fees are live; if the header
// carries a base fee we account for the ext-data gas and verify the
// block fee.
if header.BaseFee != nil && header.BaseFee.Sign() > 0 {
headerExtra.ExtDataGasUsed = extDataGasUsed
if headerExtra.ExtDataGasUsed == nil {
headerExtra.ExtDataGasUsed = new(big.Int)
}
// Set the native header field for ExtDataGasUsed (used by ExtraPrefix -> GetHeaderExtra fallback)
header.ExtDataGasUsed = headerExtra.ExtDataGasUsed
// Verify that this block covers the block fee.
if err := eng.verifyBlockFee(
header.BaseFee,
headerExtra.BlockGasCost,
txs,
receipts,
contribution,
); err != nil {
return nil, err
}
}
// finalize the header.Extra
extraPrefix, err := customheader.ExtraPrefix(configExtra, parent, header, eng.desiredTargetExcess)
if err != nil {
return nil, fmt.Errorf("failed to calculate new header.Extra: %w", err)
}
header.Extra = append(extraPrefix, header.Extra...)
// commit the final state root
header.Root = state.IntermediateRoot(chain.Config().IsEIP158(header.Number))
// Store header extras now that the header hash is finalized
// (header.Extra and header.Root are set, so hash is stable)
customtypes.SetHeaderExtra(header, headerExtra)
// Header seems complete, assemble into a block and return. Under
// activate-all-implicitly the ext-data hash is always present.
return customtypes.NewBlockWithExtData(
header, txs, uncles, receipts, trie.NewStackTrie(nil),
extraData, true,
), nil
}
func (*DummyEngine) CalcDifficulty(chain consensus.ChainHeaderReader, time uint64, parent *types.Header) *big.Int {
return big.NewInt(1)
}
func (*DummyEngine) Close() error {
return nil
}