mirror of
https://github.com/luxfi/chains.git
synced 2026-09-24 00:26:00 +00:00
Red confirmed by execution that the receive path was not the build path, and three of its findings were chain-integrity defects rather than gaps. Parse discarded the transaction set, so a block arriving over the network had none -- and the ML-DSA check was gated on having some. The check therefore ran only on blocks this node built itself. The same bytes verified in memory and passed off the wire, and a block carrying arbitrary transaction bytes was accepted and committed. Parse now reconstructs the full set, signatures included, and refuses a declared count its bytes cannot hold or lengths that do not partition the blob. Accept never asked whether the parent was the tip -- only that the height followed it. So an old sibling rewound a five-block chain and the height index went on serving the orphaned branch to bootstrapping peers as canonical. The tip reader answered the zero id for any failure, and the genesis seeder read that as a fresh chain: one transient read at boot committed genesis over a live tip, durably, and Initialize returned nil. Not-found is now distinguished from unreadable, and only the first means new. The harness that was supposed to defend the signer-identity fix could not see it: it gave each node its own ChainID, which production never does, so reverting the fix left the suite green. It now holds the chain constant and varies the node, which is the shape the defect lives in. Also: the proposer no longer builds a block its own Verify refuses when the clock trails the tip. 96.4% coverage.
121 lines
4.0 KiB
Go
121 lines
4.0 KiB
Go
// 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(),
|
|
chainID: vm.blockchainID,
|
|
networkID: vm.NetworkID,
|
|
transactions: []Transaction{stampedTx(1, "op")},
|
|
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 := tipOf(t, vm)
|
|
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())
|
|
}
|
|
}
|
|
|
|
// TestServeAndReceiveAgree pins the asymmetry itself.
|
|
//
|
|
// A responder answers a catch-up request with the bytes it stored; the
|
|
// requester admits the reply through 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 TestServeAndReceiveAgree(t *testing.T) {
|
|
vm := parseVM(t)
|
|
if err := vm.seedGenesis(); err != nil {
|
|
t.Fatalf("seedGenesis: %v", err)
|
|
}
|
|
|
|
blk := &Block{
|
|
timestamp: time.Unix(2000, 0).UTC(),
|
|
height: 1,
|
|
parentID: tipOf(t, vm),
|
|
chainID: vm.blockchainID,
|
|
networkID: vm.NetworkID,
|
|
transactions: []Transaction{stampedTx(1, "op")},
|
|
vm: vm,
|
|
}
|
|
blk.id = blk.computeID()
|
|
if err := blk.Accept(context.Background()); err != nil {
|
|
t.Fatalf("accept: %v", err)
|
|
}
|
|
|
|
served, servErr := vm.GetBlock(context.Background(), blk.id) // responder side
|
|
received, recvErr := vm.ParseBlock(context.Background(), blk.Bytes())
|
|
if (servErr == nil) != (recvErr == nil) {
|
|
t.Fatalf("the two paths disagree on identical bytes: serve=%v receive=%v\n"+
|
|
"every node would serve this block and every node would refuse it", servErr, recvErr)
|
|
}
|
|
if servErr == nil && served.ID() != received.ID() {
|
|
t.Fatalf("one block read back under two ids: %s vs %s", served.ID(), received.ID())
|
|
}
|
|
}
|