Files
chains/quantumvm/block.go
T
zeekayandHanzo Dev d87600fbef 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]>
2026-08-20 11:44:10 -07:00

165 lines
3.8 KiB
Go

// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package quantumvm
import (
"context"
"encoding/binary"
"errors"
"time"
"github.com/luxfi/chains/quantumvm/quantum"
"github.com/luxfi/ids"
)
var (
errBlockVerificationFailed = errors.New("block verification failed")
errInvalidBlockHeight = errors.New("invalid block height")
errInvalidParentID = errors.New("invalid parent ID")
)
// Block represents a QVM block with quantum features
type Block struct {
id ids.ID
timestamp time.Time
height uint64
parentID ids.ID
transactions []Transaction
vm *VM
bytes []byte
}
// ID returns the block ID
func (b *Block) ID() ids.ID {
return b.id
}
// Accept marks the block as accepted
func (b *Block) Accept(context.Context) error {
b.vm.lock.Lock()
defer b.vm.lock.Unlock()
// Store block in database
if err := b.vm.state.Put(b.id[:], b.Bytes()); err != nil {
return err
}
// Update last accepted
if err := b.vm.state.Put([]byte("lastAccepted"), b.id[:]); err != nil {
return err
}
// Update height
heightBytes := make([]byte, 8)
binary.BigEndian.PutUint64(heightBytes, b.height)
if err := b.vm.state.Put([]byte("height"), heightBytes); err != nil {
return err
}
// Process transactions
for _, tx := range b.transactions {
if err := b.vm.txPool.RemoveTransaction(tx.ID()); err != nil {
b.vm.log.Error("failed to remove tx from pool", "txID", tx.ID(), "error", err)
}
}
b.vm.log.Info("block accepted",
"blockID", b.id,
"height", b.height,
"txCount", len(b.transactions),
)
return nil
}
// Reject marks the block as rejected
func (b *Block) Reject(context.Context) error {
b.vm.lock.Lock()
defer b.vm.lock.Unlock()
// Return transactions to pool
for _, tx := range b.transactions {
if err := b.vm.txPool.AddTransaction(tx); err != nil {
b.vm.log.Error("failed to return tx to pool", "txID", tx.ID(), "error", err)
}
}
b.vm.log.Debug("block rejected", "blockID", b.id, "height", b.height)
return nil
}
// Verify verifies the block validity
func (b *Block) Verify(ctx context.Context) error {
b.vm.lock.RLock()
defer b.vm.lock.RUnlock()
// Verify height
if b.height == 0 {
return errInvalidBlockHeight
}
// Verify parent exists (except for genesis)
if b.height > 1 {
if _, err := b.vm.GetBlock(ctx, b.parentID); err != nil {
return errInvalidParentID
}
}
// Verify transactions in parallel
if len(b.transactions) > 0 {
msgs := make([][]byte, len(b.transactions))
sigs := make([]*quantum.QuantumSignature, len(b.transactions))
for i, tx := range b.transactions {
msgs[i] = tx.Bytes()
sigs[i] = tx.GetQuantumSignature()
}
if b.vm.Config.CoronaEnabled {
if err := b.vm.quantumSigner.ParallelVerify(msgs, sigs); err != nil {
return errBlockVerificationFailed
}
}
}
return nil
}
// Parent returns the parent block ID
func (b *Block) Parent() ids.ID {
return b.parentID
}
// ParentID returns the parent block ID (implements consensus Block interface)
func (b *Block) ParentID() ids.ID {
return b.parentID
}
// Height returns the block height
func (b *Block) Height() uint64 {
return b.height
}
// Timestamp returns the block timestamp (implements block.Block interface)
func (b *Block) Timestamp() time.Time {
return b.timestamp
}
// TimestampUnix returns the block timestamp as Unix seconds.
func (b *Block) TimestampUnix() int64 {
return b.timestamp.Unix()
}
// Status returns the block status. Q-Chain does not yet track per-block
// status separately (all blocks reaching here are processing or accepted),
// so we return 0 (Unknown) until a status field is added.
func (b *Block) Status() uint8 {
return 0
}
// String returns a string representation of the block
func (b *Block) String() string {
return b.id.String()
}