mirror of
https://github.com/luxfi/chains.git
synced 2026-09-24 00:26:00 +00:00
quantumvm: a block carries no signature of its own
ParseBlock demanded a field the wire has no room for. The block format is timestamp, height, parentID, tx lengths, tx blob; the stamp lived only on the object the builder made and was gone the moment the block was serialized, which is every time it leaves the process. So the gate refused every block that ever crossed the wire, genesis included, and Q-Chain accepted nothing on any network for as long as it ran -- certAccepted=0, appliedHead=0, everywhere. The two doors into the format disagreed, which is what made it a loop rather than a stall. A responder answers a catch-up request through vm.parseBlock, which has no gate and succeeds; the requester admits the reply through VM.ParseBlock, which had the gate and failed. Same bytes, two verdicts: every node served the block every node refused. Putting the stamp on the wire would be worse than leaving it off. The block id is sha256 of its own bytes, so a stamp in those bytes makes the id depend on who signed, and two honest nodes compute two ids for one block. seedGenesis already refuses wall-clock time for exactly this reason. The stamp could not have been load-bearing anyway: it was made with a key generated fresh for each block, and a signature that ships its own throwaway public key states nothing an attacker cannot also state. Its thirty-second validity window is also the opposite of what catch-up needs, which is to serve blocks from hours ago. A block-level signature belongs to consensus, where it already is: a cert is a quorum's statement about a block, checkable by anyone holding the validator set, and it is not a field on the block. Quasar still signs there. Transaction-level quantum signatures are untouched -- those are serialized, and they are verified. The tests stayed green throughout because none of them sent a block through Bytes() and back. Three do now: parse what we serialize, parse GENESIS, and the two parse paths reach the same verdict on identical bytes. Restoring the gate fails all three. Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
@@ -26,7 +26,6 @@ type Block struct {
|
||||
height uint64
|
||||
parentID ids.ID
|
||||
transactions []Transaction
|
||||
quantumSignature *quantum.QuantumSignature
|
||||
vm *VM
|
||||
bytes []byte
|
||||
}
|
||||
@@ -107,16 +106,6 @@ func (b *Block) Verify(ctx context.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Verify quantum signature if enabled
|
||||
if b.vm.Config.QuantumStampEnabled {
|
||||
if b.quantumSignature == nil {
|
||||
return errInvalidQuantumStamp
|
||||
}
|
||||
if err := b.vm.quantumSigner.Verify(b.Bytes(), b.quantumSignature); err != nil {
|
||||
return errBlockVerificationFailed
|
||||
}
|
||||
}
|
||||
|
||||
// Verify transactions in parallel
|
||||
if len(b.transactions) > 0 {
|
||||
msgs := make([][]byte, len(b.transactions))
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package quantumvm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/chains/quantumvm/config"
|
||||
"github.com/luxfi/ids"
|
||||
)
|
||||
|
||||
// parseVM is a VM with the stock config — the one production runs — and enough
|
||||
// wiring to serialize and parse. Building the whole VM would test the
|
||||
// constructor, not the property.
|
||||
func parseVM(t *testing.T) *VM {
|
||||
t.Helper()
|
||||
vm, _ := seedVM(t)
|
||||
vm.Config = config.DefaultConfig()
|
||||
return vm
|
||||
}
|
||||
|
||||
// TestParseAcceptsWhatWeSerialize is the property this chain could not move
|
||||
// without, and the one nothing asserted.
|
||||
//
|
||||
// ParseBlock is the sole entry point for catch-up, gossip and Put. If it refuses
|
||||
// the bytes this VM itself produces, then every node serves a block every node
|
||||
// refuses, and the chain accepts nothing for as long as it runs — which is what
|
||||
// happened: certAccepted=0, appliedHead=0 on every validator, on every network,
|
||||
// forever, while the package's own tests stayed green because none of them ever
|
||||
// sent a block through Bytes() and back.
|
||||
func TestParseAcceptsWhatWeSerialize(t *testing.T) {
|
||||
vm := parseVM(t)
|
||||
|
||||
blk := &Block{
|
||||
timestamp: time.Unix(1000, 0).UTC(),
|
||||
height: 7,
|
||||
parentID: ids.GenerateTestID(),
|
||||
vm: vm,
|
||||
}
|
||||
blk.id = blk.computeID()
|
||||
|
||||
got, err := vm.ParseBlock(context.Background(), blk.Bytes())
|
||||
if err != nil {
|
||||
t.Fatalf("ParseBlock refused bytes this VM serialized: %v\n"+
|
||||
"a block every node serves and every node refuses is a chain that cannot move", err)
|
||||
}
|
||||
if got.ID() != blk.id {
|
||||
t.Fatalf("round trip changed the block id: %s -> %s", blk.id, got.ID())
|
||||
}
|
||||
if got.Height() != 7 {
|
||||
t.Fatalf("round trip changed height: %d", got.Height())
|
||||
}
|
||||
}
|
||||
|
||||
// TestGenesisParses: genesis is the block every node must parse, and it is
|
||||
// signed by nobody — seedGenesis deliberately writes no signature, because a
|
||||
// per-node signature would give each node a different genesis id. Any gate that
|
||||
// demands one therefore refuses the one block the whole chain is anchored to.
|
||||
func TestGenesisParses(t *testing.T) {
|
||||
vm := parseVM(t)
|
||||
if err := vm.seedGenesis(); err != nil {
|
||||
t.Fatalf("seedGenesis: %v", err)
|
||||
}
|
||||
gid := vm.getLastAcceptedID()
|
||||
raw, err := vm.state.Get(gid[:])
|
||||
if err != nil {
|
||||
t.Fatalf("read genesis bytes: %v", err)
|
||||
}
|
||||
got, err := vm.ParseBlock(context.Background(), raw)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseBlock refused GENESIS: %v", err)
|
||||
}
|
||||
if got.Height() != 0 {
|
||||
t.Fatalf("genesis height = %d, want 0", got.Height())
|
||||
}
|
||||
}
|
||||
|
||||
// TestBothParsePathsAgree pins the asymmetry itself.
|
||||
//
|
||||
// The responder answers a catch-up request through vm.parseBlock; the requester
|
||||
// admits the reply through VM.ParseBlock. They are two doors into one format, so
|
||||
// a check on only one of them means the fleet serves what it will not accept.
|
||||
// Whatever either door does to a block, both must reach the same verdict.
|
||||
func TestBothParsePathsAgree(t *testing.T) {
|
||||
vm := parseVM(t)
|
||||
|
||||
blk := &Block{
|
||||
timestamp: time.Unix(2000, 0).UTC(),
|
||||
height: 3,
|
||||
parentID: ids.GenerateTestID(),
|
||||
vm: vm,
|
||||
}
|
||||
blk.id = blk.computeID()
|
||||
raw := blk.Bytes()
|
||||
|
||||
_, servErr := vm.parseBlock(raw) // responder side
|
||||
_, recvErr := vm.ParseBlock(context.Background(), raw) // requester side
|
||||
|
||||
if (servErr == nil) != (recvErr == nil) {
|
||||
t.Fatalf("the two parse paths disagree on identical bytes: serve=%v receive=%v\n"+
|
||||
"every node would serve this block and every node would refuse it", servErr, recvErr)
|
||||
}
|
||||
}
|
||||
@@ -64,7 +64,7 @@ func (s *Service) GetBlock(r *http.Request, args *GetBlockArgs, reply *GetBlockR
|
||||
reply.Height = qBlock.height
|
||||
reply.Timestamp = qBlock.timestamp.Unix()
|
||||
reply.TxCount = len(qBlock.transactions)
|
||||
reply.QuantumSig = qBlock.quantumSignature != nil
|
||||
reply.QuantumSig = false // blocks carry no stamp; see signBlockWithQuasar
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
+18
-53
@@ -282,13 +282,7 @@ func (vm *VM) BuildBlock(ctx context.Context) (chain.Block, error) {
|
||||
vm: vm,
|
||||
}
|
||||
block.id = block.computeID()
|
||||
|
||||
// Sign block with quantum signature if enabled
|
||||
if vm.Config.QuantumStampEnabled {
|
||||
if err := vm.signBlockWithQuantum(block); err != nil {
|
||||
return nil, fmt.Errorf("failed to sign block with quantum stamp: %w", err)
|
||||
}
|
||||
}
|
||||
vm.signBlockWithQuasar(block)
|
||||
|
||||
vm.log.Debug("built block",
|
||||
"blockID", block.ID(),
|
||||
@@ -309,13 +303,6 @@ func (vm *VM) ParseBlock(ctx context.Context, blockBytes []byte) (chain.Block, e
|
||||
return nil, fmt.Errorf("failed to parse block: %w", err)
|
||||
}
|
||||
|
||||
// Verify quantum signature if enabled
|
||||
if vm.Config.QuantumStampEnabled {
|
||||
if err := vm.verifyBlockQuantumSignature(block); err != nil {
|
||||
return nil, fmt.Errorf("quantum signature verification failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return block, nil
|
||||
}
|
||||
|
||||
@@ -430,47 +417,25 @@ func (vm *VM) processTransactionsParallel(txs []Transaction) ([]Transaction, err
|
||||
return validTxs, nil
|
||||
}
|
||||
|
||||
// signBlockWithQuantum signs a block with quantum signature using Quasar hybrid consensus
|
||||
func (vm *VM) signBlockWithQuantum(block *Block) error {
|
||||
ctx := context.Background()
|
||||
blockData := block.Bytes()
|
||||
|
||||
// Use Quasar bridge for dual BLS+Corona threshold signing
|
||||
if vm.quasarBridge != nil {
|
||||
_, err := vm.quasarBridge.SignBlock(ctx, block.ID(), blockData, block.Height())
|
||||
if err != nil {
|
||||
vm.log.Warn("Quasar signing failed, falling back to ML-DSA", "error", err)
|
||||
} else {
|
||||
vm.log.Debug("Block signed with Quasar BLS threshold",
|
||||
"blockID", block.ID(),
|
||||
"height", block.Height(),
|
||||
)
|
||||
}
|
||||
// Quasar signs the block for the CONSENSUS layer, which is where a block-level
|
||||
// signature belongs: a cert is a quorum's statement about a block, verifiable by
|
||||
// anyone holding the validator set. It is deliberately not a field on the block.
|
||||
//
|
||||
// A per-block stamp cannot be one. The block id is sha256 of its own bytes, so a
|
||||
// stamp on the wire makes the id depend on WHO signed and two honest nodes compute
|
||||
// two ids for one block -- a fork by construction, the same hazard seedGenesis
|
||||
// avoids by refusing to put wall-clock time in a block. Kept off the wire instead,
|
||||
// it exists only on the object the builder made and is gone the moment the block
|
||||
// is serialized, which is every time it leaves this process.
|
||||
func (vm *VM) signBlockWithQuasar(block *Block) {
|
||||
if vm.quasarBridge == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Also sign with ML-DSA for quantum resistance (standalone signature)
|
||||
key, err := vm.quantumSigner.GenerateCoronaKey()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate corona key: %w", err)
|
||||
if _, err := vm.quasarBridge.SignBlock(context.Background(), block.ID(), block.Bytes(), block.Height()); err != nil {
|
||||
vm.log.Warn("quasar block signing failed", "blockID", block.ID(), "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
sig, err := vm.quantumSigner.Sign(blockData, key)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to sign block with ML-DSA: %w", err)
|
||||
}
|
||||
|
||||
block.quantumSignature = sig
|
||||
return nil
|
||||
}
|
||||
|
||||
// verifyBlockQuantumSignature verifies a block's quantum signature
|
||||
func (vm *VM) verifyBlockQuantumSignature(block *Block) error {
|
||||
if block.quantumSignature == nil {
|
||||
return errInvalidQuantumStamp
|
||||
}
|
||||
|
||||
blockData := block.Bytes()
|
||||
return vm.quantumSigner.Verify(blockData, block.quantumSignature)
|
||||
vm.log.Debug("block signed with quasar BLS threshold", "blockID", block.ID(), "height", block.Height())
|
||||
}
|
||||
|
||||
// parseGenesis parses genesis data
|
||||
|
||||
Reference in New Issue
Block a user