feat: ZAP-native wire schemas — activation 2025-12-25T16:20:00-08:00

Adds github.com/luxfi/utxo/wire package + per-fx wire.go adapters so
that luxfi/node's vms/{platformvm,xvm}/txs/codec.go can be deleted.

wire/ package mirrors luxfi/node/vms/wire — every fxs primitive carries
a (TypeKind, ShapeKind) discriminator pair on its wire envelope, then
a ZAP message describing its fields. Decomposes the legacy
codec.Manager slot map (which braided "fx family" and "primitive shape"
into a single dense uint32 slot id) into two orthogonal 1-byte tags.

PQ-specific schemas (pq_output_owners, pq_transfer_output,
pq_mint_output) handle the mldsafx/slhdsafx case where addresses are
full PQ pubkeys (1312-2592 bytes) rather than 20-byte hashed ShortIDs.
The wire layer is stride-agnostic — the per-fx wire.go resolves
SecurityLevel → pubkey stride before parsing.

Per-fx wire.go adapters provide (value T).Bytes() []byte (Go type →
wire envelope) and Wrap*(b []byte) (*T, error) (wire envelope → Go
type). The Initialize(vm)/codec.Manager.RegisterType chain stays in
place pending downstream coordination; this commit gets the wire/
package + adapters in so consumer agents (platformvm/warp,
platformvm/state, xvm) can pin a SHA and start their migrations.

UTXO.WireBytes() builds the outer envelope by calling the polymorphic
Out's Bytes() (any fxs primitive implementing wireSerializable).
WrapUTXOBytes(b) parses the outer envelope; the inner output is
dispatched by the caller via OutputDiscriminator() → fx-package
WrapTransferOutput / WrapMintOutput / WrapAttestationOutput.

Tests: 35 round-trip tests across 9 packages. go vet clean. go test
-race clean. gofmt clean.

