Files
chains/quantumvm/wire_test.go
T
zeekayandHanzo Dev 9ec41cf150 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 <[email protected]>
2026-07-16 13:44:24 -07:00

123 lines
4.7 KiB
Go

// 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")
}