Files
Antje WorringandHanzo Dev c872eb3cd0 style(coreth): rename local luxAssetID -> utxoAssetID for naming consistency
Package-local identifier only; no exported field, json tag, or wire change.

Co-authored-by: Hanzo Dev <[email protected]>
2026-06-06 23:13:16 -07:00

338 lines
11 KiB
Go

// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package atomic
import (
"bytes"
"errors"
"fmt"
"math/big"
"sort"
"github.com/holiman/uint256"
"github.com/luxfi/geth/common"
"github.com/luxfi/geth/core/tracing"
"github.com/luxfi/coreth/params/extras"
consensusctx "github.com/luxfi/consensus/context"
"github.com/luxfi/crypto/hash"
"github.com/luxfi/crypto/secp256k1"
"github.com/luxfi/ids"
luxfiids "github.com/luxfi/ids"
"github.com/luxfi/math/set"
"github.com/luxfi/p2p/gossip"
"github.com/luxfi/utils/wrappers"
"github.com/luxfi/utxo/secp256k1fx"
"github.com/luxfi/vm/chains/atomic"
"github.com/luxfi/vm/components/verify"
)
var _ gossip.Gossipable = (*Tx)(nil)
const (
X2CRateUint64 uint64 = 1_000_000_000
x2cRateMinus1Uint64 uint64 = X2CRateUint64 - 1
)
var (
ErrWrongNetworkID = errors.New("tx was issued with a different network ID")
ErrNilTx = errors.New("tx is nil")
errNoValueOutput = errors.New("output has no value")
ErrNoValueInput = errors.New("input has no value")
errNilOutput = errors.New("nil output")
errNilInput = errors.New("nil input")
errEmptyAssetID = errors.New("empty asset ID is not valid")
errNilBaseFee = errors.New("cannot calculate dynamic fee with nil baseFee")
errFeeOverflow = errors.New("overflow occurred while calculating the fee")
)
// Constants for calculating the gas consumed by atomic transactions
var (
TxBytesGas uint64 = 1
EVMOutputGas uint64 = (common.AddressLength + wrappers.LongLen + hash.HashLen) * TxBytesGas
EVMInputGas uint64 = (common.AddressLength+wrappers.LongLen+hash.HashLen+wrappers.LongLen)*TxBytesGas + secp256k1fx.CostPerSignature
// X2CRate is the conversion rate between the smallest denomination on the X-Chain
// 1 nLUX and the smallest denomination on the C-Chain 1 wei. Where 1 nLUX = 1 gWei.
// This is only required for LUX because the denomination of 1 LUX is 9 decimal
// places on the X and P chains, but is 18 decimal places within the EVM.
X2CRate = uint256.NewInt(X2CRateUint64)
x2cRateMinus1 = uint256.NewInt(x2cRateMinus1Uint64)
)
// EVMOutput defines an output that is added to the EVM state created by import transactions
type EVMOutput struct {
Address common.Address `serialize:"true" json:"address"`
Amount uint64 `serialize:"true" json:"amount"`
AssetID ids.ID `serialize:"true" json:"assetID"`
}
func (o EVMOutput) Compare(other EVMOutput) int {
addrComp := bytes.Compare(o.Address.Bytes(), other.Address.Bytes())
if addrComp != 0 {
return addrComp
}
return bytes.Compare(o.AssetID[:], other.AssetID[:])
}
// EVMInput defines an input created from the EVM state to fund export transactions
type EVMInput struct {
Address common.Address `serialize:"true" json:"address"`
Amount uint64 `serialize:"true" json:"amount"`
AssetID ids.ID `serialize:"true" json:"assetID"`
Nonce uint64 `serialize:"true" json:"nonce"`
}
func (i EVMInput) Compare(other EVMInput) int {
addrComp := bytes.Compare(i.Address.Bytes(), other.Address.Bytes())
if addrComp != 0 {
return addrComp
}
return bytes.Compare(i.AssetID[:], other.AssetID[:])
}
// Verify ...
func (o *EVMOutput) Verify() error {
switch {
case o == nil:
return errNilOutput
case o.Amount == 0:
return errNoValueOutput
case o.AssetID == ids.Empty:
return errEmptyAssetID
}
return nil
}
// Verify ...
func (i *EVMInput) Verify() error {
switch {
case i == nil:
return errNilInput
case i.Amount == 0:
return ErrNoValueInput
case i.AssetID == ids.Empty:
return errEmptyAssetID
}
return nil
}
type AtomicBlockContext interface {
AtomicTxs() []*Tx
}
// Visitor allows executing custom logic against the underlying transaction types.
type Visitor interface {
ImportTx(*UnsignedImportTx) error
ExportTx(*UnsignedExportTx) error
}
// UnsignedTx is an unsigned transaction
type UnsignedTx interface {
Initialize(unsignedBytes, signedBytes []byte)
ID() ids.ID
GasUsed(fixedFee bool) (uint64, error)
Burned(assetID ids.ID) (uint64, error)
Bytes() []byte
SignedBytes() []byte
}
type StateDB interface {
AddBalance(common.Address, *uint256.Int, tracing.BalanceChangeReason) uint256.Int
AddBalanceMultiCoin(common.Address, common.Hash, *big.Int)
SubBalance(common.Address, *uint256.Int, tracing.BalanceChangeReason) uint256.Int
SubBalanceMultiCoin(common.Address, common.Hash, *big.Int)
GetBalance(common.Address) *uint256.Int
GetBalanceMultiCoin(common.Address, common.Hash) *big.Int
GetNonce(common.Address) uint64
SetNonce(common.Address, uint64, tracing.NonceChangeReason)
}
// UnsignedAtomicTx is an unsigned operation that can be atomically accepted
type UnsignedAtomicTx interface {
UnsignedTx
// InputUTXOs returns the UTXOs this tx consumes
InputUTXOs() set.Set[ids.ID]
// Verify attempts to verify that the transaction is well formed
Verify(ctx *consensusctx.Context, rules extras.Rules) error
// Visit calls the corresponding method for the underlying transaction type
// implementing [Visitor].
// This is used in semantic verification of the tx.
Visit(v Visitor) error
// AtomicOps returns the blockchainID and set of atomic requests that
// must be applied to shared memory for this transaction to be accepted.
// The set of atomic requests must be returned in a consistent order.
AtomicOps() (ids.ID, *atomic.Requests, error)
EVMStateTransfer(ctx *consensusctx.Context, state StateDB) error
}
// Tx is a signed transaction
type Tx struct {
// The body of this transaction
UnsignedAtomicTx `serialize:"true" json:"unsignedTx"`
// The credentials of this transaction
Creds []verify.Verifiable `serialize:"true" json:"credentials"`
}
func (tx *Tx) Compare(other *Tx) int {
txHex := tx.ID().Hex()
otherHex := other.ID().Hex()
switch {
case txHex < otherHex:
return -1
case txHex > otherHex:
return 1
default:
return 0
}
}
// Sign this transaction with the provided signers. The atomic-tx package
// owns its own wire format via [Codec]; callers no longer pass a codec
// manager in. Pass nil signers to compute and stash the unsigned/signed
// byte forms without adding any new credentials.
func (tx *Tx) Sign(signers [][]*secp256k1.PrivateKey) error {
unsignedBytes, err := Codec.Marshal(CodecVersion, &tx.UnsignedAtomicTx)
if err != nil {
return fmt.Errorf("couldn't marshal UnsignedAtomicTx: %w", err)
}
// Attach credentials
hash := hash.ComputeHash256(unsignedBytes)
for _, keys := range signers {
cred := &secp256k1fx.Credential{
Sigs: make([][secp256k1.SignatureLen]byte, len(keys)),
}
for i, key := range keys {
sig, err := key.SignHash(hash) // Sign hash
if err != nil {
return fmt.Errorf("problem generating credential: %w", err)
}
copy(cred.Sigs[i][:], sig)
}
tx.Creds = append(tx.Creds, cred) // Attach credential
}
signedBytes, err := Codec.Marshal(CodecVersion, tx)
if err != nil {
return fmt.Errorf("couldn't marshal Tx: %w", err)
}
tx.Initialize(unsignedBytes, signedBytes)
return nil
}
// initializeBytes re-marshals [tx] via [c] to materialize the unsigned and
// signed byte forms stashed in Metadata. Used by [ExtractAtomicTx] and
// [ExtractAtomicTxsBatch] after Unmarshal to populate ID(), Bytes() and
// SignedBytes() for the round-tripped tx. The manager is parameterized so
// the test codec (atomictest.TestTxCodec) can round-trip its own unsigned
// shape without leaking into the production singleton.
func (tx *Tx) initializeBytes(c Manager) error {
unsignedBytes, err := c.Marshal(CodecVersion, &tx.UnsignedAtomicTx)
if err != nil {
return fmt.Errorf("couldn't marshal UnsignedAtomicTx: %w", err)
}
signedBytes, err := c.Marshal(CodecVersion, tx)
if err != nil {
return fmt.Errorf("couldn't marshal Tx: %w", err)
}
tx.Initialize(unsignedBytes, signedBytes)
return nil
}
// BlockFeeContribution calculates how much LUX towards the block fee contribution was paid
// for via this transaction denominated in [utxoAssetID] with [baseFee] used to calculate the
// cost of this transaction. This function also returns the [gasUsed] by the
// transaction for inclusion in the [baseFee] algorithm.
func (tx *Tx) BlockFeeContribution(fixedFee bool, utxoAssetID ids.ID, baseFee *big.Int) (*big.Int, *big.Int, error) {
if baseFee == nil {
return nil, nil, errNilBaseFee
}
if baseFee.Cmp(common.Big0) <= 0 {
return nil, nil, fmt.Errorf("cannot calculate tip with base fee %d <= 0", baseFee)
}
gasUsed, err := tx.GasUsed(fixedFee)
if err != nil {
return nil, nil, err
}
txFee, err := CalculateDynamicFee(gasUsed, baseFee)
if err != nil {
return nil, nil, err
}
burned, err := tx.Burned(utxoAssetID)
if err != nil {
return nil, nil, err
}
if txFee > burned {
return nil, nil, fmt.Errorf("insufficient LUX burned (%d) to cover import tx fee (%d)", burned, txFee)
}
excessBurned := burned - txFee
// Calculate the amount of LUX that has been burned above the required fee denominated
// in C-Chain native 18 decimal places
blockFeeContribution := new(big.Int).Mul(new(big.Int).SetUint64(excessBurned), X2CRate.ToBig())
return blockFeeContribution, new(big.Int).SetUint64(gasUsed), nil
}
func (tx *Tx) GossipID() luxfiids.ID {
nodeID := tx.ID()
var luxfiID luxfiids.ID
copy(luxfiID[:], nodeID[:])
return luxfiID
}
// innerSortInputsAndSigners implements sort.Interface for EVMInput
type innerSortInputsAndSigners struct {
inputs []EVMInput
signers [][]*secp256k1.PrivateKey
}
func (ins *innerSortInputsAndSigners) Less(i, j int) bool {
addrComp := bytes.Compare(ins.inputs[i].Address.Bytes(), ins.inputs[j].Address.Bytes())
if addrComp != 0 {
return addrComp < 0
}
return bytes.Compare(ins.inputs[i].AssetID[:], ins.inputs[j].AssetID[:]) < 0
}
func (ins *innerSortInputsAndSigners) Len() int { return len(ins.inputs) }
func (ins *innerSortInputsAndSigners) Swap(i, j int) {
ins.inputs[j], ins.inputs[i] = ins.inputs[i], ins.inputs[j]
ins.signers[j], ins.signers[i] = ins.signers[i], ins.signers[j]
}
// SortEVMInputsAndSigners sorts the list of EVMInputs based on the addresses and assetIDs
func SortEVMInputsAndSigners(inputs []EVMInput, signers [][]*secp256k1.PrivateKey) {
sort.Sort(&innerSortInputsAndSigners{inputs: inputs, signers: signers})
}
// calculates the amount of LUX that must be burned by an atomic transaction
// that consumes [cost] at [baseFee].
func CalculateDynamicFee(cost uint64, baseFee *big.Int) (uint64, error) {
if baseFee == nil {
return 0, errNilBaseFee
}
// fee = (cost * baseFee + [X2CRate] - 1) / [X2CRate]
fee := new(big.Int).SetUint64(cost)
fee.Mul(fee, baseFee)
fee.Add(fee, x2cRateMinus1.ToBig())
fee.Div(fee, X2CRate.ToBig())
if !fee.IsUint64() {
// the fee is more than can fit in a uint64
return 0, errFeeOverflow
}
return fee.Uint64(), nil
}
func calcBytesCost(len int) uint64 {
return uint64(len) * TxBytesGas
}