mirror of
https://github.com/luxfi/coreth.git
synced 2026-10-30 17:00:51 +00:00
Package-local identifier only; no exported field, json tag, or wire change. Co-authored-by: Hanzo Dev <[email protected]>
338 lines
11 KiB
Go
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
|
|
}
|