mirror of
https://github.com/luxfi/chains.git
synced 2026-09-24 00:26:00 +00:00
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]>
273 lines
6.8 KiB
Go
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
|
|
}
|