mirror of
https://github.com/luxfi/utxo.git
synced 2026-07-27 03:39:23 +00:00
257 lines
7.5 KiB
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()
|
|
}
|