Files
evmgpu/plugin/evm/vm_test.go.disabled
zeekay 401b447ae9 rename: Etna → Quasar Edition
Wire-level renames in EVMGPU (parallel of luxfi/evm):
  - extras.NetworkUpgrades.EtnaTimestamp → QuasarTimestamp
    json tag etnaTimestamp → quasarTimestamp
  - extras.NetworkUpgrades.IsEtna()      → IsQuasar()
  - extras.LuxRules.IsEtna               → IsQuasar
  - params.RulesExtra.IsEtna             → IsQuasar
  - extras.TestEtnaChainConfig           → TestQuasarChainConfig
  - params.TestEtnaChainConfig           → TestQuasarChainConfig
  - upgradetest.Etna fork enum           → Quasar (luxfi/upgrade dep)

Bumps luxfi/upgrade@main + luxfi/constants@main.
Excludes user WIP on core/parallel/gpu_*.go (pkg-config + $HOME path
edits — separate change set).
No backcompat aliases — forwards-only.
2026-06-02 23:05:26 -07:00

4420 lines
141 KiB
Plaintext

// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package evm
import (
"context"
"crypto/ecdsa"
"encoding/json"
"errors"
"fmt"
"math/big"
"os"
"path/filepath"
"strings"
"sync"
"testing"
"time"
"github.com/luxfi/crypto"
"github.com/luxfi/crypto/bls"
"github.com/luxfi/crypto/utils/crypto/bls/signer/localsigner"
"github.com/luxfi/geth/common"
"github.com/luxfi/geth/core/rawdb"
warpSigner "github.com/luxfi/warp/signer"
"github.com/luxfi/geth/core/types"
"github.com/luxfi/geth/trie"
"github.com/luxfi/log"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
commonEng "github.com/luxfi/consensus/core"
nodeConsensusBlock "github.com/luxfi/consensus/engine/chain/block"
"github.com/luxfi/crypto/secp256k1"
nodeChain "github.com/luxfi/vms/components/chain"
"github.com/luxfi/database"
"github.com/luxfi/database/memdb"
"github.com/luxfi/database/prefixdb"
"github.com/luxfi/evm/commontype"
"github.com/luxfi/evm/constants"
"github.com/luxfi/evm/core"
"github.com/luxfi/evm/core/txpool"
"github.com/luxfi/evm/eth"
"github.com/luxfi/evm/params"
"github.com/luxfi/evm/params/extras"
"github.com/luxfi/evm/plugin/evm/config"
"github.com/luxfi/evm/plugin/evm/customrawdb"
"github.com/luxfi/evm/plugin/evm/customtypes"
"github.com/luxfi/evm/plugin/evm/header"
"github.com/luxfi/evm/plugin/evm/vmerrors"
"github.com/luxfi/evm/precompile/allowlist"
"github.com/luxfi/evm/precompile/contracts/deployerallowlist"
"github.com/luxfi/evm/precompile/contracts/feemanager"
"github.com/luxfi/evm/precompile/contracts/rewardmanager"
"github.com/luxfi/evm/precompile/contracts/txallowlist"
"github.com/luxfi/evm/rpc"
"github.com/luxfi/evm/utils"
"github.com/luxfi/evm/utils/utilstest"
"github.com/luxfi/ids"
"github.com/luxfi/vm/chains/atomic"
nodeConsensus "github.com/luxfi/consensus"
"github.com/luxfi/consensus/utils/set"
luxdconstants "github.com/luxfi/constants"
)
var (
// Firewood scheme is disabled/experimental, so only test with HashScheme
schemes = []string{rawdb.HashScheme}
testNetworkID uint32 = luxdconstants.UnitTestID
testMinGasPrice int64 = 225_000_000_000
testKeys = secp256k1.TestKeys()[:3]
testEthAddrs []common.Address // testEthAddrs[i] corresponds to testKeys[i]
firstTxAmount = new(big.Int).Mul(big.NewInt(testMinGasPrice), big.NewInt(21000*100))
toGenesisJSON = func(cfg *params.ChainConfig) string {
g := new(core.Genesis)
g.Difficulty = big.NewInt(0)
g.GasLimit = 8000000
g.Timestamp = 0 // Genesis timestamp
// Use chainId: 43111, so that it does not overlap with any Lux ChainIDs, which may have their
// config overridden in vm.Initialize.
// Copy the config and its extras
cpy := *cfg
cpy.ChainID = big.NewInt(43111)
// Copy the extras to the new config
if extra := params.GetExtra(cfg); extra != nil {
extraCpy := *extra
params.SetExtra(&cpy, &extraCpy)
}
g.Config = &cpy
// Create allocation for the test addresses
g.Alloc = make(types.GenesisAlloc)
for _, addr := range testEthAddrs {
// Set balance to be enough for gas cost plus some extra for value transfers
// Previous: 0x4192927743b88000 = 4725000000000000000 wei (exactly 21000 * testMinGasPrice)
// New: 0x4192927743b88001 = 4725000000000000001 wei (includes 1 wei for value)
// Actually, let's give them 10 ETH to avoid any issues
balance := new(big.Int).Mul(big.NewInt(params.Ether), big.NewInt(10))
g.Alloc[addr] = types.Account{
Balance: balance,
}
}
// Marshal the genesis normally
b, err := json.Marshal(g)
if err != nil {
panic(err)
}
// Now we need to add the network upgrades to the JSON
// Parse the JSON into a map to add the extras
var jsonMap map[string]interface{}
if err := json.Unmarshal(b, &jsonMap); err != nil {
panic(err)
}
// Add the network upgrades to the config
if configMap, ok := jsonMap["config"].(map[string]interface{}); ok {
if extra := params.GetExtra(&cpy); extra != nil {
// Add the network upgrade timestamps
if extra.EVMTimestamp != nil {
configMap["evmTimestamp"] = *extra.EVMTimestamp
}
if extra.DurangoTimestamp != nil {
configMap["durangoTimestamp"] = *extra.DurangoTimestamp
}
if extra.QuasarTimestamp != nil {
configMap["quasarTimestamp"] = *extra.QuasarTimestamp
}
if extra.FortunaTimestamp != nil {
configMap["fortunaTimestamp"] = *extra.FortunaTimestamp
}
if extra.GraniteTimestamp != nil {
configMap["graniteTimestamp"] = *extra.GraniteTimestamp
}
}
}
// Marshal the modified map back to JSON
b, err = json.Marshal(jsonMap)
if err != nil {
panic(err)
}
// Debug: Print the config to verify it includes timestamps
if configMap, ok := jsonMap["config"].(map[string]interface{}); ok {
if durangoTs, ok := configMap["durangoTimestamp"]; ok {
fmt.Printf("DEBUG: Genesis includes durangoTimestamp: %v\n", durangoTs)
} else {
fmt.Println("DEBUG: Genesis missing durangoTimestamp")
}
}
return string(b)
}
// marshalGenesisWithExtras marshals a Genesis to JSON while preserving all extras
// like network upgrade timestamps and genesis precompiles.
marshalGenesisWithExtras = func(genesis *core.Genesis) string {
// Marshal the genesis normally
b, err := json.Marshal(genesis)
if err != nil {
panic(err)
}
// Now we need to add the extras to the JSON
// Parse the JSON into a map to add the extras
var jsonMap map[string]interface{}
if err := json.Unmarshal(b, &jsonMap); err != nil {
panic(err)
}
// Add the network upgrades and precompiles to the config
if configMap, ok := jsonMap["config"].(map[string]interface{}); ok {
if extra := params.GetExtra(genesis.Config); extra != nil {
// Add the network upgrade timestamps
if extra.EVMTimestamp != nil {
configMap["evmTimestamp"] = *extra.EVMTimestamp
}
if extra.DurangoTimestamp != nil {
configMap["durangoTimestamp"] = *extra.DurangoTimestamp
}
if extra.QuasarTimestamp != nil {
configMap["quasarTimestamp"] = *extra.QuasarTimestamp
}
if extra.FortunaTimestamp != nil {
configMap["fortunaTimestamp"] = *extra.FortunaTimestamp
}
if extra.GraniteTimestamp != nil {
configMap["graniteTimestamp"] = *extra.GraniteTimestamp
}
// Add genesis precompiles if present - these go at the TOP level of config, not under "genesisPrecompiles"
if extra.GenesisPrecompiles != nil && len(extra.GenesisPrecompiles) > 0 {
for key, config := range extra.GenesisPrecompiles {
// Marshal the precompile config
configBytes, err := json.Marshal(config)
if err != nil {
panic(fmt.Sprintf("failed to marshal precompile config %s: %v", key, err))
}
var precompileConfigMap map[string]interface{}
if err := json.Unmarshal(configBytes, &precompileConfigMap); err != nil {
panic(fmt.Sprintf("failed to unmarshal precompile config %s: %v", key, err))
}
// Add directly to configMap at top level
configMap[key] = precompileConfigMap
}
}
// Add FeeConfig if present
if extra.FeeConfig.GasLimit != nil {
feeConfigBytes, err := json.Marshal(extra.FeeConfig)
if err != nil {
panic(fmt.Sprintf("failed to marshal feeConfig: %v", err))
}
var feeConfigMap map[string]interface{}
if err := json.Unmarshal(feeConfigBytes, &feeConfigMap); err != nil {
panic(fmt.Sprintf("failed to unmarshal feeConfig: %v", err))
}
configMap["feeConfig"] = feeConfigMap
}
// Add AllowFeeRecipients if true
if extra.AllowFeeRecipients {
configMap["allowFeeRecipients"] = true
}
}
}
// Marshal the modified map back to JSON
b, err = json.Marshal(jsonMap)
if err != nil {
panic(err)
}
return string(b)
}
// forkToChainConfig maps a fork name to a chain config
forkToChainConfig = map[string]*params.ChainConfig{
"Durango": params.TestDurangoChainConfig,
"Quasar": params.TestQuasarChainConfig,
"Granite": params.TestGraniteChainConfig,
"Latest": params.TestGraniteChainConfig,
}
// These will be initialized after init() runs
genesisJSONPreSubnetEVM string
genesisJSONSubnetEVM string
)
func init() {
for _, key := range testKeys {
// Convert secp256k1 key to Ethereum address using proper ECDSA conversion
ecdsaKey, err := toECDSA(key)
if err != nil {
panic(fmt.Sprintf("failed to convert test key: %v", err))
}
// Use geth's crypto to get proper Ethereum address
pubKeyBytes := crypto.FromECDSAPub(&ecdsaKey.PublicKey)
hash := crypto.Keccak256(pubKeyBytes[1:])
var addr common.Address
copy(addr[:], hash[12:])
testEthAddrs = append(testEthAddrs, addr)
}
genesisJSONPreSubnetEVM = toGenesisJSON(params.TestPreSubnetEVMChainConfig)
genesisJSONSubnetEVM = toGenesisJSON(params.TestSubnetEVMChainConfig)
}
type testVMConfig struct {
isSyncing bool
fork *string
// If genesisJSON is empty, defaults to the genesis corresponding to the
// fork.
genesisJSON string
upgradeJSON string
configJSON string
}
type testVM struct {
vm *VM
db *prefixdb.Database
atomicMemory *atomic.Memory
appSender *TestSender
}
// mockValidatorState implements consensus.ValidatorState interface for tests
type mockValidatorState struct{}
func (m *mockValidatorState) GetChainID(id ids.ID) (ids.ID, error) {
return ids.Empty, nil
}
func (m *mockValidatorState) GetNetID(id ids.ID) (ids.ID, error) {
return ids.Empty, nil
}
func (m *mockValidatorState) GetSubnetID(chainID ids.ID) (ids.ID, error) {
return ids.Empty, nil
}
func (m *mockValidatorState) GetValidatorSet(height uint64, netID ids.ID) (map[ids.NodeID]uint64, error) {
// Return a single test validator
nodeID := ids.GenerateTestNodeID()
return map[ids.NodeID]uint64{
nodeID: 100,
}, nil
}
func (m *mockValidatorState) GetCurrentHeight(ctx context.Context) (uint64, error) {
return 0, nil
}
func (m *mockValidatorState) GetMinimumHeight(ctx context.Context) (uint64, error) {
return 0, nil
}
// testWarpSignerHolder holds both the warp signer and the underlying BLS signer
// The warp signer is used by the VM, the BLS signer is used by tests for verification
type testWarpSignerHolder struct {
warpSigner *warpSigner.LocalSigner // Implements signer.Signer for the VM
blsSigner bls.Signer // Underlying BLS signer for test verification
nodeID ids.NodeID
}
// newMockWarpSigner creates a warp signer that the VM can use
func newMockWarpSigner(t *testing.T) *testWarpSignerHolder {
// Create the underlying BLS signer
blsSigner, err := localsigner.New()
require.NoError(t, err)
// Wrap it with warp's LocalSigner
warpLocalSigner := warpSigner.NewLocalSigner(blsSigner)
return &testWarpSignerHolder{
warpSigner: warpLocalSigner,
blsSigner: blsSigner,
nodeID: ids.GenerateTestNodeID(),
}
}
// GetWarpSigner returns the signer.Signer interface for the VM
func (h *testWarpSignerHolder) GetWarpSigner() *warpSigner.LocalSigner {
return h.warpSigner
}
// GetBLSSigner returns the underlying bls.Signer for signature verification in tests
func (h *testWarpSignerHolder) GetBLSSigner() bls.Signer {
return h.blsSigner
}
// testWarpHolder is stored alongside test contexts for tests that need access to the BLS signer
var testWarpHolder *testWarpSignerHolder
// createTestConsensusContext creates a test consensus context with the required fields
func createTestConsensusContext(t *testing.T) *nodeConsensus.Context {
// Create a valid context using the actual structure
// QuantumID must be UnitTestID (369) to skip standalone database initialization
// which uses a hardcoded /tmp/chaindata path and causes lock contention in parallel tests
// Note: GetNetworkID() returns QuantumID (not NetworkID), so we set QuantumID
// Create the warp signer holder and store it for tests that need BLS verification
testWarpHolder = newMockWarpSigner(t)
ctx := &nodeConsensus.Context{
QuantumID: luxdconstants.UnitTestID,
NetworkID: luxdconstants.UnitTestID,
ChainID: utilstest.SubnetEVMTestChainID,
NodeID: ids.GenerateTestNodeID(),
ValidatorState: &mockValidatorState{}, // Required for validator sync
WarpSigner: testWarpHolder.GetWarpSigner(), // Pass the actual signer.Signer
}
return ctx
}
// getTestWarpHolder returns the test warp holder for BLS signature verification in tests
func getTestWarpHolder() *testWarpSignerHolder {
return testWarpHolder
}
// toECDSA converts a secp256k1 key to ECDSA key for transaction signing
func toECDSA(key *secp256k1.PrivateKey) (*ecdsa.PrivateKey, error) {
keyBytes := key.Bytes()
return crypto.ToECDSA(keyBytes)
}
// getEthBlock safely extracts the underlying ethBlock from a Block
func getEthBlock(t *testing.T, blk *Block) *types.Block {
t.Helper()
return blk.ethBlock
}
// getInternalBlock safely extracts the internal Block from a Block
func getInternalBlock(t *testing.T, blk *Block) *Block {
t.Helper()
return blk
}
// getEthBlockFromProtocol safely extracts the underlying ethBlock from a Block
func getEthBlockFromProtocol(t *testing.T, blk *Block) *types.Block {
t.Helper()
return blk.ethBlock
}
// unwrapBlock extracts the internal *Block from various wrapper types (BlockWrapper, BlockAdapter, or direct *Block)
// It handles nested wrapping by recursively unwrapping until it reaches a *Block.
func unwrapBlock(t *testing.T, blk nodeConsensusBlock.Block) *Block {
t.Helper()
// Loop to handle nested wrappers
for {
switch b := blk.(type) {
case *Block:
return b
case *nodeChain.BlockWrapper:
// Unwrap the BlockWrapper
if inner, ok := b.Block.(*Block); ok {
return inner
}
// It might be wrapped in another type, continue unwrapping
if innerAdapter, ok := b.Block.(*BlockAdapter); ok {
blk = innerAdapter.Unwrap()
continue
}
if innerWrapper, ok := b.Block.(*nodeChain.BlockWrapper); ok {
blk = innerWrapper
continue
}
t.Fatalf("Expected wrapped block to be *Block, got %T", b.Block)
case *BlockAdapter:
// Get the wrapped consensus block and continue unwrapping
blk = b.Unwrap()
default:
t.Fatalf("Expected block to be *Block, *BlockAdapter, or *nodeChain.BlockWrapper, got %T", blk)
return nil
}
}
}
// getInternalBlockFromProtocol safely extracts the internal Block from a Block
func getInternalBlockFromProtocol(t *testing.T, blk *Block) *Block {
t.Helper()
return blk
}
func newVM(t *testing.T, config testVMConfig) *testVM {
ctx := createTestConsensusContext(t)
fork := "Latest"
if config.fork != nil {
fork = *config.fork
}
// Network upgrades will be handled differently since Context doesn't have this field
if len(config.genesisJSON) == 0 {
config.genesisJSON = toGenesisJSON(forkToChainConfig[fork])
}
baseDB := memdb.New()
// initialize the atomic memory
atomicMemory := atomic.NewMemory(prefixdb.New([]byte{0}, baseDB))
// SharedMemory and Lock are not part of the actual Context structure
// We'll need to handle these differently for testing purposes
sharedMem := atomicMemory.NewSharedMemory(ctx.ChainID)
_ = sharedMem // Store for later use if needed
prefixedDB := prefixdb.New([]byte{1}, baseDB)
vm := &VM{}
appSender := &TestSender{T: t}
appSender.CantSendAppGossip = true
appSender.SendAppGossipF = func(context.Context, set.Set[ids.NodeID], []byte) error { return nil }
err := vm.Initialize(
context.Background(),
ctx,
prefixedDB,
[]byte(config.genesisJSON),
[]byte(config.upgradeJSON),
[]byte(config.configJSON),
nil, // msgChan parameter
nil, // fxs parameter
appSender,
)
require.NoError(t, err, "error initializing vm")
if !config.isSyncing {
require.NoError(t, vm.SetState(context.Background(), uint32(commonEng.VMBootstrapping)))
require.NoError(t, vm.SetState(context.Background(), uint32(commonEng.VMNormalOp)))
}
return &testVM{
vm: vm,
db: prefixedDB,
atomicMemory: atomicMemory,
appSender: appSender,
}
}
// Firewood cannot yet be run with an empty config.
func getConfig(scheme, otherConfig string) string {
innerConfig := otherConfig
if scheme == customrawdb.FirewoodScheme {
if len(innerConfig) > 0 {
innerConfig += ", "
}
innerConfig += fmt.Sprintf(`"state-scheme": "%s", "snapshot-cache": 0, "pruning-enabled": true, "state-sync-enabled": false, "metrics-expensive-enabled": false`, customrawdb.FirewoodScheme)
}
return fmt.Sprintf(`{%s}`, innerConfig)
}
// setupGenesis sets up the genesis
func setupGenesis(
t *testing.T,
fork string,
) (*nodeConsensus.Context,
*prefixdb.Database,
[]byte,
*atomic.Memory,
) {
ctx := createTestConsensusContext(t)
genesisJSON := toGenesisJSON(forkToChainConfig[fork])
// Network upgrades will be handled differently since Context doesn't have this field
baseDB := memdb.New()
// initialize the atomic memory
atomicMemory := atomic.NewMemory(prefixdb.New([]byte{0}, baseDB))
// SharedMemory and Lock are not part of the actual Context structure
// We'll need to handle these differently for testing purposes
sharedMem := atomicMemory.NewSharedMemory(ctx.ChainID)
_ = sharedMem // Store for later use if needed
prefixedDB := prefixdb.New([]byte{1}, baseDB)
return ctx, prefixedDB, []byte(genesisJSON), atomicMemory
}
func TestVMConfig(t *testing.T) {
txFeeCap := float64(11)
enabledEthAPIs := []string{"debug"}
vm := newVM(t, testVMConfig{
configJSON: fmt.Sprintf(`{"rpc-tx-fee-cap": %g,"eth-apis": %s}`, txFeeCap, fmt.Sprintf("[%q]", enabledEthAPIs[0])),
}).vm
require.Equal(t, vm.config.RPCTxFeeCap, txFeeCap, "Tx Fee Cap should be set")
require.Equal(t, vm.config.EthAPIs(), enabledEthAPIs, "EnabledEthAPIs should be set")
require.NoError(t, vm.Shutdown(context.Background()))
}
func TestVMConfigDefaults(t *testing.T) {
txFeeCap := float64(11)
enabledEthAPIs := []string{"debug"}
vm := newVM(t, testVMConfig{
configJSON: fmt.Sprintf(`{"rpc-tx-fee-cap": %g,"eth-apis": %s}`, txFeeCap, fmt.Sprintf("[%q]", enabledEthAPIs[0])),
}).vm
var vmConfig config.Config
vmConfig.SetDefaults(defaultTxPoolConfig)
vmConfig.RPCTxFeeCap = txFeeCap
vmConfig.EnabledEthAPIs = enabledEthAPIs
require.Equal(t, vmConfig, vm.config, "VM Config should match default with overrides")
require.NoError(t, vm.Shutdown(context.Background()))
}
func TestVMNilConfig(t *testing.T) {
vm := newVM(t, testVMConfig{}).vm
// VM Config should match defaults if no config is passed in
var vmConfig config.Config
vmConfig.SetDefaults(defaultTxPoolConfig)
require.Equal(t, vmConfig, vm.config, "VM Config should match default config")
require.NoError(t, vm.Shutdown(context.Background()))
}
func TestVMContinuousProfiler(t *testing.T) {
profilerDir := t.TempDir()
profilerFrequency := 500 * time.Millisecond
vm := newVM(t, testVMConfig{
configJSON: fmt.Sprintf(`{"continuous-profiler-dir": %q,"continuous-profiler-frequency": "500ms"}`, profilerDir),
}).vm
require.Equal(t, vm.config.ContinuousProfilerDir, profilerDir, "profiler dir should be set")
require.Equal(t, vm.config.ContinuousProfilerFrequency.Duration, profilerFrequency, "profiler frequency should be set")
// Sleep for twice the frequency of the profiler to give it time
// to generate the first profile.
time.Sleep(2 * time.Second)
require.NoError(t, vm.Shutdown(context.Background()))
// Check that the first profile was generated
expectedFileName := filepath.Join(profilerDir, "cpu.profile.1")
_, err := os.Stat(expectedFileName)
require.NoError(t, err, "Expected continuous profiler to generate the first CPU profile at %s", expectedFileName)
}
func TestVMUpgrades(t *testing.T) {
for _, scheme := range schemes {
t.Run(scheme, func(t *testing.T) {
testVMUpgrades(t, scheme)
})
}
}
func testVMUpgrades(t *testing.T, scheme string) {
genesisTests := []struct {
name string
genesisJSON string
expectedGasPrice *big.Int
}{
{
name: "Subnet EVM",
genesisJSON: genesisJSONSubnetEVM,
expectedGasPrice: big.NewInt(0),
},
{
name: "Durango",
genesisJSON: toGenesisJSON(params.TestDurangoChainConfig),
expectedGasPrice: big.NewInt(0),
},
}
for _, test := range genesisTests {
t.Run(test.name, func(t *testing.T) {
require := require.New(t)
vm := newVM(t, testVMConfig{
genesisJSON: test.genesisJSON,
configJSON: getConfig(scheme, ""),
}).vm
defer func() {
require.NoError(vm.Shutdown(context.Background()))
}()
require.Equal(test.expectedGasPrice, vm.txPool.GasTip())
// Verify that the genesis is correctly managed.
lastAcceptedID, err := vm.LastAccepted(context.Background())
require.NoError(err)
require.Equal(ids.ID(vm.genesisHash), lastAcceptedID)
genesisBlk, err := vm.GetBlock(context.Background(), lastAcceptedID)
require.NoError(err)
require.Zero(genesisBlk.Height())
_, err = vm.ParseBlock(context.Background(), genesisBlk.Bytes())
require.NoError(err)
})
}
}
func issueAndAccept(t *testing.T, vm *VM) *Block {
t.Helper()
msg, err := vm.WaitForEvent(context.Background())
require.NoError(t, err)
require.Equal(t, commonEng.PendingTxs, msg)
blk, err := vm.BuildBlock(context.Background())
if err != nil {
t.Fatal(err)
}
if err := blk.Verify(context.Background()); err != nil {
t.Fatal(err)
}
if err := vm.SetPreference(context.Background(), blk.ID()); err != nil {
t.Fatal(err)
}
if err := blk.Accept(context.Background()); err != nil {
t.Fatal(err)
}
// Type assert the block to our internal Block type
// BuildBlock returns a *nodeChain.BlockWrapper which wraps the internal *Block
var internalBlk *Block
if wrapper, ok := blk.(*nodeChain.BlockWrapper); ok {
// BlockWrapper embeds block.Block - which is our internal *Block
if internal, ok := wrapper.Block.(*Block); ok {
internalBlk = internal
} else {
t.Fatalf("Expected wrapped block to be *Block, got %T", wrapper.Block)
}
} else if adapter, ok := blk.(*BlockAdapter); ok {
// Unwrap the BlockAdapter to get the consensus block, then assert to *Block
if internal, ok := adapter.Unwrap().(*Block); ok {
internalBlk = internal
} else {
t.Fatalf("Expected unwrapped block to be *Block, got %T", adapter.Unwrap())
}
} else if direct, ok := blk.(*Block); ok {
// Direct *Block (shouldn't happen but handle it)
internalBlk = direct
} else {
t.Fatalf("Expected block to be *Block, *BlockAdapter, or *nodeChain.BlockWrapper, got %T", blk)
}
return internalBlk
}
func TestBuildEthTxBlock(t *testing.T) {
for _, scheme := range schemes {
t.Run(scheme, func(t *testing.T) {
testBuildEthTxBlock(t, scheme)
})
}
}
func testBuildEthTxBlock(t *testing.T, scheme string) {
// fork := upgradetest.Latest // Not used currently
tvm := newVM(t, testVMConfig{
genesisJSON: genesisJSONSubnetEVM,
configJSON: getConfig(scheme, `"pruning-enabled":true`),
})
defer func() {
if err := tvm.vm.Shutdown(context.Background()); err != nil {
t.Fatal(err)
}
}()
newTxPoolHeadChan := make(chan core.NewTxPoolReorgEvent, 1)
tvm.vm.txPool.SubscribeNewReorgEvent(newTxPoolHeadChan)
key := utilstest.NewKey(t)
tx := types.NewTransaction(uint64(0), key.Address, firstTxAmount, 21000, big.NewInt(testMinGasPrice), nil)
// Convert secp256k1 key to ECDSA for signing
keyBytes := testKeys[0].Bytes()
privKey, err := crypto.ToECDSA(keyBytes)
if err != nil {
t.Fatal(err)
}
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(tvm.vm.chainConfig.ChainID), privKey)
if err != nil {
t.Fatal(err)
}
errs := tvm.vm.txPool.AddRemotesSync([]*types.Transaction{signedTx})
for i, err := range errs {
if err != nil {
t.Fatalf("Failed to add tx at index %d: %s", i, err)
}
}
blk1 := issueAndAccept(t, tvm.vm)
newHead := <-newTxPoolHeadChan
if newHead.Head.Hash() != common.Hash(blk1.ID()) {
t.Fatalf("Expected new block to match")
}
txs := make([]*types.Transaction, 10)
for i := 0; i < 10; i++ {
tx := types.NewTransaction(uint64(i), key.Address, big.NewInt(10), 21000, big.NewInt(testMinGasPrice), nil)
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(tvm.vm.chainConfig.ChainID), key.PrivateKey)
if err != nil {
t.Fatal(err)
}
txs[i] = signedTx
}
errs = tvm.vm.txPool.AddRemotesSync(txs)
for i, err := range errs {
if err != nil {
t.Fatalf("Failed to add tx at index %d: %s", i, err)
}
}
tvm.vm.clock.Set(tvm.vm.clock.Time().Add(2 * time.Second))
blk2 := issueAndAccept(t, tvm.vm)
newHead = <-newTxPoolHeadChan
if newHead.Head.Hash() != common.Hash(blk2.ID()) {
t.Fatalf("Expected new block to match")
}
lastAcceptedID, err := tvm.vm.LastAccepted(context.Background())
if err != nil {
t.Fatal(err)
}
if lastAcceptedID != blk2.ID() {
t.Fatalf("Expected last accepted blockID to be the accepted block: %s, but found %s", blk2.ID(), lastAcceptedID)
}
ethBlk1 := getEthBlock(t, blk1)
if ethBlk1Root := ethBlk1.Root(); !tvm.vm.blockChain.HasState(ethBlk1Root) {
t.Fatalf("Expected blk1 state root to not yet be pruned after blk2 was accepted because of tip buffer")
}
// Clear the cache and ensure that GetBlock returns internal blocks with the correct status
tvm.vm.Flush()
blk2Refreshed, err := tvm.vm.GetBlockInternal(context.Background(), blk2.ID())
if err != nil {
t.Fatal(err)
}
blk1RefreshedID := blk2Refreshed.Parent()
blk1Refreshed, err := tvm.vm.GetBlockInternal(context.Background(), blk1RefreshedID)
if err != nil {
t.Fatal(err)
}
if blk1Refreshed.ID() != blk1.ID() {
t.Fatalf("Found unexpected blkID for parent of blk2")
}
restartedVM := &VM{}
newCTX := createTestConsensusContext(t)
// NetworkUpgrades and ChainDataDir handled differently for testing
if err := restartedVM.Initialize(
context.Background(),
newCTX,
tvm.db,
[]byte(genesisJSONSubnetEVM),
[]byte(""),
[]byte(getConfig(scheme, `"pruning-enabled":true`)),
nil, // msgChan parameter
nil, // fxs parameter
nil, // appSender parameter
); err != nil {
t.Fatal(err)
}
// State root should not have been committed and discarded on restart
if ethBlk1Root := ethBlk1.Root(); restartedVM.blockChain.HasState(ethBlk1Root) {
t.Fatalf("Expected blk1 state root to be pruned after blk2 was accepted on top of it in pruning mode")
}
// State root should be committed when accepted tip on shutdown
ethBlk2 := getEthBlock(t, blk2)
if ethBlk2Root := ethBlk2.Root(); !restartedVM.blockChain.HasState(ethBlk2Root) {
t.Fatalf("Expected blk2 state root to not be pruned after shutdown (last accepted tip should be committed)")
}
}
// Regression test to ensure that after accepting block A
// then calling SetPreference on block B (when it becomes preferred)
// and the head of a longer chain (block D) does not corrupt the
// canonical chain.
//
// A
// / \
// B C
// |
// D
func TestSetPreferenceRace(t *testing.T) {
for _, scheme := range schemes {
t.Run(scheme, func(t *testing.T) {
testSetPreferenceRace(t, scheme)
})
}
}
func testSetPreferenceRace(t *testing.T, scheme string) {
// Create two VMs which will agree on block A and then
// build the two distinct preferred chains above
tvmConfig := testVMConfig{
genesisJSON: genesisJSONSubnetEVM,
configJSON: getConfig(scheme, `"pruning-enabled":true`),
}
tvm1 := newVM(t, tvmConfig)
tvm2 := newVM(t, tvmConfig)
vm1 := tvm1.vm
vm2 := tvm2.vm
defer func() {
if err := vm1.Shutdown(context.Background()); err != nil {
t.Fatal(err)
}
if err := vm2.Shutdown(context.Background()); err != nil {
t.Fatal(err)
}
}()
newTxPoolHeadChan1 := make(chan core.NewTxPoolReorgEvent, 1)
vm1.txPool.SubscribeNewReorgEvent(newTxPoolHeadChan1)
newTxPoolHeadChan2 := make(chan core.NewTxPoolReorgEvent, 1)
vm2.txPool.SubscribeNewReorgEvent(newTxPoolHeadChan2)
tx := types.NewTransaction(uint64(0), testEthAddrs[1], firstTxAmount, 21000, big.NewInt(testMinGasPrice), nil)
// Convert secp256k1 key to ECDSA for signing
keyBytes := testKeys[0].Bytes()
privKey, err := crypto.ToECDSA(keyBytes)
if err != nil {
t.Fatal(err)
}
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(vm1.chainConfig.ChainID), privKey)
if err != nil {
t.Fatal(err)
}
txErrors := vm1.txPool.AddRemotesSync([]*types.Transaction{signedTx})
for i, err := range txErrors {
if err != nil {
t.Fatalf("Failed to add tx at index %d: %s", i, err)
}
}
msg, err := vm1.WaitForEvent(context.Background())
require.NoError(t, err)
require.Equal(t, commonEng.PendingTxs, msg)
vm1BlkA, err := vm1.BuildBlock(context.Background())
if err != nil {
t.Fatalf("Failed to build block with import transaction: %s", err)
}
if err := vm1BlkA.Verify(context.Background()); err != nil {
t.Fatalf("Block failed verification on VM1: %s", err)
}
if err := vm1.SetPreference(context.Background(), vm1BlkA.ID()); err != nil {
t.Fatal(err)
}
vm2BlkA, err := vm2.ParseBlock(context.Background(), vm1BlkA.Bytes())
if err != nil {
t.Fatalf("Unexpected error parsing block from vm2: %s", err)
}
if err := vm2BlkA.Verify(context.Background()); err != nil {
t.Fatalf("Block failed verification on VM2: %s", err)
}
if err := vm2.SetPreference(context.Background(), vm2BlkA.ID()); err != nil {
t.Fatal(err)
}
if err := vm1BlkA.Accept(context.Background()); err != nil {
t.Fatalf("VM1 failed to accept block: %s", err)
}
if err := vm2BlkA.Accept(context.Background()); err != nil {
t.Fatalf("VM2 failed to accept block: %s", err)
}
newHead := <-newTxPoolHeadChan1
if newHead.Head.Hash() != common.Hash(vm1BlkA.ID()) {
t.Fatalf("Expected new block to match")
}
newHead = <-newTxPoolHeadChan2
if newHead.Head.Hash() != common.Hash(vm2BlkA.ID()) {
t.Fatalf("Expected new block to match")
}
// Create list of 10 successive transactions to build block A on vm1
// and to be split into two separate blocks on VM2
txs := make([]*types.Transaction, 10)
for i := 0; i < 10; i++ {
tx := types.NewTransaction(uint64(i), testEthAddrs[0], big.NewInt(10), 21000, big.NewInt(testMinGasPrice), nil)
// Convert secp256k1 key to ECDSA for signing
keyBytes := testKeys[1].Bytes()
privKey, err := crypto.ToECDSA(keyBytes)
if err != nil {
t.Fatal(err)
}
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(vm1.chainConfig.ChainID), privKey)
if err != nil {
t.Fatal(err)
}
txs[i] = signedTx
}
var errs []error
// Add the remote transactions, build the block, and set VM1's preference for block A
errs = vm1.txPool.AddRemotesSync(txs)
for i, err := range errs {
if err != nil {
t.Fatalf("Failed to add transaction to VM1 at index %d: %s", i, err)
}
}
msg, err = vm1.WaitForEvent(context.Background())
require.NoError(t, err)
require.Equal(t, commonEng.PendingTxs, msg)
vm1BlkB, err := vm1.BuildBlock(context.Background())
if err != nil {
t.Fatal(err)
}
if err := vm1BlkB.Verify(context.Background()); err != nil {
t.Fatal(err)
}
if err := vm1.SetPreference(context.Background(), vm1BlkB.ID()); err != nil {
t.Fatal(err)
}
// Split the transactions over two blocks, and set VM2's preference to them in sequence
// after building each block
// Block C
errs = vm2.txPool.AddRemotesSync(txs[0:5])
for i, err := range errs {
if err != nil {
t.Fatalf("Failed to add transaction to VM2 at index %d: %s", i, err)
}
}
msg, err = vm2.WaitForEvent(context.Background())
require.NoError(t, err)
require.Equal(t, commonEng.PendingTxs, msg)
vm2BlkC, err := vm2.BuildBlock(context.Background())
if err != nil {
t.Fatalf("Failed to build BlkC on VM2: %s", err)
}
if err := vm2BlkC.Verify(context.Background()); err != nil {
t.Fatalf("BlkC failed verification on VM2: %s", err)
}
if err := vm2.SetPreference(context.Background(), vm2BlkC.ID()); err != nil {
t.Fatal(err)
}
newHead = <-newTxPoolHeadChan2
if newHead.Head.Hash() != common.Hash(vm2BlkC.ID()) {
t.Fatalf("Expected new block to match")
}
// Block D
errs = vm2.txPool.AddRemotesSync(txs[5:10])
for i, err := range errs {
if err != nil {
t.Fatalf("Failed to add transaction to VM2 at index %d: %s", i, err)
}
}
msg, err = vm2.WaitForEvent(context.Background())
require.NoError(t, err)
require.Equal(t, commonEng.PendingTxs, msg)
vm2BlkD, err := vm2.BuildBlock(context.Background())
if err != nil {
t.Fatalf("Failed to build BlkD on VM2: %s", err)
}
if err := vm2BlkD.Verify(context.Background()); err != nil {
t.Fatalf("BlkD failed verification on VM2: %s", err)
}
if err := vm2.SetPreference(context.Background(), vm2BlkD.ID()); err != nil {
t.Fatal(err)
}
// VM1 receives blkC and blkD from VM1
// and happens to call SetPreference on blkD without ever calling SetPreference
// on blkC
// Here we parse them in reverse order to simulate receiving a chain from the tip
// back to the last accepted block as would typically be the case in the consensus
// engine
vm1BlkD, err := vm1.ParseBlock(context.Background(), vm2BlkD.Bytes())
if err != nil {
t.Fatalf("VM1 errored parsing blkD: %s", err)
}
vm1BlkC, err := vm1.ParseBlock(context.Background(), vm2BlkC.Bytes())
if err != nil {
t.Fatalf("VM1 errored parsing blkC: %s", err)
}
// The blocks must be verified in order. This invariant is maintained
// in the consensus engine.
if err := vm1BlkC.Verify(context.Background()); err != nil {
t.Fatalf("VM1 BlkC failed verification: %s", err)
}
if err := vm1BlkD.Verify(context.Background()); err != nil {
t.Fatalf("VM1 BlkD failed verification: %s", err)
}
// Set VM1's preference to blockD, skipping blockC
if err := vm1.SetPreference(context.Background(), vm1BlkD.ID()); err != nil {
t.Fatal(err)
}
// Accept the longer chain on both VMs and ensure there are no errors
// VM1 Accepts the blocks in order
if err := vm1BlkC.Accept(context.Background()); err != nil {
t.Fatalf("VM1 BlkC failed on accept: %s", err)
}
if err := vm1BlkD.Accept(context.Background()); err != nil {
t.Fatalf("VM1 BlkC failed on accept: %s", err)
}
// VM2 Accepts the blocks in order
if err := vm2BlkC.Accept(context.Background()); err != nil {
t.Fatalf("VM2 BlkC failed on accept: %s", err)
}
if err := vm2BlkD.Accept(context.Background()); err != nil {
t.Fatalf("VM2 BlkC failed on accept: %s", err)
}
log.Info("Validating canonical chain")
// Verify the Canonical Chain for Both VMs
if err := vm2.blockChain.ValidateCanonicalChain(); err != nil {
t.Fatalf("VM2 failed canonical chain verification due to: %s", err)
}
if err := vm1.blockChain.ValidateCanonicalChain(); err != nil {
t.Fatalf("VM1 failed canonical chain verification due to: %s", err)
}
}
// Regression test to ensure that a VM that accepts block A and B
// will not attempt to orphan either when verifying blocks C and D
// from another VM (which have a common ancestor under the finalized
// frontier).
//
// A
// / \
// B C
//
// verifies block B and C, then Accepts block B. Then we test to ensure
// that the VM defends against any attempt to set the preference or to
// accept block C, which should be an orphaned block at this point and
// get rejected.
func TestReorgProtection(t *testing.T) {
for _, scheme := range schemes {
t.Run(scheme, func(t *testing.T) {
testReorgProtection(t, scheme)
})
}
}
func testReorgProtection(t *testing.T, scheme string) {
tvmConfig := testVMConfig{
genesisJSON: genesisJSONSubnetEVM,
configJSON: getConfig(scheme, `"pruning-enabled":false`),
}
tvm1 := newVM(t, tvmConfig)
tvm2 := newVM(t, tvmConfig)
vm1 := tvm1.vm
vm2 := tvm2.vm
defer func() {
if err := vm1.Shutdown(context.Background()); err != nil {
t.Fatal(err)
}
if err := vm2.Shutdown(context.Background()); err != nil {
t.Fatal(err)
}
}()
newTxPoolHeadChan1 := make(chan core.NewTxPoolReorgEvent, 1)
vm1.txPool.SubscribeNewReorgEvent(newTxPoolHeadChan1)
newTxPoolHeadChan2 := make(chan core.NewTxPoolReorgEvent, 1)
vm2.txPool.SubscribeNewReorgEvent(newTxPoolHeadChan2)
tx := types.NewTransaction(uint64(0), testEthAddrs[1], firstTxAmount, 21000, big.NewInt(testMinGasPrice), nil)
// Convert secp256k1 key to ECDSA for signing
keyBytes := testKeys[0].Bytes()
privKey, err := crypto.ToECDSA(keyBytes)
if err != nil {
t.Fatal(err)
}
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(vm1.chainConfig.ChainID), privKey)
if err != nil {
t.Fatal(err)
}
txErrors := vm1.txPool.AddRemotesSync([]*types.Transaction{signedTx})
for i, err := range txErrors {
if err != nil {
t.Fatalf("Failed to add tx at index %d: %s", i, err)
}
}
msg, err := vm1.WaitForEvent(context.Background())
require.NoError(t, err)
require.Equal(t, commonEng.PendingTxs, msg)
vm1BlkA, err := vm1.BuildBlock(context.Background())
if err != nil {
t.Fatalf("Failed to build block with import transaction: %s", err)
}
if err := vm1BlkA.Verify(context.Background()); err != nil {
t.Fatalf("Block failed verification on VM1: %s", err)
}
if err := vm1.SetPreference(context.Background(), vm1BlkA.ID()); err != nil {
t.Fatal(err)
}
vm2BlkA, err := vm2.ParseBlock(context.Background(), vm1BlkA.Bytes())
if err != nil {
t.Fatalf("Unexpected error parsing block from vm2: %s", err)
}
if err := vm2BlkA.Verify(context.Background()); err != nil {
t.Fatalf("Block failed verification on VM2: %s", err)
}
if err := vm2.SetPreference(context.Background(), vm2BlkA.ID()); err != nil {
t.Fatal(err)
}
if err := vm1BlkA.Accept(context.Background()); err != nil {
t.Fatalf("VM1 failed to accept block: %s", err)
}
if err := vm2BlkA.Accept(context.Background()); err != nil {
t.Fatalf("VM2 failed to accept block: %s", err)
}
newHead := <-newTxPoolHeadChan1
if newHead.Head.Hash() != common.Hash(vm1BlkA.ID()) {
t.Fatalf("Expected new block to match")
}
newHead = <-newTxPoolHeadChan2
if newHead.Head.Hash() != common.Hash(vm2BlkA.ID()) {
t.Fatalf("Expected new block to match")
}
// Create list of 10 successive transactions to build block A on vm1
// and to be split into two separate blocks on VM2
txs := make([]*types.Transaction, 10)
for i := 0; i < 10; i++ {
tx := types.NewTransaction(uint64(i), testEthAddrs[0], big.NewInt(10), 21000, big.NewInt(testMinGasPrice), nil)
// Convert secp256k1 key to ECDSA for signing
keyBytes := testKeys[1].Bytes()
privKey, err := crypto.ToECDSA(keyBytes)
if err != nil {
t.Fatal(err)
}
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(vm1.chainConfig.ChainID), privKey)
if err != nil {
t.Fatal(err)
}
txs[i] = signedTx
}
var errs []error
// Add the remote transactions, build the block, and set VM1's preference for block A
errs = vm1.txPool.AddRemotesSync(txs)
for i, err := range errs {
if err != nil {
t.Fatalf("Failed to add transaction to VM1 at index %d: %s", i, err)
}
}
msg, err = vm1.WaitForEvent(context.Background())
require.NoError(t, err)
require.Equal(t, commonEng.PendingTxs, msg)
vm1BlkB, err := vm1.BuildBlock(context.Background())
if err != nil {
t.Fatal(err)
}
if err := vm1BlkB.Verify(context.Background()); err != nil {
t.Fatal(err)
}
if err := vm1.SetPreference(context.Background(), vm1BlkB.ID()); err != nil {
t.Fatal(err)
}
// Split the transactions over two blocks, and set VM2's preference to them in sequence
// after building each block
// Block C
errs = vm2.txPool.AddRemotesSync(txs[0:5])
for i, err := range errs {
if err != nil {
t.Fatalf("Failed to add transaction to VM2 at index %d: %s", i, err)
}
}
msg, err = vm2.WaitForEvent(context.Background())
require.NoError(t, err)
require.Equal(t, commonEng.PendingTxs, msg)
vm2BlkC, err := vm2.BuildBlock(context.Background())
if err != nil {
t.Fatalf("Failed to build BlkC on VM2: %s", err)
}
if err := vm2BlkC.Verify(context.Background()); err != nil {
t.Fatalf("Block failed verification on VM2: %s", err)
}
vm1BlkC, err := vm1.ParseBlock(context.Background(), vm2BlkC.Bytes())
if err != nil {
t.Fatalf("Unexpected error parsing block from vm2: %s", err)
}
if err := vm1BlkC.Verify(context.Background()); err != nil {
t.Fatalf("Block failed verification on VM1: %s", err)
}
// Accept B, such that block C should get Rejected.
if err := vm1BlkB.Accept(context.Background()); err != nil {
t.Fatalf("VM1 failed to accept block: %s", err)
}
// The below (setting preference blocks that have a common ancestor
// with the preferred chain lower than the last finalized block)
// should NEVER happen. However, the VM defends against this
// just in case.
if err := vm1.SetPreference(context.Background(), vm1BlkC.ID()); !strings.Contains(err.Error(), "cannot orphan finalized block") {
t.Fatalf("Unexpected error when setting preference that would trigger reorg: %s", err)
}
if err := vm1BlkC.Accept(context.Background()); !strings.Contains(err.Error(), "expected accepted block to have parent") {
t.Fatalf("Unexpected error when setting block at finalized height: %s", err)
}
}
// Regression test to ensure that a VM that accepts block C while preferring
// block B will trigger a reorg.
//
// A
// / \
// B C
func TestNonCanonicalAccept(t *testing.T) {
for _, scheme := range schemes {
t.Run(scheme, func(t *testing.T) {
testNonCanonicalAccept(t, scheme)
})
}
}
func testNonCanonicalAccept(t *testing.T, scheme string) {
tvmConfig := testVMConfig{
genesisJSON: genesisJSONSubnetEVM,
configJSON: getConfig(scheme, ""),
}
tvm1 := newVM(t, tvmConfig)
tvm2 := newVM(t, tvmConfig)
vm1 := tvm1.vm
vm2 := tvm2.vm
defer func() {
if err := vm1.Shutdown(context.Background()); err != nil {
t.Fatal(err)
}
if err := vm2.Shutdown(context.Background()); err != nil {
t.Fatal(err)
}
}()
newTxPoolHeadChan1 := make(chan core.NewTxPoolReorgEvent, 1)
vm1.txPool.SubscribeNewReorgEvent(newTxPoolHeadChan1)
newTxPoolHeadChan2 := make(chan core.NewTxPoolReorgEvent, 1)
vm2.txPool.SubscribeNewReorgEvent(newTxPoolHeadChan2)
tx := types.NewTransaction(uint64(0), testEthAddrs[1], firstTxAmount, 21000, big.NewInt(testMinGasPrice), nil)
// Convert secp256k1 key to ECDSA for signing
keyBytes := testKeys[0].Bytes()
privKey, err := crypto.ToECDSA(keyBytes)
if err != nil {
t.Fatal(err)
}
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(vm1.chainConfig.ChainID), privKey)
if err != nil {
t.Fatal(err)
}
txErrors := vm1.txPool.AddRemotesSync([]*types.Transaction{signedTx})
for i, err := range txErrors {
if err != nil {
t.Fatalf("Failed to add tx at index %d: %s", i, err)
}
}
msg, err := vm1.WaitForEvent(context.Background())
require.NoError(t, err)
require.Equal(t, commonEng.PendingTxs, msg)
vm1BlkA, err := vm1.BuildBlock(context.Background())
if err != nil {
t.Fatalf("Failed to build block with import transaction: %s", err)
}
if err := vm1BlkA.Verify(context.Background()); err != nil {
t.Fatalf("Block failed verification on VM1: %s", err)
}
if _, err := vm1.GetBlockIDAtHeight(context.Background(), vm1BlkA.Height()); err != database.ErrNotFound {
t.Fatalf("Expected unaccepted block not to be indexed by height, but found %s", err)
}
if err := vm1.SetPreference(context.Background(), vm1BlkA.ID()); err != nil {
t.Fatal(err)
}
vm2BlkA, err := vm2.ParseBlock(context.Background(), vm1BlkA.Bytes())
if err != nil {
t.Fatalf("Unexpected error parsing block from vm2: %s", err)
}
if err := vm2BlkA.Verify(context.Background()); err != nil {
t.Fatalf("Block failed verification on VM2: %s", err)
}
if _, err := vm2.GetBlockIDAtHeight(context.Background(), vm2BlkA.Height()); err != database.ErrNotFound {
t.Fatalf("Expected unaccepted block not to be indexed by height, but found %s", err)
}
if err := vm2.SetPreference(context.Background(), vm2BlkA.ID()); err != nil {
t.Fatal(err)
}
if err := vm1BlkA.Accept(context.Background()); err != nil {
t.Fatalf("VM1 failed to accept block: %s", err)
}
if blkID, err := vm1.GetBlockIDAtHeight(context.Background(), vm1BlkA.Height()); err != nil {
t.Fatalf("Height lookuped failed on accepted block: %s", err)
} else if blkID != vm1BlkA.ID() {
t.Fatalf("Expected accepted block to be indexed by height, but found %s", blkID)
}
if err := vm2BlkA.Accept(context.Background()); err != nil {
t.Fatalf("VM2 failed to accept block: %s", err)
}
if blkID, err := vm2.GetBlockIDAtHeight(context.Background(), vm2BlkA.Height()); err != nil {
t.Fatalf("Height lookuped failed on accepted block: %s", err)
} else if blkID != vm2BlkA.ID() {
t.Fatalf("Expected accepted block to be indexed by height, but found %s", blkID)
}
newHead := <-newTxPoolHeadChan1
if newHead.Head.Hash() != common.Hash(vm1BlkA.ID()) {
t.Fatalf("Expected new block to match")
}
newHead = <-newTxPoolHeadChan2
if newHead.Head.Hash() != common.Hash(vm2BlkA.ID()) {
t.Fatalf("Expected new block to match")
}
// Create list of 10 successive transactions to build block A on vm1
// and to be split into two separate blocks on VM2
txs := make([]*types.Transaction, 10)
for i := 0; i < 10; i++ {
tx := types.NewTransaction(uint64(i), testEthAddrs[0], big.NewInt(10), 21000, big.NewInt(testMinGasPrice), nil)
// Convert secp256k1 key to ECDSA for signing
keyBytes := testKeys[1].Bytes()
privKey, err := crypto.ToECDSA(keyBytes)
if err != nil {
t.Fatal(err)
}
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(vm1.chainConfig.ChainID), privKey)
if err != nil {
t.Fatal(err)
}
txs[i] = signedTx
}
var errs []error
// Add the remote transactions, build the block, and set VM1's preference for block A
errs = vm1.txPool.AddRemotesSync(txs)
for i, err := range errs {
if err != nil {
t.Fatalf("Failed to add transaction to VM1 at index %d: %s", i, err)
}
}
msg, err = vm1.WaitForEvent(context.Background())
require.NoError(t, err)
require.Equal(t, commonEng.PendingTxs, msg)
vm1BlkB, err := vm1.BuildBlock(context.Background())
if err != nil {
t.Fatal(err)
}
if err := vm1BlkB.Verify(context.Background()); err != nil {
t.Fatal(err)
}
if _, err := vm1.GetBlockIDAtHeight(context.Background(), vm1BlkB.Height()); err != database.ErrNotFound {
t.Fatalf("Expected unaccepted block not to be indexed by height, but found %s", err)
}
if err := vm1.SetPreference(context.Background(), vm1BlkB.ID()); err != nil {
t.Fatal(err)
}
blkBHeight := vm1BlkB.Height()
vm1BlkBInternal := unwrapBlock(t, vm1BlkB)
blkBHash := getEthBlock(t, vm1BlkBInternal).Hash()
if b := vm1.blockChain.GetBlockByNumber(blkBHeight); b.Hash() != blkBHash {
t.Fatalf("expected block at %d to have hash %s but got %s", blkBHeight, blkBHash.Hex(), b.Hash().Hex())
}
errs = vm2.txPool.AddRemotesSync(txs[0:5])
for i, err := range errs {
if err != nil {
t.Fatalf("Failed to add transaction to VM2 at index %d: %s", i, err)
}
}
msg, err = vm2.WaitForEvent(context.Background())
require.NoError(t, err)
require.Equal(t, commonEng.PendingTxs, msg)
vm2BlkC, err := vm2.BuildBlock(context.Background())
if err != nil {
t.Fatalf("Failed to build BlkC on VM2: %s", err)
}
vm1BlkC, err := vm1.ParseBlock(context.Background(), vm2BlkC.Bytes())
if err != nil {
t.Fatalf("Unexpected error parsing block from vm2: %s", err)
}
if err := vm1BlkC.Verify(context.Background()); err != nil {
t.Fatalf("Block failed verification on VM1: %s", err)
}
if _, err := vm1.GetBlockIDAtHeight(context.Background(), vm1BlkC.Height()); err != database.ErrNotFound {
t.Fatalf("Expected unaccepted block not to be indexed by height, but found %s", err)
}
if err := vm1BlkC.Accept(context.Background()); err != nil {
t.Fatalf("VM1 failed to accept block: %s", err)
}
if blkID, err := vm1.GetBlockIDAtHeight(context.Background(), vm1BlkC.Height()); err != nil {
t.Fatalf("Height lookuped failed on accepted block: %s", err)
} else if blkID != vm1BlkC.ID() {
t.Fatalf("Expected accepted block to be indexed by height, but found %s", blkID)
}
vm1BlkCInternal := unwrapBlock(t, vm1BlkC)
blkCHash := getEthBlockFromProtocol(t, vm1BlkCInternal).Hash()
if b := vm1.blockChain.GetBlockByNumber(blkBHeight); b.Hash() != blkCHash {
t.Fatalf("expected block at %d to have hash %s but got %s", blkBHeight, blkCHash.Hex(), b.Hash().Hex())
}
}
// Regression test to ensure that a VM that verifies block B, C, then
// D (preferring block B) does not trigger a reorg through the re-verification
// of block C or D.
//
// A
// / \
// B C
// |
// D
func TestStickyPreference(t *testing.T) {
for _, scheme := range schemes {
t.Run(scheme, func(t *testing.T) {
testStickyPreference(t, scheme)
})
}
}
func testStickyPreference(t *testing.T, scheme string) {
tvmConfig := testVMConfig{
genesisJSON: genesisJSONSubnetEVM,
configJSON: getConfig(scheme, ""),
}
tvm1 := newVM(t, tvmConfig)
tvm2 := newVM(t, tvmConfig)
vm1 := tvm1.vm
vm2 := tvm2.vm
defer func() {
if err := vm1.Shutdown(context.Background()); err != nil {
t.Fatal(err)
}
if err := vm2.Shutdown(context.Background()); err != nil {
t.Fatal(err)
}
}()
newTxPoolHeadChan1 := make(chan core.NewTxPoolReorgEvent, 1)
vm1.txPool.SubscribeNewReorgEvent(newTxPoolHeadChan1)
newTxPoolHeadChan2 := make(chan core.NewTxPoolReorgEvent, 1)
vm2.txPool.SubscribeNewReorgEvent(newTxPoolHeadChan2)
tx := types.NewTransaction(uint64(0), testEthAddrs[1], firstTxAmount, 21000, big.NewInt(testMinGasPrice), nil)
// Convert secp256k1 key to ECDSA for signing
keyBytes := testKeys[0].Bytes()
privKey, err := crypto.ToECDSA(keyBytes)
if err != nil {
t.Fatal(err)
}
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(vm1.chainConfig.ChainID), privKey)
if err != nil {
t.Fatal(err)
}
txErrors := vm1.txPool.AddRemotesSync([]*types.Transaction{signedTx})
for i, err := range txErrors {
if err != nil {
t.Fatalf("Failed to add tx at index %d: %s", i, err)
}
}
msg, err := vm1.WaitForEvent(context.Background())
require.NoError(t, err)
require.Equal(t, commonEng.PendingTxs, msg)
vm1BlkA, err := vm1.BuildBlock(context.Background())
if err != nil {
t.Fatalf("Failed to build block with import transaction: %s", err)
}
if err := vm1BlkA.Verify(context.Background()); err != nil {
t.Fatalf("Block failed verification on VM1: %s", err)
}
if err := vm1.SetPreference(context.Background(), vm1BlkA.ID()); err != nil {
t.Fatal(err)
}
vm2BlkA, err := vm2.ParseBlock(context.Background(), vm1BlkA.Bytes())
if err != nil {
t.Fatalf("Unexpected error parsing block from vm2: %s", err)
}
if err := vm2BlkA.Verify(context.Background()); err != nil {
t.Fatalf("Block failed verification on VM2: %s", err)
}
if err := vm2.SetPreference(context.Background(), vm2BlkA.ID()); err != nil {
t.Fatal(err)
}
if err := vm1BlkA.Accept(context.Background()); err != nil {
t.Fatalf("VM1 failed to accept block: %s", err)
}
if err := vm2BlkA.Accept(context.Background()); err != nil {
t.Fatalf("VM2 failed to accept block: %s", err)
}
newHead := <-newTxPoolHeadChan1
if newHead.Head.Hash() != common.Hash(vm1BlkA.ID()) {
t.Fatalf("Expected new block to match")
}
newHead = <-newTxPoolHeadChan2
if newHead.Head.Hash() != common.Hash(vm2BlkA.ID()) {
t.Fatalf("Expected new block to match")
}
// Create list of 10 successive transactions to build block A on vm1
// and to be split into two separate blocks on VM2
txs := make([]*types.Transaction, 10)
for i := 0; i < 10; i++ {
tx := types.NewTransaction(uint64(i), testEthAddrs[0], big.NewInt(10), 21000, big.NewInt(testMinGasPrice), nil)
// Convert secp256k1 key to ECDSA for signing
keyBytes := testKeys[1].Bytes()
privKey, err := crypto.ToECDSA(keyBytes)
if err != nil {
t.Fatal(err)
}
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(vm1.chainConfig.ChainID), privKey)
if err != nil {
t.Fatal(err)
}
txs[i] = signedTx
}
var errs []error
// Add the remote transactions, build the block, and set VM1's preference for block A
errs = vm1.txPool.AddRemotesSync(txs)
for i, err := range errs {
if err != nil {
t.Fatalf("Failed to add transaction to VM1 at index %d: %s", i, err)
}
}
msg, err = vm1.WaitForEvent(context.Background())
require.NoError(t, err)
require.Equal(t, commonEng.PendingTxs, msg)
vm1BlkB, err := vm1.BuildBlock(context.Background())
if err != nil {
t.Fatal(err)
}
if err := vm1BlkB.Verify(context.Background()); err != nil {
t.Fatal(err)
}
if err := vm1.SetPreference(context.Background(), vm1BlkB.ID()); err != nil {
t.Fatal(err)
}
blkBHeight := vm1BlkB.Height()
vm1BlkBInternal := unwrapBlock(t, vm1BlkB)
blkBHash := getEthBlock(t, vm1BlkBInternal).Hash()
if b := vm1.blockChain.GetBlockByNumber(blkBHeight); b.Hash() != blkBHash {
t.Fatalf("expected block at %d to have hash %s but got %s", blkBHeight, blkBHash.Hex(), b.Hash().Hex())
}
errs = vm2.txPool.AddRemotesSync(txs[0:5])
for i, err := range errs {
if err != nil {
t.Fatalf("Failed to add transaction to VM2 at index %d: %s", i, err)
}
}
msg, err = vm2.WaitForEvent(context.Background())
require.NoError(t, err)
require.Equal(t, commonEng.PendingTxs, msg)
vm2BlkC, err := vm2.BuildBlock(context.Background())
if err != nil {
t.Fatalf("Failed to build BlkC on VM2: %s", err)
}
if err := vm2BlkC.Verify(context.Background()); err != nil {
t.Fatalf("BlkC failed verification on VM2: %s", err)
}
if err := vm2.SetPreference(context.Background(), vm2BlkC.ID()); err != nil {
t.Fatal(err)
}
newHead = <-newTxPoolHeadChan2
if newHead.Head.Hash() != common.Hash(vm2BlkC.ID()) {
t.Fatalf("Expected new block to match")
}
errs = vm2.txPool.AddRemotesSync(txs[5:])
for i, err := range errs {
if err != nil {
t.Fatalf("Failed to add transaction to VM2 at index %d: %s", i, err)
}
}
msg, err = vm2.WaitForEvent(context.Background())
require.NoError(t, err)
require.Equal(t, commonEng.PendingTxs, msg)
vm2BlkD, err := vm2.BuildBlock(context.Background())
if err != nil {
t.Fatalf("Failed to build BlkD on VM2: %s", err)
}
// Parse blocks produced in vm2
vm1BlkC, err := vm1.ParseBlock(context.Background(), vm2BlkC.Bytes())
if err != nil {
t.Fatalf("Unexpected error parsing block from vm2: %s", err)
}
vm1BlkCInternal := unwrapBlock(t, vm1BlkC)
blkCHash := getEthBlockFromProtocol(t, vm1BlkCInternal).Hash()
vm1BlkD, err := vm1.ParseBlock(context.Background(), vm2BlkD.Bytes())
if err != nil {
t.Fatalf("Unexpected error parsing block from vm2: %s", err)
}
blkDHeight := vm1BlkD.Height()
vm1BlkDInternal := unwrapBlock(t, vm1BlkD)
blkDHash := getEthBlockFromProtocol(t, vm1BlkDInternal).Hash()
// Should be no-ops
if err := vm1BlkC.Verify(context.Background()); err != nil {
t.Fatalf("Block failed verification on VM1: %s", err)
}
if err := vm1BlkD.Verify(context.Background()); err != nil {
t.Fatalf("Block failed verification on VM1: %s", err)
}
if b := vm1.blockChain.GetBlockByNumber(blkBHeight); b.Hash() != blkBHash {
t.Fatalf("expected block at %d to have hash %s but got %s", blkBHeight, blkBHash.Hex(), b.Hash().Hex())
}
if b := vm1.blockChain.GetBlockByNumber(blkDHeight); b != nil {
t.Fatalf("expected block at %d to be nil but got %s", blkDHeight, b.Hash().Hex())
}
if b := vm1.blockChain.CurrentBlock(); b.Hash() != blkBHash {
t.Fatalf("expected current block to have hash %s but got %s", blkBHash.Hex(), b.Hash().Hex())
}
// Should still be no-ops on re-verify
if err := vm1BlkC.Verify(context.Background()); err != nil {
t.Fatalf("Block failed verification on VM1: %s", err)
}
if err := vm1BlkD.Verify(context.Background()); err != nil {
t.Fatalf("Block failed verification on VM1: %s", err)
}
if b := vm1.blockChain.GetBlockByNumber(blkBHeight); b.Hash() != blkBHash {
t.Fatalf("expected block at %d to have hash %s but got %s", blkBHeight, blkBHash.Hex(), b.Hash().Hex())
}
if b := vm1.blockChain.GetBlockByNumber(blkDHeight); b != nil {
t.Fatalf("expected block at %d to be nil but got %s", blkDHeight, b.Hash().Hex())
}
if b := vm1.blockChain.CurrentBlock(); b.Hash() != blkBHash {
t.Fatalf("expected current block to have hash %s but got %s", blkBHash.Hex(), b.Hash().Hex())
}
// Should be queryable after setting preference to side chain
if err := vm1.SetPreference(context.Background(), vm1BlkD.ID()); err != nil {
t.Fatal(err)
}
if b := vm1.blockChain.GetBlockByNumber(blkBHeight); b.Hash() != blkCHash {
t.Fatalf("expected block at %d to have hash %s but got %s", blkBHeight, blkCHash.Hex(), b.Hash().Hex())
}
if b := vm1.blockChain.GetBlockByNumber(blkDHeight); b.Hash() != blkDHash {
t.Fatalf("expected block at %d to have hash %s but got %s", blkDHeight, blkDHash.Hex(), b.Hash().Hex())
}
if b := vm1.blockChain.CurrentBlock(); b.Hash() != blkDHash {
t.Fatalf("expected current block to have hash %s but got %s", blkDHash.Hex(), b.Hash().Hex())
}
// Attempt to accept out of order
if err := vm1BlkD.Accept(context.Background()); !strings.Contains(err.Error(), "expected accepted block to have parent") {
t.Fatalf("unexpected error when accepting out of order block: %s", err)
}
// Accept in order
if err := vm1BlkC.Accept(context.Background()); err != nil {
t.Fatalf("Block failed verification on VM1: %s", err)
}
if err := vm1BlkD.Accept(context.Background()); err != nil {
t.Fatalf("Block failed acceptance on VM1: %s", err)
}
// Ensure queryable after accepting
if b := vm1.blockChain.GetBlockByNumber(blkBHeight); b.Hash() != blkCHash {
t.Fatalf("expected block at %d to have hash %s but got %s", blkBHeight, blkCHash.Hex(), b.Hash().Hex())
}
if b := vm1.blockChain.GetBlockByNumber(blkDHeight); b.Hash() != blkDHash {
t.Fatalf("expected block at %d to have hash %s but got %s", blkDHeight, blkDHash.Hex(), b.Hash().Hex())
}
if b := vm1.blockChain.CurrentBlock(); b.Hash() != blkDHash {
t.Fatalf("expected current block to have hash %s but got %s", blkDHash.Hex(), b.Hash().Hex())
}
}
// Regression test to ensure that a VM that prefers block B is able to parse
// block C but unable to parse block D because it names B as an uncle, which
// are not supported.
//
// A
// / \
// B C
// |
// D
func TestUncleBlock(t *testing.T) {
for _, scheme := range schemes {
t.Run(scheme, func(t *testing.T) {
testUncleBlock(t, scheme)
})
}
}
func testUncleBlock(t *testing.T, scheme string) {
tvmConfig := testVMConfig{
genesisJSON: genesisJSONSubnetEVM,
configJSON: getConfig(scheme, ""),
}
tvm1 := newVM(t, tvmConfig)
tvm2 := newVM(t, tvmConfig)
vm1 := tvm1.vm
vm2 := tvm2.vm
defer func() {
if err := vm1.Shutdown(context.Background()); err != nil {
t.Fatal(err)
}
if err := vm2.Shutdown(context.Background()); err != nil {
t.Fatal(err)
}
}()
newTxPoolHeadChan1 := make(chan core.NewTxPoolReorgEvent, 1)
vm1.txPool.SubscribeNewReorgEvent(newTxPoolHeadChan1)
newTxPoolHeadChan2 := make(chan core.NewTxPoolReorgEvent, 1)
vm2.txPool.SubscribeNewReorgEvent(newTxPoolHeadChan2)
tx := types.NewTransaction(uint64(0), testEthAddrs[1], firstTxAmount, 21000, big.NewInt(testMinGasPrice), nil)
// Convert secp256k1 key to ECDSA for signing
keyBytes := testKeys[0].Bytes()
privKey, err := crypto.ToECDSA(keyBytes)
if err != nil {
t.Fatal(err)
}
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(vm1.chainConfig.ChainID), privKey)
if err != nil {
t.Fatal(err)
}
txErrors := vm1.txPool.AddRemotesSync([]*types.Transaction{signedTx})
for i, err := range txErrors {
if err != nil {
t.Fatalf("Failed to add tx at index %d: %s", i, err)
}
}
msg, err := vm1.WaitForEvent(context.Background())
require.NoError(t, err)
require.Equal(t, commonEng.PendingTxs, msg)
vm1BlkA, err := vm1.BuildBlock(context.Background())
if err != nil {
t.Fatalf("Failed to build block with import transaction: %s", err)
}
if err := vm1BlkA.Verify(context.Background()); err != nil {
t.Fatalf("Block failed verification on VM1: %s", err)
}
if err := vm1.SetPreference(context.Background(), vm1BlkA.ID()); err != nil {
t.Fatal(err)
}
vm2BlkA, err := vm2.ParseBlock(context.Background(), vm1BlkA.Bytes())
if err != nil {
t.Fatalf("Unexpected error parsing block from vm2: %s", err)
}
if err := vm2BlkA.Verify(context.Background()); err != nil {
t.Fatalf("Block failed verification on VM2: %s", err)
}
if err := vm2.SetPreference(context.Background(), vm2BlkA.ID()); err != nil {
t.Fatal(err)
}
if err := vm1BlkA.Accept(context.Background()); err != nil {
t.Fatalf("VM1 failed to accept block: %s", err)
}
if err := vm2BlkA.Accept(context.Background()); err != nil {
t.Fatalf("VM2 failed to accept block: %s", err)
}
newHead := <-newTxPoolHeadChan1
if newHead.Head.Hash() != common.Hash(vm1BlkA.ID()) {
t.Fatalf("Expected new block to match")
}
newHead = <-newTxPoolHeadChan2
if newHead.Head.Hash() != common.Hash(vm2BlkA.ID()) {
t.Fatalf("Expected new block to match")
}
txs := make([]*types.Transaction, 10)
for i := 0; i < 10; i++ {
tx := types.NewTransaction(uint64(i), testEthAddrs[0], big.NewInt(10), 21000, big.NewInt(testMinGasPrice), nil)
// Convert secp256k1 key to ECDSA for signing
keyBytes := testKeys[1].Bytes()
privKey, err := crypto.ToECDSA(keyBytes)
if err != nil {
t.Fatal(err)
}
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(vm1.chainConfig.ChainID), privKey)
if err != nil {
t.Fatal(err)
}
txs[i] = signedTx
}
var errs []error
errs = vm1.txPool.AddRemotesSync(txs)
for i, err := range errs {
if err != nil {
t.Fatalf("Failed to add transaction to VM1 at index %d: %s", i, err)
}
}
msg, err = vm1.WaitForEvent(context.Background())
require.NoError(t, err)
require.Equal(t, commonEng.PendingTxs, msg)
vm1BlkB, err := vm1.BuildBlock(context.Background())
if err != nil {
t.Fatal(err)
}
if err := vm1BlkB.Verify(context.Background()); err != nil {
t.Fatal(err)
}
if err := vm1.SetPreference(context.Background(), vm1BlkB.ID()); err != nil {
t.Fatal(err)
}
errs = vm2.txPool.AddRemotesSync(txs[0:5])
for i, err := range errs {
if err != nil {
t.Fatalf("Failed to add transaction to VM2 at index %d: %s", i, err)
}
}
msg, err = vm2.WaitForEvent(context.Background())
require.NoError(t, err)
require.Equal(t, commonEng.PendingTxs, msg)
vm2BlkC, err := vm2.BuildBlock(context.Background())
if err != nil {
t.Fatalf("Failed to build BlkC on VM2: %s", err)
}
if err := vm2BlkC.Verify(context.Background()); err != nil {
t.Fatalf("BlkC failed verification on VM2: %s", err)
}
if err := vm2.SetPreference(context.Background(), vm2BlkC.ID()); err != nil {
t.Fatal(err)
}
newHead = <-newTxPoolHeadChan2
if newHead.Head.Hash() != common.Hash(vm2BlkC.ID()) {
t.Fatalf("Expected new block to match")
}
errs = vm2.txPool.AddRemotesSync(txs[5:10])
for i, err := range errs {
if err != nil {
t.Fatalf("Failed to add transaction to VM2 at index %d: %s", i, err)
}
}
msg, err = vm2.WaitForEvent(context.Background())
require.NoError(t, err)
require.Equal(t, commonEng.PendingTxs, msg)
vm2BlkD, err := vm2.BuildBlock(context.Background())
if err != nil {
t.Fatalf("Failed to build BlkD on VM2: %s", err)
}
// Create uncle block from blkD
vm2BlkDInternal := unwrapBlock(t, vm2BlkD)
blkDEthBlock := getEthBlock(t, vm2BlkDInternal)
vm1BlkBInternal2 := unwrapBlock(t, vm1BlkB)
uncles := []*types.Header{getEthBlock(t, vm1BlkBInternal2).Header()}
uncleBlockHeader := types.CopyHeader(blkDEthBlock.Header())
uncleBlockHeader.UncleHash = types.CalcUncleHash(uncles)
uncleEthBlock := types.NewBlock(
uncleBlockHeader,
&types.Body{
Transactions: blkDEthBlock.Transactions(),
Uncles: uncles,
},
nil,
trie.NewStackTrie(nil),
)
uncleBlock := vm2.newBlock(uncleEthBlock)
if err := uncleBlock.Verify(context.Background()); !errors.Is(err, errUnclesUnsupported) {
t.Fatalf("VM2 should have failed with %q but got %v", errUnclesUnsupported, err)
}
if _, err := vm1.ParseBlock(context.Background(), vm2BlkC.Bytes()); err != nil {
t.Fatalf("VM1 errored parsing blkC: %s", err)
}
if _, err := vm1.ParseBlock(context.Background(), uncleBlock.Bytes()); !errors.Is(err, errUnclesUnsupported) {
t.Fatalf("VM1 should have failed with %q but got %v", errUnclesUnsupported, err)
}
}
// Regression test to ensure that a VM that is not able to parse a block that
// contains no transactions.
func TestEmptyBlock(t *testing.T) {
for _, scheme := range schemes {
t.Run(scheme, func(t *testing.T) {
testEmptyBlock(t, scheme)
})
}
}
func testEmptyBlock(t *testing.T, scheme string) {
tvm := newVM(t, testVMConfig{
genesisJSON: genesisJSONSubnetEVM,
configJSON: getConfig(scheme, ""),
})
defer func() {
if err := tvm.vm.Shutdown(context.Background()); err != nil {
t.Fatal(err)
}
}()
tx := types.NewTransaction(uint64(0), testEthAddrs[1], firstTxAmount, 21000, big.NewInt(testMinGasPrice), nil)
// Convert secp256k1 key to ECDSA for signing
keyBytes := testKeys[0].Bytes()
privKey, err := crypto.ToECDSA(keyBytes)
if err != nil {
t.Fatal(err)
}
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(tvm.vm.chainConfig.ChainID), privKey)
if err != nil {
t.Fatal(err)
}
txErrors := tvm.vm.txPool.AddRemotesSync([]*types.Transaction{signedTx})
for i, err := range txErrors {
if err != nil {
t.Fatalf("Failed to add tx at index %d: %s", i, err)
}
}
msg, err := tvm.vm.WaitForEvent(context.Background())
require.NoError(t, err)
require.Equal(t, commonEng.PendingTxs, msg)
blk, err := tvm.vm.BuildBlock(context.Background())
if err != nil {
t.Fatalf("Failed to build block with import transaction: %s", err)
}
// Create empty block from blkA
blkInternal := unwrapBlock(t, blk)
ethBlock := getEthBlock(t, blkInternal)
emptyEthBlock := types.NewBlock(
types.CopyHeader(ethBlock.Header()),
&types.Body{},
nil,
new(trie.Trie),
)
emptyBlock := tvm.vm.newBlock(emptyEthBlock)
if _, err := tvm.vm.ParseBlock(context.Background(), emptyBlock.Bytes()); !errors.Is(err, errEmptyBlock) {
t.Fatalf("VM should have failed with errEmptyBlock but got %s", err.Error())
}
if err := emptyBlock.Verify(context.Background()); !errors.Is(err, errEmptyBlock) {
t.Fatalf("block should have failed verification with errEmptyBlock but got %s", err.Error())
}
}
// Regression test to ensure that a VM that verifies block B, C, then
// D (preferring block B) reorgs when C and then D are accepted.
//
// A
// / \
// B C
// |
// D
func TestAcceptReorg(t *testing.T) {
for _, scheme := range schemes {
t.Run(scheme, func(t *testing.T) {
testAcceptReorg(t, scheme)
})
}
}
func testAcceptReorg(t *testing.T, scheme string) {
tvmConfig := testVMConfig{
genesisJSON: genesisJSONSubnetEVM,
configJSON: getConfig(scheme, ""),
}
tvm1 := newVM(t, tvmConfig)
tvm2 := newVM(t, tvmConfig)
vm1 := tvm1.vm
vm2 := tvm2.vm
defer func() {
if err := vm1.Shutdown(context.Background()); err != nil {
t.Fatal(err)
}
if err := vm2.Shutdown(context.Background()); err != nil {
t.Fatal(err)
}
}()
newTxPoolHeadChan1 := make(chan core.NewTxPoolReorgEvent, 1)
vm1.txPool.SubscribeNewReorgEvent(newTxPoolHeadChan1)
newTxPoolHeadChan2 := make(chan core.NewTxPoolReorgEvent, 1)
vm2.txPool.SubscribeNewReorgEvent(newTxPoolHeadChan2)
tx := types.NewTransaction(uint64(0), testEthAddrs[1], firstTxAmount, 21000, big.NewInt(testMinGasPrice), nil)
// Convert secp256k1 key to ECDSA for signing
keyBytes := testKeys[0].Bytes()
privKey, err := crypto.ToECDSA(keyBytes)
if err != nil {
t.Fatal(err)
}
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(vm1.chainConfig.ChainID), privKey)
if err != nil {
t.Fatal(err)
}
txErrors := vm1.txPool.AddRemotesSync([]*types.Transaction{signedTx})
for i, err := range txErrors {
if err != nil {
t.Fatalf("Failed to add tx at index %d: %s", i, err)
}
}
msg, err := vm1.WaitForEvent(context.Background())
require.NoError(t, err)
require.Equal(t, commonEng.PendingTxs, msg)
vm1BlkA, err := vm1.BuildBlock(context.Background())
if err != nil {
t.Fatalf("Failed to build block with import transaction: %s", err)
}
if err := vm1BlkA.Verify(context.Background()); err != nil {
t.Fatalf("Block failed verification on VM1: %s", err)
}
if err := vm1.SetPreference(context.Background(), vm1BlkA.ID()); err != nil {
t.Fatal(err)
}
vm2BlkA, err := vm2.ParseBlock(context.Background(), vm1BlkA.Bytes())
if err != nil {
t.Fatalf("Unexpected error parsing block from vm2: %s", err)
}
if err := vm2BlkA.Verify(context.Background()); err != nil {
t.Fatalf("Block failed verification on VM2: %s", err)
}
if err := vm2.SetPreference(context.Background(), vm2BlkA.ID()); err != nil {
t.Fatal(err)
}
if err := vm1BlkA.Accept(context.Background()); err != nil {
t.Fatalf("VM1 failed to accept block: %s", err)
}
if err := vm2BlkA.Accept(context.Background()); err != nil {
t.Fatalf("VM2 failed to accept block: %s", err)
}
newHead := <-newTxPoolHeadChan1
if newHead.Head.Hash() != common.Hash(vm1BlkA.ID()) {
t.Fatalf("Expected new block to match")
}
newHead = <-newTxPoolHeadChan2
if newHead.Head.Hash() != common.Hash(vm2BlkA.ID()) {
t.Fatalf("Expected new block to match")
}
// Create list of 10 successive transactions to build block A on vm1
// and to be split into two separate blocks on VM2
txs := make([]*types.Transaction, 10)
for i := 0; i < 10; i++ {
tx := types.NewTransaction(uint64(i), testEthAddrs[0], big.NewInt(10), 21000, big.NewInt(testMinGasPrice), nil)
// Convert secp256k1 key to ECDSA for signing
keyBytes := testKeys[1].Bytes()
privKey, err := crypto.ToECDSA(keyBytes)
if err != nil {
t.Fatal(err)
}
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(vm1.chainConfig.ChainID), privKey)
if err != nil {
t.Fatal(err)
}
txs[i] = signedTx
}
// Add the remote transactions, build the block, and set VM1's preference
// for block B
errs := vm1.txPool.AddRemotesSync(txs)
for i, err := range errs {
if err != nil {
t.Fatalf("Failed to add transaction to VM1 at index %d: %s", i, err)
}
}
msg, err = vm1.WaitForEvent(context.Background())
require.NoError(t, err)
require.Equal(t, commonEng.PendingTxs, msg)
vm1BlkB, err := vm1.BuildBlock(context.Background())
if err != nil {
t.Fatal(err)
}
if err := vm1BlkB.Verify(context.Background()); err != nil {
t.Fatal(err)
}
if err := vm1.SetPreference(context.Background(), vm1BlkB.ID()); err != nil {
t.Fatal(err)
}
errs = vm2.txPool.AddRemotesSync(txs[0:5])
for i, err := range errs {
if err != nil {
t.Fatalf("Failed to add transaction to VM2 at index %d: %s", i, err)
}
}
msg, err = vm2.WaitForEvent(context.Background())
require.NoError(t, err)
require.Equal(t, commonEng.PendingTxs, msg)
vm2BlkC, err := vm2.BuildBlock(context.Background())
if err != nil {
t.Fatalf("Failed to build BlkC on VM2: %s", err)
}
if err := vm2BlkC.Verify(context.Background()); err != nil {
t.Fatalf("BlkC failed verification on VM2: %s", err)
}
if err := vm2.SetPreference(context.Background(), vm2BlkC.ID()); err != nil {
t.Fatal(err)
}
newHead = <-newTxPoolHeadChan2
if newHead.Head.Hash() != common.Hash(vm2BlkC.ID()) {
t.Fatalf("Expected new block to match")
}
errs = vm2.txPool.AddRemotesSync(txs[5:])
for i, err := range errs {
if err != nil {
t.Fatalf("Failed to add transaction to VM2 at index %d: %s", i, err)
}
}
msg, err = vm2.WaitForEvent(context.Background())
require.NoError(t, err)
require.Equal(t, commonEng.PendingTxs, msg)
vm2BlkD, err := vm2.BuildBlock(context.Background())
if err != nil {
t.Fatalf("Failed to build BlkD on VM2: %s", err)
}
// Parse blocks produced in vm2
vm1BlkC, err := vm1.ParseBlock(context.Background(), vm2BlkC.Bytes())
if err != nil {
t.Fatalf("Unexpected error parsing block from vm2: %s", err)
}
vm1BlkD, err := vm1.ParseBlock(context.Background(), vm2BlkD.Bytes())
if err != nil {
t.Fatalf("Unexpected error parsing block from vm2: %s", err)
}
if err := vm1BlkC.Verify(context.Background()); err != nil {
t.Fatalf("Block failed verification on VM1: %s", err)
}
if err := vm1BlkD.Verify(context.Background()); err != nil {
t.Fatalf("Block failed verification on VM1: %s", err)
}
vm1BlkBInternal3 := unwrapBlock(t, vm1BlkB)
blkBHash := getEthBlock(t, vm1BlkBInternal3).Hash()
if b := vm1.blockChain.CurrentBlock(); b.Hash() != blkBHash {
t.Fatalf("expected current block to have hash %s but got %s", blkBHash.Hex(), b.Hash().Hex())
}
if err := vm1BlkC.Accept(context.Background()); err != nil {
t.Fatal(err)
}
vm1BlkCInternal := unwrapBlock(t, vm1BlkC)
blkCHash := getEthBlockFromProtocol(t, vm1BlkCInternal).Hash()
if b := vm1.blockChain.CurrentBlock(); b.Hash() != blkCHash {
t.Fatalf("expected current block to have hash %s but got %s", blkCHash.Hex(), b.Hash().Hex())
}
if err := vm1BlkB.Reject(context.Background()); err != nil {
t.Fatal(err)
}
if err := vm1BlkD.Accept(context.Background()); err != nil {
t.Fatal(err)
}
vm1BlkDInternal := unwrapBlock(t, vm1BlkD)
blkDHash := getEthBlockFromProtocol(t, vm1BlkDInternal).Hash()
if b := vm1.blockChain.CurrentBlock(); b.Hash() != blkDHash {
t.Fatalf("expected current block to have hash %s but got %s", blkDHash.Hex(), b.Hash().Hex())
}
}
func TestFutureBlock(t *testing.T) {
for _, scheme := range schemes {
t.Run(scheme, func(t *testing.T) {
testFutureBlock(t, scheme)
})
}
}
func testFutureBlock(t *testing.T, scheme string) {
tvm := newVM(t, testVMConfig{
genesisJSON: genesisJSONSubnetEVM,
configJSON: getConfig(scheme, ""),
})
defer func() {
if err := tvm.vm.Shutdown(context.Background()); err != nil {
t.Fatal(err)
}
}()
tx := types.NewTransaction(uint64(0), testEthAddrs[1], firstTxAmount, 21000, big.NewInt(testMinGasPrice), nil)
// Convert secp256k1 key to ECDSA for signing
keyBytes := testKeys[0].Bytes()
privKey, err := crypto.ToECDSA(keyBytes)
if err != nil {
t.Fatal(err)
}
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(tvm.vm.chainConfig.ChainID), privKey)
if err != nil {
t.Fatal(err)
}
txErrors := tvm.vm.txPool.AddRemotesSync([]*types.Transaction{signedTx})
for i, err := range txErrors {
if err != nil {
t.Fatalf("Failed to add tx at index %d: %s", i, err)
}
}
msg, err := tvm.vm.WaitForEvent(context.Background())
require.NoError(t, err)
require.Equal(t, commonEng.PendingTxs, msg)
blkA, err := tvm.vm.BuildBlock(context.Background())
if err != nil {
t.Fatalf("Failed to build block with import transaction: %s", err)
}
// Create empty block from blkA
blkAInternal := unwrapBlock(t, blkA)
internalBlkA := getInternalBlock(t, blkAInternal)
modifiedHeader := types.CopyHeader(internalBlkA.ethBlock.Header())
// Set the VM's clock to the time of the produced block
tvm.vm.clock.Set(time.Unix(int64(modifiedHeader.Time), 0))
// Set the modified time to exceed the allowed future time
modifiedTime := modifiedHeader.Time + uint64(maxFutureBlockTime.Seconds()+1)
modifiedHeader.Time = modifiedTime
modifiedBlock := types.NewBlock(
modifiedHeader,
&types.Body{
Transactions: internalBlkA.ethBlock.Transactions(),
},
nil,
trie.NewStackTrie(nil),
)
futureBlock := tvm.vm.newBlock(modifiedBlock)
if err := futureBlock.Verify(context.Background()); err == nil {
t.Fatal("Future block should have failed verification due to block timestamp too far in the future")
} else if !strings.Contains(err.Error(), "block timestamp is too far in the future") {
t.Fatalf("Expected error to be block timestamp too far in the future but found %s", err)
}
}
func TestLastAcceptedBlockNumberAllow(t *testing.T) {
for _, scheme := range schemes {
t.Run(scheme, func(t *testing.T) {
testLastAcceptedBlockNumberAllow(t, scheme)
})
}
}
func testLastAcceptedBlockNumberAllow(t *testing.T, scheme string) {
tvm := newVM(t, testVMConfig{
genesisJSON: genesisJSONSubnetEVM,
configJSON: getConfig(scheme, ""),
})
defer func() {
if err := tvm.vm.Shutdown(context.Background()); err != nil {
t.Fatal(err)
}
}()
tx := types.NewTransaction(uint64(0), testEthAddrs[1], firstTxAmount, 21000, big.NewInt(testMinGasPrice), nil)
// Convert secp256k1 key to ECDSA for signing
keyBytes := testKeys[0].Bytes()
privKey, err := crypto.ToECDSA(keyBytes)
if err != nil {
t.Fatal(err)
}
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(tvm.vm.chainConfig.ChainID), privKey)
if err != nil {
t.Fatal(err)
}
txErrors := tvm.vm.txPool.AddRemotesSync([]*types.Transaction{signedTx})
for i, err := range txErrors {
if err != nil {
t.Fatalf("Failed to add tx at index %d: %s", i, err)
}
}
msg, err := tvm.vm.WaitForEvent(context.Background())
require.NoError(t, err)
require.Equal(t, commonEng.PendingTxs, msg)
blk, err := tvm.vm.BuildBlock(context.Background())
if err != nil {
t.Fatalf("Failed to build block with import transaction: %s", err)
}
if err := blk.Verify(context.Background()); err != nil {
t.Fatalf("Block failed verification on VM: %s", err)
}
if err := tvm.vm.SetPreference(context.Background(), blk.ID()); err != nil {
t.Fatal(err)
}
blkHeight := blk.Height()
blkInternal2 := unwrapBlock(t, blk)
blkHash := getEthBlock(t, blkInternal2).Hash()
tvm.vm.eth.APIBackend.SetAllowUnfinalizedQueries(true)
ctx := context.Background()
b, err := tvm.vm.eth.APIBackend.BlockByNumber(ctx, rpc.BlockNumber(blkHeight))
if err != nil {
t.Fatal(err)
}
if b.Hash() != blkHash {
t.Fatalf("expected block at %d to have hash %s but got %s", blkHeight, blkHash.Hex(), b.Hash().Hex())
}
tvm.vm.eth.APIBackend.SetAllowUnfinalizedQueries(false)
_, err = tvm.vm.eth.APIBackend.BlockByNumber(ctx, rpc.BlockNumber(blkHeight))
if !errors.Is(err, eth.ErrUnfinalizedData) {
t.Fatalf("expected ErrUnfinalizedData but got %s", err.Error())
}
if err := blk.Accept(context.Background()); err != nil {
t.Fatalf("VM failed to accept block: %s", err)
}
if b := tvm.vm.blockChain.GetBlockByNumber(blkHeight); b.Hash() != blkHash {
t.Fatalf("expected block at %d to have hash %s but got %s", blkHeight, blkHash.Hex(), b.Hash().Hex())
}
}
// Regression test to ensure we can build blocks if we are starting with the
// Subnet EVM ruleset in genesis.
func TestBuildSubnetEVMBlock(t *testing.T) {
for _, scheme := range schemes {
t.Run(scheme, func(t *testing.T) {
testBuildSubnetEVMBlock(t, scheme)
})
}
}
func testBuildSubnetEVMBlock(t *testing.T, scheme string) {
tvm := newVM(t, testVMConfig{
genesisJSON: genesisJSONSubnetEVM,
configJSON: getConfig(scheme, ""),
})
defer func() {
if err := tvm.vm.Shutdown(context.Background()); err != nil {
t.Fatal(err)
}
}()
newTxPoolHeadChan := make(chan core.NewTxPoolReorgEvent, 1)
tvm.vm.txPool.SubscribeNewReorgEvent(newTxPoolHeadChan)
tx := types.NewTransaction(uint64(0), testEthAddrs[1], new(big.Int).Mul(firstTxAmount, big.NewInt(4)), 21000, big.NewInt(testMinGasPrice*3), nil)
// Convert secp256k1 key to ECDSA for signing
keyBytes := testKeys[0].Bytes()
privKey, err := crypto.ToECDSA(keyBytes)
if err != nil {
t.Fatal(err)
}
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(tvm.vm.chainConfig.ChainID), privKey)
if err != nil {
t.Fatal(err)
}
txErrors := tvm.vm.txPool.AddRemotesSync([]*types.Transaction{signedTx})
for i, err := range txErrors {
if err != nil {
t.Fatalf("Failed to add tx at index %d: %s", i, err)
}
}
blk := issueAndAccept(t, tvm.vm)
newHead := <-newTxPoolHeadChan
if newHead.Head.Hash() != common.Hash(blk.ID()) {
t.Fatalf("Expected new block to match")
}
txs := make([]*types.Transaction, 10)
for i := 0; i < 10; i++ {
tx := types.NewTransaction(uint64(i), testEthAddrs[0], big.NewInt(10), 21000, big.NewInt(testMinGasPrice*3), nil)
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(tvm.vm.chainConfig.ChainID), func() *ecdsa.PrivateKey {
key, err := toECDSA(testKeys[1])
if err != nil {
t.Fatal(err)
}
return key
}())
if err != nil {
t.Fatal(err)
}
txs[i] = signedTx
}
errs := tvm.vm.txPool.AddRemotesSync(txs)
for i, err := range errs {
if err != nil {
t.Fatalf("Failed to add tx at index %d: %s", i, err)
}
}
blk = issueAndAccept(t, tvm.vm)
ethBlk := getEthBlock(t, blk)
if customtypes.BlockGasCost(ethBlk) == nil || customtypes.BlockGasCost(ethBlk).Cmp(big.NewInt(100)) < 0 {
t.Fatalf("expected blockGasCost to be at least 100 but got %d", customtypes.BlockGasCost(ethBlk))
}
chainConfig := params.GetExtra(tvm.vm.chainConfig)
minRequiredTip, err := header.EstimateRequiredTip(chainConfig, ethBlk.Header())
if err != nil {
t.Fatal(err)
}
if minRequiredTip == nil || minRequiredTip.Cmp(big.NewInt(0.05*utils.GWei)) < 0 {
t.Fatalf("expected minRequiredTip to be at least 0.05 gwei but got %d", minRequiredTip)
}
lastAcceptedID, err := tvm.vm.LastAccepted(context.Background())
if err != nil {
t.Fatal(err)
}
if lastAcceptedID != blk.ID() {
t.Fatalf("Expected last accepted blockID to be the accepted block: %s, but found %s", blk.ID(), lastAcceptedID)
}
// Confirm all txs are present
ethBlkTxs := tvm.vm.blockChain.GetBlockByNumber(2).Transactions()
for i, tx := range txs {
if len(ethBlkTxs) <= i {
t.Fatalf("missing transactions expected: %d but found: %d", len(txs), len(ethBlkTxs))
}
if ethBlkTxs[i].Hash() != tx.Hash() {
t.Fatalf("expected tx at index %d to have hash: %x but has: %x", i, txs[i].Hash(), tx.Hash())
}
}
}
func TestBuildAllowListActivationBlock(t *testing.T) {
for _, scheme := range schemes {
t.Run(scheme, func(t *testing.T) {
testBuildAllowListActivationBlock(t, scheme)
})
}
}
func testBuildAllowListActivationBlock(t *testing.T, scheme string) {
genesis := &core.Genesis{}
if err := genesis.UnmarshalJSON([]byte(genesisJSONSubnetEVM)); err != nil {
t.Fatal(err)
}
extra := params.GetExtra(genesis.Config)
// Set required timestamps that aren't preserved during JSON unmarshalling
extra.EVMTimestamp = utils.NewUint64(0)
extra.DurangoTimestamp = utils.NewUint64(0)
extra.GenesisPrecompiles = extras.Precompiles{
deployerallowlist.ConfigKey: deployerallowlist.NewConfig(utils.TimeToNewUint64(time.Now()), testEthAddrs, nil, nil),
}
genesisJSON := marshalGenesisWithExtras(genesis)
tvm := newVM(t, testVMConfig{
genesisJSON: genesisJSON,
configJSON: getConfig(scheme, ""),
})
defer func() {
if err := tvm.vm.Shutdown(context.Background()); err != nil {
t.Fatal(err)
}
}()
newTxPoolHeadChan := make(chan core.NewTxPoolReorgEvent, 1)
tvm.vm.txPool.SubscribeNewReorgEvent(newTxPoolHeadChan)
genesisState, err := tvm.vm.blockChain.StateAt(tvm.vm.blockChain.Genesis().Root())
if err != nil {
t.Fatal(err)
}
role := deployerallowlist.GetContractDeployerAllowListStatus(genesisState, testEthAddrs[0])
if role != allowlist.NoRole {
t.Fatalf("Expected allow list status to be set to no role: %s, but found: %s", allowlist.NoRole, role)
}
// Send basic transaction to construct a simple block and confirm that the precompile state configuration in the worker behaves correctly.
tx := types.NewTransaction(uint64(0), testEthAddrs[1], new(big.Int).Mul(firstTxAmount, big.NewInt(4)), 21000, big.NewInt(testMinGasPrice*3), nil)
// Convert secp256k1 key to ECDSA for signing
keyBytes := testKeys[0].Bytes()
privKey, err := crypto.ToECDSA(keyBytes)
if err != nil {
t.Fatal(err)
}
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(tvm.vm.chainConfig.ChainID), privKey)
if err != nil {
t.Fatal(err)
}
txErrors := tvm.vm.txPool.AddRemotesSync([]*types.Transaction{signedTx})
for i, err := range txErrors {
if err != nil {
t.Fatalf("Failed to add tx at index %d: %s", i, err)
}
}
blk := issueAndAccept(t, tvm.vm)
newHead := <-newTxPoolHeadChan
if newHead.Head.Hash() != common.Hash(blk.ID()) {
t.Fatalf("Expected new block to match")
}
// Verify that the allow list config activation was handled correctly in the first block.
blkState, err := tvm.vm.blockChain.StateAt(getEthBlock(t, blk).Root())
if err != nil {
t.Fatal(err)
}
role = deployerallowlist.GetContractDeployerAllowListStatus(blkState, testEthAddrs[0])
if role != allowlist.AdminRole {
t.Fatalf("Expected allow list status to be set role %s, but found: %s", allowlist.AdminRole, role)
}
}
// Test that the tx allow list allows whitelisted transactions and blocks non-whitelisted addresses
func TestTxAllowListSuccessfulTx(t *testing.T) {
// Setup chain params
managerKey := testKeys[1]
managerAddress := testEthAddrs[1]
genesis := &core.Genesis{}
if err := genesis.UnmarshalJSON([]byte(toGenesisJSON(forkToChainConfig["Durango"]))); err != nil {
t.Fatal(err)
}
// this manager role should not be activated because DurangoTimestamp is in the future
params.GetExtra(genesis.Config).GenesisPrecompiles = extras.Precompiles{
txallowlist.ConfigKey: txallowlist.NewConfig(utils.NewUint64(0), testEthAddrs[0:1], nil, nil),
}
durangoTime := time.Now().Add(10 * time.Hour)
params.GetExtra(genesis.Config).DurangoTimestamp = utils.TimeToNewUint64(durangoTime)
genesisJSON := marshalGenesisWithExtras(genesis)
// prepare the new upgrade bytes to disable the TxAllowList
disableAllowListTime := durangoTime.Add(10 * time.Hour)
reenableAllowlistTime := disableAllowListTime.Add(10 * time.Hour)
upgradeConfig := &extras.UpgradeConfig{
PrecompileUpgrades: []extras.PrecompileUpgrade{
{
Config: txallowlist.NewDisableConfig(utils.TimeToNewUint64(disableAllowListTime)),
},
// re-enable the tx allowlist after Durango to set the manager role
{
Config: txallowlist.NewConfig(utils.TimeToNewUint64(reenableAllowlistTime), testEthAddrs[0:1], nil, []common.Address{managerAddress}),
},
},
}
upgradeBytesJSON, err := json.Marshal(upgradeConfig)
require.NoError(t, err)
tvm := newVM(t, testVMConfig{
genesisJSON: string(genesisJSON),
upgradeJSON: string(upgradeBytesJSON),
})
defer func() {
if err := tvm.vm.Shutdown(context.Background()); err != nil {
t.Fatal(err)
}
}()
newTxPoolHeadChan := make(chan core.NewTxPoolReorgEvent, 1)
tvm.vm.txPool.SubscribeNewReorgEvent(newTxPoolHeadChan)
genesisState, err := tvm.vm.blockChain.StateAt(tvm.vm.blockChain.Genesis().Root())
if err != nil {
t.Fatal(err)
}
// Check that address 0 is whitelisted and address 1 is not
role := txallowlist.GetTxAllowListStatus(genesisState, testEthAddrs[0])
if role != allowlist.AdminRole {
t.Fatalf("Expected allow list status to be set to admin: %s, but found: %s", allowlist.AdminRole, role)
}
role = txallowlist.GetTxAllowListStatus(genesisState, testEthAddrs[1])
if role != allowlist.NoRole {
t.Fatalf("Expected allow list status to be set to no role: %s, but found: %s", allowlist.NoRole, role)
}
// Should not be a manager role because Durango has not activated yet
role = txallowlist.GetTxAllowListStatus(genesisState, managerAddress)
require.Equal(t, allowlist.NoRole, role)
// Submit a successful transaction
tx0 := types.NewTransaction(uint64(0), testEthAddrs[0], big.NewInt(1), 21000, big.NewInt(testMinGasPrice), nil)
signedTx0, err := types.SignTx(tx0, types.NewEIP155Signer(tvm.vm.chainConfig.ChainID), func() *ecdsa.PrivateKey {
key, err := toECDSA(testKeys[0])
if err != nil {
t.Fatal(err)
}
return key
}())
require.NoError(t, err)
errs := tvm.vm.txPool.AddRemotesSync([]*types.Transaction{signedTx0})
if err := errs[0]; err != nil {
t.Fatalf("Failed to add tx at index: %s", err)
}
// Submit a rejected transaction, should throw an error
tx1 := types.NewTransaction(uint64(0), testEthAddrs[1], big.NewInt(2), 21000, big.NewInt(testMinGasPrice), nil)
signedTx1, err := types.SignTx(tx1, types.NewEIP155Signer(tvm.vm.chainConfig.ChainID), func() *ecdsa.PrivateKey {
key, err := toECDSA(testKeys[1])
if err != nil {
t.Fatal(err)
}
return key
}())
if err != nil {
t.Fatal(err)
}
errs = tvm.vm.txPool.AddRemotesSync([]*types.Transaction{signedTx1})
if err := errs[0]; !errors.Is(err, vmerrors.ErrSenderAddressNotAllowListed) {
t.Fatalf("expected ErrSenderAddressNotAllowListed, got: %s", err)
}
// Submit a rejected transaction, should throw an error because manager is not activated
tx2 := types.NewTransaction(uint64(0), managerAddress, big.NewInt(2), 21000, big.NewInt(testMinGasPrice), nil)
// Convert secp256k1 key to ECDSA for signing
managerKeyBytes := managerKey.Bytes()
managerPrivKey, err := crypto.ToECDSA(managerKeyBytes)
if err != nil {
t.Fatal(err)
}
signedTx2, err := types.SignTx(tx2, types.NewEIP155Signer(tvm.vm.chainConfig.ChainID), managerPrivKey)
require.NoError(t, err)
errs = tvm.vm.txPool.AddRemotesSync([]*types.Transaction{signedTx2})
require.ErrorIs(t, errs[0], vmerrors.ErrSenderAddressNotAllowListed)
blk := issueAndAccept(t, tvm.vm)
newHead := <-newTxPoolHeadChan
require.Equal(t, newHead.Head.Hash(), common.Hash(blk.ID()))
// Verify that the constructed block only has the whitelisted tx
block := getEthBlock(t, blk)
txs := block.Transactions()
if txs.Len() != 1 {
t.Fatalf("Expected number of txs to be %d, but found %d", 1, txs.Len())
}
require.Equal(t, signedTx0.Hash(), txs[0].Hash())
tvm.vm.clock.Set(reenableAllowlistTime.Add(time.Hour))
// Re-Submit a successful transaction
tx0 = types.NewTransaction(uint64(1), testEthAddrs[0], big.NewInt(1), 21000, big.NewInt(testMinGasPrice), nil)
signedTx0, err = types.SignTx(tx0, types.NewEIP155Signer(tvm.vm.chainConfig.ChainID), func() *ecdsa.PrivateKey {
key, err := toECDSA(testKeys[0])
if err != nil {
t.Fatal(err)
}
return key
}())
require.NoError(t, err)
errs = tvm.vm.txPool.AddRemotesSync([]*types.Transaction{signedTx0})
require.NoError(t, errs[0])
// accept block to trigger upgrade
blk = issueAndAccept(t, tvm.vm)
newHead = <-newTxPoolHeadChan
require.Equal(t, newHead.Head.Hash(), common.Hash(blk.ID()))
block = getEthBlock(t, blk)
blkState, err := tvm.vm.blockChain.StateAt(block.Root())
require.NoError(t, err)
// Check that address 0 is admin and address 1 is manager
role = txallowlist.GetTxAllowListStatus(blkState, testEthAddrs[0])
require.Equal(t, allowlist.AdminRole, role)
role = txallowlist.GetTxAllowListStatus(blkState, managerAddress)
require.Equal(t, allowlist.ManagerRole, role)
tvm.vm.clock.Set(tvm.vm.clock.Time().Add(2 * time.Second)) // add 2 seconds for gas fee to adjust
// Submit a successful transaction, should not throw an error because manager is activated
tx3 := types.NewTransaction(uint64(0), managerAddress, big.NewInt(1), 21000, big.NewInt(testMinGasPrice), nil)
// Convert secp256k1 key to ECDSA for signing
managerKeyBytes3 := managerKey.Bytes()
managerPrivKey3, err := crypto.ToECDSA(managerKeyBytes3)
if err != nil {
t.Fatal(err)
}
signedTx3, err := types.SignTx(tx3, types.NewEIP155Signer(tvm.vm.chainConfig.ChainID), managerPrivKey3)
require.NoError(t, err)
tvm.vm.clock.Set(tvm.vm.clock.Time().Add(2 * time.Second)) // add 2 seconds for gas fee to adjust
errs = tvm.vm.txPool.AddRemotesSync([]*types.Transaction{signedTx3})
require.NoError(t, errs[0])
blk = issueAndAccept(t, tvm.vm)
newHead = <-newTxPoolHeadChan
require.Equal(t, newHead.Head.Hash(), common.Hash(blk.ID()))
// Verify that the constructed block only has the whitelisted tx
block = getEthBlock(t, blk)
txs = block.Transactions()
require.Len(t, txs, 1)
require.Equal(t, signedTx3.Hash(), txs[0].Hash())
}
func TestVerifyManagerConfig(t *testing.T) {
genesis := &core.Genesis{}
ctx, dbManager, genesisBytes, _ := setupGenesis(t, "Durango")
require.NoError(t, genesis.UnmarshalJSON(genesisBytes))
durangoTimestamp := time.Now().Add(10 * time.Hour)
params.GetExtra(genesis.Config).DurangoTimestamp = utils.TimeToNewUint64(durangoTimestamp)
// this manager role should not be activated because DurangoTimestamp is in the future
params.GetExtra(genesis.Config).GenesisPrecompiles = extras.Precompiles{
txallowlist.ConfigKey: txallowlist.NewConfig(utils.NewUint64(0), testEthAddrs[0:1], nil, []common.Address{testEthAddrs[1]}),
}
genesisJSON := marshalGenesisWithExtras(genesis)
vm := &VM{}
err := vm.Initialize(
context.Background(),
ctx,
dbManager,
[]byte(genesisJSON), // Manually set genesis bytes due to custom genesis
[]byte(""),
[]byte(""),
nil, // msgChan parameter
nil, // fxs parameter
nil, // appSender parameter
)
require.ErrorIs(t, err, allowlist.ErrCannotAddManagersBeforeDurango)
genesis = &core.Genesis{}
require.NoError(t, genesis.UnmarshalJSON([]byte(toGenesisJSON(forkToChainConfig["Durango"]))))
params.GetExtra(genesis.Config).DurangoTimestamp = utils.TimeToNewUint64(durangoTimestamp)
genesisJSON = marshalGenesisWithExtras(genesis)
// use an invalid upgrade now with managers set before Durango
upgradeConfig := &extras.UpgradeConfig{
PrecompileUpgrades: []extras.PrecompileUpgrade{
{
Config: txallowlist.NewConfig(utils.TimeToNewUint64(durangoTimestamp.Add(-time.Second)), nil, nil, []common.Address{testEthAddrs[1]}),
},
},
}
upgradeBytesJSON, err := json.Marshal(upgradeConfig)
require.NoError(t, err)
vm = &VM{}
ctx, dbManager, _, _ = setupGenesis(t, "Latest")
err = vm.Initialize(
context.Background(),
ctx,
dbManager,
[]byte(genesisJSON), // Manually set genesis bytes due to custom genesis
upgradeBytesJSON,
[]byte(""),
nil, // msgChan parameter
nil, // fxs parameter
nil, // appSender parameter
)
require.ErrorIs(t, err, allowlist.ErrCannotAddManagersBeforeDurango)
}
// Test that the tx allow list allows whitelisted transactions and blocks non-whitelisted addresses
// and the allowlist is removed after the precompile is disabled.
func TestTxAllowListDisablePrecompile(t *testing.T) {
// Setup chain params
genesis := &core.Genesis{}
if err := genesis.UnmarshalJSON([]byte(genesisJSONSubnetEVM)); err != nil {
t.Fatal(err)
}
extra := params.GetExtra(genesis.Config)
extra.EVMTimestamp = utils.NewUint64(0)
extra.DurangoTimestamp = utils.NewUint64(0)
enableAllowListTimestamp := time.Unix(0, 0) // enable at genesis
extra.GenesisPrecompiles = extras.Precompiles{
txallowlist.ConfigKey: txallowlist.NewConfig(utils.TimeToNewUint64(enableAllowListTimestamp), testEthAddrs[0:1], nil, nil),
}
genesisJSON := []byte(marshalGenesisWithExtras(genesis))
// arbitrary choice ahead of enableAllowListTimestamp
disableAllowListTimestamp := enableAllowListTimestamp.Add(10 * time.Hour)
// configure a network upgrade to remove the allowlist
upgradeConfig := fmt.Sprintf(`
{
"precompileUpgrades": [
{
"txAllowListConfig": {
"blockTimestamp": %d,
"disable": true
}
}
]
}
`, disableAllowListTimestamp.Unix())
tvm := newVM(t, testVMConfig{
genesisJSON: string(genesisJSON),
upgradeJSON: upgradeConfig,
})
tvm.vm.clock.Set(disableAllowListTimestamp) // upgrade takes effect after a block is issued, so we can set vm's clock here.
defer func() {
if err := tvm.vm.Shutdown(context.Background()); err != nil {
t.Fatal(err)
}
}()
newTxPoolHeadChan := make(chan core.NewTxPoolReorgEvent, 1)
tvm.vm.txPool.SubscribeNewReorgEvent(newTxPoolHeadChan)
genesisState, err := tvm.vm.blockChain.StateAt(tvm.vm.blockChain.Genesis().Root())
if err != nil {
t.Fatal(err)
}
// Check that address 0 is whitelisted and address 1 is not
role := txallowlist.GetTxAllowListStatus(genesisState, testEthAddrs[0])
if role != allowlist.AdminRole {
t.Fatalf("Expected allow list status to be set to admin: %s, but found: %s", allowlist.AdminRole, role)
}
role = txallowlist.GetTxAllowListStatus(genesisState, testEthAddrs[1])
if role != allowlist.NoRole {
t.Fatalf("Expected allow list status to be set to no role: %s, but found: %s", allowlist.NoRole, role)
}
// Submit a successful transaction
tx0 := types.NewTransaction(uint64(0), testEthAddrs[0], big.NewInt(1), 21000, big.NewInt(testMinGasPrice), nil)
signedTx0, err := types.SignTx(tx0, types.NewEIP155Signer(tvm.vm.chainConfig.ChainID), func() *ecdsa.PrivateKey {
key, err := toECDSA(testKeys[0])
if err != nil {
t.Fatal(err)
}
return key
}())
require.NoError(t, err)
errs := tvm.vm.txPool.AddRemotesSync([]*types.Transaction{signedTx0})
if err := errs[0]; err != nil {
t.Fatalf("Failed to add tx at index: %s", err)
}
// Submit a rejected transaction, should throw an error
tx1 := types.NewTransaction(uint64(0), testEthAddrs[1], big.NewInt(2), 21000, big.NewInt(testMinGasPrice), nil)
signedTx1, err := types.SignTx(tx1, types.NewEIP155Signer(tvm.vm.chainConfig.ChainID), func() *ecdsa.PrivateKey {
key, err := toECDSA(testKeys[1])
if err != nil {
t.Fatal(err)
}
return key
}())
if err != nil {
t.Fatal(err)
}
errs = tvm.vm.txPool.AddRemotesSync([]*types.Transaction{signedTx1})
if err := errs[0]; !errors.Is(err, vmerrors.ErrSenderAddressNotAllowListed) {
t.Fatalf("expected ErrSenderAddressNotAllowListed, got: %s", err)
}
blk := issueAndAccept(t, tvm.vm)
// Verify that the constructed block only has the whitelisted tx
block := getEthBlock(t, blk)
txs := block.Transactions()
if txs.Len() != 1 {
t.Fatalf("Expected number of txs to be %d, but found %d", 1, txs.Len())
}
require.Equal(t, signedTx0.Hash(), txs[0].Hash())
// verify the issued block is after the network upgrade
require.GreaterOrEqual(t, int64(block.Time()), disableAllowListTimestamp.Unix())
<-newTxPoolHeadChan // wait for new head in tx pool
// retry the rejected Tx, which should now succeed
errs = tvm.vm.txPool.AddRemotesSync([]*types.Transaction{signedTx1})
if err := errs[0]; err != nil {
t.Fatalf("Failed to add tx at index: %s", err)
}
tvm.vm.clock.Set(tvm.vm.clock.Time().Add(2 * time.Second)) // add 2 seconds for gas fee to adjust
blk = issueAndAccept(t, tvm.vm)
// Verify that the constructed block only has the previously rejected tx
block = getEthBlock(t, blk)
txs = block.Transactions()
if txs.Len() != 1 {
t.Fatalf("Expected number of txs to be %d, but found %d", 1, txs.Len())
}
require.Equal(t, signedTx1.Hash(), txs[0].Hash())
}
// Test that the fee manager changes fee configuration
func TestFeeManagerChangeFee(t *testing.T) {
// Setup chain params
genesis := &core.Genesis{}
if err := genesis.UnmarshalJSON([]byte(genesisJSONSubnetEVM)); err != nil {
t.Fatal(err)
}
configExtra := params.GetExtra(genesis.Config)
configExtra.EVMTimestamp = utils.NewUint64(0)
configExtra.DurangoTimestamp = utils.NewUint64(0)
configExtra.GenesisPrecompiles = extras.Precompiles{
feemanager.ConfigKey: feemanager.NewConfig(utils.NewUint64(0), testEthAddrs[0:1], nil, nil, nil),
}
// set a lower fee config now
testLowFeeConfig := commontype.FeeConfig{
GasLimit: big.NewInt(8_000_000),
TargetBlockRate: 5, // in seconds
MinBaseFee: big.NewInt(5_000_000_000),
TargetGas: big.NewInt(18_000_000),
BaseFeeChangeDenominator: big.NewInt(3396),
MinBlockGasCost: big.NewInt(0),
MaxBlockGasCost: big.NewInt(4_000_000),
BlockGasCostStep: big.NewInt(500_000),
}
configExtra.FeeConfig = testLowFeeConfig
genesisJSON := []byte(marshalGenesisWithExtras(genesis))
tvm := newVM(t, testVMConfig{
genesisJSON: string(genesisJSON),
})
defer func() {
if err := tvm.vm.Shutdown(context.Background()); err != nil {
t.Fatal(err)
}
}()
newTxPoolHeadChan := make(chan core.NewTxPoolReorgEvent, 1)
tvm.vm.txPool.SubscribeNewReorgEvent(newTxPoolHeadChan)
genesisState, err := tvm.vm.blockChain.StateAt(tvm.vm.blockChain.Genesis().Root())
if err != nil {
t.Fatal(err)
}
// Check that address 0 is whitelisted and address 1 is not
role := feemanager.GetFeeManagerStatus(genesisState, testEthAddrs[0])
if role != allowlist.AdminRole {
t.Fatalf("Expected fee manager list status to be set to admin: %s, but found: %s", allowlist.AdminRole, role)
}
role = feemanager.GetFeeManagerStatus(genesisState, testEthAddrs[1])
if role != allowlist.NoRole {
t.Fatalf("Expected fee manager list status to be set to no role: %s, but found: %s", allowlist.NoRole, role)
}
// Contract is initialized but no preconfig is given, reader should return genesis fee config
feeConfig, lastChangedAt, err := tvm.vm.blockChain.GetFeeConfigAt(tvm.vm.blockChain.Genesis().Header())
require.NoError(t, err)
require.EqualValues(t, feeConfig, testLowFeeConfig)
require.Zero(t, tvm.vm.blockChain.CurrentBlock().Number.Cmp(lastChangedAt))
// set a different fee config now
testHighFeeConfig := testLowFeeConfig
testHighFeeConfig.MinBaseFee = big.NewInt(28_000_000_000)
data, err := feemanager.PackSetFeeConfig(testHighFeeConfig)
require.NoError(t, err)
tx := types.NewTx(&types.DynamicFeeTx{
ChainID: genesis.Config.ChainID,
Nonce: uint64(0),
To: &feemanager.ContractAddress,
Gas: testLowFeeConfig.GasLimit.Uint64(),
Value: common.Big0,
GasFeeCap: testLowFeeConfig.MinBaseFee, // give low fee, it should work since we still haven't applied high fees
GasTipCap: common.Big0,
Data: data,
})
signedTx, err := types.SignTx(tx, types.LatestSigner(genesis.Config), func() *ecdsa.PrivateKey {
key, err := toECDSA(testKeys[0])
if err != nil {
t.Fatal(err)
}
return key
}())
if err != nil {
t.Fatal(err)
}
errs := tvm.vm.txPool.AddRemotesSync([]*types.Transaction{signedTx})
if err := errs[0]; err != nil {
t.Fatalf("Failed to add tx at index: %s", err)
}
blk := issueAndAccept(t, tvm.vm)
newHead := <-newTxPoolHeadChan
if newHead.Head.Hash() != common.Hash(blk.ID()) {
t.Fatalf("Expected new block to match")
}
block := getEthBlock(t, blk)
feeConfig, lastChangedAt, err = tvm.vm.blockChain.GetFeeConfigAt(block.Header())
require.NoError(t, err)
require.EqualValues(t, testHighFeeConfig, feeConfig)
require.EqualValues(t, tvm.vm.blockChain.CurrentBlock().Number, lastChangedAt)
// should fail, with same params since fee is higher now
tx2 := types.NewTx(&types.DynamicFeeTx{
ChainID: genesis.Config.ChainID,
Nonce: uint64(1),
To: &feemanager.ContractAddress,
Gas: configExtra.FeeConfig.GasLimit.Uint64(),
Value: common.Big0,
GasFeeCap: testLowFeeConfig.MinBaseFee, // this is too low for applied config, should fail
GasTipCap: common.Big0,
Data: data,
})
signedTx2, err := types.SignTx(tx2, types.LatestSigner(genesis.Config), func() *ecdsa.PrivateKey {
key, err := toECDSA(testKeys[0])
if err != nil {
t.Fatal(err)
}
return key
}())
if err != nil {
t.Fatal(err)
}
err = tvm.vm.txPool.AddRemotesSync([]*types.Transaction{signedTx2})[0]
require.ErrorIs(t, err, txpool.ErrUnderpriced)
}
// Test Allow Fee Recipients is disabled and, etherbase must be blackhole address
func TestAllowFeeRecipientDisabled(t *testing.T) {
for _, scheme := range schemes {
t.Run(scheme, func(t *testing.T) {
testAllowFeeRecipientDisabled(t, scheme)
})
}
}
func testAllowFeeRecipientDisabled(t *testing.T, scheme string) {
genesis := &core.Genesis{}
if err := genesis.UnmarshalJSON([]byte(genesisJSONSubnetEVM)); err != nil {
t.Fatal(err)
}
extra := params.GetExtra(genesis.Config)
extra.EVMTimestamp = utils.NewUint64(0)
extra.DurangoTimestamp = utils.NewUint64(0)
extra.AllowFeeRecipients = false // set to false initially
genesisJSON := []byte(marshalGenesisWithExtras(genesis))
tvm := newVM(t, testVMConfig{
genesisJSON: string(genesisJSON),
configJSON: getConfig(scheme, ""),
})
tvm.vm.miner.SetEtherbase(common.HexToAddress("0x0123456789")) // set non-blackhole address by force
defer func() {
if err := tvm.vm.Shutdown(context.Background()); err != nil {
t.Fatal(err)
}
}()
newTxPoolHeadChan := make(chan core.NewTxPoolReorgEvent, 1)
tvm.vm.txPool.SubscribeNewReorgEvent(newTxPoolHeadChan)
tx := types.NewTransaction(uint64(0), testEthAddrs[1], new(big.Int).Mul(firstTxAmount, big.NewInt(4)), 21000, big.NewInt(testMinGasPrice*3), nil)
// Convert secp256k1 key to ECDSA for signing
keyBytes := testKeys[0].Bytes()
privKey, err := crypto.ToECDSA(keyBytes)
if err != nil {
t.Fatal(err)
}
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(tvm.vm.chainConfig.ChainID), privKey)
if err != nil {
t.Fatal(err)
}
txErrors := tvm.vm.txPool.AddRemotesSync([]*types.Transaction{signedTx})
for i, err := range txErrors {
if err != nil {
t.Fatalf("Failed to add tx at index %d: %s", i, err)
}
}
msg, err := tvm.vm.WaitForEvent(context.Background())
require.NoError(t, err)
require.Equal(t, commonEng.PendingTxs, msg)
blk, err := tvm.vm.BuildBlock(context.Background())
require.NoError(t, err) // this won't return an error since miner will set the etherbase to blackhole address
blkInternal3 := unwrapBlock(t, blk)
ethBlock := getEthBlock(t, blkInternal3)
require.Equal(t, constants.BlackholeAddr, ethBlock.Coinbase())
// Create empty block from blk
internalBlk := getInternalBlock(t, blkInternal3)
// Get the original header extra
originalHeader := internalBlk.ethBlock.Header()
originalExtra := customtypes.GetHeaderExtra(originalHeader)
// Copy header and modify coinbase
modifiedHeader := types.CopyHeader(originalHeader)
modifiedHeader.Coinbase = common.HexToAddress("0x0123456789") // set non-blackhole address by force
// Create the block first - NewBlock copies the header and modifies TxHash, ReceiptHash, etc.
// which changes the hash again
modifiedBlock := types.NewBlock(
modifiedHeader,
&types.Body{
Transactions: internalBlk.ethBlock.Transactions(),
},
nil,
trie.NewStackTrie(nil),
)
// Set the extra AFTER NewBlock, using the block's final header hash
if originalExtra != nil && originalExtra.BlockGasCost != nil {
customtypes.SetHeaderExtra(modifiedBlock.Header(), originalExtra)
}
modifiedBlk := tvm.vm.newBlock(modifiedBlock)
err = modifiedBlk.Verify(context.Background())
require.ErrorIs(t, err, vmerrors.ErrInvalidCoinbase)
}
func TestAllowFeeRecipientEnabled(t *testing.T) {
genesis := &core.Genesis{}
if err := genesis.UnmarshalJSON([]byte(genesisJSONSubnetEVM)); err != nil {
t.Fatal(err)
}
extra := params.GetExtra(genesis.Config)
extra.EVMTimestamp = utils.NewUint64(0)
extra.DurangoTimestamp = utils.NewUint64(0)
extra.AllowFeeRecipients = true
genesisJSON := []byte(marshalGenesisWithExtras(genesis))
etherBase := common.HexToAddress("0x0123456789")
c := config.Config{}
c.SetDefaults(defaultTxPoolConfig)
c.FeeRecipient = etherBase.String()
configJSON, err := json.Marshal(c)
if err != nil {
t.Fatal(err)
}
tvm := newVM(t, testVMConfig{
genesisJSON: string(genesisJSON),
configJSON: string(configJSON),
})
defer func() {
if err := tvm.vm.Shutdown(context.Background()); err != nil {
t.Fatal(err)
}
}()
newTxPoolHeadChan := make(chan core.NewTxPoolReorgEvent, 1)
tvm.vm.txPool.SubscribeNewReorgEvent(newTxPoolHeadChan)
tx := types.NewTransaction(uint64(0), testEthAddrs[1], new(big.Int).Mul(firstTxAmount, big.NewInt(4)), 21000, big.NewInt(testMinGasPrice*3), nil)
// Convert secp256k1 key to ECDSA for signing
keyBytes := testKeys[0].Bytes()
privKey, err := crypto.ToECDSA(keyBytes)
if err != nil {
t.Fatal(err)
}
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(tvm.vm.chainConfig.ChainID), privKey)
if err != nil {
t.Fatal(err)
}
txErrors := tvm.vm.txPool.AddRemotesSync([]*types.Transaction{signedTx})
for i, err := range txErrors {
if err != nil {
t.Fatalf("Failed to add tx at index %d: %s", i, err)
}
}
blk := issueAndAccept(t, tvm.vm)
newHead := <-newTxPoolHeadChan
if newHead.Head.Hash() != common.Hash(blk.ID()) {
t.Fatalf("Expected new block to match")
}
ethBlock := getEthBlock(t, blk)
require.Equal(t, etherBase, ethBlock.Coinbase())
// Verify that etherBase has received fees
blkState, err := tvm.vm.blockChain.StateAt(ethBlock.Root())
if err != nil {
t.Fatal(err)
}
balance := blkState.GetBalance(etherBase)
require.Equal(t, 1, balance.Cmp(common.U2560))
}
func TestRewardManagerPrecompileSetRewardAddress(t *testing.T) {
genesis := &core.Genesis{}
require.NoError(t, genesis.UnmarshalJSON([]byte(genesisJSONSubnetEVM)))
extra := params.GetExtra(genesis.Config)
extra.EVMTimestamp = utils.NewUint64(0)
extra.DurangoTimestamp = utils.NewUint64(0)
extra.GenesisPrecompiles = extras.Precompiles{
rewardmanager.ConfigKey: rewardmanager.NewConfig(utils.NewUint64(0), testEthAddrs[0:1], nil, nil, nil),
}
extra.AllowFeeRecipients = true // enable this in genesis to test if this is recognized by the reward manager
genesisJSON := []byte(marshalGenesisWithExtras(genesis))
etherBase := common.HexToAddress("0x0123456789") // give custom ether base
c := config.Config{}
c.SetDefaults(defaultTxPoolConfig)
c.FeeRecipient = etherBase.String()
configJSON, err := json.Marshal(c)
require.NoError(t, err)
// arbitrary choice ahead of enableAllowListTimestamp
// configure a network upgrade to remove the reward manager
disableTime := time.Now().Add(10 * time.Hour)
// configure a network upgrade to remove the allowlist
upgradeConfig := fmt.Sprintf(`
{
"precompileUpgrades": [
{
"rewardManagerConfig": {
"blockTimestamp": %d,
"disable": true
}
}
]
}
`, disableTime.Unix())
tvm := newVM(t, testVMConfig{
genesisJSON: string(genesisJSON),
configJSON: string(configJSON),
upgradeJSON: upgradeConfig,
})
defer func() {
require.NoError(t, tvm.vm.Shutdown(context.Background()))
}()
newTxPoolHeadChan := make(chan core.NewTxPoolReorgEvent, 1)
tvm.vm.txPool.SubscribeNewReorgEvent(newTxPoolHeadChan)
testAddr := common.HexToAddress("0x9999991111")
data, err := rewardmanager.PackSetRewardAddress(testAddr)
require.NoError(t, err)
gas := 21000 + 240 + rewardmanager.SetRewardAddressGasCost + rewardmanager.RewardAddressChangedEventGasCost // 21000 for tx, 240 for tx data
tx := types.NewTransaction(uint64(0), rewardmanager.ContractAddress, big.NewInt(1), gas, big.NewInt(testMinGasPrice), data)
// Convert secp256k1 key to ECDSA for signing
keyBytes := testKeys[0].Bytes()
privKey, err := crypto.ToECDSA(keyBytes)
if err != nil {
t.Fatal(err)
}
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(tvm.vm.chainConfig.ChainID), privKey)
require.NoError(t, err)
txErrors := tvm.vm.txPool.AddRemotesSync([]*types.Transaction{signedTx})
for _, err := range txErrors {
require.NoError(t, err)
}
blk := issueAndAccept(t, tvm.vm)
newHead := <-newTxPoolHeadChan
require.Equal(t, newHead.Head.Hash(), common.Hash(blk.ID()))
ethBlock := getEthBlock(t, blk)
require.Equal(t, etherBase, ethBlock.Coinbase()) // reward address is activated at this block so this is fine
tx1 := types.NewTransaction(uint64(0), testEthAddrs[0], big.NewInt(2), 21000, big.NewInt(testMinGasPrice*3), nil)
signedTx1, err := types.SignTx(tx1, types.NewEIP155Signer(tvm.vm.chainConfig.ChainID), func() *ecdsa.PrivateKey {
key, err := toECDSA(testKeys[1])
if err != nil {
t.Fatal(err)
}
return key
}())
require.NoError(t, err)
txErrors = tvm.vm.txPool.AddRemotesSync([]*types.Transaction{signedTx1})
for _, err := range txErrors {
require.NoError(t, err)
}
blk = issueAndAccept(t, tvm.vm)
newHead = <-newTxPoolHeadChan
require.Equal(t, newHead.Head.Hash(), common.Hash(blk.ID()))
ethBlock = getEthBlock(t, blk)
require.Equal(t, testAddr, ethBlock.Coinbase()) // reward address was activated at previous block
// Verify that etherBase has received fees
blkState, err := tvm.vm.blockChain.StateAt(ethBlock.Root())
require.NoError(t, err)
balance := blkState.GetBalance(testAddr)
require.Equal(t, 1, balance.Cmp(common.U2560))
// Test Case: Disable reward manager
// This should revert back to enabling fee recipients
previousBalance := blkState.GetBalance(etherBase)
// issue a new block to trigger the upgrade
tvm.vm.clock.Set(disableTime) // upgrade takes effect after a block is issued, so we can set vm's clock here.
tx2 := types.NewTransaction(uint64(1), testEthAddrs[0], big.NewInt(2), 21000, big.NewInt(testMinGasPrice), nil)
signedTx2, err := types.SignTx(tx2, types.NewEIP155Signer(tvm.vm.chainConfig.ChainID), func() *ecdsa.PrivateKey {
key, err := toECDSA(testKeys[1])
if err != nil {
t.Fatal(err)
}
return key
}())
require.NoError(t, err)
txErrors = tvm.vm.txPool.AddRemotesSync([]*types.Transaction{signedTx2})
for _, err := range txErrors {
require.NoError(t, err)
}
blk = issueAndAccept(t, tvm.vm)
newHead = <-newTxPoolHeadChan
require.Equal(t, newHead.Head.Hash(), common.Hash(blk.ID()))
ethBlock = getEthBlock(t, blk)
// Reward manager deactivated at this block, so we expect the parent state
// to determine the coinbase for this block before full deactivation in the
// next block.
require.Equal(t, testAddr, ethBlock.Coinbase())
require.GreaterOrEqual(t, int64(ethBlock.Time()), disableTime.Unix())
tvm.vm.clock.Set(tvm.vm.clock.Time().Add(3 * time.Hour)) // let time pass to decrease gas price
// issue another block to verify that the reward manager is disabled
tx2 = types.NewTransaction(uint64(2), testEthAddrs[0], big.NewInt(2), 21000, big.NewInt(testMinGasPrice), nil)
signedTx2, err = types.SignTx(tx2, types.NewEIP155Signer(tvm.vm.chainConfig.ChainID), func() *ecdsa.PrivateKey {
key, err := toECDSA(testKeys[1])
if err != nil {
t.Fatal(err)
}
return key
}())
require.NoError(t, err)
txErrors = tvm.vm.txPool.AddRemotesSync([]*types.Transaction{signedTx2})
for _, err := range txErrors {
require.NoError(t, err)
}
blk = issueAndAccept(t, tvm.vm)
newHead = <-newTxPoolHeadChan
require.Equal(t, newHead.Head.Hash(), common.Hash(blk.ID()))
ethBlock = getEthBlock(t, blk)
// reward manager was disabled at previous block
// so this block should revert back to enabling fee recipients
require.Equal(t, etherBase, ethBlock.Coinbase())
require.GreaterOrEqual(t, int64(ethBlock.Time()), disableTime.Unix())
// Verify that Blackhole has received fees
blkState, err = tvm.vm.blockChain.StateAt(ethBlock.Root())
require.NoError(t, err)
balance = blkState.GetBalance(etherBase)
require.Equal(t, 1, balance.Cmp(previousBalance))
}
func TestRewardManagerPrecompileAllowFeeRecipients(t *testing.T) {
genesis := &core.Genesis{}
require.NoError(t, genesis.UnmarshalJSON([]byte(genesisJSONSubnetEVM)))
extra := params.GetExtra(genesis.Config)
extra.EVMTimestamp = utils.NewUint64(0)
extra.DurangoTimestamp = utils.NewUint64(0)
extra.GenesisPrecompiles = extras.Precompiles{
rewardmanager.ConfigKey: rewardmanager.NewConfig(utils.NewUint64(0), testEthAddrs[0:1], nil, nil, nil),
}
extra.AllowFeeRecipients = false // disable this in genesis
genesisJSON := []byte(marshalGenesisWithExtras(genesis))
etherBase := common.HexToAddress("0x0123456789") // give custom ether base
c := config.Config{}
c.SetDefaults(defaultTxPoolConfig)
c.FeeRecipient = etherBase.String()
configJSON, err := json.Marshal(c)
require.NoError(t, err)
// configure a network upgrade to remove the reward manager
// arbitrary choice ahead of enableAllowListTimestamp
// configure a network upgrade to remove the reward manager
disableTime := time.Now().Add(10 * time.Hour)
// configure a network upgrade to remove the allowlist
upgradeConfig := fmt.Sprintf(`
{
"precompileUpgrades": [
{
"rewardManagerConfig": {
"blockTimestamp": %d,
"disable": true
}
}
]
}
`, disableTime.Unix())
tvm := newVM(t, testVMConfig{
genesisJSON: string(genesisJSON),
configJSON: string(configJSON),
upgradeJSON: upgradeConfig,
})
defer func() {
require.NoError(t, tvm.vm.Shutdown(context.Background()))
}()
newTxPoolHeadChan := make(chan core.NewTxPoolReorgEvent, 1)
tvm.vm.txPool.SubscribeNewReorgEvent(newTxPoolHeadChan)
data, err := rewardmanager.PackAllowFeeRecipients()
require.NoError(t, err)
gas := 21000 + 240 + rewardmanager.SetRewardAddressGasCost + rewardmanager.RewardAddressChangedEventGasCost // 21000 for tx, 240 for tx data
tx := types.NewTransaction(uint64(0), rewardmanager.ContractAddress, big.NewInt(1), gas, big.NewInt(testMinGasPrice), data)
// Convert secp256k1 key to ECDSA for signing
keyBytes := testKeys[0].Bytes()
privKey, err := crypto.ToECDSA(keyBytes)
if err != nil {
t.Fatal(err)
}
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(tvm.vm.chainConfig.ChainID), privKey)
require.NoError(t, err)
txErrors := tvm.vm.txPool.AddRemotesSync([]*types.Transaction{signedTx})
for _, err := range txErrors {
require.NoError(t, err)
}
blk := issueAndAccept(t, tvm.vm)
newHead := <-newTxPoolHeadChan
require.Equal(t, newHead.Head.Hash(), common.Hash(blk.ID()))
ethBlock := getEthBlock(t, blk)
require.Equal(t, constants.BlackholeAddr, ethBlock.Coinbase()) // reward address is activated at this block so this is fine
tx1 := types.NewTransaction(uint64(0), testEthAddrs[0], big.NewInt(2), 21000, big.NewInt(testMinGasPrice*3), nil)
signedTx1, err := types.SignTx(tx1, types.NewEIP155Signer(tvm.vm.chainConfig.ChainID), func() *ecdsa.PrivateKey {
key, err := toECDSA(testKeys[1])
if err != nil {
t.Fatal(err)
}
return key
}())
require.NoError(t, err)
txErrors = tvm.vm.txPool.AddRemotesSync([]*types.Transaction{signedTx1})
for _, err := range txErrors {
require.NoError(t, err)
}
blk = issueAndAccept(t, tvm.vm)
newHead = <-newTxPoolHeadChan
require.Equal(t, newHead.Head.Hash(), common.Hash(blk.ID()))
ethBlock = getEthBlock(t, blk)
require.Equal(t, etherBase, ethBlock.Coinbase()) // reward address was activated at previous block
// Verify that etherBase has received fees
blkState, err := tvm.vm.blockChain.StateAt(ethBlock.Root())
require.NoError(t, err)
balance := blkState.GetBalance(etherBase)
require.Equal(t, 1, balance.Cmp(common.U2560))
// Test Case: Disable reward manager
// This should revert back to burning fees
previousBalance := blkState.GetBalance(constants.BlackholeAddr)
tvm.vm.clock.Set(disableTime) // upgrade takes effect after a block is issued, so we can set vm's clock here.
tx2 := types.NewTransaction(uint64(1), testEthAddrs[0], big.NewInt(2), 21000, big.NewInt(testMinGasPrice), nil)
signedTx2, err := types.SignTx(tx2, types.NewEIP155Signer(tvm.vm.chainConfig.ChainID), func() *ecdsa.PrivateKey {
key, err := toECDSA(testKeys[1])
if err != nil {
t.Fatal(err)
}
return key
}())
require.NoError(t, err)
txErrors = tvm.vm.txPool.AddRemotesSync([]*types.Transaction{signedTx2})
for _, err := range txErrors {
require.NoError(t, err)
}
blk = issueAndAccept(t, tvm.vm)
newHead = <-newTxPoolHeadChan
require.Equal(t, newHead.Head.Hash(), common.Hash(blk.ID()))
ethBlock = getEthBlock(t, blk)
require.Equal(t, etherBase, ethBlock.Coinbase()) // reward address was activated at previous block
require.GreaterOrEqual(t, int64(ethBlock.Time()), disableTime.Unix())
tvm.vm.clock.Set(tvm.vm.clock.Time().Add(3 * time.Hour)) // let time pass so that gas price is reduced
tx2 = types.NewTransaction(uint64(2), testEthAddrs[0], big.NewInt(2), 21000, big.NewInt(testMinGasPrice), nil)
signedTx2, err = types.SignTx(tx2, types.NewEIP155Signer(tvm.vm.chainConfig.ChainID), func() *ecdsa.PrivateKey {
key, err := toECDSA(testKeys[1])
if err != nil {
t.Fatal(err)
}
return key
}())
require.NoError(t, err)
txErrors = tvm.vm.txPool.AddRemotesSync([]*types.Transaction{signedTx2})
for _, err := range txErrors {
require.NoError(t, err)
}
blk = issueAndAccept(t, tvm.vm)
newHead = <-newTxPoolHeadChan
require.Equal(t, newHead.Head.Hash(), common.Hash(blk.ID()))
ethBlock = getEthBlock(t, blk)
require.Equal(t, constants.BlackholeAddr, ethBlock.Coinbase()) // reward address was activated at previous block
require.Greater(t, int64(ethBlock.Time()), disableTime.Unix())
// Verify that Blackhole has received fees
blkState, err = tvm.vm.blockChain.StateAt(ethBlock.Root())
require.NoError(t, err)
balance = blkState.GetBalance(constants.BlackholeAddr)
require.Equal(t, 1, balance.Cmp(previousBalance))
}
func TestSkipChainConfigCheckCompatible(t *testing.T) {
// The most recent network upgrade in Subnet-EVM is SubnetEVM itself, which cannot be disabled for this test since it results in
// disabling dynamic fees and causes a panic since some code assumes that this is enabled.
// NOTE: EVMTimestamp must be 0 (activated at genesis), so we use genesisJSONSubnetEVM
tvm := newVM(t, testVMConfig{
genesisJSON: genesisJSONSubnetEVM,
configJSON: `{"pruning-enabled":true}`,
})
defer func() {
if err := tvm.vm.Shutdown(context.Background()); err != nil {
t.Fatal(err)
}
}()
newTxPoolHeadChan := make(chan core.NewTxPoolReorgEvent, 1)
tvm.vm.txPool.SubscribeNewReorgEvent(newTxPoolHeadChan)
key := utilstest.NewKey(t)
tx := types.NewTransaction(uint64(0), key.Address, firstTxAmount, 21000, big.NewInt(testMinGasPrice), nil)
// Convert secp256k1 key to ECDSA for signing
keyBytes := testKeys[0].Bytes()
privKey, err := crypto.ToECDSA(keyBytes)
if err != nil {
t.Fatal(err)
}
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(tvm.vm.chainConfig.ChainID), privKey)
if err != nil {
t.Fatal(err)
}
errs := tvm.vm.txPool.AddRemotesSync([]*types.Transaction{signedTx})
for i, err := range errs {
if err != nil {
t.Fatalf("Failed to add tx at index %d: %s", i, err)
}
}
blk := issueAndAccept(t, tvm.vm)
newHead := <-newTxPoolHeadChan
if newHead.Head.Hash() != common.Hash(blk.ID()) {
t.Fatalf("Expected new block to match")
}
reinitVM := &VM{}
// use the block's timestamp instead of 0 since rewind to genesis
// is hardcoded to be allowed in core/genesis.go.
// Since json.Unmarshal loses the extras, we manipulate the JSON directly
genesisJSON := toGenesisJSON(forkToChainConfig["Durango"])
var genesisMap map[string]interface{}
require.NoError(t, json.Unmarshal([]byte(genesisJSON), &genesisMap))
configMap := genesisMap["config"].(map[string]interface{})
// Set QuasarTimestamp to the block's timestamp to trigger the compatibility check error
configMap["quasarTimestamp"] = blk.Timestamp().Unix()
// Ensure DurangoTimestamp is set (required for validation)
if _, ok := configMap["durangoTimestamp"]; !ok {
configMap["durangoTimestamp"] = uint64(0)
}
genesisWithUpgradeBytes, err := json.Marshal(genesisMap)
require.NoError(t, err)
// Reset metrics to allow re-initialization
// Note: Cannot directly modify context fields, skipping metrics reset
// this will not be allowed - the error may be about blobSchedule or timestamp mismatch
// depending on which validation fails first
initErr := reinitVM.Initialize(context.Background(), tvm.vm.chainCtx, tvm.db, genesisWithUpgradeBytes, []byte{}, []byte{}, nil, nil, tvm.appSender)
require.Error(t, initErr, "expected initialization to fail with chain config incompatibility")
// Reset metrics to allow re-initialization
// Note: Cannot directly modify context fields, skipping metrics reset
// try again with skip-upgrade-check - this should allow the initialization to proceed
// even with chain config differences (but note: blobSchedule errors are not skippable)
config := []byte(`{"skip-upgrade-check": true}`)
// The skip-upgrade-check flag only skips fork timestamp checks, not all validation
// So we just verify that either it passes or fails with a different (non-upgrade-related) error
reinitErr := reinitVM.Initialize(context.Background(), tvm.vm.chainCtx, tvm.db, genesisWithUpgradeBytes, []byte{}, config, nil, nil, tvm.appSender)
if reinitErr != nil {
// If it failed, it should NOT be the same error about timestamp mismatch
// (because skip-upgrade-check should have bypassed that)
require.NotContains(t, reinitErr.Error(), "mismatching", "skip-upgrade-check should bypass timestamp checks")
} else {
// If it succeeded, clean up
require.NoError(t, reinitVM.Shutdown(context.Background()))
}
}
func TestParentBeaconRootBlock(t *testing.T) {
tests := []struct {
name string
genesisJSON string
beaconRoot *common.Hash
expectedError bool
errString string
}{
{
name: "non-empty parent beacon root in Durango",
genesisJSON: toGenesisJSON(forkToChainConfig["Durango"]),
beaconRoot: &common.Hash{0x01},
expectedError: true,
// err string wont work because it will also fail with blob gas is non-empty (zeroed)
},
{
name: "empty parent beacon root in Durango",
genesisJSON: toGenesisJSON(forkToChainConfig["Durango"]),
beaconRoot: &common.Hash{},
expectedError: true,
},
{
name: "nil parent beacon root in Durango",
genesisJSON: toGenesisJSON(forkToChainConfig["Durango"]),
beaconRoot: nil,
expectedError: false,
},
{
name: "non-empty parent beacon root in E-Upgrade (Cancun)",
genesisJSON: toGenesisJSON(forkToChainConfig["Quasar"]),
beaconRoot: &common.Hash{0x01},
expectedError: true,
errString: "expected empty hash",
},
{
name: "empty parent beacon root in E-Upgrade (Cancun)",
genesisJSON: toGenesisJSON(forkToChainConfig["Quasar"]),
beaconRoot: &common.Hash{},
expectedError: false,
},
{
name: "nil parent beacon root in E-Upgrade (Cancun)",
genesisJSON: toGenesisJSON(forkToChainConfig["Quasar"]),
beaconRoot: nil,
expectedError: true,
errString: "header is missing parentBeaconRoot",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
tvm := newVM(t, testVMConfig{
genesisJSON: test.genesisJSON,
})
defer func() {
if err := tvm.vm.Shutdown(context.Background()); err != nil {
t.Fatal(err)
}
}()
tx := types.NewTransaction(uint64(0), testEthAddrs[1], firstTxAmount, 21000, big.NewInt(testMinGasPrice), nil)
// Convert secp256k1 key to ECDSA for signing
keyBytes := testKeys[0].Bytes()
privKey, err := crypto.ToECDSA(keyBytes)
if err != nil {
t.Fatal(err)
}
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(tvm.vm.chainConfig.ChainID), privKey)
if err != nil {
t.Fatal(err)
}
txErrors := tvm.vm.txPool.AddRemotesSync([]*types.Transaction{signedTx})
for i, err := range txErrors {
if err != nil {
t.Fatalf("Failed to add tx at index %d: %s", i, err)
}
}
msg, err := tvm.vm.WaitForEvent(context.Background())
require.NoError(t, err)
require.Equal(t, commonEng.PendingTxs, msg)
blk, err := tvm.vm.BuildBlock(context.Background())
if err != nil {
t.Fatalf("Failed to build block with import transaction: %s", err)
}
// Modify the block to have a parent beacon root
blkInternalTest := unwrapBlock(t, blk)
ethBlock := getEthBlock(t, blkInternalTest)
// Get the original extra before modifying
originalExtra := customtypes.GetHeaderExtra(ethBlock.Header())
header := types.CopyHeader(ethBlock.Header())
header.ParentBeaconRoot = test.beaconRoot
parentBeaconEthBlock := ethBlock.WithSeal(header)
// Set the extra on the new block's header (WithSeal creates a new header)
if originalExtra != nil && originalExtra.BlockGasCost != nil {
customtypes.SetHeaderExtra(parentBeaconEthBlock.Header(), originalExtra)
}
parentBeaconBlock := tvm.vm.newBlock(parentBeaconEthBlock)
errCheck := func(err error) {
if test.expectedError {
if test.errString != "" {
require.ErrorContains(t, err, test.errString)
} else {
require.Error(t, err)
}
} else {
require.NoError(t, err)
}
}
_, err = tvm.vm.ParseBlock(context.Background(), parentBeaconBlock.Bytes())
errCheck(err)
err = parentBeaconBlock.Verify(context.Background())
errCheck(err)
})
}
}
func TestStandaloneDB(t *testing.T) {
vm := &VM{}
ctx := createTestConsensusContext(t)
baseDB := memdb.New()
atomicMemory := atomic.NewMemory(prefixdb.New([]byte{0}, baseDB))
// SharedMemory not part of Context structure - handle differently for testing
_ = atomicMemory.NewSharedMemory(ctx.ChainID)
sharedDB := prefixdb.New([]byte{1}, baseDB)
// Use use-standalone-database config flag to force standalone DB mode even in unit tests
// (UnitTestID normally skips standalone DB initialization)
appSender := &TestSender{T: t}
appSender.CantSendAppGossip = true
appSender.SendAppGossipF = func(context.Context, set.Set[ids.NodeID], []byte) error { return nil }
configJSON := `{"database-type": "memdb", "use-standalone-database": true}`
isDBEmpty := func(db database.Database) bool {
it := db.NewIterator()
defer it.Release()
return !it.Next()
}
// Ensure that the database is empty
require.True(t, isDBEmpty(baseDB))
err := vm.Initialize(
context.Background(),
ctx,
sharedDB,
[]byte(toGenesisJSON(forkToChainConfig["Latest"])),
nil,
[]byte(configJSON),
nil, // msgChan parameter
nil, // fxs parameter
appSender,
)
defer vm.Shutdown(context.Background())
require.NoError(t, err, "error initializing VM")
require.NoError(t, vm.SetState(context.Background(), uint32(commonEng.VMBootstrapping)))
require.NoError(t, vm.SetState(context.Background(), uint32(commonEng.VMNormalOp)))
// Issue a block
acceptedBlockEvent := make(chan core.ChainEvent, 1)
vm.blockChain.SubscribeChainAcceptedEvent(acceptedBlockEvent)
tx0 := types.NewTransaction(uint64(0), testEthAddrs[0], big.NewInt(1), 21000, big.NewInt(testMinGasPrice), nil)
signedTx0, err := types.SignTx(tx0, types.NewEIP155Signer(vm.chainConfig.ChainID), func() *ecdsa.PrivateKey {
key, err := toECDSA(testKeys[0])
if err != nil {
t.Fatal(err)
}
return key
}())
require.NoError(t, err)
// Debug: Check sender address
sender, err := types.Sender(types.NewEIP155Signer(vm.chainConfig.ChainID), signedTx0)
require.NoError(t, err)
t.Logf("Transaction sender: %s, expected: %s", sender.Hex(), testEthAddrs[0].Hex())
// Check balance using state DB
stateDB, err := vm.blockChain.State()
require.NoError(t, err)
balance := stateDB.GetBalance(sender)
t.Logf("Sender balance: %s", balance.String())
errs := vm.txPool.AddRemotesSync([]*types.Transaction{signedTx0})
require.NoError(t, errs[0])
// accept block
blk := issueAndAccept(t, vm)
newBlock := <-acceptedBlockEvent
require.Equal(t, newBlock.Block.Hash(), common.Hash(blk.ID()))
// Ensure that the shared database is empty
assert.True(t, isDBEmpty(baseDB))
// Ensure that the standalone database is not empty
assert.False(t, isDBEmpty(vm.db))
assert.False(t, isDBEmpty(vm.acceptedBlockDB))
}
func TestFeeManagerRegressionMempoolMinFeeAfterRestart(t *testing.T) {
// SKIP: This test requires VM restart functionality, but the current implementation
// closes the chainDb during shutdown, making the database unusable for restart.
// This requires architectural changes to properly handle database lifecycle in tests.
t.Skip("VM restart tests need architectural fix for database lifecycle")
// Setup chain params
genesis := &core.Genesis{}
if err := genesis.UnmarshalJSON([]byte(genesisJSONSubnetEVM)); err != nil {
t.Fatal(err)
}
precompileActivationTime := utils.NewUint64(genesis.Timestamp + 5) // 5 seconds after genesis
configExtra := params.GetExtra(genesis.Config)
configExtra.EVMTimestamp = utils.NewUint64(0)
configExtra.DurangoTimestamp = utils.NewUint64(0)
configExtra.GenesisPrecompiles = extras.Precompiles{
feemanager.ConfigKey: feemanager.NewConfig(precompileActivationTime, testEthAddrs[0:1], nil, nil, nil),
}
// set a higher fee config now
testHighFeeConfig := commontype.FeeConfig{
GasLimit: big.NewInt(8_000_000),
TargetBlockRate: 5, // in seconds
MinBaseFee: big.NewInt(50_000_000),
TargetGas: big.NewInt(18_000_000),
BaseFeeChangeDenominator: big.NewInt(3396),
MinBlockGasCost: big.NewInt(0),
MaxBlockGasCost: big.NewInt(4_000_000),
BlockGasCostStep: big.NewInt(500_000),
}
configExtra.FeeConfig = testHighFeeConfig
genesisJSON := []byte(marshalGenesisWithExtras(genesis))
tvm := newVM(t, testVMConfig{
genesisJSON: string(genesisJSON),
})
// tx pool min base fee should be the high fee config
tx := types.NewTx(&types.DynamicFeeTx{
ChainID: genesis.Config.ChainID,
Nonce: uint64(0),
To: &feemanager.ContractAddress,
Gas: 21_000,
Value: common.Big0,
GasFeeCap: big.NewInt(5_000_000), // give a lower base fee
GasTipCap: common.Big0,
Data: nil,
})
signedTx, err := types.SignTx(tx, types.LatestSigner(genesis.Config), func() *ecdsa.PrivateKey {
key, err := toECDSA(testKeys[0])
if err != nil {
t.Fatal(err)
}
return key
}())
require.NoError(t, err)
errs := tvm.vm.txPool.AddRemotesSync([]*types.Transaction{signedTx})
require.Len(t, errs, 1)
require.ErrorIs(t, errs[0], txpool.ErrUnderpriced) // should fail because mempool expects higher fee
// restart vm and try again
restartedVM, err := restartVM(tvm.vm, tvm.db, genesisJSON, tvm.appSender, true)
require.NoError(t, err)
// it still should fail
errs = restartedVM.txPool.AddRemotesSync([]*types.Transaction{signedTx})
require.Len(t, errs, 1)
require.ErrorIs(t, errs[0], txpool.ErrUnderpriced)
// send a tx to activate the precompile
newTxPoolHeadChan := make(chan core.NewTxPoolReorgEvent, 1)
restartedVM.txPool.SubscribeNewReorgEvent(newTxPoolHeadChan)
restartedVM.clock.Set(utils.Uint64ToTime(precompileActivationTime).Add(time.Second * 10))
tx = types.NewTransaction(uint64(0), testEthAddrs[0], common.Big0, 21000, big.NewInt(testHighFeeConfig.MinBaseFee.Int64()), nil)
signedTx, err = types.SignTx(tx, types.LatestSigner(genesis.Config), func() *ecdsa.PrivateKey {
key, err := toECDSA(testKeys[0])
if err != nil {
t.Fatal(err)
}
return key
}())
require.NoError(t, err)
errs = restartedVM.txPool.AddRemotesSync([]*types.Transaction{signedTx})
require.NoError(t, errs[0])
blk := issueAndAccept(t, restartedVM)
newHead := <-newTxPoolHeadChan
require.Equal(t, newHead.Head.Hash(), common.Hash(blk.ID()))
// Contract is initialized but no preconfig is given, reader should return genesis fee config
// We must query the current block header here (not genesis) because the FeeManager precompile
// is only activated at precompileActivationTime, not at genesis. Querying the genesis header would
// return the chain config fee config and lastChangedAt as zero, which is not correct after activation.
feeConfig, lastChangedAt, err := restartedVM.blockChain.GetFeeConfigAt(restartedVM.blockChain.CurrentBlock())
require.NoError(t, err)
require.EqualValues(t, feeConfig, testHighFeeConfig)
require.EqualValues(t, restartedVM.blockChain.CurrentBlock().Number, lastChangedAt)
// set a lower fee config now through feemanager
testLowFeeConfig := testHighFeeConfig
testLowFeeConfig.MinBaseFee = big.NewInt(25_000_000)
data, err := feemanager.PackSetFeeConfig(testLowFeeConfig)
require.NoError(t, err)
tx = types.NewTx(&types.DynamicFeeTx{
ChainID: genesis.Config.ChainID,
Nonce: uint64(1),
To: &feemanager.ContractAddress,
Gas: 1_000_000,
Value: common.Big0,
GasFeeCap: testHighFeeConfig.MinBaseFee, // the blockchain state still expects high fee
Data: data,
})
// let some time pass for block gas cost
restartedVM.clock.Set(restartedVM.clock.Time().Add(time.Second * 10))
signedTx, err = types.SignTx(tx, types.LatestSigner(genesis.Config), func() *ecdsa.PrivateKey {
key, err := toECDSA(testKeys[0])
if err != nil {
t.Fatal(err)
}
return key
}())
require.NoError(t, err)
errs = restartedVM.txPool.AddRemotesSync([]*types.Transaction{signedTx})
require.NoError(t, errs[0])
blk = issueAndAccept(t, restartedVM)
newHead = <-newTxPoolHeadChan
require.Equal(t, newHead.Head.Hash(), common.Hash(blk.ID()))
// check that the fee config is updated
block := getEthBlock(t, blk)
feeConfig, lastChangedAt, err = restartedVM.blockChain.GetFeeConfigAt(block.Header())
require.NoError(t, err)
require.EqualValues(t, restartedVM.blockChain.CurrentBlock().Number, lastChangedAt)
require.EqualValues(t, testLowFeeConfig, feeConfig)
// send another tx with low fee
tx = types.NewTransaction(uint64(2), testEthAddrs[0], common.Big0, 21000, big.NewInt(testLowFeeConfig.MinBaseFee.Int64()), nil)
signedTx, err = types.SignTx(tx, types.LatestSigner(genesis.Config), func() *ecdsa.PrivateKey {
key, err := toECDSA(testKeys[0])
if err != nil {
t.Fatal(err)
}
return key
}())
require.NoError(t, err)
errs = restartedVM.txPool.AddRemotesSync([]*types.Transaction{signedTx})
require.NoError(t, errs[0])
// let some time pass for block gas cost and fees to be updated
restartedVM.clock.Set(restartedVM.clock.Time().Add(time.Hour * 10))
blk = issueAndAccept(t, restartedVM)
newHead = <-newTxPoolHeadChan
require.Equal(t, newHead.Head.Hash(), common.Hash(blk.ID()))
// Regression: Mempool should see the new config after restart
restartedVM, err = restartVM(restartedVM, tvm.db, genesisJSON, tvm.appSender, true)
require.NoError(t, err)
newTxPoolHeadChan = make(chan core.NewTxPoolReorgEvent, 1)
restartedVM.txPool.SubscribeNewReorgEvent(newTxPoolHeadChan)
// send a tx with low fee
tx = types.NewTransaction(uint64(3), testEthAddrs[0], common.Big0, 21000, big.NewInt(testLowFeeConfig.MinBaseFee.Int64()), nil)
signedTx, err = types.SignTx(tx, types.LatestSigner(genesis.Config), func() *ecdsa.PrivateKey {
key, err := toECDSA(testKeys[0])
if err != nil {
t.Fatal(err)
}
return key
}())
require.NoError(t, err)
errs = restartedVM.txPool.AddRemotesSync([]*types.Transaction{signedTx})
require.NoError(t, errs[0])
blk = issueAndAccept(t, restartedVM)
newHead = <-newTxPoolHeadChan
require.Equal(t, newHead.Head.Hash(), common.Hash(blk.ID()))
}
func restartVM(vm *VM, sharedDB database.Database, genesisBytes []byte, appSender *TestSender, finishBootstrapping bool) (*VM, error) {
vm.Shutdown(context.Background())
restartedVM := &VM{}
// Note: Cannot directly modify context fields, skipping metrics reset
err := restartedVM.Initialize(context.Background(), vm.chainCtx, sharedDB, genesisBytes, nil, nil, nil, nil, appSender)
if err != nil {
return nil, err
}
if finishBootstrapping {
err = restartedVM.SetState(context.Background(), uint32(commonEng.VMBootstrapping))
if err != nil {
return nil, err
}
err = restartedVM.SetState(context.Background(), uint32(commonEng.VMNormalOp))
if err != nil {
return nil, err
}
}
return restartedVM, nil
}
func TestWaitForEvent(t *testing.T) {
for _, testCase := range []struct {
name string
testCase func(*testing.T, *VM)
}{
{
name: "WaitForEvent with context cancelled returns 0",
testCase: func(t *testing.T, vm *VM) {
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*100)
defer cancel()
var wg sync.WaitGroup
wg.Add(1)
// We run WaitForEvent in a goroutine to ensure it can be safely called concurrently.
go func() {
defer wg.Done()
msg, err := vm.WaitForEvent(ctx)
require.ErrorIs(t, err, context.DeadlineExceeded)
require.Zero(t, msg)
}()
wg.Wait()
},
},
{
name: "WaitForEvent returns when a transaction is added to the mempool",
testCase: func(t *testing.T, vm *VM) {
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
msg, err := vm.WaitForEvent(context.Background())
require.NoError(t, err)
require.Equal(t, commonEng.PendingTxs, msg)
}()
tx := types.NewTransaction(uint64(0), testEthAddrs[1], firstTxAmount, 21000, big.NewInt(testMinGasPrice), nil)
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(vm.chainConfig.ChainID), func() *ecdsa.PrivateKey {
key, err := toECDSA(testKeys[0])
if err != nil {
t.Fatal(err)
}
return key
}())
require.NoError(t, err)
for _, err := range vm.txPool.AddRemotesSync([]*types.Transaction{signedTx}) {
require.NoError(t, err)
}
wg.Wait()
},
},
{
name: "WaitForEvent doesn't return once a block is built and accepted",
testCase: func(t *testing.T, vm *VM) {
tx := types.NewTransaction(uint64(0), testEthAddrs[1], firstTxAmount, 21000, big.NewInt(testMinGasPrice), nil)
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(vm.chainConfig.ChainID), func() *ecdsa.PrivateKey {
key, err := toECDSA(testKeys[0])
if err != nil {
t.Fatal(err)
}
return key
}())
require.NoError(t, err)
for _, err := range vm.txPool.AddRemotesSync([]*types.Transaction{signedTx}) {
require.NoError(t, err)
}
blk, err := vm.BuildBlock(context.Background())
require.NoError(t, err)
require.NoError(t, blk.Verify(context.Background()))
require.NoError(t, vm.SetPreference(context.Background(), blk.ID()))
require.NoError(t, blk.Accept(context.Background()))
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*100)
defer cancel()
var wg sync.WaitGroup
wg.Add(1)
// We run WaitForEvent in a goroutine to ensure it can be safely called concurrently.
go func() {
defer wg.Done()
msg, err := vm.WaitForEvent(ctx)
require.ErrorIs(t, err, context.DeadlineExceeded)
require.Zero(t, msg)
}()
wg.Wait()
t.Log("WaitForEvent returns when regular transactions are added to the mempool")
time.Sleep(time.Second * 2) // sleep some time to let the gas capacity to refill
tx = types.NewTransaction(uint64(1), testEthAddrs[1], firstTxAmount, 21000, big.NewInt(testMinGasPrice), nil)
signedTx, err = types.SignTx(tx, types.NewEIP155Signer(vm.chainConfig.ChainID), func() *ecdsa.PrivateKey {
key, err := toECDSA(testKeys[0])
if err != nil {
t.Fatal(err)
}
return key
}())
require.NoError(t, err)
for _, err := range vm.txPool.AddRemotesSync([]*types.Transaction{signedTx}) {
require.NoError(t, err)
}
wg.Add(1)
go func() {
defer wg.Done()
msg, err := vm.WaitForEvent(context.Background())
require.NoError(t, err)
require.Equal(t, commonEng.PendingTxs, msg)
}()
wg.Wait()
// Build a block again to wipe out the subscription
blk, err = vm.BuildBlock(context.Background())
require.NoError(t, err)
require.NoError(t, blk.Verify(context.Background()))
require.NoError(t, vm.SetPreference(context.Background(), blk.ID()))
require.NoError(t, blk.Accept(context.Background()))
},
},
{
name: "WaitForEvent waits some time after a block is built",
testCase: func(t *testing.T, vm *VM) {
tx := types.NewTransaction(uint64(0), testEthAddrs[1], firstTxAmount, 21000, big.NewInt(testMinGasPrice), nil)
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(vm.chainConfig.ChainID), func() *ecdsa.PrivateKey {
key, err := toECDSA(testKeys[0])
if err != nil {
t.Fatal(err)
}
return key
}())
require.NoError(t, err)
for _, err := range vm.txPool.AddRemotesSync([]*types.Transaction{signedTx}) {
require.NoError(t, err)
}
lastBuildBlockTime := time.Now()
blk, err := vm.BuildBlock(context.Background())
require.NoError(t, err)
require.NoError(t, blk.Verify(context.Background()))
require.NoError(t, vm.SetPreference(context.Background(), blk.ID()))
require.NoError(t, blk.Accept(context.Background()))
tx = types.NewTransaction(uint64(1), testEthAddrs[1], firstTxAmount, 21000, big.NewInt(testMinGasPrice), nil)
signedTx, err = types.SignTx(tx, types.NewEIP155Signer(vm.chainConfig.ChainID), func() *ecdsa.PrivateKey {
key, err := toECDSA(testKeys[0])
if err != nil {
t.Fatal(err)
}
return key
}())
require.NoError(t, err)
for _, err := range vm.txPool.AddRemotesSync([]*types.Transaction{signedTx}) {
require.NoError(t, err)
}
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
msg, err := vm.WaitForEvent(context.Background())
require.NoError(t, err)
require.Equal(t, commonEng.PendingTxs, msg)
require.GreaterOrEqual(t, time.Since(lastBuildBlockTime), minBlockBuildingRetryDelay)
}()
wg.Wait()
},
},
} {
t.Run(testCase.name, func(t *testing.T) {
tvm := newVM(t, testVMConfig{
genesisJSON: genesisJSONSubnetEVM,
}).vm
testCase.testCase(t, tvm)
tvm.Shutdown(context.Background())
})
}
}