Native-ZAP struct-is-wire for the four straggler VMs (aivm/keyvm/quantumvm/dexvm)

Completes the "one and only one way" wire migration: every VM in this repo now
serializes blocks and txs through luxfi/zap struct-is-wire at fixed field
offsets — no encoding/json, no hand-rolled big-endian, no cursor codec.

- aivm: Block + AIVertex + nested CIntent native-encoded. Fixes a latent
  data-loss bug — AIVertex had unexported fields, so encoding/json silently
  marshalled it as {}; the native encoder round-trips every field.
- keyvm: Transaction (signing preimage = SigningBytes(), excludes Auth/Sig;
  full wire = signing || sig object, so the signed bytes stay a genuine prefix
  and authenticate() re-derives a byte-identical preimage) + Block.
- quantumvm: Block + BaseTransaction. Block wire is the quantum-signature
  preimage (deterministic fixed offsets, ordered tx list).
- dexvm: block envelope (txs were already ZAP). Preserves UnixNano timestamps,
  deterministic tx order, and content-addressed carried fills.

Hardening from the adversarial crypto review (must-holds all verified: no
keyvm signature malleability/replay, no aivm CIntent id-forgery/escrow tamper,
no dexvm ordering/fill tamper, all parsers fail closed):

- keyvm (M1): make the wire strictly canonical. zap follows the root offset
  and ignores unreferenced padding inside a message's declared size, so a twin
  buffer could decode to identical fields with a different hash (id-malleability).
  ParseTransaction now binds the id to the re-serialized (canonical) form and
  rejects any non-canonical input at the door — exactly one byte-string
  authenticates per logical tx. Regression test: the padded twin is rejected.
- quantumvm (M2): derive block + tx ids as sha256(Bytes()), matching the other
  three VMs. The prior ids.ToID(parent||height) over 40 bytes (and ids.ToID over
  the >=32-byte tx wire) silently yielded ids.Empty for every block/tx,
  collapsing the block store and tx pool to a single slot. The block id is no
  longer stored in the wire, so the invariant id == sha256(Bytes()) holds
  unconditionally. Regression tests: ids are non-Empty and collision-distinct.

Not in scope (tracked separately, fail-closed today): aivm RewardPerOperator is
escrowed but not bound by ComputeIntentID — a pre-existing design gap that needs
a coordinated A-side + C-side intent-id preimage bump before any non-fail-closed
CCommitVerifier ships.

