mirror of
https://github.com/luxfi/utxo.git
synced 2026-07-27 03:39:23 +00:00
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:
@@ -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.
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
@@ -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())
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user