mirror of
https://github.com/luxfi/utxo.git
synced 2026-07-27 03:39:23 +00:00
wire: TransferableOut/In envelopes (AssetID + inner fx) — multi-asset XVMBaseTx
X-Chain is multi-asset, but XVMBaseTx.OutAt/InAt returned bare fx TransferOutput/TransferInput with no AssetID. Add the reserved cross-fx envelopes TransferableOut (ShapeKindTransferableOut=0x0B) and TransferableIn (ShapeKindTransferableIn=0x0C) in wire/transferable.go, mirroring wire/utxo.go's AssetID+Output layout, and rewire OutAt/InAt to return them so callers recover the per-out/per-in AssetID (and, for inputs, the spent UTXOID). - TransferableOut: AssetID 32B @0, Output bytes @32 (size 40); TypeKindReserved + ShapeKindTransferableOut. - TransferableIn: TxID 32B @0, OutputIndex u32 @32, AssetID 32B @36, Input bytes @68 (size 76); TypeKindReserved + ShapeKindTransferableIn. - Both Wrap* reject trailing bytes (msg.Size()!=len) via ErrTrailingBytes, matching the proven node block/blockwire.go ErrExtraSpace canonical gate. - NewXVMBaseTx unchanged (Outs/Ins are pre-built envelopes). Co-authored-by: Hanzo Dev <dev@hanzo.ai>
This commit is contained in:
@@ -86,6 +86,13 @@ var (
|
||||
ErrWrongTypeKind = errors.New("wire: TypeKind discriminator does not match expected fx family")
|
||||
ErrWrongShapeKind = errors.New("wire: ShapeKind discriminator does not match expected primitive shape")
|
||||
ErrShortEnvelope = errors.New("wire: envelope shorter than 2-byte discriminator prefix")
|
||||
// ErrTrailingBytes is returned when an envelope carries bytes beyond the
|
||||
// self-delimiting ZAP message. zap.Parse truncates to the header size
|
||||
// field, so a buffer with a trailing tail would wrap the same message
|
||||
// but hash to a different id — a malleability vector. Canonical
|
||||
// envelopes MUST consume every byte; mirrors the proven node-side
|
||||
// block/blockwire.go ErrExtraSpace (msg.Size() != len(bytes)).
|
||||
ErrTrailingBytes = errors.New("wire: trailing bytes after zap message (non-canonical envelope)")
|
||||
)
|
||||
|
||||
// EnvelopePrefix is the 2-byte discriminator prefix that every fxs
|
||||
|
||||
@@ -0,0 +1,281 @@
|
||||
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package wire
|
||||
|
||||
import (
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/zap"
|
||||
)
|
||||
|
||||
// This file closes the multi-asset correctness gap in wire.XVMBaseTx.
|
||||
// X-Chain is a universal multi-asset settlement layer, so an output/input
|
||||
// on the wire is NOT a bare fx TransferOutput/TransferInput — it MUST name
|
||||
// the asset it moves. TransferableOut and TransferableIn are the reserved
|
||||
// cross-fx envelopes that bind an AssetID (and, for inputs, the spent
|
||||
// UTXOID) to a concrete inner fx primitive.
|
||||
//
|
||||
// Both are cross-fx CONTAINERS: TypeKind is TypeKindReserved (0x00) — the
|
||||
// envelope itself is not fx-owned; the owning fx TypeKind travels on the
|
||||
// INNER Output/Input envelope's own discriminator prefix. This mirrors the
|
||||
// UTXO envelope (wire/utxo.go), which is likewise reserved-TypeKind and
|
||||
// carries an fx-typed Output.
|
||||
|
||||
// ---- TransferableOut (ShapeKindTransferableOut = 0x0B) ----
|
||||
//
|
||||
// TransferableOut binds an AssetID to a concrete spending Output. It is the
|
||||
// output-list counterpart of UTXO's (AssetID, Output) tail — the same
|
||||
// AssetID+Output layout, minus the UTXOID (TxID+OutputIndex), because a
|
||||
// freshly-created output is not yet a spent-UTXO reference.
|
||||
//
|
||||
// The Output bytes field carries the inner fx primitive's wire envelope
|
||||
// (2-byte discriminator prefix + ZAP message): a TransferOutput, MintOutput,
|
||||
// or one of the NFT* output shapes. Consumers dispatch on the inner
|
||||
// envelope's (TypeKind, ShapeKind) pair via OutputDiscriminator, then
|
||||
// WrapTransferOutput / WrapMintOutput / WrapNFT*.
|
||||
//
|
||||
// Fixed-section layout (size 40 bytes):
|
||||
//
|
||||
// AssetID 32B @ 0
|
||||
// Output bytes @ 32 (relOffset + length, 8 bytes)
|
||||
//
|
||||
// Wire prefix: TypeKind=0x00 (reserved), ShapeKind=0x0B (TransferableOut).
|
||||
const (
|
||||
OffsetTransferableOut_AssetID = 0 // 32B
|
||||
OffsetTransferableOut_Output = 32 // bytes (relOffset + length, 8 bytes)
|
||||
SizeTransferableOut = 40
|
||||
)
|
||||
|
||||
// TransferableOut is the zero-copy typed accessor over a ZAP-encoded
|
||||
// TransferableOut wire envelope.
|
||||
//
|
||||
// READ-ONLY: every field aliases the underlying ZAP buffer. Mutation
|
||||
// corrupts any TxID = hash(buffer) computed downstream. Use
|
||||
// append([]byte(nil), ...) to take ownership of OutputBytes when handing
|
||||
// off to another goroutine.
|
||||
type TransferableOut struct {
|
||||
b []byte
|
||||
msg *zap.Message
|
||||
obj zap.Object
|
||||
}
|
||||
|
||||
// AssetID returns the asset identifier this output moves.
|
||||
func (t TransferableOut) AssetID() ids.ID {
|
||||
var out ids.ID
|
||||
for i := 0; i < 32; i++ {
|
||||
out[i] = t.obj.Uint8(OffsetTransferableOut_AssetID + i)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// OutputBytes returns the inner fx Output wire envelope (2-byte
|
||||
// discriminator prefix + ZAP message). Dispatch on OutputDiscriminator,
|
||||
// then WrapTransferOutput / WrapMintOutput / WrapNFT*.
|
||||
//
|
||||
// READ-ONLY: aliases the underlying buffer.
|
||||
func (t TransferableOut) OutputBytes() []byte {
|
||||
return t.obj.Bytes(OffsetTransferableOut_Output)
|
||||
}
|
||||
|
||||
// OutputDiscriminator returns the (TypeKind, ShapeKind) pair embedded at the
|
||||
// head of OutputBytes(). Returns (0, 0) when OutputBytes is shorter than the
|
||||
// 2-byte prefix. This is the type accessor consumers dispatch on to pick the
|
||||
// inner fx Wrap*.
|
||||
func (t TransferableOut) OutputDiscriminator() (TypeKind, ShapeKind) {
|
||||
b := t.OutputBytes()
|
||||
if len(b) < EnvelopePrefix {
|
||||
return 0, 0
|
||||
}
|
||||
return TypeKind(b[0]), ShapeKind(b[1])
|
||||
}
|
||||
|
||||
// Bytes returns the full wire envelope (2-byte discriminator prefix + ZAP
|
||||
// message). Stable across calls — backed by the originally-parsed buffer.
|
||||
func (t TransferableOut) Bytes() []byte { return t.b }
|
||||
|
||||
// IsZero reports whether the accessor wraps a parsed message.
|
||||
func (t TransferableOut) IsZero() bool { return t.msg == nil }
|
||||
|
||||
// WrapTransferableOut parses a TransferableOut wire envelope into a typed
|
||||
// accessor.
|
||||
//
|
||||
// Returns ErrShortEnvelope when the buffer is shorter than the 2-byte
|
||||
// discriminator prefix; ErrWrongShapeKind when the prefix names a
|
||||
// non-TransferableOut shape; ErrTrailingBytes when the buffer carries bytes
|
||||
// beyond the self-delimiting ZAP message (non-canonical).
|
||||
func WrapTransferableOut(b []byte) (TransferableOut, error) {
|
||||
_, sk, zapBytes, err := readEnvelopePrefix(b)
|
||||
if err != nil {
|
||||
return TransferableOut{}, err
|
||||
}
|
||||
if sk != ShapeKindTransferableOut {
|
||||
return TransferableOut{}, ErrWrongShapeKind
|
||||
}
|
||||
msg, err := zap.Parse(zapBytes)
|
||||
if err != nil {
|
||||
return TransferableOut{}, err
|
||||
}
|
||||
if msg.Size() != len(zapBytes) {
|
||||
return TransferableOut{}, ErrTrailingBytes
|
||||
}
|
||||
return TransferableOut{b: b, msg: msg, obj: msg.Root()}, nil
|
||||
}
|
||||
|
||||
// NewTransferableOut builds a TransferableOut wire envelope from an asset id
|
||||
// and an already-built inner fx Output envelope (from NewTransferOutput /
|
||||
// NewMintOutput / NewNFT*). The inner envelope's bytes are stored verbatim
|
||||
// in the Output field.
|
||||
func NewTransferableOut(assetID ids.ID, innerEnvelope []byte) []byte {
|
||||
capEstimate := zap.HeaderSize + SizeTransferableOut + len(innerEnvelope) + 64
|
||||
b := zap.NewBuilder(capEstimate)
|
||||
|
||||
ob := b.StartObject(SizeTransferableOut)
|
||||
for i := 0; i < 32; i++ {
|
||||
ob.SetUint8(OffsetTransferableOut_AssetID+i, assetID[i])
|
||||
}
|
||||
ob.SetBytes(OffsetTransferableOut_Output, innerEnvelope)
|
||||
ob.FinishAsRoot()
|
||||
return writeEnvelopePrefix(TypeKindReserved, ShapeKindTransferableOut, b.Finish())
|
||||
}
|
||||
|
||||
// ---- TransferableIn (ShapeKindTransferableIn = 0x0C) ----
|
||||
//
|
||||
// TransferableIn binds a UTXO reference (TxID + OutputIndex + AssetID) to a
|
||||
// spending Input. Every consumed input on the wire must name the UTXO it
|
||||
// spends AND the asset that UTXO holds — so an XVMBaseTx input is a
|
||||
// TransferableIn (UTXOID + AssetID + inner fx Input), NOT a bare
|
||||
// TransferInput.
|
||||
//
|
||||
// This mirrors UTXO's fixed-section layout exactly (TxID + OutputIndex +
|
||||
// AssetID + inner-envelope pointer): a TransferableIn IS a reference to a
|
||||
// UTXO plus the Input that unlocks it.
|
||||
//
|
||||
// The Input bytes field carries the inner fx primitive's wire envelope
|
||||
// (2-byte discriminator prefix + ZAP message): a TransferInput. Consumers
|
||||
// dispatch on the inner (TypeKind, ShapeKind) pair via InputDiscriminator,
|
||||
// then WrapTransferInput.
|
||||
//
|
||||
// Fixed-section layout (size 76 bytes; uint32/uint64 reads alignment-tolerant):
|
||||
//
|
||||
// TxID 32B @ 0
|
||||
// OutputIndex uint32 @ 32
|
||||
// AssetID 32B @ 36
|
||||
// Input bytes @ 68 (relOffset + length, 8 bytes)
|
||||
//
|
||||
// Wire prefix: TypeKind=0x00 (reserved), ShapeKind=0x0C (TransferableIn).
|
||||
const (
|
||||
OffsetTransferableIn_TxID = 0 // 32B
|
||||
OffsetTransferableIn_OutputIndex = 32 // uint32
|
||||
OffsetTransferableIn_AssetID = 36 // 32B
|
||||
OffsetTransferableIn_Input = 68 // bytes (relOffset + length, 8 bytes)
|
||||
SizeTransferableIn = 76
|
||||
)
|
||||
|
||||
// TransferableIn is the zero-copy typed accessor over a ZAP-encoded
|
||||
// TransferableIn wire envelope.
|
||||
//
|
||||
// READ-ONLY: every field aliases the underlying ZAP buffer. Use
|
||||
// append([]byte(nil), ...) to take ownership of InputBytes when handing off
|
||||
// to another goroutine.
|
||||
type TransferableIn struct {
|
||||
b []byte
|
||||
msg *zap.Message
|
||||
obj zap.Object
|
||||
}
|
||||
|
||||
// TxID returns the spent UTXO's tx id.
|
||||
func (t TransferableIn) TxID() ids.ID {
|
||||
var out ids.ID
|
||||
for i := 0; i < 32; i++ {
|
||||
out[i] = t.obj.Uint8(OffsetTransferableIn_TxID + i)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// OutputIndex returns the spent UTXO's output index.
|
||||
func (t TransferableIn) OutputIndex() uint32 {
|
||||
return t.obj.Uint32(OffsetTransferableIn_OutputIndex)
|
||||
}
|
||||
|
||||
// AssetID returns the asset identifier the spent UTXO holds.
|
||||
func (t TransferableIn) AssetID() ids.ID {
|
||||
var out ids.ID
|
||||
for i := 0; i < 32; i++ {
|
||||
out[i] = t.obj.Uint8(OffsetTransferableIn_AssetID + i)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// InputBytes returns the inner fx Input wire envelope (2-byte discriminator
|
||||
// prefix + ZAP message). Dispatch on InputDiscriminator, then
|
||||
// WrapTransferInput.
|
||||
//
|
||||
// READ-ONLY: aliases the underlying buffer.
|
||||
func (t TransferableIn) InputBytes() []byte {
|
||||
return t.obj.Bytes(OffsetTransferableIn_Input)
|
||||
}
|
||||
|
||||
// InputDiscriminator returns the (TypeKind, ShapeKind) pair embedded at the
|
||||
// head of InputBytes(). Returns (0, 0) when InputBytes is shorter than the
|
||||
// 2-byte prefix. This is the type accessor consumers dispatch on to pick the
|
||||
// inner fx Wrap*.
|
||||
func (t TransferableIn) InputDiscriminator() (TypeKind, ShapeKind) {
|
||||
b := t.InputBytes()
|
||||
if len(b) < EnvelopePrefix {
|
||||
return 0, 0
|
||||
}
|
||||
return TypeKind(b[0]), ShapeKind(b[1])
|
||||
}
|
||||
|
||||
// Bytes returns the full wire envelope (2-byte discriminator prefix + ZAP
|
||||
// message). Stable across calls — backed by the originally-parsed buffer.
|
||||
func (t TransferableIn) Bytes() []byte { return t.b }
|
||||
|
||||
// IsZero reports whether the accessor wraps a parsed message.
|
||||
func (t TransferableIn) IsZero() bool { return t.msg == nil }
|
||||
|
||||
// WrapTransferableIn parses a TransferableIn wire envelope into a typed
|
||||
// accessor.
|
||||
//
|
||||
// Returns ErrShortEnvelope when the buffer is shorter than the 2-byte
|
||||
// discriminator prefix; ErrWrongShapeKind when the prefix names a
|
||||
// non-TransferableIn shape; ErrTrailingBytes when the buffer carries bytes
|
||||
// beyond the self-delimiting ZAP message (non-canonical).
|
||||
func WrapTransferableIn(b []byte) (TransferableIn, error) {
|
||||
_, sk, zapBytes, err := readEnvelopePrefix(b)
|
||||
if err != nil {
|
||||
return TransferableIn{}, err
|
||||
}
|
||||
if sk != ShapeKindTransferableIn {
|
||||
return TransferableIn{}, ErrWrongShapeKind
|
||||
}
|
||||
msg, err := zap.Parse(zapBytes)
|
||||
if err != nil {
|
||||
return TransferableIn{}, err
|
||||
}
|
||||
if msg.Size() != len(zapBytes) {
|
||||
return TransferableIn{}, ErrTrailingBytes
|
||||
}
|
||||
return TransferableIn{b: b, msg: msg, obj: msg.Root()}, nil
|
||||
}
|
||||
|
||||
// NewTransferableIn builds a TransferableIn wire envelope from a UTXO
|
||||
// reference (txID + outputIndex + assetID) and an already-built inner fx
|
||||
// Input envelope (from NewTransferInput). The inner envelope's bytes are
|
||||
// stored verbatim in the Input field.
|
||||
func NewTransferableIn(txID ids.ID, outputIndex uint32, assetID ids.ID, innerEnvelope []byte) []byte {
|
||||
capEstimate := zap.HeaderSize + SizeTransferableIn + len(innerEnvelope) + 64
|
||||
b := zap.NewBuilder(capEstimate)
|
||||
|
||||
ob := b.StartObject(SizeTransferableIn)
|
||||
for i := 0; i < 32; i++ {
|
||||
ob.SetUint8(OffsetTransferableIn_TxID+i, txID[i])
|
||||
}
|
||||
ob.SetUint32(OffsetTransferableIn_OutputIndex, outputIndex)
|
||||
for i := 0; i < 32; i++ {
|
||||
ob.SetUint8(OffsetTransferableIn_AssetID+i, assetID[i])
|
||||
}
|
||||
ob.SetBytes(OffsetTransferableIn_Input, innerEnvelope)
|
||||
ob.FinishAsRoot()
|
||||
return writeEnvelopePrefix(TypeKindReserved, ShapeKindTransferableIn, b.Finish())
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package wire
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/ids"
|
||||
)
|
||||
|
||||
// innerSecp256k1TransferOutput builds the exact wire envelope that
|
||||
// secp256k1fx.TransferOutput.Bytes() produces (TypeKindSecp256k1 +
|
||||
// ShapeKindTransferOutput). Built via the wire constructor here to avoid the
|
||||
// secp256k1fx -> wire import cycle in this in-package test.
|
||||
func innerSecp256k1TransferOutput(amount uint64, addr ids.ShortID) []byte {
|
||||
return NewTransferOutput(TransferOutputInput{
|
||||
TypeKind: TypeKindSecp256k1,
|
||||
Amount: amount,
|
||||
Locktime: 0,
|
||||
Threshold: 1,
|
||||
Addresses: []ids.ShortID{addr},
|
||||
})
|
||||
}
|
||||
|
||||
// innerSecp256k1TransferInput mirrors secp256k1fx.TransferInput.Bytes().
|
||||
func innerSecp256k1TransferInput(amount uint64, sigIndices []uint32) []byte {
|
||||
return NewTransferInput(TransferInputInput{
|
||||
TypeKind: TypeKindSecp256k1,
|
||||
Amount: amount,
|
||||
SigIndices: sigIndices,
|
||||
})
|
||||
}
|
||||
|
||||
func TestTransferableOut_RoundTrip(t *testing.T) {
|
||||
addr := ids.ShortID{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}
|
||||
inner := innerSecp256k1TransferOutput(2_500_000, addr)
|
||||
assetID := ids.ID{32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1}
|
||||
|
||||
envelope := NewTransferableOut(assetID, inner)
|
||||
|
||||
// Canonical: the envelope must consume every byte (no trailing tail).
|
||||
if _, _, zapBytes, err := readEnvelopePrefix(envelope); err != nil {
|
||||
t.Fatalf("readEnvelopePrefix: %v", err)
|
||||
} else if len(zapBytes) == 0 {
|
||||
t.Fatal("empty zap body")
|
||||
}
|
||||
|
||||
got, err := WrapTransferableOut(envelope)
|
||||
if err != nil {
|
||||
t.Fatalf("WrapTransferableOut: %v", err)
|
||||
}
|
||||
if got.AssetID() != assetID {
|
||||
t.Errorf("AssetID: got %x, want %x", got.AssetID(), assetID)
|
||||
}
|
||||
outBytes := got.OutputBytes()
|
||||
if !bytes.Equal(outBytes, inner) {
|
||||
t.Errorf("OutputBytes mismatch: got %x, want %x", outBytes, inner)
|
||||
}
|
||||
tk, sk := got.OutputDiscriminator()
|
||||
if tk != TypeKindSecp256k1 {
|
||||
t.Errorf("OutputDiscriminator TypeKind: got %x, want %x", tk, TypeKindSecp256k1)
|
||||
}
|
||||
if sk != ShapeKindTransferOutput {
|
||||
t.Errorf("OutputDiscriminator ShapeKind: got %x, want %x", sk, ShapeKindTransferOutput)
|
||||
}
|
||||
|
||||
// Round-trip the inner secp256k1fx output.
|
||||
innerGot, err := WrapTransferOutput(outBytes)
|
||||
if err != nil {
|
||||
t.Fatalf("WrapTransferOutput inner: %v", err)
|
||||
}
|
||||
if innerGot.Amount() != 2_500_000 {
|
||||
t.Errorf("inner Amount: got %d, want 2_500_000", innerGot.Amount())
|
||||
}
|
||||
if innerGot.TypeKind() != TypeKindSecp256k1 {
|
||||
t.Errorf("inner TypeKind: got %x, want %x", innerGot.TypeKind(), TypeKindSecp256k1)
|
||||
}
|
||||
|
||||
// Wrong-shape rejection: a TransferableOut buffer must not Wrap as
|
||||
// TransferableIn.
|
||||
if _, err := WrapTransferableIn(envelope); err != ErrWrongShapeKind {
|
||||
t.Errorf("WrapTransferableIn(out envelope): got %v, want ErrWrongShapeKind", err)
|
||||
}
|
||||
|
||||
// Canonical rejection: a trailing byte must be refused.
|
||||
tampered := append(append([]byte(nil), envelope...), 0xFF)
|
||||
if _, err := WrapTransferableOut(tampered); err != ErrTrailingBytes {
|
||||
t.Errorf("WrapTransferableOut(trailing): got %v, want ErrTrailingBytes", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTransferableIn_RoundTrip(t *testing.T) {
|
||||
inner := innerSecp256k1TransferInput(2_500_000, []uint32{0, 3})
|
||||
txID := ids.ID{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32}
|
||||
assetID := ids.ID{32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1}
|
||||
|
||||
envelope := NewTransferableIn(txID, 7, assetID, inner)
|
||||
|
||||
got, err := WrapTransferableIn(envelope)
|
||||
if err != nil {
|
||||
t.Fatalf("WrapTransferableIn: %v", err)
|
||||
}
|
||||
if got.TxID() != txID {
|
||||
t.Errorf("TxID: got %x, want %x", got.TxID(), txID)
|
||||
}
|
||||
if got.OutputIndex() != 7 {
|
||||
t.Errorf("OutputIndex: got %d, want 7", got.OutputIndex())
|
||||
}
|
||||
if got.AssetID() != assetID {
|
||||
t.Errorf("AssetID: got %x, want %x", got.AssetID(), assetID)
|
||||
}
|
||||
inBytes := got.InputBytes()
|
||||
if !bytes.Equal(inBytes, inner) {
|
||||
t.Errorf("InputBytes mismatch: got %x, want %x", inBytes, inner)
|
||||
}
|
||||
tk, sk := got.InputDiscriminator()
|
||||
if tk != TypeKindSecp256k1 {
|
||||
t.Errorf("InputDiscriminator TypeKind: got %x, want %x", tk, TypeKindSecp256k1)
|
||||
}
|
||||
if sk != ShapeKindTransferInput {
|
||||
t.Errorf("InputDiscriminator ShapeKind: got %x, want %x", sk, ShapeKindTransferInput)
|
||||
}
|
||||
|
||||
// Round-trip the inner secp256k1fx input.
|
||||
innerGot, err := WrapTransferInput(inBytes)
|
||||
if err != nil {
|
||||
t.Fatalf("WrapTransferInput inner: %v", err)
|
||||
}
|
||||
if innerGot.Amount() != 2_500_000 {
|
||||
t.Errorf("inner Amount: got %d, want 2_500_000", innerGot.Amount())
|
||||
}
|
||||
sigs := innerGot.SigIndices()
|
||||
if len(sigs) != 2 || sigs[0] != 0 || sigs[1] != 3 {
|
||||
t.Errorf("inner SigIndices: got %v, want [0 3]", sigs)
|
||||
}
|
||||
|
||||
// Wrong-shape + canonical rejection.
|
||||
if _, err := WrapTransferableOut(envelope); err != ErrWrongShapeKind {
|
||||
t.Errorf("WrapTransferableOut(in envelope): got %v, want ErrWrongShapeKind", err)
|
||||
}
|
||||
tampered := append(append([]byte(nil), envelope...), 0xFF)
|
||||
if _, err := WrapTransferableIn(tampered); err != ErrTrailingBytes {
|
||||
t.Errorf("WrapTransferableIn(trailing): got %v, want ErrTrailingBytes", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestXVMBaseTx_MultiAsset is the end-to-end proof that the multi-asset gap is
|
||||
// closed: an XVMBaseTx built from TransferableOut/TransferableIn envelopes
|
||||
// yields per-out/per-in AssetIDs via OutAt/InAt.
|
||||
func TestXVMBaseTx_MultiAsset(t *testing.T) {
|
||||
addr := ids.ShortID{9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9}
|
||||
assetA := ids.ID{0xA1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}
|
||||
assetB := ids.ID{0xB2, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}
|
||||
txID := ids.ID{7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7}
|
||||
|
||||
out0 := NewTransferableOut(assetA, innerSecp256k1TransferOutput(1_000, addr))
|
||||
out1 := NewTransferableOut(assetB, innerSecp256k1TransferOutput(2_000, addr))
|
||||
in0 := NewTransferableIn(txID, 0, assetA, innerSecp256k1TransferInput(1_000, []uint32{0}))
|
||||
in1 := NewTransferableIn(txID, 1, assetB, innerSecp256k1TransferInput(2_000, []uint32{0}))
|
||||
|
||||
envelope := NewXVMBaseTx(XVMBaseTxInput{
|
||||
NetworkID: 96369,
|
||||
BlockchainID: [32]byte{1, 2, 3},
|
||||
Outs: [][]byte{out0, out1},
|
||||
Ins: [][]byte{in0, in1},
|
||||
Memo: []byte("multi-asset"),
|
||||
})
|
||||
|
||||
tx, err := WrapXVMBaseTx(envelope)
|
||||
if err != nil {
|
||||
t.Fatalf("WrapXVMBaseTx: %v", err)
|
||||
}
|
||||
if tx.OutsCount() != 2 {
|
||||
t.Fatalf("OutsCount: got %d, want 2", tx.OutsCount())
|
||||
}
|
||||
if tx.InsCount() != 2 {
|
||||
t.Fatalf("InsCount: got %d, want 2", tx.InsCount())
|
||||
}
|
||||
|
||||
// Outputs carry their AssetIDs (the gap that OutAt previously dropped).
|
||||
gotOut0, err := tx.OutAt(0)
|
||||
if err != nil {
|
||||
t.Fatalf("OutAt(0): %v", err)
|
||||
}
|
||||
if gotOut0.AssetID() != assetA {
|
||||
t.Errorf("OutAt(0).AssetID: got %x, want %x", gotOut0.AssetID(), assetA)
|
||||
}
|
||||
gotOut1, err := tx.OutAt(1)
|
||||
if err != nil {
|
||||
t.Fatalf("OutAt(1): %v", err)
|
||||
}
|
||||
if gotOut1.AssetID() != assetB {
|
||||
t.Errorf("OutAt(1).AssetID: got %x, want %x", gotOut1.AssetID(), assetB)
|
||||
}
|
||||
inner1, err := WrapTransferOutput(gotOut1.OutputBytes())
|
||||
if err != nil {
|
||||
t.Fatalf("inner WrapTransferOutput: %v", err)
|
||||
}
|
||||
if inner1.Amount() != 2_000 {
|
||||
t.Errorf("OutAt(1) inner Amount: got %d, want 2000", inner1.Amount())
|
||||
}
|
||||
|
||||
// Inputs carry their UTXOID + AssetID (the gap that InAt previously dropped).
|
||||
gotIn0, err := tx.InAt(0)
|
||||
if err != nil {
|
||||
t.Fatalf("InAt(0): %v", err)
|
||||
}
|
||||
if gotIn0.AssetID() != assetA || gotIn0.TxID() != txID || gotIn0.OutputIndex() != 0 {
|
||||
t.Errorf("InAt(0): got (asset=%x, tx=%x, idx=%d)", gotIn0.AssetID(), gotIn0.TxID(), gotIn0.OutputIndex())
|
||||
}
|
||||
gotIn1, err := tx.InAt(1)
|
||||
if err != nil {
|
||||
t.Fatalf("InAt(1): %v", err)
|
||||
}
|
||||
if gotIn1.AssetID() != assetB || gotIn1.OutputIndex() != 1 {
|
||||
t.Errorf("InAt(1): got (asset=%x, idx=%d), want (assetB, 1)", gotIn1.AssetID(), gotIn1.OutputIndex())
|
||||
}
|
||||
if !bytes.Equal(tx.Memo(), []byte("multi-asset")) {
|
||||
t.Errorf("Memo: got %q, want %q", tx.Memo(), "multi-asset")
|
||||
}
|
||||
}
|
||||
+20
-12
@@ -14,10 +14,14 @@ import "github.com/luxfi/zap"
|
||||
//
|
||||
// NetworkID uint32 — network this chain lives on
|
||||
// BlockchainID 32B — chain id (replay-protection)
|
||||
// Outs []TransferableOutput (each carries its own fx TypeKind)
|
||||
// Ins []TransferableInput (each carries its own fx TypeKind)
|
||||
// Outs []TransferableOut (AssetID + inner fx Output)
|
||||
// Ins []TransferableIn (UTXOID + AssetID + inner fx Input)
|
||||
// Memo bytes — arbitrary, up to MaxMemoSize on the verifier
|
||||
//
|
||||
// X-Chain is multi-asset: each out/in is a TransferableOut / TransferableIn
|
||||
// envelope that NAMES the asset it moves (the inner fx Output/Input carries
|
||||
// the owning fx TypeKind). See wire/transferable.go.
|
||||
//
|
||||
// Outs/Ins are variable-stride byte envelopes (each carries its own
|
||||
// TypeKind+ShapeKind+ZAP message of independent length). The list is
|
||||
// encoded as `Count uint32 + Bytes (concatenated envelopes)` — the same
|
||||
@@ -75,7 +79,7 @@ func (t XVMBaseTx) OutsCount() uint32 {
|
||||
return t.obj.Uint32(OffsetXVMBaseTx_OutsCount)
|
||||
}
|
||||
|
||||
// OutsBytes returns the concatenated TransferableOutput envelopes blob.
|
||||
// OutsBytes returns the concatenated TransferableOut envelopes blob.
|
||||
// Each entry is a self-describing wire envelope; see OutAt for the
|
||||
// index walk.
|
||||
//
|
||||
@@ -84,13 +88,15 @@ func (t XVMBaseTx) OutsBytes() []byte {
|
||||
return t.obj.Bytes(OffsetXVMBaseTx_OutsBytes)
|
||||
}
|
||||
|
||||
// OutAt parses the i'th TransferableOutput envelope.
|
||||
func (t XVMBaseTx) OutAt(i uint32) (TransferOutput, error) {
|
||||
// OutAt parses the i'th TransferableOut envelope (AssetID + inner fx
|
||||
// Output). Callers get the AssetID via the returned accessor — X-Chain is
|
||||
// multi-asset, so every output names the asset it moves.
|
||||
func (t XVMBaseTx) OutAt(i uint32) (TransferableOut, error) {
|
||||
env, err := nthEnvelope(t.OutsBytes(), t.OutsCount(), i)
|
||||
if err != nil {
|
||||
return TransferOutput{}, err
|
||||
return TransferableOut{}, err
|
||||
}
|
||||
return WrapTransferOutput(env)
|
||||
return WrapTransferableOut(env)
|
||||
}
|
||||
|
||||
// InsCount returns the number of transferable inputs.
|
||||
@@ -98,20 +104,22 @@ func (t XVMBaseTx) InsCount() uint32 {
|
||||
return t.obj.Uint32(OffsetXVMBaseTx_InsCount)
|
||||
}
|
||||
|
||||
// InsBytes returns the concatenated TransferableInput envelopes blob.
|
||||
// InsBytes returns the concatenated TransferableIn envelopes blob.
|
||||
//
|
||||
// READ-ONLY: aliases the underlying buffer.
|
||||
func (t XVMBaseTx) InsBytes() []byte {
|
||||
return t.obj.Bytes(OffsetXVMBaseTx_InsBytes)
|
||||
}
|
||||
|
||||
// InAt parses the i'th TransferableInput envelope.
|
||||
func (t XVMBaseTx) InAt(i uint32) (TransferInput, error) {
|
||||
// InAt parses the i'th TransferableIn envelope (UTXOID + AssetID + inner fx
|
||||
// Input). Callers get the spent UTXO reference + AssetID via the returned
|
||||
// accessor.
|
||||
func (t XVMBaseTx) InAt(i uint32) (TransferableIn, error) {
|
||||
env, err := nthEnvelope(t.InsBytes(), t.InsCount(), i)
|
||||
if err != nil {
|
||||
return TransferInput{}, err
|
||||
return TransferableIn{}, err
|
||||
}
|
||||
return WrapTransferInput(env)
|
||||
return WrapTransferableIn(env)
|
||||
}
|
||||
|
||||
// Memo returns the memo bytes.
|
||||
|
||||
Reference in New Issue
Block a user