mirror of
https://github.com/luxfi/evmgpu.git
synced 2026-10-30 16:57:11 +00:00
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.
4420 lines
141 KiB
Plaintext
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())
|
|
})
|
|
}
|
|
}
|