Files
chains/quantumvm/transaction.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

273 lines
6.8 KiB
Go

// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package quantumvm
import (
"crypto/sha256"
"errors"
"sync"
"time"
"github.com/luxfi/chains/quantumvm/quantum"
"github.com/luxfi/ids"
"github.com/luxfi/log"
)
// Transaction represents a QVM transaction
type Transaction interface {
ID() ids.ID
Bytes() []byte
Verify() error
Execute() error
GetQuantumSignature() *quantum.QuantumSignature
Timestamp() time.Time
// Fee returns the user-paid tx burn in nLUX. Must satisfy the
// chain's FeePolicy (>= fee.MinTxFeeFloor on Q-Chain).
Fee() uint64
}
// BaseTransaction provides common transaction functionality
type BaseTransaction struct {
id ids.ID
timestamp time.Time
nonce uint64
data []byte
fee uint64
quantumSignature *quantum.QuantumSignature
}
// 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.ID(sha256.Sum256(tx.Bytes()))
}
return tx.id
}
// GetQuantumSignature returns the quantum signature
func (tx *BaseTransaction) GetQuantumSignature() *quantum.QuantumSignature {
return tx.quantumSignature
}
// Timestamp returns the transaction timestamp
func (tx *BaseTransaction) Timestamp() time.Time {
return tx.timestamp
}
// Fee returns the user-paid tx burn (nLUX). Defaults to zero on a
// zero-value BaseTransaction — callers must set it explicitly for the
// fee gate to accept the tx.
func (tx *BaseTransaction) Fee() uint64 {
return tx.fee
}
// SetFee sets the user-paid tx burn in nLUX. Convenience setter for
// tests + tx builders; production callers should pass the fee at
// construction time.
func (tx *BaseTransaction) SetFee(f uint64) { tx.fee = f }
// Verify verifies the transaction
func (tx *BaseTransaction) Verify() error {
if tx.quantumSignature == nil {
return errors.New("missing quantum signature")
}
return nil
}
// Execute executes the transaction
func (tx *BaseTransaction) Execute() error {
// Implementation depends on transaction type
return nil
}
// TransactionPool manages pending transactions
type TransactionPool struct {
pending map[ids.ID]Transaction
queue []Transaction
maxSize int
batchSize int
log log.Logger
mu sync.RWMutex
closed bool
closeChan chan struct{}
}
// NewTransactionPool creates a new transaction pool
func NewTransactionPool(maxSize, batchSize int, logger log.Logger) *TransactionPool {
return &TransactionPool{
pending: make(map[ids.ID]Transaction),
queue: make([]Transaction, 0, maxSize),
maxSize: maxSize,
batchSize: batchSize,
log: logger,
closeChan: make(chan struct{}),
}
}
// AddTransaction adds a transaction to the pool
func (p *TransactionPool) AddTransaction(tx Transaction) error {
p.mu.Lock()
defer p.mu.Unlock()
if p.closed {
return errors.New("pool is closed")
}
if len(p.pending) >= p.maxSize {
return errors.New("pool is full")
}
txID := tx.ID()
if _, exists := p.pending[txID]; exists {
return errors.New("transaction already exists")
}
// Verify transaction
if err := tx.Verify(); err != nil {
return err
}
p.pending[txID] = tx
p.queue = append(p.queue, tx)
return nil
}
// RemoveTransaction removes a transaction from the pool
func (p *TransactionPool) RemoveTransaction(txID ids.ID) error {
p.mu.Lock()
defer p.mu.Unlock()
if _, exists := p.pending[txID]; !exists {
return errors.New("transaction not found")
}
delete(p.pending, txID)
// Remove from queue
newQueue := make([]Transaction, 0, len(p.queue)-1)
for _, tx := range p.queue {
if tx.ID() != txID {
newQueue = append(newQueue, tx)
}
}
p.queue = newQueue
return nil
}
// GetPendingTransactions returns pending transactions up to the limit
func (p *TransactionPool) GetPendingTransactions(limit int) []Transaction {
p.mu.RLock()
defer p.mu.RUnlock()
if limit <= 0 || limit > len(p.queue) {
limit = len(p.queue)
}
txs := make([]Transaction, limit)
copy(txs, p.queue[:limit])
return txs
}
// PendingCount returns the number of pending transactions
func (p *TransactionPool) PendingCount() int {
p.mu.RLock()
defer p.mu.RUnlock()
return len(p.pending)
}
// Close closes the transaction pool
func (p *TransactionPool) Close() {
p.mu.Lock()
defer p.mu.Unlock()
if !p.closed {
p.closed = true
close(p.closeChan)
p.pending = nil
p.queue = nil
}
}
// TransactionWorker processes transactions in parallel
type TransactionWorker struct {
vm *VM
quantumSigner *quantum.QuantumSigner
}
// ProcessBatch processes a batch of transactions.
// Uses GPU batch ML-DSA verification when available and batch is large enough.
func (w *TransactionWorker) ProcessBatch(txs []Transaction) ([]Transaction, error) {
// Phase 1: basic validation (no crypto)
var verified []Transaction
for _, tx := range txs {
if err := tx.Verify(); err != nil {
w.vm.log.Debug("transaction verification failed", "txID", tx.ID(), "error", err)
continue
}
if w.vm.Config.QuantumStampEnabled && tx.GetQuantumSignature() == nil {
w.vm.log.Debug("missing quantum signature", "txID", tx.ID())
continue
}
verified = append(verified, tx)
}
if len(verified) == 0 {
return nil, nil
}
// Phase 2: quantum signature verification (GPU batch when possible)
sigValid := make([]bool, len(verified))
if w.vm.Config.QuantumStampEnabled {
msgs := make([][]byte, len(verified))
sigs := make([]*quantum.QuantumSignature, len(verified))
for i, tx := range verified {
msgs[i] = tx.Bytes()
sigs[i] = tx.GetQuantumSignature()
}
// ParallelVerifyWithThreshold picks GPU or CPU automatically
err := w.quantumSigner.ParallelVerifyWithThreshold(msgs, sigs, w.vm.Config.GPUBatchThreshold)
if err == nil {
// All passed
for i := range sigValid {
sigValid[i] = true
}
} else {
// Batch failed -- verify individually to find which ones are bad
for i := range verified {
if verr := w.quantumSigner.Verify(msgs[i], sigs[i]); verr == nil {
sigValid[i] = true
} else {
w.vm.log.Debug("quantum signature verification failed", "txID", verified[i].ID(), "error", verr)
}
}
}
} else {
for i := range sigValid {
sigValid[i] = true
}
}
// Phase 3: execute valid transactions
validTxs := make([]Transaction, 0, len(verified))
for i, tx := range verified {
if !sigValid[i] {
continue
}
if err := tx.Execute(); err != nil {
w.vm.log.Debug("transaction execution failed", "txID", tx.ID(), "error", err)
continue
}
validTxs = append(validTxs, tx)
}
return validTxs, nil
}