Builds + tests green under the canonical CGO=0 -mod=readonly recipe; go.mod/go.sum
are the go mod tidy canonical set (indirect deps resolved forward, all within
major). Cryptographer verdict: SAFE TO PUBLISH.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
This commit is contained in:
zeekay
2026-07-16 13:44:24 -07:00
co-authored by Hanzo Dev
parent d2478e11ff
commit b463e0f236
18 changed files with 1675 additions and 624 deletions
+9 -13
View File
@@ -7,7 +7,6 @@ import (
"context"
"crypto/sha256"
"encoding/binary"
"encoding/json"
"errors"
"github.com/luxfi/consensus/core/choices"
@@ -36,12 +35,12 @@ type AIVertex struct {
vm *VM
}
func (v *AIVertex) ID() ids.ID { return v.id }
func (v *AIVertex) Bytes() []byte { return v.bytes }
func (v *AIVertex) Height() uint64 { return v.height }
func (v *AIVertex) Epoch() uint32 { return v.epoch }
func (v *AIVertex) Parents() []ids.ID { return v.parents }
func (v *AIVertex) Txs() []ids.ID { return v.txIDs }
func (v *AIVertex) ID() ids.ID { return v.id }
func (v *AIVertex) Bytes() []byte { return v.bytes }
func (v *AIVertex) Height() uint64 { return v.height }
func (v *AIVertex) Epoch() uint32 { return v.epoch }
func (v *AIVertex) Parents() []ids.ID { return v.parents }
func (v *AIVertex) Txs() []ids.ID { return v.txIDs }
func (v *AIVertex) Status() choices.Status { return v.status }
func (v *AIVertex) Verify(ctx context.Context) error {
@@ -59,10 +58,7 @@ func (v *AIVertex) Accept(ctx context.Context) error {
v.vm.mu.Lock()
defer v.vm.mu.Unlock()
b, err := json.Marshal(v)
if err != nil {
return err
}
b := marshalVertex(v)
if err := v.vm.db.Put(v.id[:], b); err != nil {
return err
}
@@ -171,14 +167,14 @@ func (vm *VM) BuildVertex(ctx context.Context) (vertex.Vertex, error) {
vm: vm,
}
v.id = v.computeID()
v.bytes, _ = json.Marshal(v)
v.bytes = marshalVertex(v)
return v, nil
}
// ParseVertex deserializes a vertex from bytes.
func (vm *VM) ParseVertex(ctx context.Context, b []byte) (vertex.Vertex, error) {
v := &AIVertex{vm: vm}
if err := json.Unmarshal(b, v); err != nil {
if err := parseVertex(b, v); err != nil {
return nil, err
}
v.id = v.computeID()
+9 -6
View File
@@ -630,10 +630,11 @@ func (vm *VM) BuildBlock(ctx context.Context) (chain.Block, error) {
// ParseBlock implements chain.ChainVM interface
func (vm *VM) ParseBlock(ctx context.Context, bytes []byte) (chain.Block, error) {
blk := &Block{vm: vm}
if err := json.Unmarshal(bytes, blk); err != nil {
if err := parseBlock(bytes, blk); err != nil {
return nil, err
}
blk.ID_ = blk.computeID()
blk.bytes = bytes
return blk, nil
}
@@ -661,9 +662,11 @@ func (vm *VM) GetBlock(ctx context.Context, id ids.ID) (chain.Block, error) {
}
blk := &Block{vm: vm}
if err := json.Unmarshal(bytes, blk); err != nil {
if err := parseBlock(bytes, blk); err != nil {
return nil, err
}
blk.ID_ = id
blk.bytes = bytes
return blk, nil
}
@@ -728,9 +731,9 @@ func (vm *VM) HealthCheck(ctx context.Context) (chain.HealthResult, error) {
// Block Methods (implements chain.Block interface)
// =============================================================================
// computeID computes the block ID from its contents
// computeID computes the block ID from its canonical ZAP wire (ID_ excluded).
func (blk *Block) computeID() ids.ID {
bytes, _ := json.Marshal(blk)
bytes, _ := blk.Marshal()
hash := sha256.Sum256(bytes)
return ids.ID(hash)
}
@@ -804,7 +807,7 @@ func (blk *Block) Accept(ctx context.Context) error {
// Store the block bytes (commitEngine already flushed engine slots; the block
// key is disjoint from the av/state/ keyspace).
bytes, err := json.Marshal(blk)
bytes, err := blk.Marshal()
if err != nil {
return err
}
@@ -837,7 +840,7 @@ func (blk *Block) Reject(ctx context.Context) error {
// Bytes returns the serialized block
func (blk *Block) Bytes() []byte {
if blk.bytes == nil {
blk.bytes, _ = json.Marshal(blk)
blk.bytes, _ = blk.Marshal()
}
return blk.bytes
}
+484
View File
@@ -0,0 +1,484 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package aivm
import (
"encoding/json"
"fmt"
"time"
"github.com/holiman/uint256"
"github.com/luxfi/ids"
"github.com/luxfi/zap"
aicore "github.com/luxfi/ai/pkg/aivm"
)
// Native-ZAP struct-is-wire for A-Chain (aivm). No encoding/json on the
// block/vertex envelope, no reflection codec, no BigEndian hand-rolling — the
// Block and the AIVertex each own their Marshal/parse over a zap object at
// FIXED field offsets. Re-genesis is authorized, so the on-disk/wire format is
// exactly these offsets (canonical: parse rejects trailing bytes and checks the
// leading kind byte). blockID = sha256(Block.Marshal); vertexID = computeID over
// the structural fields — determinism follows from the deterministic wire.
//
// A 1-byte kind discriminator at object offset 0 separates the two persisted
// wire types (block vs vertex) that share the VM's db keyspace and gives version
// headroom — the same idiom as the node's proposervm blockwire.
//
// Nested/dynamic payloads use the "u32-length list + concatenated blob" idiom.
// Two sub-encodings:
//
// - CIntent (ImportedIntents) is a FIXED-schema struct and is the
// security-critical C->A value seam — Block.Verify re-runs it under
// consensus and its id-binding check re-derives intent_id from these exact
// bytes. It is encoded NATIVELY (fixed offsets, 32-byte big-endian uint256),
// which is strictly higher-integrity than JSON: canonical bytes, no parse
// ambiguity, and the downstream id-binding + receipt-root checks act as a
// built-in verification of the round-trip.
//
// - Tasks / Results / ProviderRegs carry irreducibly-dynamic data
// (aivm.Task.Input/Output are json.RawMessage; ProviderReg embeds opaque
// external TEE attestation quotes). Following the identityvm Claims
// precedent, each element rides the wire as one opaque, self-describing
// canonical-JSON blob in a ZAP bytes slot — encoding arbitrary dynamic JSON
// "natively" would just re-introduce the reflection codec this migration
// kills. Go's encoding/json emits struct fields in declaration order with no
// maps here, so the bytes are deterministic and parse∘marshal is byte-stable
// (block id stays stable across a wire round-trip).
//
// Boundaries intentionally kept as JSON (NOT block/vertex wire): Config and
// Genesis (config boundary, vm.go Initialize) and the RPC reply types in
// service.go. None of those are block/vertex wire.
// wireKind is the 1-byte discriminator at object offset 0 of every aivm wire
// buffer. parse reads it and rejects a buffer of the wrong kind.
type wireKind uint8
const (
kindReserved wireKind = iota
kindBlock
kindVertex
)
// ---- Block ----
//
// kind u8 @ 0 (= kindBlock) (ID_ is derived: sha256(Marshal); excluded)
// ParentID 32B @ 1
// Height u64 @ 33
// Timestamp i64 @ 41 (UnixNano)
// MerkleRoot 32B @ 49
// ReceiptRoot 32B @ 81
// IntentLens list @ 113 (u32 per CIntent wire length)
// IntentBlob bytes @ 121 (concatenated CIntent wire objects)
// TaskLens list @ 129 (u32 per Task JSON length)
// TaskBlob bytes @ 137
// ResultLens list @ 145 (u32 per TaskResult JSON length)
// ResultBlob bytes @ 153
// PRegLens list @ 161 (u32 per ProviderReg JSON length)
// PRegBlob bytes @ 169
const (
blkKind = 0
blkParent = 1
blkHeight = 33
blkTime = 41
blkMerkle = 49
blkReceipt = 81
blkIntentLens = 113
blkIntentBlob = 121
blkTaskLens = 129
blkTaskBlob = 137
blkResLens = 145
blkResBlob = 153
blkPRegLens = 161
blkPRegBlob = 169
blkSize = 177
)
// Marshal encodes the block (excluding the derived ID_/cache fields) to
// canonical wire. blockID = sha256(this).
func (blk *Block) Marshal() ([]byte, error) {
intentLens, intentBlob := packObjs(blk.ImportedIntents, marshalCIntent)
taskLens, taskBlob := packJSON(blk.Tasks)
resLens, resBlob := packJSON(blk.Results)
pregLens, pregBlob := packJSON(blk.ProviderRegs)
b := zap.NewBuilder(zap.HeaderSize + blkSize + len(intentBlob) + len(taskBlob) +
len(resBlob) + len(pregBlob) +
4*(len(intentLens)+len(taskLens)+len(resLens)+len(pregLens)) + 256)
intentLensOff := writeU32List(b, intentLens)
taskLensOff := writeU32List(b, taskLens)
resLensOff := writeU32List(b, resLens)
pregLensOff := writeU32List(b, pregLens)
ob := b.StartObject(blkSize)
ob.SetUint8(blkKind, uint8(kindBlock))
ob.SetBytesFixed(blkParent, blk.ParentID_[:])
ob.SetUint64(blkHeight, blk.Height_)
ob.SetInt64(blkTime, blk.Timestamp_.UnixNano())
ob.SetBytesFixed(blkMerkle, blk.MerkleRoot[:])
ob.SetBytesFixed(blkReceipt, blk.ReceiptRoot[:])
ob.SetList(blkIntentLens, intentLensOff, len(intentLens))
ob.SetBytes(blkIntentBlob, intentBlob)
ob.SetList(blkTaskLens, taskLensOff, len(taskLens))
ob.SetBytes(blkTaskBlob, taskBlob)
ob.SetList(blkResLens, resLensOff, len(resLens))
ob.SetBytes(blkResBlob, resBlob)
ob.SetList(blkPRegLens, pregLensOff, len(pregLens))
ob.SetBytes(blkPRegBlob, pregBlob)
ob.FinishAsRoot()
return b.Finish(), nil
}
// parseBlock fills the wire fields of blk from canonical block bytes. The caller
// sets the non-wire cache fields (vm, bytes, ID_).
func parseBlock(data []byte, blk *Block) error {
msg, err := zap.Parse(data)
if err != nil {
return err
}
if msg.Size() != len(data) {
return fmt.Errorf("aivm block: trailing bytes")
}
o := msg.Root()
if wireKind(o.Uint8(blkKind)) != kindBlock {
return fmt.Errorf("aivm block: wrong kind byte")
}
copy(blk.ParentID_[:], o.BytesFixedSlice(blkParent, 32))
blk.Height_ = o.Uint64(blkHeight)
blk.Timestamp_ = time.Unix(0, o.Int64(blkTime)).UTC()
copy(blk.MerkleRoot[:], o.BytesFixedSlice(blkMerkle, 32))
copy(blk.ReceiptRoot[:], o.BytesFixedSlice(blkReceipt, 32))
if blk.ImportedIntents, err = unpackObjs(readU32List(o, blkIntentLens), o.Bytes(blkIntentBlob), parseCIntent); err != nil {
return err
}
if blk.Tasks, err = unpackJSON[aicore.Task](readU32List(o, blkTaskLens), o.Bytes(blkTaskBlob)); err != nil {
return err
}
if blk.Results, err = unpackJSON[aicore.TaskResult](readU32List(o, blkResLens), o.Bytes(blkResBlob)); err != nil {
return err
}
if blk.ProviderRegs, err = unpackJSON[ProviderReg](readU32List(o, blkPRegLens), o.Bytes(blkPRegBlob)); err != nil {
return err
}
return nil
}
// ---- CIntent (nested in Block.ImportedIntents; native, security-critical) ----
//
// IntentID 32B @ 0
// CChainID 32B @ 32
// AChainID 32B @ 64
// CTxHash 32B @ 96
// ModelSpecHash 32B @ 128
// PromptHash 32B @ 160
// Caller 20B @ 192
// CallIndex u32 @ 212
// N u16 @ 216
// Threshold u16 @ 218
// Fee 32B @ 220 (uint256 big-endian; nil == zero, u256be-equivalent)
// RewardPerOp 32B @ 252 (uint256 big-endian; nil == zero)
const (
ciIntentID = 0
ciCChain = 32
ciAChain = 64
ciCTxHash = 96
ciModel = 128
ciPrompt = 160
ciCaller = 192
ciCallIdx = 212
ciN = 216
ciThresh = 218
ciFee = 220
ciReward = 252
ciSize = 284
)
func marshalCIntent(in CIntent) []byte {
b := zap.NewBuilder(zap.HeaderSize + ciSize)
ob := b.StartObject(ciSize)
ob.SetBytesFixed(ciIntentID, in.IntentID[:])
ob.SetBytesFixed(ciCChain, in.CChainID[:])
ob.SetBytesFixed(ciAChain, in.AChainID[:])
ob.SetBytesFixed(ciCTxHash, in.CTxHash[:])
ob.SetBytesFixed(ciModel, in.ModelSpecHash[:])
ob.SetBytesFixed(ciPrompt, in.PromptHash[:])
ob.SetBytesFixed(ciCaller, in.Caller[:])
ob.SetUint32(ciCallIdx, in.CallIndex)
ob.SetUint16(ciN, in.N)
ob.SetUint16(ciThresh, in.Threshold)
setU256(ob, ciFee, in.Fee)
setU256(ob, ciReward, in.RewardPerOperator)
ob.FinishAsRoot()
return b.Finish()
}
func parseCIntent(data []byte) (CIntent, error) {
msg, err := zap.Parse(data)
if err != nil {
return CIntent{}, err
}
if msg.Size() != len(data) {
return CIntent{}, fmt.Errorf("aivm c-intent: trailing bytes")
}
o := msg.Root()
var in CIntent
copy(in.IntentID[:], o.BytesFixedSlice(ciIntentID, 32))
copy(in.CChainID[:], o.BytesFixedSlice(ciCChain, 32))
copy(in.AChainID[:], o.BytesFixedSlice(ciAChain, 32))
copy(in.CTxHash[:], o.BytesFixedSlice(ciCTxHash, 32))
copy(in.ModelSpecHash[:], o.BytesFixedSlice(ciModel, 32))
copy(in.PromptHash[:], o.BytesFixedSlice(ciPrompt, 32))
copy(in.Caller[:], o.BytesFixedSlice(ciCaller, 20))
in.CallIndex = o.Uint32(ciCallIdx)
in.N = o.Uint16(ciN)
in.Threshold = o.Uint16(ciThresh)
in.Fee = new(uint256.Int).SetBytes(o.BytesFixedSlice(ciFee, 32))
in.RewardPerOperator = new(uint256.Int).SetBytes(o.BytesFixedSlice(ciReward, 32))
return in, nil
}
// ---- Vertex ----
//
// kind u8 @ 0 (= kindVertex) (id is derived: computeID; excluded.
// status is a local decision; excluded.)
// Height u64 @ 1
// Epoch u32 @ 9
// Parents bytes @ 13 (concatenated 32-byte ids)
// TxIDs bytes @ 21 (concatenated 32-byte ids)
// JobLens list @ 29 (u32 per jobID string length)
// JobBlob bytes @ 37 (concatenated jobID strings)
// TaskLens list @ 45 (u32 per Task JSON length)
// TaskBlob bytes @ 53
// ResultLens list @ 61 (u32 per TaskResult JSON length)
// ResultBlob bytes @ 69
const (
vxKind = 0
vxHeight = 1
vxEpoch = 9
vxParents = 13
vxTxIDs = 21
vxJobLens = 29
vxJobBlob = 37
vxTaskLens = 45
vxTaskBlob = 53
vxResLens = 61
vxResBlob = 69
vxSize = 77
)
// marshalVertex encodes the vertex's structural + payload fields (excluding the
// derived id, the local status, and the runtime vm/bytes fields).
func marshalVertex(v *AIVertex) []byte {
parents := concatIDs(v.parents)
txIDs := concatIDs(v.txIDs)
jobLens, jobBlob := packStrings(v.jobIDs)
taskLens, taskBlob := packJSON(v.tasks)
resLens, resBlob := packJSON(v.results)
b := zap.NewBuilder(zap.HeaderSize + vxSize + len(parents) + len(txIDs) +
len(jobBlob) + len(taskBlob) + len(resBlob) +
4*(len(jobLens)+len(taskLens)+len(resLens)) + 256)
jobLensOff := writeU32List(b, jobLens)
taskLensOff := writeU32List(b, taskLens)
resLensOff := writeU32List(b, resLens)
ob := b.StartObject(vxSize)
ob.SetUint8(vxKind, uint8(kindVertex))
ob.SetUint64(vxHeight, v.height)
ob.SetUint32(vxEpoch, v.epoch)
ob.SetBytes(vxParents, parents)
ob.SetBytes(vxTxIDs, txIDs)
ob.SetList(vxJobLens, jobLensOff, len(jobLens))
ob.SetBytes(vxJobBlob, jobBlob)
ob.SetList(vxTaskLens, taskLensOff, len(taskLens))
ob.SetBytes(vxTaskBlob, taskBlob)
ob.SetList(vxResLens, resLensOff, len(resLens))
ob.SetBytes(vxResBlob, resBlob)
ob.FinishAsRoot()
return b.Finish()
}
// parseVertex fills the wire fields of v. The caller sets the derived id and the
// runtime vm/bytes fields; status stays at its zero value (a local decision).
func parseVertex(data []byte, v *AIVertex) error {
msg, err := zap.Parse(data)
if err != nil {
return err
}
if msg.Size() != len(data) {
return fmt.Errorf("aivm vertex: trailing bytes")
}
o := msg.Root()
if wireKind(o.Uint8(vxKind)) != kindVertex {
return fmt.Errorf("aivm vertex: wrong kind byte")
}
v.height = o.Uint64(vxHeight)
v.epoch = o.Uint32(vxEpoch)
v.parents = splitIDs(o.Bytes(vxParents))
v.txIDs = splitIDs(o.Bytes(vxTxIDs))
v.jobIDs = unpackStrings(readU32List(o, vxJobLens), o.Bytes(vxJobBlob))
if v.tasks, err = unpackJSON[*aicore.Task](readU32List(o, vxTaskLens), o.Bytes(vxTaskBlob)); err != nil {
return err
}
if v.results, err = unpackJSON[*aicore.TaskResult](readU32List(o, vxResLens), o.Bytes(vxResBlob)); err != nil {
return err
}
return nil
}
// ---- shared helpers ----
// setU256 writes x as 32-byte big-endian. A nil pointer writes zero bytes, which
// is byte-identical to u256be(nil) used in ComputeIntentID — so a nil Fee still
// re-derives the same intent_id (the nil-amount guard rejects it earlier anyway).
func setU256(ob zap.ObjectBuilder, off int, x *uint256.Int) {
var b [32]byte
if x != nil {
b = x.Bytes32()
}
ob.SetBytesFixed(off, b[:])
}
func writeU32List(b *zap.Builder, xs []uint32) int {
lb := b.StartList(4)
for _, x := range xs {
lb.AddUint32(x)
}
off, _ := lb.Finish()
return off
}
func readU32List(o zap.Object, ptrOff int) []uint32 {
l := o.ListStride(ptrOff, 4)
n := l.Len()
out := make([]uint32, n)
for i := 0; i < n; i++ {
out[i] = l.Uint32(i)
}
return out
}
// packObjs marshals each item and returns (per-item lengths, concat blob).
func packObjs[T any](items []T, marshal func(T) []byte) (lens []uint32, blob []byte) {
if len(items) == 0 {
return nil, nil
}
lens = make([]uint32, len(items))
for i, it := range items {
m := marshal(it)
lens[i] = uint32(len(m))
blob = append(blob, m...)
}
return lens, blob
}
// unpackObjs re-splits a packed blob by lengths and parses each sub-object.
func unpackObjs[T any](lens []uint32, blob []byte, parse func([]byte) (T, error)) ([]T, error) {
if len(lens) == 0 {
return nil, nil
}
out := make([]T, 0, len(lens))
pos := 0
for i, l := range lens {
if pos+int(l) > len(blob) {
return nil, fmt.Errorf("aivm: packed obj %d out of bounds", i)
}
v, err := parse(blob[pos : pos+int(l)])
if err != nil {
return nil, err
}
out = append(out, v)
pos += int(l)
}
return out, nil
}
// packJSON marshals each item to canonical JSON and returns (lengths, concat
// blob). Used only for irreducibly-dynamic payloads (see file header).
func packJSON[T any](items []T) (lens []uint32, blob []byte) {
if len(items) == 0 {
return nil, nil
}
lens = make([]uint32, len(items))
for i, it := range items {
m, _ := json.Marshal(it)
lens[i] = uint32(len(m))
blob = append(blob, m...)
}
return lens, blob
}
func unpackJSON[T any](lens []uint32, blob []byte) ([]T, error) {
if len(lens) == 0 {
return nil, nil
}
out := make([]T, 0, len(lens))
pos := 0
for i, l := range lens {
if pos+int(l) > len(blob) {
return nil, fmt.Errorf("aivm: json sub-blob %d out of bounds", i)
}
var v T
if err := json.Unmarshal(blob[pos:pos+int(l)], &v); err != nil {
return nil, err
}
out = append(out, v)
pos += int(l)
}
return out, nil
}
func packStrings(ss []string) ([]uint32, []byte) {
if len(ss) == 0 {
return nil, nil
}
lens := make([]uint32, len(ss))
var blob []byte
for i, s := range ss {
lens[i] = uint32(len(s))
blob = append(blob, s...)
}
return lens, blob
}
func unpackStrings(lens []uint32, blob []byte) []string {
if len(lens) == 0 {
return nil
}
out := make([]string, 0, len(lens))
pos := 0
for _, l := range lens {
if pos+int(l) > len(blob) {
break
}
out = append(out, string(blob[pos:pos+int(l)]))
pos += int(l)
}
return out
}
func concatIDs(list []ids.ID) []byte {
if len(list) == 0 {
return nil
}
out := make([]byte, 0, 32*len(list))
for _, id := range list {
out = append(out, id[:]...)
}
return out
}
func splitIDs(blob []byte) []ids.ID {
n := len(blob) / 32
if n == 0 {
return nil
}
out := make([]ids.ID, n)
for i := 0; i < n; i++ {
copy(out[i][:], blob[i*32:(i+1)*32])
}
return out
}
+162
View File
@@ -0,0 +1,162 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package aivm
import (
"encoding/json"
"testing"
"time"
"github.com/holiman/uint256"
"github.com/luxfi/geth/common"
"github.com/luxfi/ids"
"github.com/stretchr/testify/require"
aicore "github.com/luxfi/ai/pkg/aivm"
)
// mkIntent builds a CIntent whose IntentID is the real ComputeIntentID over its
// own fields — so a wire round-trip that preserves the id-binding invariant is a
// genuine proof the security-critical fields survived byte-for-byte.
func mkWireIntent() CIntent {
cChain := common.HexToHash("0x0c0c")
aChain := common.HexToHash("0x0a0a")
cTx := common.HexToHash("0xdeadbeef")
caller := common.HexToAddress("0xF00DBABE00000000000000000000000000000001")
model := common.HexToHash("0x1111")
prompt := common.HexToHash("0x2222")
fee := uint256.NewInt(123456789)
in := CIntent{
CChainID: cChain,
AChainID: aChain,
CTxHash: cTx,
CallIndex: 7,
Caller: caller,
ModelSpecHash: model,
PromptHash: prompt,
N: 5,
Threshold: 3,
Fee: fee,
RewardPerOperator: uint256.NewInt(1_000_000),
}
in.IntentID = ComputeIntentID(cChain, aChain, cTx, in.CallIndex, caller, model, prompt, in.N, in.Threshold, fee)
return in
}
func TestBlockWireRoundTrip(t *testing.T) {
require := require.New(t)
orig := &Block{
ParentID_: ids.GenerateTestID(),
Height_: 42,
Timestamp_: time.Unix(1_700_000_123, 456).UTC(),
MerkleRoot: [32]byte{1, 2, 3, 9, 9},
ReceiptRoot: common.HexToHash("0xabc123"),
ImportedIntents: []CIntent{mkWireIntent()},
Tasks: []aicore.Task{{
ID: "task-1",
Type: aicore.TaskTypeInference,
Model: "qwen3-8b",
Input: json.RawMessage(`{"b":1,"a":2}`),
Status: aicore.TaskStatusPending,
Fee: 99,
}},
}
wire, err := orig.Marshal()
require.NoError(err)
got := &Block{}
require.NoError(parseBlock(wire, got))
// structural fields
require.Equal(orig.ParentID_, got.ParentID_)
require.Equal(orig.Height_, got.Height_)
require.Equal(orig.Timestamp_.UnixNano(), got.Timestamp_.UnixNano())
require.Equal(orig.MerkleRoot, got.MerkleRoot)
require.Equal(orig.ReceiptRoot, got.ReceiptRoot)
// security-critical CIntent seam: every field survived, AND the id-binding
// invariant re-derives — exactly what ImportCommittedIntent re-checks.
require.Len(got.ImportedIntents, 1)
gi := got.ImportedIntents[0]
oi := orig.ImportedIntents[0]
require.Equal(oi.IntentID, gi.IntentID)
require.Equal(oi.Caller, gi.Caller)
require.Equal(oi.CallIndex, gi.CallIndex)
require.Equal(oi.N, gi.N)
require.Equal(oi.Threshold, gi.Threshold)
require.Equal(0, oi.Fee.Cmp(gi.Fee), "Fee value preserved")
require.Equal(0, oi.RewardPerOperator.Cmp(gi.RewardPerOperator), "reward preserved")
require.Equal(gi.IntentID, ComputeIntentID(
gi.CChainID, gi.AChainID, gi.CTxHash, gi.CallIndex, gi.Caller,
gi.ModelSpecHash, gi.PromptHash, gi.N, gi.Threshold, gi.Fee,
), "id-binding invariant holds after wire round-trip")
// dynamic JSON payload round-trips verbatim
require.Len(got.Tasks, 1)
require.Equal(orig.Tasks[0].ID, got.Tasks[0].ID)
require.JSONEq(string(orig.Tasks[0].Input), string(got.Tasks[0].Input))
// block id is stable across the wire round-trip (parse∘marshal is byte-stable)
require.Equal(orig.computeID(), got.computeID(), "block id stable across round-trip")
}
func TestBlockWireEmpty(t *testing.T) {
require := require.New(t)
// A minimal block (BuildBlock's common case: no tasks/results/pregs) must
// round-trip and produce a deterministic, non-empty id.
orig := &Block{ParentID_: ids.Empty, Height_: 1, Timestamp_: time.Unix(1, 0).UTC()}
wire, err := orig.Marshal()
require.NoError(err)
got := &Block{}
require.NoError(parseBlock(wire, got))
require.Equal(orig.Height_, got.Height_)
require.Empty(got.ImportedIntents)
require.Equal(orig.computeID(), got.computeID())
}
func TestVertexWireRoundTrip(t *testing.T) {
require := require.New(t)
orig := &AIVertex{
height: 17,
epoch: 2,
parents: []ids.ID{ids.GenerateTestID(), ids.GenerateTestID()},
txIDs: []ids.ID{ids.GenerateTestID()},
jobIDs: []string{"job-123", "job-456"},
}
orig.id = orig.computeID()
wire := marshalVertex(orig)
got := &AIVertex{}
require.NoError(parseVertex(wire, got))
require.Equal(orig.height, got.height)
require.Equal(orig.epoch, got.epoch)
require.Equal(orig.parents, got.parents)
require.Equal(orig.txIDs, got.txIDs)
require.Equal(orig.jobIDs, got.jobIDs, "jobIDs survive the wire (previously lost: json of all-unexported fields was {})")
// vertex id is stable across round-trip, and conflict detection is preserved
require.Equal(orig.computeID(), got.computeID(), "vertex id stable across round-trip")
require.True(got.Conflicts(orig), "same jobIDs still conflict after round-trip")
}
func TestWireKindGuards(t *testing.T) {
require := require.New(t)
blk := &Block{ParentID_: ids.Empty, Height_: 1, Timestamp_: time.Unix(1, 0).UTC()}
blkWire, err := blk.Marshal()
require.NoError(err)
vtxWire := marshalVertex(&AIVertex{height: 1, jobIDs: []string{"j"}})
// cross-parsing the wrong kind is rejected (defense-in-depth discriminator)
require.Error(parseBlock(vtxWire, &Block{}), "vertex bytes must not parse as a block")
require.Error(parseVertex(blkWire, &AIVertex{}), "block bytes must not parse as a vertex")
// trailing bytes are rejected (canonical)
require.Error(parseBlock(append(append([]byte(nil), blkWire...), 0x00), &Block{}))
require.Error(parseVertex(append(append([]byte(nil), vtxWire...), 0x00), &AIVertex{}))
}
+83 -78
View File
@@ -6,11 +6,11 @@ package dexvm
import (
"context"
"crypto/sha256"
"encoding/binary"
"time"
"github.com/luxfi/vm/chain"
"github.com/luxfi/ids"
"github.com/luxfi/vm/chain"
"github.com/luxfi/zap"
)
// Ensure Block implements chain.Block
@@ -83,55 +83,52 @@ func (b *Block) Timestamp() time.Time {
return b.timestamp
}
// Block wire (native ZAP, object offsets; RED finding #9). The block carries the
// proposer's confirmed d-chain fills so every validator settles from bytes
// instead of relaying per-validator:
//
// Height u64 @ 0
// Timestamp i64 @ 8 (UnixNano — sub-second cadence preserved, no floor)
// ParentID 32B @ 16
// TxLens list @ 48 (u32 per tx; ORDER is the DEX fairness invariant)
// TxBlob bytes @ 56 (concatenated raw tx bytes, order preserved)
// Fills bytes @ 64 (carried-fills section: encodeCarriedFills output)
//
// blockID = sha256(Bytes()); the fills ride in the bytes, so the id commits to
// them (a peer cannot swap the proposer's fills and keep the same id). Changing
// this wire is a network-upgrade-gated, lockstep validator change.
const (
blkHeight = 0
blkTime = 8
blkParent = 16
blkTxLens = 48
blkTxBlob = 56
blkFills = 64
blkSize = 72
)
// Bytes returns the serialized block.
//
// WIRE FORMAT (NETWORK-UPGRADE-GATED, LOCKSTEP — RED finding #9). The block now
// carries the proposer's confirmed d-chain fills so every validator settles from
// bytes instead of relaying per-validator. The layout is:
//
// height[8] | timestamp[8] | parentID[32] |
// txCount[4] | txCount × ( txLen[4] | txBytes ) |
// carried-fills section (carried_fills.go encodeCarriedFills):
// entryCount[4] | entryCount × ( txIndex[4] | fillCount[4] | fillCount×17 ) |
// sigLen[4] | sig[sigLen] // reserved fill-attestation, empty today
//
// The txCount prefix (NEW) makes the txs self-delimiting so the carried-fills
// section can follow unambiguously; the prior format ran txs to end-of-buffer.
// This is a consensus-breaking change activated in lockstep across the validator
// set behind a network upgrade.
func (b *Block) Bytes() []byte {
size := 8 + 8 + 32 + 4 // header + txCount
for _, tx := range b.txs {
size += 4 + len(tx) // length prefix + tx
txLens := make([]uint32, len(b.txs))
var txBlob []byte
for i, tx := range b.txs {
txLens[i] = uint32(len(tx))
txBlob = append(txBlob, tx...)
}
fillsSection := encodeCarriedFills(b.carriedFills, b.fillSig)
size += len(fillsSection)
data := make([]byte, size)
offset := 0
bld := zap.NewBuilder(zap.HeaderSize + blkSize + len(txBlob) + 4*len(txLens) + len(fillsSection) + 128)
txLensOff := writeU32List(bld, txLens)
binary.BigEndian.PutUint64(data[offset:], b.height)
offset += 8
binary.BigEndian.PutUint64(data[offset:], uint64(b.timestamp.UnixNano()))
offset += 8
copy(data[offset:], b.parentID[:])
offset += 32
binary.BigEndian.PutUint32(data[offset:], uint32(len(b.txs)))
offset += 4
for _, tx := range b.txs {
binary.BigEndian.PutUint32(data[offset:], uint32(len(tx)))
offset += 4
copy(data[offset:], tx)
offset += len(tx)
}
copy(data[offset:], fillsSection)
offset += len(fillsSection)
return data
ob := bld.StartObject(blkSize)
ob.SetUint64(blkHeight, b.height)
ob.SetInt64(blkTime, b.timestamp.UnixNano())
ob.SetBytesFixed(blkParent, b.parentID[:])
ob.SetList(blkTxLens, txLensOff, len(txLens))
ob.SetBytes(blkTxBlob, txBlob)
ob.SetBytes(blkFills, fillsSection)
ob.FinishAsRoot()
return bld.Finish()
}
// Verify verifies the block is valid by processing it deterministically, then
@@ -193,53 +190,42 @@ func (b *Block) Status() uint8 {
// section (RED #9). Every length is bounds-checked so a malformed block is
// rejected as errInvalidBlock rather than panicking or over-allocating.
func parseBlock(vm *ChainVM, data []byte) (*Block, error) {
if len(data) < 8+8+32+4 { // header + txCount
msg, err := zap.Parse(data)
if err != nil {
return nil, errInvalidBlock
}
if msg.Size() != len(data) {
return nil, errInvalidBlock
}
o := msg.Root()
b := &Block{
vm: vm,
status: StatusUnknown,
}
b.height = o.Uint64(blkHeight)
b.timestamp = time.Unix(0, o.Int64(blkTime))
copy(b.parentID[:], o.BytesFixedSlice(blkParent, 32))
offset := 0
b.height = binary.BigEndian.Uint64(data[offset:])
offset += 8
ts := binary.BigEndian.Uint64(data[offset:])
b.timestamp = time.Unix(0, int64(ts))
offset += 8
copy(b.parentID[:], data[offset:offset+32])
offset += 32
// txCount-delimited transactions (the prefix makes the carried-fills section
// that follows unambiguous).
txCount := binary.BigEndian.Uint32(data[offset:])
offset += 4
for i := uint32(0); i < txCount; i++ {
if offset+4 > len(data) {
// txs in wire order — the DEX fairness/determinism invariant.
lens := readU32List(o, blkTxLens)
blob := o.Bytes(blkTxBlob)
pos := 0
for _, l := range lens {
if pos+int(l) > len(blob) {
return nil, errInvalidBlock
}
txLen := binary.BigEndian.Uint32(data[offset:])
offset += 4
if offset+int(txLen) > len(data) {
return nil, errInvalidBlock
}
tx := make([]byte, txLen)
copy(tx, data[offset:offset+int(txLen)])
tx := make([]byte, l)
copy(tx, blob[pos:pos+int(l)])
b.txs = append(b.txs, tx)
offset += int(txLen)
pos += int(l)
}
// Carried-fills section: the proposer's confirmed fills every validator settles
// from. Must be exactly consumed to end-of-block (no trailing garbage).
entries, sig, consumed, err := decodeCarriedFills(data[offset:])
if err != nil {
return nil, errInvalidBlock
}
if offset+consumed != len(data) {
// from. Must be exactly consumed within its field (no trailing garbage).
fills := o.Bytes(blkFills)
entries, sig, consumed, err := decodeCarriedFills(fills)
if err != nil || consumed != len(fills) {
return nil, errInvalidBlock
}
b.carriedFills = entries
@@ -251,3 +237,22 @@ func parseBlock(vm *ChainVM, data []byte) (*Block, error) {
return b, nil
}
func writeU32List(b *zap.Builder, xs []uint32) int {
lb := b.StartList(4)
for _, x := range xs {
lb.AddUint32(x)
}
off, _ := lb.Finish()
return off
}
func readU32List(o zap.Object, ptrOff int) []uint32 {
l := o.ListStride(ptrOff, 4)
n := l.Len()
out := make([]uint32, n)
for i := 0; i < n; i++ {
out[i] = l.Uint32(i)
}
return out
}
+99 -129
View File
@@ -6,13 +6,13 @@ package block
import (
"context"
"encoding/binary"
"errors"
"fmt"
"time"
"github.com/luxfi/ids"
"github.com/luxfi/chains/dexvm/txs"
"github.com/luxfi/ids"
"github.com/luxfi/zap"
)
var (
@@ -206,68 +206,56 @@ func (b *Block) computeID() ids.ID {
return id
}
// serialize serializes the block to bytes.
// Block wire (native ZAP, object offsets). timestamp is nanoseconds; the
// transaction list preserves proposer order.
//
// ParentID 32B @ 0
// Height u64 @ 32
// Timestamp i64 @ 40 (nanoseconds)
// TxRoot 32B @ 48
// StateRoot 32B @ 80
// Producer 20B @ 112
// Signature bytes @ 132
// TxLens list @ 140 (u32 per tx; order preserved)
// TxBlob bytes @ 148 (concatenated tx wire bytes)
const (
sbParent = 0
sbHeight = 32
sbTime = 40
sbTxRoot = 48
sbState = 80
sbProd = 112
sbSig = 132
sbTxLens = 140
sbTxBlob = 148
sbSize = 156
)
// serialize serializes the block to its canonical ZAP wire.
func (b *Block) serialize() []byte {
// Calculate size
// Header: parentID (32) + height (8) + timestamp (8) + txRoot (32) + stateRoot (32) + producer (20) + sigLen (4) + sig
// Txs: numTxs (4) + [txLen (4) + txBytes]...
sigLen := len(b.signature)
txsSize := 4
for _, tx := range b.transactions {
txsSize += 4 + len(tx.Bytes())
txLens := make([]uint32, len(b.transactions))
var txBlob []byte
for i, tx := range b.transactions {
tb := tx.Bytes()
txLens[i] = uint32(len(tb))
txBlob = append(txBlob, tb...)
}
headerSize := 32 + 8 + 8 + 32 + 32 + 20 + 4 + sigLen
totalSize := headerSize + txsSize
bld := zap.NewBuilder(zap.HeaderSize + sbSize + len(b.signature) + len(txBlob) + 4*len(txLens) + 128)
txLensOff := writeU32List(bld, txLens)
data := make([]byte, totalSize)
offset := 0
// Parent ID
copy(data[offset:], b.parentID[:])
offset += 32
// Height
binary.BigEndian.PutUint64(data[offset:], b.height)
offset += 8
// Timestamp
binary.BigEndian.PutUint64(data[offset:], uint64(b.timestamp))
offset += 8
// TX Root
copy(data[offset:], b.txRoot[:])
offset += 32
// State Root
copy(data[offset:], b.stateRoot[:])
offset += 32
// Producer
copy(data[offset:], b.producer[:])
offset += 20
// Signature length and signature
binary.BigEndian.PutUint32(data[offset:], uint32(sigLen))
offset += 4
copy(data[offset:], b.signature)
offset += sigLen
// Number of transactions
binary.BigEndian.PutUint32(data[offset:], uint32(len(b.transactions)))
offset += 4
// Transactions
for _, tx := range b.transactions {
txBytes := tx.Bytes()
binary.BigEndian.PutUint32(data[offset:], uint32(len(txBytes)))
offset += 4
copy(data[offset:], txBytes)
offset += len(txBytes)
}
return data
ob := bld.StartObject(sbSize)
ob.SetBytesFixed(sbParent, b.parentID[:])
ob.SetUint64(sbHeight, b.height)
ob.SetInt64(sbTime, b.timestamp)
ob.SetBytesFixed(sbTxRoot, b.txRoot[:])
ob.SetBytesFixed(sbState, b.stateRoot[:])
ob.SetBytesFixed(sbProd, b.producer[:])
ob.SetBytes(sbSig, b.signature)
ob.SetList(sbTxLens, txLensOff, len(txLens))
ob.SetBytes(sbTxBlob, txBlob)
ob.FinishAsRoot()
return bld.Finish()
}
// BlockParser parses blocks from bytes.
@@ -282,91 +270,73 @@ func NewBlockParser() *BlockParser {
}
}
// Parse parses a block from bytes.
// Parse parses a block from its ZAP wire (the inverse of serialize). Every
// length is bounds-checked; a malformed block is rejected rather than panicking.
func (p *BlockParser) Parse(data []byte) (*Block, error) {
if len(data) < 136 { // Minimum header size
return nil, errors.New("block data too short")
msg, err := zap.Parse(data)
if err != nil {
return nil, err
}
b := &Block{
bytes: data,
if msg.Size() != len(data) {
return nil, errors.New("invalid block: trailing bytes")
}
o := msg.Root()
offset := 0
b := &Block{bytes: data}
copy(b.parentID[:], o.BytesFixedSlice(sbParent, 32))
b.height = o.Uint64(sbHeight)
b.timestamp = o.Int64(sbTime)
copy(b.txRoot[:], o.BytesFixedSlice(sbTxRoot, 32))
copy(b.stateRoot[:], o.BytesFixedSlice(sbState, 32))
copy(b.producer[:], o.BytesFixedSlice(sbProd, 20))
b.signature = appendBytes(o.Bytes(sbSig))
// Parent ID
copy(b.parentID[:], data[offset:offset+32])
offset += 32
// Height
b.height = binary.BigEndian.Uint64(data[offset:])
offset += 8
// Timestamp
b.timestamp = int64(binary.BigEndian.Uint64(data[offset:]))
offset += 8
// TX Root
copy(b.txRoot[:], data[offset:offset+32])
offset += 32
// State Root
copy(b.stateRoot[:], data[offset:offset+32])
offset += 32
// Producer
copy(b.producer[:], data[offset:offset+20])
offset += 20
// Signature length and signature
if offset+4 > len(data) {
return nil, errors.New("invalid block: missing signature length")
}
sigLen := binary.BigEndian.Uint32(data[offset:])
offset += 4
if offset+int(sigLen) > len(data) {
return nil, errors.New("invalid block: signature truncated")
}
b.signature = make([]byte, sigLen)
copy(b.signature, data[offset:offset+int(sigLen)])
offset += int(sigLen)
// Number of transactions
if offset+4 > len(data) {
return nil, errors.New("invalid block: missing tx count")
}
numTxs := binary.BigEndian.Uint32(data[offset:])
offset += 4
// Parse transactions
b.transactions = make([]txs.Tx, 0, numTxs)
for i := uint32(0); i < numTxs; i++ {
if offset+4 > len(data) {
return nil, errors.New("invalid block: tx length truncated")
lens := readU32List(o, sbTxLens)
blob := o.Bytes(sbTxBlob)
b.transactions = make([]txs.Tx, 0, len(lens))
pos := 0
for i, l := range lens {
if pos+int(l) > len(blob) {
return nil, fmt.Errorf("invalid block: tx %d truncated", i)
}
txLen := binary.BigEndian.Uint32(data[offset:])
offset += 4
if offset+int(txLen) > len(data) {
return nil, errors.New("invalid block: tx data truncated")
}
txBytes := data[offset : offset+int(txLen)]
offset += int(txLen)
tx, err := p.txParser.Parse(txBytes)
tx, err := p.txParser.Parse(blob[pos : pos+int(l)])
if err != nil {
return nil, fmt.Errorf("failed to parse tx %d: %w", i, err)
}
b.transactions = append(b.transactions, tx)
pos += int(l)
}
// Compute ID
b.id = b.computeID()
return b, nil
}
func writeU32List(b *zap.Builder, xs []uint32) int {
lb := b.StartList(4)
for _, x := range xs {
lb.AddUint32(x)
}
off, _ := lb.Finish()
return off
}
func readU32List(o zap.Object, ptrOff int) []uint32 {
l := o.ListStride(ptrOff, 4)
n := l.Len()
out := make([]uint32, n)
for i := 0; i < n; i++ {
out[i] = l.Uint32(i)
}
return out
}
func appendBytes(b []byte) []byte {
if len(b) == 0 {
return nil
}
return append([]byte(nil), b...)
}
// Builder builds new blocks.
type Builder struct {
parentID ids.ID
+58
View File
@@ -0,0 +1,58 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package block
import (
"testing"
"time"
"github.com/luxfi/ids"
"github.com/stretchr/testify/require"
"github.com/luxfi/chains/dexvm/txs"
)
// TestBlockWireRoundTrip proves the native-ZAP block envelope round-trips every
// header field, preserves the nanosecond timestamp (no seconds floor), and keeps
// transactions in proposer order — the DEX determinism invariant.
func TestBlockWireRoundTrip(t *testing.T) {
require := require.New(t)
from := ids.GenerateTestShortID()
var poolID [32]byte
copy(poolID[:], []byte("pool-1"))
tx1 := txs.NewPlaceOrderTx(from, 1, poolID, 0, 100, 5)
tx2 := txs.NewCancelOrderTx(from, 2, poolID, 7)
orig := &Block{
parentID: ids.GenerateTestID(),
height: 11,
timestamp: time.Unix(1_700_000_000, 123_456_789).UnixNano(), // sub-second component
transactions: []txs.Tx{tx1, tx2},
txRoot: ids.GenerateTestID(),
stateRoot: ids.GenerateTestID(),
producer: ids.GenerateTestNodeID(),
signature: []byte("producer-signature"),
}
wire := orig.serialize()
parsed, err := NewBlockParser().Parse(wire)
require.NoError(err)
require.Equal(orig.parentID, parsed.parentID)
require.Equal(orig.height, parsed.height)
require.Equal(orig.timestamp, parsed.timestamp, "nanosecond timestamp preserved (no floor)")
require.Equal(orig.txRoot, parsed.txRoot)
require.Equal(orig.stateRoot, parsed.stateRoot)
require.Equal(orig.producer, parsed.producer)
require.Equal(orig.signature, parsed.signature)
require.Len(parsed.transactions, 2)
require.Equal(tx1.Bytes(), parsed.transactions[0].Bytes(), "tx order preserved (DEX fairness)")
require.Equal(tx2.Bytes(), parsed.transactions[1].Bytes())
// canonical: trailing bytes rejected
_, err = NewBlockParser().Parse(append(append([]byte(nil), wire...), 0x00))
require.Error(err)
}
+7 -8
View File
@@ -74,7 +74,7 @@ require (
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudflare/circl v1.6.3 // indirect
github.com/cockroachdb/errors v1.12.0 // indirect
github.com/cockroachdb/errors v1.13.0 // indirect
github.com/cockroachdb/fifo v0.0.0-20240816210425-c5d0cb0b6fc0 // indirect
github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506 // indirect
github.com/cockroachdb/pebble v1.1.5 // indirect
@@ -101,7 +101,7 @@ require (
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.1 // indirect
github.com/gballet/go-libpcsclite v0.0.0-20250918194357-1ec6f2e601c6 // indirect
github.com/getsentry/sentry-go v0.44.1 // indirect
github.com/getsentry/sentry-go v0.46.2 // indirect
github.com/go-ini/ini v1.67.0 // indirect
github.com/go-json-experiment/json v0.0.0-20260601182631-00ed12fed2a6 // indirect
github.com/go-logr/logr v1.4.3 // indirect
@@ -151,8 +151,8 @@ require (
github.com/luxfi/filesystem v0.0.1 // indirect
github.com/luxfi/formatting v1.1.1 // indirect
github.com/luxfi/genesis v1.16.2 // indirect
github.com/luxfi/go-bip32 v1.0.2 // indirect
github.com/luxfi/go-bip39 v1.1.2 // indirect
github.com/luxfi/go-bip32 v1.1.0 // indirect
github.com/luxfi/go-bip39 v1.2.0 // indirect
github.com/luxfi/gpu v1.0.1 // indirect
github.com/luxfi/keychain v1.1.1 // indirect
github.com/luxfi/lens v0.2.1 // indirect
@@ -160,7 +160,7 @@ require (
github.com/luxfi/math/big v0.1.0 // indirect
github.com/luxfi/math/safe v0.0.1 // indirect
github.com/luxfi/mdns v0.1.1 // indirect
github.com/luxfi/mlwe v0.2.1 // indirect
github.com/luxfi/mlwe v0.3.0 // indirect
github.com/luxfi/mock v0.1.1 // indirect
github.com/luxfi/net v0.1.1 // indirect
github.com/luxfi/p2p v1.22.1 // indirect
@@ -202,9 +202,9 @@ require (
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/common v0.68.0 // indirect
github.com/prometheus/procfs v0.20.1 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/rogpeppe/go-internal v1.15.0 // indirect
github.com/rs/cors v1.11.1 // indirect
github.com/rs/xid v1.6.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
@@ -237,7 +237,6 @@ require (
go.uber.org/mock v0.6.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.1 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/exp v0.0.0-20260529124908-c761662dc8c9 // indirect
golang.org/x/mod v0.36.0 // indirect
+106 -106
View File
@@ -99,8 +99,8 @@ github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4=
github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU=
github.com/cockroachdb/errors v1.12.0 h1:d7oCs6vuIMUQRVbi6jWWWEJZahLCfJpnJSVobd1/sUo=
github.com/cockroachdb/errors v1.12.0/go.mod h1:SvzfYNNBshAVbZ8wzNc/UPK3w1vf0dKDUP41ucAIf7g=
github.com/cockroachdb/errors v1.13.0 h1:BoCcJeiP9hpBJDETkX19qi8Tb8So37srSsp3stTaDMQ=
github.com/cockroachdb/errors v1.13.0/go.mod h1:bjxt/4E5+OyuAnacpTIU9rn2mzPu1VlthvHP+xpROq0=
github.com/cockroachdb/fifo v0.0.0-20240816210425-c5d0cb0b6fc0 h1:pU88SPhIFid6/k0egdR5V6eALQYq2qbSmukrkgIh/0A=
github.com/cockroachdb/fifo v0.0.0-20240816210425-c5d0cb0b6fc0/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M=
github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506 h1:ASDL+UJcILMqgNeV5jiqR4j+sTuvQNHdf2chuKj1M5k=
@@ -171,8 +171,8 @@ github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+r
github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/gballet/go-libpcsclite v0.0.0-20250918194357-1ec6f2e601c6 h1:ko+DlyhLqUHpgrvwqs5ybydoGAqjpJQTXpAS7vUqVlM=
github.com/gballet/go-libpcsclite v0.0.0-20250918194357-1ec6f2e601c6/go.mod h1:3IVE7v4II2gS2V5amIH7F7NeYQtbbORtQtjdflgS1vk=
github.com/getsentry/sentry-go v0.44.1 h1:/cPtrA5qB7uMRrhgSn9TYtcEF36auGP3Y6+ThvD/yaI=
github.com/getsentry/sentry-go v0.44.1/go.mod h1:XDotiNZbgf5U8bPDUAfvcFmOnMQQceESxyKaObSssW0=
github.com/getsentry/sentry-go v0.46.2 h1:1jhYwrKGa3sIpo/y5iDNXS5wDoT7I1KNzMHrnK6ojns=
github.com/getsentry/sentry-go v0.46.2/go.mod h1:evVbw2qotNUdYG8KxXbAdjOQWWvWIwKxpjdZZIvcIPw=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
@@ -288,148 +288,148 @@ github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzW
github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c=
github.com/luxfi/accel v1.2.4 h1:5VbIHyEvvfobn2zBiTFODxDw1CeqxCepZOLlvkuf9yQ=
github.com/luxfi/accel v1.2.4/go.mod h1:ISIwAX+ZfsL/S5nsP2JvfldXN6Nc+QzoWf6Jtaq+xsQ=
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/address v1.1.1 h1:4afWzyBWzTiZN7RenBtdMC9LIvP9L4CSBzSquwKEAgI=
github.com/luxfi/address v1.1.1/go.mod h1:KG0jUBcgoJYeieKP5jboCq9UewwDBIOus8ZCqUMVlw8=
github.com/luxfi/age v1.6.0 h1:KMD8gSOP4NVCb7NWSlRcgBZNV2xm2a+qQWPyPmiX6f4=
github.com/luxfi/age v1.6.0/go.mod h1:7cu9CIyikgyAvr5MlXFapEDQ15yBaHOSdKkK5lG04WE=
github.com/luxfi/ai v0.1.0 h1:PwTGob0GJivbdqNpUs82bvwcE8/qxyTokxfY7BZVPoo=
github.com/luxfi/ai v0.1.0/go.mod h1:lTuY32dGjEJUgVheYuanlt1O0jHj0AZ09sUSpRsmH6M=
github.com/luxfi/api v1.0.16 h1:RrNHafKYDzI49vHZigz+A8Kmlf60hiZZYcJD9dWfswg=
github.com/luxfi/api v1.0.16/go.mod h1:Eer59msIXMnOlFncG0XjEGH3TZML0Dd1bUu4GtB7f4Q=
github.com/luxfi/api v1.1.1 h1:CXD7m0quPmUm+Qw35TrF+E7b0Fq4qz9gHfrZ5gyrjHU=
github.com/luxfi/api v1.1.1/go.mod h1:g6J0iohVqaIj2aO1u/ZJPqjiX2tog0NM3/SBf7wJ4cA=
github.com/luxfi/atomic v1.0.0 h1:xUV60MuzRvXngaQ1sM0yVC2v4TRoLlUGkkH7M9PS4yw=
github.com/luxfi/atomic v1.0.0/go.mod h1:0G2mTlQ6TXWHICUHrUUPu1/qAiIyR4gSZ2tva9ci/bI=
github.com/luxfi/cache v1.2.1 h1:kAzOS55/hmYeNKR+0HAKv4ma48Y6JjkI8UQeqdZ8bfI=
github.com/luxfi/cache v1.2.1/go.mod h1:co7JTxZZHpKT31Yh01LFp5aZOxmoUg157FhBLQdQHVU=
github.com/luxfi/codec v1.1.5 h1:KBq8uvYm5Dy+E1heG8WBmqbqu8kstlFyE5ASBBB+C8I=
github.com/luxfi/codec v1.1.5/go.mod h1:/ugIv5iEgI+VAuPIetzxNT0eJaEjOID/mrIsgIjJh8g=
github.com/luxfi/compress v0.0.5 h1:4tEUHw5MK1bu5UOjfYCt4OKMiH7yykIgmGPRA/BfJTM=
github.com/luxfi/compress v0.0.5/go.mod h1:Cc1yxD2pfzrvpO32W2GDwLKff+CylHEvzZh2Ko8RSIU=
github.com/luxfi/concurrent v0.0.3 h1:eJyv1fhaC0jMLMw6+QS774cUmp7GK+ouMgvLCqnC7cc=
github.com/luxfi/concurrent v0.0.3/go.mod h1:Aj/FR5NpM0cB2P4Nt3+tz9+dV6V+LUW4HuMgSjwq5hw=
github.com/luxfi/consensus v1.36.1 h1:aHCUTgu0SaJXRouisQuFo1CkkdpHX0XftpM9GU07LzU=
github.com/luxfi/consensus v1.36.1/go.mod h1:hmsGz3CcTrKxvw7/YSmfu8qAtzgyL5zQ3ajpUNcub/4=
github.com/luxfi/constants v1.6.1 h1:4AfBh1YxDgnQjWPLqLpjiBaLAjPBw5naTTzRWWM19ms=
github.com/luxfi/constants v1.6.1/go.mod h1:Pu5jWHdnUtQRbWC43yTUjU/pbIIKMDOd2a2yroSfo48=
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/corona v0.10.3 h1:Yi1oAkW0HEsf5fvst/tUN0AjRVg6DoNHB/IC0qrFWZE=
github.com/luxfi/corona v0.10.3/go.mod h1:xe5qRir0p+FA6eETpyGDv4LjYySg1zVB13kmHpy9x94=
github.com/luxfi/crypto v1.20.0 h1:JNsQ25sVO6T8XuIHRue4akOpnt5pNmk1xg5hzmU6dNE=
github.com/luxfi/crypto v1.20.0/go.mod h1:bLCBuIV/KDjPytld7jSYe1WbfWknPQXcivq88Qo96QU=
github.com/luxfi/cache v1.3.1 h1:grQhi/B5GKypG7avDMeY143QTgFbfEvQICKNIh1Cw6U=
github.com/luxfi/cache v1.3.1/go.mod h1:2MokdbeNUy/9O3mdREWkE6BiN7tRvePkXiKkcb+4M7g=
github.com/luxfi/codec v1.2.1 h1:NA/O3dWm9QejQPdjLEIVx42ddVqAXsy6Y6igL/V1+aU=
github.com/luxfi/codec v1.2.1/go.mod h1:xjWOTEbw9gxY/N8nZwQPvRCfPnK/ugJHBWsb3BZ0HHs=
github.com/luxfi/compress v0.1.1 h1:cQjRYQFRrw8HinjW1T8FmKgqdRwrFYDCdecc1t0i+uQ=
github.com/luxfi/compress v0.1.1/go.mod h1:d4CkRRmwPzafIp57Jxra3RmAmNwNwU8Oc0QBBRlTqGo=
github.com/luxfi/concurrent v0.1.1 h1:LkAmNXybOsAm97nFILqMDBp/YLgleuLmyGomFT7YNxU=
github.com/luxfi/concurrent v0.1.1/go.mod h1:GlkfiJtPBpatNxvROf7hkPi4gsnmdHGmqhDnVWFrkZE=
github.com/luxfi/consensus v1.36.2 h1:IbeWQF1wEcA/MNxe4fKniSICPndAGEysV/A3DZGMXFM=
github.com/luxfi/consensus v1.36.2/go.mod h1:m4aMFbKYxwM2X4oPYRjq4LmDSShfCgAHnhmxIGKDyFU=
github.com/luxfi/constants v1.6.2 h1:pXHdKIFbfE9qX4xOjq2LxYvagNhhNvspUVEbPcIEKfA=
github.com/luxfi/constants v1.6.2/go.mod h1:r0oH8C/+r/XFYBq1AJxt6zWRKKRKgDzrEMop/CCs9rI=
github.com/luxfi/container v0.2.1 h1:MTnfKXzS5+oxV5jKZerdOxSA6iMPaQI9/FWGufizzaw=
github.com/luxfi/container v0.2.1/go.mod h1:B+uM0wP0lGvt/SSK7QOEn/qBcsHzILVHlKikdCyzSgM=
github.com/luxfi/corona v0.10.4 h1:/+Uy5iOWBMkr+XACnRRiyrbb5ebsZLsUXjHfJW3sFyw=
github.com/luxfi/corona v0.10.4/go.mod h1:44Tjnm2uRG22kmmLfCzR8QEO8OXZ3jR/OnUKLsnjVJ8=
github.com/luxfi/crypto v1.20.2 h1:L81WEsU/hs2A76F5PWBusG0yU74QqkDdUqqgexWUxh4=
github.com/luxfi/crypto v1.20.2/go.mod h1:qYHOM0lO4PRh7LEaObxFQUIMjmT1/paVm/WgZkobT1k=
github.com/luxfi/crypto/ipa v1.2.4 h1:6xfwhI9/HrcDkF3Ti5/NxsNQIWbwYDJmRSNIHRQ/xfU=
github.com/luxfi/crypto/ipa v1.2.4/go.mod h1:43J6f6rcfUMrZt4cQectMOZb6Ps+fAEj8ZTPC3Kk+gE=
github.com/luxfi/database v1.20.4 h1:WOt2GIGJxf8AFpg49odMz8DZ8RFSLDrozGhZtmorN70=
github.com/luxfi/database v1.20.4/go.mod h1:S/LvmfzNYWVNslcEcZwDrntqUO2ksaL8ql1nRmLUA/Q=
github.com/luxfi/database v1.21.1 h1:GNnoWVa82l+n2dK7x2aG8LR2NEToq6ZCRX0sQjmK0OM=
github.com/luxfi/database v1.21.1/go.mod h1:Gc7Z2OPrrcYLnAL8B1trOnguXauOlSDV5tkviLN6Xec=
github.com/luxfi/dex v1.14.0 h1:4PtorVbRmbD73S6YWPvgp5GRCb9jeuaedl7mo1CbzCI=
github.com/luxfi/dex v1.14.0/go.mod h1:wYWQmwospvdKBWQ6OJMXb8I+kfgEtE46R1rD8H7vHCQ=
github.com/luxfi/dkg v0.3.5 h1:s2L2mMQaz+n9m0b0ghvoV5VZNxiwb2z4WrGugvK0udY=
github.com/luxfi/dkg v0.3.5/go.mod h1:M+WH7GFRN+YUD851Rlnumdp0Md98kplNN8pVx65U8I8=
github.com/luxfi/evm v1.104.4 h1:Y2OxnhMZBOG4O5CdYcBkNhO3O/Y8PW+LafTuy1lpJME=
github.com/luxfi/evm v1.104.4/go.mod h1:OjKvdO53g3LrNutLgx6N0ZXair6glO+MtueD4MHCKaE=
github.com/luxfi/fhe v1.8.2 h1:QllnObNFbi6D4mvFI6uQkepW8HgLtdy4RMR1TKYAInA=
github.com/luxfi/fhe v1.8.2/go.mod h1:16yxwhcnCez/rNcd/C9JjH9IjbEz73X+0tvlsONyLeA=
github.com/luxfi/evm v1.104.10 h1:NH7G/CpCu41aXrPwPsSyxQmoFCO0yQ2EJy2FgvDbe8U=
github.com/luxfi/evm v1.104.10/go.mod h1:fKIeKHjpFNBJLXYRfiggrqCr+1Yaa0TWPHZAXFaHX4M=
github.com/luxfi/fhe v1.11.1 h1:fDTpXhWrqgtrmgvu/tOkDt4A5omJ6cU6u/wrRMBC6lc=
github.com/luxfi/fhe v1.11.1/go.mod h1:QeNytuStXaY+HWvUN4+ULZBRMzvnrzjA/AV6FWksBro=
github.com/luxfi/filesystem v0.0.1 h1:VZ6xMFKaAPBW/ddlMsDnI2G0VU1lV5rYaVcW5d+KwEY=
github.com/luxfi/filesystem v0.0.1/go.mod h1:OQVSU6XNwqrr1AI+MqkID2taHUclx7NYmmr3svgttec=
github.com/luxfi/formatting v1.0.1 h1:ZnE1rAdEUds9yAegdVdGDOBGN6hLMPOv6E03Fp8IEYo=
github.com/luxfi/formatting v1.0.1/go.mod h1:mYzNf5DJOiqSSKUPzNj5dKy4tstFbN3pZlkI5716eKc=
github.com/luxfi/genesis v1.16.1 h1:t8zFIeFg9hwl39HpYKshpB2hlHjVVBpd8va4d+FZ8T0=
github.com/luxfi/genesis v1.16.1/go.mod h1:vpnyQ/YcGINhUekrCiZWFryvP3qgYzTgFkfWoExlUdE=
github.com/luxfi/geth v1.17.12 h1:UP/fhpcfbGPTrkOCwX3d88Oc3jVm5gTOgfjgq+lek6s=
github.com/luxfi/geth v1.17.12/go.mod h1:3vQfQJd9JC+AVBjxNXa9PYQOqpbE2dKu8E3jqhPZ3LU=
github.com/luxfi/go-bip32 v1.0.2 h1:7vFbb+Wr4Z499q2tuCLdd7wWjtn8sH+HWBlx76mhH9Y=
github.com/luxfi/go-bip32 v1.0.2/go.mod h1:bc7/LXDKAJQZ/F0Xjf5yXaTZxY9/ssLb4FC+Hxn/cDk=
github.com/luxfi/go-bip39 v1.1.2 h1:p+wLMPGs6MLQh7q0YIsmy2EhHL7LHiELEGTJko6t/Jg=
github.com/luxfi/go-bip39 v1.1.2/go.mod h1:96de9VkR2kY/ASAnhMtvt3TSh+PZkAFAngNj0GjRGDo=
github.com/luxfi/formatting v1.1.1 h1:MJhVXIPh1dbysvYEjtaEA/Z0FUTiI7n0DwOF54FS08c=
github.com/luxfi/formatting v1.1.1/go.mod h1:zhBWp6fLZduhpiAdPgVDdPVOyhw4FvwRUksF6+xKQCE=
github.com/luxfi/genesis v1.16.2 h1:OLQg5+ln8qgORBYnJuQsZxar8AfZlsXg5g/f95AraPI=
github.com/luxfi/genesis v1.16.2/go.mod h1:piaSqJY80eVpgov8DUWQXll9IdlCroWgvhnwC7/3lTA=
github.com/luxfi/geth v1.20.1 h1:QUGQr4AKvADjwMi7t8a0OfoyxShgEcI9pwie1jFYfm0=
github.com/luxfi/geth v1.20.1/go.mod h1:GV5bIMEgWviRN+jPXERyVpI16H3iHqPcdIokDoZdrvU=
github.com/luxfi/go-bip32 v1.1.0 h1:zjy19WKa1KJnGamRNyOZM3l/MPtV/sax7M4NMPwLHZQ=
github.com/luxfi/go-bip32 v1.1.0/go.mod h1:QyDXlzWL3xRiCbMUi4Z3J52Q/SMoCvdRLXXlYvSZor8=
github.com/luxfi/go-bip39 v1.2.0 h1:fx3pFuSGawCG4In6pA4OLLStqbgIqD1j8EygFskoHzY=
github.com/luxfi/go-bip39 v1.2.0/go.mod h1:if+2OVbG4k4jKIuBt/Rse1KV1kgWQM5j5xFbUtwbNtc=
github.com/luxfi/gpu v1.0.1 h1:7cDJTwTZ8Wk5KmyzsiGPe1nekyTgsSG6LJ+4rgKim8E=
github.com/luxfi/gpu v1.0.1/go.mod h1:8r1ReBPsLx1zmcA3GksKD4J7azP52FSTU/LpT9TyYHE=
github.com/luxfi/ids v1.3.1 h1:CGE3QvYzdwfDpfODAVNjMygSaueVPWXSB9yaeyCEd+k=
github.com/luxfi/ids v1.3.1/go.mod h1:6vpdcdZW0qxeade+3xby8aLTutbcJ7O0r8+fNQrksGI=
github.com/luxfi/keychain v1.0.2 h1:uQgmjs37/VBIALEiYrrszTpxvtqr07/YvS9TnmxGafs=
github.com/luxfi/keychain v1.0.2/go.mod h1:q/4ULgZBlstKkwzOzG/0T6y73BDPgnkrcibbJyTvmbU=
github.com/luxfi/ids v1.3.2 h1:c6Rft5kZB4XqiCtWaGH47bfhaNFm3FGRfhEzI01GVeI=
github.com/luxfi/ids v1.3.2/go.mod h1:+5l8cYMbKpORJbQ2r98CYJo9TQATgUdnmzpYFZWMwwc=
github.com/luxfi/keychain v1.1.1 h1:dTYEPy6CGVC1sogMci4iJogUvW6VdTmemplQdzRqnAs=
github.com/luxfi/keychain v1.1.1/go.mod h1:hAzBcwxGumtoYrM5hfhwdt8wE0p7r2JCd5AxswqfkoY=
github.com/luxfi/lattice/v7 v7.1.4 h1:hQR02M6cHTAV5+joOPi9gb9Gm+z/hKJnhJF4IlciIJs=
github.com/luxfi/lattice/v7 v7.1.4/go.mod h1:DmIQFi3mJiehVsR235l1NKYEU0JhU649OX5p7gMEW2c=
github.com/luxfi/lens v0.1.4 h1:goGjGDXx2BNdjzXDunL5QT8elK2ZyCcc0z8TAbtWYrg=
github.com/luxfi/lens v0.1.4/go.mod h1:mL+G8IK+9L41d78/2FYRgfhEzAjcr5+VEXB8SGuHbus=
github.com/luxfi/lens v0.2.1 h1:5Qd0GdjbM+XUVgwDbZ452tKkR7yeE8QnBTHHaH8fJNY=
github.com/luxfi/lens v0.2.1/go.mod h1:6FIhC8weEE5RbNMF3SaE+XPSB9cr6FmjypYBoHkz4JQ=
github.com/luxfi/log v1.4.3 h1:xkUKRWvQ4ZwvlUC2e0/RTtHYZOYSMvSQ9W9lbjwBmiI=
github.com/luxfi/log v1.4.3/go.mod h1:myIkufyiQomSQH34K981kbz6cG4WUoerRUh7F4XhlQI=
github.com/luxfi/magnetar v1.2.3 h1:n4UrJZLK+mhDDZr1HLl2H/KgA6o6v62r5oiC61R7awE=
github.com/luxfi/magnetar v1.2.3/go.mod h1:z9PLkqzzYiaFGT/qFBQSnNoHmZrg8y7JlYGiNnHAAdk=
github.com/luxfi/math v1.4.1 h1:1t9bCCsEqnl9yIKrShlbs80DBKyYTWdnzkVfBqEeO7Q=
github.com/luxfi/math v1.4.1/go.mod h1:QvbRxauQyE1w4lvbcLSe6c8yeJz2Zj1Bq1rayGgs2tA=
github.com/luxfi/math v1.5.1 h1:FDOY75e4vn/Xra1ij99xOS/9XdxQGCPP6HONHRkCwfg=
github.com/luxfi/math v1.5.1/go.mod h1:3j9R24hVfPhrbvs45YSJP7jAyVNfwx/cj/+lAO8IGro=
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/math/safe v0.0.1 h1:GfSBINV9mOFgHzd32JbgfHSLhlNn0BwnP43rteYEosc=
github.com/luxfi/math/safe v0.0.1/go.mod h1:EejrmOJHh03YAD8+Zww8cPcMR1K3Q2I7w1dX4sMloeo=
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.6.0 h1:PIxHOk8R0qs5etnWsUSPoZ5wGh37APgUiSt4gijOl80=
github.com/luxfi/metric v1.6.0/go.mod h1:ux+w3RZQCfF1zM8MO0wAWyNj/CsDlPd2mwTGshB9vY0=
github.com/luxfi/mlwe v0.2.1 h1:pRwTjNUUtzUxRIlMbUPpeh9DE2/NdqfS17hfdogazp4=
github.com/luxfi/mlwe v0.2.1/go.mod h1:DD9EHTeiyh/y0KGGeqL+q9S4n8raeGiGdaG/BQPAvT0=
github.com/luxfi/metric v1.8.1 h1:v58GgPFAOLPVxSa/JiNLwqJQNEFHdWbXZV28piMXX4s=
github.com/luxfi/metric v1.8.1/go.mod h1:R1OPAIeW4UBW3osK7j2r3/XPmczfNRFTXg4bnlemTuE=
github.com/luxfi/mlwe v0.3.0 h1:5mtXLbO2RxaE45r75sj43c6UdpjDKQ5nTQcOGuoRQT8=
github.com/luxfi/mlwe v0.3.0/go.mod h1:DD9EHTeiyh/y0KGGeqL+q9S4n8raeGiGdaG/BQPAvT0=
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/net v0.0.5 h1:F1lD3NsIioV0wr2V5jWc4TtMyiE/Ffo1LoeblFv3TrI=
github.com/luxfi/net v0.0.5/go.mod h1:BEQR1HEVmkjii/F1R6vJrNUVE7wr55b4eMq9Iz5wjUw=
github.com/luxfi/node v1.36.9 h1:4Ci0tmdPoTNZ9LqESY7z6yJMZlyZc8/pjzjyYU9zb3A=
github.com/luxfi/node v1.36.9/go.mod h1:oLFo9c24zjjcIFvMfo+tHlwgtVJfW6q/COP35i+BpmQ=
github.com/luxfi/oracle v1.0.0 h1:Jcr1jbiVCFuaeer2Vcrabc0A8/u2CNCiAEoauftlxfE=
github.com/luxfi/oracle v1.0.0/go.mod h1:7xmiYMZwKiIazVr56lMHDxC0kMF0/Y/9OLSdOBBQ7/M=
github.com/luxfi/p2p v1.21.1 h1:gmz1JMDhzHIL3dQlhwIDvR4OlFuhNVfnWUl/ipYhAIo=
github.com/luxfi/p2p v1.21.1/go.mod h1:SsNPR5fPGWWNem9plGWhSmRqyDoysJ3kPAN0zG0g3iw=
github.com/luxfi/net v0.1.1 h1:jIQCb8ulBGEvHIcorzDDNCeWzutFzLVtRWodutrQE24=
github.com/luxfi/net v0.1.1/go.mod h1:SwxbUQ538u4QAcgC/N61uahDk1TDHL6Ku89CX/WV2lk=
github.com/luxfi/node v1.36.15 h1:tpa1Z0OjJ1QD2kuzrTlpyavE0YL7EEZL+qEhnrZ2VAs=
github.com/luxfi/node v1.36.15/go.mod h1:35u+z4SbpTjOLLXuCQcfrqD5voSI88osoTru0UoNv4Q=
github.com/luxfi/oracle v1.1.1 h1:KwU9TEeHdJb+vc6VJlMxS7nKhLOJ9EI4WGqxwSJp/J0=
github.com/luxfi/oracle v1.1.1/go.mod h1:ST/4cVIUBa08GnkI9vQbfg8GGt0oDCXfTnEkx0WsnPs=
github.com/luxfi/p2p v1.22.1 h1:M59Iy+FIJta99aFfTpG6EE70jm9uqIX+iHrGBYPHDhg=
github.com/luxfi/p2p v1.22.1/go.mod h1:FHOSavVcq8JS1ZQtfddd9Jj1gaPstz1cjvNgczAQRyg=
github.com/luxfi/pq v1.1.0 h1:ADplfUSyirLymSxs3Ix0HeDTyl5oswCNUpXJt/5vLY8=
github.com/luxfi/pq v1.1.0/go.mod h1:KT5rG9ztpzIkT9QSnXK4WFqBBLzKCLjY7l1c/unBi8I=
github.com/luxfi/precompile v0.19.1 h1:nTfhwrubwQKED5SAOFqIbFO1o6J49IZNdSYbOEaiPpA=
github.com/luxfi/precompile v0.19.1/go.mod h1:AOMGWGFXHtnGVYjel/mP/7Dt60e2u7ef0SswY4j8F+k=
github.com/luxfi/proto v1.3.5 h1:AW11rnu5xyvB7beyowoiY9uIffLOF3+eMR/a3EkK2c8=
github.com/luxfi/proto v1.3.5/go.mod h1:ixTofGpdW1rTYr+wgTuBhAsgBv8GnWYHMLWbPNEdm7M=
github.com/luxfi/pulsar v1.9.0 h1:c0JnatYF79aN87aof9VlYjIoCzmixxrgNPeUUuh8ScU=
github.com/luxfi/pulsar v1.9.0/go.mod h1:1+/atAiiiOm9RnXM3c66eHF3garjAa3C+sn4rAU7JUU=
github.com/luxfi/relay v1.0.0 h1:AHE8JtNmicYcwu7hCrUUMJ8WfCtjL4UlkzVaHwIrBpI=
github.com/luxfi/relay v1.0.0/go.mod h1:srhFAQ3dS+WBTRwPqzinZiw+QbWdRhajT+W7k9ShjrE=
github.com/luxfi/resource v0.0.1 h1:mTh+ICWSy548GTUSSyx7V/X5dV18oEwxZeQEYGJQhD4=
github.com/luxfi/resource v0.0.1/go.mod h1:wWpZktciYwIi6RNqA+fHwzmPrUJa7PRX7urfwT+spRE=
github.com/luxfi/precompile v0.19.3 h1:ljJholS+9mlR8EjbfM96BLYvIni7xCt7KkX06YzAIJU=
github.com/luxfi/precompile v0.19.3/go.mod h1:c+dh+FWfpKr3FNfI4LhIdnn0naQmmS5Yz0KKTt4LknY=
github.com/luxfi/proto v1.4.2 h1:dEGTE7xOWVPTXRZNDBJfwh2Q19vE7MQBhyXXmWenw+8=
github.com/luxfi/proto v1.4.2/go.mod h1:vwVkrC9ghhuCL8bluA59+G5jaD6WuqiDyd1iKh7yzOE=
github.com/luxfi/pulsar v1.9.2 h1:pFLoAfBlCwFZfchqHn78J6yMe29AhaoKGRa/KTUP8CE=
github.com/luxfi/pulsar v1.9.2/go.mod h1:uTOtribcUvTTwAOy0Ztg0S2AUiNAsVqFfopTrKW+zjM=
github.com/luxfi/relay v1.1.1 h1:s5gH9dmTyz2X+K4GvCSuG4baZLjiJxK8ji8xvqj5k1E=
github.com/luxfi/relay v1.1.1/go.mod h1:jdgwxl8G0iykzyqW43uazRPsdz49awgv24kev3cMiQI=
github.com/luxfi/resource v0.1.1 h1:k11s5xLGX85UWq/iLZyWLhnqeLTlp3FHEt8u/8AHdkY=
github.com/luxfi/resource v0.1.1/go.mod h1:s7SbZOSVbgj9bWFOcLCcXgnMlHxbqtqhAeJ3f0Xuy/Y=
github.com/luxfi/rpc v1.1.0 h1:B/PJbK399th1mHRDSufhCpVbAciZqId3LsaWhIGNWH4=
github.com/luxfi/rpc v1.1.0/go.mod h1:s0bI7/Wg1ZdFdG/cQK+4pZNdEmUsXNBA3HeZRZ+XLeM=
github.com/luxfi/runtime v1.1.3 h1:6Yp/PKwQCohjXmBR9GA+gamdSAp+xA2rdN6J/74Y4aw=
github.com/luxfi/runtime v1.1.3/go.mod h1:r1uonDnxRCnPz6N6WYwaC72HW95KbFIAyChnJyxePGs=
github.com/luxfi/runtime v1.3.1 h1:vsQZ3sl6XMeyHuNGCMM86d0whrE/lhMre4CCJaClPdE=
github.com/luxfi/runtime v1.3.1/go.mod h1:Tct998uUcmCQbUC1WLEeGLDH2IaVgMb+SkKcHKZpHV0=
github.com/luxfi/sampler v1.1.0 h1:u3iRDl7V06ARh0e85h3HT+aZ1saCFo2yMMsh+dCJbqk=
github.com/luxfi/sampler v1.1.0/go.mod h1:kJa53S3tC9+VSbuV3RFu68MmbCCBlr2UM39LOClQ/Hs=
github.com/luxfi/staking v1.5.1 h1:f9MaGnRm0xc02crDm5Qs1T2r88d3KzNkHZypAvsmAlU=
github.com/luxfi/staking v1.5.1/go.mod h1:lT7KLaiTpdq3lg78H0gp2qSEfX9LaK1vs7w73XV/9nw=
github.com/luxfi/staking v1.6.1 h1:be023mY88AnFgF9P+MMvILX21I2CaqVAVkGcc+a20so=
github.com/luxfi/staking v1.6.1/go.mod h1:X8i/uCLc009mlIUnFMErrQMCwfGaOVCAVf+GbDN5nn4=
github.com/luxfi/sys v0.1.0 h1:M7RYOt8W4Wws7cxxsyOHe50UKMYTzIu7HYknqW4xt0Y=
github.com/luxfi/sys v0.1.0/go.mod h1:GT8vGdYTfoqRy9/11blmRuqPPypzwrudCTHZXT+ru9M=
github.com/luxfi/threshold v1.12.1 h1:pA6ZB8Qv6BStprSemfoCY3fD7P5PEod36Nj6FmJR1jQ=
github.com/luxfi/threshold v1.12.1/go.mod h1:iuRQGDAy8ZKjQhZjkSKg7NtbP75/8Up9zj52y7IuyZo=
github.com/luxfi/timer v1.0.2 h1:g/odi0VQJIsrzdklJUG1thHZ/sGNnbIiVGcU6LctJm0=
github.com/luxfi/timer v1.0.2/go.mod h1:SoaZwntYigUE3H6z1GV32YwP8QaSiAT0UiEv7iPugXg=
github.com/luxfi/tls v1.0.3 h1:rK3nxSAxrUOOSHOZnKChwV4f6UJ+cfOl8KWJXAQx/SI=
github.com/luxfi/tls v1.0.3/go.mod h1:dQqSiGE7YxXUxOwICoReUuIitBms9DYOaCeteBwmIWw=
github.com/luxfi/trace v1.1.0 h1:eQoObwStrdQN879zfJWJeN1l0FJnfOKQHQHyEJOYjCI=
github.com/luxfi/trace v1.1.0/go.mod h1:Sgtpj8ZE5GBSi4ZyQOL3rL9enl59sSWswWWKw3BUdpk=
github.com/luxfi/threshold v1.12.3 h1:CYqcPfzVUs0M/+e/ja1q7aLBZZUi6Rv/XIXR5lrZ7aE=
github.com/luxfi/threshold v1.12.3/go.mod h1:xQT8xzmh9pQ1CdBgpl7P3ezXQZcqTPv3tqpIytvuiTA=
github.com/luxfi/timer v1.1.1 h1:54GiNBKydQ0VF5/EwVc/mCsbqe0yJNfZV7Ae8qJhCwk=
github.com/luxfi/timer v1.1.1/go.mod h1:OXY/8ZFKCdEsimpfnUG1MQWvzjjFbsmBOiW/m6KSrC0=
github.com/luxfi/tls v1.1.1 h1:BSZ0gHSp7U8vzlmzx7WSSCz+b7Ky4JtD9HDDhn7vrDg=
github.com/luxfi/tls v1.1.1/go.mod h1:+5TDy8UtLL+tz124brZzpUDBRj+sKrq0JFqdmpMUHgw=
github.com/luxfi/trace v1.2.1 h1:MPV079P2eTijB7F06AyJU1HJwpQVxRx1qYXhva9YsvM=
github.com/luxfi/trace v1.2.1/go.mod h1:/bX8g0RRHPHUq7kX8of/Aaq6C7rD0JuNH57vVbw7ZG8=
github.com/luxfi/units v1.0.0 h1:2aNVB+WsP1XeDob71IsO0w3jJqP3FtZdYnFsmORkJZg=
github.com/luxfi/units v1.0.0/go.mod h1:tma28v4ed1tupdS0kpSeyO+u1wWK/g1NqODPbN1YzmA=
github.com/luxfi/upgrade v1.0.1 h1:7+ygYeUf/MuLeGL7pjIu6ckQimxctCp+Swybhpy64go=
github.com/luxfi/upgrade v1.0.1/go.mod h1:Re7g9Y+SYf/LvkHFpN0vbtlVH/Rr5ZpHQdPeVFEo3Jw=
github.com/luxfi/utils v1.2.0 h1:gtEiI7/NM6PQ/OasEpH0PvB+e5hIS/tpum9r64pYjMc=
github.com/luxfi/utils v1.2.0/go.mod h1:T2OCKT1xG9jtKR/gyJQoSkticzrE9WFQ8eohJHGu9Fg=
github.com/luxfi/utxo v0.5.7 h1:ocmCvtL5/QrxcDBjwISD3gKKyydeNpnlFHZ4E+/YYIM=
github.com/luxfi/utxo v0.5.7/go.mod h1:n5Rzk6idEPAdTFLnioVQDhw+ypVtyE2TiJqWaTX7ZIA=
github.com/luxfi/validators v1.2.0 h1:VygpiBqBAdGrfkb7xzE2yrVmnXaqE+hm8FLWdGXO7G8=
github.com/luxfi/validators v1.2.0/go.mod h1:GYLulrNXAan23ZlX7sgWVbVnLpUexeB/m2qr2ymsXok=
github.com/luxfi/upgrade v1.0.3 h1:mFMfIb88HzoL4fp7I6w1g+VnxlAWj6UQdZOdf4AkCLk=
github.com/luxfi/upgrade v1.0.3/go.mod h1:3c/u4T/n7EsODofkWg5VigSt9BATQM8EmaNttfHMCrU=
github.com/luxfi/utils v1.3.1 h1:Us02ag60kGu94B41XIelExa7c+K6zPKwDJyq+eB+hc0=
github.com/luxfi/utils v1.3.1/go.mod h1:ROZrzpt6Kx8ttS1mo12oqsOzRB088GQ1h9jXEoDDpNA=
github.com/luxfi/utxo v0.5.8 h1:HydTOKERb8vY0Z39Dvg/V3qg7My3N/eXOY4nLv3iezg=
github.com/luxfi/utxo v0.5.8/go.mod h1:kqkwMm99NbWwGZrOzuRzF0vck+lCJwrlejmmCwj5pZc=
github.com/luxfi/validators v1.3.1 h1:+/7j0CTXlMKyaSLFM+gd6Fq64/edORJfrD/xGnf7Xcg=
github.com/luxfi/validators v1.3.1/go.mod h1:sIQyUZOvXoJ/9/RCOhHSzjumZIoxiqacNOn5mQWVozU=
github.com/luxfi/version v1.0.1 h1:T/1KYWEMmsrNQk7pN7PFPAwh/7XbeX7cFAKLBqI37Sk=
github.com/luxfi/version v1.0.1/go.mod h1:Y5fPkQ2DB0XRBCxgSPXp4ISzL1/jptKnmFknShRJCyg=
github.com/luxfi/vm v1.2.7 h1:/lHRgSU/Jmn3D5hBg4R3ZntWnY/L5fjF8JcYPoqcIjc=
github.com/luxfi/vm v1.2.7/go.mod h1:o52+zrBZCqBPrAO0dIAmK5Px7oKevT0sup5LssgFdYM=
github.com/luxfi/warp v1.24.0 h1:jrcJNlbOiZsAEopJMy9bSaCwI5NDZ8qgp/6sNoXqepg=
github.com/luxfi/warp v1.24.0/go.mod h1:bKvTi24JHlANsl7qkWZAVr/DsMfvwy42f+Cc9x4+Sq8=
github.com/luxfi/zap v1.2.5 h1:SfUzw4cp01xGfDoKRQBRzXsxCqY7jQ2UFJo/KWkhu8I=
github.com/luxfi/zap v1.2.5/go.mod h1:JfqII8VtVQYLLTX6obU1DP9sjGqf9L24vfug5ifh0b8=
github.com/luxfi/zapcodec v1.0.1 h1:pRxLxCOi6uihQMg8A8riDjNjefU2cXZxfRVZ+obeuL8=
github.com/luxfi/zapcodec v1.0.1/go.mod h1:txrRt2JK4O76ssTxlXIwoNVsgzyZVL0ES4mlXqGNogs=
github.com/luxfi/vm v1.3.1 h1:rLCbygaajehVkUoJ5czwhpUAJaC5J5okq3p+j2QSPSo=
github.com/luxfi/vm v1.3.1/go.mod h1:uViH3COP8hhCbj42v1MTohkPCDwYQCZanNIOb/StWqY=
github.com/luxfi/warp v1.24.1 h1:9F+z6fy4sxmXp+3LlR9m3TiZ8Dfthh+s5KOeewSJL30=
github.com/luxfi/warp v1.24.1/go.mod h1:kRGQDt6EB3oRLYo71aXqnmJuCBVfND3oQ5/2miaLoyg=
github.com/luxfi/zap v1.2.6 h1:NBpbm9Gib41Oi/XAkAZKQ3hb+xCafo7JsrUjw+bKiAc=
github.com/luxfi/zap v1.2.6/go.mod h1:sTAe/AMMamoE85cVoe81+NbqHJkgvqS0LhY9ByHEmr0=
github.com/luxfi/zapcodec v1.1.1 h1:SdYexj7oWdks/wfF9s+8m/9PAYt3S8QjORxURutvIs0=
github.com/luxfi/zapcodec v1.1.1/go.mod h1:fmmgd8C/JrQdh3b1OzBkrvPN4TYs5uRfVV3sVtbiLbQ=
github.com/luxfi/zapdb v1.10.1 h1:XV3k4UTTKKxUMgbfC7woPXgUEIJd3P5nj2lGTQ88xeE=
github.com/luxfi/zapdb v1.10.1/go.mod h1:3Y0hH2A9kvjR+Bp9N2yEbtHnhXGHhqCQOLvBRkHrrM0=
github.com/luxfi/zwing v0.5.2 h1:2+nDKHVIdT8GvaKO79GuO3x/3DgJvX0gI32h4P+P6RU=
github.com/luxfi/zwing v0.5.2/go.mod h1:8nixkEL3bhO2LrqVqhJ8WgT+QGUtnCPIQIhFQh9gQio=
github.com/luxfi/zwing v0.6.1 h1:Ve7RvYVBXx4JWe8TZhLfpez1N1G685hl/PSMDsR4RI4=
github.com/luxfi/zwing v0.6.1/go.mod h1:Eal4hnjmdFXc6rxciA6cxeCfV1BKs8le03v83W5oiKg=
github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
@@ -511,8 +511,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/common v0.68.0 h1:8rQJvQmYltsR2L7h8Zw0Iyj8WYNNmpwikoQTZXwfVeA=
github.com/prometheus/common v0.68.0/go.mod h1:4soH+U8yJSROk7OJ//hmTiWKsxapv6zRGgTt3keN8gQ=
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
github.com/prysmaticlabs/gohashtree v0.0.4-beta h1:H/EbCuXPeTV3lpKeXGPpEV9gsUpkqOOVnWapUyeWro4=
@@ -520,8 +520,8 @@ github.com/prysmaticlabs/gohashtree v0.0.4-beta/go.mod h1:BFdtALS+Ffhg3lGQIHv9HD
github.com/quic-go/quic-go v0.59.1 h1:0Gmua0HW1Tv7ANR7hUYwRyD0MG5OJfgvYSZasGZzBic=
github.com/quic-go/quic-go v0.59.1/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rogpeppe/go-internal v1.15.0 h1:D0RCU5rMAp+SpgkiNdrjfJ+LX4J1M32V2NeCY7EJ6hc=
github.com/rogpeppe/go-internal v1.15.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs=
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
-58
View File
@@ -52,64 +52,6 @@ func (b *Block) Parent() ids.ID { return b.parentID }
func (b *Block) Height() uint64 { return b.height }
func (b *Block) Timestamp() time.Time { return b.timestamp }
// Bytes serializes the block (parent, height, timestamp, transactions).
func (b *Block) Bytes() []byte {
data := make([]byte, 0, 256)
data = append(data, b.parentID[:]...)
var u8 [8]byte
binary.BigEndian.PutUint64(u8[:], b.height)
data = append(data, u8[:]...)
binary.BigEndian.PutUint64(u8[:], uint64(b.timestamp.Unix()))
data = append(data, u8[:]...)
var u4 [4]byte
binary.BigEndian.PutUint32(u4[:], uint32(len(b.transactions)))
data = append(data, u4[:]...)
for _, tx := range b.transactions {
txb := tx.Bytes()
binary.BigEndian.PutUint32(u4[:], uint32(len(txb)))
data = append(data, u4[:]...)
data = append(data, txb...)
}
return data
}
func parseBlock(vm *VM, data []byte) (*Block, error) {
c := &cursor{b: data}
b := &Block{vm: vm}
parent, err := c.fixed(32)
if err != nil {
return nil, err
}
copy(b.parentID[:], parent)
if b.height, err = c.u64(); err != nil {
return nil, err
}
ts, err := c.u64()
if err != nil {
return nil, err
}
b.timestamp = time.Unix(int64(ts), 0)
cnt, err := c.fixed(4)
if err != nil {
return nil, err
}
n := int(binary.BigEndian.Uint32(cnt))
b.transactions = make([]*Transaction, 0, n)
for i := 0; i < n; i++ {
txb, err := c.bytes()
if err != nil {
return nil, err
}
tx, err := ParseTransaction(txb)
if err != nil {
return nil, err
}
b.transactions = append(b.transactions, tx)
}
b.id = b.computeID()
return b, nil
}
// Verify checks the block can be accepted WITHOUT mutating state: the parent
// exists, every transaction is well-formed and authenticated, and every payer
// can afford its fee — including the cumulative fees of multiple transactions
-120
View File
@@ -79,41 +79,6 @@ type RevokePayload struct {
Reason string `json:"reason"`
}
// putU16/putU32/putU64 append big-endian length-prefixed fields.
func putBytes(dst []byte, b []byte) []byte {
var l [4]byte
binary.BigEndian.PutUint32(l[:], uint32(len(b)))
dst = append(dst, l[:]...)
return append(dst, b...)
}
// SigningBytes is the deterministic encoding the payer signs. It binds every
// semantically meaningful field — including Payer — but excludes Auth and Sig.
// Because Payer is bound here and authenticate() requires Payer ==
// addressOf(Auth), an attacker cannot swap in a different public key.
func (tx *Transaction) SigningBytes() []byte {
b := make([]byte, 0, 128+len(tx.Payload))
b = append(b, tx.Type)
b = putBytes(b, []byte(tx.Algorithm))
b = append(b, tx.Payer[:]...)
b = append(b, tx.KeyID[:]...)
var u8 [8]byte
binary.BigEndian.PutUint64(u8[:], tx.GasLimit)
b = append(b, u8[:]...)
binary.BigEndian.PutUint64(u8[:], tx.Nonce)
b = append(b, u8[:]...)
b = putBytes(b, tx.Payload)
return b
}
// Bytes is the full wire encoding: SigningBytes followed by Auth and Sig.
func (tx *Transaction) Bytes() []byte {
b := tx.SigningBytes()
b = putBytes(b, tx.Auth)
b = putBytes(b, tx.Sig)
return b
}
// ID returns the transaction's content hash (over the full Bytes).
func (tx *Transaction) ID() ids.ID {
if tx.id == ids.Empty {
@@ -122,91 +87,6 @@ func (tx *Transaction) ID() ids.ID {
return tx.id
}
type cursor struct {
b []byte
off int
}
func (c *cursor) u8() (uint8, error) {
if c.off+1 > len(c.b) {
return 0, ErrInvalidPayload
}
v := c.b[c.off]
c.off++
return v, nil
}
func (c *cursor) fixed(n int) ([]byte, error) {
if c.off+n > len(c.b) {
return nil, ErrInvalidPayload
}
v := c.b[c.off : c.off+n]
c.off += n
return v, nil
}
func (c *cursor) u64() (uint64, error) {
v, err := c.fixed(8)
if err != nil {
return 0, err
}
return binary.BigEndian.Uint64(v), nil
}
func (c *cursor) bytes() ([]byte, error) {
lb, err := c.fixed(4)
if err != nil {
return nil, err
}
n := int(binary.BigEndian.Uint32(lb))
return c.fixed(n)
}
// ParseTransaction decodes a transaction from its wire encoding.
func ParseTransaction(data []byte) (*Transaction, error) {
c := &cursor{b: data}
tx := &Transaction{}
var err error
if tx.Type, err = c.u8(); err != nil {
return nil, err
}
algo, err := c.bytes()
if err != nil {
return nil, err
}
tx.Algorithm = string(algo)
payer, err := c.fixed(ids.ShortIDLen)
if err != nil {
return nil, err
}
copy(tx.Payer[:], payer)
keyID, err := c.fixed(32)
if err != nil {
return nil, err
}
copy(tx.KeyID[:], keyID)
if tx.GasLimit, err = c.u64(); err != nil {
return nil, err
}
if tx.Nonce, err = c.u64(); err != nil {
return nil, err
}
if tx.Payload, err = c.bytes(); err != nil {
return nil, err
}
if tx.Auth, err = c.bytes(); err != nil {
return nil, err
}
if tx.Sig, err = c.bytes(); err != nil {
return nil, err
}
if c.off != len(data) {
return nil, fmt.Errorf("keyvm: %w: trailing bytes", ErrInvalidPayload)
}
tx.id = ids.ID(sha256.Sum256(data))
return tx, nil
}
// addressOf derives a payer account from an ML-DSA public key. K is internally
// consistent: it derives the same address it checks a payer against. This is a
// PUBLIC, one-way derivation (no secret involved).
+263
View File
@@ -0,0 +1,263 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package keyvm
import (
"bytes"
"crypto/sha256"
"encoding/binary"
"fmt"
"time"
"github.com/luxfi/ids"
"github.com/luxfi/zap"
)
// Native-ZAP struct-is-wire for K-Chain (keyvm). No hand-rolled big-endian, no
// cursor codec — the Transaction and the Block each own their marshal/parse over
// zap objects at FIXED field offsets. Re-genesis authorized, so the on-wire
// format is exactly these offsets (canonical: parse rejects trailing bytes).
//
// SECURITY — the signing/authentication architecture is preserved byte-for-byte
// in meaning, only the encoding changes:
//
// - SigningBytes() is the deterministic preimage the payer SIGNS. It is one
// zap object binding every semantically-meaningful field (Type, Algorithm,
// Payer, KeyID, GasLimit, Nonce, Payload) and EXCLUDING Auth/Sig. Because
// Payer is bound here and authenticate() requires Payer == addressOf(Auth),
// an attacker cannot swap in a different public key.
//
// - Bytes() is SigningBytes() ‖ sigObject, where sigObject carries Auth+Sig.
// The signed bytes are therefore a genuine byte-prefix of the full wire (the
// node's proposervm signed-block idiom). ID() = sha256(Bytes()).
//
// - authenticate() re-derives SigningBytes() from the parsed fields and
// verifies the signature against it; a deterministic zap marshal guarantees
// the re-derived preimage equals the signed prefix.
// ---- Transaction signing object (the SIGNED preimage; excludes Auth/Sig) ----
//
// Type u8 @ 0
// Payer 20B @ 1
// KeyID 32B @ 21
// GasLimit u64 @ 53
// Nonce u64 @ 61
// Algorithm bytes @ 69
// Payload bytes @ 77
const (
txType = 0
txPayer = 1
txKeyID = 21
txGas = 53
txNonce = 61
txAlgo = 69
txPayld = 77
txSize = 85
)
// ---- Transaction sig object (appended after the signing object) ----
//
// Auth bytes @ 0
// Sig bytes @ 8
const (
sgAuth = 0
sgSig = 8
sgSize = 16
)
// SigningBytes is the deterministic encoding the payer signs. It binds every
// semantically meaningful field — including Payer — but excludes Auth and Sig.
func (tx *Transaction) SigningBytes() []byte {
b := zap.NewBuilder(zap.HeaderSize + txSize + len(tx.Algorithm) + len(tx.Payload) + 64)
ob := b.StartObject(txSize)
ob.SetUint8(txType, tx.Type)
ob.SetBytesFixed(txPayer, tx.Payer[:])
ob.SetBytesFixed(txKeyID, tx.KeyID[:])
ob.SetUint64(txGas, tx.GasLimit)
ob.SetUint64(txNonce, tx.Nonce)
ob.SetBytes(txAlgo, []byte(tx.Algorithm))
ob.SetBytes(txPayld, tx.Payload)
ob.FinishAsRoot()
return b.Finish()
}
// Bytes is the full wire encoding: the signing object followed by an appended
// zap object carrying Auth and Sig. The signing prefix is byte-identical to
// SigningBytes(), so the payer's signature covers exactly Bytes()[:zapLen].
func (tx *Transaction) Bytes() []byte {
signing := tx.SigningBytes()
sb := zap.NewBuilder(zap.HeaderSize + sgSize + len(tx.Auth) + len(tx.Sig) + 32)
so := sb.StartObject(sgSize)
so.SetBytes(sgAuth, tx.Auth)
so.SetBytes(sgSig, tx.Sig)
so.FinishAsRoot()
sig := sb.Finish()
out := make([]byte, 0, len(signing)+len(sig))
out = append(out, signing...)
out = append(out, sig...)
return out
}
// ParseTransaction decodes a transaction from its wire encoding: the leading
// signing object, then the appended Auth/Sig object. Canonical: rejects
// trailing bytes.
func ParseTransaction(data []byte) (*Transaction, error) {
n, err := zapLen(data)
if err != nil {
return nil, err
}
sm, err := zap.Parse(data[:n])
if err != nil {
return nil, err
}
gm, err := zap.Parse(data[n:])
if err != nil {
return nil, err
}
if n+gm.Size() != len(data) {
return nil, fmt.Errorf("keyvm: %w: trailing bytes", ErrInvalidPayload)
}
so := sm.Root()
go_ := gm.Root()
tx := &Transaction{
Type: so.Uint8(txType),
Algorithm: string(so.Bytes(txAlgo)),
GasLimit: so.Uint64(txGas),
Nonce: so.Uint64(txNonce),
Payload: appendBytes(so.Bytes(txPayld)),
Auth: appendBytes(go_.Bytes(sgAuth)),
Sig: appendBytes(go_.Bytes(sgSig)),
}
copy(tx.Payer[:], so.BytesFixedSlice(txPayer, ids.ShortIDLen))
copy(tx.KeyID[:], so.BytesFixedSlice(txKeyID, 32))
// Canonical wire: zap follows the root offset and ignores unreferenced
// padding inside a message's declared size, so distinct byte-strings can
// decode to identical fields. Bind the id to the RE-SERIALIZED (canonical)
// form and reject any input that is not already canonical — exactly one
// byte-string authenticates per logical tx (no id-malleability, fail-closed).
canonical := tx.Bytes()
if !bytes.Equal(data, canonical) {
return nil, fmt.Errorf("keyvm: %w: non-canonical tx encoding", ErrInvalidPayload)
}
tx.id = ids.ID(sha256.Sum256(canonical))
return tx, nil
}
// ---- Block ----
//
// ParentID 32B @ 0
// Height u64 @ 32
// Timestamp i64 @ 40 (Unix seconds — K-Chain's block-time resolution)
// TxLens list @ 48 (u32 per Transaction wire length)
// TxBlob bytes @ 56 (concatenated Transaction wire objects)
const (
blkParent = 0
blkHeight = 32
blkTime = 40
blkTxLens = 48
blkTxBlob = 56
blkSize = 64
)
// Bytes serializes the block (parent, height, timestamp, transactions).
func (b *Block) Bytes() []byte {
txLens := make([]uint32, len(b.transactions))
var txBlob []byte
for i, tx := range b.transactions {
txb := tx.Bytes()
txLens[i] = uint32(len(txb))
txBlob = append(txBlob, txb...)
}
bld := zap.NewBuilder(zap.HeaderSize + blkSize + len(txBlob) + 4*len(txLens) + 128)
txLensOff := writeU32List(bld, txLens)
ob := bld.StartObject(blkSize)
ob.SetBytesFixed(blkParent, b.parentID[:])
ob.SetUint64(blkHeight, b.height)
ob.SetInt64(blkTime, b.timestamp.Unix())
ob.SetList(blkTxLens, txLensOff, len(txLens))
ob.SetBytes(blkTxBlob, txBlob)
ob.FinishAsRoot()
return bld.Finish()
}
func parseBlock(vm *VM, data []byte) (*Block, error) {
msg, err := zap.Parse(data)
if err != nil {
return nil, err
}
if msg.Size() != len(data) {
return nil, fmt.Errorf("keyvm: %w: block trailing bytes", ErrInvalidPayload)
}
o := msg.Root()
b := &Block{vm: vm}
copy(b.parentID[:], o.BytesFixedSlice(blkParent, 32))
b.height = o.Uint64(blkHeight)
b.timestamp = time.Unix(o.Int64(blkTime), 0)
lens := readU32List(o, blkTxLens)
blob := o.Bytes(blkTxBlob)
b.transactions = make([]*Transaction, 0, len(lens))
pos := 0
for _, l := range lens {
if pos+int(l) > len(blob) {
return nil, fmt.Errorf("keyvm: %w: tx blob out of bounds", ErrInvalidPayload)
}
tx, err := ParseTransaction(blob[pos : pos+int(l)])
if err != nil {
return nil, err
}
b.transactions = append(b.transactions, tx)
pos += int(l)
}
b.id = b.computeID()
return b, nil
}
// ---- shared helpers ----
// zapLen returns the total length of the leading self-delimiting zap message in
// b (its header size field @[12:16]) — the split point between the signing
// prefix and the appended Auth/Sig object.
func zapLen(b []byte) (int, error) {
if len(b) < zap.HeaderSize {
return 0, fmt.Errorf("keyvm: %w: short buffer", ErrInvalidPayload)
}
n := int(binary.LittleEndian.Uint32(b[12:16]))
if n < zap.HeaderSize || n > len(b) {
return 0, fmt.Errorf("keyvm: %w: bad zap length", ErrInvalidPayload)
}
return n, nil
}
func writeU32List(b *zap.Builder, xs []uint32) int {
lb := b.StartList(4)
for _, x := range xs {
lb.AddUint32(x)
}
off, _ := lb.Finish()
return off
}
func readU32List(o zap.Object, ptrOff int) []uint32 {
l := o.ListStride(ptrOff, 4)
n := l.Len()
out := make([]uint32, n)
for i := 0; i < n; i++ {
out[i] = l.Uint32(i)
}
return out
}
func appendBytes(b []byte) []byte {
if len(b) == 0 {
return nil
}
return append([]byte(nil), b...)
}
+108
View File
@@ -0,0 +1,108 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package keyvm
import (
"crypto/sha256"
"encoding/binary"
"testing"
"github.com/luxfi/ids"
"github.com/luxfi/zap"
)
// sampleTx is a fully-populated transaction whose every field is non-empty, so
// the canonical round-trip exercises every offset.
func sampleTx() *Transaction {
tx := &Transaction{
Type: TxRegisterKey,
Algorithm: "ml-dsa-65",
KeyID: ids.ID{9, 8, 7, 6, 5, 4, 3, 2, 1},
GasLimit: 21000,
Nonce: 42,
Payload: []byte("register-key-payload"),
Auth: []byte("payer-public-key-bytes"),
Sig: []byte("payer-signature-bytes"),
}
copy(tx.Payer[:], []byte("payer-address-20byte"))
return tx
}
// TestWireCanonicalRoundTrip proves Bytes()→ParseTransaction is a faithful,
// idempotent round-trip and that the id is the content hash of the canonical
// wire (id == sha256(Bytes())).
func TestWireCanonicalRoundTrip(t *testing.T) {
tx := sampleTx()
data := tx.Bytes()
parsed, err := ParseTransaction(data)
if err != nil {
t.Fatalf("ParseTransaction(canonical) failed: %v", err)
}
if got := parsed.Bytes(); string(got) != string(data) {
t.Fatalf("re-serialization not canonical: parsed.Bytes() != input\n in =%x\n out=%x", data, got)
}
if want := ids.ID(sha256.Sum256(data)); parsed.ID() != want {
t.Fatalf("id != sha256(Bytes()): got %s want %s", parsed.ID(), want)
}
// Field fidelity across the seam.
if parsed.Type != tx.Type || parsed.Algorithm != tx.Algorithm ||
parsed.GasLimit != tx.GasLimit || parsed.Nonce != tx.Nonce ||
parsed.Payer != tx.Payer || parsed.KeyID != tx.KeyID ||
string(parsed.Payload) != string(tx.Payload) ||
string(parsed.Auth) != string(tx.Auth) || string(parsed.Sig) != string(tx.Sig) {
t.Fatalf("field mismatch after round-trip:\n in =%+v\n out=%+v", tx, parsed)
}
}
// TestWireRejectsNonCanonical is the M1 regression guard: zap follows the root
// offset and ignores unreferenced padding inside a message's declared size, so a
// twin buffer can decode to identical fields yet hash differently. The canonical
// gate must reject any input that is not already byte-equal to its re-serialized
// form — exactly one byte-string authenticates per logical tx (no id-malleability).
func TestWireRejectsNonCanonical(t *testing.T) {
tx := sampleTx()
data := tx.Bytes()
// Split the two concatenated zap messages (signing ‖ sig).
n, err := zapLen(data)
if err != nil {
t.Fatalf("zapLen: %v", err)
}
// Craft a non-canonical twin: append 8 unreferenced bytes to the trailing
// (sig) message and bump its declared size field so the old
// n+gm.Size()==len(data) trailing-bytes check still passes. The root offset
// does not reference the padding, so the fields decode identically.
sig := append([]byte(nil), data[n:]...)
sig = append(sig, make([]byte, 8)...)
binary.LittleEndian.PutUint32(sig[12:16], uint32(len(sig)))
twin := append(append([]byte(nil), data[:n]...), sig...)
if string(twin) == string(data) {
t.Fatal("twin construction failed to differ from canonical")
}
// Sanity: the twin's trailing sub-message still self-reports the padded size
// (so a naive size==len check would accept it) — the property M1 closes.
if got := int(binary.LittleEndian.Uint32(twin[n+12 : n+16])); got != len(sig) {
t.Fatalf("twin size field not bumped: got %d want %d", got, len(sig))
}
if _, err := ParseTransaction(twin); err == nil {
t.Fatal("ParseTransaction accepted a non-canonical (padded) twin — id-malleability not closed")
}
}
// TestWireParseRejectsTruncated confirms the parser fails closed on a short buffer.
func TestWireParseRejectsTruncated(t *testing.T) {
data := sampleTx().Bytes()
for _, cut := range []int{0, 4, zap.HeaderSize, len(data) - 1} {
if _, err := ParseTransaction(data[:cut]); err == nil {
t.Fatalf("ParseTransaction accepted truncated buffer of len %d", cut)
}
}
}
+1 -38
View File
@@ -9,8 +9,8 @@ import (
"errors"
"time"
"github.com/luxfi/ids"
"github.com/luxfi/chains/quantumvm/quantum"
"github.com/luxfi/ids"
)
var (
@@ -169,43 +169,6 @@ func (b *Block) Status() uint8 {
return 0
}
// Bytes returns the block bytes
func (b *Block) Bytes() []byte {
if b.bytes != nil {
return b.bytes
}
// Serialize block
size := 32 + 8 + 8 + 32 + 4 // id + timestamp + height + parentID + tx count
for _, tx := range b.transactions {
size += len(tx.Bytes())
}
bytes := make([]byte, 0, size)
bytes = append(bytes, b.id[:]...)
timestampBytes := make([]byte, 8)
binary.BigEndian.PutUint64(timestampBytes, uint64(b.timestamp.Unix()))
bytes = append(bytes, timestampBytes...)
heightBytes := make([]byte, 8)
binary.BigEndian.PutUint64(heightBytes, b.height)
bytes = append(bytes, heightBytes...)
bytes = append(bytes, b.parentID[:]...)
txCountBytes := make([]byte, 4)
binary.BigEndian.PutUint32(txCountBytes, uint32(len(b.transactions)))
bytes = append(bytes, txCountBytes...)
for _, tx := range b.transactions {
bytes = append(bytes, tx.Bytes()...)
}
b.bytes = bytes
return bytes
}
// String returns a string representation of the block
func (b *Block) String() string {
return b.id.String()
+7 -22
View File
@@ -4,14 +4,14 @@
package quantumvm
import (
"encoding/binary"
"crypto/sha256"
"errors"
"sync"
"time"
"github.com/luxfi/chains/quantumvm/quantum"
"github.com/luxfi/ids"
"github.com/luxfi/log"
"github.com/luxfi/chains/quantumvm/quantum"
)
// Transaction represents a QVM transaction
@@ -37,32 +37,17 @@ type BaseTransaction struct {
quantumSignature *quantum.QuantumSignature
}
// ID returns the transaction ID
// ID returns the transaction ID — the content hash of the canonical ZAP wire
// (sha256(Bytes())), matching the other VMs in this repo. The prior
// ids.ToID(Bytes()) required an exactly-32-byte input and silently yielded
// ids.Empty for the always-≥32-byte wire, collapsing every tx to one pool slot.
func (tx *BaseTransaction) ID() ids.ID {
if tx.id == ids.Empty {
tx.id, _ = ids.ToID(tx.Bytes())
tx.id = ids.ID(sha256.Sum256(tx.Bytes()))
}
return tx.id
}
// Bytes returns the transaction bytes
func (tx *BaseTransaction) Bytes() []byte {
size := 8 + 8 + len(tx.data) // timestamp + nonce + data
bytes := make([]byte, 0, size)
timestampBytes := make([]byte, 8)
binary.BigEndian.PutUint64(timestampBytes, uint64(tx.timestamp.Unix()))
bytes = append(bytes, timestampBytes...)
nonceBytes := make([]byte, 8)
binary.BigEndian.PutUint64(nonceBytes, tx.nonce)
bytes = append(bytes, nonceBytes...)
bytes = append(bytes, tx.data...)
return bytes
}
// GetQuantumSignature returns the quantum signature
func (tx *BaseTransaction) GetQuantumSignature() *quantum.QuantumSignature {
return tx.quantumSignature
+16 -46
View File
@@ -10,9 +10,10 @@ import (
"fmt"
"net/http"
"sync"
"time"
"github.com/gorilla/rpc/v2"
"github.com/luxfi/chains/quantumvm/config"
"github.com/luxfi/chains/quantumvm/quantum"
consensuschain "github.com/luxfi/consensus/engine/chain"
"github.com/luxfi/consensus/protocol/quasar"
"github.com/luxfi/database"
@@ -23,10 +24,8 @@ import (
"github.com/luxfi/node/cache"
"github.com/luxfi/node/utils/json"
"github.com/luxfi/node/vms/types/fee"
"github.com/luxfi/version"
"github.com/luxfi/chains/quantumvm/config"
"github.com/luxfi/chains/quantumvm/quantum"
"github.com/luxfi/timer/mockable"
"github.com/luxfi/version"
luxvm "github.com/luxfi/vm"
"github.com/luxfi/vm/chain"
)
@@ -264,24 +263,18 @@ func (vm *VM) BuildBlock(ctx context.Context) (chain.Block, error) {
return nil, fmt.Errorf("failed to process transactions: %w", err)
}
// Create new block with valid transactions
// Generate block ID from block data
blockData := make([]byte, 0, 100)
lastAccepted := vm.getLastAcceptedID()
blockData = append(blockData, lastAccepted[:]...)
heightBytes := make([]byte, 8)
binary.BigEndian.PutUint64(heightBytes, vm.getHeight()+1)
blockData = append(blockData, heightBytes...)
blockID, _ := ids.ToID(blockData)
// Create new block with valid transactions. The id is the content hash of
// the canonical wire (computeID = sha256(Bytes())); it is derived AFTER the
// structural fields are set, and the wire does not carry the id, so
// Bytes()/id are independent of ordering.
block := &Block{
id: blockID,
timestamp: vm.clock.Time(),
height: vm.getHeight() + 1,
parentID: vm.getLastAcceptedID(),
transactions: validTxs,
vm: vm,
}
block.id = block.computeID()
// Sign block with quantum signature if enabled
if vm.Config.QuantumStampEnabled {
@@ -481,34 +474,11 @@ func (vm *VM) parseGenesis(genesisBytes []byte) error {
return nil
}
// parseBlock parses a block from bytes
// parseBlock parses a block from its ZAP wire (see wire.go). The transaction set
// is not reconstructed — the concrete Transaction is an interface with no
// on-chain deserializer; the raw wire is retained for signature re-verification.
func (vm *VM) parseBlock(blockBytes []byte) (*Block, error) {
// Block format: id(32) + timestamp(8) + height(8) + parentID(32) + txCount(4) + txs...
minSize := 32 + 8 + 8 + 32 + 4
if len(blockBytes) < minSize {
return nil, fmt.Errorf("block bytes too short: got %d, need at least %d", len(blockBytes), minSize)
}
var id ids.ID
copy(id[:], blockBytes[:32])
timestamp := binary.BigEndian.Uint64(blockBytes[32:40])
height := binary.BigEndian.Uint64(blockBytes[40:48])
var parentID ids.ID
copy(parentID[:], blockBytes[48:80])
txCount := binary.BigEndian.Uint32(blockBytes[80:84])
_ = txCount // Transaction parsing would go here
return &Block{
id: id,
timestamp: time.Unix(int64(timestamp), 0),
height: height,
parentID: parentID,
vm: vm,
bytes: blockBytes,
}, nil
return parseBlockBytes(vm, blockBytes)
}
// initializeHTTPHandlers sets up HTTP handlers
@@ -580,10 +550,10 @@ func (vm *VM) HealthCheck(ctx context.Context) (chain.HealthResult, error) {
return chain.HealthResult{
Healthy: !vm.isShuttingDown(),
Details: map[string]string{
"version": Version,
"quantumEnabled": fmt.Sprintf("%v", vm.Config.QuantumStampEnabled),
"coronaEnabled": fmt.Sprintf("%v", vm.Config.CoronaEnabled),
"pendingTxs": fmt.Sprintf("%d", vm.txPool.PendingCount()),
"version": Version,
"quantumEnabled": fmt.Sprintf("%v", vm.Config.QuantumStampEnabled),
"coronaEnabled": fmt.Sprintf("%v", vm.Config.CoronaEnabled),
"pendingTxs": fmt.Sprintf("%d", vm.txPool.PendingCount()),
},
}, nil
}
+141
View File
@@ -0,0 +1,141 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package quantumvm
import (
"crypto/sha256"
"fmt"
"time"
"github.com/luxfi/ids"
"github.com/luxfi/zap"
)
// Native-ZAP struct-is-wire for Q-Chain (quantumvm). No hand-rolled big-endian —
// the Block and BaseTransaction own their serialization over zap objects at
// FIXED field offsets. Re-genesis authorized, so the on-wire format is exactly
// these offsets (canonical: parse rejects trailing bytes).
//
// The Block wire is the quantum-signature preimage: BuildBlock/Verify sign and
// re-check quantumSigner over Block.Bytes(), so the encoding must be
// deterministic (it is: fixed offsets, ordered tx list). It carries the
// structural fields and the committed transaction bytes, so the block signature
// commits to every transaction. The block id is NOT stored in the wire: it is
// the content hash sha256(Bytes()) (see computeID), matching keyvm/dexvm/aivm —
// one and only one id derivation across every VM in this repo, and the clean
// invariant id == sha256(Bytes()) holds.
//
// parseBlock restores the structural fields (timestamp/height/parentID), derives
// the id from the bytes, and leaves transactions unpopulated: the concrete
// Transaction is an interface with no on-chain deserializer, so the tx blob rides
// the wire for signature coverage only — this preserves the prior parseBlock
// contract for the tx set.
// ---- Block ----
//
// Timestamp i64 @ 0 (Unix seconds — Q-Chain block-time resolution)
// Height u64 @ 8
// ParentID 32B @ 16
// TxLens list @ 48 (u32 per Transaction wire length)
// TxBlob bytes @ 56 (concatenated Transaction wire bytes)
const (
blkTime = 0
blkHeight = 8
blkParent = 16
blkTxLens = 48
blkTxBlob = 56
blkSize = 64
)
// Bytes returns the block's canonical ZAP wire (cached).
func (b *Block) Bytes() []byte {
if b.bytes != nil {
return b.bytes
}
txLens := make([]uint32, len(b.transactions))
var txBlob []byte
for i, tx := range b.transactions {
txb := tx.Bytes()
txLens[i] = uint32(len(txb))
txBlob = append(txBlob, txb...)
}
bld := zap.NewBuilder(zap.HeaderSize + blkSize + len(txBlob) + 4*len(txLens) + 128)
txLensOff := writeU32List(bld, txLens)
ob := bld.StartObject(blkSize)
ob.SetInt64(blkTime, b.timestamp.Unix())
ob.SetUint64(blkHeight, b.height)
ob.SetBytesFixed(blkParent, b.parentID[:])
ob.SetList(blkTxLens, txLensOff, len(txLens))
ob.SetBytes(blkTxBlob, txBlob)
ob.FinishAsRoot()
b.bytes = bld.Finish()
return b.bytes
}
// computeID is the block's content id: sha256 over the canonical ZAP wire. The
// id is not stored in the wire, so this invariant holds unconditionally —
// id == sha256(Bytes()) — identically to every other VM in this repo.
func (b *Block) computeID() ids.ID {
return ids.ID(sha256.Sum256(b.Bytes()))
}
// parseBlockBytes decodes a block's structural fields from its ZAP wire. The
// transaction set is intentionally not reconstructed (see file header); the raw
// wire is retained in b.bytes so the quantum signature can be re-verified over
// the exact bytes.
func parseBlockBytes(vm *VM, data []byte) (*Block, error) {
msg, err := zap.Parse(data)
if err != nil {
return nil, err
}
if msg.Size() != len(data) {
return nil, fmt.Errorf("quantumvm: block trailing bytes")
}
o := msg.Root()
b := &Block{vm: vm, bytes: data}
b.timestamp = time.Unix(o.Int64(blkTime), 0)
b.height = o.Uint64(blkHeight)
copy(b.parentID[:], o.BytesFixedSlice(blkParent, 32))
b.id = ids.ID(sha256.Sum256(data))
return b, nil
}
// ---- BaseTransaction ----
//
// Timestamp i64 @ 0 (Unix seconds)
// Nonce u64 @ 8
// Data bytes @ 16
const (
txTime = 0
txNonce = 8
txData = 16
txSize = 24
)
// Bytes returns the transaction's canonical ZAP wire. It excludes the quantum
// signature (which signs over exactly these bytes).
func (tx *BaseTransaction) Bytes() []byte {
b := zap.NewBuilder(zap.HeaderSize + txSize + len(tx.data) + 32)
ob := b.StartObject(txSize)
ob.SetInt64(txTime, tx.timestamp.Unix())
ob.SetUint64(txNonce, tx.nonce)
ob.SetBytes(txData, tx.data)
ob.FinishAsRoot()
return b.Finish()
}
// ---- shared helpers ----
func writeU32List(b *zap.Builder, xs []uint32) int {
lb := b.StartList(4)
for _, x := range xs {
lb.AddUint32(x)
}
off, _ := lb.Finish()
return off
}
+122
View File
@@ -0,0 +1,122 @@
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package quantumvm
import (
"crypto/sha256"
"testing"
"time"
"github.com/luxfi/ids"
"github.com/stretchr/testify/require"
)
func sampleBlock() *Block {
blk := &Block{
timestamp: time.Unix(1_700_000_500, 0),
height: 9,
parentID: ids.GenerateTestID(),
transactions: []Transaction{
&BaseTransaction{timestamp: time.Unix(1_700_000_400, 0), nonce: 1, data: []byte("op-a")},
&BaseTransaction{timestamp: time.Unix(1_700_000_450, 0), nonce: 2, data: []byte("op-bb")},
},
}
blk.id = blk.computeID()
return blk
}
func TestBlockWireRoundTrip(t *testing.T) {
require := require.New(t)
blk := sampleBlock()
wire := blk.Bytes()
require.NotEmpty(wire)
got, err := parseBlockBytes(nil, wire)
require.NoError(err)
require.Equal(blk.id, got.id)
require.Equal(blk.timestamp.Unix(), got.timestamp.Unix())
require.Equal(blk.height, got.height)
require.Equal(blk.parentID, got.parentID)
// The transaction bytes are committed to the wire (block signature covers
// them), even though parse does not reconstruct the concrete txs.
require.Nil(got.transactions, "parse preserves the tx-lossy contract")
empty := (&Block{timestamp: blk.timestamp, height: blk.height, parentID: blk.parentID}).Bytes()
require.Greater(len(wire), len(empty), "committed tx bytes are present in the block wire")
// re-parse of the retained bytes is stable (signature re-verification path)
require.Equal(wire, got.Bytes())
}
// TestBlockIDIsContentHash is the M2 regression guard: the block id is the
// content hash sha256(Bytes()) and is NOT stored in the wire — so it is never
// ids.Empty (the prior ids.ToID(parent‖height) over 40 bytes silently yielded
// Empty for every block, collapsing the block store to one slot).
func TestBlockIDIsContentHash(t *testing.T) {
require := require.New(t)
blk := sampleBlock()
require.NotEqual(ids.Empty, blk.id, "block id must never be ids.Empty")
require.Equal(ids.ID(sha256.Sum256(blk.Bytes())), blk.id, "id == sha256(Bytes())")
// A round-tripped block recovers the same content id from the bytes alone.
got, err := parseBlockBytes(nil, blk.Bytes())
require.NoError(err)
require.Equal(blk.id, got.id)
// Distinct content ⇒ distinct ids (height, parent, and tx set each move it).
byHeight := &Block{timestamp: blk.timestamp, height: blk.height + 1, parentID: blk.parentID, transactions: blk.transactions}
byParent := &Block{timestamp: blk.timestamp, height: blk.height, parentID: ids.GenerateTestID(), transactions: blk.transactions}
byTx := &Block{timestamp: blk.timestamp, height: blk.height, parentID: blk.parentID,
transactions: []Transaction{&BaseTransaction{timestamp: blk.timestamp, nonce: 99, data: []byte("different")}}}
seen := map[ids.ID]string{blk.id: "base"}
for _, c := range []*Block{byHeight, byParent, byTx} {
id := c.computeID()
require.NotEqual(ids.Empty, id)
require.NotContains(seen, id, "distinct block content produced a colliding id")
seen[id] = "variant"
}
}
func TestBlockWireRejectsTrailing(t *testing.T) {
blk := &Block{timestamp: time.Unix(1, 0), height: 1, parentID: ids.GenerateTestID()}
wire := blk.Bytes()
_, err := parseBlockBytes(nil, append(append([]byte(nil), wire...), 0x00))
require.Error(t, err, "trailing bytes must be rejected")
}
func TestBaseTransactionWireDeterministic(t *testing.T) {
require := require.New(t)
mk := func() *BaseTransaction {
return &BaseTransaction{timestamp: time.Unix(1_700_000_000, 0), nonce: 42, data: []byte("payload")}
}
a := mk().Bytes()
b := mk().Bytes()
require.Equal(a, b, "tx wire is deterministic (stable id + signature preimage)")
require.NotEmpty(a)
// The signature is NOT part of the signed bytes.
signed := &BaseTransaction{timestamp: time.Unix(1_700_000_000, 0), nonce: 42, data: []byte("payload")}
signed.quantumSignature = nil
require.Equal(a, signed.Bytes(), "quantum signature is excluded from tx wire")
}
// TestBaseTransactionIDNonEmpty is the M2 regression guard for the tx pool: the
// tx id is sha256(Bytes()) and never ids.Empty, so distinct txs occupy distinct
// pool slots (the prior ids.ToID over the always-≥32-byte wire yielded Empty for
// every tx, collapsing the pool to a single entry).
func TestBaseTransactionIDNonEmpty(t *testing.T) {
require := require.New(t)
tx := &BaseTransaction{timestamp: time.Unix(1_700_000_000, 0), nonce: 42, data: []byte("payload")}
id := tx.ID()
require.NotEqual(ids.Empty, id, "tx id must never be ids.Empty")
require.Equal(ids.ID(sha256.Sum256(tx.Bytes())), id, "id == sha256(Bytes())")
other := &BaseTransaction{timestamp: time.Unix(1_700_000_000, 0), nonce: 43, data: []byte("payload")}
require.NotEqual(id, other.ID(), "distinct txs must have distinct ids")
}