mirror of
https://github.com/luxfi/node.git
synced 2026-07-27 03:39:39 +00:00
components/lux UTXO value tree -> native ZAP (Wave-A Substrate)
The shared UTXO value tree (UTXO/TransferableOutput/TransferableInput/Asset/ UTXOID/BaseTx/Metadata + OutputOwners path) gets native struct-is-wire Marshal/Unmarshal (new marshal.go), replacing every pcodecs.Manager. Codec param dropped from utxo_state / atomic_utxos / transferables (Sort* sorts on inner fx wire Bytes()); flow_checker pcodecs.Errs -> errors.Join. Node consensus persistence uses luxfi/utxo (unchanged); components/lux is the tx-builder/#58 surface — encoding-only change, type tree NOT collapsed (#58 respected). Both P and X share the encoding => internally consistent, re-genesis safe. Whole node builds EXIT 0; 8 components/lux round-trips + P-chain txs/block tests green. Co-authored-by: Hanzo Dev <dev@hanzo.ai>
This commit is contained in:
@@ -8,22 +8,20 @@ import (
|
||||
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/math/set"
|
||||
"github.com/luxfi/node/vms/pcodecs"
|
||||
"github.com/luxfi/vm/chains/atomic"
|
||||
)
|
||||
|
||||
var _ AtomicUTXOManager = (*atomicUTXOManager)(nil)
|
||||
|
||||
type atomicUTXOManager struct {
|
||||
sm atomic.SharedMemory
|
||||
codec pcodecs.Manager
|
||||
sm atomic.SharedMemory
|
||||
}
|
||||
|
||||
func NewAtomicUTXOManager(sm atomic.SharedMemory, codec pcodecs.Manager) AtomicUTXOManager {
|
||||
return &atomicUTXOManager{
|
||||
sm: sm,
|
||||
codec: codec,
|
||||
}
|
||||
// NewAtomicUTXOManager returns an AtomicUTXOManager backed by ZAP-native
|
||||
// wire bytes in cross-chain shared memory (no codec.Manager). Callers rely
|
||||
// on this package's init() fx-aware UTXO.Unmarshal dispatch.
|
||||
func NewAtomicUTXOManager(sm atomic.SharedMemory) AtomicUTXOManager {
|
||||
return &atomicUTXOManager{sm: sm}
|
||||
}
|
||||
|
||||
func (a *atomicUTXOManager) GetAtomicUTXOs(
|
||||
@@ -64,7 +62,7 @@ func (a *atomicUTXOManager) GetAtomicUTXOs(
|
||||
utxos := make([]*UTXO, len(allUTXOBytes))
|
||||
for i, utxoBytes := range allUTXOBytes {
|
||||
utxo := &UTXO{}
|
||||
if _, err := a.codec.Unmarshal(utxoBytes, utxo); err != nil {
|
||||
if err := utxo.Unmarshal(utxoBytes); err != nil {
|
||||
return nil, ids.ShortID{}, ids.Empty, fmt.Errorf("error parsing UTXO: %w", err)
|
||||
}
|
||||
utxos[i] = utxo
|
||||
@@ -84,13 +82,12 @@ func (a *atomicUTXOManager) GetAtomicUTXOs(
|
||||
// * Any error that may have occurred upstream.
|
||||
func GetAtomicUTXOs(
|
||||
sharedMemory atomic.SharedMemory,
|
||||
codec pcodecs.Manager,
|
||||
chainID ids.ID,
|
||||
addrs set.Set[ids.ShortID],
|
||||
startAddr ids.ShortID,
|
||||
startUTXOID ids.ID,
|
||||
limit int,
|
||||
) ([]*UTXO, ids.ShortID, ids.ID, error) {
|
||||
manager := NewAtomicUTXOManager(sharedMemory, codec)
|
||||
manager := NewAtomicUTXOManager(sharedMemory)
|
||||
return manager.GetAtomicUTXOs(chainID, addrs, startAddr, startUTXOID, limit)
|
||||
}
|
||||
|
||||
@@ -8,14 +8,13 @@ import (
|
||||
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/math"
|
||||
"github.com/luxfi/node/vms/pcodecs"
|
||||
)
|
||||
|
||||
var ErrInsufficientFunds = errors.New("insufficient funds")
|
||||
|
||||
type FlowChecker struct {
|
||||
consumed, produced map[ids.ID]uint64
|
||||
errs pcodecs.Errs
|
||||
errs []error
|
||||
}
|
||||
|
||||
func NewFlowChecker() *FlowChecker {
|
||||
@@ -36,18 +35,20 @@ func (fc *FlowChecker) Produce(assetID ids.ID, amount uint64) {
|
||||
func (fc *FlowChecker) add(value map[ids.ID]uint64, assetID ids.ID, amount uint64) {
|
||||
var err error
|
||||
value[assetID], err = math.Add64(value[assetID], amount)
|
||||
fc.errs.Add(err)
|
||||
if err != nil {
|
||||
fc.errs = append(fc.errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (fc *FlowChecker) Verify() error {
|
||||
if !fc.errs.Errored() {
|
||||
if len(fc.errs) == 0 {
|
||||
for assetID, producedAssetAmount := range fc.produced {
|
||||
consumedAssetAmount := fc.consumed[assetID]
|
||||
if producedAssetAmount > consumedAssetAmount {
|
||||
fc.errs.Add(ErrInsufficientFunds)
|
||||
fc.errs = append(fc.errs, ErrInsufficientFunds)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return fc.errs.Err
|
||||
return errors.Join(fc.errs...)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,421 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package lux
|
||||
|
||||
// Native ZAP wire for the shared UTXO value tree: the struct IS the wire.
|
||||
// Each type Marshal()s to a single zap object (StartObject / Set* /
|
||||
// FinishAsRoot) and Unmarshal()s via offset accessors — no codec, no
|
||||
// pcodecs.Manager, no serialize-tag reflection, no codec-version prefix.
|
||||
//
|
||||
// The polymorphic inner Out/In (fx feature-extension outputs/inputs) is
|
||||
// composed, not re-encoded here: each fx primitive already owns a native
|
||||
// ZAP wire envelope via its Bytes() method (and the WrapOutputBytes /
|
||||
// wrapInputBytes fx-aware dispatchers reconstruct it). components/lux
|
||||
// stays fx-agnostic — it hand-rolls only the outer node envelope and
|
||||
// stores the fx child as an opaque bytes field. This is the same
|
||||
// (TypeKind, ShapeKind, ZAP-message) envelope the fx packages hit on the
|
||||
// P/X data path, so components/lux wire bytes stay consistent with the
|
||||
// canonical luxfi/utxo tree without collapsing the two type trees (#58).
|
||||
//
|
||||
// Object fixed sections (all offsets object-relative, little-endian):
|
||||
//
|
||||
// Asset assetID 32B @0 size 32
|
||||
// UTXOID txID 32B @0, index u32 @32 size 36
|
||||
// TransferableOutput assetID 32B @0, outBytes ptr @32 size 40
|
||||
// TransferableInput txID 32B @0, index u32 @32, assetID 32B @36, inBytes ptr @68 size 76
|
||||
// UTXO txID 32B @0, index u32 @32, assetID 32B @36, outBytes ptr @68 size 76
|
||||
// BaseTx networkID u32 @0, blockchainID 32B @4, outsLen ptr @36,
|
||||
// outsBlob ptr @44, insLen ptr @52, insBlob ptr @60, memo ptr @68 size 76
|
||||
// Metadata unsignedBytes ptr @0, signedBytes ptr @8 size 16
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/luxfi/utxo/secp256k1fx"
|
||||
"github.com/luxfi/zap"
|
||||
)
|
||||
|
||||
// ErrOutNotWireSerializable is returned when an in-memory Out/In is not a
|
||||
// known fx primitive carrying a native ZAP Bytes() adapter.
|
||||
var ErrOutNotWireSerializable = errors.New("lux: fx output/input type does not implement wire-serializable Bytes() []byte")
|
||||
|
||||
// wireSerializable is the minimal contract every fx primitive's wire.go
|
||||
// adapter satisfies for the polymorphic child payload.
|
||||
type wireSerializable interface{ Bytes() []byte }
|
||||
|
||||
func childBytes(v any) ([]byte, error) {
|
||||
ws, ok := v.(wireSerializable)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%w: %T", ErrOutNotWireSerializable, v)
|
||||
}
|
||||
return ws.Bytes(), nil
|
||||
}
|
||||
|
||||
// ---- fixed offsets / sizes ----
|
||||
|
||||
const (
|
||||
offAssetOnly = 0
|
||||
sizeAsset = 32
|
||||
|
||||
offUTXOIDTxID = 0
|
||||
offUTXOIDIndex = 32
|
||||
sizeUTXOID = 36
|
||||
|
||||
offTOAsset = 0
|
||||
offTOOut = 32
|
||||
sizeTO = 40
|
||||
|
||||
offTITxID = 0
|
||||
offTIIndex = 32
|
||||
offTIAsset = 36
|
||||
offTIIn = 68
|
||||
sizeTI = 76
|
||||
|
||||
offUTXOTxID = 0
|
||||
offUTXOIndex = 32
|
||||
offUTXOAsset = 36
|
||||
offUTXOOut = 68
|
||||
sizeUTXOObj = 76
|
||||
|
||||
offBTNetworkID = 0
|
||||
offBTBlockchainID = 4
|
||||
offBTOutsLen = 36
|
||||
offBTOutsBlob = 44
|
||||
offBTInsLen = 52
|
||||
offBTInsBlob = 60
|
||||
offBTMemo = 68
|
||||
sizeBT = 76
|
||||
|
||||
offMDUnsigned = 0
|
||||
offMDSigned = 8
|
||||
sizeMD = 16
|
||||
|
||||
idLen = 32
|
||||
itemLenStrid = 4 // uint32 element for the per-item length lists
|
||||
)
|
||||
|
||||
// ---- Asset ----
|
||||
|
||||
func (a *Asset) Marshal() ([]byte, error) {
|
||||
b := zap.NewBuilder(zap.HeaderSize + sizeAsset)
|
||||
ob := b.StartObject(sizeAsset)
|
||||
ob.SetBytesFixed(offAssetOnly, a.ID[:])
|
||||
ob.FinishAsRoot()
|
||||
return b.Finish(), nil
|
||||
}
|
||||
|
||||
func (a *Asset) Unmarshal(bytes []byte) error {
|
||||
msg, err := zap.Parse(bytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
copy(a.ID[:], msg.Root().BytesFixedSlice(offAssetOnly, idLen))
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---- UTXOID ----
|
||||
|
||||
func (u *UTXOID) Marshal() ([]byte, error) {
|
||||
b := zap.NewBuilder(zap.HeaderSize + sizeUTXOID)
|
||||
ob := b.StartObject(sizeUTXOID)
|
||||
ob.SetBytesFixed(offUTXOIDTxID, u.TxID[:])
|
||||
ob.SetUint32(offUTXOIDIndex, u.OutputIndex)
|
||||
ob.FinishAsRoot()
|
||||
return b.Finish(), nil
|
||||
}
|
||||
|
||||
func (u *UTXOID) Unmarshal(bytes []byte) error {
|
||||
msg, err := zap.Parse(bytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
obj := msg.Root()
|
||||
copy(u.TxID[:], obj.BytesFixedSlice(offUTXOIDTxID, idLen))
|
||||
u.OutputIndex = obj.Uint32(offUTXOIDIndex)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---- TransferableOutput ----
|
||||
|
||||
func (out *TransferableOutput) Marshal() ([]byte, error) {
|
||||
if out == nil || out.Out == nil {
|
||||
return nil, ErrNilTransferableFxOutput
|
||||
}
|
||||
child, err := childBytes(out.Out)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b := zap.NewBuilder(zap.HeaderSize + sizeTO + len(child) + 32)
|
||||
ob := b.StartObject(sizeTO)
|
||||
ob.SetBytesFixed(offTOAsset, out.Asset.ID[:])
|
||||
ob.SetBytes(offTOOut, child)
|
||||
ob.FinishAsRoot()
|
||||
return b.Finish(), nil
|
||||
}
|
||||
|
||||
func (out *TransferableOutput) Unmarshal(bytes []byte) error {
|
||||
msg, err := zap.Parse(bytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
obj := msg.Root()
|
||||
copy(out.Asset.ID[:], obj.BytesFixedSlice(offTOAsset, idLen))
|
||||
fxOut, err := WrapOutputBytes(obj.Bytes(offTOOut))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
to, ok := fxOut.(TransferableOut)
|
||||
if !ok {
|
||||
return fmt.Errorf("lux: decoded output %T is not a TransferableOut", fxOut)
|
||||
}
|
||||
out.Out = to
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---- TransferableInput ----
|
||||
|
||||
func (in *TransferableInput) Marshal() ([]byte, error) {
|
||||
if in == nil || in.In == nil {
|
||||
return nil, ErrNilTransferableFxInput
|
||||
}
|
||||
child, err := childBytes(in.In)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b := zap.NewBuilder(zap.HeaderSize + sizeTI + len(child) + 32)
|
||||
ob := b.StartObject(sizeTI)
|
||||
ob.SetBytesFixed(offTITxID, in.UTXOID.TxID[:])
|
||||
ob.SetUint32(offTIIndex, in.UTXOID.OutputIndex)
|
||||
ob.SetBytesFixed(offTIAsset, in.Asset.ID[:])
|
||||
ob.SetBytes(offTIIn, child)
|
||||
ob.FinishAsRoot()
|
||||
return b.Finish(), nil
|
||||
}
|
||||
|
||||
func (in *TransferableInput) Unmarshal(bytes []byte) error {
|
||||
msg, err := zap.Parse(bytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
obj := msg.Root()
|
||||
copy(in.UTXOID.TxID[:], obj.BytesFixedSlice(offTITxID, idLen))
|
||||
in.UTXOID.OutputIndex = obj.Uint32(offTIIndex)
|
||||
copy(in.Asset.ID[:], obj.BytesFixedSlice(offTIAsset, idLen))
|
||||
fxIn, err := wrapInputBytes(obj.Bytes(offTIIn))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
in.In = fxIn
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---- UTXO ----
|
||||
|
||||
func (utxo *UTXO) Marshal() ([]byte, error) {
|
||||
if utxo == nil || utxo.Out == nil {
|
||||
return nil, errEmptyUTXO
|
||||
}
|
||||
child, err := childBytes(utxo.Out)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b := zap.NewBuilder(zap.HeaderSize + sizeUTXOObj + len(child) + 32)
|
||||
ob := b.StartObject(sizeUTXOObj)
|
||||
ob.SetBytesFixed(offUTXOTxID, utxo.UTXOID.TxID[:])
|
||||
ob.SetUint32(offUTXOIndex, utxo.UTXOID.OutputIndex)
|
||||
ob.SetBytesFixed(offUTXOAsset, utxo.Asset.ID[:])
|
||||
ob.SetBytes(offUTXOOut, child)
|
||||
ob.FinishAsRoot()
|
||||
return b.Finish(), nil
|
||||
}
|
||||
|
||||
func (utxo *UTXO) Unmarshal(bytes []byte) error {
|
||||
msg, err := zap.Parse(bytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
obj := msg.Root()
|
||||
copy(utxo.UTXOID.TxID[:], obj.BytesFixedSlice(offUTXOTxID, idLen))
|
||||
utxo.UTXOID.OutputIndex = obj.Uint32(offUTXOIndex)
|
||||
copy(utxo.Asset.ID[:], obj.BytesFixedSlice(offUTXOAsset, idLen))
|
||||
fxOut, err := WrapOutputBytes(obj.Bytes(offUTXOOut))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
utxo.Out = fxOut
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---- BaseTx ----
|
||||
|
||||
func (t *BaseTx) Marshal() ([]byte, error) {
|
||||
b := zap.NewBuilder(zap.HeaderSize + sizeBT + 256)
|
||||
|
||||
outsLenOff, outsLenCount, outsBlob, err := writeOutList(b, t.Outs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
insLenOff, insLenCount, insBlob, err := writeInList(b, t.Ins)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ob := b.StartObject(sizeBT)
|
||||
ob.SetUint32(offBTNetworkID, t.NetworkID)
|
||||
ob.SetBytesFixed(offBTBlockchainID, t.BlockchainID[:])
|
||||
ob.SetList(offBTOutsLen, outsLenOff, outsLenCount)
|
||||
ob.SetBytes(offBTOutsBlob, outsBlob)
|
||||
ob.SetList(offBTInsLen, insLenOff, insLenCount)
|
||||
ob.SetBytes(offBTInsBlob, insBlob)
|
||||
ob.SetBytes(offBTMemo, t.Memo)
|
||||
ob.FinishAsRoot()
|
||||
return b.Finish(), nil
|
||||
}
|
||||
|
||||
func (t *BaseTx) Unmarshal(bytes []byte) error {
|
||||
msg, err := zap.Parse(bytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
obj := msg.Root()
|
||||
t.NetworkID = obj.Uint32(offBTNetworkID)
|
||||
copy(t.BlockchainID[:], obj.BytesFixedSlice(offBTBlockchainID, idLen))
|
||||
if t.Outs, err = readOutList(obj, offBTOutsLen, offBTOutsBlob); err != nil {
|
||||
return err
|
||||
}
|
||||
if t.Ins, err = readInList(obj, offBTInsLen, offBTInsBlob); err != nil {
|
||||
return err
|
||||
}
|
||||
if m := obj.Bytes(offBTMemo); len(m) > 0 {
|
||||
t.Memo = append([]byte(nil), m...)
|
||||
} else {
|
||||
t.Memo = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeOutList marshals each output and packs (per-item u32 length list,
|
||||
// concatenated blob). AddUint32 counts elements, so the list length is the
|
||||
// item count directly (unlike an AddBytes list, which counts bytes).
|
||||
func writeOutList(b *zap.Builder, outs []*TransferableOutput) (lenOff, lenCount int, blob []byte, err error) {
|
||||
if len(outs) == 0 {
|
||||
return 0, 0, nil, nil
|
||||
}
|
||||
lb := b.StartList(itemLenStrid)
|
||||
for i, o := range outs {
|
||||
raw, err := o.Marshal()
|
||||
if err != nil {
|
||||
return 0, 0, nil, fmt.Errorf("output %d: %w", i, err)
|
||||
}
|
||||
lb.AddUint32(uint32(len(raw)))
|
||||
blob = append(blob, raw...)
|
||||
}
|
||||
lenOff, lenCount = lb.Finish()
|
||||
return lenOff, lenCount, blob, nil
|
||||
}
|
||||
|
||||
func writeInList(b *zap.Builder, ins []*TransferableInput) (lenOff, lenCount int, blob []byte, err error) {
|
||||
if len(ins) == 0 {
|
||||
return 0, 0, nil, nil
|
||||
}
|
||||
lb := b.StartList(itemLenStrid)
|
||||
for i, in := range ins {
|
||||
raw, err := in.Marshal()
|
||||
if err != nil {
|
||||
return 0, 0, nil, fmt.Errorf("input %d: %w", i, err)
|
||||
}
|
||||
lb.AddUint32(uint32(len(raw)))
|
||||
blob = append(blob, raw...)
|
||||
}
|
||||
lenOff, lenCount = lb.Finish()
|
||||
return lenOff, lenCount, blob, nil
|
||||
}
|
||||
|
||||
func readOutList(obj zap.Object, lenPtrOff, blobPtrOff int) ([]*TransferableOutput, error) {
|
||||
lengths := obj.ListStride(lenPtrOff, itemLenStrid)
|
||||
n := lengths.Len()
|
||||
if n == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
blob := obj.Bytes(blobPtrOff)
|
||||
out := make([]*TransferableOutput, n)
|
||||
cursor := 0
|
||||
for i := 0; i < n; i++ {
|
||||
size := int(lengths.Uint32(i))
|
||||
if size < 0 || cursor+size > len(blob) {
|
||||
return nil, fmt.Errorf("lux: output %d length %d overruns blob (%d)", i, size, len(blob))
|
||||
}
|
||||
o := &TransferableOutput{}
|
||||
if err := o.Unmarshal(blob[cursor : cursor+size]); err != nil {
|
||||
return nil, fmt.Errorf("lux: unmarshal output %d: %w", i, err)
|
||||
}
|
||||
out[i] = o
|
||||
cursor += size
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func readInList(obj zap.Object, lenPtrOff, blobPtrOff int) ([]*TransferableInput, error) {
|
||||
lengths := obj.ListStride(lenPtrOff, itemLenStrid)
|
||||
n := lengths.Len()
|
||||
if n == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
blob := obj.Bytes(blobPtrOff)
|
||||
ins := make([]*TransferableInput, n)
|
||||
cursor := 0
|
||||
for i := 0; i < n; i++ {
|
||||
size := int(lengths.Uint32(i))
|
||||
if size < 0 || cursor+size > len(blob) {
|
||||
return nil, fmt.Errorf("lux: input %d length %d overruns blob (%d)", i, size, len(blob))
|
||||
}
|
||||
in := &TransferableInput{}
|
||||
if err := in.Unmarshal(blob[cursor : cursor+size]); err != nil {
|
||||
return nil, fmt.Errorf("lux: unmarshal input %d: %w", i, err)
|
||||
}
|
||||
ins[i] = in
|
||||
cursor += size
|
||||
}
|
||||
return ins, nil
|
||||
}
|
||||
|
||||
// ---- Metadata ----
|
||||
|
||||
func (md *Metadata) Marshal() ([]byte, error) {
|
||||
b := zap.NewBuilder(zap.HeaderSize + sizeMD + len(md.unsignedBytes) + len(md.bytes) + 32)
|
||||
ob := b.StartObject(sizeMD)
|
||||
ob.SetBytes(offMDUnsigned, md.unsignedBytes)
|
||||
ob.SetBytes(offMDSigned, md.bytes)
|
||||
ob.FinishAsRoot()
|
||||
return b.Finish(), nil
|
||||
}
|
||||
|
||||
func (md *Metadata) Unmarshal(bytes []byte) error {
|
||||
msg, err := zap.Parse(bytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
obj := msg.Root()
|
||||
unsigned := append([]byte(nil), obj.Bytes(offMDUnsigned)...)
|
||||
signed := append([]byte(nil), obj.Bytes(offMDSigned)...)
|
||||
md.Initialize(unsigned, signed)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---- OutputOwners path ----
|
||||
|
||||
// MarshalOwner is the ONE canonical byte encoding of an owner: the fx's
|
||||
// own native ZAP OutputOwners envelope (TypeKindReserved,
|
||||
// ShapeKindOutputOwners). Takes `any` to match the fx.Owned.Owners()
|
||||
// surface. Same wire the fx TransferOutput embeds — no second encoding.
|
||||
func MarshalOwner(o any) ([]byte, error) {
|
||||
return childBytes(o)
|
||||
}
|
||||
|
||||
// UnmarshalOwner is the exact inverse of MarshalOwner: parses the
|
||||
// canonical OutputOwners envelope back into *secp256k1fx.OutputOwners.
|
||||
func UnmarshalOwner(b []byte) (*secp256k1fx.OutputOwners, error) {
|
||||
return secp256k1fx.WrapOutputOwners(b)
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package lux
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/utils"
|
||||
"github.com/luxfi/utxo/secp256k1fx"
|
||||
)
|
||||
|
||||
func testTransferOutput() *secp256k1fx.TransferOutput {
|
||||
// OutputOwners.Verify() requires sorted+unique addresses — the wire
|
||||
// encoding preserves order faithfully, so seed canonical (sorted) data.
|
||||
addrs := []ids.ShortID{ids.GenerateTestShortID(), ids.GenerateTestShortID()}
|
||||
utils.Sort(addrs)
|
||||
return &secp256k1fx.TransferOutput{
|
||||
Amt: 12345,
|
||||
OutputOwners: secp256k1fx.OutputOwners{
|
||||
Locktime: 67,
|
||||
Threshold: 1,
|
||||
Addrs: addrs,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func testTransferInput() *secp256k1fx.TransferInput {
|
||||
return &secp256k1fx.TransferInput{
|
||||
Amt: 99,
|
||||
Input: secp256k1fx.Input{SigIndices: []uint32{0, 2, 5}},
|
||||
}
|
||||
}
|
||||
|
||||
func TestAssetRoundTrip(t *testing.T) {
|
||||
require := require.New(t)
|
||||
a := &Asset{ID: ids.GenerateTestID()}
|
||||
|
||||
b, err := a.Marshal()
|
||||
require.NoError(err)
|
||||
|
||||
got := &Asset{}
|
||||
require.NoError(got.Unmarshal(b))
|
||||
require.Equal(a.ID, got.ID)
|
||||
}
|
||||
|
||||
func TestUTXOIDRoundTrip(t *testing.T) {
|
||||
require := require.New(t)
|
||||
u := &UTXOID{TxID: ids.GenerateTestID(), OutputIndex: 7}
|
||||
|
||||
b, err := u.Marshal()
|
||||
require.NoError(err)
|
||||
|
||||
got := &UTXOID{}
|
||||
require.NoError(got.Unmarshal(b))
|
||||
require.Equal(u.TxID, got.TxID)
|
||||
require.Equal(u.OutputIndex, got.OutputIndex)
|
||||
}
|
||||
|
||||
func TestTransferableOutputRoundTrip(t *testing.T) {
|
||||
require := require.New(t)
|
||||
out := &TransferableOutput{
|
||||
Asset: Asset{ID: ids.GenerateTestID()},
|
||||
Out: testTransferOutput(),
|
||||
}
|
||||
|
||||
b, err := out.Marshal()
|
||||
require.NoError(err)
|
||||
|
||||
got := &TransferableOutput{}
|
||||
require.NoError(got.Unmarshal(b))
|
||||
require.Equal(out.Asset.ID, got.Asset.ID)
|
||||
require.Equal(out.Out, got.Out)
|
||||
require.NoError(got.Verify())
|
||||
|
||||
// Wire bytes are stable across a re-marshal of the decoded value.
|
||||
b2, err := got.Marshal()
|
||||
require.NoError(err)
|
||||
require.Equal(b, b2)
|
||||
}
|
||||
|
||||
func TestTransferableInputRoundTrip(t *testing.T) {
|
||||
require := require.New(t)
|
||||
in := &TransferableInput{
|
||||
UTXOID: UTXOID{TxID: ids.GenerateTestID(), OutputIndex: 3},
|
||||
Asset: Asset{ID: ids.GenerateTestID()},
|
||||
In: testTransferInput(),
|
||||
}
|
||||
|
||||
b, err := in.Marshal()
|
||||
require.NoError(err)
|
||||
|
||||
got := &TransferableInput{}
|
||||
require.NoError(got.Unmarshal(b))
|
||||
require.Equal(in.UTXOID.TxID, got.UTXOID.TxID)
|
||||
require.Equal(in.UTXOID.OutputIndex, got.UTXOID.OutputIndex)
|
||||
require.Equal(in.Asset.ID, got.Asset.ID)
|
||||
require.Equal(in.In, got.In)
|
||||
require.NoError(got.Verify())
|
||||
}
|
||||
|
||||
func TestUTXORoundTrip(t *testing.T) {
|
||||
require := require.New(t)
|
||||
utxo := &UTXO{
|
||||
UTXOID: UTXOID{TxID: ids.GenerateTestID(), OutputIndex: 4},
|
||||
Asset: Asset{ID: ids.GenerateTestID()},
|
||||
Out: testTransferOutput(),
|
||||
}
|
||||
|
||||
b, err := utxo.Marshal()
|
||||
require.NoError(err)
|
||||
|
||||
got := &UTXO{}
|
||||
require.NoError(got.Unmarshal(b))
|
||||
require.Equal(utxo.UTXOID.TxID, got.UTXOID.TxID)
|
||||
require.Equal(utxo.UTXOID.OutputIndex, got.UTXOID.OutputIndex)
|
||||
require.Equal(utxo.Asset.ID, got.Asset.ID)
|
||||
require.Equal(utxo.Out, got.Out)
|
||||
require.NoError(got.Verify())
|
||||
|
||||
// InputID (TxID.Prefix(index)) is preserved end-to-end.
|
||||
require.Equal(utxo.InputID(), got.InputID())
|
||||
|
||||
b2, err := got.Marshal()
|
||||
require.NoError(err)
|
||||
require.Equal(b, b2)
|
||||
}
|
||||
|
||||
func TestBaseTxRoundTrip(t *testing.T) {
|
||||
require := require.New(t)
|
||||
tx := &BaseTx{
|
||||
NetworkID: 96369,
|
||||
BlockchainID: ids.GenerateTestID(),
|
||||
Outs: []*TransferableOutput{
|
||||
{Asset: Asset{ID: ids.GenerateTestID()}, Out: testTransferOutput()},
|
||||
},
|
||||
Ins: []*TransferableInput{
|
||||
{
|
||||
UTXOID: UTXOID{TxID: ids.GenerateTestID(), OutputIndex: 1},
|
||||
Asset: Asset{ID: ids.GenerateTestID()},
|
||||
In: testTransferInput(),
|
||||
},
|
||||
},
|
||||
Memo: []byte("round-trip"),
|
||||
}
|
||||
|
||||
b, err := tx.Marshal()
|
||||
require.NoError(err)
|
||||
|
||||
got := &BaseTx{}
|
||||
require.NoError(got.Unmarshal(b))
|
||||
require.Equal(tx.NetworkID, got.NetworkID)
|
||||
require.Equal(tx.BlockchainID, got.BlockchainID)
|
||||
require.Len(got.Outs, 1)
|
||||
require.Len(got.Ins, 1)
|
||||
require.Equal(tx.Outs[0].Asset.ID, got.Outs[0].Asset.ID)
|
||||
require.Equal(tx.Outs[0].Out, got.Outs[0].Out)
|
||||
require.Equal(tx.Ins[0].In, got.Ins[0].In)
|
||||
require.Equal(tx.Memo, got.Memo)
|
||||
}
|
||||
|
||||
func TestMetadataRoundTrip(t *testing.T) {
|
||||
require := require.New(t)
|
||||
md := &Metadata{}
|
||||
md.Initialize([]byte("unsigned-bytes"), []byte("signed-bytes"))
|
||||
|
||||
b, err := md.Marshal()
|
||||
require.NoError(err)
|
||||
|
||||
got := &Metadata{}
|
||||
require.NoError(got.Unmarshal(b))
|
||||
require.Equal(md.Bytes(), got.Bytes())
|
||||
require.Equal(md.SignedBytes(), got.SignedBytes())
|
||||
require.Equal(md.ID(), got.ID())
|
||||
require.NoError(got.Verify())
|
||||
}
|
||||
|
||||
// TestOutputOwnersRoundTrip exercises the standalone OutputOwners path
|
||||
// (MarshalOwner / UnmarshalOwner) shared with off-tx owner identity keys.
|
||||
func TestOutputOwnersRoundTrip(t *testing.T) {
|
||||
require := require.New(t)
|
||||
owners := &secp256k1fx.OutputOwners{
|
||||
Locktime: 5,
|
||||
Threshold: 1,
|
||||
Addrs: []ids.ShortID{ids.GenerateTestShortID()},
|
||||
}
|
||||
|
||||
b, err := MarshalOwner(owners)
|
||||
require.NoError(err)
|
||||
|
||||
got, err := UnmarshalOwner(b)
|
||||
require.NoError(err)
|
||||
require.Equal(owners.Locktime, got.Locktime)
|
||||
require.Equal(owners.Threshold, got.Threshold)
|
||||
require.Equal(owners.Addrs, got.Addrs)
|
||||
}
|
||||
@@ -6,9 +6,9 @@ package lux
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/luxfi/crypto/hash"
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/node/vms/components/verify"
|
||||
"github.com/luxfi/crypto/hash"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -3,10 +3,6 @@
|
||||
|
||||
package lux
|
||||
|
||||
const (
|
||||
codecVersion = 0
|
||||
)
|
||||
|
||||
// Addressable is the interface a feature extension must provide to be able to
|
||||
// be tracked as a part of the utxo set for a set of addresses
|
||||
type Addressable interface {
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"github.com/luxfi/crypto/secp256k1"
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/node/vms/components/verify"
|
||||
"github.com/luxfi/node/vms/pcodecs"
|
||||
"github.com/luxfi/runtime"
|
||||
"github.com/luxfi/utils"
|
||||
)
|
||||
@@ -87,9 +86,19 @@ func (out *TransferableOutput) Verify() error {
|
||||
}
|
||||
}
|
||||
|
||||
// wireBytesOrNil returns the native ZAP wire envelope of a TransferableOut
|
||||
// when the inner fx primitive carries a Bytes() adapter (every production
|
||||
// fx does). This is the single source of truth for canonical ordering —
|
||||
// the same bytes that hit disk and the wire, no separate codec marshal.
|
||||
func wireBytesOrNil(out TransferableOut) []byte {
|
||||
if ws, ok := out.(interface{ Bytes() []byte }); ok {
|
||||
return ws.Bytes()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type innerSortTransferableOutputs struct {
|
||||
outs []*TransferableOutput
|
||||
codec pcodecs.Manager
|
||||
outs []*TransferableOutput
|
||||
}
|
||||
|
||||
func (outs *innerSortTransferableOutputs) Less(i, j int) bool {
|
||||
@@ -106,15 +115,7 @@ func (outs *innerSortTransferableOutputs) Less(i, j int) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
iBytes, err := outs.codec.Marshal(codecVersion, &iOut.Out)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
jBytes, err := outs.codec.Marshal(codecVersion, &jOut.Out)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return bytes.Compare(iBytes, jBytes) == -1
|
||||
return bytes.Compare(wireBytesOrNil(iOut.Out), wireBytesOrNil(jOut.Out)) == -1
|
||||
}
|
||||
|
||||
func (outs *innerSortTransferableOutputs) Len() int {
|
||||
@@ -126,14 +127,15 @@ func (outs *innerSortTransferableOutputs) Swap(i, j int) {
|
||||
o[j], o[i] = o[i], o[j]
|
||||
}
|
||||
|
||||
// SortTransferableOutputs sorts output objects
|
||||
func SortTransferableOutputs(outs []*TransferableOutput, c pcodecs.Manager) {
|
||||
sort.Sort(&innerSortTransferableOutputs{outs: outs, codec: c})
|
||||
// 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, c pcodecs.Manager) bool {
|
||||
return sort.IsSorted(&innerSortTransferableOutputs{outs: outs, codec: c})
|
||||
func IsSortedTransferableOutputs(outs []*TransferableOutput) bool {
|
||||
return sort.IsSorted(&innerSortTransferableOutputs{outs: outs})
|
||||
}
|
||||
|
||||
type TransferableInput struct {
|
||||
@@ -211,7 +213,6 @@ func VerifyTx(
|
||||
feeAssetID ids.ID,
|
||||
allIns [][]*TransferableInput,
|
||||
allOuts [][]*TransferableOutput,
|
||||
c pcodecs.Manager,
|
||||
) error {
|
||||
fc := NewFlowChecker()
|
||||
|
||||
@@ -225,7 +226,7 @@ func VerifyTx(
|
||||
}
|
||||
fc.Produce(out.AssetID(), out.Output().Amount())
|
||||
}
|
||||
if !IsSortedTransferableOutputs(outs, c) {
|
||||
if !IsSortedTransferableOutputs(outs) {
|
||||
return ErrOutputsNotSorted
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,3 +176,45 @@ func wrapOutput(b []byte, tk wire.TypeKind, sk wire.ShapeKind) (verify.State, er
|
||||
}
|
||||
return nil, fmt.Errorf("zap utxo dispatch: unknown (TypeKind=0x%02x, ShapeKind=0x%02x)", tk, sk)
|
||||
}
|
||||
|
||||
// wrapInputBytes is the input-side counterpart of wrapOutput: it
|
||||
// reconstructs a TransferableIn from its fx wire envelope, dispatching on
|
||||
// the (TypeKind, ShapeKind) discriminator. Each branch calls exactly one
|
||||
// fx-package WrapTransferInput / WrapAttestationInput — the fx primitive
|
||||
// owns its own wire; components/lux stays fx-agnostic. Used by
|
||||
// TransferableInput.Unmarshal.
|
||||
func wrapInputBytes(b []byte) (TransferableIn, error) {
|
||||
tk, sk, err := wire.PeekDiscriminator(b)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("peek input discriminator: %w", err)
|
||||
}
|
||||
switch tk {
|
||||
case wire.TypeKindSecp256k1:
|
||||
if sk == wire.ShapeKindTransferInput {
|
||||
return secp256k1fx.WrapTransferInput(b)
|
||||
}
|
||||
case wire.TypeKindMLDSA:
|
||||
if sk == wire.ShapeKindTransferInput {
|
||||
return mldsafx.WrapTransferInput(b)
|
||||
}
|
||||
case wire.TypeKindSLHDSA:
|
||||
if sk == wire.ShapeKindTransferInput {
|
||||
return slhdsafx.WrapTransferInput(b)
|
||||
}
|
||||
case wire.TypeKindEd25519:
|
||||
if sk == wire.ShapeKindTransferInput {
|
||||
return ed25519fx.WrapTransferInput(b)
|
||||
}
|
||||
case wire.TypeKindSecp256r1:
|
||||
if sk == wire.ShapeKindTransferInput {
|
||||
return secp256r1fx.WrapTransferInput(b)
|
||||
}
|
||||
case wire.TypeKindSchnorr:
|
||||
if sk == wire.ShapeKindTransferInput {
|
||||
return schnorrfx.WrapTransferInput(b)
|
||||
}
|
||||
}
|
||||
// bls12381fx attestations are not value-transfer inputs (no Amount),
|
||||
// so they never appear as a TransferableIn.
|
||||
return nil, fmt.Errorf("zap input dispatch: unknown (TypeKind=0x%02x, ShapeKind=0x%02x)", tk, sk)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"github.com/luxfi/metric"
|
||||
"github.com/luxfi/node/cache"
|
||||
"github.com/luxfi/node/cache/metercacher"
|
||||
"github.com/luxfi/node/vms/pcodecs"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -72,8 +71,6 @@ type UTXOWriter interface {
|
||||
}
|
||||
|
||||
type utxoState struct {
|
||||
codec pcodecs.Manager
|
||||
|
||||
// UTXO ID -> *UTXO. If the *UTXO is nil the UTXO doesn't exist
|
||||
utxoCache cache.Cacher[ids.ID, *UTXO]
|
||||
utxoDB database.Database
|
||||
@@ -85,14 +82,16 @@ type utxoState struct {
|
||||
checksum ids.ID
|
||||
}
|
||||
|
||||
// NewUTXOState returns a UTXOState backed by ZAP-native wire bytes (no
|
||||
// codec.Manager): UTXO.Marshal on write, UTXO.Unmarshal on read. The
|
||||
// fx-aware output dispatch that reconstructs the polymorphic Out is wired
|
||||
// via this package's init() (utxo_parser.go) plus the stakeable
|
||||
// LockedOutputHandler registration.
|
||||
func NewUTXOState(
|
||||
db database.Database,
|
||||
codec pcodecs.Manager,
|
||||
trackChecksum bool,
|
||||
) (UTXOState, error) {
|
||||
s := &utxoState{
|
||||
codec: codec,
|
||||
|
||||
utxoCache: &cache.LRU[ids.ID, *UTXO]{Size: utxoCacheSize},
|
||||
utxoDB: prefixdb.New(utxoPrefix, db),
|
||||
|
||||
@@ -106,7 +105,6 @@ func NewUTXOState(
|
||||
|
||||
func NewMeteredUTXOState(
|
||||
db database.Database,
|
||||
codec pcodecs.Manager,
|
||||
metrics metric.Registerer,
|
||||
trackChecksum bool,
|
||||
) (UTXOState, error) {
|
||||
@@ -135,8 +133,6 @@ func NewMeteredUTXOState(
|
||||
}
|
||||
|
||||
s := &utxoState{
|
||||
codec: codec,
|
||||
|
||||
utxoCache: utxoCache,
|
||||
utxoDB: prefixdb.New(utxoPrefix, db),
|
||||
|
||||
@@ -165,9 +161,9 @@ func (s *utxoState) GetUTXO(utxoID ids.ID) (*UTXO, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// The key was in the database
|
||||
// The key was in the database — ZAP-native decode (fx-aware Out dispatch).
|
||||
utxo := &UTXO{}
|
||||
if _, err := s.codec.Unmarshal(bytes, utxo); err != nil {
|
||||
if err := utxo.Unmarshal(bytes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -176,7 +172,9 @@ func (s *utxoState) GetUTXO(utxoID ids.ID) (*UTXO, error) {
|
||||
}
|
||||
|
||||
func (s *utxoState) PutUTXO(utxo *UTXO) error {
|
||||
utxoBytes, err := s.codec.Marshal(codecVersion, utxo)
|
||||
// ZAP-native: the same wire bytes flow to disk and across chains. No
|
||||
// separate codec.Marshal step.
|
||||
utxoBytes, err := utxo.Marshal()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user