Files
utxo/transferables.go

257 lines
7.5 KiB
Go

// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package utxo
import (
"bytes"
"errors"
"sort"
"github.com/luxfi/crypto/secp256k1"
"github.com/luxfi/ids"
"github.com/luxfi/runtime"
"github.com/luxfi/vm/components/verify"
)
var (
ErrNilTransferableOutput = errors.New("nil transferable output is not valid")
ErrNilTransferableFxOutput = errors.New("nil transferable feature extension output is not valid")
ErrOutputsNotSorted = errors.New("outputs not sorted")
ErrNilTransferableInput = errors.New("nil transferable input is not valid")
ErrNilTransferableFxInput = errors.New("nil transferable feature extension input is not valid")
ErrInputsNotSortedUnique = errors.New("inputs not sorted and unique")
_ verify.Verifiable = (*TransferableOutput)(nil)
_ verify.Verifiable = (*TransferableInput)(nil)
_ interface{ Compare(*TransferableInput) int } = (*TransferableInput)(nil)
)
// Amounter is a data structure that has an amount of something associated with it
type Amounter interface {
// Amount returns how much value this element represents of the asset in its
// transaction.
Amount() uint64
}
// Coster is a data structure that has a cost associated with it
type Coster interface {
// Cost returns how much this element costs to be included in its
// transaction.
Cost() (uint64, error)
}
// TransferableIn is the interface a feature extension must provide to transfer
// value between features extensions.
type TransferableIn interface {
verify.Verifiable
Amounter
Coster
}
// TransferableOut is the interface a feature extension must provide to transfer
// value between features extensions.
type TransferableOut interface {
verify.State
Amounter
InitRuntime(*runtime.Runtime)
}
type TransferableOutput struct {
Asset `serialize:"true"`
// FxID has serialize false because we don't want this to be encoded in bytes
FxID ids.ID `serialize:"-" json:"fxID"`
Out TransferableOut `serialize:"true" json:"output"`
}
func (out *TransferableOutput) InitRuntime(rt *runtime.Runtime) {
out.Out.InitRuntime(rt)
}
// Output returns the feature extension output that this Output is using.
func (out *TransferableOutput) Output() TransferableOut {
return out.Out
}
func (out *TransferableOutput) Verify() error {
switch {
case out == nil:
return ErrNilTransferableOutput
case out.Out == nil:
return ErrNilTransferableFxOutput
default:
return verify.All(&out.Asset, out.Out)
}
}
// wireBytesOrNil returns the ZAP wire envelope for a TransferableOut if
// the inner fxs primitive implements the per-fx wire.go Bytes() bridge.
// Returns nil when the type does not carry a wire adapter — callers must
// not rely on a stable sort across unknown types. Every production fxs
// primitive (secp256k1fx/mldsafx/slhdsafx/ed25519fx/secp256r1fx/schnorrfx/
// bls12381fx/nftfx/propertyfx) satisfies this contract via wire.go.
func wireBytesOrNil(out TransferableOut) []byte {
if ws, ok := out.(interface{ Bytes() []byte }); ok {
return ws.Bytes()
}
return nil
}
type innerSortTransferableOutputs struct {
outs []*TransferableOutput
}
func (outs *innerSortTransferableOutputs) Less(i, j int) bool {
iOut := outs.outs[i]
jOut := outs.outs[j]
iAssetID := iOut.AssetID()
jAssetID := jOut.AssetID()
switch bytes.Compare(iAssetID[:], jAssetID[:]) {
case -1:
return true
case 1:
return false
}
// ZAP-native canonical sort: bytes of the inner fx wire envelope.
// The per-fx wire.go Bytes() returns the same TypeKind+ShapeKind+
// ZAP-message envelope that hits the wire and disk — single source
// of truth for total ordering, no separate codec marshal step.
return bytes.Compare(wireBytesOrNil(iOut.Out), wireBytesOrNil(jOut.Out)) == -1
}
func (outs *innerSortTransferableOutputs) Len() int {
return len(outs.outs)
}
func (outs *innerSortTransferableOutputs) Swap(i, j int) {
o := outs.outs
o[j], o[i] = o[i], o[j]
}
// SortTransferableOutputs sorts output objects by (AssetID, inner-output
// ZAP wire bytes). ZAP-native — no codec.Manager needed.
func SortTransferableOutputs(outs []*TransferableOutput) {
sort.Sort(&innerSortTransferableOutputs{outs: outs})
}
// IsSortedTransferableOutputs returns true if output objects are sorted.
func IsSortedTransferableOutputs(outs []*TransferableOutput) bool {
return sort.IsSorted(&innerSortTransferableOutputs{outs: outs})
}
type TransferableInput struct {
UTXOID `serialize:"true"`
Asset `serialize:"true"`
// FxID has serialize false because we don't want this to be encoded in bytes
FxID ids.ID `serialize:"-" json:"fxID"`
In TransferableIn `serialize:"true" json:"input"`
}
// Input returns the feature extension input that this Input is using.
func (in *TransferableInput) Input() TransferableIn {
return in.In
}
func (in *TransferableInput) Verify() error {
switch {
case in == nil:
return ErrNilTransferableInput
case in.In == nil:
return ErrNilTransferableFxInput
default:
return verify.All(&in.UTXOID, &in.Asset, in.In)
}
}
func (in *TransferableInput) Compare(other *TransferableInput) int {
return in.UTXOID.Compare(&other.UTXOID)
}
// InitRuntime forwards the runtime to the inner Fx input if it implements
// the runtime-aware interface. No-op otherwise. Used by VMs that need to
// hand a runtime context down to feature-extension input types.
func (in *TransferableInput) InitRuntime(rt *runtime.Runtime) {
if contextInput, ok := in.In.(interface{ InitRuntime(*runtime.Runtime) }); ok {
contextInput.InitRuntime(rt)
}
}
type innerSortTransferableInputsWithSigners struct {
ins []*TransferableInput
signers [][]*secp256k1.PrivateKey
}
func (ins *innerSortTransferableInputsWithSigners) Less(i, j int) bool {
iID, iIndex := ins.ins[i].InputSource()
jID, jIndex := ins.ins[j].InputSource()
switch bytes.Compare(iID[:], jID[:]) {
case -1:
return true
case 0:
return iIndex < jIndex
default:
return false
}
}
func (ins *innerSortTransferableInputsWithSigners) Len() int {
return len(ins.ins)
}
func (ins *innerSortTransferableInputsWithSigners) Swap(i, j int) {
ins.ins[j], ins.ins[i] = ins.ins[i], ins.ins[j]
ins.signers[j], ins.signers[i] = ins.signers[i], ins.signers[j]
}
// SortTransferableInputsWithSigners sorts the inputs and signers based on the
// input's utxo ID
func SortTransferableInputsWithSigners(ins []*TransferableInput, signers [][]*secp256k1.PrivateKey) {
sort.Sort(&innerSortTransferableInputsWithSigners{ins: ins, signers: signers})
}
// VerifyTx verifies that the inputs and outputs flowcheck, including a fee.
// Additionally, this verifies that the inputs and outputs are sorted.
func VerifyTx(
feeAmount uint64,
feeAssetID ids.ID,
allIns [][]*TransferableInput,
allOuts [][]*TransferableOutput,
) error {
fc := NewFlowChecker()
fc.Produce(feeAssetID, feeAmount) // The txFee must be burned
// Add all the outputs to the flow checker and make sure they are sorted
for _, outs := range allOuts {
for _, out := range outs {
if err := out.Verify(); err != nil {
return err
}
fc.Produce(out.AssetID(), out.Output().Amount())
}
if !IsSortedTransferableOutputs(outs) {
return ErrOutputsNotSorted
}
}
// Add all the inputs to the flow checker and make sure they are sorted
for _, ins := range allIns {
for _, in := range ins {
if err := in.Verify(); err != nil {
return err
}
fc.Consume(in.AssetID(), in.Input().Amount())
}
if !isSortedAndUniqueByCompare(ins) {
return ErrInputsNotSortedUnique
}
}
return fc.Verify()
}