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