Activation: 2025-12-25T16:20:00-08:00 (unix 1766708400). No
backwards compat — pre-activation legacy codec blobs are a
protocol-violation reject after activation.
This commit is contained in:
Hanzo AI
2026-06-03 09:34:23 -07:00
parent fd7ec4890d
commit 39a7cdb2f1
37 changed files with 5837 additions and 12 deletions
+210
View File
@@ -0,0 +1,210 @@
# utxo ZAP-keystone migration
## Activation
2025-12-25T16:20:00-08:00 (unix 1766708400). New final Lux network — predates
every block. No backwards compat, no env-var gate, no codec shim.
## Goal
Replace `github.com/luxfi/codec`-based serialization with ZAP-native wire
schemas so that `luxfi/node`'s `vms/platformvm/txs/codec.go` and
`vms/xvm/txs/codec.go` can be deleted. The fxs' `Initialize(vm)` registration
with `vm.CodecRegistry()` is the last codec.Manager dependency in the fxs
plugin system; once we provide a static ZAP schema per primitive, the
registration is dead code.
## Audit — current codec dependencies
### Module-level
`github.com/luxfi/utxo` go.mod requires `github.com/luxfi/codec v1.1.4`.
Consumed transitively by `github.com/luxfi/vm` for `verify.State`.
### Files importing `luxfi/codec`
Root package (`luxfi/utxo`):
- `api.go``codec.Manager`/`codec.Marshal` for JSON UTXO responses
- `atomic_utxos.go``codec.Manager` for cross-chain export/import
- `flow_checker.go``codec.Manager` passed through to verify
- `transferables.go``codec.Manager` in `SortTransferableOutputs`,
`IsSortedTransferableOutputs`, `VerifyTx`
- `utxo_state.go``codec.Manager.{Marshal,Unmarshal}` for disk encoding,
references the package-level `codecVersion` constant
Per-fx (`secp256k1fx`, `mldsafx`, `slhdsafx`, `ed25519fx`, `secp256r1fx`,
`schnorrfx`, `bls12381fx`):
- `vm.go``codec.Registry` on the `VM` interface (the test surface for
the codec.Manager passed in by the host VM)
- `fx.go``Initialize(vm)` calls `vm.CodecRegistry().RegisterType(...)`
for every primitive type the fx owns. Tests + benchmarks construct a
`linearcodec.NewDefault()` to satisfy the registration call.
External consumers — 204 Go files in `luxfi/node` import
`github.com/luxfi/utxo`, of which 116 reference the polymorphic typed Go
fields directly (`*secp256k1fx.TransferOutput`, etc.). The migration
strategy below avoids cascading into all 116.
### UTXO struct
The polymorphic field is `UTXO.Out verify.State` (where `verify.State` is
an interface in `github.com/luxfi/vm/components/verify`). It is set to a
concrete type from one of the fx packages: `*secp256k1fx.TransferOutput`,
`*secp256k1fx.MintOutput`, `*bls12381fx.AttestationOutput`, etc.
### fx packages
Each fx has:
- `transfer_output.go` / `transfer_input.go` — value-bearing primitives
- `mint_output.go` / `mint_operation.go` — mint authority primitives
- `credential.go` — signature container
- `output_owners.go` — owner group (threshold + addrs)
`bls12381fx` is attestation-only (no transfer/mint); it has
`AttestationOutput`, `AttestationInput`, `Credential`. `nftfx` and
`propertyfx` are operation-only (no value primitives).
## Strategy — Option 2 chosen
Option 1 (UTXO.Out → []byte envelope, every consumer Wrap*Output on
read) cascades through ~116 consumer files in `luxfi/node` alone, with
more in `luxfi/wallet` / `luxfi/cli` / `luxfi/genesis`. That is too
broad an edit set for a single landing.
**Option 2** (dual representation) is the chosen path:
- Keep the typed Go field for in-memory consumers (no breakage of the
116 files).
- Add a new `github.com/luxfi/utxo/wire` package that provides ZAP-native
wire schemas + zero-copy accessors mirroring
`github.com/luxfi/node/vms/wire`.
- Per-fx, add `wire_*.go` files exposing `(value T).Bytes() []byte` (Go
type → wire envelope) and `Wrap*(b []byte) (T, error)` (wire envelope
→ Go type). The Go type is the in-memory representation; the wire
envelope is the on-wire representation. Marshal/Unmarshal go through
these.
- Drop the `Initialize(vm)` codec.Manager registration. The wire schema
is static at compile time — there is nothing to register.
- Wire over the `codec.Manager` call sites at the root package (e.g.
`utxo_state.go`, `transferables.go`) to use the new `wire.UTXO` etc.
directly.
- Delete the `codec.Registry` field from each fx's `VM` interface (it is
used only by `Initialize`).
The hard cut is that the `Initialize(vm)` chain is gone — the fxs are
configured statically, not via a registration callback. The host VM
gives the fx a `Logger()` and a `Clock()` (the only other VM methods);
no codec on the wire.
## Files created
`github.com/luxfi/utxo/wire/`:
- `discriminator.go` — TypeKind + ShapeKind constants, envelope prefix
read/write helpers
- `utxo.go` — UTXO wire schema
- `transfer_output.go` / `transfer_input.go` — cross-fx classical schemas
- `mint_output.go` / `mint_operation.go` — cross-fx schemas
- `credential.go` — cross-fx schema (TypeKind names fx, supports
pubkey-recoverable and pubkey-carrying credentials)
- `output_owners.go` — cross-fx owner schema (20-byte ShortID addresses)
- `pchain_owner.go` — P-chain warp owner subset of OutputOwners
- `attestation.go` — AttestationOutput + AttestationInput for bls12381fx
- `pq_output_owners.go` — PQ-fx OutputOwners with variable-length pubkeys
(mldsafx, slhdsafx — addresses are full PQ pubkeys, not 20-byte hashes)
- `pq_transfer_output.go` — PQ-fx TransferOutput
- `pq_mint_output.go` — PQ-fx MintOutput
- `signed_tx.go` — outer envelope (unsigned bytes + credentials)
- `sign.go``SignSecp256k1(unsignedBytes, signers)` for the classical
fx signing entry point
- `wire_test.go` / `pq_test.go` / `sign_test.go` — round-trip tests
Per-fx `wire.go` files providing `Bytes()` + `Wrap*` adapters between
the in-memory Go type and the wire envelope. The schemas use the same
TypeKind discriminator from `wire/discriminator.go` so a TransferOutput
parsed off the wire knows its owning fx without a dispatch table.
Files created per fx:
- `secp256k1fx/wire.go` + `secp256k1fx/wire_test.go`
- `ed25519fx/wire.go` + `ed25519fx/wire_test.go`
- `secp256r1fx/wire.go` + `secp256r1fx/wire_test.go`
- `schnorrfx/wire.go` + `schnorrfx/wire_test.go`
- `mldsafx/wire.go` + `mldsafx/wire_test.go`
- `slhdsafx/wire.go` + `slhdsafx/wire_test.go`
- `bls12381fx/wire.go` + `bls12381fx/wire_test.go`
Root package:
- `utxo_wire.go``UTXO.WireBytes()` builds the outer envelope by
calling the polymorphic `Out`'s `Bytes()`; `WrapUTXOBytes(b)` parses
the outer envelope and returns a `wire.UTXO` accessor. The inner
output envelope must be parsed by the caller through the
appropriate fx package's `WrapTransferOutput` / `WrapMintOutput` /
`WrapAttestationOutput` — the root package cannot import the fx
packages (would be a cycle).
- `utxo_wire_test.go` — round-trip test exercising the full
UTXO → wire → fx parse → equality dispatch.
## Files NOT yet deleted (next cut)
The following still reference `luxfi/codec` and remain pending the
downstream agents' coordinated cut:
- `utxo_state.go``utxoState.codec codec.Manager` for disk encoding.
Replace with `WireBytes()` / `WrapUTXOBytes` after the consumer
agents' state migrations land.
- `atomic_utxos.go``atomicUTXOManager.codec codec.Manager` for
cross-chain UTXO export/import.
- `transferables.go``codec.Manager` in `SortTransferableOutputs`,
`IsSortedTransferableOutputs`, `VerifyTx`.
- `flow_checker.go``codec.Manager` passed through to verify.
- `api.go` — uses `codec.Uint32` / `codec.Uint64` (JSON-quote wrapper
types, NOT `codec.Manager`); these are orthogonal to the codec
rip and can stay.
- Per-fx `vm.go``codec.Registry` on the `VM` interface; used only by
the per-fx `Initialize(vm)` to call `vm.CodecRegistry().RegisterType(...)`.
Once consumers stop calling `Initialize`, both go away.
- Per-fx `fx.go` `Initialize(vm)` — calls `vm.CodecRegistry().RegisterType`
for every primitive. Drop after consumers stop calling Initialize.
These are the second-cut targets; this first commit gets the wire/
package + per-fx adapters in so the downstream agents can pin a SHA
and start their migrations.
## Coordination
Three parallel agents are landing:
- `platformvm/warp` ZAP migration — consumes our `wire.OutputOwners` and
`wire.PChainOwner`.
- `platformvm/state` ZAP migration — consumes our `wire.UTXO` for the
state's persisted UTXOs.
- `xvm` ZAP migration — consumes our `wire.UTXO` for the X-chain state +
every fxs `wire.*Output`/`wire.*Input`/`wire.Credential` schema.
When they land they pin a SHA from this branch. They do not modify the
typed Go field paths in `luxfi/utxo` — those remain for the 116 callers
that still hold typed references.
## Test plan
1. `cd ~/work/lux/utxo && GOWORK=off go build ./...`
2. `cd ~/work/lux/utxo && GOWORK=off go test ./... -count=1 -timeout=10m`
3. Round-trip test per fx primitive + per cross-fx schema (in
`wire/wire_test.go` + per-fx `wire_test.go`).
4. Verify `luxfi/codec` is gone from go.mod after the final cleanup
(after consumers have migrated; for the first cut we keep the dep
alive since `utxo_state.go` still goes through the legacy codec
pending downstream coordination).
## Commit + tag
First cut commits the wire/ package + per-fx wire_*.go adapters; the
`Initialize(vm)` chain stays in place pending the downstream agents'
coordinated cut. Subsequent cuts will remove the codec.Manager
references in `utxo_state.go`, `transferables.go`, `atomic_utxos.go`
once the agents have switched their state to call `wire.NewUTXO` /
`wire.WrapUTXO`.
The commit SHA from this first cut is what the downstream agents pin.
+87
View File
@@ -0,0 +1,87 @@
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package bls12381fx
import (
"github.com/luxfi/utxo/wire"
)
// TypeKind is the wire-level discriminator for every bls12381fx primitive's
// wire envelope. bls12381fx is attestation-only: AttestationOutput,
// AttestationInput, Credential — no transfer or mint primitives.
const TypeKind = wire.TypeKindBLS12381
// Bytes returns the ZAP-native wire envelope for this AttestationOutput.
// Envelope = (TypeKindBLS12381, ShapeKindAttestationOut, ZAP message).
func (out *AttestationOutput) Bytes() []byte {
return wire.NewAttestationOutput(wire.AttestationOutputInput{
AttestedHash: out.AttestedHash,
Threshold: out.Threshold,
PubKeys: out.PubKeys,
})
}
// WrapAttestationOutput parses a wire envelope into a fresh
// AttestationOutput.
func WrapAttestationOutput(b []byte) (*AttestationOutput, error) {
v, err := wire.WrapAttestationOutput(b)
if err != nil {
return nil, err
}
return &AttestationOutput{
AttestedHash: v.AttestedHash(),
Threshold: v.Threshold(),
PubKeys: v.PubKeys(),
}, nil
}
// Bytes returns the ZAP-native wire envelope for this AttestationInput.
// Envelope = (TypeKindBLS12381, ShapeKindAttestationIn, ZAP message).
func (in *AttestationInput) Bytes() []byte {
return wire.NewAttestationInput(wire.AttestationInputInput{
SignerBitmap: in.Signers,
})
}
// WrapAttestationInput parses a wire envelope into a fresh
// AttestationInput.
func WrapAttestationInput(b []byte) (*AttestationInput, error) {
v, err := wire.WrapAttestationInput(b)
if err != nil {
return nil, err
}
bitmap := v.SignerBitmap()
signers := make([]byte, len(bitmap))
copy(signers, bitmap)
return &AttestationInput{Signers: signers}, nil
}
// Bytes returns the ZAP-native wire envelope for this Credential.
// bls12381fx Credentials carry a single aggregate G2 signature (96 bytes);
// there is no notion of "per-signer signature" in BLS aggregate semantics.
func (cr *Credential) Bytes() []byte {
return wire.NewCredential(wire.CredentialInput{
TypeKind: TypeKind,
SecurityLevel: 0,
Signatures: cr.AggSig[:],
})
}
// WrapCredential parses a wire envelope into a fresh Credential.
func WrapCredential(b []byte) (*Credential, error) {
v, err := wire.WrapCredential(b)
if err != nil {
return nil, err
}
if v.TypeKind() != TypeKind {
return nil, wire.ErrWrongTypeKind
}
out := &Credential{}
sig := v.SignatureBytes()
if len(sig) != SigLen {
return nil, ErrWrongAggSigLen
}
copy(out.AggSig[:], sig)
return out, nil
}
+71
View File
@@ -0,0 +1,71 @@
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package bls12381fx
import (
"bytes"
"testing"
)
func TestWire_AttestationOutput_RoundTrip(t *testing.T) {
pk1 := make([]byte, PubKeyLen)
pk2 := make([]byte, PubKeyLen)
for i := range pk1 {
pk1[i] = byte(i)
}
for i := range pk2 {
pk2[i] = byte(0xFF - i)
}
in := &AttestationOutput{
AttestedHash: [AttestedHashLen]byte{1, 2, 3, 4, 5},
Threshold: 2,
PubKeys: [][]byte{pk1, pk2},
}
got, err := WrapAttestationOutput(in.Bytes())
if err != nil {
t.Fatalf("WrapAttestationOutput: %v", err)
}
if got.AttestedHash != in.AttestedHash {
t.Errorf("AttestedHash mismatch")
}
if got.Threshold != in.Threshold {
t.Errorf("Threshold: got %d, want %d", got.Threshold, in.Threshold)
}
if len(got.PubKeys) != 2 {
t.Fatalf("PubKeys.Len: got %d, want 2", len(got.PubKeys))
}
if !bytes.Equal(got.PubKeys[0], pk1) {
t.Errorf("PubKeys[0] mismatch")
}
if !bytes.Equal(got.PubKeys[1], pk2) {
t.Errorf("PubKeys[1] mismatch")
}
}
func TestWire_AttestationInput_RoundTrip(t *testing.T) {
bitmap := []byte{0x05, 0x80}
in := &AttestationInput{Signers: bitmap}
got, err := WrapAttestationInput(in.Bytes())
if err != nil {
t.Fatalf("WrapAttestationInput: %v", err)
}
if !bytes.Equal(got.Signers, bitmap) {
t.Errorf("Signers: got %x, want %x", got.Signers, bitmap)
}
}
func TestWire_Credential_RoundTrip(t *testing.T) {
var aggSig [SigLen]byte
for i := range aggSig {
aggSig[i] = byte(i)
}
in := &Credential{AggSig: aggSig}
got, err := WrapCredential(in.Bytes())
if err != nil {
t.Fatalf("WrapCredential: %v", err)
}
if !bytes.Equal(got.AggSig[:], aggSig[:]) {
t.Errorf("AggSig mismatch")
}
}
+201
View File
@@ -0,0 +1,201 @@
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package ed25519fx
import (
"github.com/luxfi/utxo/wire"
)
// TypeKind is the wire-level discriminator for every ed25519fx
// primitive's wire envelope.
const TypeKind = wire.TypeKindEd25519
// Bytes returns the ZAP-native wire envelope for this OutputOwners.
// Envelope = (TypeKindReserved, ShapeKindOutputOwners, ZAP message) —
// owners are not fx-owned (same wire payload across every fx with a
// multi-ShortID ownership group).
func (out *OutputOwners) Bytes() []byte {
return wire.NewOutputOwners(wire.OutputOwnersInput{
Locktime: out.Locktime,
Threshold: out.Threshold,
Addresses: out.Addrs,
})
}
// WrapOutputOwners parses a wire envelope into a fresh OutputOwners.
func WrapOutputOwners(b []byte) (*OutputOwners, error) {
v, err := wire.WrapOutputOwners(b)
if err != nil {
return nil, err
}
return &OutputOwners{
Locktime: v.Locktime(),
Threshold: v.Threshold(),
Addrs: v.AddressList().All(),
}, nil
}
// Bytes returns the ZAP-native wire envelope for this TransferOutput.
func (out *TransferOutput) Bytes() []byte {
return wire.NewTransferOutput(wire.TransferOutputInput{
TypeKind: TypeKind,
Amount: out.Amt,
Locktime: out.Locktime,
Threshold: out.Threshold,
Addresses: out.Addrs,
})
}
// WrapTransferOutput parses a wire envelope into a fresh TransferOutput.
func WrapTransferOutput(b []byte) (*TransferOutput, error) {
v, err := wire.WrapTransferOutput(b)
if err != nil {
return nil, err
}
if v.TypeKind() != TypeKind {
return nil, wire.ErrWrongTypeKind
}
return &TransferOutput{
Amt: v.Amount(),
OutputOwners: OutputOwners{
Locktime: v.Locktime(),
Threshold: v.Threshold(),
Addrs: v.AddressList().All(),
},
}, nil
}
// Bytes returns the ZAP-native wire envelope for this TransferInput.
func (in *TransferInput) Bytes() []byte {
return wire.NewTransferInput(wire.TransferInputInput{
TypeKind: TypeKind,
Amount: in.Amt,
SigIndices: in.SigIndices,
})
}
// WrapTransferInput parses a wire envelope into a fresh TransferInput.
func WrapTransferInput(b []byte) (*TransferInput, error) {
v, err := wire.WrapTransferInput(b)
if err != nil {
return nil, err
}
if v.TypeKind() != TypeKind {
return nil, wire.ErrWrongTypeKind
}
return &TransferInput{
Amt: v.Amount(),
Input: Input{SigIndices: v.SigIndices()},
}, nil
}
// Bytes returns the ZAP-native wire envelope for this MintOutput.
func (out *MintOutput) Bytes() []byte {
return wire.NewMintOutput(wire.MintOutputInput{
TypeKind: TypeKind,
Locktime: out.Locktime,
Threshold: out.Threshold,
Addresses: out.Addrs,
})
}
// WrapMintOutput parses a wire envelope into a fresh MintOutput.
func WrapMintOutput(b []byte) (*MintOutput, error) {
v, err := wire.WrapMintOutput(b)
if err != nil {
return nil, err
}
if v.TypeKind() != TypeKind {
return nil, wire.ErrWrongTypeKind
}
return &MintOutput{
OutputOwners: OutputOwners{
Locktime: v.Locktime(),
Threshold: v.Threshold(),
Addrs: v.AddressList().All(),
},
}, nil
}
// Bytes returns the ZAP-native wire envelope for this MintOperation.
func (op *MintOperation) Bytes() []byte {
return wire.NewMintOperation(wire.MintOperationInput{
TypeKind: TypeKind,
SigIndices: op.MintInput.SigIndices,
MintOutput: op.MintOutput.Bytes(),
TransferOutput: op.TransferOutput.Bytes(),
})
}
// WrapMintOperation parses a wire envelope into a fresh MintOperation.
func WrapMintOperation(b []byte) (*MintOperation, error) {
v, err := wire.WrapMintOperation(b)
if err != nil {
return nil, err
}
if v.TypeKind() != TypeKind {
return nil, wire.ErrWrongTypeKind
}
mintOutput, err := WrapMintOutput(v.MintOutputBytes())
if err != nil {
return nil, err
}
transferOutput, err := WrapTransferOutput(v.TransferOutputBytes())
if err != nil {
return nil, err
}
return &MintOperation{
MintInput: Input{SigIndices: v.SigIndices()},
MintOutput: *mintOutput,
TransferOutput: *transferOutput,
}, nil
}
// Bytes returns the ZAP-native wire envelope for this Credential. Ed25519
// is not pubkey-recoverable from a signature, so the credential carries
// pubkeys alongside signatures. Both are concatenated into a single byte
// run with fixed per-element strides (SigLen / PubKeyLen).
func (cr *Credential) Bytes() []byte {
sigsConcat := make([]byte, 0, len(cr.Sigs)*SigLen)
for _, sig := range cr.Sigs {
sigsConcat = append(sigsConcat, sig[:]...)
}
pksConcat := make([]byte, 0, len(cr.PubKeys)*PubKeyLen)
for _, pk := range cr.PubKeys {
pksConcat = append(pksConcat, pk...)
}
return wire.NewCredential(wire.CredentialInput{
TypeKind: TypeKind,
SecurityLevel: 0,
Signatures: sigsConcat,
PubKeys: pksConcat,
})
}
// WrapCredential parses a wire envelope into a fresh Credential. Each
// signature is exactly SigLen bytes; each pubkey is exactly PubKeyLen
// bytes.
func WrapCredential(b []byte) (*Credential, error) {
v, err := wire.WrapCredential(b)
if err != nil {
return nil, err
}
if v.TypeKind() != TypeKind {
return nil, wire.ErrWrongTypeKind
}
n := v.SignatureCount(SigLen)
sigs := make([][SigLen]byte, n)
for i := 0; i < n; i++ {
raw := v.SignatureAt(i, SigLen)
copy(sigs[i][:], raw)
}
pkBytes := v.PubKeyBytes()
pks := make([][]byte, 0, len(pkBytes)/PubKeyLen)
for i := 0; i+PubKeyLen <= len(pkBytes); i += PubKeyLen {
entry := make([]byte, PubKeyLen)
copy(entry, pkBytes[i:i+PubKeyLen])
pks = append(pks, entry)
}
return &Credential{Sigs: sigs, PubKeys: pks}, nil
}
+144
View File
@@ -0,0 +1,144 @@
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package ed25519fx
import (
"bytes"
"testing"
"github.com/luxfi/ids"
)
func TestWire_OutputOwners_RoundTrip(t *testing.T) {
in := &OutputOwners{
Locktime: 1234,
Threshold: 2,
Addrs: []ids.ShortID{
{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
{2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2},
},
}
got, err := WrapOutputOwners(in.Bytes())
if err != nil {
t.Fatalf("WrapOutputOwners: %v", err)
}
if !in.Equals(got) {
t.Errorf("OutputOwners mismatch")
}
}
func TestWire_TransferOutput_RoundTrip(t *testing.T) {
in := &TransferOutput{
Amt: 1_000_000,
OutputOwners: OutputOwners{
Locktime: 42,
Threshold: 1,
Addrs: []ids.ShortID{{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}},
},
}
got, err := WrapTransferOutput(in.Bytes())
if err != nil {
t.Fatalf("WrapTransferOutput: %v", err)
}
if got.Amt != in.Amt {
t.Errorf("Amt: got %d, want %d", got.Amt, in.Amt)
}
if !in.OutputOwners.Equals(&got.OutputOwners) {
t.Errorf("OutputOwners mismatch")
}
}
func TestWire_TransferInput_RoundTrip(t *testing.T) {
in := &TransferInput{
Amt: 500,
Input: Input{SigIndices: []uint32{0, 2}},
}
got, err := WrapTransferInput(in.Bytes())
if err != nil {
t.Fatalf("WrapTransferInput: %v", err)
}
if got.Amt != in.Amt {
t.Errorf("Amt: got %d, want %d", got.Amt, in.Amt)
}
if len(got.SigIndices) != 2 {
t.Fatalf("SigIndices.Len: got %d, want 2", len(got.SigIndices))
}
}
func TestWire_MintOutput_RoundTrip(t *testing.T) {
in := &MintOutput{
OutputOwners: OutputOwners{
Threshold: 1,
Addrs: []ids.ShortID{{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}},
},
}
got, err := WrapMintOutput(in.Bytes())
if err != nil {
t.Fatalf("WrapMintOutput: %v", err)
}
if !in.OutputOwners.Equals(&got.OutputOwners) {
t.Errorf("OutputOwners mismatch")
}
}
func TestWire_MintOperation_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}
owners := OutputOwners{Threshold: 1, Addrs: []ids.ShortID{addr}}
in := &MintOperation{
MintInput: Input{SigIndices: []uint32{0}},
MintOutput: MintOutput{OutputOwners: owners},
TransferOutput: TransferOutput{Amt: 100, OutputOwners: owners},
}
got, err := WrapMintOperation(in.Bytes())
if err != nil {
t.Fatalf("WrapMintOperation: %v", err)
}
if got.TransferOutput.Amt != 100 {
t.Errorf("TransferOutput.Amt: got %d, want 100", got.TransferOutput.Amt)
}
}
func TestWire_Credential_RoundTrip(t *testing.T) {
var s1, s2 [SigLen]byte
for i := range s1 {
s1[i] = byte(i)
}
for i := range s2 {
s2[i] = byte(0xFF - i)
}
pk1 := make([]byte, PubKeyLen)
pk2 := make([]byte, PubKeyLen)
for i := range pk1 {
pk1[i] = byte(i)
}
for i := range pk2 {
pk2[i] = byte(0xFF - i)
}
in := &Credential{
Sigs: [][SigLen]byte{s1, s2},
PubKeys: [][]byte{pk1, pk2},
}
got, err := WrapCredential(in.Bytes())
if err != nil {
t.Fatalf("WrapCredential: %v", err)
}
if len(got.Sigs) != 2 {
t.Fatalf("Sigs.Len: got %d, want 2", len(got.Sigs))
}
if !bytes.Equal(got.Sigs[0][:], s1[:]) {
t.Errorf("Sigs[0] mismatch")
}
if !bytes.Equal(got.Sigs[1][:], s2[:]) {
t.Errorf("Sigs[1] mismatch")
}
if len(got.PubKeys) != 2 {
t.Fatalf("PubKeys.Len: got %d, want 2", len(got.PubKeys))
}
if !bytes.Equal(got.PubKeys[0], pk1) {
t.Errorf("PubKeys[0] mismatch")
}
if !bytes.Equal(got.PubKeys[1], pk2) {
t.Errorf("PubKeys[1] mismatch")
}
}
+13 -4
View File
@@ -8,7 +8,7 @@ require (
github.com/luxfi/cache v1.2.1
github.com/luxfi/codec v1.1.4
github.com/luxfi/constants v1.4.7
github.com/luxfi/crypto v1.19.16
github.com/luxfi/crypto v1.19.17
github.com/luxfi/database v1.17.44
github.com/luxfi/formatting v1.0.1
github.com/luxfi/geth v1.16.98
@@ -21,6 +21,7 @@ require (
github.com/luxfi/timer v1.0.1
github.com/luxfi/utils v1.1.4
github.com/luxfi/vm v1.0.40
github.com/luxfi/zap v0.7.2
github.com/stretchr/testify v1.11.1
go.uber.org/mock v0.6.0
)
@@ -29,20 +30,23 @@ require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/btcsuite/btcd/btcutil v1.1.6 // indirect
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
github.com/cloudflare/circl v1.6.3 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect
github.com/gorilla/rpc v1.2.1 // indirect
github.com/grandcat/zeroconf v1.0.0 // indirect
github.com/holiman/uint256 v1.3.2 // indirect
github.com/klauspost/compress v1.18.5 // indirect
github.com/luxfi/accel v1.1.4 // indirect
github.com/luxfi/accel v1.1.9 // indirect
github.com/luxfi/atomic v1.0.0 // indirect
github.com/luxfi/compress v0.0.5 // indirect
github.com/luxfi/concurrent v0.0.3 // indirect
github.com/luxfi/consensus v1.22.84 // indirect
github.com/luxfi/container v0.0.4 // indirect
github.com/luxfi/math/big v0.1.0 // indirect
github.com/luxfi/mdns v0.1.1 // indirect
github.com/luxfi/mock v0.1.1 // indirect
github.com/luxfi/p2p v1.19.2 // indirect
github.com/luxfi/sampler v1.0.0 // indirect
@@ -51,12 +55,17 @@ require (
github.com/luxfi/warp v1.18.5 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/miekg/dns v1.1.72 // indirect
github.com/mr-tron/base58 v1.2.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/supranational/blst v0.3.16 // indirect
golang.org/x/crypto v0.49.0 // indirect
golang.org/x/crypto v0.52.0 // indirect
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/mod v0.36.0 // indirect
golang.org/x/net v0.55.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.45.0 // indirect
golang.org/x/tools v0.45.0 // indirect
gonum.org/v1/gonum v0.17.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
+38 -8
View File
@@ -26,6 +26,8 @@ github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku
github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -58,6 +60,8 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX
github.com/gorilla/rpc v1.2.1 h1:yC+LMV5esttgpVvNORL/xX4jvTTEUE30UZhZ5JF7K9k=
github.com/gorilla/rpc v1.2.1/go.mod h1:uNpOihAlF5xRFLuTYhfR0yfCTm0WTQSQttkMSptRfGk=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grandcat/zeroconf v1.0.0 h1:uHhahLBKqwWBV6WZUDAT71044vwOTL+McW0mBJvo6kE=
github.com/grandcat/zeroconf v1.0.0/go.mod h1:lTKmG1zh86XyCoUeIHSA4FJMBwCJiQmGfcP2PdzytEs=
github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA=
github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
@@ -71,8 +75,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/luxfi/accel v1.1.4 h1:UOvS/00vG6WByf2P1tGYRkoBcsUkFuCgw7o2xU03OoE=
github.com/luxfi/accel v1.1.4/go.mod h1:K00BcnLzEYMPHwCFq8Tf/450ApmTs9xBVvYOnofJCkc=
github.com/luxfi/accel v1.1.9 h1:Tsk6gXj2uKE19501bD0ajRYdeCHIlTGb6jYyLc+F8hc=
github.com/luxfi/accel v1.1.9/go.mod h1:K00BcnLzEYMPHwCFq8Tf/450ApmTs9xBVvYOnofJCkc=
github.com/luxfi/address v1.0.1 h1:Sc4keyuVzBIvHr7uVeYZf2/WY9YDGUgDi/iiWenj49g=
github.com/luxfi/address v1.0.1/go.mod h1:5j3Eh66v9zvv1GbNdZwt+23krV8JlSDaRzmWZU8ZRM0=
github.com/luxfi/atomic v1.0.0 h1:xUV60MuzRvXngaQ1sM0yVC2v4TRoLlUGkkH7M9PS4yw=
@@ -91,8 +95,8 @@ github.com/luxfi/constants v1.4.7 h1:e/Qs+DQP3pugle3Zncq6fZCxKgqqtbyD/z7Gm4ZjsYg
github.com/luxfi/constants v1.4.7/go.mod h1:hOszZ2NDQ8gMZKncfcZ67PXkb5OIbnwAzXC3oFbQwW0=
github.com/luxfi/container v0.0.4 h1:BXhF82WyfqVP5mjlNcr7tP0Fcnvl0Ap1rkiu+rq5XuM=
github.com/luxfi/container v0.0.4/go.mod h1:Z3SpmMF5d4t77MM0nHYXURpn+EMVaeu1fhbd/3BGaek=
github.com/luxfi/crypto v1.19.16 h1:YCAXtLS65TCi7/iw6zBglFW0YS80zJj9Y61fTxdP+wo=
github.com/luxfi/crypto v1.19.16/go.mod h1:INjdZtke85k8hX/QAmTMAY8bbZ4gzGZQLqURg3xf6Gk=
github.com/luxfi/crypto v1.19.17 h1:l2LLu7UFyICtJVfraLDLRi+lFGiDXKHSL18M9/m1gsQ=
github.com/luxfi/crypto v1.19.17/go.mod h1:INjdZtke85k8hX/QAmTMAY8bbZ4gzGZQLqURg3xf6Gk=
github.com/luxfi/database v1.17.44 h1:hfiTls7sqbweW+o4iaZqB8P997paC+vpgWmhN6v5MJ0=
github.com/luxfi/database v1.17.44/go.mod h1:6Ey5y3I0WNLHbxIlIdFqUuKfBg+b0fAgTA8FgRgQ8zg=
github.com/luxfi/formatting v1.0.1 h1:ZnE1rAdEUds9yAegdVdGDOBGN6hLMPOv6E03Fp8IEYo=
@@ -109,12 +113,16 @@ github.com/luxfi/math v1.4.0 h1:/sb7Grw3hfO+5INWAWdB95jTvCeXg8fSQxsxDzcFtd4=
github.com/luxfi/math v1.4.0/go.mod h1:iW0FOCC8qF2mPE+MakG780CAHA83848lb1L04thA1Pg=
github.com/luxfi/math/big v0.1.0 h1:Vz4c0RsZVPdIKPsHPgAJChH/R3p15WHRUz7LkLf+NIQ=
github.com/luxfi/math/big v0.1.0/go.mod h1:BuxSu22RbO93xBLk5Eam5nldFponoJ73xDFz4uJ3Huk=
github.com/luxfi/mdns v0.1.1 h1:g2eRr9AXcziPkkcd24M+Qu9ApEpoKKjfI79QSNqv0rQ=
github.com/luxfi/mdns v0.1.1/go.mod h1:dbp5f3h3aE7CGzwbaWzBM9cwdcekhmSrWhQevgYhhNA=
github.com/luxfi/metric v1.5.1 h1:6tVarXMnNR3Xzud8FYUHjtXabTll3HI/OEqG0tgLAcI=
github.com/luxfi/metric v1.5.1/go.mod h1:PkD4D4JoGuyKtfUkqPNYkrg9xKrJeNVZFdW/5XvAe/A=
github.com/luxfi/mock v0.1.1 h1:0HEtIjg1J6CWz+IUyP6rsGqNWTcmxjFnSQIhaDuARwY=
github.com/luxfi/mock v0.1.1/go.mod h1:jo35akl3Vtd8LbzDts8VJ0jmSVycrd1/eBi6g6t5hKU=
github.com/luxfi/p2p v1.19.2 h1:uqZq7ofmEDbXlTkv1QThtci01Q+dmDkNmAPeORIyP8E=
github.com/luxfi/p2p v1.19.2/go.mod h1:tI9Bt1R0ouvVtJvXG4e20GlGeV4AR230k4mFF9Vglzk=
github.com/luxfi/pq v1.0.3 h1:pFlQm1+5FuKTDUh2y/23bXWkN4I2Rc5iuxJypwDFFMs=
github.com/luxfi/pq v1.0.3/go.mod h1:8bppZcRElfrVt0n3nYCZW3iX1TvhvzNbdjNdK1irgIE=
github.com/luxfi/runtime v1.0.1 h1:cii3OsRiVSIl9jzfWFM6++62T1r6ZSGP+/3F0Ezhpqw=
github.com/luxfi/runtime v1.0.1/go.mod h1:2hBKjzbEeE4dzrhUKH8dqkRgLEyiXz6GmuVusy3vJMs=
github.com/luxfi/sampler v1.0.0 h1:k8Sf6otW83w4pQp0jXLA+g3J/joB7w7SqXQsWmNTOV0=
@@ -131,10 +139,15 @@ github.com/luxfi/vm v1.0.40 h1:kA0V/9p1VdC88EjgbljWb8t+fl9qz5bCyF3IxnV20yg=
github.com/luxfi/vm v1.0.40/go.mod h1:Y5WUKhT76PR6HnbUykHBWI2f5sjssrm5BL6/o7b6IfQ=
github.com/luxfi/warp v1.18.5 h1:yXFCT+lnvzJHs8nVvfUCQ9wNvhtgbDGaXJkJeiwgKf4=
github.com/luxfi/warp v1.18.5/go.mod h1:SFyC529HDvbP/TWRAdYQSyJUliMa5JKFRtBrTLEElp4=
github.com/luxfi/zap v0.7.2 h1:YecWTWNE5PPJXL56sLIkzS8b23bprUwZ5lPAQuLUtTE=
github.com/luxfi/zap v0.7.2/go.mod h1:1k+nwT+JW802YzuPAuf7CxMSGr/qxvbGgGwi5k6X9Ok=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
@@ -146,6 +159,8 @@ github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -167,35 +182,50 @@ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA=
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
+269
View File
@@ -0,0 +1,269 @@
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package mldsafx
import (
"errors"
"fmt"
"github.com/luxfi/utxo/wire"
)
// TypeKind is the wire-level discriminator for every mldsafx primitive's
// wire envelope.
const TypeKind = wire.TypeKindMLDSA
// ErrUnknownSecurityLevel is returned when a wire envelope carries a
// SecurityLevel byte the fx package does not recognize. Re-exposes the
// fx-package ErrInvalidSecLevel for callers that switch on the wire
// error.
var ErrUnknownSecurityLevel = errors.New("mldsafx: wire envelope carries unrecognized SecurityLevel")
// strideForLevel resolves the wire-level uint8 SecurityLevel byte into a
// per-pubkey stride (in bytes). The wire layer is stride-agnostic; this
// mapping is the only place mldsafx-specific byte widths leak in.
func strideForLevel(level uint8) (int, error) {
switch SecurityLevel(level) {
case SecLevelMLDSA44:
return MLDSA44PubKeyLen, nil
case SecLevelMLDSA65:
return MLDSA65PubKeyLen, nil
case SecLevelMLDSA87:
return MLDSA87PubKeyLen, nil
default:
return 0, fmt.Errorf("%w: %d", ErrUnknownSecurityLevel, level)
}
}
// Bytes returns the ZAP-native wire envelope for this OutputOwners.
// Envelope = (TypeKindMLDSA, ShapeKindPQOutputOwners, ZAP message).
func (out *OutputOwners) Bytes() []byte {
return wire.NewPQOutputOwners(wire.PQOutputOwnersInput{
TypeKind: TypeKind,
SecurityLevel: uint8(out.Level),
Locktime: out.Locktime,
Threshold: out.Threshold,
PubKeyStride: out.Level.PubKeyLen(),
PubKeys: out.Addrs,
})
}
// WrapOutputOwners parses a wire envelope into a fresh OutputOwners.
// Envelope TypeKind must be TypeKindMLDSA; the SecurityLevel byte
// selects the pubkey stride.
func WrapOutputOwners(b []byte) (*OutputOwners, error) {
// Peek the SecurityLevel byte without parsing the message twice. The
// SecurityLevel field is at wire offset 0 inside the ZAP message
// body, so we read it by parsing once with stride=1 (which always
// succeeds) and then re-parsing with the correct stride. The extra
// parse is acceptable because PQOutputOwners messages are
// medium-sized (a few kB) and verification is the bottleneck.
tmp, err := wire.WrapPQOutputOwners(b, 1)
if err != nil {
return nil, err
}
if tmp.TypeKind() != TypeKind {
return nil, wire.ErrWrongTypeKind
}
stride, err := strideForLevel(tmp.SecurityLevel())
if err != nil {
return nil, err
}
v, err := wire.WrapPQOutputOwners(b, stride)
if err != nil {
return nil, err
}
return &OutputOwners{
Level: SecurityLevel(v.SecurityLevel()),
Locktime: v.Locktime(),
Threshold: v.Threshold(),
Addrs: v.PubKeys().All(),
}, nil
}
// Bytes returns the ZAP-native wire envelope for this TransferOutput.
// Envelope = (TypeKindMLDSA, ShapeKindPQTransferOutput, ZAP message).
func (out *TransferOutput) Bytes() []byte {
return wire.NewPQTransferOutput(wire.PQTransferOutputInput{
TypeKind: TypeKind,
SecurityLevel: uint8(out.Level),
Amount: out.Amt,
Locktime: out.Locktime,
Threshold: out.Threshold,
PubKeyStride: out.Level.PubKeyLen(),
PubKeys: out.Addrs,
})
}
// WrapTransferOutput parses a wire envelope into a fresh TransferOutput.
func WrapTransferOutput(b []byte) (*TransferOutput, error) {
tmp, err := wire.WrapPQTransferOutput(b, 1)
if err != nil {
return nil, err
}
if tmp.TypeKind() != TypeKind {
return nil, wire.ErrWrongTypeKind
}
stride, err := strideForLevel(tmp.SecurityLevel())
if err != nil {
return nil, err
}
v, err := wire.WrapPQTransferOutput(b, stride)
if err != nil {
return nil, err
}
return &TransferOutput{
Amt: v.Amount(),
OutputOwners: OutputOwners{
Level: SecurityLevel(v.SecurityLevel()),
Locktime: v.Locktime(),
Threshold: v.Threshold(),
Addrs: v.PubKeys().All(),
},
}, nil
}
// Bytes returns the ZAP-native wire envelope for this TransferInput.
// Envelope = (TypeKindMLDSA, ShapeKindTransferInput, ZAP message).
// The TransferInput shape is shared across classical and PQ fxs — the
// per-fx variation lives only in the matching Credential's signature
// stride.
func (in *TransferInput) Bytes() []byte {
return wire.NewTransferInput(wire.TransferInputInput{
TypeKind: TypeKind,
Amount: in.Amt,
SigIndices: in.SigIndices,
})
}
// WrapTransferInput parses a wire envelope into a fresh TransferInput.
func WrapTransferInput(b []byte) (*TransferInput, error) {
v, err := wire.WrapTransferInput(b)
if err != nil {
return nil, err
}
if v.TypeKind() != TypeKind {
return nil, wire.ErrWrongTypeKind
}
return &TransferInput{
Amt: v.Amount(),
Input: Input{SigIndices: v.SigIndices()},
}, nil
}
// Bytes returns the ZAP-native wire envelope for this MintOutput.
// Envelope = (TypeKindMLDSA, ShapeKindPQMintOutput, ZAP message).
func (out *MintOutput) Bytes() []byte {
return wire.NewPQMintOutput(wire.PQMintOutputInput{
TypeKind: TypeKind,
SecurityLevel: uint8(out.Level),
Locktime: out.Locktime,
Threshold: out.Threshold,
PubKeyStride: out.Level.PubKeyLen(),
PubKeys: out.Addrs,
})
}
// WrapMintOutput parses a wire envelope into a fresh MintOutput.
func WrapMintOutput(b []byte) (*MintOutput, error) {
tmp, err := wire.WrapPQMintOutput(b, 1)
if err != nil {
return nil, err
}
if tmp.TypeKind() != TypeKind {
return nil, wire.ErrWrongTypeKind
}
stride, err := strideForLevel(tmp.SecurityLevel())
if err != nil {
return nil, err
}
v, err := wire.WrapPQMintOutput(b, stride)
if err != nil {
return nil, err
}
return &MintOutput{
OutputOwners: OutputOwners{
Level: SecurityLevel(v.SecurityLevel()),
Locktime: v.Locktime(),
Threshold: v.Threshold(),
Addrs: v.PubKeys().All(),
},
}, nil
}
// Bytes returns the ZAP-native wire envelope for this MintOperation.
// Envelope carries SigIndices + nested MintOutput + nested TransferOutput
// wire bytes.
func (op *MintOperation) Bytes() []byte {
return wire.NewMintOperation(wire.MintOperationInput{
TypeKind: TypeKind,
SigIndices: op.MintInput.SigIndices,
MintOutput: op.MintOutput.Bytes(),
TransferOutput: op.TransferOutput.Bytes(),
})
}
// WrapMintOperation parses a wire envelope into a fresh MintOperation.
func WrapMintOperation(b []byte) (*MintOperation, error) {
v, err := wire.WrapMintOperation(b)
if err != nil {
return nil, err
}
if v.TypeKind() != TypeKind {
return nil, wire.ErrWrongTypeKind
}
mintOutput, err := WrapMintOutput(v.MintOutputBytes())
if err != nil {
return nil, err
}
transferOutput, err := WrapTransferOutput(v.TransferOutputBytes())
if err != nil {
return nil, err
}
return &MintOperation{
MintInput: Input{SigIndices: v.SigIndices()},
MintOutput: *mintOutput,
TransferOutput: *transferOutput,
}, nil
}
// Bytes returns the ZAP-native wire envelope for this Credential.
// Envelope = (TypeKindMLDSA, ShapeKindCredential, ZAP message). Signatures
// are concatenated into a single byte run; per-signature stride is
// resolved at parse time via the SecurityLevel byte.
func (cr *Credential) Bytes() []byte {
sigsConcat := make([]byte, 0, len(cr.Sigs)*cr.Level.SignatureLen())
for _, sig := range cr.Sigs {
sigsConcat = append(sigsConcat, sig...)
}
return wire.NewCredential(wire.CredentialInput{
TypeKind: TypeKind,
SecurityLevel: uint8(cr.Level),
Signatures: sigsConcat,
})
}
// WrapCredential parses a wire envelope into a fresh Credential. The
// SecurityLevel byte selects the per-signature stride; the byte run
// is split into Sigs accordingly.
func WrapCredential(b []byte) (*Credential, error) {
v, err := wire.WrapCredential(b)
if err != nil {
return nil, err
}
if v.TypeKind() != TypeKind {
return nil, wire.ErrWrongTypeKind
}
level := SecurityLevel(v.SecurityLevel())
sigLen := level.SignatureLen()
if sigLen == 0 {
return nil, fmt.Errorf("%w: %d", ErrUnknownSecurityLevel, v.SecurityLevel())
}
n := v.SignatureCount(sigLen)
sigs := make([][]byte, n)
for i := 0; i < n; i++ {
sigs[i] = v.SignatureAt(i, sigLen)
}
return &Credential{Level: level, Sigs: sigs}, nil
}
+201
View File
@@ -0,0 +1,201 @@
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package mldsafx
import (
"bytes"
"testing"
)
func TestWire_OutputOwners_RoundTrip(t *testing.T) {
// Two ML-DSA-65 pubkeys; build them so pk1 < pk2 lexicographically.
pk1 := make([]byte, MLDSA65PubKeyLen)
pk2 := make([]byte, MLDSA65PubKeyLen)
for i := range pk1 {
pk1[i] = byte(i)
}
for i := range pk2 {
pk2[i] = byte(0xFF - i)
}
in := &OutputOwners{
Level: SecLevelMLDSA65,
Locktime: 9999,
Threshold: 1,
Addrs: [][]byte{pk1, pk2},
}
got, err := WrapOutputOwners(in.Bytes())
if err != nil {
t.Fatalf("WrapOutputOwners: %v", err)
}
if got.Level != SecLevelMLDSA65 {
t.Errorf("Level: got %v, want %v", got.Level, SecLevelMLDSA65)
}
if got.Locktime != 9999 {
t.Errorf("Locktime: got %d, want 9999", got.Locktime)
}
if got.Threshold != 1 {
t.Errorf("Threshold: got %d, want 1", got.Threshold)
}
if len(got.Addrs) != 2 {
t.Fatalf("Addrs.Len: got %d, want 2", len(got.Addrs))
}
if !bytes.Equal(got.Addrs[0], pk1) {
t.Errorf("Addrs[0] mismatch")
}
if !bytes.Equal(got.Addrs[1], pk2) {
t.Errorf("Addrs[1] mismatch")
}
}
func TestWire_TransferOutput_RoundTrip(t *testing.T) {
pk := make([]byte, MLDSA65PubKeyLen)
for i := range pk {
pk[i] = byte(i)
}
in := &TransferOutput{
Amt: 7_777_777,
OutputOwners: OutputOwners{
Level: SecLevelMLDSA65,
Locktime: 42,
Threshold: 1,
Addrs: [][]byte{pk},
},
}
got, err := WrapTransferOutput(in.Bytes())
if err != nil {
t.Fatalf("WrapTransferOutput: %v", err)
}
if got.Amt != in.Amt {
t.Errorf("Amt: got %d, want %d", got.Amt, in.Amt)
}
if !got.OutputOwners.Equals(&in.OutputOwners) {
t.Errorf("OutputOwners mismatch")
}
}
func TestWire_TransferInput_RoundTrip(t *testing.T) {
in := &TransferInput{
Amt: 500,
Input: Input{SigIndices: []uint32{0, 2, 5, 7}},
}
got, err := WrapTransferInput(in.Bytes())
if err != nil {
t.Fatalf("WrapTransferInput: %v", err)
}
if got.Amt != in.Amt {
t.Errorf("Amt: got %d, want %d", got.Amt, in.Amt)
}
if len(got.SigIndices) != len(in.SigIndices) {
t.Fatalf("SigIndices.Len: got %d, want %d", len(got.SigIndices), len(in.SigIndices))
}
for i, v := range in.SigIndices {
if got.SigIndices[i] != v {
t.Errorf("SigIndices[%d]: got %d, want %d", i, got.SigIndices[i], v)
}
}
}
func TestWire_MintOutput_RoundTrip(t *testing.T) {
pk := make([]byte, MLDSA65PubKeyLen)
for i := range pk {
pk[i] = byte(i + 7)
}
in := &MintOutput{
OutputOwners: OutputOwners{
Level: SecLevelMLDSA65,
Locktime: 0,
Threshold: 1,
Addrs: [][]byte{pk},
},
}
got, err := WrapMintOutput(in.Bytes())
if err != nil {
t.Fatalf("WrapMintOutput: %v", err)
}
if !got.OutputOwners.Equals(&in.OutputOwners) {
t.Errorf("OutputOwners mismatch")
}
}
func TestWire_MintOperation_RoundTrip(t *testing.T) {
pk := make([]byte, MLDSA65PubKeyLen)
for i := range pk {
pk[i] = byte(i)
}
owners := OutputOwners{
Level: SecLevelMLDSA65,
Locktime: 0,
Threshold: 1,
Addrs: [][]byte{pk},
}
in := &MintOperation{
MintInput: Input{SigIndices: []uint32{0}},
MintOutput: MintOutput{
OutputOwners: owners,
},
TransferOutput: TransferOutput{
Amt: 100,
OutputOwners: owners,
},
}
got, err := WrapMintOperation(in.Bytes())
if err != nil {
t.Fatalf("WrapMintOperation: %v", err)
}
if got.TransferOutput.Amt != 100 {
t.Errorf("TransferOutput.Amt: got %d, want 100", got.TransferOutput.Amt)
}
}
func TestWire_Credential_RoundTrip_MLDSA65(t *testing.T) {
sig1 := make([]byte, MLDSA65SigLen)
sig2 := make([]byte, MLDSA65SigLen)
for i := range sig1 {
sig1[i] = byte(i)
}
for i := range sig2 {
sig2[i] = byte(0xFF - i)
}
in := &Credential{
Level: SecLevelMLDSA65,
Sigs: [][]byte{sig1, sig2},
}
got, err := WrapCredential(in.Bytes())
if err != nil {
t.Fatalf("WrapCredential: %v", err)
}
if got.Level != SecLevelMLDSA65 {
t.Errorf("Level: got %v, want %v", got.Level, SecLevelMLDSA65)
}
if len(got.Sigs) != 2 {
t.Fatalf("Sigs.Len: got %d, want 2", len(got.Sigs))
}
if !bytes.Equal(got.Sigs[0], sig1) {
t.Errorf("Sigs[0] mismatch")
}
if !bytes.Equal(got.Sigs[1], sig2) {
t.Errorf("Sigs[1] mismatch")
}
}
func TestWire_Credential_RoundTrip_MLDSA44(t *testing.T) {
sig := make([]byte, MLDSA44SigLen)
for i := range sig {
sig[i] = byte(i)
}
in := &Credential{
Level: SecLevelMLDSA44,
Sigs: [][]byte{sig},
}
got, err := WrapCredential(in.Bytes())
if err != nil {
t.Fatalf("WrapCredential: %v", err)
}
if got.Level != SecLevelMLDSA44 {
t.Errorf("Level: got %v, want %v", got.Level, SecLevelMLDSA44)
}
if !bytes.Equal(got.Sigs[0], sig) {
t.Errorf("Sigs[0] mismatch")
}
}
+195
View File
@@ -0,0 +1,195 @@
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package schnorrfx
import (
"github.com/luxfi/utxo/wire"
)
// TypeKind is the wire-level discriminator for every schnorrfx primitive's
// wire envelope.
const TypeKind = wire.TypeKindSchnorr
// Bytes returns the ZAP-native wire envelope for this OutputOwners.
func (out *OutputOwners) Bytes() []byte {
return wire.NewOutputOwners(wire.OutputOwnersInput{
Locktime: out.Locktime,
Threshold: out.Threshold,
Addresses: out.Addrs,
})
}
// WrapOutputOwners parses a wire envelope into a fresh OutputOwners.
func WrapOutputOwners(b []byte) (*OutputOwners, error) {
v, err := wire.WrapOutputOwners(b)
if err != nil {
return nil, err
}
return &OutputOwners{
Locktime: v.Locktime(),
Threshold: v.Threshold(),
Addrs: v.AddressList().All(),
}, nil
}
// Bytes returns the ZAP-native wire envelope for this TransferOutput.
func (out *TransferOutput) Bytes() []byte {
return wire.NewTransferOutput(wire.TransferOutputInput{
TypeKind: TypeKind,
Amount: out.Amt,
Locktime: out.Locktime,
Threshold: out.Threshold,
Addresses: out.Addrs,
})
}
// WrapTransferOutput parses a wire envelope into a fresh TransferOutput.
func WrapTransferOutput(b []byte) (*TransferOutput, error) {
v, err := wire.WrapTransferOutput(b)
if err != nil {
return nil, err
}
if v.TypeKind() != TypeKind {
return nil, wire.ErrWrongTypeKind
}
return &TransferOutput{
Amt: v.Amount(),
OutputOwners: OutputOwners{
Locktime: v.Locktime(),
Threshold: v.Threshold(),
Addrs: v.AddressList().All(),
},
}, nil
}
// Bytes returns the ZAP-native wire envelope for this TransferInput.
func (in *TransferInput) Bytes() []byte {
return wire.NewTransferInput(wire.TransferInputInput{
TypeKind: TypeKind,
Amount: in.Amt,
SigIndices: in.SigIndices,
})
}
// WrapTransferInput parses a wire envelope into a fresh TransferInput.
func WrapTransferInput(b []byte) (*TransferInput, error) {
v, err := wire.WrapTransferInput(b)
if err != nil {
return nil, err
}
if v.TypeKind() != TypeKind {
return nil, wire.ErrWrongTypeKind
}
return &TransferInput{
Amt: v.Amount(),
Input: Input{SigIndices: v.SigIndices()},
}, nil
}
// Bytes returns the ZAP-native wire envelope for this MintOutput.
func (out *MintOutput) Bytes() []byte {
return wire.NewMintOutput(wire.MintOutputInput{
TypeKind: TypeKind,
Locktime: out.Locktime,
Threshold: out.Threshold,
Addresses: out.Addrs,
})
}
// WrapMintOutput parses a wire envelope into a fresh MintOutput.
func WrapMintOutput(b []byte) (*MintOutput, error) {
v, err := wire.WrapMintOutput(b)
if err != nil {
return nil, err
}
if v.TypeKind() != TypeKind {
return nil, wire.ErrWrongTypeKind
}
return &MintOutput{
OutputOwners: OutputOwners{
Locktime: v.Locktime(),
Threshold: v.Threshold(),
Addrs: v.AddressList().All(),
},
}, nil
}
// Bytes returns the ZAP-native wire envelope for this MintOperation.
func (op *MintOperation) Bytes() []byte {
return wire.NewMintOperation(wire.MintOperationInput{
TypeKind: TypeKind,
SigIndices: op.MintInput.SigIndices,
MintOutput: op.MintOutput.Bytes(),
TransferOutput: op.TransferOutput.Bytes(),
})
}
// WrapMintOperation parses a wire envelope into a fresh MintOperation.
func WrapMintOperation(b []byte) (*MintOperation, error) {
v, err := wire.WrapMintOperation(b)
if err != nil {
return nil, err
}
if v.TypeKind() != TypeKind {
return nil, wire.ErrWrongTypeKind
}
mintOutput, err := WrapMintOutput(v.MintOutputBytes())
if err != nil {
return nil, err
}
transferOutput, err := WrapTransferOutput(v.TransferOutputBytes())
if err != nil {
return nil, err
}
return &MintOperation{
MintInput: Input{SigIndices: v.SigIndices()},
MintOutput: *mintOutput,
TransferOutput: *transferOutput,
}, nil
}
// Bytes returns the ZAP-native wire envelope for this Credential. BIP-340
// Schnorr verification requires the x-only pubkey, so the credential
// carries pubkeys alongside signatures.
func (cr *Credential) Bytes() []byte {
sigsConcat := make([]byte, 0, len(cr.Sigs)*SigLen)
for _, sig := range cr.Sigs {
sigsConcat = append(sigsConcat, sig[:]...)
}
pksConcat := make([]byte, 0, len(cr.PubKeys)*PubKeyLen)
for _, pk := range cr.PubKeys {
pksConcat = append(pksConcat, pk...)
}
return wire.NewCredential(wire.CredentialInput{
TypeKind: TypeKind,
SecurityLevel: 0,
Signatures: sigsConcat,
PubKeys: pksConcat,
})
}
// WrapCredential parses a wire envelope into a fresh Credential.
func WrapCredential(b []byte) (*Credential, error) {
v, err := wire.WrapCredential(b)
if err != nil {
return nil, err
}
if v.TypeKind() != TypeKind {
return nil, wire.ErrWrongTypeKind
}
n := v.SignatureCount(SigLen)
sigs := make([][SigLen]byte, n)
for i := 0; i < n; i++ {
raw := v.SignatureAt(i, SigLen)
copy(sigs[i][:], raw)
}
pkBytes := v.PubKeyBytes()
pks := make([][]byte, 0, len(pkBytes)/PubKeyLen)
for i := 0; i+PubKeyLen <= len(pkBytes); i += PubKeyLen {
entry := make([]byte, PubKeyLen)
copy(entry, pkBytes[i:i+PubKeyLen])
pks = append(pks, entry)
}
return &Credential{Sigs: sigs, PubKeys: pks}, nil
}
+115
View File
@@ -0,0 +1,115 @@
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package schnorrfx
import (
"bytes"
"testing"
"github.com/luxfi/ids"
)
func TestWire_OutputOwners_RoundTrip(t *testing.T) {
in := &OutputOwners{
Locktime: 1234,
Threshold: 1,
Addrs: []ids.ShortID{{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}},
}
got, err := WrapOutputOwners(in.Bytes())
if err != nil {
t.Fatalf("WrapOutputOwners: %v", err)
}
if !in.Equals(got) {
t.Errorf("OutputOwners mismatch")
}
}
func TestWire_TransferOutput_RoundTrip(t *testing.T) {
in := &TransferOutput{
Amt: 1_000_000,
OutputOwners: OutputOwners{
Threshold: 1,
Addrs: []ids.ShortID{{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}},
},
}
got, err := WrapTransferOutput(in.Bytes())
if err != nil {
t.Fatalf("WrapTransferOutput: %v", err)
}
if got.Amt != in.Amt {
t.Errorf("Amt: got %d, want %d", got.Amt, in.Amt)
}
}
func TestWire_TransferInput_RoundTrip(t *testing.T) {
in := &TransferInput{
Amt: 500,
Input: Input{SigIndices: []uint32{0, 2}},
}
got, err := WrapTransferInput(in.Bytes())
if err != nil {
t.Fatalf("WrapTransferInput: %v", err)
}
if got.Amt != in.Amt {
t.Errorf("Amt: got %d, want %d", got.Amt, in.Amt)
}
}
func TestWire_MintOutput_RoundTrip(t *testing.T) {
in := &MintOutput{
OutputOwners: OutputOwners{
Threshold: 1,
Addrs: []ids.ShortID{{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}},
},
}
got, err := WrapMintOutput(in.Bytes())
if err != nil {
t.Fatalf("WrapMintOutput: %v", err)
}
if !in.OutputOwners.Equals(&got.OutputOwners) {
t.Errorf("OutputOwners mismatch")
}
}
func TestWire_MintOperation_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}
owners := OutputOwners{Threshold: 1, Addrs: []ids.ShortID{addr}}
in := &MintOperation{
MintInput: Input{SigIndices: []uint32{0}},
MintOutput: MintOutput{OutputOwners: owners},
TransferOutput: TransferOutput{Amt: 100, OutputOwners: owners},
}
got, err := WrapMintOperation(in.Bytes())
if err != nil {
t.Fatalf("WrapMintOperation: %v", err)
}
if got.TransferOutput.Amt != 100 {
t.Errorf("TransferOutput.Amt: got %d, want 100", got.TransferOutput.Amt)
}
}
func TestWire_Credential_RoundTrip(t *testing.T) {
var s [SigLen]byte
for i := range s {
s[i] = byte(i)
}
pk := make([]byte, PubKeyLen)
for i := range pk {
pk[i] = byte(i)
}
in := &Credential{
Sigs: [][SigLen]byte{s},
PubKeys: [][]byte{pk},
}
got, err := WrapCredential(in.Bytes())
if err != nil {
t.Fatalf("WrapCredential: %v", err)
}
if !bytes.Equal(got.Sigs[0][:], s[:]) {
t.Errorf("Sigs[0] mismatch")
}
if !bytes.Equal(got.PubKeys[0], pk) {
t.Errorf("PubKeys[0] mismatch")
}
}
+209
View File
@@ -0,0 +1,209 @@
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package secp256k1fx
import (
"github.com/luxfi/crypto/secp256k1"
"github.com/luxfi/ids"
"github.com/luxfi/utxo/wire"
)
// TypeKind is the wire-level discriminator for every secp256k1fx
// primitive's wire envelope. Mirrors the codec.Manager slot the legacy
// linearcodec assigned to this fx — but as a single 1-byte tag, not a
// dense uint32 slot id.
const TypeKind = wire.TypeKindSecp256k1
// Bytes returns the ZAP-native wire envelope for this OutputOwners.
// Envelope = (TypeKindReserved, ShapeKindOutputOwners, ZAP message) —
// owners are not fx-owned (same wire payload is shared across every fx
// that has a multi-address ownership group).
func (out *OutputOwners) Bytes() []byte {
return wire.NewOutputOwners(wire.OutputOwnersInput{
Locktime: out.Locktime,
Threshold: out.Threshold,
Addresses: out.Addrs,
})
}
// WrapOutputOwners parses a wire envelope into a fresh OutputOwners.
// Envelope must carry ShapeKindOutputOwners.
func WrapOutputOwners(b []byte) (*OutputOwners, error) {
v, err := wire.WrapOutputOwners(b)
if err != nil {
return nil, err
}
return &OutputOwners{
Locktime: v.Locktime(),
Threshold: v.Threshold(),
Addrs: v.AddressList().All(),
}, nil
}
// Bytes returns the ZAP-native wire envelope for this TransferOutput.
// Envelope = (TypeKindSecp256k1, ShapeKindTransferOutput, ZAP message).
func (out *TransferOutput) Bytes() []byte {
return wire.NewTransferOutput(wire.TransferOutputInput{
TypeKind: TypeKind,
Amount: out.Amt,
Locktime: out.Locktime,
Threshold: out.Threshold,
Addresses: out.Addrs,
})
}
// WrapTransferOutput parses a wire envelope into a fresh TransferOutput.
// Envelope TypeKind must be TypeKindSecp256k1.
func WrapTransferOutput(b []byte) (*TransferOutput, error) {
v, err := wire.WrapTransferOutput(b)
if err != nil {
return nil, err
}
if v.TypeKind() != TypeKind {
return nil, wire.ErrWrongTypeKind
}
return &TransferOutput{
Amt: v.Amount(),
OutputOwners: OutputOwners{
Locktime: v.Locktime(),
Threshold: v.Threshold(),
Addrs: v.AddressList().All(),
},
}, nil
}
// Bytes returns the ZAP-native wire envelope for this TransferInput.
// Envelope = (TypeKindSecp256k1, ShapeKindTransferInput, ZAP message).
func (in *TransferInput) Bytes() []byte {
return wire.NewTransferInput(wire.TransferInputInput{
TypeKind: TypeKind,
Amount: in.Amt,
SigIndices: in.SigIndices,
})
}
// WrapTransferInput parses a wire envelope into a fresh TransferInput.
// Envelope TypeKind must be TypeKindSecp256k1.
func WrapTransferInput(b []byte) (*TransferInput, error) {
v, err := wire.WrapTransferInput(b)
if err != nil {
return nil, err
}
if v.TypeKind() != TypeKind {
return nil, wire.ErrWrongTypeKind
}
return &TransferInput{
Amt: v.Amount(),
Input: Input{SigIndices: v.SigIndices()},
}, nil
}
// Bytes returns the ZAP-native wire envelope for this MintOutput.
// Envelope = (TypeKindSecp256k1, ShapeKindMintOutput, ZAP message).
// Same payload as a TransferOutput's owner section but with
// ShapeKindMintOutput instead of ShapeKindTransferOutput.
func (out *MintOutput) Bytes() []byte {
return wire.NewMintOutput(wire.MintOutputInput{
TypeKind: TypeKind,
Locktime: out.Locktime,
Threshold: out.Threshold,
Addresses: out.Addrs,
})
}
// WrapMintOutput parses a wire envelope into a fresh MintOutput.
// Envelope TypeKind must be TypeKindSecp256k1.
func WrapMintOutput(b []byte) (*MintOutput, error) {
v, err := wire.WrapMintOutput(b)
if err != nil {
return nil, err
}
if v.TypeKind() != TypeKind {
return nil, wire.ErrWrongTypeKind
}
return &MintOutput{
OutputOwners: OutputOwners{
Locktime: v.Locktime(),
Threshold: v.Threshold(),
Addrs: v.AddressList().All(),
},
}, nil
}
// Bytes returns the ZAP-native wire envelope for this MintOperation.
// Envelope carries the operation's SigIndices + nested MintOutput +
// nested TransferOutput, each as length-prefixed bytes fields.
func (op *MintOperation) Bytes() []byte {
return wire.NewMintOperation(wire.MintOperationInput{
TypeKind: TypeKind,
SigIndices: op.MintInput.SigIndices,
MintOutput: op.MintOutput.Bytes(),
TransferOutput: op.TransferOutput.Bytes(),
})
}
// WrapMintOperation parses a wire envelope into a fresh MintOperation.
// Envelope TypeKind must be TypeKindSecp256k1.
func WrapMintOperation(b []byte) (*MintOperation, error) {
v, err := wire.WrapMintOperation(b)
if err != nil {
return nil, err
}
if v.TypeKind() != TypeKind {
return nil, wire.ErrWrongTypeKind
}
mintOutput, err := WrapMintOutput(v.MintOutputBytes())
if err != nil {
return nil, err
}
transferOutput, err := WrapTransferOutput(v.TransferOutputBytes())
if err != nil {
return nil, err
}
return &MintOperation{
MintInput: Input{SigIndices: v.SigIndices()},
MintOutput: *mintOutput,
TransferOutput: *transferOutput,
}, nil
}
// Bytes returns the ZAP-native wire envelope for this Credential.
// Envelope = (TypeKindSecp256k1, ShapeKindCredential, ZAP message).
// Signatures are concatenated into a single byte run (stride is
// secp256k1.SignatureLen = 65 bytes).
func (cr *Credential) Bytes() []byte {
sigsConcat := make([]byte, 0, len(cr.Sigs)*secp256k1.SignatureLen)
for _, sig := range cr.Sigs {
sigsConcat = append(sigsConcat, sig[:]...)
}
return wire.NewCredential(wire.CredentialInput{
TypeKind: TypeKind,
SecurityLevel: 0,
Signatures: sigsConcat,
})
}
// WrapCredential parses a wire envelope into a fresh Credential. Each
// signature is exactly secp256k1.SignatureLen bytes; ErrShortEnvelope
// is returned when the byte run doesn't divide cleanly.
func WrapCredential(b []byte) (*Credential, error) {
v, err := wire.WrapCredential(b)
if err != nil {
return nil, err
}
if v.TypeKind() != TypeKind {
return nil, wire.ErrWrongTypeKind
}
n := v.SignatureCount(secp256k1.SignatureLen)
sigs := make([][secp256k1.SignatureLen]byte, n)
for i := 0; i < n; i++ {
raw := v.SignatureAt(i, secp256k1.SignatureLen)
copy(sigs[i][:], raw)
}
return &Credential{Sigs: sigs}, nil
}
// ensure ids import is used (wire envelope tests + cross-fx assert helpers).
var _ ids.ShortID
+149
View File
@@ -0,0 +1,149 @@
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package secp256k1fx
import (
"bytes"
"testing"
"github.com/luxfi/crypto/secp256k1"
"github.com/luxfi/ids"
)
func TestWire_OutputOwners_RoundTrip(t *testing.T) {
in := &OutputOwners{
Locktime: 1234567,
Threshold: 2,
Addrs: []ids.ShortID{
{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
{2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2},
{3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3},
},
}
got, err := WrapOutputOwners(in.Bytes())
if err != nil {
t.Fatalf("WrapOutputOwners: %v", err)
}
if !in.Equals(got) {
t.Errorf("OutputOwners mismatch: got %+v, want %+v", got, in)
}
}
func TestWire_TransferOutput_RoundTrip(t *testing.T) {
in := &TransferOutput{
Amt: 7_777_777,
OutputOwners: OutputOwners{
Locktime: 42,
Threshold: 1,
Addrs: []ids.ShortID{{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}},
},
}
got, err := WrapTransferOutput(in.Bytes())
if err != nil {
t.Fatalf("WrapTransferOutput: %v", err)
}
if got.Amt != in.Amt {
t.Errorf("Amt: got %d, want %d", got.Amt, in.Amt)
}
if !in.OutputOwners.Equals(&got.OutputOwners) {
t.Errorf("OutputOwners mismatch: got %+v, want %+v", got.OutputOwners, in.OutputOwners)
}
}
func TestWire_TransferInput_RoundTrip(t *testing.T) {
in := &TransferInput{
Amt: 500,
Input: Input{SigIndices: []uint32{0, 2, 5, 7}},
}
got, err := WrapTransferInput(in.Bytes())
if err != nil {
t.Fatalf("WrapTransferInput: %v", err)
}
if got.Amt != in.Amt {
t.Errorf("Amt: got %d, want %d", got.Amt, in.Amt)
}
if !equalUint32Slice(got.SigIndices, in.SigIndices) {
t.Errorf("SigIndices: got %v, want %v", got.SigIndices, in.SigIndices)
}
}
func TestWire_MintOutput_RoundTrip(t *testing.T) {
in := &MintOutput{
OutputOwners: OutputOwners{
Locktime: 0,
Threshold: 1,
Addrs: []ids.ShortID{{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}},
},
}
got, err := WrapMintOutput(in.Bytes())
if err != nil {
t.Fatalf("WrapMintOutput: %v", err)
}
if !in.OutputOwners.Equals(&got.OutputOwners) {
t.Errorf("OutputOwners mismatch: got %+v, want %+v", got.OutputOwners, in.OutputOwners)
}
}
func TestWire_MintOperation_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}
in := &MintOperation{
MintInput: Input{SigIndices: []uint32{0}},
MintOutput: MintOutput{
OutputOwners: OutputOwners{Locktime: 0, Threshold: 1, Addrs: []ids.ShortID{addr}},
},
TransferOutput: TransferOutput{
Amt: 100,
OutputOwners: OutputOwners{Locktime: 0, Threshold: 1, Addrs: []ids.ShortID{addr}},
},
}
got, err := WrapMintOperation(in.Bytes())
if err != nil {
t.Fatalf("WrapMintOperation: %v", err)
}
if !equalUint32Slice(got.MintInput.SigIndices, in.MintInput.SigIndices) {
t.Errorf("MintInput.SigIndices: got %v, want %v", got.MintInput.SigIndices, in.MintInput.SigIndices)
}
if !in.MintOutput.OutputOwners.Equals(&got.MintOutput.OutputOwners) {
t.Errorf("MintOutput.OutputOwners mismatch")
}
if got.TransferOutput.Amt != in.TransferOutput.Amt {
t.Errorf("TransferOutput.Amt: got %d, want %d", got.TransferOutput.Amt, in.TransferOutput.Amt)
}
}
func TestWire_Credential_RoundTrip(t *testing.T) {
var s1, s2 [secp256k1.SignatureLen]byte
for i := range s1 {
s1[i] = byte(i)
}
for i := range s2 {
s2[i] = byte(0xFF - i)
}
in := &Credential{Sigs: [][secp256k1.SignatureLen]byte{s1, s2}}
got, err := WrapCredential(in.Bytes())
if err != nil {
t.Fatalf("WrapCredential: %v", err)
}
if len(got.Sigs) != 2 {
t.Fatalf("Sigs len: got %d, want 2", len(got.Sigs))
}
if !bytes.Equal(got.Sigs[0][:], s1[:]) {
t.Errorf("Sigs[0] mismatch")
}
if !bytes.Equal(got.Sigs[1][:], s2[:]) {
t.Errorf("Sigs[1] mismatch")
}
}
func equalUint32Slice(a, b []uint32) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
+195
View File
@@ -0,0 +1,195 @@
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package secp256r1fx
import (
"github.com/luxfi/utxo/wire"
)
// TypeKind is the wire-level discriminator for every secp256r1fx
// primitive's wire envelope.
const TypeKind = wire.TypeKindSecp256r1
// Bytes returns the ZAP-native wire envelope for this OutputOwners.
func (out *OutputOwners) Bytes() []byte {
return wire.NewOutputOwners(wire.OutputOwnersInput{
Locktime: out.Locktime,
Threshold: out.Threshold,
Addresses: out.Addrs,
})
}
// WrapOutputOwners parses a wire envelope into a fresh OutputOwners.
func WrapOutputOwners(b []byte) (*OutputOwners, error) {
v, err := wire.WrapOutputOwners(b)
if err != nil {
return nil, err
}
return &OutputOwners{
Locktime: v.Locktime(),
Threshold: v.Threshold(),
Addrs: v.AddressList().All(),
}, nil
}
// Bytes returns the ZAP-native wire envelope for this TransferOutput.
func (out *TransferOutput) Bytes() []byte {
return wire.NewTransferOutput(wire.TransferOutputInput{
TypeKind: TypeKind,
Amount: out.Amt,
Locktime: out.Locktime,
Threshold: out.Threshold,
Addresses: out.Addrs,
})
}
// WrapTransferOutput parses a wire envelope into a fresh TransferOutput.
func WrapTransferOutput(b []byte) (*TransferOutput, error) {
v, err := wire.WrapTransferOutput(b)
if err != nil {
return nil, err
}
if v.TypeKind() != TypeKind {
return nil, wire.ErrWrongTypeKind
}
return &TransferOutput{
Amt: v.Amount(),
OutputOwners: OutputOwners{
Locktime: v.Locktime(),
Threshold: v.Threshold(),
Addrs: v.AddressList().All(),
},
}, nil
}
// Bytes returns the ZAP-native wire envelope for this TransferInput.
func (in *TransferInput) Bytes() []byte {
return wire.NewTransferInput(wire.TransferInputInput{
TypeKind: TypeKind,
Amount: in.Amt,
SigIndices: in.SigIndices,
})
}
// WrapTransferInput parses a wire envelope into a fresh TransferInput.
func WrapTransferInput(b []byte) (*TransferInput, error) {
v, err := wire.WrapTransferInput(b)
if err != nil {
return nil, err
}
if v.TypeKind() != TypeKind {
return nil, wire.ErrWrongTypeKind
}
return &TransferInput{
Amt: v.Amount(),
Input: Input{SigIndices: v.SigIndices()},
}, nil
}
// Bytes returns the ZAP-native wire envelope for this MintOutput.
func (out *MintOutput) Bytes() []byte {
return wire.NewMintOutput(wire.MintOutputInput{
TypeKind: TypeKind,
Locktime: out.Locktime,
Threshold: out.Threshold,
Addresses: out.Addrs,
})
}
// WrapMintOutput parses a wire envelope into a fresh MintOutput.
func WrapMintOutput(b []byte) (*MintOutput, error) {
v, err := wire.WrapMintOutput(b)
if err != nil {
return nil, err
}
if v.TypeKind() != TypeKind {
return nil, wire.ErrWrongTypeKind
}
return &MintOutput{
OutputOwners: OutputOwners{
Locktime: v.Locktime(),
Threshold: v.Threshold(),
Addrs: v.AddressList().All(),
},
}, nil
}
// Bytes returns the ZAP-native wire envelope for this MintOperation.
func (op *MintOperation) Bytes() []byte {
return wire.NewMintOperation(wire.MintOperationInput{
TypeKind: TypeKind,
SigIndices: op.MintInput.SigIndices,
MintOutput: op.MintOutput.Bytes(),
TransferOutput: op.TransferOutput.Bytes(),
})
}
// WrapMintOperation parses a wire envelope into a fresh MintOperation.
func WrapMintOperation(b []byte) (*MintOperation, error) {
v, err := wire.WrapMintOperation(b)
if err != nil {
return nil, err
}
if v.TypeKind() != TypeKind {
return nil, wire.ErrWrongTypeKind
}
mintOutput, err := WrapMintOutput(v.MintOutputBytes())
if err != nil {
return nil, err
}
transferOutput, err := WrapTransferOutput(v.TransferOutputBytes())
if err != nil {
return nil, err
}
return &MintOperation{
MintInput: Input{SigIndices: v.SigIndices()},
MintOutput: *mintOutput,
TransferOutput: *transferOutput,
}, nil
}
// Bytes returns the ZAP-native wire envelope for this Credential. P-256
// is not pubkey-recoverable, so the credential carries pubkeys alongside
// signatures (same shape as Ed25519/Schnorr credentials).
func (cr *Credential) Bytes() []byte {
sigsConcat := make([]byte, 0, len(cr.Sigs)*SigLen)
for _, sig := range cr.Sigs {
sigsConcat = append(sigsConcat, sig[:]...)
}
pksConcat := make([]byte, 0, len(cr.PubKeys)*PubKeyLen)
for _, pk := range cr.PubKeys {
pksConcat = append(pksConcat, pk...)
}
return wire.NewCredential(wire.CredentialInput{
TypeKind: TypeKind,
SecurityLevel: 0,
Signatures: sigsConcat,
PubKeys: pksConcat,
})
}
// WrapCredential parses a wire envelope into a fresh Credential.
func WrapCredential(b []byte) (*Credential, error) {
v, err := wire.WrapCredential(b)
if err != nil {
return nil, err
}
if v.TypeKind() != TypeKind {
return nil, wire.ErrWrongTypeKind
}
n := v.SignatureCount(SigLen)
sigs := make([][SigLen]byte, n)
for i := 0; i < n; i++ {
raw := v.SignatureAt(i, SigLen)
copy(sigs[i][:], raw)
}
pkBytes := v.PubKeyBytes()
pks := make([][]byte, 0, len(pkBytes)/PubKeyLen)
for i := 0; i+PubKeyLen <= len(pkBytes); i += PubKeyLen {
entry := make([]byte, PubKeyLen)
copy(entry, pkBytes[i:i+PubKeyLen])
pks = append(pks, entry)
}
return &Credential{Sigs: sigs, PubKeys: pks}, nil
}
+115
View File
@@ -0,0 +1,115 @@
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package secp256r1fx
import (
"bytes"
"testing"
"github.com/luxfi/ids"
)
func TestWire_OutputOwners_RoundTrip(t *testing.T) {
in := &OutputOwners{
Locktime: 1234,
Threshold: 1,
Addrs: []ids.ShortID{{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}},
}
got, err := WrapOutputOwners(in.Bytes())
if err != nil {
t.Fatalf("WrapOutputOwners: %v", err)
}
if !in.Equals(got) {
t.Errorf("OutputOwners mismatch")
}
}
func TestWire_TransferOutput_RoundTrip(t *testing.T) {
in := &TransferOutput{
Amt: 1_000_000,
OutputOwners: OutputOwners{
Threshold: 1,
Addrs: []ids.ShortID{{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}},
},
}
got, err := WrapTransferOutput(in.Bytes())
if err != nil {
t.Fatalf("WrapTransferOutput: %v", err)
}
if got.Amt != in.Amt {
t.Errorf("Amt: got %d, want %d", got.Amt, in.Amt)
}
}
func TestWire_TransferInput_RoundTrip(t *testing.T) {
in := &TransferInput{
Amt: 500,
Input: Input{SigIndices: []uint32{0, 2}},
}
got, err := WrapTransferInput(in.Bytes())
if err != nil {
t.Fatalf("WrapTransferInput: %v", err)
}
if got.Amt != in.Amt {
t.Errorf("Amt: got %d, want %d", got.Amt, in.Amt)
}
}
func TestWire_MintOutput_RoundTrip(t *testing.T) {
in := &MintOutput{
OutputOwners: OutputOwners{
Threshold: 1,
Addrs: []ids.ShortID{{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}},
},
}
got, err := WrapMintOutput(in.Bytes())
if err != nil {
t.Fatalf("WrapMintOutput: %v", err)
}
if !in.OutputOwners.Equals(&got.OutputOwners) {
t.Errorf("OutputOwners mismatch")
}
}
func TestWire_MintOperation_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}
owners := OutputOwners{Threshold: 1, Addrs: []ids.ShortID{addr}}
in := &MintOperation{
MintInput: Input{SigIndices: []uint32{0}},
MintOutput: MintOutput{OutputOwners: owners},
TransferOutput: TransferOutput{Amt: 100, OutputOwners: owners},
}
got, err := WrapMintOperation(in.Bytes())
if err != nil {
t.Fatalf("WrapMintOperation: %v", err)
}
if got.TransferOutput.Amt != 100 {
t.Errorf("TransferOutput.Amt: got %d, want 100", got.TransferOutput.Amt)
}
}
func TestWire_Credential_RoundTrip(t *testing.T) {
var s [SigLen]byte
for i := range s {
s[i] = byte(i)
}
pk := make([]byte, PubKeyLen)
for i := range pk {
pk[i] = byte(i + 7)
}
in := &Credential{
Sigs: [][SigLen]byte{s},
PubKeys: [][]byte{pk},
}
got, err := WrapCredential(in.Bytes())
if err != nil {
t.Fatalf("WrapCredential: %v", err)
}
if !bytes.Equal(got.Sigs[0][:], s[:]) {
t.Errorf("Sigs[0] mismatch")
}
if !bytes.Equal(got.PubKeys[0], pk) {
t.Errorf("PubKeys[0] mismatch")
}
}
+244
View File
@@ -0,0 +1,244 @@
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package slhdsafx
import (
"errors"
"fmt"
"github.com/luxfi/utxo/wire"
)
// TypeKind is the wire-level discriminator for every slhdsafx primitive's
// wire envelope.
const TypeKind = wire.TypeKindSLHDSA
// ErrUnknownSecurityLevel is returned when a wire envelope carries a
// SecurityLevel byte the fx package does not recognize.
var ErrUnknownSecurityLevel = errors.New("slhdsafx: wire envelope carries unrecognized SecurityLevel")
// strideForLevel resolves the wire-level uint8 SecurityLevel byte into a
// per-pubkey stride (in bytes).
func strideForLevel(level uint8) (int, error) {
switch SecurityLevel(level) {
case SecLevelSLH128f:
return SLH128fPubKeyLen, nil
case SecLevelSLH192f:
return SLH192fPubKeyLen, nil
case SecLevelSLH256f:
return SLH256fPubKeyLen, nil
default:
return 0, fmt.Errorf("%w: %d", ErrUnknownSecurityLevel, level)
}
}
// Bytes returns the ZAP-native wire envelope for this OutputOwners.
func (out *OutputOwners) Bytes() []byte {
return wire.NewPQOutputOwners(wire.PQOutputOwnersInput{
TypeKind: TypeKind,
SecurityLevel: uint8(out.Level),
Locktime: out.Locktime,
Threshold: out.Threshold,
PubKeyStride: out.Level.PubKeyLen(),
PubKeys: out.Addrs,
})
}
// WrapOutputOwners parses a wire envelope into a fresh OutputOwners.
func WrapOutputOwners(b []byte) (*OutputOwners, error) {
tmp, err := wire.WrapPQOutputOwners(b, 1)
if err != nil {
return nil, err
}
if tmp.TypeKind() != TypeKind {
return nil, wire.ErrWrongTypeKind
}
stride, err := strideForLevel(tmp.SecurityLevel())
if err != nil {
return nil, err
}
v, err := wire.WrapPQOutputOwners(b, stride)
if err != nil {
return nil, err
}
return &OutputOwners{
Level: SecurityLevel(v.SecurityLevel()),
Locktime: v.Locktime(),
Threshold: v.Threshold(),
Addrs: v.PubKeys().All(),
}, nil
}
// Bytes returns the ZAP-native wire envelope for this TransferOutput.
func (out *TransferOutput) Bytes() []byte {
return wire.NewPQTransferOutput(wire.PQTransferOutputInput{
TypeKind: TypeKind,
SecurityLevel: uint8(out.Level),
Amount: out.Amt,
Locktime: out.Locktime,
Threshold: out.Threshold,
PubKeyStride: out.Level.PubKeyLen(),
PubKeys: out.Addrs,
})
}
// WrapTransferOutput parses a wire envelope into a fresh TransferOutput.
func WrapTransferOutput(b []byte) (*TransferOutput, error) {
tmp, err := wire.WrapPQTransferOutput(b, 1)
if err != nil {
return nil, err
}
if tmp.TypeKind() != TypeKind {
return nil, wire.ErrWrongTypeKind
}
stride, err := strideForLevel(tmp.SecurityLevel())
if err != nil {
return nil, err
}
v, err := wire.WrapPQTransferOutput(b, stride)
if err != nil {
return nil, err
}
return &TransferOutput{
Amt: v.Amount(),
OutputOwners: OutputOwners{
Level: SecurityLevel(v.SecurityLevel()),
Locktime: v.Locktime(),
Threshold: v.Threshold(),
Addrs: v.PubKeys().All(),
},
}, nil
}
// Bytes returns the ZAP-native wire envelope for this TransferInput.
func (in *TransferInput) Bytes() []byte {
return wire.NewTransferInput(wire.TransferInputInput{
TypeKind: TypeKind,
Amount: in.Amt,
SigIndices: in.SigIndices,
})
}
// WrapTransferInput parses a wire envelope into a fresh TransferInput.
func WrapTransferInput(b []byte) (*TransferInput, error) {
v, err := wire.WrapTransferInput(b)
if err != nil {
return nil, err
}
if v.TypeKind() != TypeKind {
return nil, wire.ErrWrongTypeKind
}
return &TransferInput{
Amt: v.Amount(),
Input: Input{SigIndices: v.SigIndices()},
}, nil
}
// Bytes returns the ZAP-native wire envelope for this MintOutput.
func (out *MintOutput) Bytes() []byte {
return wire.NewPQMintOutput(wire.PQMintOutputInput{
TypeKind: TypeKind,
SecurityLevel: uint8(out.Level),
Locktime: out.Locktime,
Threshold: out.Threshold,
PubKeyStride: out.Level.PubKeyLen(),
PubKeys: out.Addrs,
})
}
// WrapMintOutput parses a wire envelope into a fresh MintOutput.
func WrapMintOutput(b []byte) (*MintOutput, error) {
tmp, err := wire.WrapPQMintOutput(b, 1)
if err != nil {
return nil, err
}
if tmp.TypeKind() != TypeKind {
return nil, wire.ErrWrongTypeKind
}
stride, err := strideForLevel(tmp.SecurityLevel())
if err != nil {
return nil, err
}
v, err := wire.WrapPQMintOutput(b, stride)
if err != nil {
return nil, err
}
return &MintOutput{
OutputOwners: OutputOwners{
Level: SecurityLevel(v.SecurityLevel()),
Locktime: v.Locktime(),
Threshold: v.Threshold(),
Addrs: v.PubKeys().All(),
},
}, nil
}
// Bytes returns the ZAP-native wire envelope for this MintOperation.
func (op *MintOperation) Bytes() []byte {
return wire.NewMintOperation(wire.MintOperationInput{
TypeKind: TypeKind,
SigIndices: op.MintInput.SigIndices,
MintOutput: op.MintOutput.Bytes(),
TransferOutput: op.TransferOutput.Bytes(),
})
}
// WrapMintOperation parses a wire envelope into a fresh MintOperation.
func WrapMintOperation(b []byte) (*MintOperation, error) {
v, err := wire.WrapMintOperation(b)
if err != nil {
return nil, err
}
if v.TypeKind() != TypeKind {
return nil, wire.ErrWrongTypeKind
}
mintOutput, err := WrapMintOutput(v.MintOutputBytes())
if err != nil {
return nil, err
}
transferOutput, err := WrapTransferOutput(v.TransferOutputBytes())
if err != nil {
return nil, err
}
return &MintOperation{
MintInput: Input{SigIndices: v.SigIndices()},
MintOutput: *mintOutput,
TransferOutput: *transferOutput,
}, nil
}
// Bytes returns the ZAP-native wire envelope for this Credential.
func (cr *Credential) Bytes() []byte {
sigsConcat := make([]byte, 0, len(cr.Sigs)*cr.Level.SignatureLen())
for _, sig := range cr.Sigs {
sigsConcat = append(sigsConcat, sig...)
}
return wire.NewCredential(wire.CredentialInput{
TypeKind: TypeKind,
SecurityLevel: uint8(cr.Level),
Signatures: sigsConcat,
})
}
// WrapCredential parses a wire envelope into a fresh Credential.
func WrapCredential(b []byte) (*Credential, error) {
v, err := wire.WrapCredential(b)
if err != nil {
return nil, err
}
if v.TypeKind() != TypeKind {
return nil, wire.ErrWrongTypeKind
}
level := SecurityLevel(v.SecurityLevel())
sigLen := level.SignatureLen()
if sigLen == 0 {
return nil, fmt.Errorf("%w: %d", ErrUnknownSecurityLevel, v.SecurityLevel())
}
n := v.SignatureCount(sigLen)
sigs := make([][]byte, n)
for i := 0; i < n; i++ {
sigs[i] = v.SignatureAt(i, sigLen)
}
return &Credential{Level: level, Sigs: sigs}, nil
}
+151
View File
@@ -0,0 +1,151 @@
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package slhdsafx
import (
"bytes"
"testing"
)
func TestWire_OutputOwners_RoundTrip(t *testing.T) {
pk1 := make([]byte, SLH192fPubKeyLen)
pk2 := make([]byte, SLH192fPubKeyLen)
for i := range pk1 {
pk1[i] = byte(i)
}
for i := range pk2 {
pk2[i] = byte(0xFF - i)
}
in := &OutputOwners{
Level: SecLevelSLH192f,
Locktime: 9999,
Threshold: 1,
Addrs: [][]byte{pk1, pk2},
}
got, err := WrapOutputOwners(in.Bytes())
if err != nil {
t.Fatalf("WrapOutputOwners: %v", err)
}
if got.Level != SecLevelSLH192f {
t.Errorf("Level: got %v, want %v", got.Level, SecLevelSLH192f)
}
if got.Threshold != 1 {
t.Errorf("Threshold: got %d, want 1", got.Threshold)
}
if !bytes.Equal(got.Addrs[0], pk1) {
t.Errorf("Addrs[0] mismatch")
}
}
func TestWire_TransferOutput_RoundTrip(t *testing.T) {
pk := make([]byte, SLH192fPubKeyLen)
for i := range pk {
pk[i] = byte(i)
}
in := &TransferOutput{
Amt: 1_000_000,
OutputOwners: OutputOwners{
Level: SecLevelSLH192f,
Locktime: 42,
Threshold: 1,
Addrs: [][]byte{pk},
},
}
got, err := WrapTransferOutput(in.Bytes())
if err != nil {
t.Fatalf("WrapTransferOutput: %v", err)
}
if got.Amt != in.Amt {
t.Errorf("Amt: got %d, want %d", got.Amt, in.Amt)
}
if !got.OutputOwners.Equals(&in.OutputOwners) {
t.Errorf("OutputOwners mismatch")
}
}
func TestWire_TransferInput_RoundTrip(t *testing.T) {
in := &TransferInput{
Amt: 500,
Input: Input{SigIndices: []uint32{0, 1}},
}
got, err := WrapTransferInput(in.Bytes())
if err != nil {
t.Fatalf("WrapTransferInput: %v", err)
}
if got.Amt != in.Amt {
t.Errorf("Amt: got %d, want %d", got.Amt, in.Amt)
}
if len(got.SigIndices) != 2 {
t.Fatalf("SigIndices.Len: got %d, want 2", len(got.SigIndices))
}
}
func TestWire_MintOutput_RoundTrip(t *testing.T) {
pk := make([]byte, SLH192fPubKeyLen)
for i := range pk {
pk[i] = byte(i + 7)
}
in := &MintOutput{
OutputOwners: OutputOwners{
Level: SecLevelSLH192f,
Threshold: 1,
Addrs: [][]byte{pk},
},
}
got, err := WrapMintOutput(in.Bytes())
if err != nil {
t.Fatalf("WrapMintOutput: %v", err)
}
if !got.OutputOwners.Equals(&in.OutputOwners) {
t.Errorf("OutputOwners mismatch")
}
}
func TestWire_MintOperation_RoundTrip(t *testing.T) {
pk := make([]byte, SLH192fPubKeyLen)
for i := range pk {
pk[i] = byte(i)
}
owners := OutputOwners{
Level: SecLevelSLH192f,
Threshold: 1,
Addrs: [][]byte{pk},
}
in := &MintOperation{
MintInput: Input{SigIndices: []uint32{0}},
MintOutput: MintOutput{OutputOwners: owners},
TransferOutput: TransferOutput{Amt: 100, OutputOwners: owners},
}
got, err := WrapMintOperation(in.Bytes())
if err != nil {
t.Fatalf("WrapMintOperation: %v", err)
}
if got.TransferOutput.Amt != 100 {
t.Errorf("TransferOutput.Amt: got %d, want 100", got.TransferOutput.Amt)
}
}
func TestWire_Credential_RoundTrip_192f(t *testing.T) {
sig := make([]byte, SLH192fSigLen)
for i := range sig {
sig[i] = byte(i)
}
in := &Credential{
Level: SecLevelSLH192f,
Sigs: [][]byte{sig},
}
got, err := WrapCredential(in.Bytes())
if err != nil {
t.Fatalf("WrapCredential: %v", err)
}
if got.Level != SecLevelSLH192f {
t.Errorf("Level: got %v, want %v", got.Level, SecLevelSLH192f)
}
if len(got.Sigs) != 1 {
t.Fatalf("Sigs.Len: got %d, want 1", len(got.Sigs))
}
if !bytes.Equal(got.Sigs[0], sig) {
t.Errorf("Sigs[0] mismatch")
}
}
+69
View File
@@ -0,0 +1,69 @@
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package utxo
import (
"errors"
"github.com/luxfi/utxo/wire"
)
// ErrUTXOOutNotWireSerializable is returned by UTXO.WireBytes when the
// in-memory polymorphic Out field is not a known fxs primitive with a
// wire.NewXxx adapter. Each fx package's wire.go adapter file registers
// its concrete types to satisfy the wireSerializable interface — if a
// caller stuffs a third-party type into UTXO.Out, the wire layer cannot
// build a deterministic envelope and refuses.
var ErrUTXOOutNotWireSerializable = errors.New("utxo: UTXO.Out type does not implement wire-serializable interface; add Bytes() []byte to the fx primitive")
// wireSerializable is the minimal contract every fxs primitive's wire
// adapter must satisfy: a Bytes() returning the (TypeKind+ShapeKind+
// ZAP-message) envelope. All fx wire.go files satisfy this for their
// TransferOutput / MintOutput / AttestationOutput types.
type wireSerializable interface {
Bytes() []byte
}
// WireBytes returns the ZAP-native wire envelope for the UTXO. The
// envelope is = (TypeKindReserved, ShapeKindUTXO, ZAP message) where the
// ZAP message carries TxID, OutputIndex, AssetID, and the inner output's
// wire envelope (which itself carries its own TypeKind+ShapeKind+ZAP
// message).
//
// Returns ErrUTXOOutNotWireSerializable when the in-memory Out is not a
// known fxs primitive. The fx packages provide the adapter side via
// their `wire.go` Bytes() methods.
func (u *UTXO) WireBytes() ([]byte, error) {
if u == nil {
return nil, errNilUTXO
}
if u.Out == nil {
return nil, errEmptyUTXO
}
ws, ok := u.Out.(wireSerializable)
if !ok {
return nil, ErrUTXOOutNotWireSerializable
}
outputBytes := ws.Bytes()
return wire.NewUTXO(wire.UTXOInput{
TxID: u.TxID,
OutputIndex: u.OutputIndex,
AssetID: u.Asset.ID,
Output: outputBytes,
}), nil
}
// WrapUTXOBytes parses the outer UTXO wire envelope into the (TxID,
// OutputIndex, AssetID, Output-envelope-bytes) tuple. The caller is
// responsible for parsing the Output envelope through the appropriate
// fx package's WrapTransferOutput / WrapMintOutput / WrapAttestationOutput
// dispatched on the inner discriminator pair.
//
// This split is intentional: the root utxo package cannot import the fx
// packages (would be a cycle), so the inner envelope parse stays at the
// caller. Use wire.UTXO.OutputDiscriminator() to learn (TypeKind,
// ShapeKind) before dispatch.
func WrapUTXOBytes(b []byte) (wire.UTXO, error) {
return wire.WrapUTXO(b)
}
+99
View File
@@ -0,0 +1,99 @@
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package utxo_test
// External test package — exercises both the root utxo package and the
// fx packages without creating an import cycle.
import (
"testing"
"github.com/luxfi/ids"
"github.com/luxfi/vm/components/verify"
"github.com/luxfi/utxo"
"github.com/luxfi/utxo/secp256k1fx"
"github.com/luxfi/utxo/wire"
)
// Test that a UTXO carrying a *secp256k1fx.TransferOutput round-trips
// through the wire envelope cleanly.
func TestUTXO_WireBytes_Secp256k1_TransferOutput_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}
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}
inOut := &secp256k1fx.TransferOutput{
Amt: 1_000_000,
OutputOwners: secp256k1fx.OutputOwners{
Locktime: 42,
Threshold: 1,
Addrs: []ids.ShortID{addr},
},
}
in := &utxo.UTXO{
UTXOID: utxo.UTXOID{TxID: txID, OutputIndex: 7},
Asset: utxo.Asset{ID: assetID},
Out: inOut,
}
envelope, err := in.WireBytes()
if err != nil {
t.Fatalf("WireBytes: %v", err)
}
got, err := utxo.WrapUTXOBytes(envelope)
if err != nil {
t.Fatalf("WrapUTXOBytes: %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)
}
tk, sk := got.OutputDiscriminator()
if tk != wire.TypeKindSecp256k1 {
t.Errorf("OutputDiscriminator TypeKind: got %x, want %x", tk, wire.TypeKindSecp256k1)
}
if sk != wire.ShapeKindTransferOutput {
t.Errorf("OutputDiscriminator ShapeKind: got %x, want %x", sk, wire.ShapeKindTransferOutput)
}
// Dispatch on the discriminator into the fx-package WrapTransferOutput.
gotOut, err := secp256k1fx.WrapTransferOutput(got.OutputBytes())
if err != nil {
t.Fatalf("WrapTransferOutput: %v", err)
}
if gotOut.Amt != inOut.Amt {
t.Errorf("Out.Amt: got %d, want %d", gotOut.Amt, inOut.Amt)
}
if !gotOut.OutputOwners.Equals(&inOut.OutputOwners) {
t.Errorf("Out.OutputOwners mismatch")
}
}
// Test that WireBytes refuses a non-wire-serializable Out.
func TestUTXO_WireBytes_RejectsNonWireSerializableOut(t *testing.T) {
in := &utxo.UTXO{
UTXOID: utxo.UTXOID{TxID: ids.ID{1}, OutputIndex: 0},
Asset: utxo.Asset{ID: ids.ID{2}},
Out: unknownOut{},
}
if _, err := in.WireBytes(); err != utxo.ErrUTXOOutNotWireSerializable {
t.Errorf("WireBytes: got err=%v, want ErrUTXOOutNotWireSerializable", err)
}
}
// unknownOut is a verify.State that does NOT implement Bytes() — used to
// confirm WireBytes refuses unknown types cleanly.
type unknownOut struct {
verify.IsState
}
func (unknownOut) Verify() error { return nil }
+217
View File
@@ -0,0 +1,217 @@
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package wire
import "github.com/luxfi/zap"
// BLS12381 attestation primitives. The bls12381fx is divergent from the
// classical / PQ pattern — it is attestation-only (no Mint primitives,
// no transfer-value semantics). The output records "this 32-byte
// commitment was attested to by the listed BLS public keys at the listed
// threshold". The input is a signer-bitmap selecting which committee
// members contributed.
//
// BLS12381 sizes:
// - PubKeyLen = 48 (compressed G1)
// - AggSigLen = 96 (compressed G2)
// - AttestedHashLen = 32
const (
BLS12381PubKeyLen = 48
BLS12381AggSigLen = 96
BLS12381AttestedHashLen = 32
)
// AttestationOutput is the cross-fx ZAP schema for the bls12381fx
// AttestationOutput.
//
// Fixed-section layout (size 48 bytes):
//
// AttestedHash 32B @ 0
// Threshold uint32 @ 32
// _padding 4B @ 36 (reserved-zero, 8-aligned)
// PubKeyList list @ 40 (8 bytes; payload is stride-48 G1 pubkeys)
//
// Wire prefix: TypeKind=0x07 (BLS12381), ShapeKind=0x07
// (AttestationOutput).
const (
OffsetAttestationOutput_AttestedHash = 0 // 32B
OffsetAttestationOutput_Threshold = 32 // uint32
OffsetAttestationOutput_PubKeyList = 40 // list (8 bytes)
SizeAttestationOutput = 48
)
// AttestationOutput is the zero-copy typed accessor.
type AttestationOutput struct {
msg *zap.Message
obj zap.Object
}
// AttestedHash returns the 32-byte commitment.
func (a AttestationOutput) AttestedHash() [BLS12381AttestedHashLen]byte {
var out [BLS12381AttestedHashLen]byte
for i := 0; i < BLS12381AttestedHashLen; i++ {
out[i] = a.obj.Uint8(OffsetAttestationOutput_AttestedHash + i)
}
return out
}
// Threshold returns the minimum number of pubkeys required.
func (a AttestationOutput) Threshold() uint32 {
return a.obj.Uint32(OffsetAttestationOutput_Threshold)
}
// PubKeys returns the committee pubkeys as a fresh [][]byte slice (each
// inner []byte is BLS12381PubKeyLen = 48 bytes).
func (a AttestationOutput) PubKeys() [][]byte {
l := a.obj.ListStride(OffsetAttestationOutput_PubKeyList, BLS12381PubKeyLen)
n := l.Len()
out := make([][]byte, n)
for i := 0; i < n; i++ {
pk := make([]byte, BLS12381PubKeyLen)
obj := l.Object(i, BLS12381PubKeyLen)
for j := 0; j < BLS12381PubKeyLen; j++ {
pk[j] = obj.Uint8(j)
}
out[i] = pk
}
return out
}
// IsZero reports whether the accessor wraps a parsed message.
func (a AttestationOutput) IsZero() bool { return a.msg == nil }
// WrapAttestationOutput parses an AttestationOutput wire envelope.
func WrapAttestationOutput(b []byte) (AttestationOutput, error) {
tk, sk, zapBytes, err := readEnvelopePrefix(b)
if err != nil {
return AttestationOutput{}, err
}
if tk != TypeKindBLS12381 {
return AttestationOutput{}, ErrWrongTypeKind
}
if sk != ShapeKindAttestationOut {
return AttestationOutput{}, ErrWrongShapeKind
}
msg, err := zap.Parse(zapBytes)
if err != nil {
return AttestationOutput{}, err
}
return AttestationOutput{msg: msg, obj: msg.Root()}, nil
}
// AttestationOutputInput is the constructor input.
type AttestationOutputInput struct {
AttestedHash [BLS12381AttestedHashLen]byte
Threshold uint32
// PubKeys MUST each be BLS12381PubKeyLen bytes; the constructor does
// not pad or truncate. Sort lexicographically before passing in —
// AttestationOutput.Verify requires sorted-unique pubkeys.
PubKeys [][]byte
}
// NewAttestationOutput builds an AttestationOutput wire envelope.
func NewAttestationOutput(in AttestationOutputInput) []byte {
capEstimate := zap.HeaderSize + SizeAttestationOutput + len(in.PubKeys)*BLS12381PubKeyLen + 64
b := zap.NewBuilder(capEstimate)
pkListOff, pkListCount := writePubKeyList(b, in.PubKeys, BLS12381PubKeyLen)
ob := b.StartObject(SizeAttestationOutput)
for i := 0; i < BLS12381AttestedHashLen; i++ {
ob.SetUint8(OffsetAttestationOutput_AttestedHash+i, in.AttestedHash[i])
}
ob.SetUint32(OffsetAttestationOutput_Threshold, in.Threshold)
ob.SetList(OffsetAttestationOutput_PubKeyList, pkListOff, pkListCount)
ob.FinishAsRoot()
return writeEnvelopePrefix(TypeKindBLS12381, ShapeKindAttestationOut, b.Finish())
}
// writePubKeyList writes a stride-N pubkey list. N is the per-pubkey byte
// width (48 for BLS12-381 G1, 32 for Ed25519/Schnorr, 64 for secp256r1).
func writePubKeyList(b *zap.Builder, pks [][]byte, pkLen int) (offset, entryCount int) {
if len(pks) == 0 {
return 0, 0
}
lb := b.StartList(pkLen)
for _, pk := range pks {
// Pad/truncate to pkLen so all entries are the same stride. A
// well-behaved caller already enforces uniform length; we still
// pad here so a buggy caller produces a parseable (if invalid)
// message rather than corrupted memory.
entry := make([]byte, pkLen)
copy(entry, pk)
lb.AddBytes(entry)
}
off, _ := lb.Finish()
return off, len(pks)
}
// AttestationInput is the cross-fx ZAP schema for the bls12381fx
// AttestationInput.
//
// Fixed-section layout (size 8 bytes):
//
// SignerBitmap bytes @ 0 (8 bytes — relOffset + length)
//
// The signer bitmap is a packed bit array where bit i (LSB of byte i/8)
// indicates that PubKeys[i] of the spent AttestationOutput contributed
// to the aggregate signature.
//
// Wire prefix: TypeKind=0x07 (BLS12381), ShapeKind=0x08
// (AttestationInput).
const (
OffsetAttestationInput_SignerBitmap = 0 // bytes (8 bytes)
SizeAttestationInput = 8
)
// AttestationInput is the zero-copy typed accessor.
type AttestationInput struct {
msg *zap.Message
obj zap.Object
}
// SignerBitmap returns the signer-selection bitmap.
//
// READ-ONLY: aliases the underlying buffer.
func (a AttestationInput) SignerBitmap() []byte {
return a.obj.Bytes(OffsetAttestationInput_SignerBitmap)
}
// IsZero reports whether the accessor wraps a parsed message.
func (a AttestationInput) IsZero() bool { return a.msg == nil }
// WrapAttestationInput parses an AttestationInput wire envelope.
func WrapAttestationInput(b []byte) (AttestationInput, error) {
tk, sk, zapBytes, err := readEnvelopePrefix(b)
if err != nil {
return AttestationInput{}, err
}
if tk != TypeKindBLS12381 {
return AttestationInput{}, ErrWrongTypeKind
}
if sk != ShapeKindAttestationIn {
return AttestationInput{}, ErrWrongShapeKind
}
msg, err := zap.Parse(zapBytes)
if err != nil {
return AttestationInput{}, err
}
return AttestationInput{msg: msg, obj: msg.Root()}, nil
}
// AttestationInputInput is the constructor input.
type AttestationInputInput struct {
SignerBitmap []byte
}
// NewAttestationInput builds an AttestationInput wire envelope.
func NewAttestationInput(in AttestationInputInput) []byte {
capEstimate := zap.HeaderSize + SizeAttestationInput + len(in.SignerBitmap) + 64
b := zap.NewBuilder(capEstimate)
ob := b.StartObject(SizeAttestationInput)
ob.SetBytes(OffsetAttestationInput_SignerBitmap, in.SignerBitmap)
ob.FinishAsRoot()
return writeEnvelopePrefix(TypeKindBLS12381, ShapeKindAttestationIn, b.Finish())
}
+183
View File
@@ -0,0 +1,183 @@
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package wire
import (
"github.com/luxfi/zap"
)
// Credential is the cross-fx ZAP schema for fxs signature credentials.
//
// Classical fx credentials (secp256k1fx, schnorrfx) carry only signatures
// (the pubkey is recoverable from the sig or implicit from the spent
// output's address list). Modern fx credentials (ed25519fx, secp256r1fx)
// carry pubkeys alongside signatures because those schemes are not
// pubkey-recoverable from a single signature. Post-quantum credentials
// (mldsafx, slhdsafx) carry a SecurityLevel byte selecting the parameter
// set, then variable-length signatures.
//
// Fixed-section layout (size 12 bytes):
//
// SecurityLevel uint8 @ 0 (0=L1, 1=L2, 2=L3, ... — fx-specific; 0 for classical)
// _padding 3B @ 1 (reserved-zero for 4-byte alignment)
// SignatureList list @ 4 (8 bytes; payload is concatenated signature blobs)
//
// Wire prefix: TypeKind names the fx; ShapeKind is ShapeKindCredential
// (0x06). The SignatureList payload is a stride-N byte run where N is
// the per-fx signature size (e.g. 65 for secp256k1, 64 for Ed25519/Schnorr,
// 3309 for ML-DSA-65). Length field counts CONCATENATED BYTES, not
// number of signatures — divide by per-fx stride to recover sig count.
//
// PubKeys (when an fx ships them on the wire alongside sigs) live in a
// trailing PubKeyList field; classical and PQ fxs leave it empty.
const (
OffsetCredential_SecurityLevel = 0 // uint8
OffsetCredential_SignatureList = 4 // list (8 bytes)
OffsetCredential_PubKeyList = 12
SizeCredential = 20
)
// Credential is the zero-copy typed accessor.
type Credential struct {
tk TypeKind
msg *zap.Message
obj zap.Object
}
// TypeKind returns the fx family of this credential.
func (c Credential) TypeKind() TypeKind { return c.tk }
// SecurityLevel returns the fx-specific security level byte. 0 for
// classical fxs; (0,1,2) for ML-DSA (-44/-65/-87) and SLH-DSA variants.
func (c Credential) SecurityLevel() uint8 {
return c.obj.Uint8(OffsetCredential_SecurityLevel)
}
// SignatureBytes returns the concatenated signature blob as a single
// fresh []byte. Divide len()/sigSize to get the signature count.
//
// READ-ONLY: aliases the underlying buffer when used via the List
// accessor. The returned slice is a copy.
func (c Credential) SignatureBytes() []byte {
l := c.obj.ListStride(OffsetCredential_SignatureList, 1)
n := l.Len()
if n == 0 {
return nil
}
out := make([]byte, n)
for i := 0; i < n; i++ {
out[i] = l.Uint8(i)
}
return out
}
// SignatureCount returns the number of signatures, given the per-fx
// signature size. Returns 0 if sigSize is 0 or the total bytes don't
// divide cleanly.
func (c Credential) SignatureCount(sigSize int) int {
if sigSize <= 0 {
return 0
}
total := c.obj.ListStride(OffsetCredential_SignatureList, 1).Len()
if total%sigSize != 0 {
return 0
}
return total / sigSize
}
// SignatureAt returns the i'th signature, given the per-fx signature size.
// Returns nil when i is out of range or sigSize doesn't divide cleanly.
func (c Credential) SignatureAt(i, sigSize int) []byte {
if sigSize <= 0 || i < 0 {
return nil
}
all := c.SignatureBytes()
start := i * sigSize
end := start + sigSize
if end > len(all) {
return nil
}
return all[start:end]
}
// PubKeyBytes returns the concatenated pubkey blob, for fxs that ship
// pubkeys on the wire (Ed25519, secp256r1, Schnorr). Classical (secp256k1)
// and PQ (ML-DSA, SLH-DSA) leave this empty.
func (c Credential) PubKeyBytes() []byte {
l := c.obj.ListStride(OffsetCredential_PubKeyList, 1)
n := l.Len()
if n == 0 {
return nil
}
out := make([]byte, n)
for i := 0; i < n; i++ {
out[i] = l.Uint8(i)
}
return out
}
// IsZero reports whether the accessor wraps a parsed message.
func (c Credential) IsZero() bool { return c.msg == nil }
// WrapCredential parses a Credential wire envelope.
func WrapCredential(b []byte) (Credential, error) {
tk, sk, zapBytes, err := readEnvelopePrefix(b)
if err != nil {
return Credential{}, err
}
if sk != ShapeKindCredential {
return Credential{}, ErrWrongShapeKind
}
if tk == TypeKindReserved {
return Credential{}, ErrWrongTypeKind
}
msg, err := zap.Parse(zapBytes)
if err != nil {
return Credential{}, err
}
return Credential{tk: tk, msg: msg, obj: msg.Root()}, nil
}
// CredentialInput is the constructor input. Signatures and PubKeys MUST
// already be concatenated by the caller — the constructor does not split
// or pad. For classical fxs, PubKeys should be nil.
type CredentialInput struct {
TypeKind TypeKind
SecurityLevel uint8
// Signatures is the concatenated signature blob: e.g. 2 secp256k1
// signatures = 2*65 = 130 bytes.
Signatures []byte
// PubKeys is the concatenated pubkey blob (empty for classical fxs
// and ML-DSA/SLH-DSA).
PubKeys []byte
}
// NewCredential builds a Credential wire envelope.
func NewCredential(in CredentialInput) []byte {
capEstimate := zap.HeaderSize + SizeCredential + len(in.Signatures) + len(in.PubKeys) + 64
b := zap.NewBuilder(capEstimate)
sigsOff, sigsCount := writeByteList(b, in.Signatures)
pubKeysOff, pubKeysCount := writeByteList(b, in.PubKeys)
ob := b.StartObject(SizeCredential)
ob.SetUint8(OffsetCredential_SecurityLevel, in.SecurityLevel)
ob.SetList(OffsetCredential_SignatureList, sigsOff, sigsCount)
ob.SetList(OffsetCredential_PubKeyList, pubKeysOff, pubKeysCount)
ob.FinishAsRoot()
return writeEnvelopePrefix(in.TypeKind, ShapeKindCredential, b.Finish())
}
// writeByteList writes a stride-1 byte list and returns (offset, length).
func writeByteList(b *zap.Builder, data []byte) (offset, length int) {
if len(data) == 0 {
return 0, 0
}
lb := b.StartList(1)
for _, byteVal := range data {
lb.AddUint8(byteVal)
}
off, _ := lb.Finish()
return off, len(data)
}
+105
View File
@@ -0,0 +1,105 @@
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Package wire is the cross-VM ZAP-native wire format for the fxs
// (feature-extension) primitives shared by P-chain (platformvm) and
// X-chain (xvm). It replaces the legacy luxfi/codec.Manager registry
// with a (TypeKind, ShapeKind) discriminator pair on every primitive's
// wire envelope.
//
// Activation: 2025-12-25T16:20:00-08:00 (LP-023). New final Lux network.
// No backwards compatibility — legacy codec-encoded fxs primitives on the
// wire are a protocol-violation reject after activation.
//
// Wire envelope per primitive: [TypeKind:1][ShapeKind:1][ZAP message: N].
//
// The TypeKind byte names the fx family (secp256k1fx, mldsafx, ...) and
// the ShapeKind byte names the primitive shape within that family
// (TransferOutput, TransferInput, MintOutput, MintOperation, Credential,
// AttestationOutput, AttestationInput, OutputOwners). The remaining
// bytes are a ZAP message describing the shape's fields.
//
// This is decomposed from the legacy codec.Manager slot map (which
// braided "is this an Output? is this an Input? which fx?" into a
// single dense uint32 slot id). TypeKind + ShapeKind separates the two
// values cleanly — composition over inheritance.
package wire
import "errors"
// TypeKind names the fx family that owns the primitive. Dense values,
// 0x00 reserved (rejected by every Wrap*).
type TypeKind uint8
const (
TypeKindReserved TypeKind = 0x00
TypeKindSecp256k1 TypeKind = 0x01
TypeKindMLDSA TypeKind = 0x02
TypeKindSLHDSA TypeKind = 0x03
TypeKindEd25519 TypeKind = 0x04
TypeKindSecp256r1 TypeKind = 0x05
TypeKindSchnorr TypeKind = 0x06
TypeKindBLS12381 TypeKind = 0x07
)
// ShapeKind names the primitive shape within a fx family. Dense values,
// 0x00 reserved.
type ShapeKind uint8
const (
ShapeKindReserved ShapeKind = 0x00
ShapeKindTransferOutput ShapeKind = 0x01
ShapeKindTransferInput ShapeKind = 0x02
ShapeKindMintOutput ShapeKind = 0x03
ShapeKindMintInput ShapeKind = 0x04
ShapeKindMintOperation ShapeKind = 0x05
ShapeKindCredential ShapeKind = 0x06
ShapeKindAttestationOut ShapeKind = 0x07
ShapeKindAttestationIn ShapeKind = 0x08
ShapeKindOutputOwners ShapeKind = 0x09
ShapeKindUTXO ShapeKind = 0x0A
ShapeKindTransferableOut ShapeKind = 0x0B
ShapeKindTransferableIn ShapeKind = 0x0C
ShapeKindPChainOwner ShapeKind = 0x0D
ShapeKindSignedTx ShapeKind = 0x0E
)
// Errors returned by every Wrap*Primitive accessor when the discriminator
// pair on the wire does not match the expected (TypeKind, ShapeKind) for
// the function being called. This closes the cross-type confusion surface
// where a TransferInput buffer could be Wrap'd as a TransferOutput and
// return garbage-but-deterministic field reads.
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")
)
// EnvelopePrefix is the 2-byte discriminator prefix that every fxs
// primitive's wire envelope begins with. The remainder of the envelope
// is a ZAP message describing the shape's fields.
const EnvelopePrefix = 2
// readEnvelopePrefix parses the (TypeKind, ShapeKind) prefix from a wire
// buffer and returns the post-prefix ZAP slice. Returns ErrShortEnvelope
// when the buffer is shorter than 2 bytes.
func readEnvelopePrefix(b []byte) (TypeKind, ShapeKind, []byte, error) {
if len(b) < EnvelopePrefix {
return 0, 0, nil, ErrShortEnvelope
}
return TypeKind(b[0]), ShapeKind(b[1]), b[EnvelopePrefix:], nil
}
// writeEnvelopePrefix prepends a (TypeKind, ShapeKind) discriminator to a
// ZAP message and returns the concatenated wire envelope.
//
// Allocates a single fresh buffer of size 2 + len(zapBytes). Callers
// that hold the ZAP bytes by reference must not retain ownership of the
// input slice — the returned slice is the canonical wire envelope.
func writeEnvelopePrefix(tk TypeKind, sk ShapeKind, zapBytes []byte) []byte {
out := make([]byte, EnvelopePrefix+len(zapBytes))
out[0] = byte(tk)
out[1] = byte(sk)
copy(out[EnvelopePrefix:], zapBytes)
return out
}
+113
View File
@@ -0,0 +1,113 @@
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package wire
import "github.com/luxfi/zap"
// MintOperation is the cross-fx ZAP schema for the MintOperation
// primitive (Input { SigIndices } + MintOutput + TransferOutput).
// Operationally: an asset-minting authority (the MintInput's SigIndices)
// produces a new TransferOutput (value) and updates/re-confirms the
// MintOutput (mint authority continuation).
//
// Fixed-section layout (size 24 bytes):
//
// SigIndicesList list @ 0 (8 bytes — for the mint authority signatures)
// MintOutputBytes bytes @ 8 (8 bytes — wire envelope of MintOutput)
// TransferOutputBytes bytes @ 16 (8 bytes — wire envelope of TransferOutput)
//
// Wire prefix: TypeKind names the fx; ShapeKind is
// ShapeKindMintOperation (0x05). The inner MintOutputBytes and
// TransferOutputBytes carry their own discriminator pairs — consumers
// dispatch via WrapMintOutput / WrapTransferOutput.
const (
OffsetMintOperation_SigIndicesList = 0 // list (8 bytes)
OffsetMintOperation_MintOutputBytes = 8 // bytes (8 bytes)
OffsetMintOperation_TransferOutputBytes = 16 // bytes (8 bytes)
SizeMintOperation = 24
)
// MintOperation is the zero-copy typed accessor.
type MintOperation struct {
tk TypeKind
msg *zap.Message
obj zap.Object
}
// TypeKind returns the fx family that owns this operation.
func (m MintOperation) TypeKind() TypeKind { return m.tk }
// SigIndices returns the mint-authority signature indices.
func (m MintOperation) SigIndices() []uint32 {
l := m.obj.ListStride(OffsetMintOperation_SigIndicesList, SigIndexStride)
n := l.Len()
out := make([]uint32, n)
for i := 0; i < n; i++ {
out[i] = l.Uint32(i)
}
return out
}
// MintOutputBytes returns the inner MintOutput wire envelope. Pass to
// WrapMintOutput for typed access.
//
// READ-ONLY: aliases the underlying buffer.
func (m MintOperation) MintOutputBytes() []byte {
return m.obj.Bytes(OffsetMintOperation_MintOutputBytes)
}
// TransferOutputBytes returns the inner TransferOutput wire envelope.
// Pass to WrapTransferOutput for typed access.
//
// READ-ONLY: aliases the underlying buffer.
func (m MintOperation) TransferOutputBytes() []byte {
return m.obj.Bytes(OffsetMintOperation_TransferOutputBytes)
}
// IsZero reports whether the accessor wraps a parsed message.
func (m MintOperation) IsZero() bool { return m.msg == nil }
// WrapMintOperation parses a MintOperation wire envelope.
func WrapMintOperation(b []byte) (MintOperation, error) {
tk, sk, zapBytes, err := readEnvelopePrefix(b)
if err != nil {
return MintOperation{}, err
}
if sk != ShapeKindMintOperation {
return MintOperation{}, ErrWrongShapeKind
}
if tk == TypeKindReserved {
return MintOperation{}, ErrWrongTypeKind
}
msg, err := zap.Parse(zapBytes)
if err != nil {
return MintOperation{}, err
}
return MintOperation{tk: tk, msg: msg, obj: msg.Root()}, nil
}
// MintOperationInput is the constructor input.
type MintOperationInput struct {
TypeKind TypeKind
SigIndices []uint32
MintOutput []byte // wire envelope from NewMintOutput
TransferOutput []byte // wire envelope from NewTransferOutput
}
// NewMintOperation builds a MintOperation wire envelope.
func NewMintOperation(in MintOperationInput) []byte {
capEstimate := zap.HeaderSize + SizeMintOperation +
len(in.SigIndices)*SigIndexStride +
len(in.MintOutput) + len(in.TransferOutput) + 64
b := zap.NewBuilder(capEstimate)
sigIdxOff, sigIdxCount := writeSigIndices(b, in.SigIndices)
ob := b.StartObject(SizeMintOperation)
ob.SetList(OffsetMintOperation_SigIndicesList, sigIdxOff, sigIdxCount)
ob.SetBytes(OffsetMintOperation_MintOutputBytes, in.MintOutput)
ob.SetBytes(OffsetMintOperation_TransferOutputBytes, in.TransferOutput)
ob.FinishAsRoot()
return writeEnvelopePrefix(in.TypeKind, ShapeKindMintOperation, b.Finish())
}
+122
View File
@@ -0,0 +1,122 @@
// 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"
)
// MintOutput is the cross-fx ZAP schema for the MintOutput primitive
// (`OutputOwners`). Same shape as OutputOwners — MintOutput identifies a
// mint-authority owner group, not a value-bearing output.
//
// Fixed-section layout (size 20 bytes; identical to OutputOwners):
//
// Locktime uint64 @ 0
// Threshold uint32 @ 8
// AddressList list @ 12 (4-byte relOffset + 4-byte length, 8 bytes)
//
// Wire prefix: TypeKind names the fx; ShapeKind is ShapeKindMintOutput
// (0x03). Same payload bytes as a TransferOutput owner section — only
// the ShapeKind discriminator distinguishes "mint authority" from
// "spending output".
const (
OffsetMintOutput_Locktime = OffsetOutputOwners_Locktime // 0
OffsetMintOutput_Threshold = OffsetOutputOwners_Threshold // 8
OffsetMintOutput_AddressList = OffsetOutputOwners_AddressList // 12
SizeMintOutput = SizeOutputOwners // 20
)
// MintOutput is the zero-copy typed accessor.
type MintOutput struct {
tk TypeKind
msg *zap.Message
obj zap.Object
}
// TypeKind returns the fx family that owns this mint authority.
func (m MintOutput) TypeKind() TypeKind { return m.tk }
// Locktime returns the unix timestamp before which mint cannot fire.
func (m MintOutput) Locktime() uint64 {
return m.obj.Uint64(OffsetMintOutput_Locktime)
}
// Threshold returns the signatures-required count for mint authority.
func (m MintOutput) Threshold() uint32 {
return m.obj.Uint32(OffsetMintOutput_Threshold)
}
// AddressList returns the mint authority address list.
func (m MintOutput) AddressList() AddressList {
return AddressList{list: m.obj.ListStride(OffsetMintOutput_AddressList, AddressStride)}
}
// SyntacticVerify enforces the same gates as OutputOwners.
func (m MintOutput) SyntacticVerify() error {
addrs := m.AddressList()
n := addrs.Len()
if n == 0 {
return ErrOwnerAddrsEmpty
}
th := m.Threshold()
if th == 0 {
return ErrOwnerThresholdZero
}
if uint64(th) > uint64(n) {
return ErrOwnerThresholdExceedsAddrs
}
for i := 0; i < n; i++ {
if addrs.At(i) == (ids.ShortID{}) {
return ErrOwnerAddrZero
}
}
return nil
}
// IsZero reports whether the accessor wraps a parsed message.
func (m MintOutput) IsZero() bool { return m.msg == nil }
// WrapMintOutput parses a MintOutput wire envelope.
func WrapMintOutput(b []byte) (MintOutput, error) {
tk, sk, zapBytes, err := readEnvelopePrefix(b)
if err != nil {
return MintOutput{}, err
}
if sk != ShapeKindMintOutput {
return MintOutput{}, ErrWrongShapeKind
}
if tk == TypeKindReserved {
return MintOutput{}, ErrWrongTypeKind
}
msg, err := zap.Parse(zapBytes)
if err != nil {
return MintOutput{}, err
}
return MintOutput{tk: tk, msg: msg, obj: msg.Root()}, nil
}
// MintOutputInput is the constructor input.
type MintOutputInput struct {
TypeKind TypeKind
Locktime uint64
Threshold uint32
Addresses []ids.ShortID
}
// NewMintOutput builds a MintOutput wire envelope.
func NewMintOutput(in MintOutputInput) []byte {
capEstimate := zap.HeaderSize + SizeMintOutput + len(in.Addresses)*AddressStride + 64
b := zap.NewBuilder(capEstimate)
addrListOff, addrListCount := writeAddressList(b, in.Addresses)
ob := b.StartObject(SizeMintOutput)
ob.SetUint64(OffsetMintOutput_Locktime, in.Locktime)
ob.SetUint32(OffsetMintOutput_Threshold, in.Threshold)
ob.SetList(OffsetMintOutput_AddressList, addrListOff, addrListCount)
ob.FinishAsRoot()
return writeEnvelopePrefix(in.TypeKind, ShapeKindMintOutput, b.Finish())
}
+207
View File
@@ -0,0 +1,207 @@
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package wire
import (
"errors"
"github.com/luxfi/ids"
"github.com/luxfi/zap"
)
// OutputOwners is the cross-VM ZAP schema for
// github.com/luxfi/utxo/secp256k1fx.OutputOwners. Carries a Locktime, a
// signature-threshold count, and a variable-length list of 20-byte
// addresses (the addresses that may co-sign to spend an output owned
// by this group).
//
// Fixed-section layout (size 20 bytes; uint64 reads alignment-tolerant):
//
// Locktime uint64 @ 0
// Threshold uint32 @ 8
// AddressList list @ 12 (4-byte relOffset + 4-byte length, 8 bytes)
//
// Total fixed section = 20 bytes. AddressList payload is stride-20 in
// the variable section.
//
// Semantically equivalent to message.PChainOwner when Locktime=0; the
// PChainOwner wire envelope uses ShapeKindPChainOwner and skips the
// Locktime field entirely (see WrapPChainOwner below).
const (
OffsetOutputOwners_Locktime = 0 // uint64
OffsetOutputOwners_Threshold = 8 // uint32
OffsetOutputOwners_AddressList = 12 // list (8 bytes)
SizeOutputOwners = 20
)
// AddressStride is the per-element width of an AddressList in the
// OutputOwners variable section. Each address is a 20-byte ids.ShortID.
const AddressStride = ids.ShortIDLen
// Semantic-verification errors. The wire layer (ZAP) is permissive by
// design — semantic gates live here. Mirrors the
// zap_native/owner.go Owner.SyntacticVerify error set so cross-VM
// consumers see a single canonical error set.
var (
ErrOwnerThresholdZero = errors.New("wire: OutputOwners.Threshold must be > 0; threshold=0 disables authorization")
ErrOwnerThresholdExceedsAddrs = errors.New("wire: OutputOwners.Threshold exceeds Addresses.Len() — unsatisfiable signer quorum")
ErrOwnerAddrsEmpty = errors.New("wire: OutputOwners.Addresses is empty — signer set undefined")
ErrOwnerAddrZero = errors.New("wire: OutputOwners.Addresses contains the zero ShortID — phantom signer")
)
// OutputOwners is the zero-copy typed accessor over a ZAP-encoded
// OutputOwners wire envelope.
//
// READ-ONLY: each address aliases the underlying ZAP buffer.
type OutputOwners struct {
msg *zap.Message
obj zap.Object
}
// Locktime returns the unix timestamp before which the output cannot be
// spent.
func (o OutputOwners) Locktime() uint64 {
return o.obj.Uint64(OffsetOutputOwners_Locktime)
}
// Threshold returns the number of signatures required to spend.
func (o OutputOwners) Threshold() uint32 {
return o.obj.Uint32(OffsetOutputOwners_Threshold)
}
// AddressList returns the variable-stride address list view.
func (o OutputOwners) AddressList() AddressList {
return AddressList{list: o.obj.ListStride(OffsetOutputOwners_AddressList, AddressStride)}
}
// IsZero reports whether the accessor wraps a parsed message.
func (o OutputOwners) IsZero() bool { return o.msg == nil }
// SyntacticVerify enforces every executor-side semantic gate on an
// OutputOwners read from an untrusted wire buffer:
// - Addresses non-empty
// - Threshold > 0
// - Threshold <= len(Addresses)
// - Every Address is non-zero
//
// Returns one of the typed sentinel errors above, or nil when valid.
// Every consumer that treats Threshold/Addresses as a quorum gate MUST
// call SyntacticVerify before trusting the values.
func (o OutputOwners) SyntacticVerify() error {
addrs := o.AddressList()
n := addrs.Len()
if n == 0 {
return ErrOwnerAddrsEmpty
}
t := o.Threshold()
if t == 0 {
return ErrOwnerThresholdZero
}
if uint64(t) > uint64(n) {
return ErrOwnerThresholdExceedsAddrs
}
for i := 0; i < n; i++ {
if addrs.At(i) == (ids.ShortID{}) {
return ErrOwnerAddrZero
}
}
return nil
}
// AddressList is the zero-copy view over the address list inside an
// OutputOwners. Stride is 20 bytes (ids.ShortIDLen).
type AddressList struct {
list zap.List
}
// Len returns the number of addresses.
func (a AddressList) Len() int { return a.list.Len() }
// IsNull returns true if no list pointer was set.
func (a AddressList) IsNull() bool { return a.list.IsNull() }
// At returns the i'th address. Returns the zero ShortID when out of range.
//
// CONSUMER SAFETY (RED-HIGH-3): when i >= the actual entry count (a
// malicious encoder published a length-padded list), this returns the
// zero ShortID which is a phantom signer. Always call
// OutputOwners.SyntacticVerify() before iterating.
func (a AddressList) At(i int) ids.ShortID {
var out ids.ShortID
if i < 0 || i >= a.list.Len() {
return out
}
obj := a.list.Object(i, AddressStride)
for j := 0; j < AddressStride; j++ {
out[j] = obj.Uint8(j)
}
return out
}
// All returns a fresh []ShortID copy of every address in the list.
func (a AddressList) All() []ids.ShortID {
n := a.list.Len()
out := make([]ids.ShortID, n)
for i := 0; i < n; i++ {
out[i] = a.At(i)
}
return out
}
// WrapOutputOwners parses an OutputOwners wire envelope into a typed
// accessor. The discriminator is (TypeKindReserved, ShapeKindOutputOwners)
// — owners are not fx-owned, every fx with multi-address ownership
// shares the same wire schema.
func WrapOutputOwners(b []byte) (OutputOwners, error) {
_, sk, zapBytes, err := readEnvelopePrefix(b)
if err != nil {
return OutputOwners{}, err
}
if sk != ShapeKindOutputOwners {
return OutputOwners{}, ErrWrongShapeKind
}
msg, err := zap.Parse(zapBytes)
if err != nil {
return OutputOwners{}, err
}
return OutputOwners{msg: msg, obj: msg.Root()}, nil
}
// OutputOwnersInput is the constructor input for NewOutputOwners.
type OutputOwnersInput struct {
Locktime uint64
Threshold uint32
Addresses []ids.ShortID
}
// NewOutputOwners builds an OutputOwners wire envelope.
func NewOutputOwners(in OutputOwnersInput) []byte {
capEstimate := zap.HeaderSize + SizeOutputOwners + len(in.Addresses)*AddressStride + 64
b := zap.NewBuilder(capEstimate)
// Write the address list first so its offset is known when the parent
// object's list pointer is set.
addrListOff, addrListCount := writeAddressList(b, in.Addresses)
ob := b.StartObject(SizeOutputOwners)
ob.SetUint64(OffsetOutputOwners_Locktime, in.Locktime)
ob.SetUint32(OffsetOutputOwners_Threshold, in.Threshold)
ob.SetList(OffsetOutputOwners_AddressList, addrListOff, addrListCount)
ob.FinishAsRoot()
return writeEnvelopePrefix(TypeKindReserved, ShapeKindOutputOwners, b.Finish())
}
// writeAddressList writes a stride-20 address list and returns the
// (offset, entry-count) pair.
func writeAddressList(b *zap.Builder, addrs []ids.ShortID) (offset, entryCount int) {
if len(addrs) == 0 {
return 0, 0
}
lb := b.StartList(AddressStride)
for _, a := range addrs {
lb.AddBytes(a[:])
}
off, _ := lb.Finish()
return off, len(addrs)
}
+110
View File
@@ -0,0 +1,110 @@
// 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"
)
// PChainOwner is the cross-VM ZAP schema for
// github.com/luxfi/node/vms/platformvm/warp/message.PChainOwner. Strict
// subset of OutputOwners: Threshold + Addresses, no Locktime.
//
// Used in warp validator registration where locktime is not a meaningful
// gate (validator weight changes happen at known points in the
// permissionless validator pipeline).
//
// Fixed-section layout (size 12 bytes):
//
// Threshold uint32 @ 0
// AddressList list @ 4 (4-byte relOffset + 4-byte length, 8 bytes)
//
// Total fixed section = 12 bytes.
const (
OffsetPChainOwner_Threshold = 0 // uint32
OffsetPChainOwner_AddressList = 4 // list (8 bytes)
SizePChainOwner = 12
)
// PChainOwner is the zero-copy typed accessor over a ZAP-encoded
// PChainOwner wire envelope.
type PChainOwner struct {
msg *zap.Message
obj zap.Object
}
// Threshold returns the signatures-required count.
func (p PChainOwner) Threshold() uint32 {
return p.obj.Uint32(OffsetPChainOwner_Threshold)
}
// AddressList returns the variable-stride address list view.
func (p PChainOwner) AddressList() AddressList {
return AddressList{list: p.obj.ListStride(OffsetPChainOwner_AddressList, AddressStride)}
}
// IsZero reports whether the accessor wraps a parsed message.
func (p PChainOwner) IsZero() bool { return p.msg == nil }
// SyntacticVerify enforces the executor-side semantic gates: non-empty
// addresses, non-zero threshold, threshold <= len(addresses), every
// address non-zero. Returns one of the OutputOwners error sentinels.
func (p PChainOwner) SyntacticVerify() error {
addrs := p.AddressList()
n := addrs.Len()
if n == 0 {
return ErrOwnerAddrsEmpty
}
t := p.Threshold()
if t == 0 {
return ErrOwnerThresholdZero
}
if uint64(t) > uint64(n) {
return ErrOwnerThresholdExceedsAddrs
}
for i := 0; i < n; i++ {
if addrs.At(i) == (ids.ShortID{}) {
return ErrOwnerAddrZero
}
}
return nil
}
// WrapPChainOwner parses a PChainOwner wire envelope into a typed
// accessor.
func WrapPChainOwner(b []byte) (PChainOwner, error) {
_, sk, zapBytes, err := readEnvelopePrefix(b)
if err != nil {
return PChainOwner{}, err
}
if sk != ShapeKindPChainOwner {
return PChainOwner{}, ErrWrongShapeKind
}
msg, err := zap.Parse(zapBytes)
if err != nil {
return PChainOwner{}, err
}
return PChainOwner{msg: msg, obj: msg.Root()}, nil
}
// PChainOwnerInput is the constructor input for NewPChainOwner.
type PChainOwnerInput struct {
Threshold uint32
Addresses []ids.ShortID
}
// NewPChainOwner builds a PChainOwner wire envelope.
func NewPChainOwner(in PChainOwnerInput) []byte {
capEstimate := zap.HeaderSize + SizePChainOwner + len(in.Addresses)*AddressStride + 64
b := zap.NewBuilder(capEstimate)
addrListOff, addrListCount := writeAddressList(b, in.Addresses)
ob := b.StartObject(SizePChainOwner)
ob.SetUint32(OffsetPChainOwner_Threshold, in.Threshold)
ob.SetList(OffsetPChainOwner_AddressList, addrListOff, addrListCount)
ob.FinishAsRoot()
return writeEnvelopePrefix(TypeKindReserved, ShapeKindPChainOwner, b.Finish())
}
+140
View File
@@ -0,0 +1,140 @@
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package wire
import "github.com/luxfi/zap"
// PQMintOutput is the cross-PQ-fx ZAP schema for the MintOutput
// primitive carrying variable-length PQ pubkeys (mldsafx, slhdsafx).
//
// Same payload as PQOutputOwners — MintOutput identifies a mint-authority
// owner group, not a value-bearing output. Only the ShapeKind
// discriminator distinguishes "mint authority" from "spending owner".
const (
OffsetPQMintOutput_SecurityLevel = OffsetPQOutputOwners_SecurityLevel // 0
OffsetPQMintOutput_Locktime = OffsetPQOutputOwners_Locktime // 8
OffsetPQMintOutput_Threshold = OffsetPQOutputOwners_Threshold // 16
OffsetPQMintOutput_PubKeyList = OffsetPQOutputOwners_PubKeyList // 24
SizePQMintOutput = SizePQOutputOwners // 32
)
// ShapeKindPQMintOutput is the discriminator for PQ-fx MintOutput.
const ShapeKindPQMintOutput ShapeKind = 0x11
// PQMintOutput is the zero-copy typed accessor.
type PQMintOutput struct {
tk TypeKind
stride int
msg *zap.Message
obj zap.Object
}
// TypeKind returns the PQ fx family.
func (m PQMintOutput) TypeKind() TypeKind { return m.tk }
// SecurityLevel returns the fx-specific security level byte.
func (m PQMintOutput) SecurityLevel() uint8 {
return m.obj.Uint8(OffsetPQMintOutput_SecurityLevel)
}
// Locktime returns the unix timestamp before which mint cannot fire.
func (m PQMintOutput) Locktime() uint64 {
return m.obj.Uint64(OffsetPQMintOutput_Locktime)
}
// Threshold returns the signatures-required count for mint authority.
func (m PQMintOutput) Threshold() uint32 {
return m.obj.Uint32(OffsetPQMintOutput_Threshold)
}
// PubKeyStride returns the per-element width of the PubKeys list.
func (m PQMintOutput) PubKeyStride() int { return m.stride }
// PubKeys returns the variable-stride pubkey list view.
func (m PQMintOutput) PubKeys() PQPubKeyList {
return PQPubKeyList{stride: m.stride, list: m.obj.ListStride(OffsetPQMintOutput_PubKeyList, uint32(m.stride))}
}
// SyntacticVerify enforces the same gates as PQOutputOwners.
func (m PQMintOutput) SyntacticVerify() error {
pks := m.PubKeys()
n := pks.Len()
if n == 0 {
return ErrPQOwnerPubKeysEmpty
}
th := m.Threshold()
if th == 0 {
return ErrPQOwnerThresholdZero
}
if uint64(th) > uint64(n) {
return ErrPQOwnerThresholdExceeds
}
var prev []byte
for i := 0; i < n; i++ {
pk := pks.At(i)
if len(pk) != m.stride {
return ErrPQOwnerPubKeyWrongStride
}
if prev != nil && pqCompare(prev, pk) >= 0 {
return ErrPQOwnerPubKeysNotSortedUq
}
prev = pk
}
return nil
}
// IsZero reports whether the accessor wraps a parsed message.
func (m PQMintOutput) IsZero() bool { return m.msg == nil }
// WrapPQMintOutput parses a PQMintOutput wire envelope.
func WrapPQMintOutput(b []byte, stride int) (PQMintOutput, error) {
tk, sk, zapBytes, err := readEnvelopePrefix(b)
if err != nil {
return PQMintOutput{}, err
}
if sk != ShapeKindPQMintOutput {
return PQMintOutput{}, ErrWrongShapeKind
}
if tk == TypeKindReserved {
return PQMintOutput{}, ErrWrongTypeKind
}
if stride <= 0 {
return PQMintOutput{}, ErrPQOwnerPubKeyWrongStride
}
msg, err := zap.Parse(zapBytes)
if err != nil {
return PQMintOutput{}, err
}
return PQMintOutput{tk: tk, stride: stride, msg: msg, obj: msg.Root()}, nil
}
// PQMintOutputInput is the constructor input.
type PQMintOutputInput struct {
TypeKind TypeKind
SecurityLevel uint8
Locktime uint64
Threshold uint32
PubKeyStride int
PubKeys [][]byte
}
// NewPQMintOutput builds a PQMintOutput wire envelope.
func NewPQMintOutput(in PQMintOutputInput) []byte {
stride := in.PubKeyStride
if stride <= 0 {
stride = 1
}
capEstimate := zap.HeaderSize + SizePQMintOutput + len(in.PubKeys)*stride + 64
b := zap.NewBuilder(capEstimate)
pkListOff, pkListCount := writePQPubKeyList(b, in.PubKeys, stride)
ob := b.StartObject(SizePQMintOutput)
ob.SetUint8(OffsetPQMintOutput_SecurityLevel, in.SecurityLevel)
ob.SetUint64(OffsetPQMintOutput_Locktime, in.Locktime)
ob.SetUint32(OffsetPQMintOutput_Threshold, in.Threshold)
ob.SetList(OffsetPQMintOutput_PubKeyList, pkListOff, pkListCount)
ob.FinishAsRoot()
return writeEnvelopePrefix(in.TypeKind, ShapeKindPQMintOutput, b.Finish())
}
+266
View File
@@ -0,0 +1,266 @@
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package wire
import (
"errors"
"github.com/luxfi/zap"
)
// PQOutputOwners is the cross-PQ-fx ZAP schema for fx-specific
// OutputOwners that carry FULL post-quantum public keys (not the
// 20-byte hashed ShortIDs used by classical fxs). Both mldsafx and
// slhdsafx are pubkey-on-chain (no hash address derivation in the
// owner set), so they need a wire schema that varies the pubkey
// stride by the SecurityLevel byte.
//
// Fixed-section layout (size 28 bytes; uint64 alignment-tolerant):
//
// SecurityLevel uint8 @ 0
// _padding 7B @ 1 (reserved-zero, 8-aligned)
// Locktime uint64 @ 8
// Threshold uint32 @ 16
// _padding 4B @ 20 (reserved-zero, 8-aligned)
// PubKeyList list @ 24 (8 bytes; payload stride is fx-specific —
// mldsafx: 1312/1952/2592, slhdsafx: 32/48/64)
//
// Wire prefix: TypeKind names the fx (mldsafx, slhdsafx); ShapeKind is
// ShapeKindPQOutputOwners (0x0F).
const (
OffsetPQOutputOwners_SecurityLevel = 0 // uint8
OffsetPQOutputOwners_Locktime = 8 // uint64
OffsetPQOutputOwners_Threshold = 16 // uint32
OffsetPQOutputOwners_PubKeyList = 24 // list (8 bytes)
SizePQOutputOwners = 32
)
// ShapeKindPQOutputOwners is the discriminator for PQ-fx OutputOwners.
const ShapeKindPQOutputOwners ShapeKind = 0x0F
// Semantic-verification errors for PQ owners.
var (
ErrPQOwnerPubKeysEmpty = errors.New("wire: PQOutputOwners.PubKeys is empty — signer set undefined")
ErrPQOwnerThresholdZero = errors.New("wire: PQOutputOwners.Threshold must be > 0; threshold=0 disables authorization")
ErrPQOwnerThresholdExceeds = errors.New("wire: PQOutputOwners.Threshold exceeds PubKeys.Len() — unsatisfiable signer quorum")
ErrPQOwnerPubKeyWrongStride = errors.New("wire: PQOutputOwners.PubKeys entry does not match SecurityLevel's pubkey stride")
ErrPQOwnerPubKeysNotSortedUq = errors.New("wire: PQOutputOwners.PubKeys not sorted lexicographically and unique")
ErrPQAmountZero = errors.New("wire: PQTransferOutput.Amount must be > 0; zero-value transfers are not permitted")
)
// PQOutputOwners is the zero-copy typed accessor over a ZAP-encoded
// PQOutputOwners wire envelope.
//
// READ-ONLY: each pubkey aliases the underlying ZAP buffer.
type PQOutputOwners struct {
tk TypeKind
stride int
msg *zap.Message
obj zap.Object
}
// TypeKind returns the PQ fx family that owns this owner set.
func (o PQOutputOwners) TypeKind() TypeKind { return o.tk }
// SecurityLevel returns the fx-specific security level byte.
func (o PQOutputOwners) SecurityLevel() uint8 {
return o.obj.Uint8(OffsetPQOutputOwners_SecurityLevel)
}
// Locktime returns the unix timestamp before which the output cannot be
// spent.
func (o PQOutputOwners) Locktime() uint64 {
return o.obj.Uint64(OffsetPQOutputOwners_Locktime)
}
// Threshold returns the number of signatures required to spend.
func (o PQOutputOwners) Threshold() uint32 {
return o.obj.Uint32(OffsetPQOutputOwners_Threshold)
}
// PubKeyStride returns the per-element width of the PubKeys list, as
// passed to WrapPQOutputOwners. The caller is responsible for resolving
// SecurityLevel → stride; the wire layer is stride-agnostic.
func (o PQOutputOwners) PubKeyStride() int { return o.stride }
// PubKeys returns the variable-stride pubkey list view.
//
// READ-ONLY: each pubkey aliases the underlying ZAP buffer.
func (o PQOutputOwners) PubKeys() PQPubKeyList {
return PQPubKeyList{stride: o.stride, list: o.obj.ListStride(OffsetPQOutputOwners_PubKeyList, uint32(o.stride))}
}
// IsZero reports whether the accessor wraps a parsed message.
func (o PQOutputOwners) IsZero() bool { return o.msg == nil }
// SyntacticVerify enforces the executor-side semantic gates on a PQ
// owner set read from an untrusted wire buffer. Stride is taken from
// the WrapPQOutputOwners call; per-entry stride mismatches are
// rejected, as are zero-threshold and threshold-exceeds-pubkeys.
func (o PQOutputOwners) SyntacticVerify() error {
pks := o.PubKeys()
n := pks.Len()
if n == 0 {
return ErrPQOwnerPubKeysEmpty
}
t := o.Threshold()
if t == 0 {
return ErrPQOwnerThresholdZero
}
if uint64(t) > uint64(n) {
return ErrPQOwnerThresholdExceeds
}
var prev []byte
for i := 0; i < n; i++ {
pk := pks.At(i)
if len(pk) != o.stride {
return ErrPQOwnerPubKeyWrongStride
}
if prev != nil {
if pqCompare(prev, pk) >= 0 {
return ErrPQOwnerPubKeysNotSortedUq
}
}
prev = pk
}
return nil
}
// PQPubKeyList is the zero-copy view over the pubkey list inside a
// PQOutputOwners. Stride is fx-specific.
type PQPubKeyList struct {
stride int
list zap.List
}
// Len returns the number of pubkeys.
func (p PQPubKeyList) Len() int { return p.list.Len() }
// IsNull returns true if no list pointer was set.
func (p PQPubKeyList) IsNull() bool { return p.list.IsNull() }
// At returns the i'th pubkey as a fresh []byte copy of length stride.
// Returns nil when i is out of range.
func (p PQPubKeyList) At(i int) []byte {
if i < 0 || i >= p.list.Len() {
return nil
}
out := make([]byte, p.stride)
obj := p.list.Object(i, p.stride)
for j := 0; j < p.stride; j++ {
out[j] = obj.Uint8(j)
}
return out
}
// All returns a fresh [][]byte copy of every pubkey in the list.
func (p PQPubKeyList) All() [][]byte {
n := p.list.Len()
out := make([][]byte, n)
for i := 0; i < n; i++ {
out[i] = p.At(i)
}
return out
}
// pqCompare compares two byte slices lexicographically. Mirrors
// bytes.Compare without importing bytes (keeps the wire package's
// dependency surface minimal).
func pqCompare(a, b []byte) int {
min := len(a)
if len(b) < min {
min = len(b)
}
for i := 0; i < min; i++ {
if a[i] < b[i] {
return -1
}
if a[i] > b[i] {
return 1
}
}
if len(a) < len(b) {
return -1
}
if len(a) > len(b) {
return 1
}
return 0
}
// WrapPQOutputOwners parses a PQOutputOwners wire envelope. The stride
// argument names the fx-specific pubkey size — the wire layer does
// not infer it from SecurityLevel because the fx-to-stride mapping
// lives in the fx package (mldsafx.SecurityLevel.PubKeyLen,
// slhdsafx.SecurityLevel.PubKeyLen).
func WrapPQOutputOwners(b []byte, stride int) (PQOutputOwners, error) {
tk, sk, zapBytes, err := readEnvelopePrefix(b)
if err != nil {
return PQOutputOwners{}, err
}
if sk != ShapeKindPQOutputOwners {
return PQOutputOwners{}, ErrWrongShapeKind
}
if tk == TypeKindReserved {
return PQOutputOwners{}, ErrWrongTypeKind
}
if stride <= 0 {
return PQOutputOwners{}, ErrPQOwnerPubKeyWrongStride
}
msg, err := zap.Parse(zapBytes)
if err != nil {
return PQOutputOwners{}, err
}
return PQOutputOwners{tk: tk, stride: stride, msg: msg, obj: msg.Root()}, nil
}
// PQOutputOwnersInput is the constructor input for NewPQOutputOwners.
type PQOutputOwnersInput struct {
TypeKind TypeKind
SecurityLevel uint8
Locktime uint64
Threshold uint32
// PubKeyStride names the per-entry width of the PubKeys list. The
// caller is responsible for the SecurityLevel → stride mapping.
PubKeyStride int
// PubKeys MUST each be exactly PubKeyStride bytes. The constructor
// does not pad or truncate.
PubKeys [][]byte
}
// NewPQOutputOwners builds a PQOutputOwners wire envelope.
func NewPQOutputOwners(in PQOutputOwnersInput) []byte {
stride := in.PubKeyStride
if stride <= 0 {
stride = 1 // refuse to corrupt; caller passes a meaningful stride
}
capEstimate := zap.HeaderSize + SizePQOutputOwners + len(in.PubKeys)*stride + 64
b := zap.NewBuilder(capEstimate)
pkListOff, pkListCount := writePQPubKeyList(b, in.PubKeys, stride)
ob := b.StartObject(SizePQOutputOwners)
ob.SetUint8(OffsetPQOutputOwners_SecurityLevel, in.SecurityLevel)
ob.SetUint64(OffsetPQOutputOwners_Locktime, in.Locktime)
ob.SetUint32(OffsetPQOutputOwners_Threshold, in.Threshold)
ob.SetList(OffsetPQOutputOwners_PubKeyList, pkListOff, pkListCount)
ob.FinishAsRoot()
return writeEnvelopePrefix(in.TypeKind, ShapeKindPQOutputOwners, b.Finish())
}
// writePQPubKeyList writes a stride-N pubkey list. N is the per-pubkey
// byte width passed in by the fx package.
func writePQPubKeyList(b *zap.Builder, pks [][]byte, stride int) (offset, entryCount int) {
if len(pks) == 0 || stride <= 0 {
return 0, 0
}
lb := b.StartList(stride)
for _, pk := range pks {
entry := make([]byte, stride)
copy(entry, pk)
lb.AddBytes(entry)
}
off, _ := lb.Finish()
return off, len(pks)
}
+145
View File
@@ -0,0 +1,145 @@
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package wire
import (
"bytes"
"testing"
)
func TestPQOutputOwners_RoundTrip(t *testing.T) {
// Two ML-DSA-65 pubkeys (1952 bytes each), simulated.
const stride = 1952
pk1 := make([]byte, stride)
pk2 := make([]byte, stride)
for i := range pk1 {
pk1[i] = byte(i)
}
for i := range pk2 {
pk2[i] = byte(0xFF - i)
}
in := PQOutputOwnersInput{
TypeKind: TypeKindMLDSA,
SecurityLevel: 1, // ML-DSA-65
Locktime: 9999,
Threshold: 1,
PubKeyStride: stride,
PubKeys: [][]byte{pk1, pk2},
}
envelope := NewPQOutputOwners(in)
got, err := WrapPQOutputOwners(envelope, stride)
if err != nil {
t.Fatalf("WrapPQOutputOwners: %v", err)
}
if got.TypeKind() != TypeKindMLDSA {
t.Errorf("TypeKind: got %x, want %x", got.TypeKind(), TypeKindMLDSA)
}
if got.SecurityLevel() != 1 {
t.Errorf("SecurityLevel: got %d, want 1", got.SecurityLevel())
}
if got.Locktime() != 9999 {
t.Errorf("Locktime: got %d, want 9999", got.Locktime())
}
if got.Threshold() != 1 {
t.Errorf("Threshold: got %d, want 1", got.Threshold())
}
pks := got.PubKeys()
if pks.Len() != 2 {
t.Fatalf("PubKeys.Len: got %d, want 2", pks.Len())
}
if !bytes.Equal(pks.At(0), pk1) {
t.Errorf("PubKeys[0] mismatch")
}
if !bytes.Equal(pks.At(1), pk2) {
t.Errorf("PubKeys[1] mismatch")
}
// pk1 < pk2 lexicographically (bytes 0x00..ff < 0xFF..00 starting at byte 0).
if err := got.SyntacticVerify(); err != nil {
t.Errorf("SyntacticVerify: %v", err)
}
}
func TestPQTransferOutput_RoundTrip(t *testing.T) {
// Single SLH-DSA-SHA2-192f pubkey (48 bytes), simulated.
const stride = 48
pk := make([]byte, stride)
for i := range pk {
pk[i] = byte(i + 10)
}
in := PQTransferOutputInput{
TypeKind: TypeKindSLHDSA,
SecurityLevel: 1,
Amount: 7_777_777,
Locktime: 42,
Threshold: 1,
PubKeyStride: stride,
PubKeys: [][]byte{pk},
}
envelope := NewPQTransferOutput(in)
got, err := WrapPQTransferOutput(envelope, stride)
if err != nil {
t.Fatalf("WrapPQTransferOutput: %v", err)
}
if got.Amount() != 7_777_777 {
t.Errorf("Amount: got %d, want 7_777_777", got.Amount())
}
if got.Threshold() != 1 {
t.Errorf("Threshold: got %d, want 1", got.Threshold())
}
pks := got.PubKeys()
if pks.Len() != 1 {
t.Fatalf("PubKeys.Len: got %d, want 1", pks.Len())
}
if !bytes.Equal(pks.At(0), pk) {
t.Errorf("PubKeys[0] mismatch")
}
if err := got.SyntacticVerify(); err != nil {
t.Errorf("SyntacticVerify: %v", err)
}
}
func TestPQMintOutput_RoundTrip(t *testing.T) {
const stride = 1952
pk := make([]byte, stride)
for i := range pk {
pk[i] = byte(i * 3)
}
in := PQMintOutputInput{
TypeKind: TypeKindMLDSA,
SecurityLevel: 1,
Locktime: 0,
Threshold: 1,
PubKeyStride: stride,
PubKeys: [][]byte{pk},
}
envelope := NewPQMintOutput(in)
got, err := WrapPQMintOutput(envelope, stride)
if err != nil {
t.Fatalf("WrapPQMintOutput: %v", err)
}
if got.SecurityLevel() != 1 {
t.Errorf("SecurityLevel: got %d, want 1", got.SecurityLevel())
}
if got.Threshold() != 1 {
t.Errorf("Threshold: got %d, want 1", got.Threshold())
}
if !bytes.Equal(got.PubKeys().At(0), pk) {
t.Errorf("PubKeys[0] mismatch")
}
if err := got.SyntacticVerify(); err != nil {
t.Errorf("SyntacticVerify: %v", err)
}
}
func TestPQOutputOwners_RejectsWrongShape(t *testing.T) {
// Build a classical OutputOwners and feed it to the PQ Wrap. Should fail
// the ShapeKind check.
classical := NewOutputOwners(OutputOwnersInput{
Locktime: 0,
Threshold: 1,
})
if _, err := WrapPQOutputOwners(classical, 1952); err != ErrWrongShapeKind {
t.Errorf("WrapPQOutputOwners(classical): got err=%v, want ErrWrongShapeKind", err)
}
}
+161
View File
@@ -0,0 +1,161 @@
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package wire
import "github.com/luxfi/zap"
// PQTransferOutput is the cross-PQ-fx ZAP schema for the TransferOutput
// primitive carrying variable-length PQ pubkeys (mldsafx, slhdsafx).
//
// Fixed-section layout (size 40 bytes; uint64 alignment-tolerant):
//
// SecurityLevel uint8 @ 0
// _padding 7B @ 1 (reserved-zero, 8-aligned)
// Amount uint64 @ 8
// Locktime uint64 @ 16
// Threshold uint32 @ 24
// _padding 4B @ 28 (reserved-zero, 8-aligned)
// PubKeyList list @ 32 (8 bytes; payload stride is fx-specific)
//
// Wire prefix: TypeKind names the fx; ShapeKind is
// ShapeKindPQTransferOutput (0x10).
const (
OffsetPQTransferOutput_SecurityLevel = 0 // uint8
OffsetPQTransferOutput_Amount = 8 // uint64
OffsetPQTransferOutput_Locktime = 16 // uint64
OffsetPQTransferOutput_Threshold = 24 // uint32
OffsetPQTransferOutput_PubKeyList = 32 // list (8 bytes)
SizePQTransferOutput = 40
)
// ShapeKindPQTransferOutput is the discriminator for PQ-fx TransferOutput.
const ShapeKindPQTransferOutput ShapeKind = 0x10
// PQTransferOutput is the zero-copy typed accessor.
type PQTransferOutput struct {
tk TypeKind
stride int
msg *zap.Message
obj zap.Object
}
// TypeKind returns the PQ fx family.
func (t PQTransferOutput) TypeKind() TypeKind { return t.tk }
// SecurityLevel returns the fx-specific security level byte.
func (t PQTransferOutput) SecurityLevel() uint8 {
return t.obj.Uint8(OffsetPQTransferOutput_SecurityLevel)
}
// Amount returns the asset amount this output is worth.
func (t PQTransferOutput) Amount() uint64 {
return t.obj.Uint64(OffsetPQTransferOutput_Amount)
}
// Locktime returns the unix timestamp before which the output cannot be
// spent.
func (t PQTransferOutput) Locktime() uint64 {
return t.obj.Uint64(OffsetPQTransferOutput_Locktime)
}
// Threshold returns the signatures-required count.
func (t PQTransferOutput) Threshold() uint32 {
return t.obj.Uint32(OffsetPQTransferOutput_Threshold)
}
// PubKeyStride returns the per-element width of the PubKeys list.
func (t PQTransferOutput) PubKeyStride() int { return t.stride }
// PubKeys returns the variable-stride pubkey list view.
func (t PQTransferOutput) PubKeys() PQPubKeyList {
return PQPubKeyList{stride: t.stride, list: t.obj.ListStride(OffsetPQTransferOutput_PubKeyList, uint32(t.stride))}
}
// SyntacticVerify enforces Amount > 0 plus the PQOutputOwners gates.
func (t PQTransferOutput) SyntacticVerify() error {
if t.Amount() == 0 {
return ErrPQAmountZero
}
pks := t.PubKeys()
n := pks.Len()
if n == 0 {
return ErrPQOwnerPubKeysEmpty
}
th := t.Threshold()
if th == 0 {
return ErrPQOwnerThresholdZero
}
if uint64(th) > uint64(n) {
return ErrPQOwnerThresholdExceeds
}
var prev []byte
for i := 0; i < n; i++ {
pk := pks.At(i)
if len(pk) != t.stride {
return ErrPQOwnerPubKeyWrongStride
}
if prev != nil && pqCompare(prev, pk) >= 0 {
return ErrPQOwnerPubKeysNotSortedUq
}
prev = pk
}
return nil
}
// IsZero reports whether the accessor wraps a parsed message.
func (t PQTransferOutput) IsZero() bool { return t.msg == nil }
// WrapPQTransferOutput parses a PQTransferOutput wire envelope.
func WrapPQTransferOutput(b []byte, stride int) (PQTransferOutput, error) {
tk, sk, zapBytes, err := readEnvelopePrefix(b)
if err != nil {
return PQTransferOutput{}, err
}
if sk != ShapeKindPQTransferOutput {
return PQTransferOutput{}, ErrWrongShapeKind
}
if tk == TypeKindReserved {
return PQTransferOutput{}, ErrWrongTypeKind
}
if stride <= 0 {
return PQTransferOutput{}, ErrPQOwnerPubKeyWrongStride
}
msg, err := zap.Parse(zapBytes)
if err != nil {
return PQTransferOutput{}, err
}
return PQTransferOutput{tk: tk, stride: stride, msg: msg, obj: msg.Root()}, nil
}
// PQTransferOutputInput is the constructor input.
type PQTransferOutputInput struct {
TypeKind TypeKind
SecurityLevel uint8
Amount uint64
Locktime uint64
Threshold uint32
PubKeyStride int
PubKeys [][]byte
}
// NewPQTransferOutput builds a PQTransferOutput wire envelope.
func NewPQTransferOutput(in PQTransferOutputInput) []byte {
stride := in.PubKeyStride
if stride <= 0 {
stride = 1
}
capEstimate := zap.HeaderSize + SizePQTransferOutput + len(in.PubKeys)*stride + 64
b := zap.NewBuilder(capEstimate)
pkListOff, pkListCount := writePQPubKeyList(b, in.PubKeys, stride)
ob := b.StartObject(SizePQTransferOutput)
ob.SetUint8(OffsetPQTransferOutput_SecurityLevel, in.SecurityLevel)
ob.SetUint64(OffsetPQTransferOutput_Amount, in.Amount)
ob.SetUint64(OffsetPQTransferOutput_Locktime, in.Locktime)
ob.SetUint32(OffsetPQTransferOutput_Threshold, in.Threshold)
ob.SetList(OffsetPQTransferOutput_PubKeyList, pkListOff, pkListCount)
ob.FinishAsRoot()
return writeEnvelopePrefix(in.TypeKind, ShapeKindPQTransferOutput, b.Finish())
}
+67
View File
@@ -0,0 +1,67 @@
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package wire
import (
"fmt"
"github.com/luxfi/crypto/hash"
"github.com/luxfi/crypto/secp256k1"
)
// SignSecp256k1 is the ZAP-native replacement for the legacy
// `txs.Tx.Sign(c codec.Manager, ...)` method. It takes the unsigned tx
// bytes (already ZAP-encoded by the txs.zap_native build path) plus a
// list of signer groups (one group per input, like the legacy API), and
// returns a fully-formed SignedTx wire envelope.
//
// The signing target is hash(unsignedBytes) — same as the legacy Sign.
// Each signer group produces one Credential whose Signatures field is
// the concatenation of all per-key secp256k1 signatures.
//
// This is the canonical secp256k1fx signing entry point. For multi-fx
// signing (mixing secp256k1 + ML-DSA + Ed25519 credentials), build the
// per-input credentials individually with NewCredential and pass them
// directly to NewSignedTx.
//
// Returns:
// - signedTxBytes: the canonical wire envelope (SignedTx{unsigned, creds})
// - err: a typed error from secp256k1.PrivateKey.SignHash on failure
func SignSecp256k1(unsignedBytes []byte, signers [][]*secp256k1.PrivateKey) ([]byte, error) {
txHash := hash.ComputeHash256(unsignedBytes)
credentials := make([][]byte, 0, len(signers))
for groupIdx, keys := range signers {
// Each input may have multiple co-signers; concatenate their
// secp256k1.SignatureLen-byte signatures into the credential.
sigsConcat := make([]byte, 0, len(keys)*secp256k1.SignatureLen)
for keyIdx, key := range keys {
sig, err := key.SignHash(txHash)
if err != nil {
return nil, fmt.Errorf(
"wire.SignSecp256k1: group %d key %d: %w",
groupIdx, keyIdx, err,
)
}
if len(sig) != secp256k1.SignatureLen {
return nil, fmt.Errorf(
"wire.SignSecp256k1: group %d key %d: sig length %d != %d",
groupIdx, keyIdx, len(sig), secp256k1.SignatureLen,
)
}
sigsConcat = append(sigsConcat, sig...)
}
cred := NewCredential(CredentialInput{
TypeKind: TypeKindSecp256k1,
SecurityLevel: 0,
Signatures: sigsConcat,
})
credentials = append(credentials, cred)
}
return NewSignedTx(SignedTxInput{
UnsignedBytes: unsignedBytes,
Credentials: credentials,
}), nil
}
+97
View File
@@ -0,0 +1,97 @@
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package wire
import (
"bytes"
"testing"
"github.com/luxfi/crypto/hash"
"github.com/luxfi/crypto/secp256k1"
)
func TestSignSecp256k1_RoundTrip(t *testing.T) {
// Generate a couple of test keys.
k1, err := secp256k1.NewPrivateKey()
if err != nil {
t.Fatalf("NewPrivateKey: %v", err)
}
k2, err := secp256k1.NewPrivateKey()
if err != nil {
t.Fatalf("NewPrivateKey: %v", err)
}
// Pretend we have an unsigned tx blob (in production this would be a
// zap_native-encoded UnsignedTx with a TxKind discriminator at offset 0).
unsigned := bytes.Repeat([]byte("UNSIGNED-TX-BYTES "), 16)
// Two inputs: input 0 needs (k1, k2) co-signers; input 1 needs k2 alone.
signers := [][]*secp256k1.PrivateKey{
{k1, k2},
{k2},
}
signedTxBytes, err := SignSecp256k1(unsigned, signers)
if err != nil {
t.Fatalf("SignSecp256k1: %v", err)
}
got, err := WrapSignedTx(signedTxBytes)
if err != nil {
t.Fatalf("WrapSignedTx: %v", err)
}
if !bytes.Equal(got.UnsignedBytes(), unsigned) {
t.Fatalf("UnsignedBytes mismatch")
}
if got.CredentialCount() != 2 {
t.Fatalf("CredentialCount: got %d, want 2", got.CredentialCount())
}
all, err := got.AllCredentials()
if err != nil {
t.Fatalf("AllCredentials: %v", err)
}
if len(all) != 2 {
t.Fatalf("AllCredentials len: got %d, want 2", len(all))
}
// Validate the signatures recover the right pubkey.
txHash := hash.ComputeHash256(unsigned)
// Input 0: 2 sigs (k1, k2).
cred0 := all[0]
if cred0.SignatureCount(secp256k1.SignatureLen) != 2 {
t.Fatalf("cred0 sig count: got %d, want 2", cred0.SignatureCount(secp256k1.SignatureLen))
}
sig0_0 := cred0.SignatureAt(0, secp256k1.SignatureLen)
pk0_0, err := secp256k1.RecoverPublicKeyFromHash(txHash, sig0_0)
if err != nil {
t.Fatalf("recover sig0_0: %v", err)
}
if !bytes.Equal(pk0_0.Bytes(), k1.PublicKey().Bytes()) {
t.Errorf("sig0_0 pubkey mismatch")
}
sig0_1 := cred0.SignatureAt(1, secp256k1.SignatureLen)
pk0_1, err := secp256k1.RecoverPublicKeyFromHash(txHash, sig0_1)
if err != nil {
t.Fatalf("recover sig0_1: %v", err)
}
if !bytes.Equal(pk0_1.Bytes(), k2.PublicKey().Bytes()) {
t.Errorf("sig0_1 pubkey mismatch")
}
// Input 1: 1 sig (k2).
cred1 := all[1]
if cred1.SignatureCount(secp256k1.SignatureLen) != 1 {
t.Fatalf("cred1 sig count: got %d, want 1", cred1.SignatureCount(secp256k1.SignatureLen))
}
sig1_0 := cred1.SignatureAt(0, secp256k1.SignatureLen)
pk1_0, err := secp256k1.RecoverPublicKeyFromHash(txHash, sig1_0)
if err != nil {
t.Fatalf("recover sig1_0: %v", err)
}
if !bytes.Equal(pk1_0.Bytes(), k2.PublicKey().Bytes()) {
t.Errorf("sig1_0 pubkey mismatch")
}
}
+198
View File
@@ -0,0 +1,198 @@
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package wire
import "github.com/luxfi/zap"
// SignedTx is the ZAP-native replacement for the legacy
// `txs.Tx{Unsigned UnsignedTx; Creds []verify.Verifiable}` envelope used
// by both platformvm and xvm. It wraps:
// - UnsignedBytes: the canonical wire bytes the signature was computed over
// - Credentials: a list of fxs Credential wire envelopes (one per input)
//
// The unsigned bytes already carry a TxKind discriminator at offset 0
// (see vms/platformvm/txs/zap_native/kind.go). The Credentials list
// indices align with the UnsignedTx's input list indices (1:1 by index)
// just like the legacy Tx.Creds slice.
//
// Fixed-section layout (size 16 bytes):
//
// UnsignedBytes bytes @ 0 (8 bytes — relOffset + length)
// Credentials list @ 8 (8 bytes — relOffset + length; stride is
// the per-credential ENVELOPE size, variable)
//
// Wire prefix: TypeKind=0x00 (reserved/cross-VM envelope),
// ShapeKind=0x0E (SignedTx).
//
// Because credentials are variable-stride byte envelopes (each carries
// its own TypeKind+ShapeKind+ZAP message of independent length), the
// credential list is encoded as a packed sequence of length-prefixed
// envelopes rather than a fixed-stride ZAP list. See
// SignedTx.CredentialAt for the parser.
const (
OffsetSignedTx_UnsignedBytes = 0 // bytes (8 bytes)
OffsetSignedTx_CredentialCount = 8 // uint32 (number of credentials)
OffsetSignedTx_CredentialBytes = 12 // bytes (8 bytes — all credential envelopes concatenated)
SizeSignedTx = 20
)
// SignedTx is the zero-copy typed accessor.
type SignedTx struct {
msg *zap.Message
obj zap.Object
}
// UnsignedBytes returns the unsigned tx bytes. This is the canonical
// signing target — every fxs signature was computed over a hash of
// these bytes.
//
// READ-ONLY: aliases the underlying buffer. Use append([]byte(nil), ...)
// to take ownership.
func (s SignedTx) UnsignedBytes() []byte {
return s.obj.Bytes(OffsetSignedTx_UnsignedBytes)
}
// CredentialCount returns the number of credentials.
func (s SignedTx) CredentialCount() uint32 {
return s.obj.Uint32(OffsetSignedTx_CredentialCount)
}
// CredentialBytes returns the concatenated credential envelopes blob.
// Each credential is a self-describing wire envelope (2-byte prefix +
// length-prefixed ZAP message); see CredentialAt for the index walk.
func (s SignedTx) CredentialBytes() []byte {
return s.obj.Bytes(OffsetSignedTx_CredentialBytes)
}
// CredentialAt parses the i'th credential envelope from the concatenated
// blob and returns its typed accessor. Returns the zero Credential
// (IsZero=true) if i is out of range or the blob is malformed.
//
// Walking is O(i) because each ZAP credential carries its own length in
// the wire header. For a hot-loop verifier, prefetch all credentials
// once into a []Credential slice.
func (s SignedTx) CredentialAt(i uint32) (Credential, error) {
count := s.CredentialCount()
if i >= count {
return Credential{}, ErrWrongShapeKind
}
blob := s.CredentialBytes()
cursor := 0
for k := uint32(0); k <= i; k++ {
if cursor+EnvelopePrefix > len(blob) {
return Credential{}, ErrShortEnvelope
}
// Peek the inner ZAP message length from its header. ZAP header
// carries (Magic:4, Version:2, Flags:2, RootOffset:4, Size:4) =
// 16 bytes; Size is the inner message byte count.
zapStart := cursor + EnvelopePrefix
if zapStart+zap.HeaderSize > len(blob) {
return Credential{}, ErrShortEnvelope
}
// Size is at offset 12..16 within the ZAP header (little-endian
// uint32).
zapSize := int(blob[zapStart+12]) |
int(blob[zapStart+13])<<8 |
int(blob[zapStart+14])<<16 |
int(blob[zapStart+15])<<24
envEnd := zapStart + zapSize
if envEnd > len(blob) {
return Credential{}, ErrShortEnvelope
}
if k == i {
return WrapCredential(blob[cursor:envEnd])
}
cursor = envEnd
}
return Credential{}, ErrWrongShapeKind
}
// AllCredentials parses every credential into a fresh slice. Use this
// when the caller needs to iterate all credentials (the executor's
// VerifyTransfer pass).
func (s SignedTx) AllCredentials() ([]Credential, error) {
count := s.CredentialCount()
out := make([]Credential, 0, count)
blob := s.CredentialBytes()
cursor := 0
for k := uint32(0); k < count; k++ {
if cursor+EnvelopePrefix > len(blob) {
return nil, ErrShortEnvelope
}
zapStart := cursor + EnvelopePrefix
if zapStart+zap.HeaderSize > len(blob) {
return nil, ErrShortEnvelope
}
zapSize := int(blob[zapStart+12]) |
int(blob[zapStart+13])<<8 |
int(blob[zapStart+14])<<16 |
int(blob[zapStart+15])<<24
envEnd := zapStart + zapSize
if envEnd > len(blob) {
return nil, ErrShortEnvelope
}
c, err := WrapCredential(blob[cursor:envEnd])
if err != nil {
return nil, err
}
out = append(out, c)
cursor = envEnd
}
return out, nil
}
// IsZero reports whether the accessor wraps a parsed message.
func (s SignedTx) IsZero() bool { return s.msg == nil }
// WrapSignedTx parses a SignedTx wire envelope.
func WrapSignedTx(b []byte) (SignedTx, error) {
_, sk, zapBytes, err := readEnvelopePrefix(b)
if err != nil {
return SignedTx{}, err
}
if sk != ShapeKindSignedTx {
return SignedTx{}, ErrWrongShapeKind
}
msg, err := zap.Parse(zapBytes)
if err != nil {
return SignedTx{}, err
}
return SignedTx{msg: msg, obj: msg.Root()}, nil
}
// SignedTxInput is the constructor input.
type SignedTxInput struct {
UnsignedBytes []byte
// Credentials is the slice of credential wire envelopes (each one
// already prefixed with its own TypeKind+ShapeKind+ZAP message).
// The constructor concatenates them — order is preserved and aligns
// 1:1 with the unsigned tx's input list.
Credentials [][]byte
}
// NewSignedTx builds a SignedTx wire envelope. The unsigned bytes are
// stored verbatim, and the credentials slice is concatenated into a
// single byte run — the per-credential ZAP header carries each
// envelope's length so the parser can walk them.
func NewSignedTx(in SignedTxInput) []byte {
// Concatenate all credential envelopes.
totalCredBytes := 0
for _, c := range in.Credentials {
totalCredBytes += len(c)
}
credBlob := make([]byte, 0, totalCredBytes)
for _, c := range in.Credentials {
credBlob = append(credBlob, c...)
}
capEstimate := zap.HeaderSize + SizeSignedTx + len(in.UnsignedBytes) + totalCredBytes + 64
b := zap.NewBuilder(capEstimate)
ob := b.StartObject(SizeSignedTx)
ob.SetBytes(OffsetSignedTx_UnsignedBytes, in.UnsignedBytes)
ob.SetUint32(OffsetSignedTx_CredentialCount, uint32(len(in.Credentials)))
ob.SetBytes(OffsetSignedTx_CredentialBytes, credBlob)
ob.FinishAsRoot()
return writeEnvelopePrefix(TypeKindReserved, ShapeKindSignedTx, b.Finish())
}
+117
View File
@@ -0,0 +1,117 @@
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package wire
import "github.com/luxfi/zap"
// TransferInput is the cross-fx ZAP schema for the TransferInput
// primitive (`Amt uint64 + SigIndices []uint32`). The semantics are
// uniform across classical and PQ fxs — only the matching Credential's
// signature size varies.
//
// Fixed-section layout (size 16 bytes):
//
// Amount uint64 @ 0
// SigIndicesList list @ 8 (4-byte relOffset + 4-byte length, 8 bytes)
//
// SigIndicesList payload is stride-4 (each entry is a uint32).
//
// Wire prefix discriminator: TypeKind names the fx; ShapeKind is
// ShapeKindTransferInput (0x02).
const (
OffsetTransferInput_Amount = 0 // uint64
OffsetTransferInput_SigIndicesList = 8 // list (8 bytes)
SizeTransferInput = 16
)
// SigIndexStride is the per-element width of the SigIndices list (one
// uint32 per index).
const SigIndexStride = 4
// TransferInput is the zero-copy typed accessor.
type TransferInput struct {
tk TypeKind
msg *zap.Message
obj zap.Object
}
// TypeKind returns the fx family that owns this input.
func (t TransferInput) TypeKind() TypeKind { return t.tk }
// Amount returns the amount being spent.
func (t TransferInput) Amount() uint64 {
return t.obj.Uint64(OffsetTransferInput_Amount)
}
// SigIndices returns the SigIndices view as a fresh []uint32.
func (t TransferInput) SigIndices() []uint32 {
l := t.obj.ListStride(OffsetTransferInput_SigIndicesList, SigIndexStride)
n := l.Len()
out := make([]uint32, n)
for i := 0; i < n; i++ {
out[i] = l.Uint32(i)
}
return out
}
// SigIndicesLen returns the number of signature indices.
func (t TransferInput) SigIndicesLen() int {
return t.obj.ListStride(OffsetTransferInput_SigIndicesList, SigIndexStride).Len()
}
// IsZero reports whether the accessor wraps a parsed message.
func (t TransferInput) IsZero() bool { return t.msg == nil }
// WrapTransferInput parses a TransferInput wire envelope.
func WrapTransferInput(b []byte) (TransferInput, error) {
tk, sk, zapBytes, err := readEnvelopePrefix(b)
if err != nil {
return TransferInput{}, err
}
if sk != ShapeKindTransferInput {
return TransferInput{}, ErrWrongShapeKind
}
if tk == TypeKindReserved {
return TransferInput{}, ErrWrongTypeKind
}
msg, err := zap.Parse(zapBytes)
if err != nil {
return TransferInput{}, err
}
return TransferInput{tk: tk, msg: msg, obj: msg.Root()}, nil
}
// TransferInputInput is the constructor input.
type TransferInputInput struct {
TypeKind TypeKind
Amount uint64
SigIndices []uint32
}
// NewTransferInput builds a TransferInput wire envelope.
func NewTransferInput(in TransferInputInput) []byte {
capEstimate := zap.HeaderSize + SizeTransferInput + len(in.SigIndices)*SigIndexStride + 64
b := zap.NewBuilder(capEstimate)
sigIdxOff, sigIdxCount := writeSigIndices(b, in.SigIndices)
ob := b.StartObject(SizeTransferInput)
ob.SetUint64(OffsetTransferInput_Amount, in.Amount)
ob.SetList(OffsetTransferInput_SigIndicesList, sigIdxOff, sigIdxCount)
ob.FinishAsRoot()
return writeEnvelopePrefix(in.TypeKind, ShapeKindTransferInput, b.Finish())
}
// writeSigIndices writes a stride-4 list of uint32 signature indices.
func writeSigIndices(b *zap.Builder, sigs []uint32) (offset, entryCount int) {
if len(sigs) == 0 {
return 0, 0
}
lb := b.StartList(SigIndexStride)
for _, s := range sigs {
lb.AddUint32(s)
}
off, _ := lb.Finish()
return off, len(sigs)
}
+138
View File
@@ -0,0 +1,138 @@
// 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"
)
// TransferOutput is the cross-fx ZAP schema for the TransferOutput
// primitive (`Amt uint64 + OutputOwners`). Every classical fx
// (secp256k1fx, ed25519fx, secp256r1fx, schnorrfx) and post-quantum fx
// (mldsafx, slhdsafx) use this same shape — they differ only in the
// Credential's signature size, not in the spending-output layout.
//
// Fixed-section layout (size 28 bytes; uint64 reads alignment-tolerant):
//
// Amount uint64 @ 0
// Locktime uint64 @ 8
// Threshold uint32 @ 16
// AddressList list @ 20 (4-byte relOffset + 4-byte length, 8 bytes)
//
// Total fixed section = 28 bytes. The TypeKind discriminator byte in the
// 2-byte wire prefix names the owning fx (e.g. 0x01=secp256k1fx,
// 0x02=mldsafx). The ShapeKind byte is always
// ShapeKindTransferOutput (0x01).
const (
OffsetTransferOutput_Amount = 0 // uint64
OffsetTransferOutput_Locktime = 8 // uint64
OffsetTransferOutput_Threshold = 16 // uint32
OffsetTransferOutput_AddressList = 20 // list (8 bytes)
SizeTransferOutput = 28
)
// TransferOutput is the zero-copy typed accessor over a ZAP-encoded
// TransferOutput wire envelope.
type TransferOutput struct {
tk TypeKind
msg *zap.Message
obj zap.Object
}
// TypeKind returns the fx family that owns this output.
func (t TransferOutput) TypeKind() TypeKind { return t.tk }
// Amount returns the asset amount this output is worth.
func (t TransferOutput) Amount() uint64 {
return t.obj.Uint64(OffsetTransferOutput_Amount)
}
// Locktime returns the unix timestamp before which the output cannot be
// spent.
func (t TransferOutput) Locktime() uint64 {
return t.obj.Uint64(OffsetTransferOutput_Locktime)
}
// Threshold returns the signatures-required count.
func (t TransferOutput) Threshold() uint32 {
return t.obj.Uint32(OffsetTransferOutput_Threshold)
}
// AddressList returns the address list view.
func (t TransferOutput) AddressList() AddressList {
return AddressList{list: t.obj.ListStride(OffsetTransferOutput_AddressList, AddressStride)}
}
// SyntacticVerify enforces the same gates as OutputOwners.SyntacticVerify
// plus Amount > 0.
func (t TransferOutput) SyntacticVerify() error {
addrs := t.AddressList()
n := addrs.Len()
if n == 0 {
return ErrOwnerAddrsEmpty
}
th := t.Threshold()
if th == 0 {
return ErrOwnerThresholdZero
}
if uint64(th) > uint64(n) {
return ErrOwnerThresholdExceedsAddrs
}
for i := 0; i < n; i++ {
if addrs.At(i) == (ids.ShortID{}) {
return ErrOwnerAddrZero
}
}
return nil
}
// IsZero reports whether the accessor wraps a parsed message.
func (t TransferOutput) IsZero() bool { return t.msg == nil }
// WrapTransferOutput parses a TransferOutput wire envelope into a typed
// accessor. Accepts any TypeKind (classical or PQ); ShapeKind must be
// ShapeKindTransferOutput.
func WrapTransferOutput(b []byte) (TransferOutput, error) {
tk, sk, zapBytes, err := readEnvelopePrefix(b)
if err != nil {
return TransferOutput{}, err
}
if sk != ShapeKindTransferOutput {
return TransferOutput{}, ErrWrongShapeKind
}
if tk == TypeKindReserved {
return TransferOutput{}, ErrWrongTypeKind
}
msg, err := zap.Parse(zapBytes)
if err != nil {
return TransferOutput{}, err
}
return TransferOutput{tk: tk, msg: msg, obj: msg.Root()}, nil
}
// TransferOutputInput is the constructor input.
type TransferOutputInput struct {
TypeKind TypeKind
Amount uint64
Locktime uint64
Threshold uint32
Addresses []ids.ShortID
}
// NewTransferOutput builds a TransferOutput wire envelope.
func NewTransferOutput(in TransferOutputInput) []byte {
capEstimate := zap.HeaderSize + SizeTransferOutput + len(in.Addresses)*AddressStride + 64
b := zap.NewBuilder(capEstimate)
addrListOff, addrListCount := writeAddressList(b, in.Addresses)
ob := b.StartObject(SizeTransferOutput)
ob.SetUint64(OffsetTransferOutput_Amount, in.Amount)
ob.SetUint64(OffsetTransferOutput_Locktime, in.Locktime)
ob.SetUint32(OffsetTransferOutput_Threshold, in.Threshold)
ob.SetList(OffsetTransferOutput_AddressList, addrListOff, addrListCount)
ob.FinishAsRoot()
return writeEnvelopePrefix(in.TypeKind, ShapeKindTransferOutput, b.Finish())
}
+159
View File
@@ -0,0 +1,159 @@
// 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"
)
// UTXO is the cross-VM ZAP schema for github.com/luxfi/utxo.UTXO. Carries
// the spent TxID + OutputIndex (UTXOID), the asset identifier, and the
// concrete output payload (a fxs primitive — TransferOutput, MintOutput,
// AttestationOutput). The output payload is itself a wire envelope with
// its own (TypeKind, ShapeKind) discriminator pair, written as a bytes
// field in the parent UTXO message.
//
// Fixed-section layout (size 76 bytes; uint64 reads alignment-tolerant):
//
// TxID 32B @ 0
// OutputIndex uint32 @ 32
// AssetID 32B @ 36
// Output bytes @ 68 (relOffset + length, 8 bytes)
//
// Total fixed section = 32 + 4 + 32 + 8 = 76 bytes.
//
// The Output bytes field carries the inner fxs primitive's wire envelope
// (2-byte discriminator prefix + ZAP message). Consumers parse it via
// WrapTransferableOutput / WrapMintOutput / WrapAttestationOutput,
// dispatching on the discriminator pair.
const (
OffsetUTXO_TxID = 0
OffsetUTXO_OutputIndex = 32 // uint32
OffsetUTXO_AssetID = 36 // 32B
OffsetUTXO_Output = 68 // bytes (relOffset + length, 8 bytes)
SizeUTXO = 76
)
// UTXO is the zero-copy typed accessor over a ZAP-encoded UTXO wire
// envelope.
//
// READ-ONLY: every field aliases the underlying ZAP buffer. Mutation
// corrupts any UTXOID = hash(buffer) computed downstream and breaks
// cross-VM atomic UTXO transfer semantics. Use append([]byte(nil), ...)
// to take ownership of the Output bytes when handing off to a different
// goroutine.
type UTXO struct {
msg *zap.Message
obj zap.Object
}
// TxID returns the spent UTXO's tx id.
func (u UTXO) TxID() ids.ID {
var out ids.ID
for i := 0; i < 32; i++ {
out[i] = u.obj.Uint8(OffsetUTXO_TxID + i)
}
return out
}
// OutputIndex returns the spent UTXO's output index.
func (u UTXO) OutputIndex() uint32 {
return u.obj.Uint32(OffsetUTXO_OutputIndex)
}
// AssetID returns the asset identifier.
func (u UTXO) AssetID() ids.ID {
var out ids.ID
for i := 0; i < 32; i++ {
out[i] = u.obj.Uint8(OffsetUTXO_AssetID + i)
}
return out
}
// OutputBytes returns the inner fxs primitive's wire envelope (2-byte
// discriminator prefix + ZAP message). Use WrapTransferableOutput,
// WrapMintOutput, etc. to parse the bytes into a typed accessor after
// dispatching on the (TypeKind, ShapeKind) pair.
//
// READ-ONLY: aliases the underlying ZAP buffer.
func (u UTXO) OutputBytes() []byte {
return u.obj.Bytes(OffsetUTXO_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.
func (u UTXO) OutputDiscriminator() (TypeKind, ShapeKind) {
b := u.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) for the UTXO. Stable across calls — backed by the
// originally-parsed buffer.
func (u UTXO) Bytes() []byte {
out := make([]byte, EnvelopePrefix+len(u.msg.Bytes()))
out[0] = byte(TypeKindReserved) // UTXOs aren't fx-owned; TypeKind=0
out[1] = byte(ShapeKindUTXO)
copy(out[EnvelopePrefix:], u.msg.Bytes())
return out
}
// IsZero reports whether the accessor wraps a parsed message.
func (u UTXO) IsZero() bool { return u.msg == nil }
// WrapUTXO parses a UTXO 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-UTXO shape.
func WrapUTXO(b []byte) (UTXO, error) {
_, sk, zapBytes, err := readEnvelopePrefix(b)
if err != nil {
return UTXO{}, err
}
if sk != ShapeKindUTXO {
return UTXO{}, ErrWrongShapeKind
}
msg, err := zap.Parse(zapBytes)
if err != nil {
return UTXO{}, err
}
return UTXO{msg: msg, obj: msg.Root()}, nil
}
// UTXOInput is the constructor input for NewUTXO.
type UTXOInput struct {
TxID ids.ID
OutputIndex uint32
AssetID ids.ID
// Output is the inner fxs primitive's wire envelope (already
// prefixed with its own discriminator). The constructor stores
// these bytes verbatim in the Output field.
Output []byte
}
// NewUTXO builds a UTXO wire envelope (2-byte discriminator prefix +
// ZAP message) from the input fields. The returned slice is the
// canonical on-wire representation.
func NewUTXO(in UTXOInput) []byte {
capEstimate := zap.HeaderSize + SizeUTXO + len(in.Output) + 64
b := zap.NewBuilder(capEstimate)
ob := b.StartObject(SizeUTXO)
for i := 0; i < 32; i++ {
ob.SetUint8(OffsetUTXO_TxID+i, in.TxID[i])
}
ob.SetUint32(OffsetUTXO_OutputIndex, in.OutputIndex)
for i := 0; i < 32; i++ {
ob.SetUint8(OffsetUTXO_AssetID+i, in.AssetID[i])
}
ob.SetBytes(OffsetUTXO_Output, in.Output)
ob.FinishAsRoot()
return writeEnvelopePrefix(TypeKindReserved, ShapeKindUTXO, b.Finish())
}
+517
View File
@@ -0,0 +1,517 @@
// 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"
)
func TestDiscriminator_Prefix(t *testing.T) {
tk, sk, zapBytes, err := readEnvelopePrefix([]byte{0x01, 0x02, 0xAA, 0xBB})
if err != nil {
t.Fatalf("readEnvelopePrefix: %v", err)
}
if tk != TypeKindSecp256k1 {
t.Errorf("TypeKind: got %x, want %x", tk, TypeKindSecp256k1)
}
if sk != ShapeKindTransferInput {
t.Errorf("ShapeKind: got %x, want %x", sk, ShapeKindTransferInput)
}
if !bytes.Equal(zapBytes, []byte{0xAA, 0xBB}) {
t.Errorf("zapBytes: got %x, want %x", zapBytes, []byte{0xAA, 0xBB})
}
// Too-short buffer rejected.
if _, _, _, err := readEnvelopePrefix([]byte{0x01}); err != ErrShortEnvelope {
t.Errorf("short buffer: got err=%v, want ErrShortEnvelope", err)
}
}
func TestOutputOwners_RoundTrip(t *testing.T) {
addrs := []ids.ShortID{
{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
{2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2},
{3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3},
}
in := OutputOwnersInput{
Locktime: 1234567890,
Threshold: 2,
Addresses: addrs,
}
envelope := NewOutputOwners(in)
got, err := WrapOutputOwners(envelope)
if err != nil {
t.Fatalf("WrapOutputOwners: %v", err)
}
if got.Locktime() != in.Locktime {
t.Errorf("Locktime: got %d, want %d", got.Locktime(), in.Locktime)
}
if got.Threshold() != in.Threshold {
t.Errorf("Threshold: got %d, want %d", got.Threshold(), in.Threshold)
}
addrList := got.AddressList()
if addrList.Len() != len(addrs) {
t.Fatalf("AddressList.Len: got %d, want %d", addrList.Len(), len(addrs))
}
for i, want := range addrs {
if addrList.At(i) != want {
t.Errorf("AddressList[%d]: got %x, want %x", i, addrList.At(i), want)
}
}
if err := got.SyntacticVerify(); err != nil {
t.Errorf("SyntacticVerify: %v", err)
}
}
func TestOutputOwners_SyntacticVerify(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}
cases := []struct {
name string
in OutputOwnersInput
want error
}{
{"empty addrs", OutputOwnersInput{Threshold: 1}, ErrOwnerAddrsEmpty},
{"zero threshold", OutputOwnersInput{Threshold: 0, Addresses: []ids.ShortID{addr}}, ErrOwnerThresholdZero},
{"threshold exceeds", OutputOwnersInput{Threshold: 2, Addresses: []ids.ShortID{addr}}, ErrOwnerThresholdExceedsAddrs},
{"zero addr", OutputOwnersInput{Threshold: 1, Addresses: []ids.ShortID{{}}}, ErrOwnerAddrZero},
{"valid", OutputOwnersInput{Threshold: 1, Addresses: []ids.ShortID{addr}}, nil},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
envelope := NewOutputOwners(tc.in)
o, err := WrapOutputOwners(envelope)
if err != nil {
t.Fatalf("WrapOutputOwners: %v", err)
}
got := o.SyntacticVerify()
if got != tc.want {
t.Errorf("SyntacticVerify: got %v, want %v", got, tc.want)
}
})
}
}
func TestPChainOwner_RoundTrip(t *testing.T) {
addrs := []ids.ShortID{
{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
{2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2},
}
in := PChainOwnerInput{Threshold: 1, Addresses: addrs}
envelope := NewPChainOwner(in)
got, err := WrapPChainOwner(envelope)
if err != nil {
t.Fatalf("WrapPChainOwner: %v", err)
}
if got.Threshold() != in.Threshold {
t.Errorf("Threshold: got %d, want %d", got.Threshold(), in.Threshold)
}
if got.AddressList().Len() != len(addrs) {
t.Errorf("AddressList.Len: got %d, want %d", got.AddressList().Len(), len(addrs))
}
if err := got.SyntacticVerify(); err != nil {
t.Errorf("SyntacticVerify: %v", err)
}
}
func TestUTXO_RoundTrip(t *testing.T) {
// Build an inner TransferOutput first.
addr := ids.ShortID{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}
innerOutput := NewTransferOutput(TransferOutputInput{
TypeKind: TypeKindSecp256k1,
Amount: 1_000_000,
Locktime: 0,
Threshold: 1,
Addresses: []ids.ShortID{addr},
})
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}
in := UTXOInput{
TxID: txID,
OutputIndex: 7,
AssetID: assetID,
Output: innerOutput,
}
envelope := NewUTXO(in)
got, err := WrapUTXO(envelope)
if err != nil {
t.Fatalf("WrapUTXO: %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)
}
outBytes := got.OutputBytes()
if !bytes.Equal(outBytes, innerOutput) {
t.Errorf("OutputBytes mismatch")
}
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 output.
innerGot, err := WrapTransferOutput(outBytes)
if err != nil {
t.Fatalf("WrapTransferOutput inner: %v", err)
}
if innerGot.Amount() != 1_000_000 {
t.Errorf("inner Amount: got %d, want 1_000_000", innerGot.Amount())
}
}
func TestTransferOutput_AllFxs(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}
for _, tk := range []TypeKind{
TypeKindSecp256k1, TypeKindMLDSA, TypeKindSLHDSA,
TypeKindEd25519, TypeKindSecp256r1, TypeKindSchnorr,
} {
t.Run(tk.String(), func(t *testing.T) {
in := TransferOutputInput{
TypeKind: tk,
Amount: 42,
Locktime: 100,
Threshold: 1,
Addresses: []ids.ShortID{addr},
}
envelope := NewTransferOutput(in)
got, err := WrapTransferOutput(envelope)
if err != nil {
t.Fatalf("WrapTransferOutput: %v", err)
}
if got.TypeKind() != tk {
t.Errorf("TypeKind: got %x, want %x", got.TypeKind(), tk)
}
if got.Amount() != 42 {
t.Errorf("Amount: got %d, want 42", got.Amount())
}
if got.Locktime() != 100 {
t.Errorf("Locktime: got %d, want 100", got.Locktime())
}
if got.Threshold() != 1 {
t.Errorf("Threshold: got %d, want 1", got.Threshold())
}
})
}
}
func TestTransferInput_RoundTrip(t *testing.T) {
in := TransferInputInput{
TypeKind: TypeKindSecp256k1,
Amount: 500,
SigIndices: []uint32{0, 2, 5, 7},
}
envelope := NewTransferInput(in)
got, err := WrapTransferInput(envelope)
if err != nil {
t.Fatalf("WrapTransferInput: %v", err)
}
if got.Amount() != 500 {
t.Errorf("Amount: got %d, want 500", got.Amount())
}
if got.SigIndicesLen() != 4 {
t.Errorf("SigIndicesLen: got %d, want 4", got.SigIndicesLen())
}
sigs := got.SigIndices()
if len(sigs) != 4 {
t.Fatalf("SigIndices len: got %d, want 4", len(sigs))
}
want := []uint32{0, 2, 5, 7}
for i, w := range want {
if sigs[i] != w {
t.Errorf("SigIndices[%d]: got %d, want %d", i, sigs[i], w)
}
}
}
func TestMintOutput_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}
in := MintOutputInput{
TypeKind: TypeKindMLDSA,
Locktime: 9999,
Threshold: 1,
Addresses: []ids.ShortID{addr},
}
envelope := NewMintOutput(in)
got, err := WrapMintOutput(envelope)
if err != nil {
t.Fatalf("WrapMintOutput: %v", err)
}
if got.TypeKind() != TypeKindMLDSA {
t.Errorf("TypeKind: got %x, want %x", got.TypeKind(), TypeKindMLDSA)
}
if got.Locktime() != 9999 {
t.Errorf("Locktime: got %d, want 9999", got.Locktime())
}
if got.Threshold() != 1 {
t.Errorf("Threshold: got %d, want 1", got.Threshold())
}
if err := got.SyntacticVerify(); err != nil {
t.Errorf("SyntacticVerify: %v", err)
}
}
func TestCredential_RoundTrip_Classical(t *testing.T) {
// 2 secp256k1 sigs of 65 bytes each.
sigs := make([]byte, 0, 130)
for i := 0; i < 130; i++ {
sigs = append(sigs, byte(i))
}
in := CredentialInput{
TypeKind: TypeKindSecp256k1,
SecurityLevel: 0,
Signatures: sigs,
}
envelope := NewCredential(in)
got, err := WrapCredential(envelope)
if err != nil {
t.Fatalf("WrapCredential: %v", err)
}
if got.TypeKind() != TypeKindSecp256k1 {
t.Errorf("TypeKind: got %x, want %x", got.TypeKind(), TypeKindSecp256k1)
}
if got.SecurityLevel() != 0 {
t.Errorf("SecurityLevel: got %d, want 0", got.SecurityLevel())
}
if !bytes.Equal(got.SignatureBytes(), sigs) {
t.Errorf("SignatureBytes mismatch")
}
if got.SignatureCount(65) != 2 {
t.Errorf("SignatureCount(65): got %d, want 2", got.SignatureCount(65))
}
sig0 := got.SignatureAt(0, 65)
if !bytes.Equal(sig0, sigs[:65]) {
t.Errorf("SignatureAt(0,65) mismatch")
}
sig1 := got.SignatureAt(1, 65)
if !bytes.Equal(sig1, sigs[65:]) {
t.Errorf("SignatureAt(1,65) mismatch")
}
}
func TestCredential_RoundTrip_PQ(t *testing.T) {
// Simulate 1 ML-DSA-65 signature (3309 bytes).
const mlDSA65SigLen = 3309
sigs := make([]byte, mlDSA65SigLen)
for i := range sigs {
sigs[i] = byte(i)
}
in := CredentialInput{
TypeKind: TypeKindMLDSA,
SecurityLevel: 1, // ML-DSA-65
Signatures: sigs,
}
envelope := NewCredential(in)
got, err := WrapCredential(envelope)
if err != nil {
t.Fatalf("WrapCredential: %v", err)
}
if got.TypeKind() != TypeKindMLDSA {
t.Errorf("TypeKind: got %x, want %x", got.TypeKind(), TypeKindMLDSA)
}
if got.SecurityLevel() != 1 {
t.Errorf("SecurityLevel: got %d, want 1", got.SecurityLevel())
}
if got.SignatureCount(mlDSA65SigLen) != 1 {
t.Errorf("SignatureCount: got %d, want 1", got.SignatureCount(mlDSA65SigLen))
}
}
func TestMintOperation_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}
mintOut := NewMintOutput(MintOutputInput{
TypeKind: TypeKindSecp256k1,
Locktime: 0,
Threshold: 1,
Addresses: []ids.ShortID{addr},
})
transferOut := NewTransferOutput(TransferOutputInput{
TypeKind: TypeKindSecp256k1,
Amount: 100,
Locktime: 0,
Threshold: 1,
Addresses: []ids.ShortID{addr},
})
in := MintOperationInput{
TypeKind: TypeKindSecp256k1,
SigIndices: []uint32{0},
MintOutput: mintOut,
TransferOutput: transferOut,
}
envelope := NewMintOperation(in)
got, err := WrapMintOperation(envelope)
if err != nil {
t.Fatalf("WrapMintOperation: %v", err)
}
if got.TypeKind() != TypeKindSecp256k1 {
t.Errorf("TypeKind: got %x, want %x", got.TypeKind(), TypeKindSecp256k1)
}
if len(got.SigIndices()) != 1 {
t.Errorf("SigIndices.len: got %d, want 1", len(got.SigIndices()))
}
mo, err := WrapMintOutput(got.MintOutputBytes())
if err != nil {
t.Fatalf("WrapMintOutput inner: %v", err)
}
if mo.Threshold() != 1 {
t.Errorf("inner MintOutput Threshold: got %d, want 1", mo.Threshold())
}
to, err := WrapTransferOutput(got.TransferOutputBytes())
if err != nil {
t.Fatalf("WrapTransferOutput inner: %v", err)
}
if to.Amount() != 100 {
t.Errorf("inner TransferOutput Amount: got %d, want 100", to.Amount())
}
}
func TestAttestationOutput_RoundTrip(t *testing.T) {
pk1 := make([]byte, BLS12381PubKeyLen)
pk2 := make([]byte, BLS12381PubKeyLen)
for i := range pk1 {
pk1[i] = byte(i)
}
for i := range pk2 {
pk2[i] = byte(0xFF - i)
}
in := AttestationOutputInput{
AttestedHash: [BLS12381AttestedHashLen]byte{1, 2, 3},
Threshold: 2,
PubKeys: [][]byte{pk1, pk2},
}
envelope := NewAttestationOutput(in)
got, err := WrapAttestationOutput(envelope)
if err != nil {
t.Fatalf("WrapAttestationOutput: %v", err)
}
hash := got.AttestedHash()
if hash[0] != 1 || hash[1] != 2 || hash[2] != 3 {
t.Errorf("AttestedHash[:3]: got %v, want [1 2 3]", hash[:3])
}
if got.Threshold() != 2 {
t.Errorf("Threshold: got %d, want 2", got.Threshold())
}
pks := got.PubKeys()
if len(pks) != 2 {
t.Fatalf("PubKeys len: got %d, want 2", len(pks))
}
if !bytes.Equal(pks[0], pk1) {
t.Errorf("PubKeys[0] mismatch")
}
if !bytes.Equal(pks[1], pk2) {
t.Errorf("PubKeys[1] mismatch")
}
}
func TestAttestationInput_RoundTrip(t *testing.T) {
bitmap := []byte{0x05, 0x80} // bits 0, 2, 15 set
in := AttestationInputInput{SignerBitmap: bitmap}
envelope := NewAttestationInput(in)
got, err := WrapAttestationInput(envelope)
if err != nil {
t.Fatalf("WrapAttestationInput: %v", err)
}
if !bytes.Equal(got.SignerBitmap(), bitmap) {
t.Errorf("SignerBitmap: got %x, want %x", got.SignerBitmap(), bitmap)
}
}
func TestSignedTx_RoundTrip(t *testing.T) {
// Build two credentials.
cred1 := NewCredential(CredentialInput{
TypeKind: TypeKindSecp256k1,
SecurityLevel: 0,
Signatures: bytes.Repeat([]byte{0xAA}, 65),
})
cred2 := NewCredential(CredentialInput{
TypeKind: TypeKindSecp256k1,
SecurityLevel: 0,
Signatures: bytes.Repeat([]byte{0xBB}, 65),
})
unsigned := []byte("imagine this is a TxKind-prefixed zap_native unsigned tx blob ...")
in := SignedTxInput{
UnsignedBytes: unsigned,
Credentials: [][]byte{cred1, cred2},
}
envelope := NewSignedTx(in)
got, err := WrapSignedTx(envelope)
if err != nil {
t.Fatalf("WrapSignedTx: %v", err)
}
if !bytes.Equal(got.UnsignedBytes(), unsigned) {
t.Errorf("UnsignedBytes mismatch")
}
if got.CredentialCount() != 2 {
t.Errorf("CredentialCount: got %d, want 2", got.CredentialCount())
}
all, err := got.AllCredentials()
if err != nil {
t.Fatalf("AllCredentials: %v", err)
}
if len(all) != 2 {
t.Fatalf("AllCredentials len: got %d, want 2", len(all))
}
if all[0].TypeKind() != TypeKindSecp256k1 {
t.Errorf("cred[0].TypeKind: got %x, want %x", all[0].TypeKind(), TypeKindSecp256k1)
}
sig0 := all[0].SignatureAt(0, 65)
if !bytes.Equal(sig0, bytes.Repeat([]byte{0xAA}, 65)) {
t.Errorf("cred[0] sig mismatch")
}
sig1 := all[1].SignatureAt(0, 65)
if !bytes.Equal(sig1, bytes.Repeat([]byte{0xBB}, 65)) {
t.Errorf("cred[1] sig mismatch")
}
// CredentialAt(i) for individual access.
c0, err := got.CredentialAt(0)
if err != nil {
t.Fatalf("CredentialAt(0): %v", err)
}
if c0.TypeKind() != TypeKindSecp256k1 {
t.Errorf("CredentialAt(0).TypeKind: got %x, want %x", c0.TypeKind(), TypeKindSecp256k1)
}
}
// TypeKind.String — small helper for diagnostic test output.
func (t TypeKind) String() string {
switch t {
case TypeKindReserved:
return "TypeKindReserved"
case TypeKindSecp256k1:
return "TypeKindSecp256k1"
case TypeKindMLDSA:
return "TypeKindMLDSA"
case TypeKindSLHDSA:
return "TypeKindSLHDSA"
case TypeKindEd25519:
return "TypeKindEd25519"
case TypeKindSecp256r1:
return "TypeKindSecp256r1"
case TypeKindSchnorr:
return "TypeKindSchnorr"
case TypeKindBLS12381:
return "TypeKindBLS12381"
default:
return "TypeKindUnknown"
}
}