Files
coreth/plugin/evm/vm_test.go
zeekay 68d712fc0c rename: Etna → Quasar Edition
- plugin/evm/vm_test.go:    upgradetest.Etna → upgradetest.Quasar
  - core/state_transition_test.go: "etna" test name → "quasar"
  - docs/content/docs/evm.mdx:    (Etna) → (Quasar Edition)
  - docs/content/docs/config.mdx: etnaBlockTimestamp → quasarBlockTimestamp,
                                  `etna` → `quasar`
  - docs/content/docs/index.mdx:  after Etna upgrade → after Quasar Edition

Bumps luxfi/upgrade + luxfi/constants to renamed deps.
RELEASES.md historical changelog left untouched.
No backcompat aliases — forwards-only.
2026-06-02 23:20:23 -07:00

4502 lines
140 KiB
Go

// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
//go:build ignore
// +build ignore
// TODO: This file needs extensive refactoring for new consensus API changes:
// - Initialize signature changed (needs toEngine channel)
// - SetState uses VMState type (uint8)
// - ListHasher interface changed (trie.NewListHasher())
// - NetworkUpgrades type changed
// - Sender struct fields changed
package evm
import (
"context"
"crypto/ecdsa"
"encoding/json"
"errors"
"fmt"
"math/big"
"os"
"path/filepath"
"strings"
"sync"
"testing"
"time"
"github.com/luxfi/coreth/plugin/evm/atomic"
atomictxpool "github.com/luxfi/coreth/plugin/evm/atomic/txpool"
atomicvm "github.com/luxfi/coreth/plugin/evm/atomic/vm"
"github.com/holiman/uint256"
consensusctx "github.com/luxfi/consensus/context"
commonEng "github.com/luxfi/consensus/core"
"github.com/luxfi/consensus/engine/enginetest"
consensustest "github.com/luxfi/consensus/test/helpers"
"github.com/luxfi/constants"
"github.com/luxfi/coreth/consensus/dummy"
"github.com/luxfi/coreth/core"
"github.com/luxfi/coreth/eth"
"github.com/luxfi/coreth/eth/filters"
"github.com/luxfi/coreth/miner"
"github.com/luxfi/coreth/params"
"github.com/luxfi/coreth/plugin/evm/config"
"github.com/luxfi/coreth/plugin/evm/customrawdb"
"github.com/luxfi/coreth/plugin/evm/customtypes"
"github.com/luxfi/coreth/plugin/evm/extension"
"github.com/luxfi/coreth/plugin/evm/header"
"github.com/luxfi/coreth/plugin/evm/upgrade/dynamicfee"
"github.com/luxfi/coreth/plugin/evm/upgrade/gaslimit_initial"
"github.com/luxfi/coreth/plugin/evm/upgrade/genesisparams"
"github.com/luxfi/coreth/plugin/evm/upgrade/lp176"
"github.com/luxfi/coreth/rpc"
"github.com/luxfi/coreth/utils"
"github.com/luxfi/coreth/utils/utilstest"
"github.com/luxfi/crypto/hash"
"github.com/luxfi/crypto/secp256k1"
"github.com/luxfi/database"
"github.com/luxfi/database/memdb"
"github.com/luxfi/database/prefixdb"
"github.com/luxfi/geth/common"
"github.com/luxfi/geth/core/rawdb"
"github.com/luxfi/geth/core/types"
"github.com/luxfi/geth/rlp"
"github.com/luxfi/geth/trie"
"github.com/luxfi/ids"
log "github.com/luxfi/log"
"github.com/luxfi/math/set"
"github.com/luxfi/upgrade"
"github.com/luxfi/upgrade/upgradetest"
lux "github.com/luxfi/utxo"
"github.com/luxfi/utxo/secp256k1fx"
"github.com/luxfi/vm/api/metrics"
"github.com/luxfi/vm/chain"
luxatomic "github.com/luxfi/vm/chains/atomic"
"github.com/luxfi/warp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var (
schemes = []string{rawdb.HashScheme, customrawdb.DatabaseScheme}
initialBaseFee = big.NewInt(dynamicfee.InitialBaseFee)
testKeys = secp256k1.TestKeys()[:3]
testEthAddrs []common.Address // testEthAddrs[i] corresponds to testKeys[i]
testShortIDAddrs []ids.ShortID
genesisJSON = func(cfg *params.ChainConfig) string {
g := new(core.Genesis)
g.Difficulty = big.NewInt(0)
g.GasLimit = 0x5f5e100
g.Timestamp = uint64(upgrade.InitiallyActiveTime.Unix())
// Use chainId: 43111, so that it does not overlap with any Lux ChainIDs, which may have their
// config overridden in vm.Initialize.
cpy := *cfg
cpy.ChainID = big.NewInt(43111)
g.Config = &cpy
allocStr := `{"0100000000000000000000000000000000000000":{"code":"0x7300000000000000000000000000000000000000003014608060405260043610603d5760003560e01c80631e010439146042578063b6510bb314606e575b600080fd5b605c60048036036020811015605657600080fd5b503560b1565b60408051918252519081900360200190f35b818015607957600080fd5b5060af60048036036080811015608e57600080fd5b506001600160a01b03813516906020810135906040810135906060013560b6565b005b30cd90565b836001600160a01b031681836108fc8690811502906040516000604051808303818888878c8acf9550505050505015801560f4573d6000803e3d6000fd5b505050505056fea26469706673582212201eebce970fe3f5cb96bf8ac6ba5f5c133fc2908ae3dcd51082cfee8f583429d064736f6c634300060a0033","balance":"0x0"}}`
json.Unmarshal([]byte(allocStr), &g.Alloc)
// After Durango, an additional account is funded in tests to use
// with warp messages.
if params.GetExtra(cfg).IsDurango(0) {
addr := common.HexToAddress("0x99b9DEA54C48Dfea6aA9A4Ca4623633EE04ddbB5")
balance := new(big.Int).Mul(big.NewInt(params.Ether), big.NewInt(10))
g.Alloc[addr] = types.Account{Balance: balance}
}
b, err := json.Marshal(g)
if err != nil {
panic(err)
}
return string(b)
}
activateCancun = func(cfg *params.ChainConfig) *params.ChainConfig {
cpy := *cfg
cpy.ShanghaiTime = utils.NewUint64(0)
cpy.CancunTime = utils.NewUint64(0)
return &cpy
}
forkToChainConfig = map[upgradetest.Fork]*params.ChainConfig{
upgradetest.NoUpgrades: params.TestLaunchConfig,
upgradetest.ApricotPhase1: params.TestChainConfig,
upgradetest.ApricotPhase2: params.TestChainConfig,
upgradetest.ApricotPhase3: params.TestChainConfig,
upgradetest.ApricotPhase4: params.TestChainConfig,
upgradetest.ApricotPhase5: params.TestChainConfig,
upgradetest.ApricotPhasePre6: params.TestChainConfig,
upgradetest.ApricotPhase6: params.TestChainConfig,
upgradetest.ApricotPhasePost6: params.TestChainConfig,
upgradetest.Banff: params.TestChainConfig,
upgradetest.Cortina: params.TestChainConfig,
upgradetest.Durango: params.TestChainConfig,
upgradetest.Quasar: params.TestChainConfig,
upgradetest.Fortuna: params.TestChainConfig,
upgradetest.Granite: params.TestChainConfig,
}
genesisJSONCancun = genesisJSON(activateCancun(params.TestChainConfig))
apricotRulesPhase0 = *params.GetRulesExtra(params.TestLaunchConfig.Rules(common.Big0, params.IsMergeTODO, 0))
apricotRulesPhase1 = *params.GetRulesExtra(params.TestChainConfig.Rules(common.Big0, params.IsMergeTODO, 0))
apricotRulesPhase2 = *params.GetRulesExtra(params.TestChainConfig.Rules(common.Big0, params.IsMergeTODO, 0))
apricotRulesPhase3 = *params.GetRulesExtra(params.TestChainConfig.Rules(common.Big0, params.IsMergeTODO, 0))
apricotRulesPhase5 = *params.GetRulesExtra(params.TestChainConfig.Rules(common.Big0, params.IsMergeTODO, 0))
apricotRulesPhase6 = *params.GetRulesExtra(params.TestChainConfig.Rules(common.Big0, params.IsMergeTODO, 0))
banffRules = *params.GetRulesExtra(params.TestChainConfig.Rules(common.Big0, params.IsMergeTODO, 0))
)
func init() {
for _, key := range testKeys {
testEthAddrs = append(testEthAddrs, key.EVMAddress())
testShortIDAddrs = append(testShortIDAddrs, key.Address())
}
}
func newPrefundedGenesis(
balance int,
addresses ...common.Address,
) *core.Genesis {
alloc := types.GenesisAlloc{}
for _, address := range addresses {
alloc[address] = types.Account{
Balance: big.NewInt(int64(balance)),
}
}
return &core.Genesis{
Config: params.TestChainConfig,
Difficulty: big.NewInt(0),
Alloc: alloc,
}
}
type testVMConfig struct {
isSyncing bool
fork *upgradetest.Fork
// If genesisJSON is empty, defaults to the genesis corresponding to the
// fork.
genesisJSON string
configJSON string
// the VM will start with UTXOs in the X-Chain Shared Memory containing
// LUX based on the map
// The UTXOIDs are generated by using a hash of the address in the map such
// that the UTXOs will be generated deterministically.
utxos map[ids.ShortID]uint64
}
type testVM struct {
t *testing.T
atomicVM *atomicvm.VM
vm *VM
db *prefixdb.Database
atomicMemory *luxatomic.Memory
appSender *enginetest.Sender
}
func newVM(t *testing.T, config testVMConfig) *testVM {
ctx := consensustest.Context(t, consensustest.CChainID)
fork := upgradetest.Latest
if config.fork != nil {
fork = *config.fork
}
ctx.NetworkUpgrades = upgradetest.GetConfig(fork)
if len(config.genesisJSON) == 0 {
config.genesisJSON = genesisJSON(forkToChainConfig[fork])
}
baseDB := memdb.New()
// initialize the atomic memory
atomicMemory := luxatomic.NewMemory(prefixdb.New([]byte{0}, baseDB))
ctx.SharedMemory = atomicMemory.NewSharedMemory(ctx.ChainID)
// NB: this lock is intentionally left locked when this function returns.
// The caller of this function is responsible for unlocking.
ctx.Lock.Lock()
prefixedDB := prefixdb.New([]byte{1}, baseDB)
innerVM := &VM{}
atomicVM := atomicvm.WrapVM(innerVM)
appSender := &enginetest.Sender{
SendGossipF: func(context.Context, warp.SendConfig, []byte) error { return nil },
}
require.NoError(t, atomicVM.Initialize(
context.Background(),
ctx,
prefixedDB,
[]byte(config.genesisJSON),
nil,
[]byte(config.configJSON),
nil,
appSender,
), "error initializing vm")
if !config.isSyncing {
require.NoError(t, atomicVM.SetState(context.Background(), commonEng.VMBootstrapping))
require.NoError(t, atomicVM.SetState(context.Background(), commonEng.VMNormalOp))
}
for addr, luxAmount := range config.utxos {
txID, err := ids.ToID(hash.ComputeHash256(addr.Bytes()))
if err != nil {
t.Fatalf("Failed to generate txID from addr: %s", err)
}
if _, err := addUTXO(atomicMemory, innerVM.ctx, txID, 0, innerVM.ctx.UTXOAssetID, luxAmount, addr); err != nil {
t.Fatalf("Failed to add UTXO to shared memory: %s", err)
}
}
return &testVM{
t: t,
atomicVM: atomicVM,
vm: innerVM,
db: prefixedDB,
atomicMemory: atomicMemory,
appSender: appSender,
}
}
// Database cannot yet be run with an empty config.
func getConfig(scheme, otherConfig string) string {
innerConfig := otherConfig
if scheme == customrawdb.DatabaseScheme {
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.DatabaseScheme)
}
return fmt.Sprintf(`{%s}`, innerConfig)
}
func (vm *testVM) WaitForEvent(ctx context.Context) commonEng.Message {
result, err := vm.vm.WaitForEvent(ctx)
require.NoError(vm.t, err)
msg, ok := result.(commonEng.Message)
require.True(vm.t, ok, "expected Message type from WaitForEvent")
return msg
}
// setupGenesis sets up the genesis
func setupGenesis(
t *testing.T,
fork upgradetest.Fork,
) (*consensusctx.Context,
*prefixdb.Database,
[]byte,
*luxatomic.Memory,
) {
ctx := consensustest.Context(t, consensustest.CChainID)
ctx.NetworkUpgrades = upgradetest.GetConfig(fork)
baseDB := memdb.New()
// initialize the atomic memory
atomicMemory := luxatomic.NewMemory(prefixdb.New([]byte{0}, baseDB))
ctx.SharedMemory = atomicMemory.NewSharedMemory(ctx.ChainID)
// NB: this lock is intentionally left locked when this function returns.
// The caller of this function is responsible for unlocking.
ctx.Lock.Lock()
prefixedDB := prefixdb.New([]byte{1}, baseDB)
genesisJSON := genesisJSON(forkToChainConfig[fork])
return ctx, prefixedDB, []byte(genesisJSON), atomicMemory
}
func addUTXO(sharedMemory *luxatomic.Memory, ctx *consensusctx.Context, txID ids.ID, index uint32, assetID ids.ID, amount uint64, addr ids.ShortID) (*lux.UTXO, error) {
utxo := &lux.UTXO{
UTXOID: lux.UTXOID{
TxID: txID,
OutputIndex: index,
},
Asset: lux.Asset{ID: assetID},
Out: &secp256k1fx.TransferOutput{
Amt: amount,
OutputOwners: secp256k1fx.OutputOwners{
Threshold: 1,
Addrs: []ids.ShortID{addr},
},
},
}
utxoBytes, err := atomic.Codec.Marshal(atomic.CodecVersion, utxo)
if err != nil {
return nil, err
}
xChainSharedMemory := sharedMemory.NewSharedMemory(ctx.XChainID)
inputID := utxo.InputID()
if err := xChainSharedMemory.Apply(map[ids.ID]*luxatomic.Requests{ctx.ChainID: {PutRequests: []*luxatomic.Element{{
Key: inputID[:],
Value: utxoBytes,
Traits: [][]byte{
addr.Bytes(),
},
}}}}); err != nil {
return nil, err
}
return utxo, nil
}
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": [%q]}`, txFeeCap, 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": [%q]}`, txFeeCap, 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 {
fork upgradetest.Fork
expectedGasPrice *big.Int
}{
{
fork: upgradetest.ApricotPhase3,
expectedGasPrice: big.NewInt(0),
},
{
fork: upgradetest.ApricotPhase4,
expectedGasPrice: big.NewInt(0),
},
{
fork: upgradetest.ApricotPhase5,
expectedGasPrice: big.NewInt(0),
},
{
fork: upgradetest.ApricotPhasePre6,
expectedGasPrice: big.NewInt(0),
},
{
fork: upgradetest.ApricotPhase6,
expectedGasPrice: big.NewInt(0),
},
{
fork: upgradetest.ApricotPhasePost6,
expectedGasPrice: big.NewInt(0),
},
{
fork: upgradetest.Banff,
expectedGasPrice: big.NewInt(0),
},
{
fork: upgradetest.Cortina,
expectedGasPrice: big.NewInt(0),
},
{
fork: upgradetest.Durango,
expectedGasPrice: big.NewInt(0),
},
}
for _, test := range genesisTests {
t.Run(test.fork.String(), func(t *testing.T) {
require := require.New(t)
vm := newVM(t, testVMConfig{
fork: &test.fork,
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 TestImportMissingUTXOs(t *testing.T) {
for _, scheme := range schemes {
t.Run(scheme, func(t *testing.T) {
testImportMissingUTXOs(t, scheme)
})
}
}
func testImportMissingUTXOs(t *testing.T, scheme string) {
// make a VM with a shared memory that has an importable UTXO to build a block
importAmount := uint64(50000000)
fork := upgradetest.ApricotPhase2
tvm1 := newVM(t, testVMConfig{
fork: &fork,
utxos: map[ids.ShortID]uint64{
testShortIDAddrs[0]: importAmount,
},
configJSON: getConfig(scheme, ""),
})
defer func() {
require.NoError(t, tvm1.vm.Shutdown(context.Background()))
}()
importTx, err := tvm1.atomicVM.NewImportTx(tvm1.vm.ctx.XChainID, testEthAddrs[0], initialBaseFee, []*secp256k1.PrivateKey{testKeys[0]})
require.NoError(t, err)
require.NoError(t, tvm1.atomicVM.AtomicMempool.AddLocalTx(importTx))
require.Equal(t, commonEng.PendingTxs, tvm1.WaitForEvent(context.Background()))
blk, err := tvm1.vm.BuildBlock(context.Background())
require.NoError(t, err)
// make another VM which is missing the UTXO in shared memory
vm2 := newVM(t, testVMConfig{
fork: &fork,
configJSON: getConfig(scheme, ""),
}).vm
defer func() {
require.NoError(t, vm2.Shutdown(context.Background()))
}()
vm2Blk, err := vm2.ParseBlock(context.Background(), blk.Bytes())
require.NoError(t, err)
err = vm2Blk.Verify(context.Background())
require.ErrorIs(t, err, atomicvm.ErrMissingUTXOs)
// This should not result in a bad block since the missing UTXO should
// prevent InsertBlockManual from being called.
badBlocks, _ := vm2.blockChain.BadBlocks()
require.Len(t, badBlocks, 0)
}
// Simple test to ensure we can issue an import transaction followed by an export transaction
// and they will be indexed correctly when accepted.
func TestIssueAtomicTxs(t *testing.T) {
for _, scheme := range schemes {
t.Run(scheme, func(t *testing.T) {
testIssueAtomicTxs(t, scheme)
})
}
}
func testIssueAtomicTxs(t *testing.T, scheme string) {
importAmount := uint64(50000000)
fork := upgradetest.ApricotPhase2
tvm := newVM(t, testVMConfig{
fork: &fork,
utxos: map[ids.ShortID]uint64{
testShortIDAddrs[0]: importAmount,
},
configJSON: getConfig(scheme, ""),
})
defer func() {
if err := tvm.vm.Shutdown(context.Background()); err != nil {
t.Fatal(err)
}
}()
importTx, err := tvm.atomicVM.NewImportTx(tvm.vm.ctx.XChainID, testEthAddrs[0], initialBaseFee, []*secp256k1.PrivateKey{testKeys[0]})
if err != nil {
t.Fatal(err)
}
if err := tvm.atomicVM.AtomicMempool.AddLocalTx(importTx); err != nil {
t.Fatal(err)
}
require.Equal(t, commonEng.PendingTxs, tvm.WaitForEvent(context.Background()))
blk, err := tvm.vm.BuildBlock(context.Background())
if err != nil {
t.Fatal(err)
}
if err := blk.Verify(context.Background()); err != nil {
t.Fatal(err)
}
if err := tvm.vm.SetPreference(context.Background(), blk.ID()); err != nil {
t.Fatal(err)
}
if err := blk.Accept(context.Background()); err != nil {
t.Fatal(err)
}
if lastAcceptedID, err := tvm.vm.LastAccepted(context.Background()); err != nil {
t.Fatal(err)
} else if lastAcceptedID != blk.ID() {
t.Fatalf("Expected last accepted blockID to be the accepted block: %s, but found %s", blk.ID(), lastAcceptedID)
}
tvm.vm.blockChain.DrainAcceptorQueue()
filterAPI := filters.NewFilterAPI(filters.NewFilterSystem(tvm.vm.eth.APIBackend, filters.Config{
Timeout: 5 * time.Minute,
}))
blockHash := common.Hash(blk.ID())
logs, err := filterAPI.GetLogs(context.Background(), filters.FilterCriteria{
BlockHash: &blockHash,
})
if err != nil {
t.Fatal(err)
}
if len(logs) != 0 {
t.Fatalf("Expected log length to be 0, but found %d", len(logs))
}
if logs == nil {
t.Fatal("Expected logs to be non-nil")
}
exportTx, err := tvm.atomicVM.NewExportTx(tvm.vm.ctx.UTXOAssetID, importAmount-(2*genesisparams.AtomicTxFee), tvm.vm.ctx.XChainID, testShortIDAddrs[0], initialBaseFee, []*secp256k1.PrivateKey{testKeys[0]})
if err != nil {
t.Fatal(err)
}
if err := tvm.atomicVM.AtomicMempool.AddLocalTx(exportTx); err != nil {
t.Fatal(err)
}
require.Equal(t, commonEng.PendingTxs, tvm.WaitForEvent(context.Background()))
blk2, err := tvm.vm.BuildBlock(context.Background())
if err != nil {
t.Fatal(err)
}
if err := blk2.Verify(context.Background()); err != nil {
t.Fatal(err)
}
if err := blk2.Accept(context.Background()); err != nil {
t.Fatal(err)
}
if lastAcceptedID, err := tvm.vm.LastAccepted(context.Background()); err != nil {
t.Fatal(err)
} else if lastAcceptedID != blk2.ID() {
t.Fatalf("Expected last accepted blockID to be the accepted block: %s, but found %s", blk2.ID(), lastAcceptedID)
}
// Check that both atomic transactions were indexed as expected.
indexedImportTx, status, height, err := tvm.atomicVM.GetAtomicTx(importTx.ID())
assert.NoError(t, err)
assert.Equal(t, atomic.Accepted, status)
assert.Equal(t, uint64(1), height, "expected height of indexed import tx to be 1")
assert.Equal(t, indexedImportTx.ID(), importTx.ID(), "expected ID of indexed import tx to match original txID")
indexedExportTx, status, height, err := tvm.atomicVM.GetAtomicTx(exportTx.ID())
assert.NoError(t, err)
assert.Equal(t, atomic.Accepted, status)
assert.Equal(t, uint64(2), height, "expected height of indexed export tx to be 2")
assert.Equal(t, indexedExportTx.ID(), exportTx.ID(), "expected ID of indexed import tx to match original txID")
}
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) {
importAmount := uint64(20000000)
fork := upgradetest.ApricotPhase2
tvm := newVM(t, testVMConfig{
fork: &fork,
configJSON: getConfig(scheme, `"pruning-enabled":true`),
utxos: map[ids.ShortID]uint64{
testShortIDAddrs[0]: importAmount,
},
})
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)
importTx, err := tvm.atomicVM.NewImportTx(tvm.vm.ctx.XChainID, testEthAddrs[0], initialBaseFee, []*secp256k1.PrivateKey{testKeys[0]})
if err != nil {
t.Fatal(err)
}
if err := tvm.atomicVM.AtomicMempool.AddLocalTx(importTx); err != nil {
t.Fatal(err)
}
require.Equal(t, commonEng.PendingTxs, tvm.WaitForEvent(context.Background()))
blk1, err := tvm.vm.BuildBlock(context.Background())
if err != nil {
t.Fatal(err)
}
if err := blk1.Verify(context.Background()); err != nil {
t.Fatal(err)
}
if err := tvm.vm.SetPreference(context.Background(), blk1.ID()); err != nil {
t.Fatal(err)
}
if err := blk1.Accept(context.Background()); err != nil {
t.Fatal(err)
}
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), testEthAddrs[0], big.NewInt(10), 21000, big.NewInt(genesisparams.MinGasPrice), nil)
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(tvm.vm.chainID), testKeys[0].ToECDSA())
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)
}
}
require.Equal(t, commonEng.PendingTxs, tvm.WaitForEvent(context.Background()))
blk2, err := tvm.vm.BuildBlock(context.Background())
if err != nil {
t.Fatal(err)
}
if err := blk2.Verify(context.Background()); err != nil {
t.Fatal(err)
}
if err := blk2.Accept(context.Background()); err != nil {
t.Fatal(err)
}
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 := blk1.(*chain.BlockWrapper).Block.(*wrappedBlock).ethBlock
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.State.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 := atomicvm.WrapVM(&VM{})
newCTX := consensustest.Context(t, consensustest.CChainID)
newCTX.NetworkUpgrades = upgradetest.GetConfig(fork)
newCTX.ChainDataDir = tvm.vm.ctx.ChainDataDir
if err := restartedVM.Initialize(
context.Background(),
newCTX,
tvm.db,
[]byte(genesisJSON(forkToChainConfig[fork])),
[]byte(""),
[]byte(getConfig(scheme, `"pruning-enabled":true`)),
nil, // toEngine channel
[]*commonEng.Fx{},
nil, // appSender
); 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 := blk2.(*chain.BlockWrapper).Block.(*wrappedBlock).ethBlock
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)")
}
}
func testConflictingImportTxs(t *testing.T, fork upgradetest.Fork, scheme string) {
importAmount := uint64(10000000)
tvm := newVM(t, testVMConfig{
fork: &fork,
utxos: map[ids.ShortID]uint64{
testShortIDAddrs[0]: importAmount,
testShortIDAddrs[1]: importAmount,
testShortIDAddrs[2]: importAmount,
},
configJSON: getConfig(scheme, ""),
})
defer func() {
if err := tvm.vm.Shutdown(context.Background()); err != nil {
t.Fatal(err)
}
}()
importTxs := make([]*atomic.Tx, 0, 3)
conflictTxs := make([]*atomic.Tx, 0, 3)
for i, key := range testKeys {
importTx, err := tvm.atomicVM.NewImportTx(tvm.vm.ctx.XChainID, testEthAddrs[i], initialBaseFee, []*secp256k1.PrivateKey{key})
if err != nil {
t.Fatal(err)
}
importTxs = append(importTxs, importTx)
conflictAddr := testEthAddrs[(i+1)%len(testEthAddrs)]
conflictTx, err := tvm.atomicVM.NewImportTx(tvm.vm.ctx.XChainID, conflictAddr, initialBaseFee, []*secp256k1.PrivateKey{key})
if err != nil {
t.Fatal(err)
}
conflictTxs = append(conflictTxs, conflictTx)
}
expectedParentBlkID, err := tvm.vm.LastAccepted(context.Background())
if err != nil {
t.Fatal(err)
}
for _, tx := range importTxs[:2] {
if err := tvm.atomicVM.AtomicMempool.AddLocalTx(tx); err != nil {
t.Fatal(err)
}
require.Equal(t, commonEng.PendingTxs, tvm.WaitForEvent(context.Background()))
tvm.vm.clock.Set(tvm.vm.clock.Time().Add(2 * time.Second))
blk, err := tvm.vm.BuildBlock(context.Background())
if err != nil {
t.Fatal(err)
}
if err := blk.Verify(context.Background()); err != nil {
t.Fatal(err)
}
if parentID := blk.Parent(); parentID != expectedParentBlkID {
t.Fatalf("Expected parent to have blockID %s, but found %s", expectedParentBlkID, parentID)
}
expectedParentBlkID = blk.ID()
if err := tvm.vm.SetPreference(context.Background(), blk.ID()); err != nil {
t.Fatal(err)
}
}
// Check that for each conflict tx (whose conflict is in the chain ancestry)
// the VM returns an error when it attempts to issue the conflict into the mempool
// and when it attempts to build a block with the conflict force added to the mempool.
for i, tx := range conflictTxs[:2] {
if err := tvm.atomicVM.AtomicMempool.AddLocalTx(tx); err == nil {
t.Fatal("Expected issueTx to fail due to conflicting transaction")
}
// Force issue transaction directly to the mempool
if err := tvm.atomicVM.AtomicMempool.ForceAddTx(tx); err != nil {
t.Fatal(err)
}
require.Equal(t, commonEng.PendingTxs, tvm.WaitForEvent(context.Background()))
tvm.vm.clock.Set(tvm.vm.clock.Time().Add(2 * time.Second))
_, err = tvm.vm.BuildBlock(context.Background())
// The new block is verified in BuildBlock, so
// BuildBlock should fail due to an attempt to
// double spend an atomic UTXO.
if err == nil {
t.Fatalf("Block verification should have failed in BuildBlock %d due to double spending atomic UTXO", i)
}
}
// Generate one more valid block so that we can copy the header to create an invalid block
// with modified extra data. This new block will be invalid for more than one reason (invalid merkle root)
// so we check to make sure that the expected error is returned from block verification.
if err := tvm.atomicVM.AtomicMempool.AddLocalTx(importTxs[2]); err != nil {
t.Fatal(err)
}
require.Equal(t, commonEng.PendingTxs, tvm.WaitForEvent(context.Background()))
tvm.vm.clock.Set(tvm.vm.clock.Time().Add(2 * time.Second))
validBlock, err := tvm.vm.BuildBlock(context.Background())
if err != nil {
t.Fatal(err)
}
if err := validBlock.Verify(context.Background()); err != nil {
t.Fatal(err)
}
validEthBlock := validBlock.(*chain.BlockWrapper).Block.(extension.ExtendedBlock).GetEthBlock()
rules := tvm.vm.currentRules()
var extraData []byte
switch {
case rules.IsApricotPhase5:
extraData, err = atomic.Codec.Marshal(atomic.CodecVersion, []*atomic.Tx{conflictTxs[1]})
default:
extraData, err = atomic.Codec.Marshal(atomic.CodecVersion, conflictTxs[1])
}
if err != nil {
t.Fatal(err)
}
conflictingAtomicTxBlock := customtypes.NewBlockWithExtData(
types.CopyHeader(validEthBlock.Header()),
nil,
nil,
nil,
trie.NewListHasher(),
extraData,
true,
)
blockBytes, err := rlp.EncodeToBytes(conflictingAtomicTxBlock)
if err != nil {
t.Fatal(err)
}
parsedBlock, err := tvm.vm.ParseBlock(context.Background(), blockBytes)
if err != nil {
t.Fatal(err)
}
if err := parsedBlock.Verify(context.Background()); !errors.Is(err, atomicvm.ErrConflictingAtomicInputs) {
t.Fatalf("Expected to fail with err: %s, but found err: %s", atomicvm.ErrConflictingAtomicInputs, err)
}
if !rules.IsApricotPhase5 {
return
}
extraData, err = atomic.Codec.Marshal(atomic.CodecVersion, []*atomic.Tx{importTxs[2], conflictTxs[2]})
if err != nil {
t.Fatal(err)
}
header := types.CopyHeader(validEthBlock.Header())
headerExtra := customtypes.GetHeaderExtra(header)
headerExtra.ExtDataGasUsed.Mul(common.Big2, headerExtra.ExtDataGasUsed)
internalConflictBlock := customtypes.NewBlockWithExtData(
header,
nil,
nil,
nil,
new(trie.Trie),
extraData,
true,
)
blockBytes, err = rlp.EncodeToBytes(internalConflictBlock)
if err != nil {
t.Fatal(err)
}
parsedBlock, err = tvm.vm.ParseBlock(context.Background(), blockBytes)
if err != nil {
t.Fatal(err)
}
if err := parsedBlock.Verify(context.Background()); !errors.Is(err, atomicvm.ErrConflictingAtomicInputs) {
t.Fatalf("Expected to fail with err: %s, but found err: %s", atomicvm.ErrConflictingAtomicInputs, err)
}
}
func TestReissueAtomicTxHigherGasPrice(t *testing.T) {
for _, scheme := range schemes {
t.Run(scheme, func(t *testing.T) {
testReissueAtomicTxHigherGasPrice(t, scheme)
})
}
}
func testReissueAtomicTxHigherGasPrice(t *testing.T, scheme string) {
kc := secp256k1fx.NewKeychain(testKeys...)
tests := map[string]func(t *testing.T, vm *atomicvm.VM, sharedMemory *luxatomic.Memory) (issued []*atomic.Tx, discarded []*atomic.Tx){
"single UTXO override": func(t *testing.T, vm *atomicvm.VM, sharedMemory *luxatomic.Memory) (issued []*atomic.Tx, evicted []*atomic.Tx) {
utxo, err := addUTXO(sharedMemory, vm.Ctx, ids.GenerateTestID(), 0, vm.Ctx.UTXOAssetID, constants.Lux, testShortIDAddrs[0])
if err != nil {
t.Fatal(err)
}
tx1, err := atomic.NewImportTx(vm.Ctx, vm.CurrentRules(), vm.Clock().Unix(), vm.Ctx.XChainID, testEthAddrs[0], initialBaseFee, kc, []*lux.UTXO{utxo})
if err != nil {
t.Fatal(err)
}
tx2, err := atomic.NewImportTx(vm.Ctx, vm.CurrentRules(), vm.Clock().Unix(), vm.Ctx.XChainID, testEthAddrs[0], new(big.Int).Mul(common.Big2, initialBaseFee), kc, []*lux.UTXO{utxo})
if err != nil {
t.Fatal(err)
}
if err := vm.AtomicMempool.AddLocalTx(tx1); err != nil {
t.Fatal(err)
}
if err := vm.AtomicMempool.AddLocalTx(tx2); err != nil {
t.Fatal(err)
}
return []*atomic.Tx{tx2}, []*atomic.Tx{tx1}
},
"one of two UTXOs overrides": func(t *testing.T, vm *atomicvm.VM, sharedMemory *luxatomic.Memory) (issued []*atomic.Tx, evicted []*atomic.Tx) {
utxo1, err := addUTXO(sharedMemory, vm.Ctx, ids.GenerateTestID(), 0, vm.Ctx.UTXOAssetID, constants.Lux, testShortIDAddrs[0])
if err != nil {
t.Fatal(err)
}
utxo2, err := addUTXO(sharedMemory, vm.Ctx, ids.GenerateTestID(), 0, vm.Ctx.UTXOAssetID, constants.Lux, testShortIDAddrs[0])
if err != nil {
t.Fatal(err)
}
tx1, err := atomic.NewImportTx(vm.Ctx, vm.CurrentRules(), vm.Clock().Unix(), vm.Ctx.XChainID, testEthAddrs[0], initialBaseFee, kc, []*lux.UTXO{utxo1, utxo2})
if err != nil {
t.Fatal(err)
}
tx2, err := atomic.NewImportTx(vm.Ctx, vm.CurrentRules(), vm.Clock().Unix(), vm.Ctx.XChainID, testEthAddrs[0], new(big.Int).Mul(common.Big2, initialBaseFee), kc, []*lux.UTXO{utxo1})
if err != nil {
t.Fatal(err)
}
if err := vm.AtomicMempool.AddLocalTx(tx1); err != nil {
t.Fatal(err)
}
if err := vm.AtomicMempool.AddLocalTx(tx2); err != nil {
t.Fatal(err)
}
return []*atomic.Tx{tx2}, []*atomic.Tx{tx1}
},
"hola": func(t *testing.T, vm *atomicvm.VM, sharedMemory *luxatomic.Memory) (issued []*atomic.Tx, evicted []*atomic.Tx) {
utxo1, err := addUTXO(sharedMemory, vm.Ctx, ids.GenerateTestID(), 0, vm.Ctx.UTXOAssetID, constants.Lux, testShortIDAddrs[0])
if err != nil {
t.Fatal(err)
}
utxo2, err := addUTXO(sharedMemory, vm.Ctx, ids.GenerateTestID(), 0, vm.Ctx.UTXOAssetID, constants.Lux, testShortIDAddrs[0])
if err != nil {
t.Fatal(err)
}
importTx1, err := atomic.NewImportTx(vm.Ctx, vm.CurrentRules(), vm.Clock().Unix(), vm.Ctx.XChainID, testEthAddrs[0], initialBaseFee, kc, []*lux.UTXO{utxo1})
if err != nil {
t.Fatal(err)
}
importTx2, err := atomic.NewImportTx(vm.Ctx, vm.CurrentRules(), vm.Clock().Unix(), vm.Ctx.XChainID, testEthAddrs[0], new(big.Int).Mul(big.NewInt(3), initialBaseFee), kc, []*lux.UTXO{utxo2})
if err != nil {
t.Fatal(err)
}
reissuanceTx1, err := atomic.NewImportTx(vm.Ctx, vm.CurrentRules(), vm.Clock().Unix(), vm.Ctx.XChainID, testEthAddrs[0], new(big.Int).Mul(big.NewInt(2), initialBaseFee), kc, []*lux.UTXO{utxo1, utxo2})
if err != nil {
t.Fatal(err)
}
if err := vm.AtomicMempool.AddLocalTx(importTx1); err != nil {
t.Fatal(err)
}
if err := vm.AtomicMempool.AddLocalTx(importTx2); err != nil {
t.Fatal(err)
}
if err := vm.AtomicMempool.AddLocalTx(reissuanceTx1); !errors.Is(err, atomictxpool.ErrConflict) {
t.Fatalf("Expected to fail with err: %s, but found err: %s", atomictxpool.ErrConflict, err)
}
assert.True(t, vm.AtomicMempool.Has(importTx1.ID()))
assert.True(t, vm.AtomicMempool.Has(importTx2.ID()))
assert.False(t, vm.AtomicMempool.Has(reissuanceTx1.ID()))
reissuanceTx2, err := atomic.NewImportTx(vm.Ctx, vm.CurrentRules(), vm.Clock().Unix(), vm.Ctx.XChainID, testEthAddrs[0], new(big.Int).Mul(big.NewInt(4), initialBaseFee), kc, []*lux.UTXO{utxo1, utxo2})
if err != nil {
t.Fatal(err)
}
if err := vm.AtomicMempool.AddLocalTx(reissuanceTx2); err != nil {
t.Fatal(err)
}
return []*atomic.Tx{reissuanceTx2}, []*atomic.Tx{importTx1, importTx2}
},
}
for name, issueTxs := range tests {
t.Run(name, func(t *testing.T) {
fork := upgradetest.ApricotPhase5
tvm := newVM(t, testVMConfig{
fork: &fork,
configJSON: getConfig(scheme, `"pruning-enabled":true`),
})
issuedTxs, evictedTxs := issueTxs(t, tvm.atomicVM, tvm.atomicMemory)
for i, tx := range issuedTxs {
_, issued := tvm.atomicVM.AtomicMempool.GetPendingTx(tx.ID())
assert.True(t, issued, "expected issued tx at index %d to be issued", i)
}
for i, tx := range evictedTxs {
_, discarded, _ := tvm.atomicVM.AtomicMempool.GetTx(tx.ID())
assert.True(t, discarded, "expected discarded tx at index %d to be discarded", i)
}
})
}
}
func TestConflictingImportTxsAcrossBlocks(t *testing.T) {
for _, fork := range []upgradetest.Fork{
upgradetest.ApricotPhase1,
upgradetest.ApricotPhase2,
upgradetest.ApricotPhase3,
upgradetest.ApricotPhase4,
upgradetest.ApricotPhase5,
} {
t.Run(fork.String(), func(t *testing.T) {
for _, scheme := range schemes {
t.Run(scheme, func(t *testing.T) {
testConflictingImportTxs(t, fork, scheme)
})
}
})
}
}
// 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
importAmount := uint64(1000000000)
fork := upgradetest.NoUpgrades
tvmConfig := testVMConfig{
fork: &fork,
configJSON: getConfig(scheme, `"pruning-enabled":true`),
utxos: map[ids.ShortID]uint64{
testShortIDAddrs[0]: importAmount,
},
}
tvm1 := newVM(t, tvmConfig)
tvm2 := newVM(t, tvmConfig)
defer func() {
if err := tvm1.vm.Shutdown(context.Background()); err != nil {
t.Fatal(err)
}
if err := tvm2.vm.Shutdown(context.Background()); err != nil {
t.Fatal(err)
}
}()
newTxPoolHeadChan1 := make(chan core.NewTxPoolReorgEvent, 1)
tvm1.vm.txPool.SubscribeNewReorgEvent(newTxPoolHeadChan1)
newTxPoolHeadChan2 := make(chan core.NewTxPoolReorgEvent, 1)
tvm2.vm.txPool.SubscribeNewReorgEvent(newTxPoolHeadChan2)
importTx, err := tvm1.atomicVM.NewImportTx(tvm1.vm.ctx.XChainID, testEthAddrs[1], initialBaseFee, []*secp256k1.PrivateKey{testKeys[0]})
if err != nil {
t.Fatal(err)
}
if err := tvm1.atomicVM.AtomicMempool.AddLocalTx(importTx); err != nil {
t.Fatal(err)
}
require.Equal(t, commonEng.PendingTxs, tvm1.WaitForEvent(context.Background()))
vm1BlkA, err := tvm1.vm.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 := tvm1.vm.SetPreference(context.Background(), vm1BlkA.ID()); err != nil {
t.Fatal(err)
}
vm2BlkA, err := tvm2.vm.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 := tvm2.vm.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[1], big.NewInt(10), 21000, big.NewInt(genesisparams.MinGasPrice), nil)
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(tvm1.vm.chainID), testKeys[1].ToECDSA())
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 = tvm1.vm.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)
}
}
require.Equal(t, commonEng.PendingTxs, tvm1.WaitForEvent(context.Background()))
vm1BlkB, err := tvm1.vm.BuildBlock(context.Background())
if err != nil {
t.Fatal(err)
}
if err := vm1BlkB.Verify(context.Background()); err != nil {
t.Fatal(err)
}
if err := tvm1.vm.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 = tvm2.vm.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)
}
}
require.Equal(t, commonEng.PendingTxs, tvm2.WaitForEvent(context.Background()))
vm2BlkC, err := tvm2.vm.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 := tvm2.vm.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 = tvm2.vm.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)
}
}
require.Equal(t, commonEng.PendingTxs, tvm2.WaitForEvent(context.Background()))
vm2BlkD, err := tvm2.vm.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 := tvm2.vm.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 := tvm1.vm.ParseBlock(context.Background(), vm2BlkD.Bytes())
if err != nil {
t.Fatalf("VM1 errored parsing blkD: %s", err)
}
vm1BlkC, err := tvm1.vm.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 := tvm1.vm.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 := tvm2.vm.blockChain.ValidateCanonicalChain(); err != nil {
t.Fatalf("VM2 failed canonical chain verification due to: %s", err)
}
if err := tvm1.vm.blockChain.ValidateCanonicalChain(); err != nil {
t.Fatalf("VM1 failed canonical chain verification due to: %s", err)
}
}
func TestConflictingTransitiveAncestryWithGap(t *testing.T) {
for _, scheme := range schemes {
t.Run(scheme, func(t *testing.T) {
testConflictingTransitiveAncestryWithGap(t, scheme)
})
}
}
func testConflictingTransitiveAncestryWithGap(t *testing.T, scheme string) {
key := utilstest.NewKey(t)
key0 := testKeys[0]
addr0 := key0.Address()
key1 := testKeys[1]
addr1 := key1.Address()
importAmount := uint64(1000000000)
fork := upgradetest.NoUpgrades
tvm := newVM(t, testVMConfig{
fork: &fork,
utxos: map[ids.ShortID]uint64{
addr0: importAmount,
addr1: importAmount,
},
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)
importTx0A, err := tvm.atomicVM.NewImportTx(tvm.vm.ctx.XChainID, key.Address, initialBaseFee, []*secp256k1.PrivateKey{key0})
if err != nil {
t.Fatal(err)
}
// Create a conflicting transaction
importTx0B, err := tvm.atomicVM.NewImportTx(tvm.vm.ctx.XChainID, testEthAddrs[2], initialBaseFee, []*secp256k1.PrivateKey{key0})
if err != nil {
t.Fatal(err)
}
if err := tvm.atomicVM.AtomicMempool.AddLocalTx(importTx0A); err != nil {
t.Fatalf("Failed to issue importTx0A: %s", err)
}
require.Equal(t, commonEng.PendingTxs, tvm.WaitForEvent(context.Background()))
blk0, err := tvm.vm.BuildBlock(context.Background())
if err != nil {
t.Fatalf("Failed to build block with import transaction: %s", err)
}
if err := blk0.Verify(context.Background()); err != nil {
t.Fatalf("Block failed verification: %s", err)
}
if err := tvm.vm.SetPreference(context.Background(), blk0.ID()); err != nil {
t.Fatal(err)
}
newHead := <-newTxPoolHeadChan
if newHead.Head.Hash() != common.Hash(blk0.ID()) {
t.Fatalf("Expected new block to match")
}
tx := types.NewTransaction(0, key.Address, big.NewInt(10), 21000, big.NewInt(genesisparams.MinGasPrice), nil)
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(tvm.vm.chainID), key.PrivateKey)
if err != nil {
t.Fatal(err)
}
// Add the remote transactions, build the block, and set VM1's preference for block A
errs := tvm.vm.txPool.AddRemotesSync([]*types.Transaction{signedTx})
for i, err := range errs {
if err != nil {
t.Fatalf("Failed to add transaction to VM1 at index %d: %s", i, err)
}
}
require.Equal(t, commonEng.PendingTxs, tvm.WaitForEvent(context.Background()))
blk1, err := tvm.vm.BuildBlock(context.Background())
if err != nil {
t.Fatalf("Failed to build blk1: %s", err)
}
if err := blk1.Verify(context.Background()); err != nil {
t.Fatalf("blk1 failed verification due to %s", err)
}
if err := tvm.vm.SetPreference(context.Background(), blk1.ID()); err != nil {
t.Fatal(err)
}
importTx1, err := tvm.atomicVM.NewImportTx(tvm.vm.ctx.XChainID, key.Address, initialBaseFee, []*secp256k1.PrivateKey{key1})
if err != nil {
t.Fatalf("Failed to issue importTx1 due to: %s", err)
}
if err := tvm.atomicVM.AtomicMempool.AddLocalTx(importTx1); err != nil {
t.Fatal(err)
}
require.Equal(t, commonEng.PendingTxs, tvm.WaitForEvent(context.Background()))
blk2, err := tvm.vm.BuildBlock(context.Background())
if err != nil {
t.Fatalf("Failed to build block with import transaction: %s", err)
}
if err := blk2.Verify(context.Background()); err != nil {
t.Fatalf("Block failed verification: %s", err)
}
if err := tvm.vm.SetPreference(context.Background(), blk2.ID()); err != nil {
t.Fatal(err)
}
if err := tvm.atomicVM.AtomicMempool.AddLocalTx(importTx0B); err == nil {
t.Fatalf("Should not have been able to issue import tx with conflict")
}
// Force issue transaction directly into the mempool
if err := tvm.atomicVM.AtomicMempool.ForceAddTx(importTx0B); err != nil {
t.Fatal(err)
}
require.Equal(t, commonEng.PendingTxs, tvm.WaitForEvent(context.Background()))
_, err = tvm.vm.BuildBlock(context.Background())
if err == nil {
t.Fatal("Shouldn't have been able to build an invalid block")
}
}
func TestBonusBlocksTxs(t *testing.T) {
for _, scheme := range schemes {
t.Run(scheme, func(t *testing.T) {
testBonusBlocksTxs(t, scheme)
})
}
}
func testBonusBlocksTxs(t *testing.T, scheme string) {
fork := upgradetest.NoUpgrades
tvm := newVM(t, testVMConfig{
fork: &fork,
configJSON: getConfig(scheme, ""),
})
defer func() {
if err := tvm.vm.Shutdown(context.Background()); err != nil {
t.Fatal(err)
}
}()
importAmount := uint64(10000000)
utxoID := lux.UTXOID{TxID: ids.GenerateTestID()}
utxo := &lux.UTXO{
UTXOID: utxoID,
Asset: lux.Asset{ID: tvm.vm.ctx.UTXOAssetID},
Out: &secp256k1fx.TransferOutput{
Amt: importAmount,
OutputOwners: secp256k1fx.OutputOwners{
Threshold: 1,
Addrs: []ids.ShortID{testKeys[0].Address()},
},
},
}
utxoBytes, err := atomic.Codec.Marshal(atomic.CodecVersion, utxo)
if err != nil {
t.Fatal(err)
}
xChainSharedMemory := tvm.atomicMemory.NewSharedMemory(tvm.vm.ctx.XChainID)
inputID := utxo.InputID()
if err := xChainSharedMemory.Apply(map[ids.ID]*luxatomic.Requests{tvm.vm.ctx.ChainID: {PutRequests: []*luxatomic.Element{{
Key: inputID[:],
Value: utxoBytes,
Traits: [][]byte{
testKeys[0].Address().Bytes(),
},
}}}}); err != nil {
t.Fatal(err)
}
importTx, err := tvm.atomicVM.NewImportTx(tvm.vm.ctx.XChainID, testEthAddrs[0], initialBaseFee, []*secp256k1.PrivateKey{testKeys[0]})
if err != nil {
t.Fatal(err)
}
if err := tvm.atomicVM.AtomicMempool.AddLocalTx(importTx); err != nil {
t.Fatal(err)
}
require.Equal(t, commonEng.PendingTxs, tvm.WaitForEvent(context.Background()))
blk, err := tvm.vm.BuildBlock(context.Background())
if err != nil {
t.Fatal(err)
}
// Make [blk] a bonus block.
tvm.atomicVM.AtomicBackend.AddBonusBlock(blk.Height(), blk.ID())
// Remove the UTXOs from shared memory, so that non-bonus blocks will fail verification
if err := tvm.vm.ctx.SharedMemory.(luxatomic.SharedMemory).Apply(map[ids.ID]*luxatomic.Requests{tvm.vm.ctx.XChainID: {RemoveRequests: [][]byte{inputID[:]}}}); err != nil {
t.Fatal(err)
}
if err := blk.Verify(context.Background()); err != nil {
t.Fatal(err)
}
if err := tvm.vm.SetPreference(context.Background(), blk.ID()); err != nil {
t.Fatal(err)
}
if err := blk.Accept(context.Background()); err != nil {
t.Fatal(err)
}
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)
}
}
// 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) {
importAmount := uint64(1000000000)
fork := upgradetest.NoUpgrades
tvmConfig := testVMConfig{
fork: &fork,
configJSON: getConfig(scheme, `"pruning-enabled":false`),
utxos: map[ids.ShortID]uint64{
testShortIDAddrs[0]: importAmount,
},
}
tvm1 := newVM(t, tvmConfig)
tvm2 := newVM(t, tvmConfig)
defer func() {
if err := tvm1.vm.Shutdown(context.Background()); err != nil {
t.Fatal(err)
}
if err := tvm2.vm.Shutdown(context.Background()); err != nil {
t.Fatal(err)
}
}()
newTxPoolHeadChan1 := make(chan core.NewTxPoolReorgEvent, 1)
tvm1.vm.txPool.SubscribeNewReorgEvent(newTxPoolHeadChan1)
newTxPoolHeadChan2 := make(chan core.NewTxPoolReorgEvent, 1)
tvm2.vm.txPool.SubscribeNewReorgEvent(newTxPoolHeadChan2)
key := testKeys[0].ToECDSA()
address := testEthAddrs[0]
importTx, err := tvm1.atomicVM.NewImportTx(tvm1.vm.ctx.XChainID, address, initialBaseFee, []*secp256k1.PrivateKey{testKeys[0]})
if err != nil {
t.Fatal(err)
}
if err := tvm1.atomicVM.AtomicMempool.AddLocalTx(importTx); err != nil {
t.Fatal(err)
}
require.Equal(t, commonEng.PendingTxs, tvm1.WaitForEvent(context.Background()))
vm1BlkA, err := tvm1.vm.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 := tvm1.vm.SetPreference(context.Background(), vm1BlkA.ID()); err != nil {
t.Fatal(err)
}
vm2BlkA, err := tvm2.vm.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 := tvm2.vm.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), address, big.NewInt(10), 21000, big.NewInt(genesisparams.MinGasPrice), nil)
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(tvm1.vm.chainID), key)
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 = tvm1.vm.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)
}
}
require.Equal(t, commonEng.PendingTxs, tvm1.WaitForEvent(context.Background()))
vm1BlkB, err := tvm1.vm.BuildBlock(context.Background())
if err != nil {
t.Fatal(err)
}
if err := vm1BlkB.Verify(context.Background()); err != nil {
t.Fatal(err)
}
if err := tvm1.vm.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 = tvm2.vm.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)
}
}
require.Equal(t, commonEng.PendingTxs, tvm2.WaitForEvent(context.Background()))
vm2BlkC, err := tvm2.vm.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 := tvm1.vm.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 := tvm1.vm.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) {
importAmount := uint64(1000000000)
fork := upgradetest.NoUpgrades
tvmConfig := testVMConfig{
fork: &fork,
utxos: map[ids.ShortID]uint64{
testShortIDAddrs[0]: importAmount,
},
configJSON: getConfig(scheme, ""),
}
tvm1 := newVM(t, tvmConfig)
tvm2 := newVM(t, tvmConfig)
defer func() {
if err := tvm1.vm.Shutdown(context.Background()); err != nil {
t.Fatal(err)
}
if err := tvm2.vm.Shutdown(context.Background()); err != nil {
t.Fatal(err)
}
}()
newTxPoolHeadChan1 := make(chan core.NewTxPoolReorgEvent, 1)
tvm1.vm.txPool.SubscribeNewReorgEvent(newTxPoolHeadChan1)
newTxPoolHeadChan2 := make(chan core.NewTxPoolReorgEvent, 1)
tvm2.vm.txPool.SubscribeNewReorgEvent(newTxPoolHeadChan2)
key := testKeys[0].ToECDSA()
address := testEthAddrs[0]
importTx, err := tvm1.atomicVM.NewImportTx(tvm1.vm.ctx.XChainID, address, initialBaseFee, []*secp256k1.PrivateKey{testKeys[0]})
if err != nil {
t.Fatal(err)
}
if err := tvm1.atomicVM.AtomicMempool.AddLocalTx(importTx); err != nil {
t.Fatal(err)
}
require.Equal(t, commonEng.PendingTxs, tvm1.WaitForEvent(context.Background()))
vm1BlkA, err := tvm1.vm.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 := tvm1.vm.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 := tvm1.vm.SetPreference(context.Background(), vm1BlkA.ID()); err != nil {
t.Fatal(err)
}
vm2BlkA, err := tvm2.vm.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 := tvm2.vm.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 := tvm2.vm.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 := tvm1.vm.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 := tvm2.vm.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), address, big.NewInt(10), 21000, big.NewInt(genesisparams.MinGasPrice), nil)
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(tvm1.vm.chainID), key)
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 = tvm1.vm.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)
}
}
require.Equal(t, commonEng.PendingTxs, tvm1.WaitForEvent(context.Background()))
vm1BlkB, err := tvm1.vm.BuildBlock(context.Background())
if err != nil {
t.Fatal(err)
}
if err := vm1BlkB.Verify(context.Background()); err != nil {
t.Fatal(err)
}
if _, err := tvm1.vm.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 := tvm1.vm.SetPreference(context.Background(), vm1BlkB.ID()); err != nil {
t.Fatal(err)
}
tvm1.vm.eth.APIBackend.SetAllowUnfinalizedQueries(true)
blkBHeight := vm1BlkB.Height()
blkBHash := vm1BlkB.(*chain.BlockWrapper).Block.(extension.ExtendedBlock).GetEthBlock().Hash()
if b := tvm1.vm.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 = tvm2.vm.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)
}
}
require.Equal(t, commonEng.PendingTxs, tvm2.WaitForEvent(context.Background()))
vm2BlkC, err := tvm2.vm.BuildBlock(context.Background())
if err != nil {
t.Fatalf("Failed to build BlkC on VM2: %s", err)
}
vm1BlkC, err := tvm1.vm.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 := tvm1.vm.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 := tvm1.vm.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)
}
blkCHash := vm1BlkC.(*chain.BlockWrapper).Block.(extension.ExtendedBlock).GetEthBlock().Hash()
if b := tvm1.vm.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) {
importAmount := uint64(1000000000)
fork := upgradetest.NoUpgrades
tvmConfig := testVMConfig{
fork: &fork,
utxos: map[ids.ShortID]uint64{
testShortIDAddrs[0]: importAmount,
},
configJSON: getConfig(scheme, ""),
}
tvm1 := newVM(t, tvmConfig)
tvm2 := newVM(t, tvmConfig)
defer func() {
if err := tvm1.vm.Shutdown(context.Background()); err != nil {
t.Fatal(err)
}
if err := tvm2.vm.Shutdown(context.Background()); err != nil {
t.Fatal(err)
}
}()
newTxPoolHeadChan1 := make(chan core.NewTxPoolReorgEvent, 1)
tvm1.vm.txPool.SubscribeNewReorgEvent(newTxPoolHeadChan1)
newTxPoolHeadChan2 := make(chan core.NewTxPoolReorgEvent, 1)
tvm2.vm.txPool.SubscribeNewReorgEvent(newTxPoolHeadChan2)
key := testKeys[0].ToECDSA()
address := testEthAddrs[0]
importTx, err := tvm1.atomicVM.NewImportTx(tvm1.vm.ctx.XChainID, address, initialBaseFee, []*secp256k1.PrivateKey{testKeys[0]})
if err != nil {
t.Fatal(err)
}
if err := tvm1.atomicVM.AtomicMempool.AddLocalTx(importTx); err != nil {
t.Fatal(err)
}
require.Equal(t, commonEng.PendingTxs, tvm1.WaitForEvent(context.Background()))
vm1BlkA, err := tvm1.vm.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 := tvm1.vm.SetPreference(context.Background(), vm1BlkA.ID()); err != nil {
t.Fatal(err)
}
vm2BlkA, err := tvm2.vm.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 := tvm2.vm.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), address, big.NewInt(10), 21000, big.NewInt(genesisparams.MinGasPrice), nil)
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(tvm1.vm.chainID), key)
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 = tvm1.vm.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)
}
}
require.Equal(t, commonEng.PendingTxs, tvm1.WaitForEvent(context.Background()))
vm1BlkB, err := tvm1.vm.BuildBlock(context.Background())
if err != nil {
t.Fatal(err)
}
if err := vm1BlkB.Verify(context.Background()); err != nil {
t.Fatal(err)
}
if err := tvm1.vm.SetPreference(context.Background(), vm1BlkB.ID()); err != nil {
t.Fatal(err)
}
tvm1.vm.eth.APIBackend.SetAllowUnfinalizedQueries(true)
blkBHeight := vm1BlkB.Height()
blkBHash := vm1BlkB.(*chain.BlockWrapper).Block.(extension.ExtendedBlock).GetEthBlock().Hash()
if b := tvm1.vm.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 = tvm2.vm.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)
}
}
require.Equal(t, commonEng.PendingTxs, tvm2.WaitForEvent(context.Background()))
vm2BlkC, err := tvm2.vm.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 := tvm2.vm.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 = tvm2.vm.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)
}
}
require.Equal(t, commonEng.PendingTxs, tvm2.WaitForEvent(context.Background()))
vm2BlkD, err := tvm2.vm.BuildBlock(context.Background())
if err != nil {
t.Fatalf("Failed to build BlkD on VM2: %s", err)
}
// Parse blocks produced in vm2
vm1BlkC, err := tvm1.vm.ParseBlock(context.Background(), vm2BlkC.Bytes())
if err != nil {
t.Fatalf("Unexpected error parsing block from vm2: %s", err)
}
blkCHash := vm1BlkC.(*chain.BlockWrapper).Block.(extension.ExtendedBlock).GetEthBlock().Hash()
vm1BlkD, err := tvm1.vm.ParseBlock(context.Background(), vm2BlkD.Bytes())
if err != nil {
t.Fatalf("Unexpected error parsing block from vm2: %s", err)
}
blkDHeight := vm1BlkD.Height()
blkDHash := vm1BlkD.(*chain.BlockWrapper).Block.(extension.ExtendedBlock).GetEthBlock().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 := tvm1.vm.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 := tvm1.vm.blockChain.GetBlockByNumber(blkDHeight); b != nil {
t.Fatalf("expected block at %d to be nil but got %s", blkDHeight, b.Hash().Hex())
}
if b := tvm1.vm.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 := tvm1.vm.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 := tvm1.vm.blockChain.GetBlockByNumber(blkDHeight); b != nil {
t.Fatalf("expected block at %d to be nil but got %s", blkDHeight, b.Hash().Hex())
}
if b := tvm1.vm.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 := tvm1.vm.SetPreference(context.Background(), vm1BlkD.ID()); err != nil {
t.Fatal(err)
}
if b := tvm1.vm.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 := tvm1.vm.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 := tvm1.vm.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 := tvm1.vm.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 := tvm1.vm.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 := tvm1.vm.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) {
importAmount := uint64(1000000000)
fork := upgradetest.NoUpgrades
tvmConfig := testVMConfig{
fork: &fork,
utxos: map[ids.ShortID]uint64{
testShortIDAddrs[0]: importAmount,
},
configJSON: getConfig(scheme, ""),
}
tvm1 := newVM(t, tvmConfig)
tvm2 := newVM(t, tvmConfig)
defer func() {
if err := tvm1.vm.Shutdown(context.Background()); err != nil {
t.Fatal(err)
}
if err := tvm2.vm.Shutdown(context.Background()); err != nil {
t.Fatal(err)
}
}()
newTxPoolHeadChan1 := make(chan core.NewTxPoolReorgEvent, 1)
tvm1.vm.txPool.SubscribeNewReorgEvent(newTxPoolHeadChan1)
newTxPoolHeadChan2 := make(chan core.NewTxPoolReorgEvent, 1)
tvm2.vm.txPool.SubscribeNewReorgEvent(newTxPoolHeadChan2)
key := testKeys[0].ToECDSA()
address := testEthAddrs[0]
importTx, err := tvm1.atomicVM.NewImportTx(tvm1.vm.ctx.XChainID, address, initialBaseFee, []*secp256k1.PrivateKey{testKeys[0]})
if err != nil {
t.Fatal(err)
}
if err := tvm1.atomicVM.AtomicMempool.AddLocalTx(importTx); err != nil {
t.Fatal(err)
}
require.Equal(t, commonEng.PendingTxs, tvm1.WaitForEvent(context.Background()))
vm1BlkA, err := tvm1.vm.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 := tvm1.vm.SetPreference(context.Background(), vm1BlkA.ID()); err != nil {
t.Fatal(err)
}
vm2BlkA, err := tvm2.vm.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 := tvm2.vm.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), address, big.NewInt(10), 21000, big.NewInt(genesisparams.MinGasPrice), nil)
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(tvm1.vm.chainID), key)
if err != nil {
t.Fatal(err)
}
txs[i] = signedTx
}
var errs []error
errs = tvm1.vm.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)
}
}
require.Equal(t, commonEng.PendingTxs, tvm1.WaitForEvent(context.Background()))
vm1BlkB, err := tvm1.vm.BuildBlock(context.Background())
if err != nil {
t.Fatal(err)
}
if err := vm1BlkB.Verify(context.Background()); err != nil {
t.Fatal(err)
}
if err := tvm1.vm.SetPreference(context.Background(), vm1BlkB.ID()); err != nil {
t.Fatal(err)
}
errs = tvm2.vm.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)
}
}
require.Equal(t, commonEng.PendingTxs, tvm2.WaitForEvent(context.Background()))
vm2BlkC, err := tvm2.vm.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 := tvm2.vm.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 = tvm2.vm.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)
}
}
require.Equal(t, commonEng.PendingTxs, tvm2.WaitForEvent(context.Background()))
vm2BlkD, err := tvm2.vm.BuildBlock(context.Background())
if err != nil {
t.Fatalf("Failed to build BlkD on VM2: %s", err)
}
// Create uncle block from blkD
blkDEthBlock := vm2BlkD.(*chain.BlockWrapper).Block.(extension.ExtendedBlock).GetEthBlock()
uncles := []*types.Header{vm1BlkB.(*chain.BlockWrapper).Block.(extension.ExtendedBlock).GetEthBlock().Header()}
uncleBlockHeader := types.CopyHeader(blkDEthBlock.Header())
uncleBlockHeader.UncleHash = types.CalcUncleHash(uncles)
uncleEthBlock := customtypes.NewBlockWithExtData(
uncleBlockHeader,
blkDEthBlock.Transactions(),
uncles,
nil,
trie.NewStackTrie(nil),
customtypes.BlockExtData(blkDEthBlock),
false,
)
uncleBlock, err := wrapBlock(uncleEthBlock, tvm2.vm)
if err != nil {
t.Fatal(err)
}
if err := uncleBlock.Verify(context.Background()); !errors.Is(err, errUnclesUnsupported) {
t.Fatalf("VM2 should have failed with %q but got %q", errUnclesUnsupported, err.Error())
}
if _, err := tvm1.vm.ParseBlock(context.Background(), vm2BlkC.Bytes()); err != nil {
t.Fatalf("VM1 errored parsing blkC: %s", err)
}
if _, err := tvm1.vm.ParseBlock(context.Background(), uncleBlock.Bytes()); !errors.Is(err, errUnclesUnsupported) {
t.Fatalf("VM1 should have failed with %q but got %q", errUnclesUnsupported, err.Error())
}
}
// 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) {
importAmount := uint64(1000000000)
fork := upgradetest.NoUpgrades
tvm := newVM(t, testVMConfig{
fork: &fork,
utxos: map[ids.ShortID]uint64{
testShortIDAddrs[0]: importAmount,
},
configJSON: getConfig(scheme, ""),
})
defer func() {
if err := tvm.vm.Shutdown(context.Background()); err != nil {
t.Fatal(err)
}
}()
importTx, err := tvm.atomicVM.NewImportTx(tvm.vm.ctx.XChainID, testEthAddrs[0], initialBaseFee, []*secp256k1.PrivateKey{testKeys[0]})
if err != nil {
t.Fatal(err)
}
if err := tvm.atomicVM.AtomicMempool.AddLocalTx(importTx); err != nil {
t.Fatal(err)
}
require.Equal(t, commonEng.PendingTxs, tvm.WaitForEvent(context.Background()))
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
ethBlock := blk.(*chain.BlockWrapper).Block.(extension.ExtendedBlock).GetEthBlock()
emptyEthBlock := customtypes.NewBlockWithExtData(
types.CopyHeader(ethBlock.Header()),
nil,
nil,
nil,
new(trie.Trie),
nil,
false,
)
if len(customtypes.BlockExtData(emptyEthBlock)) != 0 || customtypes.GetHeaderExtra(emptyEthBlock.Header()).ExtDataHash != (common.Hash{}) {
t.Fatalf("emptyEthBlock should not have any extra data")
}
emptyBlock, err := wrapBlock(emptyEthBlock, tvm.vm)
if err != nil {
t.Fatal(err)
}
if _, err := tvm.vm.ParseBlock(context.Background(), emptyBlock.Bytes()); !errors.Is(err, atomicvm.ErrEmptyBlock) {
t.Fatalf("VM should have failed with errEmptyBlock but got %s", err.Error())
}
if err := emptyBlock.Verify(context.Background()); !errors.Is(err, atomicvm.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) {
importAmount := uint64(1000000000)
fork := upgradetest.NoUpgrades
tvmConfig := testVMConfig{
fork: &fork,
utxos: map[ids.ShortID]uint64{
testShortIDAddrs[0]: importAmount,
},
configJSON: getConfig(scheme, ""),
}
tvm1 := newVM(t, tvmConfig)
tvm2 := newVM(t, tvmConfig)
defer func() {
if err := tvm1.vm.Shutdown(context.Background()); err != nil {
t.Fatal(err)
}
if err := tvm2.vm.Shutdown(context.Background()); err != nil {
t.Fatal(err)
}
}()
newTxPoolHeadChan1 := make(chan core.NewTxPoolReorgEvent, 1)
tvm1.vm.txPool.SubscribeNewReorgEvent(newTxPoolHeadChan1)
newTxPoolHeadChan2 := make(chan core.NewTxPoolReorgEvent, 1)
tvm2.vm.txPool.SubscribeNewReorgEvent(newTxPoolHeadChan2)
key := testKeys[0].ToECDSA()
address := testEthAddrs[0]
importTx, err := tvm1.atomicVM.NewImportTx(tvm1.vm.ctx.XChainID, address, initialBaseFee, []*secp256k1.PrivateKey{testKeys[0]})
if err != nil {
t.Fatal(err)
}
if err := tvm1.atomicVM.AtomicMempool.AddLocalTx(importTx); err != nil {
t.Fatal(err)
}
require.Equal(t, commonEng.PendingTxs, tvm1.WaitForEvent(context.Background()))
vm1BlkA, err := tvm1.vm.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 := tvm1.vm.SetPreference(context.Background(), vm1BlkA.ID()); err != nil {
t.Fatal(err)
}
vm2BlkA, err := tvm2.vm.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 := tvm2.vm.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), address, big.NewInt(10), 21000, big.NewInt(genesisparams.MinGasPrice), nil)
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(tvm1.vm.chainID), key)
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 := tvm1.vm.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)
}
}
require.Equal(t, commonEng.PendingTxs, tvm1.WaitForEvent(context.Background()))
vm1BlkB, err := tvm1.vm.BuildBlock(context.Background())
if err != nil {
t.Fatal(err)
}
if err := vm1BlkB.Verify(context.Background()); err != nil {
t.Fatal(err)
}
if err := tvm1.vm.SetPreference(context.Background(), vm1BlkB.ID()); err != nil {
t.Fatal(err)
}
errs = tvm2.vm.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)
}
}
require.Equal(t, commonEng.PendingTxs, tvm2.WaitForEvent(context.Background()))
vm2BlkC, err := tvm2.vm.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 := tvm2.vm.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 = tvm2.vm.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)
}
}
require.Equal(t, commonEng.PendingTxs, tvm2.WaitForEvent(context.Background()))
vm2BlkD, err := tvm2.vm.BuildBlock(context.Background())
if err != nil {
t.Fatalf("Failed to build BlkD on VM2: %s", err)
}
// Parse blocks produced in vm2
vm1BlkC, err := tvm1.vm.ParseBlock(context.Background(), vm2BlkC.Bytes())
if err != nil {
t.Fatalf("Unexpected error parsing block from vm2: %s", err)
}
vm1BlkD, err := tvm1.vm.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)
}
blkBHash := vm1BlkB.(*chain.BlockWrapper).Block.(extension.ExtendedBlock).GetEthBlock().Hash()
if b := tvm1.vm.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)
}
blkCHash := vm1BlkC.(*chain.BlockWrapper).Block.(extension.ExtendedBlock).GetEthBlock().Hash()
if b := tvm1.vm.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)
}
blkDHash := vm1BlkD.(*chain.BlockWrapper).Block.(extension.ExtendedBlock).GetEthBlock().Hash()
if b := tvm1.vm.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) {
importAmount := uint64(1000000000)
fork := upgradetest.NoUpgrades
tvm := newVM(t, testVMConfig{
fork: &fork,
utxos: map[ids.ShortID]uint64{
testShortIDAddrs[0]: importAmount,
},
configJSON: getConfig(scheme, ""),
})
defer func() {
if err := tvm.vm.Shutdown(context.Background()); err != nil {
t.Fatal(err)
}
}()
importTx, err := tvm.atomicVM.NewImportTx(tvm.vm.ctx.XChainID, testEthAddrs[0], initialBaseFee, []*secp256k1.PrivateKey{testKeys[0]})
if err != nil {
t.Fatal(err)
}
if err := tvm.atomicVM.AtomicMempool.AddLocalTx(importTx); err != nil {
t.Fatal(err)
}
require.Equal(t, commonEng.PendingTxs, tvm.WaitForEvent(context.Background()))
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
internalBlkA := blkA.(*chain.BlockWrapper).Block.(extension.ExtendedBlock)
modifiedHeader := types.CopyHeader(internalBlkA.GetEthBlock().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 := customtypes.NewBlockWithExtData(
modifiedHeader,
nil,
nil,
nil,
new(trie.Trie),
customtypes.BlockExtData(internalBlkA.GetEthBlock()),
false,
)
futureBlock, err := wrapBlock(modifiedBlock, tvm.vm)
if err != nil {
t.Fatal(err)
}
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)
}
}
// Regression test to ensure we can build blocks if we are starting with the
// Apricot Phase 1 ruleset in genesis.
func TestBuildApricotPhase1Block(t *testing.T) {
for _, scheme := range schemes {
t.Run(scheme, func(t *testing.T) {
testBuildApricotPhase1Block(t, scheme)
})
}
}
func testBuildApricotPhase1Block(t *testing.T, scheme string) {
importAmount := uint64(1000000000)
fork := upgradetest.ApricotPhase1
tvm := newVM(t, testVMConfig{
fork: &fork,
utxos: map[ids.ShortID]uint64{
testShortIDAddrs[0]: importAmount,
},
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)
key := testKeys[0].ToECDSA()
address := testEthAddrs[0]
importTx, err := tvm.atomicVM.NewImportTx(tvm.vm.ctx.XChainID, address, initialBaseFee, []*secp256k1.PrivateKey{testKeys[0]})
if err != nil {
t.Fatal(err)
}
if err := tvm.atomicVM.AtomicMempool.AddLocalTx(importTx); err != nil {
t.Fatal(err)
}
require.Equal(t, commonEng.PendingTxs, tvm.WaitForEvent(context.Background()))
blk, err := tvm.vm.BuildBlock(context.Background())
if err != nil {
t.Fatal(err)
}
if err := blk.Verify(context.Background()); err != nil {
t.Fatal(err)
}
if err := tvm.vm.SetPreference(context.Background(), blk.ID()); err != nil {
t.Fatal(err)
}
if err := blk.Accept(context.Background()); err != nil {
t.Fatal(err)
}
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 < 5; i++ {
tx := types.NewTransaction(uint64(i), address, big.NewInt(10), 21000, big.NewInt(genesisparams.MinGasPrice), nil)
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(tvm.vm.chainID), key)
if err != nil {
t.Fatal(err)
}
txs[i] = signedTx
}
for i := 5; i < 10; i++ {
tx := types.NewTransaction(uint64(i), address, big.NewInt(10), 21000, big.NewInt(gaslimit_initial.MinGasPrice), nil)
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(tvm.vm.chainID), 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)
}
}
require.Equal(t, commonEng.PendingTxs, tvm.WaitForEvent(context.Background()))
blk, err = tvm.vm.BuildBlock(context.Background())
if err != nil {
t.Fatal(err)
}
if err := blk.Verify(context.Background()); err != nil {
t.Fatal(err)
}
if err := blk.Accept(context.Background()); err != nil {
t.Fatal(err)
}
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 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) {
importAmount := uint64(1000000000)
fork := upgradetest.NoUpgrades
tvm := newVM(t, testVMConfig{
fork: &fork,
utxos: map[ids.ShortID]uint64{
testShortIDAddrs[0]: importAmount,
},
configJSON: getConfig(scheme, ""),
})
defer func() {
if err := tvm.vm.Shutdown(context.Background()); err != nil {
t.Fatal(err)
}
}()
importTx, err := tvm.atomicVM.NewImportTx(tvm.vm.ctx.XChainID, testEthAddrs[0], initialBaseFee, []*secp256k1.PrivateKey{testKeys[0]})
if err != nil {
t.Fatal(err)
}
if err := tvm.atomicVM.AtomicMempool.AddLocalTx(importTx); err != nil {
t.Fatal(err)
}
require.Equal(t, commonEng.PendingTxs, tvm.WaitForEvent(context.Background()))
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()
blkHash := blk.(*chain.BlockWrapper).Block.(extension.ExtendedBlock).GetEthBlock().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())
}
}
// Builds [blkA] with a virtuous import transaction and [blkB] with a separate import transaction
// that does not conflict. Accepts [blkB] and rejects [blkA], then asserts that the virtuous atomic
// transaction in [blkA] is correctly re-issued into the atomic transaction mempool.
func TestReissueAtomicTx(t *testing.T) {
for _, scheme := range schemes {
t.Run(scheme, func(t *testing.T) {
testReissueAtomicTx(t, scheme)
})
}
}
func testReissueAtomicTx(t *testing.T, scheme string) {
fork := upgradetest.ApricotPhase1
tvm := newVM(t, testVMConfig{
fork: &fork,
utxos: map[ids.ShortID]uint64{
testShortIDAddrs[0]: 10000000,
testShortIDAddrs[1]: 10000000,
},
configJSON: getConfig(scheme, ""),
})
defer func() {
if err := tvm.vm.Shutdown(context.Background()); err != nil {
t.Fatal(err)
}
}()
genesisBlkID, err := tvm.vm.LastAccepted(context.Background())
if err != nil {
t.Fatal(err)
}
importTx, err := tvm.atomicVM.NewImportTx(tvm.vm.ctx.XChainID, testEthAddrs[0], initialBaseFee, []*secp256k1.PrivateKey{testKeys[0]})
if err != nil {
t.Fatal(err)
}
if err := tvm.atomicVM.AtomicMempool.AddLocalTx(importTx); err != nil {
t.Fatal(err)
}
require.Equal(t, commonEng.PendingTxs, tvm.WaitForEvent(context.Background()))
blkA, err := tvm.vm.BuildBlock(context.Background())
if err != nil {
t.Fatal(err)
}
if err := blkA.Verify(context.Background()); err != nil {
t.Fatal(err)
}
if err := tvm.vm.SetPreference(context.Background(), blkA.ID()); err != nil {
t.Fatal(err)
}
// SetPreference to parent before rejecting (will rollback state to genesis
// so that atomic transaction can be reissued, otherwise current block will
// conflict with UTXO to be reissued)
if err := tvm.vm.SetPreference(context.Background(), genesisBlkID); err != nil {
t.Fatal(err)
}
// Rejecting [blkA] should cause [importTx] to be re-issued into the mempool.
if err := blkA.Reject(context.Background()); err != nil {
t.Fatal(err)
}
// Sleep for a minimum of two seconds to ensure that [blkB] will have a different timestamp
// than [blkA] so that the block will be unique. This is necessary since we have marked [blkA]
// as Rejected.
time.Sleep(2 * time.Second)
require.Equal(t, commonEng.PendingTxs, tvm.WaitForEvent(context.Background()))
blkB, err := tvm.vm.BuildBlock(context.Background())
if err != nil {
t.Fatal(err)
}
if blkB.Height() != blkA.Height() {
t.Fatalf("Expected blkB (%d) to have the same height as blkA (%d)", blkB.Height(), blkA.Height())
}
if err := blkB.Verify(context.Background()); err != nil {
t.Fatal(err)
}
if err := tvm.vm.SetPreference(context.Background(), blkB.ID()); err != nil {
t.Fatal(err)
}
if err := blkB.Accept(context.Background()); err != nil {
t.Fatal(err)
}
if lastAcceptedID, err := tvm.vm.LastAccepted(context.Background()); err != nil {
t.Fatal(err)
} else if lastAcceptedID != blkB.ID() {
t.Fatalf("Expected last accepted blockID to be the accepted block: %s, but found %s", blkB.ID(), lastAcceptedID)
}
// Check that [importTx] has been indexed correctly after [blkB] is accepted.
_, height, err := tvm.atomicVM.AtomicTxRepository.GetByTxID(importTx.ID())
if err != nil {
t.Fatal(err)
} else if height != blkB.Height() {
t.Fatalf("Expected indexed height of import tx to be %d, but found %d", blkB.Height(), height)
}
}
func TestAtomicTxFailsEVMStateTransferBuildBlock(t *testing.T) {
for _, scheme := range schemes {
t.Run(scheme, func(t *testing.T) {
testAtomicTxFailsEVMStateTransferBuildBlock(t, scheme)
})
}
}
func testAtomicTxFailsEVMStateTransferBuildBlock(t *testing.T, scheme string) {
fork := upgradetest.ApricotPhase1
tvm := newVM(t, testVMConfig{
fork: &fork,
configJSON: getConfig(scheme, ""),
})
defer func() {
if err := tvm.vm.Shutdown(context.Background()); err != nil {
t.Fatal(err)
}
}()
exportTxs := createExportTxOptions(t, tvm.atomicVM, tvm.atomicMemory)
exportTx1, exportTx2 := exportTxs[0], exportTxs[1]
if err := tvm.atomicVM.AtomicMempool.AddLocalTx(exportTx1); err != nil {
t.Fatal(err)
}
require.Equal(t, commonEng.PendingTxs, tvm.WaitForEvent(context.Background()))
exportBlk1, err := tvm.vm.BuildBlock(context.Background())
if err != nil {
t.Fatal(err)
}
if err := exportBlk1.Verify(context.Background()); err != nil {
t.Fatal(err)
}
if err := tvm.vm.SetPreference(context.Background(), exportBlk1.ID()); err != nil {
t.Fatal(err)
}
if err := tvm.atomicVM.AtomicMempool.AddLocalTx(exportTx2); err == nil {
t.Fatal("Should have failed to issue due to an invalid export tx")
}
if err := tvm.atomicVM.AtomicMempool.AddRemoteTx(exportTx2); err == nil {
t.Fatal("Should have failed to add because conflicting")
}
// Manually add transaction to mempool to bypass validation
if err := tvm.atomicVM.AtomicMempool.ForceAddTx(exportTx2); err != nil {
t.Fatal(err)
}
require.Equal(t, commonEng.PendingTxs, tvm.WaitForEvent(context.Background()))
_, err = tvm.vm.BuildBlock(context.Background())
if err == nil {
t.Fatal("BuildBlock should have returned an error due to invalid export transaction")
}
}
func TestBuildInvalidBlockHead(t *testing.T) {
for _, scheme := range schemes {
t.Run(scheme, func(t *testing.T) {
testBuildInvalidBlockHead(t, scheme)
})
}
}
func testBuildInvalidBlockHead(t *testing.T, scheme string) {
fork := upgradetest.ApricotPhase1
tvm := newVM(t, testVMConfig{
fork: &fork,
configJSON: getConfig(scheme, ""),
})
defer func() {
if err := tvm.vm.Shutdown(context.Background()); err != nil {
t.Fatal(err)
}
}()
key0 := testKeys[0]
addr0 := key0.Address()
// Create the transaction
utx := &atomic.UnsignedImportTx{
NetworkID: tvm.vm.ctx.NetworkID,
BlockchainID: tvm.vm.ctx.ChainID,
Outs: []atomic.EVMOutput{{
Address: common.Address(addr0),
Amount: 1 * constants.Lux,
AssetID: tvm.vm.ctx.UTXOAssetID,
}},
ImportedInputs: []*lux.TransferableInput{
{
Asset: lux.Asset{ID: tvm.vm.ctx.UTXOAssetID},
In: &secp256k1fx.TransferInput{
Amt: 1 * constants.Lux,
Input: secp256k1fx.Input{
SigIndices: []uint32{0},
},
},
},
},
SourceChain: tvm.vm.ctx.XChainID,
}
tx := &atomic.Tx{UnsignedAtomicTx: utx}
if err := tx.Sign(atomic.Codec, [][]*secp256k1.PrivateKey{{key0}}); err != nil {
t.Fatal(err)
}
currentBlock := tvm.vm.blockChain.CurrentBlock()
// Verify that the transaction fails verification when attempting to issue
// it into the atomic mempool.
if err := tvm.atomicVM.AtomicMempool.AddLocalTx(tx); err == nil {
t.Fatal("Should have failed to issue invalid transaction")
}
// Force issue the transaction directly to the mempool
if err := tvm.atomicVM.AtomicMempool.ForceAddTx(tx); err != nil {
t.Fatal(err)
}
require.Equal(t, commonEng.PendingTxs, tvm.WaitForEvent(context.Background()))
if _, err := tvm.vm.BuildBlock(context.Background()); err == nil {
t.Fatalf("Unexpectedly created a block")
}
newCurrentBlock := tvm.vm.blockChain.CurrentBlock()
if currentBlock.Hash() != newCurrentBlock.Hash() {
t.Fatal("current block changed")
}
}
// Regression test to ensure we can build blocks if we are starting with the
// Apricot Phase 4 ruleset in genesis.
func TestBuildApricotPhase4Block(t *testing.T) {
for _, scheme := range schemes {
t.Run(scheme, func(t *testing.T) {
testBuildApricotPhase4Block(t, scheme)
})
}
}
func testBuildApricotPhase4Block(t *testing.T, scheme string) {
fork := upgradetest.ApricotPhase4
tvm := newVM(t, testVMConfig{
fork: &fork,
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)
key := testKeys[0].ToECDSA()
address := testEthAddrs[0]
importAmount := uint64(1000000000)
utxoID := lux.UTXOID{TxID: ids.GenerateTestID()}
utxo := &lux.UTXO{
UTXOID: utxoID,
Asset: lux.Asset{ID: tvm.vm.ctx.UTXOAssetID},
Out: &secp256k1fx.TransferOutput{
Amt: importAmount,
OutputOwners: secp256k1fx.OutputOwners{
Threshold: 1,
Addrs: []ids.ShortID{testKeys[0].Address()},
},
},
}
utxoBytes, err := atomic.Codec.Marshal(atomic.CodecVersion, utxo)
if err != nil {
t.Fatal(err)
}
xChainSharedMemory := tvm.atomicMemory.NewSharedMemory(tvm.vm.ctx.XChainID)
inputID := utxo.InputID()
if err := xChainSharedMemory.Apply(map[ids.ID]*luxatomic.Requests{tvm.vm.ctx.ChainID: {PutRequests: []*luxatomic.Element{{
Key: inputID[:],
Value: utxoBytes,
Traits: [][]byte{
testKeys[0].Address().Bytes(),
},
}}}}); err != nil {
t.Fatal(err)
}
importTx, err := tvm.atomicVM.NewImportTx(tvm.vm.ctx.XChainID, address, initialBaseFee, []*secp256k1.PrivateKey{testKeys[0]})
if err != nil {
t.Fatal(err)
}
if err := tvm.atomicVM.AtomicMempool.AddLocalTx(importTx); err != nil {
t.Fatal(err)
}
require.Equal(t, commonEng.PendingTxs, tvm.WaitForEvent(context.Background()))
blk, err := tvm.vm.BuildBlock(context.Background())
if err != nil {
t.Fatal(err)
}
if err := blk.Verify(context.Background()); err != nil {
t.Fatal(err)
}
if err := tvm.vm.SetPreference(context.Background(), blk.ID()); err != nil {
t.Fatal(err)
}
if err := blk.Accept(context.Background()); err != nil {
t.Fatal(err)
}
ethBlk := blk.(*chain.BlockWrapper).Block.(extension.ExtendedBlock).GetEthBlock()
if eBlockGasCost := customtypes.BlockGasCost(ethBlk); eBlockGasCost == nil || eBlockGasCost.Cmp(common.Big0) != 0 {
t.Fatalf("expected blockGasCost to be 0 but got %d", eBlockGasCost)
}
if eExtDataGasUsed := customtypes.BlockExtDataGasUsed(ethBlk); eExtDataGasUsed == nil || eExtDataGasUsed.Cmp(big.NewInt(1230)) != 0 {
t.Fatalf("expected extDataGasUsed to be 1000 but got %d", eExtDataGasUsed)
}
minRequiredTip, err := header.EstimateRequiredTip(tvm.vm.chainConfigExtra(), ethBlk.Header())
if err != nil {
t.Fatal(err)
}
if minRequiredTip == nil || minRequiredTip.Cmp(common.Big0) != 0 {
t.Fatalf("expected minRequiredTip to be 0 but got %d", minRequiredTip)
}
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 < 5; i++ {
tx := types.NewTransaction(uint64(i), address, big.NewInt(10), 21000, big.NewInt(genesisparams.MinGasPrice), nil)
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(tvm.vm.chainID), key)
if err != nil {
t.Fatal(err)
}
txs[i] = signedTx
}
for i := 5; i < 10; i++ {
tx := types.NewTransaction(uint64(i), address, big.NewInt(10), 21000, big.NewInt(gaslimit_initial.MinGasPrice), nil)
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(tvm.vm.chainID), 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)
}
}
require.Equal(t, commonEng.PendingTxs, tvm.WaitForEvent(context.Background()))
blk, err = tvm.vm.BuildBlock(context.Background())
if err != nil {
t.Fatal(err)
}
if err := blk.Verify(context.Background()); err != nil {
t.Fatal(err)
}
if err := blk.Accept(context.Background()); err != nil {
t.Fatal(err)
}
ethBlk = blk.(*chain.BlockWrapper).Block.(extension.ExtendedBlock).GetEthBlock()
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))
}
if customtypes.BlockExtDataGasUsed(ethBlk) == nil || customtypes.BlockExtDataGasUsed(ethBlk).Cmp(common.Big0) != 0 {
t.Fatalf("expected extDataGasUsed to be 0 but got %d", customtypes.BlockExtDataGasUsed(ethBlk))
}
minRequiredTip, err = header.EstimateRequiredTip(tvm.vm.chainConfigExtra(), 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())
}
}
}
// Regression test to ensure we can build blocks if we are starting with the
// Apricot Phase 5 ruleset in genesis.
func TestBuildApricotPhase5Block(t *testing.T) {
for _, scheme := range schemes {
t.Run(scheme, func(t *testing.T) {
testBuildApricotPhase5Block(t, scheme)
})
}
}
func testBuildApricotPhase5Block(t *testing.T, scheme string) {
fork := upgradetest.ApricotPhase5
tvm := newVM(t, testVMConfig{
fork: &fork,
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)
key := testKeys[0].ToECDSA()
address := testEthAddrs[0]
importAmount := uint64(1000000000)
utxoID := lux.UTXOID{TxID: ids.GenerateTestID()}
utxo := &lux.UTXO{
UTXOID: utxoID,
Asset: lux.Asset{ID: tvm.vm.ctx.UTXOAssetID},
Out: &secp256k1fx.TransferOutput{
Amt: importAmount,
OutputOwners: secp256k1fx.OutputOwners{
Threshold: 1,
Addrs: []ids.ShortID{testKeys[0].Address()},
},
},
}
utxoBytes, err := atomic.Codec.Marshal(atomic.CodecVersion, utxo)
if err != nil {
t.Fatal(err)
}
xChainSharedMemory := tvm.atomicMemory.NewSharedMemory(tvm.vm.ctx.XChainID)
inputID := utxo.InputID()
if err := xChainSharedMemory.Apply(map[ids.ID]*luxatomic.Requests{tvm.vm.ctx.ChainID: {PutRequests: []*luxatomic.Element{{
Key: inputID[:],
Value: utxoBytes,
Traits: [][]byte{
testKeys[0].Address().Bytes(),
},
}}}}); err != nil {
t.Fatal(err)
}
importTx, err := tvm.atomicVM.NewImportTx(tvm.vm.ctx.XChainID, address, initialBaseFee, []*secp256k1.PrivateKey{testKeys[0]})
if err != nil {
t.Fatal(err)
}
if err := tvm.atomicVM.AtomicMempool.AddLocalTx(importTx); err != nil {
t.Fatal(err)
}
require.Equal(t, commonEng.PendingTxs, tvm.WaitForEvent(context.Background()))
blk, err := tvm.vm.BuildBlock(context.Background())
if err != nil {
t.Fatal(err)
}
if err := blk.Verify(context.Background()); err != nil {
t.Fatal(err)
}
if err := tvm.vm.SetPreference(context.Background(), blk.ID()); err != nil {
t.Fatal(err)
}
if err := blk.Accept(context.Background()); err != nil {
t.Fatal(err)
}
ethBlk := blk.(*chain.BlockWrapper).Block.(extension.ExtendedBlock).GetEthBlock()
if eBlockGasCost := customtypes.BlockGasCost(ethBlk); eBlockGasCost == nil || eBlockGasCost.Cmp(common.Big0) != 0 {
t.Fatalf("expected blockGasCost to be 0 but got %d", eBlockGasCost)
}
if eExtDataGasUsed := customtypes.BlockExtDataGasUsed(ethBlk); eExtDataGasUsed == nil || eExtDataGasUsed.Cmp(big.NewInt(11230)) != 0 {
t.Fatalf("expected extDataGasUsed to be 11230 but got %d", eExtDataGasUsed)
}
minRequiredTip, err := header.EstimateRequiredTip(tvm.vm.chainConfigExtra(), ethBlk.Header())
if err != nil {
t.Fatal(err)
}
if minRequiredTip == nil || minRequiredTip.Cmp(common.Big0) != 0 {
t.Fatalf("expected minRequiredTip to be 0 but got %d", minRequiredTip)
}
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), address, big.NewInt(10), 21000, big.NewInt(3*genesisparams.MinGasPrice), nil)
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(tvm.vm.chainID), key)
if err != nil {
t.Fatal(err)
}
txs[i] = signedTx
}
errs := tvm.vm.txPool.Add(txs, false, false)
for i, err := range errs {
if err != nil {
t.Fatalf("Failed to add tx at index %d: %s", i, err)
}
}
require.Equal(t, commonEng.PendingTxs, tvm.WaitForEvent(context.Background()))
blk, err = tvm.vm.BuildBlock(context.Background())
if err != nil {
t.Fatal(err)
}
if err := blk.Verify(context.Background()); err != nil {
t.Fatal(err)
}
if err := blk.Accept(context.Background()); err != nil {
t.Fatal(err)
}
ethBlk = blk.(*chain.BlockWrapper).Block.(extension.ExtendedBlock).GetEthBlock()
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))
}
if customtypes.BlockExtDataGasUsed(ethBlk) == nil || customtypes.BlockExtDataGasUsed(ethBlk).Cmp(common.Big0) != 0 {
t.Fatalf("expected extDataGasUsed to be 0 but got %d", customtypes.BlockExtDataGasUsed(ethBlk))
}
minRequiredTip, err = header.EstimateRequiredTip(tvm.vm.chainConfigExtra(), 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())
}
}
}
// This is a regression test to ensure that if two consecutive atomic transactions fail verification
// in onFinalizeAndAssemble it will not cause a panic due to calling RevertToSnapshot(revID) on the
// same revision ID twice.
func TestConsecutiveAtomicTransactionsRevertSnapshot(t *testing.T) {
fork := upgradetest.ApricotPhase1
tvm := newVM(t, testVMConfig{
fork: &fork,
})
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)
// Create three conflicting import transactions
importTxs := createImportTxOptions(t, tvm.atomicVM, tvm.atomicMemory)
// Issue the first import transaction, build, and accept the block.
if err := tvm.atomicVM.AtomicMempool.AddLocalTx(importTxs[0]); err != nil {
t.Fatal(err)
}
require.Equal(t, commonEng.PendingTxs, tvm.WaitForEvent(context.Background()))
blk, err := tvm.vm.BuildBlock(context.Background())
if err != nil {
t.Fatal(err)
}
if err := blk.Verify(context.Background()); err != nil {
t.Fatal(err)
}
if err := tvm.vm.SetPreference(context.Background(), blk.ID()); err != nil {
t.Fatal(err)
}
if err := blk.Accept(context.Background()); err != nil {
t.Fatal(err)
}
newHead := <-newTxPoolHeadChan
if newHead.Head.Hash() != common.Hash(blk.ID()) {
t.Fatalf("Expected new block to match")
}
// Add the two conflicting transactions directly to the mempool, so that two consecutive transactions
// will fail verification when build block is called.
tvm.atomicVM.AtomicMempool.AddRemoteTx(importTxs[1])
tvm.atomicVM.AtomicMempool.AddRemoteTx(importTxs[2])
if _, err := tvm.vm.BuildBlock(context.Background()); err == nil {
t.Fatal("Expected build block to fail due to empty block")
}
}
func TestAtomicTxBuildBlockDropsConflicts(t *testing.T) {
importAmount := uint64(10000000)
fork := upgradetest.ApricotPhase5
tvm := newVM(t, testVMConfig{
fork: &fork,
utxos: map[ids.ShortID]uint64{
testShortIDAddrs[0]: importAmount,
testShortIDAddrs[1]: importAmount,
testShortIDAddrs[2]: importAmount,
},
})
conflictKey := utilstest.NewKey(t)
defer func() {
if err := tvm.vm.Shutdown(context.Background()); err != nil {
t.Fatal(err)
}
}()
// Create a conflict set for each pair of transactions
conflictSets := make([]set.Set[ids.ID], len(testKeys))
for index, key := range testKeys {
importTx, err := tvm.atomicVM.NewImportTx(tvm.vm.ctx.XChainID, testEthAddrs[index], initialBaseFee, []*secp256k1.PrivateKey{key})
if err != nil {
t.Fatal(err)
}
if err := tvm.atomicVM.AtomicMempool.AddLocalTx(importTx); err != nil {
t.Fatal(err)
}
conflictSets[index].Add(importTx.ID())
conflictTx, err := tvm.atomicVM.NewImportTx(tvm.vm.ctx.XChainID, conflictKey.Address, initialBaseFee, []*secp256k1.PrivateKey{key})
if err != nil {
t.Fatal(err)
}
if err := tvm.atomicVM.AtomicMempool.AddLocalTx(conflictTx); err == nil {
t.Fatal("should conflict with the utxoSet in the mempool")
}
// force add the tx
tvm.atomicVM.AtomicMempool.ForceAddTx(conflictTx)
conflictSets[index].Add(conflictTx.ID())
}
require.Equal(t, commonEng.PendingTxs, tvm.WaitForEvent(context.Background()))
// Note: this only checks the path through OnFinalizeAndAssemble, we should make sure to add a test
// that verifies blocks received from the network will also fail verification
blk, err := tvm.vm.BuildBlock(context.Background())
if err != nil {
t.Fatal(err)
}
atomicTxs := blk.(*chain.BlockWrapper).Block.(extension.ExtendedBlock).GetBlockExtension().(atomic.AtomicBlockContext).AtomicTxs()
assert.True(t, len(atomicTxs) == len(testKeys), "Conflict transactions should be out of the batch")
atomicTxIDs := set.Set[ids.ID]{}
for _, tx := range atomicTxs {
atomicTxIDs.Add(tx.ID())
}
// Check that removing the txIDs actually included in the block from each conflict set
// leaves one item remaining for each conflict set ie. only one tx from each conflict set
// has been included in the block.
for _, conflictSet := range conflictSets {
conflictSet.Difference(atomicTxIDs)
assert.Equal(t, 1, conflictSet.Len())
}
if err := blk.Verify(context.Background()); err != nil {
t.Fatal(err)
}
if err := blk.Accept(context.Background()); err != nil {
t.Fatal(err)
}
}
func TestBuildBlockDoesNotExceedAtomicGasLimit(t *testing.T) {
importAmount := uint64(10000000)
fork := upgradetest.ApricotPhase5
tvm := newVM(t, testVMConfig{
fork: &fork,
})
defer func() {
if err := tvm.vm.Shutdown(context.Background()); err != nil {
t.Fatal(err)
}
}()
kc := secp256k1fx.NewKeychain()
kc.Add(testKeys[0])
txID, err := ids.ToID(hash.ComputeHash256(testShortIDAddrs[0][:]))
assert.NoError(t, err)
mempoolTxs := 200
for i := 0; i < mempoolTxs; i++ {
utxo, err := addUTXO(tvm.atomicMemory, tvm.vm.ctx, txID, uint32(i), tvm.vm.ctx.UTXOAssetID, importAmount, testShortIDAddrs[0])
assert.NoError(t, err)
importTx, err := atomic.NewImportTx(tvm.vm.ctx, tvm.vm.currentRules(), tvm.vm.clock.Unix(), tvm.vm.ctx.XChainID, testEthAddrs[0], initialBaseFee, kc, []*lux.UTXO{utxo})
if err != nil {
t.Fatal(err)
}
if err := tvm.atomicVM.AtomicMempool.AddLocalTx(importTx); err != nil {
t.Fatal(err)
}
}
require.Equal(t, commonEng.PendingTxs, tvm.WaitForEvent(context.Background()))
blk, err := tvm.vm.BuildBlock(context.Background())
if err != nil {
t.Fatal(err)
}
atomicTxs := blk.(*chain.BlockWrapper).Block.(extension.ExtendedBlock).GetBlockExtension().(atomic.AtomicBlockContext).AtomicTxs()
// Need to ensure that not all of the transactions in the mempool are included in the block.
// This ensures that we hit the atomic gas limit while building the block before we hit the
// upper limit on the size of the codec for marshalling the atomic transactions.
if len(atomicTxs) >= mempoolTxs {
t.Fatalf("Expected number of atomic transactions included in the block (%d) to be less than the number of transactions added to the mempool (%d)", len(atomicTxs), mempoolTxs)
}
}
func TestExtraStateChangeAtomicGasLimitExceeded(t *testing.T) {
importAmount := uint64(10000000)
// We create two VMs one in ApriotPhase4 and one in ApricotPhase5, so that we can construct a block
// containing a large enough atomic transaction that it will exceed the atomic gas limit in
// ApricotPhase5.
ap4 := upgradetest.ApricotPhase4
tvm1 := newVM(t, testVMConfig{
fork: &ap4,
})
ap5 := upgradetest.ApricotPhase5
tvm2 := newVM(t, testVMConfig{
fork: &ap5,
})
defer func() {
if err := tvm1.vm.Shutdown(context.Background()); err != nil {
t.Fatal(err)
}
if err := tvm2.vm.Shutdown(context.Background()); err != nil {
t.Fatal(err)
}
}()
kc := secp256k1fx.NewKeychain()
kc.Add(testKeys[0])
txID, err := ids.ToID(hash.ComputeHash256(testShortIDAddrs[0][:]))
assert.NoError(t, err)
// Add enough UTXOs, such that the created import transaction will attempt to consume more gas than allowed
// in ApricotPhase5.
for i := 0; i < 100; i++ {
_, err := addUTXO(tvm1.atomicMemory, tvm1.vm.ctx, txID, uint32(i), tvm1.vm.ctx.UTXOAssetID, importAmount, testShortIDAddrs[0])
assert.NoError(t, err)
_, err = addUTXO(tvm2.atomicMemory, tvm2.vm.ctx, txID, uint32(i), tvm2.vm.ctx.UTXOAssetID, importAmount, testShortIDAddrs[0])
assert.NoError(t, err)
}
// Double the initial base fee used when estimating the cost of this transaction to ensure that when it is
// used in ApricotPhase5 it still pays a sufficient fee with the fixed fee per atomic transaction.
importTx, err := tvm1.atomicVM.NewImportTx(tvm1.vm.ctx.XChainID, testEthAddrs[0], new(big.Int).Mul(common.Big2, initialBaseFee), []*secp256k1.PrivateKey{testKeys[0]})
if err != nil {
t.Fatal(err)
}
if err := tvm1.atomicVM.AtomicMempool.ForceAddTx(importTx); err != nil {
t.Fatal(err)
}
require.Equal(t, commonEng.PendingTxs, tvm1.WaitForEvent(context.Background()))
blk1, err := tvm1.vm.BuildBlock(context.Background())
if err != nil {
t.Fatal(err)
}
if err := blk1.Verify(context.Background()); err != nil {
t.Fatal(err)
}
validEthBlock := blk1.(*chain.BlockWrapper).Block.(extension.ExtendedBlock).GetEthBlock()
extraData, err := atomic.Codec.Marshal(atomic.CodecVersion, []*atomic.Tx{importTx})
if err != nil {
t.Fatal(err)
}
// Construct the new block with the extra data in the new format (slice of atomic transactions).
ethBlk2 := customtypes.NewBlockWithExtData(
types.CopyHeader(validEthBlock.Header()),
nil,
nil,
nil,
new(trie.Trie),
extraData,
true,
)
state, err := tvm2.vm.blockChain.State()
if err != nil {
t.Fatal(err)
}
// Hack: test [onExtraStateChange] directly to ensure it catches the atomic gas limit error correctly.
onExtraStateChangeFn := tvm2.vm.extensionConfig.ConsensusCallbacks.OnExtraStateChange
if _, _, err := onExtraStateChangeFn(ethBlk2, nil, state); err == nil || !strings.Contains(err.Error(), "exceeds atomic gas limit") {
t.Fatalf("Expected block to fail verification due to exceeded atomic gas limit, but found error: %v", err)
}
}
func TestSkipChainConfigCheckCompatible(t *testing.T) {
importAmount := uint64(50000000)
fork := upgradetest.Durango
tvm := newVM(t, testVMConfig{
fork: &fork,
utxos: map[ids.ShortID]uint64{
testShortIDAddrs[0]: importAmount,
},
})
defer func() { require.NoError(t, tvm.vm.Shutdown(context.Background())) }()
// Since rewinding is permitted for last accepted height of 0, we must
// accept one block to test the SkipUpgradeCheck functionality.
importTx, err := tvm.atomicVM.NewImportTx(tvm.vm.ctx.XChainID, testEthAddrs[0], initialBaseFee, []*secp256k1.PrivateKey{testKeys[0]})
require.NoError(t, err)
require.NoError(t, tvm.atomicVM.AtomicMempool.AddLocalTx(importTx))
require.Equal(t, commonEng.PendingTxs, tvm.WaitForEvent(context.Background()))
blk, err := tvm.vm.BuildBlock(context.Background())
require.NoError(t, err)
require.NoError(t, blk.Verify(context.Background()))
require.NoError(t, tvm.vm.SetPreference(context.Background(), blk.ID()))
require.NoError(t, blk.Accept(context.Background()))
reinitVM := atomicvm.WrapVM(&VM{})
// use the block's timestamp instead of 0 since rewind to genesis
// is hardcoded to be allowed in core/genesis.go.
newCTX := consensustest.Context(t, tvm.vm.ctx.ChainID)
upgradetest.SetTimesTo(&newCTX.NetworkUpgrades, upgradetest.Latest, upgrade.UnscheduledActivationTime)
upgradetest.SetTimesTo(&newCTX.NetworkUpgrades, fork+1, blk.Timestamp())
upgradetest.SetTimesTo(&newCTX.NetworkUpgrades, fork, upgrade.InitiallyActiveTime)
genesis := []byte(genesisJSON(forkToChainConfig[fork]))
err = reinitVM.Initialize(context.Background(), newCTX, tvm.db, genesis, []byte{}, []byte{}, []*commonEng.Fx{}, tvm.appSender)
require.ErrorContains(t, err, "mismatching Cancun fork timestamp in database")
reinitVM = atomicvm.WrapVM(&VM{})
newCTX.Metrics = metrics.NewPrefixGatherer()
// try again with skip-upgrade-check
config := []byte(`{"skip-upgrade-check": true}`)
require.NoError(t, reinitVM.Initialize(
context.Background(),
newCTX,
tvm.db,
genesis,
[]byte{},
config,
[]*commonEng.Fx{},
tvm.appSender))
require.NoError(t, reinitVM.Shutdown(context.Background()))
}
func TestParentBeaconRootBlock(t *testing.T) {
tests := []struct {
name string
fork upgradetest.Fork
beaconRoot *common.Hash
expectedError bool
errString string
}{
{
name: "non-empty parent beacon root in Durango",
fork: upgradetest.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",
fork: upgradetest.Durango,
beaconRoot: &common.Hash{},
expectedError: true,
},
{
name: "nil parent beacon root in Durango",
fork: upgradetest.Durango,
beaconRoot: nil,
expectedError: false,
},
{
name: "non-empty parent beacon root in E-Upgrade (Cancun)",
fork: upgradetest.Quasar,
beaconRoot: &common.Hash{0x01},
expectedError: true,
errString: "expected empty hash",
},
{
name: "empty parent beacon root in E-Upgrade (Cancun)",
fork: upgradetest.Quasar,
beaconRoot: &common.Hash{},
expectedError: false,
},
{
name: "nil parent beacon root in E-Upgrade (Cancun)",
fork: upgradetest.Quasar,
beaconRoot: nil,
expectedError: true,
errString: "header is missing parentBeaconRoot",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
importAmount := uint64(1000000000)
tvm := newVM(t, testVMConfig{
fork: &test.fork,
utxos: map[ids.ShortID]uint64{
testShortIDAddrs[0]: importAmount,
},
})
defer func() {
if err := tvm.vm.Shutdown(context.Background()); err != nil {
t.Fatal(err)
}
}()
importTx, err := tvm.atomicVM.NewImportTx(tvm.vm.ctx.XChainID, testEthAddrs[0], initialBaseFee, []*secp256k1.PrivateKey{testKeys[0]})
if err != nil {
t.Fatal(err)
}
if err := tvm.atomicVM.AtomicMempool.AddLocalTx(importTx); err != nil {
t.Fatal(err)
}
require.Equal(t, commonEng.PendingTxs, tvm.WaitForEvent(context.Background()))
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
ethBlock := blk.(*chain.BlockWrapper).Block.(extension.ExtendedBlock).GetEthBlock()
header := types.CopyHeader(ethBlock.Header())
header.ParentBeaconRoot = test.beaconRoot
parentBeaconEthBlock := customtypes.NewBlockWithExtData(
header,
nil,
nil,
nil,
new(trie.Trie),
customtypes.BlockExtData(ethBlock),
false,
)
parentBeaconBlock, err := wrapBlock(parentBeaconEthBlock, tvm.vm)
if err != nil {
t.Fatal(err)
}
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 TestNoBlobsAllowed(t *testing.T) {
ctx := context.Background()
require := require.New(t)
gspec := new(core.Genesis)
require.NoError(json.Unmarshal([]byte(genesisJSONCancun), gspec))
// Make one block with a single blob tx
signer := types.NewCancunSigner(gspec.Config.ChainID)
blockGen := func(_ int, b *core.BlockGen) {
b.SetCoinbase(constants.BlackholeAddr)
fee := big.NewInt(500)
fee.Add(fee, b.BaseFee())
tx, err := types.SignTx(types.NewTx(&types.BlobTx{
Nonce: 0,
GasTipCap: uint256.NewInt(1),
GasFeeCap: uint256.MustFromBig(fee),
Gas: params.TxGas,
To: testEthAddrs[0],
BlobFeeCap: uint256.NewInt(1),
BlobHashes: []common.Hash{{1}}, // This blob is expected to cause verification to fail
Value: new(uint256.Int),
}), signer, testKeys[0].ToECDSA())
require.NoError(err)
b.AddTx(tx)
}
// FullFaker used to skip header verification so we can generate a block with blobs
_, blocks, _, err := core.GenerateChainWithGenesis(gspec, dummy.NewFullFaker(), 1, 10, blockGen)
require.NoError(err)
// Create a VM with the genesis (will use header verification)
vm := newVM(t, testVMConfig{
genesisJSON: genesisJSONCancun,
}).vm
defer func() { require.NoError(vm.Shutdown(ctx)) }()
// Verification should fail
vmBlock, err := wrapBlock(blocks[0], vm)
require.NoError(err)
_, err = vm.ParseBlock(ctx, vmBlock.Bytes())
require.ErrorContains(err, "blobs not enabled on lux networks")
err = vmBlock.Verify(ctx)
require.ErrorContains(err, "blobs not enabled on lux networks")
}
func TestBuildBlockWithInsufficientCapacity(t *testing.T) {
ctx := context.Background()
require := require.New(t)
importAmount := uint64(2_000_000_000_000_000) // 2M LUX
fork := upgradetest.Fortuna
tvm := newVM(t, testVMConfig{
fork: &fork,
utxos: map[ids.ShortID]uint64{
testShortIDAddrs[0]: importAmount,
},
})
defer func() {
require.NoError(tvm.vm.Shutdown(ctx))
}()
newTxPoolHeadChan := make(chan core.NewTxPoolReorgEvent, 1)
tvm.vm.txPool.SubscribeNewReorgEvent(newTxPoolHeadChan)
// Build a block consuming all of the available gas
var (
txs = make([]*types.Transaction, 2)
err error
)
for i := uint64(0); i < 2; i++ {
tx := types.NewContractCreation(
i,
big.NewInt(0),
lp176.MinMaxCapacity,
big.NewInt(genesisparams.MinGasPrice),
[]byte{0xfe}, // invalid opcode consumes all gas
)
txs[i], err = types.SignTx(tx, types.NewEIP155Signer(tvm.vm.chainID), testKeys[0].ToECDSA())
require.NoError(err)
}
errs := tvm.vm.txPool.AddRemotesSync([]*types.Transaction{txs[0]})
require.Len(errs, 1)
require.NoError(errs[0])
require.Equal(commonEng.PendingTxs, tvm.WaitForEvent(context.Background()))
blk2, err := tvm.vm.BuildBlock(ctx)
require.NoError(err)
require.NoError(blk2.Verify(ctx))
require.NoError(blk2.Accept(ctx))
// Attempt to build a block consuming more than the current gas capacity
errs = tvm.vm.txPool.AddRemotesSync([]*types.Transaction{txs[1]})
require.Len(errs, 1)
require.NoError(errs[0])
require.Equal(commonEng.PendingTxs, tvm.WaitForEvent(context.Background()))
// Expect block building to fail due to insufficient gas capacity
_, err = tvm.vm.BuildBlock(ctx)
require.ErrorIs(err, miner.ErrInsufficientGasCapacityToBuild)
// Wait to fill block capacity and retry block builiding
tvm.vm.clock.Set(tvm.vm.clock.Time().Add(lp176.TimeToFillCapacity * time.Second))
require.Equal(commonEng.PendingTxs, tvm.WaitForEvent(context.Background()))
blk3, err := tvm.vm.BuildBlock(ctx)
require.NoError(err)
require.NoError(blk3.Verify(ctx))
require.NoError(blk3.Accept(ctx))
}
func TestBuildBlockLargeTxStarvation(t *testing.T) {
ctx := context.Background()
require := require.New(t)
importAmount := uint64(2_000_000_000_000_000) // 2M LUX
fork := upgradetest.Fortuna
tvm := newVM(t, testVMConfig{
fork: &fork,
utxos: map[ids.ShortID]uint64{
testShortIDAddrs[0]: importAmount,
testShortIDAddrs[1]: importAmount,
},
})
defer func() {
require.NoError(tvm.vm.Shutdown(ctx))
}()
newTxPoolHeadChan := make(chan core.NewTxPoolReorgEvent, 1)
tvm.vm.txPool.SubscribeNewReorgEvent(newTxPoolHeadChan)
importTx1, err := tvm.atomicVM.NewImportTx(tvm.vm.ctx.XChainID, testEthAddrs[0], initialBaseFee, []*secp256k1.PrivateKey{testKeys[0]})
require.NoError(err)
require.NoError(tvm.atomicVM.AtomicMempool.AddLocalTx(importTx1))
importTx2, err := tvm.atomicVM.NewImportTx(tvm.vm.ctx.XChainID, testEthAddrs[1], initialBaseFee, []*secp256k1.PrivateKey{testKeys[1]})
require.NoError(err)
require.NoError(tvm.atomicVM.AtomicMempool.AddLocalTx(importTx2))
require.Equal(commonEng.PendingTxs, tvm.WaitForEvent(context.Background()))
blk1, err := tvm.vm.BuildBlock(ctx)
require.NoError(err)
require.NoError(blk1.Verify(ctx))
require.NoError(tvm.vm.SetPreference(ctx, blk1.ID()))
require.NoError(blk1.Accept(ctx))
newHead := <-newTxPoolHeadChan
require.Equal(common.Hash(blk1.ID()), newHead.Head.Hash())
// Build a block consuming all of the available gas
var (
highGasPrice = big.NewInt(2 * genesisparams.MinGasPrice)
lowGasPrice = big.NewInt(genesisparams.MinGasPrice)
)
// Refill capacity after distributing funds with import transactions
tvm.vm.clock.Set(tvm.vm.clock.Time().Add(lp176.TimeToFillCapacity * time.Second))
maxSizeTxs := make([]*types.Transaction, 2)
for i := uint64(0); i < 2; i++ {
tx := types.NewContractCreation(
i,
big.NewInt(0),
lp176.MinMaxCapacity,
highGasPrice,
[]byte{0xfe}, // invalid opcode consumes all gas
)
maxSizeTxs[i], err = types.SignTx(tx, types.NewEIP155Signer(tvm.vm.chainID), testKeys[0].ToECDSA())
require.NoError(err)
}
errs := tvm.vm.txPool.AddRemotesSync([]*types.Transaction{maxSizeTxs[0]})
require.Len(errs, 1)
require.NoError(errs[0])
require.Equal(commonEng.PendingTxs, tvm.WaitForEvent(context.Background()))
blk2, err := tvm.vm.BuildBlock(ctx)
require.NoError(err)
require.NoError(blk2.Verify(ctx))
require.NoError(blk2.Accept(ctx))
// Add a second transaction trying to consume the max guaranteed gas capacity at a higher gas price
errs = tvm.vm.txPool.AddRemotesSync([]*types.Transaction{maxSizeTxs[1]})
require.Len(errs, 1)
require.NoError(errs[0])
// Build a smaller transaction that consumes less gas at a lower price. Block building should
// fail and enforce waiting for more capacity to avoid starving the larger transaction.
tx := types.NewContractCreation(0, big.NewInt(0), 2_000_000, lowGasPrice, []byte{0xfe})
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(tvm.vm.chainID), testKeys[1].ToECDSA())
require.NoError(err)
errs = tvm.vm.txPool.AddRemotesSync([]*types.Transaction{signedTx})
require.Len(errs, 1)
require.NoError(errs[0])
require.Equal(commonEng.PendingTxs, tvm.WaitForEvent(context.Background()))
_, err = tvm.vm.BuildBlock(ctx)
require.ErrorIs(err, miner.ErrInsufficientGasCapacityToBuild)
// Wait to fill block capacity and retry block building
tvm.vm.clock.Set(tvm.vm.clock.Time().Add(lp176.TimeToFillCapacity * time.Second))
require.Equal(commonEng.PendingTxs, tvm.WaitForEvent(context.Background()))
blk4, err := tvm.vm.BuildBlock(ctx)
require.NoError(err)
ethBlk4 := blk4.(*chain.BlockWrapper).Block.(*wrappedBlock).ethBlock
actualTxs := ethBlk4.Transactions()
require.Len(actualTxs, 1)
require.Equal(maxSizeTxs[1].Hash(), actualTxs[0].Hash())
require.NoError(blk4.Verify(ctx))
require.NoError(blk4.Accept(ctx))
}
func TestWaitForEvent(t *testing.T) {
populateAtomicMemory := func(t *testing.T, tvm *testVM) (common.Address, *ecdsa.PrivateKey) {
key := testKeys[0].ToECDSA()
address := testEthAddrs[0]
importAmount := uint64(1000000000)
utxoID := lux.UTXOID{TxID: ids.GenerateTestID()}
utxo := &lux.UTXO{
UTXOID: utxoID,
Asset: lux.Asset{ID: tvm.vm.ctx.UTXOAssetID},
Out: &secp256k1fx.TransferOutput{
Amt: importAmount,
OutputOwners: secp256k1fx.OutputOwners{
Threshold: 1,
Addrs: []ids.ShortID{testKeys[0].Address()},
},
},
}
utxoBytes, err := atomic.Codec.Marshal(atomic.CodecVersion, utxo)
require.NoError(t, err)
xChainSharedMemory := tvm.atomicMemory.NewSharedMemory(tvm.vm.ctx.XChainID)
inputID := utxo.InputID()
require.NoError(t, xChainSharedMemory.Apply(map[ids.ID]*luxatomic.Requests{tvm.vm.ctx.ChainID: {PutRequests: []*luxatomic.Element{{
Key: inputID[:],
Value: utxoBytes,
Traits: [][]byte{
testKeys[0].Address().Bytes(),
},
}}}}))
return address, key
}
for _, testCase := range []struct {
name string
testCase func(*testing.T, *VM, *atomicvm.VM, common.Address, *ecdsa.PrivateKey)
}{
{
name: "WaitForEvent with context cancelled returns 0",
testCase: func(t *testing.T, vm *VM, _ *atomicvm.VM, address common.Address, key *ecdsa.PrivateKey) {
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, atomicVM *atomicvm.VM, address common.Address, key *ecdsa.PrivateKey) {
importTx, err := atomicVM.NewImportTx(vm.ctx.XChainID, address, initialBaseFee, []*secp256k1.PrivateKey{testKeys[0]})
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.NoError(t, atomicVM.AtomicMempool.AddLocalTx(importTx))
wg.Wait()
},
},
{
name: "WaitForEvent doesn't return once a block is built and accepted",
testCase: func(t *testing.T, vm *VM, atomicVM *atomicvm.VM, address common.Address, key *ecdsa.PrivateKey) {
importTx, err := atomicVM.NewImportTx(vm.ctx.XChainID, address, initialBaseFee, []*secp256k1.PrivateKey{testKeys[0]})
require.NoError(t, err)
require.NoError(t, atomicVM.AtomicMempool.AddLocalTx(importTx))
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")
txs := make([]*types.Transaction, 10)
for i := 0; i < 10; i++ {
tx := types.NewTransaction(uint64(i), address, big.NewInt(10), 21000, big.NewInt(3*genesisparams.MinGasPrice), nil)
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(vm.chainID), key)
require.NoError(t, err)
txs[i] = signedTx
}
errs := vm.txPool.Add(txs, false, false)
for _, err := range errs {
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, atomicVM *atomicvm.VM, address common.Address, key *ecdsa.PrivateKey) {
importTx, err := atomicVM.NewImportTx(vm.ctx.XChainID, address, initialBaseFee, []*secp256k1.PrivateKey{testKeys[0]})
require.NoError(t, err)
require.NoError(t, atomicVM.AtomicMempool.AddLocalTx(importTx))
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()))
txs := make([]*types.Transaction, 10)
for i := 0; i < 10; i++ {
tx := types.NewTransaction(uint64(i), address, big.NewInt(10), 21000, big.NewInt(3*genesisparams.MinGasPrice), nil)
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(vm.chainID), key)
require.NoError(t, err)
txs[i] = signedTx
}
errs := vm.txPool.Add(txs, false, false)
for _, err := range errs {
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) {
fork := upgradetest.Latest
tvm := newVM(t, testVMConfig{
fork: &fork,
})
address, key := populateAtomicMemory(t, tvm)
testCase.testCase(t, tvm.vm, tvm.atomicVM, address, key)
tvm.vm.Shutdown(context.Background())
})
}
}