Files
zeekayandHanzo Dev 938b06d485 Stop requiring the node this module is imported by
proto required luxfi/node and luxfi/node required proto, with no replace
directive on either side. The cycle was not load-bearing: most of it was proto
reaching into the node for packages proto already owns — p/warp, p/txs,
p/signer, p/stakeable, p/security, x/txs — which now name themselves.

Two were real. The UTXO components moved here, where the chains that use them
already live, and the node is the only other caller, so that dependency now
runs one way. And p/config held the node's chain manager to queue a chain the
P-Chain had accepted a CreateChainTx for; it calls one method, so it declares
one method. A P-Chain that imports the node to describe QueueChainCreation is
what made two modules require each other.

Dropping the requirement dropped a registration with it: lux.ParseUTXO is a
registry filled by an init, which the node used to link in on proto's behalf.
The packages that call it now say so themselves.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-06 12:49:55 -07:00

248 lines
8.1 KiB
Go

// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package txs
// Native-ZAP wire plumbing shared by every X-chain tx type: the (TypeKind,
// ShapeKind) envelope dispatchers that reconstruct polymorphic fx primitives
// (value outputs/inputs, state outputs, operations, credentials) and the
// length-prefixed blob-list codec used for the variable sections. There is no
// pcodecs.Manager and no reflect: an fx primitive names itself on the wire via
// its 2-byte discriminator, and each branch calls exactly one fx-package
// Wrap*. Adding a new (fx, shape) is a new branch — no shared codec to grow.
import (
"fmt"
"github.com/luxfi/ids"
componentslux "github.com/luxfi/proto/components/lux"
"github.com/luxfi/vm/components/verify"
"github.com/luxfi/proto/x/fxs"
lux "github.com/luxfi/utxo"
"github.com/luxfi/utxo/nftfx"
"github.com/luxfi/utxo/propertyfx"
"github.com/luxfi/utxo/secp256k1fx"
"github.com/luxfi/utxo/wire"
"github.com/luxfi/zap"
)
// itemLenStride is the per-element width of the u32 length list that prefixes
// every packed blob list (one length per element, elements concatenated).
const itemLenStride = 4
// childBytes returns the canonical wire envelope of a wire-serializable fx
// value (every production fx primitive exposes Bytes() []byte via its wire.go
// adapter). This is the ONE serialization contract the tx wire composes on.
func childBytes(v any) ([]byte, error) {
ws, ok := v.(interface{ Bytes() []byte })
if !ok {
return nil, fmt.Errorf("xvm txs: %T is not wire-serializable (missing Bytes() []byte)", v)
}
return ws.Bytes(), nil
}
// ---- packed blob list: (u32 length list, concatenated blob) ----
// writeBlobList packs a slice of self-contained buffers as a u32 length list
// plus a concatenated blob. AddUint32 counts elements, so the list length is
// the item count directly.
func writeBlobList(b *zap.Builder, bufs [][]byte) (lenOff, lenCount int, blob []byte) {
if len(bufs) == 0 {
return 0, 0, nil
}
lb := b.StartList(itemLenStride)
for _, buf := range bufs {
lb.AddUint32(uint32(len(buf)))
blob = append(blob, buf...)
}
lenOff, lenCount = lb.Finish()
return lenOff, lenCount, blob
}
// readBlobList slices the blob at blobPtrOff by the u32 lengths at lenPtrOff,
// returning each element's exact bytes (aliasing the parent buffer).
func readBlobList(obj zap.Object, lenPtrOff, blobPtrOff int) ([][]byte, error) {
lengths := obj.ListStride(lenPtrOff, itemLenStride)
n := lengths.Len()
if n == 0 {
return nil, nil
}
blob := obj.Bytes(blobPtrOff)
out := make([][]byte, 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("xvm txs: element %d length %d overruns blob (%d)", i, size, len(blob))
}
out[i] = blob[cursor : cursor+size]
cursor += size
}
return out, nil
}
// ---- TransferableOutput / TransferableInput <-> wire ----
//
// Build side lives in baseTxWire (BaseTx composes XVMTransferOut/In directly so
// the whole tx nests in one buffer). These reconstruct the node types from the
// nested wire accessors on the read side.
// outputFromWire reconstructs a lux.TransferableOutput from a parsed wire
// TransferableOut, dispatching the inner fx Output on its own discriminator.
func outputFromWire(w wire.TransferableOut) (*lux.TransferableOutput, error) {
fxOut, err := componentslux.WrapOutputBytes(w.OutputBytes())
if err != nil {
return nil, err
}
to, ok := fxOut.(lux.TransferableOut)
if !ok {
return nil, fmt.Errorf("xvm txs: decoded output %T is not a lux.TransferableOut", fxOut)
}
return &lux.TransferableOutput{Asset: lux.Asset{ID: w.AssetID()}, Out: to}, nil
}
// inputFromWire reconstructs a lux.TransferableInput from a parsed wire
// TransferableIn, dispatching the inner fx Input on its own discriminator.
func inputFromWire(w wire.TransferableIn) (*lux.TransferableInput, error) {
fxIn, err := componentslux.WrapInputBytes(w.InputBytes())
if err != nil {
return nil, err
}
in, ok := fxIn.(lux.TransferableIn)
if !ok {
return nil, fmt.Errorf("xvm txs: decoded input %T is not a lux.TransferableIn", fxIn)
}
return &lux.TransferableInput{
UTXOID: lux.UTXOID{TxID: w.TxID(), OutputIndex: w.OutputIndex()},
Asset: lux.Asset{ID: w.AssetID()},
In: in,
}, nil
}
// ---- fx state output ([]verify.State in InitialState) ----
// wrapFxState reconstructs a fx state output (the polymorphic verify.State an
// InitialState carries) from its wire envelope. nftfx / propertyfx state
// shapes are dispatched here; every value-transfer fx family (secp256k1,
// mldsa, slhdsa, ed25519, secp256r1, schnorr, bls) is delegated to the shared
// components/lux output dispatcher so this package stays out of their lane.
func wrapFxState(env []byte) (verify.State, error) {
tk, sk, err := wire.PeekDiscriminator(env)
if err != nil {
return nil, err
}
switch tk {
case wire.TypeKindNFT:
switch sk {
case wire.ShapeKindNFTMintOutput:
return nftfx.WrapMintOutput(env)
case wire.ShapeKindNFTTransferOutput:
return nftfx.WrapTransferOutput(env)
}
case wire.TypeKindProperty:
switch sk {
case wire.ShapeKindMintOutput:
return propertyfx.WrapMintOutput(env)
case wire.ShapeKindOwnedOutput:
return propertyfx.WrapOwnedOutput(env)
}
}
return componentslux.WrapOutputBytes(env)
}
// ---- fx operation (fxs.FxOperation in Operation) ----
// wrapFxOperation reconstructs the polymorphic fx operation an Operation
// carries from its wire envelope, dispatched on (TypeKind, ShapeKind).
func wrapFxOperation(env []byte) (fxs.FxOperation, error) {
tk, sk, err := wire.PeekDiscriminator(env)
if err != nil {
return nil, err
}
switch tk {
case wire.TypeKindSecp256k1:
if sk == wire.ShapeKindMintOperation {
return secp256k1fx.WrapMintOperation(env)
}
case wire.TypeKindNFT:
switch sk {
case wire.ShapeKindNFTMintOperation:
return nftfx.WrapMintOperation(env)
case wire.ShapeKindNFTTransferOp:
return nftfx.WrapTransferOperation(env)
}
case wire.TypeKindProperty:
switch sk {
case wire.ShapeKindMintOperation:
return propertyfx.WrapMintOperation(env)
case wire.ShapeKindBurnOperation:
return propertyfx.WrapBurnOperation(env)
}
}
return nil, fmt.Errorf("xvm txs: unknown fx operation (TypeKind=0x%02x, ShapeKind=0x%02x)", tk, sk)
}
// ---- fx credential (verify.Verifiable in FxCredential) ----
// wrapFxCredential reconstructs a fx credential from its wire envelope,
// dispatched on TypeKind (every credential carries ShapeKindCredential).
func wrapFxCredential(env []byte) (verify.Verifiable, error) {
tk, sk, err := wire.PeekDiscriminator(env)
if err != nil {
return nil, err
}
if sk != wire.ShapeKindCredential {
return nil, fmt.Errorf("xvm txs: credential envelope has non-credential shape 0x%02x", sk)
}
switch tk {
case wire.TypeKindSecp256k1:
return secp256k1fx.WrapCredential(env)
case wire.TypeKindNFT:
return nftfx.WrapCredential(env)
case wire.TypeKindProperty:
return propertyfx.WrapCredential(env)
}
return nil, fmt.Errorf("xvm txs: unknown fx credential family (TypeKind=0x%02x)", tk)
}
// ---- UTXOID fixed-stride list (36 bytes: TxID 32B + OutputIndex u32) ----
const utxoIDStride = 36
// writeUTXOIDs packs []*lux.UTXOID as a fixed 36-byte-stride list. AddBytes
// counts bytes, so the caller stores len(ids) as the list count.
func writeUTXOIDs(b *zap.Builder, utxoIDs []*lux.UTXOID) (off, count int) {
if len(utxoIDs) == 0 {
return 0, 0
}
lb := b.StartList(utxoIDStride)
for _, u := range utxoIDs {
var e [utxoIDStride]byte
copy(e[0:], u.TxID[:])
e[32] = byte(u.OutputIndex)
e[33] = byte(u.OutputIndex >> 8)
e[34] = byte(u.OutputIndex >> 16)
e[35] = byte(u.OutputIndex >> 24)
lb.AddBytes(e[:])
}
off, _ = lb.Finish()
return off, len(utxoIDs)
}
// readUTXOIDs reconstructs []*lux.UTXOID from a fixed 36-byte-stride list.
func readUTXOIDs(obj zap.Object, ptrOff int) []*lux.UTXOID {
list := obj.ListStride(ptrOff, utxoIDStride)
n := list.Len()
if n == 0 {
return nil
}
out := make([]*lux.UTXOID, n)
for i := 0; i < n; i++ {
e := list.Object(i, utxoIDStride)
var txID ids.ID
copy(txID[:], e.BytesFixedSlice(0, 32))
out[i] = &lux.UTXOID{TxID: txID, OutputIndex: e.Uint32(32)}
}
return out
}