Files
coreth/plugin/evm/import_tx_test.go
zeekayandHanzo Dev 9d13e82725 Depend on the renamed utility package
github.com/luxfi/utils is now github.com/luxfi/util. Import paths and the
module requirement follow the rename, and the luxutils alias becomes luxutil.
The json types cross the evm client boundary, so this also takes
luxfi/vm v1.3.18, where those API structs already speak the renamed types.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-06 04:29:33 -07:00

1306 lines
37 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: Depends on vm_test.go fixtures (testKeys, testEthAddrs, newVM, etc.)
// which are skipped due to API changes
package evm
import (
"math/big"
"testing"
"github.com/holiman/uint256"
"github.com/luxfi/constants"
"github.com/luxfi/coreth/core/extstate"
"github.com/luxfi/coreth/plugin/evm/atomic"
"github.com/luxfi/coreth/plugin/evm/atomic/vm"
atomicvm "github.com/luxfi/coreth/plugin/evm/atomic/vm"
"github.com/luxfi/coreth/plugin/evm/upgrade/genesisparams"
"github.com/luxfi/coreth/utils"
"github.com/luxfi/geth/common"
consensustest "github.com/luxfi/consensus/test/helpers"
"github.com/luxfi/crypto/secp256k1"
"github.com/luxfi/ids"
"github.com/luxfi/math/set"
"github.com/luxfi/upgrade/upgradetest"
luxutil "github.com/luxfi/util"
lux "github.com/luxfi/utxo"
"github.com/luxfi/utxo/secp256k1fx"
luxatomic "github.com/luxfi/vm/chains/atomic"
)
// createImportTxOptions adds a UTXO to shared memory and generates a list of import transactions sending this UTXO
// to each of the three test keys (conflicting transactions)
func createImportTxOptions(t *testing.T, vm *atomicvm.VM, sharedMemory *luxatomic.Memory) []*atomic.Tx {
utxo := &lux.UTXO{
UTXOID: lux.UTXOID{TxID: ids.GenerateTestID()},
Asset: lux.Asset{ID: vm.Ctx.UTXOAssetID},
Out: &secp256k1fx.TransferOutput{
Amt: uint64(50000000),
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 := sharedMemory.NewSharedMemory(vm.Ctx.XChainID)
inputID := utxo.InputID()
if err := xChainSharedMemory.Apply(map[ids.ID]*luxatomic.Requests{vm.Ctx.ChainID: {PutRequests: []*luxatomic.Element{{
Key: inputID[:],
Value: utxoBytes,
Traits: [][]byte{
testKeys[0].Address().Bytes(),
},
}}}}); err != nil {
t.Fatal(err)
}
importTxs := make([]*atomic.Tx, 0, 3)
for _, ethAddr := range testEthAddrs {
importTx, err := vm.NewImportTx(vm.Ctx.XChainID, ethAddr, initialBaseFee, []*secp256k1.PrivateKey{testKeys[0]})
if err != nil {
t.Fatal(err)
}
importTxs = append(importTxs, importTx)
}
return importTxs
}
func TestImportTxVerify(t *testing.T) {
ctx := consensustest.Context(t, consensustest.CChainID)
var importAmount uint64 = 10000000
txID := ids.GenerateTestID()
importTx := &atomic.UnsignedImportTx{
NetworkID: ctx.NetworkID,
BlockchainID: ctx.ChainID,
SourceChain: ctx.XChainID,
ImportedInputs: []*lux.TransferableInput{
{
UTXOID: lux.UTXOID{
TxID: txID,
OutputIndex: uint32(0),
},
Asset: lux.Asset{ID: ctx.UTXOAssetID},
In: &secp256k1fx.TransferInput{
Amt: importAmount,
Input: secp256k1fx.Input{
SigIndices: []uint32{0},
},
},
},
{
UTXOID: lux.UTXOID{
TxID: txID,
OutputIndex: uint32(1),
},
Asset: lux.Asset{ID: ctx.UTXOAssetID},
In: &secp256k1fx.TransferInput{
Amt: importAmount,
Input: secp256k1fx.Input{
SigIndices: []uint32{0},
},
},
},
},
Outs: []atomic.EVMOutput{
{
Address: testEthAddrs[0],
Amount: importAmount - genesisparams.AtomicTxFee,
AssetID: ctx.UTXOAssetID,
},
{
Address: testEthAddrs[1],
Amount: importAmount,
AssetID: ctx.UTXOAssetID,
},
},
}
// Sort the inputs and outputs to ensure the transaction is canonical
luxutil.Sort(importTx.ImportedInputs)
luxutil.Sort(importTx.Outs)
tests := map[string]atomicTxVerifyTest{
"nil tx": {
generate: func(t *testing.T) atomic.UnsignedAtomicTx {
var importTx *atomic.UnsignedImportTx
return importTx
},
ctx: ctx,
rules: apricotRulesPhase0,
expectedErr: atomic.ErrNilTx.Error(),
},
"valid import tx": {
generate: func(t *testing.T) atomic.UnsignedAtomicTx {
return importTx
},
ctx: ctx,
rules: apricotRulesPhase0,
expectedErr: "", // Expect this transaction to be valid in Apricot Phase 0
},
"valid import tx banff": {
generate: func(t *testing.T) atomic.UnsignedAtomicTx {
return importTx
},
ctx: ctx,
rules: banffRules,
expectedErr: "", // Expect this transaction to be valid in Banff
},
"invalid network ID": {
generate: func(t *testing.T) atomic.UnsignedAtomicTx {
tx := *importTx
tx.NetworkID++
return &tx
},
ctx: ctx,
rules: apricotRulesPhase0,
expectedErr: atomic.ErrWrongNetworkID.Error(),
},
"invalid blockchain ID": {
generate: func(t *testing.T) atomic.UnsignedAtomicTx {
tx := *importTx
tx.BlockchainID = ids.GenerateTestID()
return &tx
},
ctx: ctx,
rules: apricotRulesPhase0,
expectedErr: atomic.ErrWrongChainID.Error(),
},
"P-chain source before AP5": {
generate: func(t *testing.T) atomic.UnsignedAtomicTx {
tx := *importTx
tx.SourceChain = constants.PlatformChainID
return &tx
},
ctx: ctx,
rules: apricotRulesPhase0,
expectedErr: atomic.ErrWrongChainID.Error(),
},
"P-chain source after AP5": {
generate: func(t *testing.T) atomic.UnsignedAtomicTx {
tx := *importTx
tx.SourceChain = constants.PlatformChainID
return &tx
},
ctx: ctx,
rules: apricotRulesPhase5,
},
"invalid source chain ID": {
generate: func(t *testing.T) atomic.UnsignedAtomicTx {
tx := *importTx
tx.SourceChain = ids.GenerateTestID()
return &tx
},
ctx: ctx,
rules: apricotRulesPhase5,
expectedErr: atomic.ErrWrongChainID.Error(),
},
"no inputs": {
generate: func(t *testing.T) atomic.UnsignedAtomicTx {
tx := *importTx
tx.ImportedInputs = nil
return &tx
},
ctx: ctx,
rules: apricotRulesPhase0,
expectedErr: atomic.ErrNoImportInputs.Error(),
},
"inputs sorted incorrectly": {
generate: func(t *testing.T) atomic.UnsignedAtomicTx {
tx := *importTx
tx.ImportedInputs = []*lux.TransferableInput{
tx.ImportedInputs[1],
tx.ImportedInputs[0],
}
return &tx
},
ctx: ctx,
rules: apricotRulesPhase0,
expectedErr: atomic.ErrInputsNotSortedUnique.Error(),
},
"invalid input": {
generate: func(t *testing.T) atomic.UnsignedAtomicTx {
tx := *importTx
tx.ImportedInputs = []*lux.TransferableInput{
tx.ImportedInputs[0],
nil,
}
return &tx
},
ctx: ctx,
rules: apricotRulesPhase0,
expectedErr: "atomic input failed verification",
},
"unsorted outputs phase 0 passes verification": {
generate: func(t *testing.T) atomic.UnsignedAtomicTx {
tx := *importTx
tx.Outs = []atomic.EVMOutput{
tx.Outs[1],
tx.Outs[0],
}
return &tx
},
ctx: ctx,
rules: apricotRulesPhase0,
expectedErr: "",
},
"non-unique outputs phase 0 passes verification": {
generate: func(t *testing.T) atomic.UnsignedAtomicTx {
tx := *importTx
tx.Outs = []atomic.EVMOutput{
tx.Outs[0],
tx.Outs[0],
}
return &tx
},
ctx: ctx,
rules: apricotRulesPhase0,
expectedErr: "",
},
"unsorted outputs phase 1 fails verification": {
generate: func(t *testing.T) atomic.UnsignedAtomicTx {
tx := *importTx
tx.Outs = []atomic.EVMOutput{
tx.Outs[1],
tx.Outs[0],
}
return &tx
},
ctx: ctx,
rules: apricotRulesPhase1,
expectedErr: atomic.ErrOutputsNotSorted.Error(),
},
"non-unique outputs phase 1 passes verification": {
generate: func(t *testing.T) atomic.UnsignedAtomicTx {
tx := *importTx
tx.Outs = []atomic.EVMOutput{
tx.Outs[0],
tx.Outs[0],
}
return &tx
},
ctx: ctx,
rules: apricotRulesPhase1,
expectedErr: "",
},
"outputs not sorted and unique phase 2 fails verification": {
generate: func(t *testing.T) atomic.UnsignedAtomicTx {
tx := *importTx
tx.Outs = []atomic.EVMOutput{
tx.Outs[0],
tx.Outs[0],
}
return &tx
},
ctx: ctx,
rules: apricotRulesPhase2,
expectedErr: atomic.ErrOutputsNotSortedUnique.Error(),
},
"outputs not sorted phase 2 fails verification": {
generate: func(t *testing.T) atomic.UnsignedAtomicTx {
tx := *importTx
tx.Outs = []atomic.EVMOutput{
tx.Outs[1],
tx.Outs[0],
}
return &tx
},
ctx: ctx,
rules: apricotRulesPhase2,
expectedErr: atomic.ErrOutputsNotSortedUnique.Error(),
},
"invalid EVMOutput fails verification": {
generate: func(t *testing.T) atomic.UnsignedAtomicTx {
tx := *importTx
tx.Outs = []atomic.EVMOutput{
{
Address: testEthAddrs[0],
Amount: 0,
AssetID: consensustest.UTXOAssetID,
},
}
return &tx
},
ctx: ctx,
rules: apricotRulesPhase0,
expectedErr: "EVM Output failed verification",
},
"no outputs apricot phase 3": {
generate: func(t *testing.T) atomic.UnsignedAtomicTx {
tx := *importTx
tx.Outs = nil
return &tx
},
ctx: ctx,
rules: apricotRulesPhase3,
expectedErr: atomic.ErrNoEVMOutputs.Error(),
},
"non-LUX input Apricot Phase 6": {
generate: func(t *testing.T) atomic.UnsignedAtomicTx {
tx := *importTx
tx.ImportedInputs = []*lux.TransferableInput{
{
UTXOID: lux.UTXOID{
TxID: txID,
OutputIndex: uint32(0),
},
Asset: lux.Asset{ID: ids.GenerateTestID()},
In: &secp256k1fx.TransferInput{
Amt: importAmount,
Input: secp256k1fx.Input{
SigIndices: []uint32{0},
},
},
},
}
return &tx
},
ctx: ctx,
rules: apricotRulesPhase6,
expectedErr: "",
},
"non-LUX output Apricot Phase 6": {
generate: func(t *testing.T) atomic.UnsignedAtomicTx {
tx := *importTx
tx.Outs = []atomic.EVMOutput{
{
Address: importTx.Outs[0].Address,
Amount: importTx.Outs[0].Amount,
AssetID: ids.GenerateTestID(),
},
}
return &tx
},
ctx: ctx,
rules: apricotRulesPhase6,
expectedErr: "",
},
"non-LUX input Banff": {
generate: func(t *testing.T) atomic.UnsignedAtomicTx {
tx := *importTx
tx.ImportedInputs = []*lux.TransferableInput{
{
UTXOID: lux.UTXOID{
TxID: txID,
OutputIndex: uint32(0),
},
Asset: lux.Asset{ID: ids.GenerateTestID()},
In: &secp256k1fx.TransferInput{
Amt: importAmount,
Input: secp256k1fx.Input{
SigIndices: []uint32{0},
},
},
},
}
return &tx
},
ctx: ctx,
rules: banffRules,
expectedErr: atomic.ErrImportNonLUXInput.Error(),
},
"non-LUX output Banff": {
generate: func(t *testing.T) atomic.UnsignedAtomicTx {
tx := *importTx
tx.Outs = []atomic.EVMOutput{
{
Address: importTx.Outs[0].Address,
Amount: importTx.Outs[0].Amount,
AssetID: ids.GenerateTestID(),
},
}
return &tx
},
ctx: ctx,
rules: banffRules,
expectedErr: atomic.ErrImportNonLUXOutput.Error(),
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
executeTxVerifyTest(t, test)
})
}
}
func TestNewImportTx(t *testing.T) {
importAmount := uint64(5000000)
// createNewImportLUXTx adds a UTXO to shared memory and then constructs a new import transaction
// and checks that it has the correct fee for the base fee that has been used
createNewImportLUXTx := func(t *testing.T, vm *atomicvm.VM, sharedMemory *luxatomic.Memory) *atomic.Tx {
txID := ids.GenerateTestID()
_, err := addUTXO(sharedMemory, vm.Ctx, txID, 0, vm.Ctx.UTXOAssetID, importAmount, testShortIDAddrs[0])
if err != nil {
t.Fatal(err)
}
tx, err := vm.NewImportTx(vm.Ctx.XChainID, testEthAddrs[0], initialBaseFee, []*secp256k1.PrivateKey{testKeys[0]})
if err != nil {
t.Fatal(err)
}
importTx := tx.UnsignedAtomicTx
var actualFee uint64
actualLUXBurned, err := importTx.Burned(vm.Ctx.UTXOAssetID)
if err != nil {
t.Fatal(err)
}
rules := vm.CurrentRules()
switch {
case rules.IsApricotPhase3:
actualCost, err := importTx.GasUsed(rules.IsApricotPhase5)
if err != nil {
t.Fatal(err)
}
actualFee, err = atomic.CalculateDynamicFee(actualCost, initialBaseFee)
if err != nil {
t.Fatal(err)
}
case rules.IsApricotPhase2:
actualFee = 1000000
default:
actualFee = 0
}
if actualLUXBurned != actualFee {
t.Fatalf("LUX burned (%d) != actual fee (%d)", actualLUXBurned, actualFee)
}
return tx
}
checkState := func(t *testing.T, vm *atomicvm.VM) {
txs := vm.LastAcceptedExtendedBlock().GetBlockExtension().(atomic.AtomicBlockContext).AtomicTxs()
if len(txs) != 1 {
t.Fatalf("Expected one import tx to be in the last accepted block, but found %d", len(txs))
}
tx := txs[0]
actualLUXBurned, err := tx.UnsignedAtomicTx.Burned(vm.Ctx.UTXOAssetID)
if err != nil {
t.Fatal(err)
}
// Ensure that the UTXO has been removed from shared memory within Accept
addrSet := set.Set[ids.ShortID]{}
addrSet.Add(testShortIDAddrs[0])
utxos, _, _, err := lux.GetAtomicUTXOs(vm.Ctx.SharedMemory.(luxatomic.SharedMemory), atomic.Codec, vm.Ctx.XChainID, addrSet, ids.ShortEmpty, ids.Empty, 100)
if err != nil {
t.Fatal(err)
}
if len(utxos) != 0 {
t.Fatalf("Expected to find 0 UTXOs after accepting import transaction, but found %d", len(utxos))
}
// Ensure that the call to EVMStateTransfer correctly updates the balance of [addr]
sdb, err := vm.Blockchain().State()
if err != nil {
t.Fatal(err)
}
expectedRemainingBalance := new(uint256.Int).Mul(
uint256.NewInt(importAmount-actualLUXBurned), atomic.X2CRate)
addr := common.Address(testKeys[0].EVMAddress())
if actualBalance := sdb.GetBalance(addr); actualBalance.Cmp(expectedRemainingBalance) != 0 {
t.Fatalf("address remaining balance %s equal %s not %s", addr.String(), actualBalance, expectedRemainingBalance)
}
}
tests2 := map[string]atomicTxTest{
"apricot phase 0": {
setup: createNewImportLUXTx,
checkState: checkState,
fork: upgradetest.NoUpgrades,
},
"apricot phase 1": {
setup: createNewImportLUXTx,
checkState: checkState,
fork: upgradetest.ApricotPhase1,
},
"apricot phase 2": {
setup: createNewImportLUXTx,
checkState: checkState,
fork: upgradetest.ApricotPhase2,
},
"apricot phase 3": {
setup: createNewImportLUXTx,
checkState: checkState,
fork: upgradetest.ApricotPhase3,
},
}
for name, test := range tests2 {
t.Run(name, func(t *testing.T) {
executeTxTest(t, test)
})
}
}
// Note: this is a brittle test to ensure that the gas cost of a transaction does
// not change
func TestImportTxGasCost(t *testing.T) {
utxoAssetID := ids.GenerateTestID()
antAssetID := ids.GenerateTestID()
chainID := ids.GenerateTestID()
xChainID := ids.GenerateTestID()
networkID := uint32(5)
importAmount := uint64(5000000)
tests := map[string]struct {
UnsignedImportTx *atomic.UnsignedImportTx
Keys [][]*secp256k1.PrivateKey
ExpectedGasUsed uint64
ExpectedFee uint64
BaseFee *big.Int
FixedFee bool
}{
"simple import": {
UnsignedImportTx: &atomic.UnsignedImportTx{
NetworkID: networkID,
BlockchainID: chainID,
SourceChain: xChainID,
ImportedInputs: []*lux.TransferableInput{{
UTXOID: lux.UTXOID{TxID: ids.GenerateTestID()},
Asset: lux.Asset{ID: utxoAssetID},
In: &secp256k1fx.TransferInput{
Amt: importAmount,
Input: secp256k1fx.Input{SigIndices: []uint32{0}},
},
}},
Outs: []atomic.EVMOutput{{
Address: testEthAddrs[0],
Amount: importAmount,
AssetID: utxoAssetID,
}},
},
Keys: [][]*secp256k1.PrivateKey{{testKeys[0]}},
ExpectedGasUsed: 1230,
ExpectedFee: 30750,
BaseFee: big.NewInt(25 * utils.GWei),
},
"simple import 1wei": {
UnsignedImportTx: &atomic.UnsignedImportTx{
NetworkID: networkID,
BlockchainID: chainID,
SourceChain: xChainID,
ImportedInputs: []*lux.TransferableInput{{
UTXOID: lux.UTXOID{TxID: ids.GenerateTestID()},
Asset: lux.Asset{ID: utxoAssetID},
In: &secp256k1fx.TransferInput{
Amt: importAmount,
Input: secp256k1fx.Input{SigIndices: []uint32{0}},
},
}},
Outs: []atomic.EVMOutput{{
Address: testEthAddrs[0],
Amount: importAmount,
AssetID: utxoAssetID,
}},
},
Keys: [][]*secp256k1.PrivateKey{{testKeys[0]}},
ExpectedGasUsed: 1230,
ExpectedFee: 1,
BaseFee: big.NewInt(1),
},
"simple import 1wei + fixed fee": {
UnsignedImportTx: &atomic.UnsignedImportTx{
NetworkID: networkID,
BlockchainID: chainID,
SourceChain: xChainID,
ImportedInputs: []*lux.TransferableInput{{
UTXOID: lux.UTXOID{TxID: ids.GenerateTestID()},
Asset: lux.Asset{ID: utxoAssetID},
In: &secp256k1fx.TransferInput{
Amt: importAmount,
Input: secp256k1fx.Input{SigIndices: []uint32{0}},
},
}},
Outs: []atomic.EVMOutput{{
Address: testEthAddrs[0],
Amount: importAmount,
AssetID: utxoAssetID,
}},
},
Keys: [][]*secp256k1.PrivateKey{{testKeys[0]}},
ExpectedGasUsed: 11230,
ExpectedFee: 1,
BaseFee: big.NewInt(1),
FixedFee: true,
},
"simple ANT import": {
UnsignedImportTx: &atomic.UnsignedImportTx{
NetworkID: networkID,
BlockchainID: chainID,
SourceChain: xChainID,
ImportedInputs: []*lux.TransferableInput{
{
UTXOID: lux.UTXOID{TxID: ids.GenerateTestID()},
Asset: lux.Asset{ID: utxoAssetID},
In: &secp256k1fx.TransferInput{
Amt: importAmount,
Input: secp256k1fx.Input{SigIndices: []uint32{0}},
},
},
{
UTXOID: lux.UTXOID{TxID: ids.GenerateTestID()},
Asset: lux.Asset{ID: antAssetID},
In: &secp256k1fx.TransferInput{
Amt: importAmount,
Input: secp256k1fx.Input{SigIndices: []uint32{0}},
},
},
},
Outs: []atomic.EVMOutput{
{
Address: testEthAddrs[0],
Amount: importAmount,
AssetID: antAssetID,
},
},
},
Keys: [][]*secp256k1.PrivateKey{{testKeys[0]}, {testKeys[0]}},
ExpectedGasUsed: 2318,
ExpectedFee: 57950,
BaseFee: big.NewInt(25 * utils.GWei),
},
"complex ANT import": {
UnsignedImportTx: &atomic.UnsignedImportTx{
NetworkID: networkID,
BlockchainID: chainID,
SourceChain: xChainID,
ImportedInputs: []*lux.TransferableInput{
{
UTXOID: lux.UTXOID{TxID: ids.GenerateTestID()},
Asset: lux.Asset{ID: utxoAssetID},
In: &secp256k1fx.TransferInput{
Amt: importAmount,
Input: secp256k1fx.Input{SigIndices: []uint32{0}},
},
},
{
UTXOID: lux.UTXOID{TxID: ids.GenerateTestID()},
Asset: lux.Asset{ID: antAssetID},
In: &secp256k1fx.TransferInput{
Amt: importAmount,
Input: secp256k1fx.Input{SigIndices: []uint32{0}},
},
},
},
Outs: []atomic.EVMOutput{
{
Address: testEthAddrs[0],
Amount: importAmount,
AssetID: utxoAssetID,
},
{
Address: testEthAddrs[0],
Amount: importAmount,
AssetID: antAssetID,
},
},
},
Keys: [][]*secp256k1.PrivateKey{{testKeys[0]}, {testKeys[0]}},
ExpectedGasUsed: 2378,
ExpectedFee: 59450,
BaseFee: big.NewInt(25 * utils.GWei),
},
"multisig import": {
UnsignedImportTx: &atomic.UnsignedImportTx{
NetworkID: networkID,
BlockchainID: chainID,
SourceChain: xChainID,
ImportedInputs: []*lux.TransferableInput{{
UTXOID: lux.UTXOID{TxID: ids.GenerateTestID()},
Asset: lux.Asset{ID: utxoAssetID},
In: &secp256k1fx.TransferInput{
Amt: importAmount,
Input: secp256k1fx.Input{SigIndices: []uint32{0, 1}},
},
}},
Outs: []atomic.EVMOutput{{
Address: testEthAddrs[0],
Amount: importAmount,
AssetID: utxoAssetID,
}},
},
Keys: [][]*secp256k1.PrivateKey{{testKeys[0], testKeys[1]}},
ExpectedGasUsed: 2234,
ExpectedFee: 55850,
BaseFee: big.NewInt(25 * utils.GWei),
},
"large import": {
UnsignedImportTx: &atomic.UnsignedImportTx{
NetworkID: networkID,
BlockchainID: chainID,
SourceChain: xChainID,
ImportedInputs: []*lux.TransferableInput{
{
UTXOID: lux.UTXOID{TxID: ids.GenerateTestID()},
Asset: lux.Asset{ID: utxoAssetID},
In: &secp256k1fx.TransferInput{
Amt: importAmount,
Input: secp256k1fx.Input{SigIndices: []uint32{0}},
},
},
{
UTXOID: lux.UTXOID{TxID: ids.GenerateTestID()},
Asset: lux.Asset{ID: utxoAssetID},
In: &secp256k1fx.TransferInput{
Amt: importAmount,
Input: secp256k1fx.Input{SigIndices: []uint32{0}},
},
},
{
UTXOID: lux.UTXOID{TxID: ids.GenerateTestID()},
Asset: lux.Asset{ID: utxoAssetID},
In: &secp256k1fx.TransferInput{
Amt: importAmount,
Input: secp256k1fx.Input{SigIndices: []uint32{0}},
},
},
{
UTXOID: lux.UTXOID{TxID: ids.GenerateTestID()},
Asset: lux.Asset{ID: utxoAssetID},
In: &secp256k1fx.TransferInput{
Amt: importAmount,
Input: secp256k1fx.Input{SigIndices: []uint32{0}},
},
},
{
UTXOID: lux.UTXOID{TxID: ids.GenerateTestID()},
Asset: lux.Asset{ID: utxoAssetID},
In: &secp256k1fx.TransferInput{
Amt: importAmount,
Input: secp256k1fx.Input{SigIndices: []uint32{0}},
},
},
{
UTXOID: lux.UTXOID{TxID: ids.GenerateTestID()},
Asset: lux.Asset{ID: utxoAssetID},
In: &secp256k1fx.TransferInput{
Amt: importAmount,
Input: secp256k1fx.Input{SigIndices: []uint32{0}},
},
},
{
UTXOID: lux.UTXOID{TxID: ids.GenerateTestID()},
Asset: lux.Asset{ID: utxoAssetID},
In: &secp256k1fx.TransferInput{
Amt: importAmount,
Input: secp256k1fx.Input{SigIndices: []uint32{0}},
},
},
{
UTXOID: lux.UTXOID{TxID: ids.GenerateTestID()},
Asset: lux.Asset{ID: utxoAssetID},
In: &secp256k1fx.TransferInput{
Amt: importAmount,
Input: secp256k1fx.Input{SigIndices: []uint32{0}},
},
},
{
UTXOID: lux.UTXOID{TxID: ids.GenerateTestID()},
Asset: lux.Asset{ID: utxoAssetID},
In: &secp256k1fx.TransferInput{
Amt: importAmount,
Input: secp256k1fx.Input{SigIndices: []uint32{0}},
},
},
{
UTXOID: lux.UTXOID{TxID: ids.GenerateTestID()},
Asset: lux.Asset{ID: utxoAssetID},
In: &secp256k1fx.TransferInput{
Amt: importAmount,
Input: secp256k1fx.Input{SigIndices: []uint32{0}},
},
},
},
Outs: []atomic.EVMOutput{
{
Address: testEthAddrs[0],
Amount: importAmount * 10,
AssetID: utxoAssetID,
},
},
},
Keys: [][]*secp256k1.PrivateKey{
{testKeys[0]},
{testKeys[0]},
{testKeys[0]},
{testKeys[0]},
{testKeys[0]},
{testKeys[0]},
{testKeys[0]},
{testKeys[0]},
{testKeys[0]},
{testKeys[0]},
},
ExpectedGasUsed: 11022,
ExpectedFee: 275550,
BaseFee: big.NewInt(25 * utils.GWei),
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
tx := &atomic.Tx{UnsignedAtomicTx: test.UnsignedImportTx}
// Sign with the correct key
if err := tx.Sign(atomic.Codec, test.Keys); err != nil {
t.Fatal(err)
}
gasUsed, err := tx.GasUsed(test.FixedFee)
if err != nil {
t.Fatal(err)
}
if gasUsed != test.ExpectedGasUsed {
t.Fatalf("Expected gasUsed to be %d, but found %d", test.ExpectedGasUsed, gasUsed)
}
fee, err := atomic.CalculateDynamicFee(gasUsed, test.BaseFee)
if err != nil {
t.Fatal(err)
}
if fee != test.ExpectedFee {
t.Fatalf("Expected fee to be %d, but found %d", test.ExpectedFee, fee)
}
})
}
}
func TestImportTxSemanticVerify(t *testing.T) {
tests := map[string]atomicTxTest{
"UTXO not present during bootstrapping": {
setup: func(t *testing.T, vm *atomicvm.VM, sharedMemory *luxatomic.Memory) *atomic.Tx {
tx := &atomic.Tx{UnsignedAtomicTx: &atomic.UnsignedImportTx{
NetworkID: vm.Ctx.NetworkID,
BlockchainID: vm.Ctx.ChainID,
SourceChain: vm.Ctx.XChainID,
ImportedInputs: []*lux.TransferableInput{{
UTXOID: lux.UTXOID{
TxID: ids.GenerateTestID(),
},
Asset: lux.Asset{ID: vm.Ctx.UTXOAssetID},
In: &secp256k1fx.TransferInput{
Amt: 1,
Input: secp256k1fx.Input{SigIndices: []uint32{0}},
},
}},
Outs: []atomic.EVMOutput{{
Address: testEthAddrs[0],
Amount: 1,
AssetID: vm.Ctx.UTXOAssetID,
}},
}}
if err := tx.Sign(atomic.Codec, [][]*secp256k1.PrivateKey{{testKeys[0]}}); err != nil {
t.Fatal(err)
}
return tx
},
bootstrapping: true,
},
"UTXO not present": {
setup: func(t *testing.T, vm *atomicvm.VM, sharedMemory *luxatomic.Memory) *atomic.Tx {
tx := &atomic.Tx{UnsignedAtomicTx: &atomic.UnsignedImportTx{
NetworkID: vm.Ctx.NetworkID,
BlockchainID: vm.Ctx.ChainID,
SourceChain: vm.Ctx.XChainID,
ImportedInputs: []*lux.TransferableInput{{
UTXOID: lux.UTXOID{
TxID: ids.GenerateTestID(),
},
Asset: lux.Asset{ID: vm.Ctx.UTXOAssetID},
In: &secp256k1fx.TransferInput{
Amt: 1,
Input: secp256k1fx.Input{SigIndices: []uint32{0}},
},
}},
Outs: []atomic.EVMOutput{{
Address: testEthAddrs[0],
Amount: 1,
AssetID: vm.Ctx.UTXOAssetID,
}},
}}
if err := tx.Sign(atomic.Codec, [][]*secp256k1.PrivateKey{{testKeys[0]}}); err != nil {
t.Fatal(err)
}
return tx
},
semanticVerifyErr: "failed to fetch import UTXOs from",
},
"garbage UTXO": {
setup: func(t *testing.T, vm *atomicvm.VM, sharedMemory *luxatomic.Memory) *atomic.Tx {
utxoID := lux.UTXOID{TxID: ids.GenerateTestID()}
xChainSharedMemory := sharedMemory.NewSharedMemory(vm.Ctx.XChainID)
inputID := utxoID.InputID()
if err := xChainSharedMemory.Apply(map[ids.ID]*luxatomic.Requests{vm.Ctx.ChainID: {PutRequests: []*luxatomic.Element{{
Key: inputID[:],
Value: []byte("hey there"),
Traits: [][]byte{
testShortIDAddrs[0].Bytes(),
},
}}}}); err != nil {
t.Fatal(err)
}
tx := &atomic.Tx{UnsignedAtomicTx: &atomic.UnsignedImportTx{
NetworkID: vm.Ctx.NetworkID,
BlockchainID: vm.Ctx.ChainID,
SourceChain: vm.Ctx.XChainID,
ImportedInputs: []*lux.TransferableInput{{
UTXOID: utxoID,
Asset: lux.Asset{ID: vm.Ctx.UTXOAssetID},
In: &secp256k1fx.TransferInput{
Amt: 1,
Input: secp256k1fx.Input{SigIndices: []uint32{0}},
},
}},
Outs: []atomic.EVMOutput{{
Address: testEthAddrs[0],
Amount: 1,
AssetID: vm.Ctx.UTXOAssetID,
}},
}}
if err := tx.Sign(atomic.Codec, [][]*secp256k1.PrivateKey{{testKeys[0]}}); err != nil {
t.Fatal(err)
}
return tx
},
semanticVerifyErr: "failed to unmarshal UTXO",
},
"UTXO AssetID mismatch": {
setup: func(t *testing.T, vm *atomicvm.VM, sharedMemory *luxatomic.Memory) *atomic.Tx {
txID := ids.GenerateTestID()
expectedAssetID := ids.GenerateTestID()
utxo, err := addUTXO(sharedMemory, vm.Ctx, txID, 0, expectedAssetID, 1, testShortIDAddrs[0])
if err != nil {
t.Fatal(err)
}
tx := &atomic.Tx{UnsignedAtomicTx: &atomic.UnsignedImportTx{
NetworkID: vm.Ctx.NetworkID,
BlockchainID: vm.Ctx.ChainID,
SourceChain: vm.Ctx.XChainID,
ImportedInputs: []*lux.TransferableInput{{
UTXOID: utxo.UTXOID,
Asset: lux.Asset{ID: vm.Ctx.UTXOAssetID}, // Use a different assetID then the actual UTXO
In: &secp256k1fx.TransferInput{
Amt: 1,
Input: secp256k1fx.Input{SigIndices: []uint32{0}},
},
}},
Outs: []atomic.EVMOutput{{
Address: testEthAddrs[0],
Amount: 1,
AssetID: vm.Ctx.UTXOAssetID,
}},
}}
if err := tx.Sign(atomic.Codec, [][]*secp256k1.PrivateKey{{testKeys[0]}}); err != nil {
t.Fatal(err)
}
return tx
},
semanticVerifyErr: vm.ErrAssetIDMismatch.Error(),
},
"insufficient LUX funds": {
setup: func(t *testing.T, vm *atomicvm.VM, sharedMemory *luxatomic.Memory) *atomic.Tx {
txID := ids.GenerateTestID()
utxo, err := addUTXO(sharedMemory, vm.Ctx, txID, 0, vm.Ctx.UTXOAssetID, 1, testShortIDAddrs[0])
if err != nil {
t.Fatal(err)
}
tx := &atomic.Tx{UnsignedAtomicTx: &atomic.UnsignedImportTx{
NetworkID: vm.Ctx.NetworkID,
BlockchainID: vm.Ctx.ChainID,
SourceChain: vm.Ctx.XChainID,
ImportedInputs: []*lux.TransferableInput{{
UTXOID: utxo.UTXOID,
Asset: lux.Asset{ID: vm.Ctx.UTXOAssetID},
In: &secp256k1fx.TransferInput{
Amt: 1,
Input: secp256k1fx.Input{SigIndices: []uint32{0}},
},
}},
Outs: []atomic.EVMOutput{{
Address: testEthAddrs[0],
Amount: 2, // Produce more output than is consumed by the transaction
AssetID: vm.Ctx.UTXOAssetID,
}},
}}
if err := tx.Sign(atomic.Codec, [][]*secp256k1.PrivateKey{{testKeys[0]}}); err != nil {
t.Fatal(err)
}
return tx
},
semanticVerifyErr: "import tx flow check failed due to",
},
"insufficient non-LUX funds": {
setup: func(t *testing.T, vm *atomicvm.VM, sharedMemory *luxatomic.Memory) *atomic.Tx {
txID := ids.GenerateTestID()
assetID := ids.GenerateTestID()
utxo, err := addUTXO(sharedMemory, vm.Ctx, txID, 0, assetID, 1, testShortIDAddrs[0])
if err != nil {
t.Fatal(err)
}
tx := &atomic.Tx{UnsignedAtomicTx: &atomic.UnsignedImportTx{
NetworkID: vm.Ctx.NetworkID,
BlockchainID: vm.Ctx.ChainID,
SourceChain: vm.Ctx.XChainID,
ImportedInputs: []*lux.TransferableInput{{
UTXOID: utxo.UTXOID,
Asset: lux.Asset{ID: assetID},
In: &secp256k1fx.TransferInput{
Amt: 1,
Input: secp256k1fx.Input{SigIndices: []uint32{0}},
},
}},
Outs: []atomic.EVMOutput{{
Address: testEthAddrs[0],
Amount: 2, // Produce more output than is consumed by the transaction
AssetID: assetID,
}},
}}
if err := tx.Sign(atomic.Codec, [][]*secp256k1.PrivateKey{{testKeys[0]}}); err != nil {
t.Fatal(err)
}
return tx
},
semanticVerifyErr: "import tx flow check failed due to",
},
"no signatures": {
setup: func(t *testing.T, vm *atomicvm.VM, sharedMemory *luxatomic.Memory) *atomic.Tx {
txID := ids.GenerateTestID()
utxo, err := addUTXO(sharedMemory, vm.Ctx, txID, 0, vm.Ctx.UTXOAssetID, 1, testShortIDAddrs[0])
if err != nil {
t.Fatal(err)
}
tx := &atomic.Tx{UnsignedAtomicTx: &atomic.UnsignedImportTx{
NetworkID: vm.Ctx.NetworkID,
BlockchainID: vm.Ctx.ChainID,
SourceChain: vm.Ctx.XChainID,
ImportedInputs: []*lux.TransferableInput{{
UTXOID: utxo.UTXOID,
Asset: lux.Asset{ID: vm.Ctx.UTXOAssetID},
In: &secp256k1fx.TransferInput{
Amt: 1,
Input: secp256k1fx.Input{SigIndices: []uint32{0}},
},
}},
Outs: []atomic.EVMOutput{{
Address: testEthAddrs[0],
Amount: 1,
AssetID: vm.Ctx.UTXOAssetID,
}},
}}
if err := tx.Sign(atomic.Codec, nil); err != nil {
t.Fatal(err)
}
return tx
},
semanticVerifyErr: "import tx contained mismatched number of inputs/credentials",
},
"incorrect signature": {
setup: func(t *testing.T, vm *atomicvm.VM, sharedMemory *luxatomic.Memory) *atomic.Tx {
txID := ids.GenerateTestID()
utxo, err := addUTXO(sharedMemory, vm.Ctx, txID, 0, vm.Ctx.UTXOAssetID, 1, testShortIDAddrs[0])
if err != nil {
t.Fatal(err)
}
tx := &atomic.Tx{UnsignedAtomicTx: &atomic.UnsignedImportTx{
NetworkID: vm.Ctx.NetworkID,
BlockchainID: vm.Ctx.ChainID,
SourceChain: vm.Ctx.XChainID,
ImportedInputs: []*lux.TransferableInput{{
UTXOID: utxo.UTXOID,
Asset: lux.Asset{ID: vm.Ctx.UTXOAssetID},
In: &secp256k1fx.TransferInput{
Amt: 1,
Input: secp256k1fx.Input{SigIndices: []uint32{0}},
},
}},
Outs: []atomic.EVMOutput{{
Address: testEthAddrs[0],
Amount: 1,
AssetID: vm.Ctx.UTXOAssetID,
}},
}}
// Sign the transaction with the incorrect key
if err := tx.Sign(atomic.Codec, [][]*secp256k1.PrivateKey{{testKeys[1]}}); err != nil {
t.Fatal(err)
}
return tx
},
semanticVerifyErr: "import tx transfer failed verification",
},
"non-unique EVM Outputs": {
setup: func(t *testing.T, vm *atomicvm.VM, sharedMemory *luxatomic.Memory) *atomic.Tx {
txID := ids.GenerateTestID()
utxo, err := addUTXO(sharedMemory, vm.Ctx, txID, 0, vm.Ctx.UTXOAssetID, 2, testShortIDAddrs[0])
if err != nil {
t.Fatal(err)
}
tx := &atomic.Tx{UnsignedAtomicTx: &atomic.UnsignedImportTx{
NetworkID: vm.Ctx.NetworkID,
BlockchainID: vm.Ctx.ChainID,
SourceChain: vm.Ctx.XChainID,
ImportedInputs: []*lux.TransferableInput{{
UTXOID: utxo.UTXOID,
Asset: lux.Asset{ID: vm.Ctx.UTXOAssetID},
In: &secp256k1fx.TransferInput{
Amt: 2,
Input: secp256k1fx.Input{SigIndices: []uint32{0}},
},
}},
Outs: []atomic.EVMOutput{
{
Address: testEthAddrs[0],
Amount: 1,
AssetID: vm.Ctx.UTXOAssetID,
},
{
Address: testEthAddrs[0],
Amount: 1,
AssetID: vm.Ctx.UTXOAssetID,
},
},
}}
if err := tx.Sign(atomic.Codec, [][]*secp256k1.PrivateKey{{testKeys[0]}}); err != nil {
t.Fatal(err)
}
return tx
},
fork: upgradetest.ApricotPhase3,
semanticVerifyErr: atomic.ErrOutputsNotSortedUnique.Error(),
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
executeTxTest(t, test)
})
}
}
func TestImportTxEVMStateTransfer(t *testing.T) {
assetID := ids.GenerateTestID()
tests := map[string]atomicTxTest{
"LUX UTXO": {
setup: func(t *testing.T, vm *atomicvm.VM, sharedMemory *luxatomic.Memory) *atomic.Tx {
txID := ids.GenerateTestID()
utxo, err := addUTXO(sharedMemory, vm.Ctx, txID, 0, vm.Ctx.UTXOAssetID, 1, testShortIDAddrs[0])
if err != nil {
t.Fatal(err)
}
tx := &atomic.Tx{UnsignedAtomicTx: &atomic.UnsignedImportTx{
NetworkID: vm.Ctx.NetworkID,
BlockchainID: vm.Ctx.ChainID,
SourceChain: vm.Ctx.XChainID,
ImportedInputs: []*lux.TransferableInput{{
UTXOID: utxo.UTXOID,
Asset: lux.Asset{ID: vm.Ctx.UTXOAssetID},
In: &secp256k1fx.TransferInput{
Amt: 1,
Input: secp256k1fx.Input{SigIndices: []uint32{0}},
},
}},
Outs: []atomic.EVMOutput{{
Address: testEthAddrs[0],
Amount: 1,
AssetID: vm.Ctx.UTXOAssetID,
}},
}}
if err := tx.Sign(atomic.Codec, [][]*secp256k1.PrivateKey{{testKeys[0]}}); err != nil {
t.Fatal(err)
}
return tx
},
checkState: func(t *testing.T, vm *atomicvm.VM) {
lastAcceptedBlock := vm.LastAcceptedExtendedBlock()
sdb, err := vm.Blockchain().StateAt(lastAcceptedBlock.GetEthBlock().Root())
if err != nil {
t.Fatal(err)
}
luxBalance := sdb.GetBalance(testEthAddrs[0])
if luxBalance.Cmp(atomic.X2CRate) != 0 {
t.Fatalf("Expected LUX balance to be %d, found balance: %d", *atomic.X2CRate, luxBalance)
}
},
},
"non-LUX UTXO": {
setup: func(t *testing.T, vm *atomicvm.VM, sharedMemory *luxatomic.Memory) *atomic.Tx {
txID := ids.GenerateTestID()
utxo, err := addUTXO(sharedMemory, vm.Ctx, txID, 0, assetID, 1, testShortIDAddrs[0])
if err != nil {
t.Fatal(err)
}
tx := &atomic.Tx{UnsignedAtomicTx: &atomic.UnsignedImportTx{
NetworkID: vm.Ctx.NetworkID,
BlockchainID: vm.Ctx.ChainID,
SourceChain: vm.Ctx.XChainID,
ImportedInputs: []*lux.TransferableInput{{
UTXOID: utxo.UTXOID,
Asset: lux.Asset{ID: assetID},
In: &secp256k1fx.TransferInput{
Amt: 1,
Input: secp256k1fx.Input{SigIndices: []uint32{0}},
},
}},
Outs: []atomic.EVMOutput{{
Address: testEthAddrs[0],
Amount: 1,
AssetID: assetID,
}},
}}
if err := tx.Sign(atomic.Codec, [][]*secp256k1.PrivateKey{{testKeys[0]}}); err != nil {
t.Fatal(err)
}
return tx
},
checkState: func(t *testing.T, vm *atomicvm.VM) {
lastAcceptedBlock := vm.LastAcceptedExtendedBlock()
statedb, err := vm.Blockchain().StateAt(lastAcceptedBlock.GetEthBlock().Root())
if err != nil {
t.Fatal(err)
}
wrappedStateDB := extstate.New(statedb)
assetBalance := wrappedStateDB.GetBalanceMultiCoin(testEthAddrs[0], common.Hash(assetID))
if assetBalance.Cmp(common.Big1) != 0 {
t.Fatalf("Expected asset balance to be %d, found balance: %d", common.Big1, assetBalance)
}
luxBalance := wrappedStateDB.GetBalance(testEthAddrs[0])
if luxBalance.Cmp(common.U2560) != 0 {
t.Fatalf("Expected LUX balance to be 0, found balance: %d", luxBalance)
}
},
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
executeTxTest(t, test)
})
}
}