Files
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

324 lines
9.6 KiB
Go

// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package atomic
import (
"errors"
"fmt"
"math/big"
"github.com/holiman/uint256"
"github.com/luxfi/coreth/params/extras"
"github.com/luxfi/geth/core/tracing"
consensusctx "github.com/luxfi/consensus/context"
"github.com/luxfi/crypto/secp256k1"
"github.com/luxfi/geth/common"
"github.com/luxfi/ids"
log "github.com/luxfi/log"
"github.com/luxfi/math"
"github.com/luxfi/math/set"
"github.com/luxfi/util"
lux "github.com/luxfi/utxo"
"github.com/luxfi/utxo/secp256k1fx"
"github.com/luxfi/vm/chains/atomic"
"github.com/luxfi/vm/components/verify"
)
var (
_ UnsignedAtomicTx = (*UnsignedImportTx)(nil)
_ secp256k1fx.UnsignedTx = (*UnsignedImportTx)(nil)
ErrImportNonLUXInput = errors.New("import input must be LUX")
ErrImportNonLUXOutput = errors.New("import output must be LUX")
ErrNoImportInputs = errors.New("tx has no imported inputs")
ErrWrongChainID = errors.New("tx has wrong chain ID")
ErrNoEVMOutputs = errors.New("tx has no EVM outputs")
ErrInputsNotSortedUnique = errors.New("inputs not sorted and unique")
ErrOutputsNotSortedUnique = errors.New("outputs not sorted and unique")
ErrOutputsNotSorted = errors.New("tx outputs not sorted")
errMissingBaseFee = errors.New("missing base fee")
errInsufficientFundsForFee = errors.New("insufficient LUX funds to pay transaction fee")
)
// UnsignedImportTx is an unsigned ImportTx
type UnsignedImportTx struct {
Metadata
// ID of the network on which this tx was issued
NetworkID uint32 `serialize:"true" json:"networkID"`
// ID of this blockchain.
BlockchainID ids.ID `serialize:"true" json:"blockchainID"`
// Which chain to consume the funds from
SourceChain ids.ID `serialize:"true" json:"sourceChain"`
// Inputs that consume UTXOs produced on the chain
ImportedInputs []*lux.TransferableInput `serialize:"true" json:"importedInputs"`
// Outputs
Outs []EVMOutput `serialize:"true" json:"outputs"`
}
// InputUTXOs returns the UTXOIDs of the imported funds
func (utx *UnsignedImportTx) InputUTXOs() set.Set[ids.ID] {
set := set.NewSet[ids.ID](len(utx.ImportedInputs))
for _, in := range utx.ImportedInputs {
set.Add(in.InputID())
}
return set
}
// Verify this transaction is well-formed. Under activate-all-implicitly every
// collapse to the strict path.
func (utx *UnsignedImportTx) Verify(
ctx *consensusctx.Context,
_ extras.Rules,
) error {
switch {
case utx == nil:
return ErrNilTx
case len(utx.ImportedInputs) == 0:
return ErrNoImportInputs
case utx.NetworkID != ctx.NetworkID:
return ErrWrongNetworkID
case ctx.ChainID != utx.BlockchainID:
return ErrWrongChainID
case len(utx.Outs) == 0:
return ErrNoEVMOutputs
}
// SameChain verifies that [tx.SourceChain] isn't this chain's ID.
if err := verify.SameChain(ctx.ChainID, utx.SourceChain); err != nil {
return ErrWrongChainID
}
for _, out := range utx.Outs {
if err := out.Verify(); err != nil {
return fmt.Errorf("EVM Output failed verification: %w", err)
}
if out.AssetID != ctx.UTXOAssetID {
return ErrImportNonLUXOutput
}
}
for _, in := range utx.ImportedInputs {
if err := in.Verify(); err != nil {
return fmt.Errorf("atomic input failed verification: %w", err)
}
if in.AssetID() != ctx.UTXOAssetID {
return ErrImportNonLUXInput
}
}
if !utils.IsSortedAndUnique(utx.ImportedInputs) {
return ErrInputsNotSortedUnique
}
if !utils.IsSortedAndUnique(utx.Outs) {
return ErrOutputsNotSortedUnique
}
return nil
}
func (utx *UnsignedImportTx) GasUsed(fixedFee bool) (uint64, error) {
var (
cost = calcBytesCost(len(utx.Bytes()))
err error
)
for _, in := range utx.ImportedInputs {
inCost, err := in.In.Cost()
if err != nil {
return 0, err
}
cost, err = math.Add(cost, inCost)
if err != nil {
return 0, err
}
}
if fixedFee {
cost, err = math.Add(cost, AtomicTxBaseCost)
if err != nil {
return 0, err
}
}
return cost, nil
}
// Amount of [assetID] burned by this transaction
func (utx *UnsignedImportTx) Burned(assetID ids.ID) (uint64, error) {
var (
spent uint64
input uint64
err error
)
for _, out := range utx.Outs {
if out.AssetID == assetID {
spent, err = math.Add(spent, out.Amount)
if err != nil {
return 0, err
}
}
}
for _, in := range utx.ImportedInputs {
if in.AssetID() == assetID {
input, err = math.Add(input, in.Input().Amount())
if err != nil {
return 0, err
}
}
}
return math.Sub(input, spent)
}
// AtomicOps returns imported inputs spent on this transaction
// We spend imported UTXOs here rather than in verification because
// we don't want to remove an imported UTXO in verification
// only to have the transaction not be Accepted. This would be inconsistent.
// Recall that imported UTXOs are not kept in a versionDB.
func (utx *UnsignedImportTx) AtomicOps() (ids.ID, *atomic.Requests, error) {
utxoIDs := make([][]byte, len(utx.ImportedInputs))
for i, in := range utx.ImportedInputs {
inputID := in.InputID()
utxoIDs[i] = inputID[:]
}
return utx.SourceChain, &atomic.Requests{RemoveRequests: utxoIDs}, nil
}
// NewImportTx returns a new ImportTx. Under activate-all-implicitly the
func NewImportTx(
ctx *consensusctx.Context,
rules extras.Rules,
time uint64,
chainID ids.ID, // chain to import from
to common.Address, // Address of recipient
baseFee *big.Int, // fee to use against the dynamic-fee gas cost
kc *secp256k1fx.Keychain, // Keychain to use for signing the atomic UTXOs
atomicUTXOs []*lux.UTXO, // UTXOs to spend
) (*Tx, error) {
importedInputs := []*lux.TransferableInput{}
signers := [][]*secp256k1.PrivateKey{}
importedAmount := make(map[ids.ID]uint64)
for _, utxo := range atomicUTXOs {
inputIntf, utxoSigners, err := kc.Spend(utxo.Out, time)
if err != nil {
continue
}
input, ok := inputIntf.(lux.TransferableIn)
if !ok {
continue
}
aid := utxo.AssetID()
importedAmount[aid], err = math.Add(importedAmount[aid], input.Amount())
if err != nil {
return nil, err
}
importedInputs = append(importedInputs, &lux.TransferableInput{
UTXOID: utxo.UTXOID,
Asset: utxo.Asset,
In: input,
})
signers = append(signers, utxoSigners)
}
lux.SortTransferableInputsWithSigners(importedInputs, signers)
importedLUXAmount := importedAmount[ctx.UTXOAssetID]
outs := make([]EVMOutput, 0, len(importedAmount))
// This will create unique outputs (in the context of sorting)
// since each output will have a unique assetID
for assetID, amount := range importedAmount {
// Skip the LUX amount since it is included separately to account for
// the fee
if assetID == ctx.UTXOAssetID || amount == 0 {
continue
}
outs = append(outs, EVMOutput{
Address: to,
Amount: amount,
AssetID: assetID,
})
}
if baseFee == nil {
return nil, errMissingBaseFee
}
probe := &Tx{UnsignedAtomicTx: &UnsignedImportTx{
NetworkID: ctx.NetworkID,
BlockchainID: ctx.ChainID,
Outs: outs,
ImportedInputs: importedInputs,
SourceChain: chainID,
}}
if err := probe.Sign(nil); err != nil {
return nil, err
}
gasUsedWithoutChange, err := probe.GasUsed(true)
if err != nil {
return nil, err
}
gasUsedWithChange := gasUsedWithoutChange + EVMOutputGas
txFeeWithoutChange, err := CalculateDynamicFee(gasUsedWithoutChange, baseFee)
if err != nil {
return nil, err
}
txFeeWithChange, err := CalculateDynamicFee(gasUsedWithChange, baseFee)
if err != nil {
return nil, err
}
// LUX output
if importedLUXAmount < txFeeWithoutChange { // imported amount goes toward paying tx fee
return nil, errInsufficientFundsForFee
}
if importedLUXAmount > txFeeWithChange {
outs = append(outs, EVMOutput{
Address: to,
Amount: importedLUXAmount - txFeeWithChange,
AssetID: ctx.UTXOAssetID,
})
}
// If no outputs are produced, return an error.
// Note: this can happen if there is exactly enough LUX to pay the
// transaction fee, but no other funds to be imported.
if len(outs) == 0 {
return nil, ErrNoEVMOutputs
}
utils.Sort(outs)
// Create the transaction
utx := &UnsignedImportTx{
NetworkID: ctx.NetworkID,
BlockchainID: ctx.ChainID,
Outs: outs,
ImportedInputs: importedInputs,
SourceChain: chainID,
}
tx := &Tx{UnsignedAtomicTx: utx}
if err := tx.Sign(signers); err != nil {
return nil, err
}
return tx, utx.Verify(ctx, rules)
}
// EVMStateTransfer performs the state transfer to increase the balances of
// accounts accordingly with the imported EVMOutputs
func (utx *UnsignedImportTx) EVMStateTransfer(ctx *consensusctx.Context, state StateDB) error {
for _, to := range utx.Outs {
if to.AssetID == ctx.UTXOAssetID {
log.Debug("import_tx", "src", utx.SourceChain, "addr", to.Address, "amount", to.Amount, "assetID", "LUX")
// If the asset is LUX, convert the input amount in nLUX to gWei by
// multiplying by the x2c rate.
amount := new(uint256.Int).Mul(uint256.NewInt(to.Amount), X2CRate)
state.AddBalance(to.Address, amount, tracing.BalanceChangeTransfer)
} else {
log.Debug("import_tx", "src", utx.SourceChain, "addr", to.Address, "amount", to.Amount, "assetID", to.AssetID)
amount := new(big.Int).SetUint64(to.Amount)
state.AddBalanceMultiCoin(to.Address, common.Hash(to.AssetID), amount)
}
}
return nil
}
func (utx *UnsignedImportTx) Visit(v Visitor) error { return v.ImportTx(utx) }