Files
consensus/engine/chain/cert_wire_vector_test.go
zeekayandHanzo Dev fb181d586b export the reject leg the corpus generator already calls
go build ./... fails on this branch and has since 8968c19c:

    conformance/corpus.go:248:15: undefined: chain.CanonicalRejectMessage

The generator needs both decisions because the accept byte is bound into
the signed message, and proving that binding is most of what the corpus
is for — a port that folded the decision out would pass every
accept-only vector. But only the accept leg was exported;
canonicalVoteMessageFor is package-private, and package conformance is
not that package.

So export the reject leg beside it. Two named builders rather than one
taking a bool: the decision is the thing being distinguished, and Go
reads better when it is in the name.

Correct, not merely compiling: the committed corpus.json already holds
the two reject vectors (zero_reject, spec_reject, message tails 00
against the accept vectors' 01), and TestCorpusMatchesGolden compares
the regenerated corpus to it byte for byte. It passes. Invert the bool
and it fails twice over — 'the corpus moved: a finality rule changed'
and 'decision byte is reject=0x1 accept=0x1, want 0x00 and 0x01'.

The wire-vector test pinned the unexported builder, so it could not see
a wrapper handed the wrong bool, which is the only way a three-line
wrapper goes wrong. It pins both exported names now.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-29 17:52:36 -07:00

336 lines
14 KiB
Go

// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// cert_wire_vector_test.go — the two wire formats written down, by hand, from the
// SPEC rather than from the encoder.
//
// A round-trip test proves the encoder agrees with itself. Swap two same-width
// fields in both halves and it still passes, while every peer on the old build
// reads a different message under the same signature. These vectors are assembled
// byte by byte from the layouts documented in canonicalVoteMessageFor and
// cert_codec.go, so a field reordering, a width change, a domain-tag edit or a
// dropped binding fails here.
//
// The vote message is the one that matters most: it is what a validator's key
// actually signs, so it is the statement every quorum argument is about.
package chain
import (
"bytes"
"encoding/binary"
"testing"
"github.com/luxfi/ids"
)
// fill returns a 32-byte id whose every byte is b — recognisable at a glance in a
// hexdump, so a misplaced field is visible rather than merely unequal.
func fill(b byte) ids.ID {
var id ids.ID
for i := range id {
id[i] = b
}
return id
}
func fillNode(b byte) ids.NodeID {
var n ids.NodeID
for i := range n {
n[i] = b
}
return n
}
// vectorPosition is the position both vectors below are written against.
func vectorPosition() VotePosition {
return VotePosition{
ChainID: fill(0x11),
Height: 0x0102030405060708,
Round: 0x0A0B0C0D,
BlockID: fill(0x22),
ParentID: fill(0x33),
CanonicalID: fill(0x44),
ParentCanonicalID: fill(0x55),
ExecutionStateRoot: fill(0x66),
PayloadRoot: fill(0x77),
ValidatorSetRoot: fill(0x88),
}
}
// TestVoteMessage_WireVector pins the SIGNED bytes.
//
// "LUX/chain/vote/v2\x00" version:2 qc_type:1
// chain_id:32 height:8 round:4
// canonical_block_id:32 parent_canonical_id:32
// execution_state_root:32 payload_root:32 validator_set_root:32
// accept:1
//
// The outer envelope ids are deliberately ABSENT: two validators that executed
// one inner block sign identical bytes whatever wrapper they received.
func TestVoteMessage_WireVector(t *testing.T) {
var want []byte
want = append(want, "LUX/chain/vote/v2\x00"...)
want = append(want, 0x00, 0x03) // version 3, big-endian
want = append(want, 0x01) // qc_type = QCFinality
want = append(want, bytes.Repeat([]byte{0x11}, 32)...)
want = append(want, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08) // height
want = append(want, 0x0A, 0x0B, 0x0C, 0x0D) // round
want = append(want, bytes.Repeat([]byte{0x44}, 32)...) // canonical
want = append(want, bytes.Repeat([]byte{0x55}, 32)...) // parent canonical
want = append(want, bytes.Repeat([]byte{0x66}, 32)...) // execution state root
want = append(want, bytes.Repeat([]byte{0x77}, 32)...) // payload root
want = append(want, bytes.Repeat([]byte{0x88}, 32)...) // validator set root
want = append(want, 0x01) // accept
if got := CanonicalVoteMessage(vectorPosition()); !bytes.Equal(got, want) {
t.Fatalf("the signed vote message does not match the documented layout.\n got %x\nwant %x", got, want)
}
if uint16(3) != QuorumCertVersion {
t.Fatalf("the vector pins version 3; QuorumCertVersion is now %d — the wire moved, so the "+
"vector must be rewritten from the new spec, not regenerated from the encoder",
QuorumCertVersion)
}
}
// TestVoteMessage_BindsEveryConsensusField: each field the layout names must
// change the message. A field present in the struct but absent from the bytes is
// a value a signature does not cover — free for an attacker to restate.
func TestVoteMessage_BindsEveryConsensusField(t *testing.T) {
base := CanonicalVoteMessage(vectorPosition())
bound := map[string]func(*VotePosition){
"ChainID": func(p *VotePosition) { p.ChainID = fill(0xF1) },
"Height": func(p *VotePosition) { p.Height++ },
"Round": func(p *VotePosition) { p.Round++ },
"CanonicalID": func(p *VotePosition) { p.CanonicalID = fill(0xF2) },
"ParentCanonicalID": func(p *VotePosition) { p.ParentCanonicalID = fill(0xF3) },
"ExecutionStateRoot": func(p *VotePosition) { p.ExecutionStateRoot = fill(0xF4) },
"PayloadRoot": func(p *VotePosition) { p.PayloadRoot = fill(0xF5) },
"ValidatorSetRoot": func(p *VotePosition) { p.ValidatorSetRoot = fill(0xF6) },
}
for name, mutate := range bound {
pos := vectorPosition()
mutate(&pos)
if bytes.Equal(CanonicalVoteMessage(pos), base) {
t.Fatalf("%s does not reach the signed message: a signature over this position also "+
"covers every other value of %s", name, name)
}
}
// The outer envelope ids must NOT reach the message — that is what lets two
// wrappers of one execution interoperate.
for name, mutate := range map[string]func(*VotePosition){
"BlockID": func(p *VotePosition) { p.BlockID = fill(0xE1) },
"ParentID": func(p *VotePosition) { p.ParentID = fill(0xE2) },
} {
pos := vectorPosition()
mutate(&pos)
if !bytes.Equal(CanonicalVoteMessage(pos), base) {
t.Fatalf("the outer %s reached the signed message; a wrapper id in the signature splits "+
"the quorum across envelopes of one execution", name)
}
}
}
// TestVoteMessage_AcceptAndRejectAreDistinct: a reject signature must never be
// presentable as an accept. They differ in the final byte and nowhere else.
func TestVoteMessage_AcceptAndRejectAreDistinct(t *testing.T) {
pos := vectorPosition()
accept := canonicalVoteMessageFor(pos, true)
reject := canonicalVoteMessageFor(pos, false)
if bytes.Equal(accept, reject) {
t.Fatal("accept and reject sign identical bytes — a reject signature is an accept signature")
}
if len(accept) != len(reject) || !bytes.Equal(accept[:len(accept)-1], reject[:len(reject)-1]) {
t.Fatal("accept and reject differ before the decision byte; the decision must be the only difference")
}
if accept[len(accept)-1] != 0x01 || reject[len(reject)-1] != 0x00 {
t.Fatalf("decision byte is %#x/%#x, want 0x01/0x00", accept[len(accept)-1], reject[len(reject)-1])
}
}
// TestVoteMessage_ExportedAccessorsCarryTheirOwnDecision pins the two exported
// message builders to the decision each one names. The test above proves the
// accept and reject legs differ, but it calls the unexported builder directly,
// so it would still pass if an exported wrapper handed it the wrong bool —
// which is the only way a three-line wrapper can be wrong. The corpus generator
// reaches these two names and nothing else, so a swapped bool here would ship
// reject bytes to every port as the accept vector.
func TestVoteMessage_ExportedAccessorsCarryTheirOwnDecision(t *testing.T) {
pos := vectorPosition()
if !bytes.Equal(CanonicalVoteMessage(pos), canonicalVoteMessageFor(pos, true)) {
t.Fatal("CanonicalVoteMessage is not the accept leg")
}
if !bytes.Equal(CanonicalRejectMessage(pos), canonicalVoteMessageFor(pos, false)) {
t.Fatal("CanonicalRejectMessage is not the reject leg")
}
if bytes.Equal(CanonicalVoteMessage(pos), CanonicalRejectMessage(pos)) {
t.Fatal("the exported accept and reject builders return the same bytes")
}
}
// TestVoteMessage_DegradesUnsetCanonicalToTheOuterID pins the one place the
// non-wrapped degrade lives. A bare chain must sign its outer id under the
// canonical slot, and it must produce the SAME bytes a wrapped position with
// those ids set explicitly produces — otherwise the two disagree about one block.
func TestVoteMessage_DegradesUnsetCanonicalToTheOuterID(t *testing.T) {
bare := VotePosition{ChainID: fill(0x11), Height: 9, Round: 1, BlockID: fill(0x22), ParentID: fill(0x33)}
explicit := bare
explicit.CanonicalID, explicit.ParentCanonicalID = fill(0x22), fill(0x33)
if !bytes.Equal(CanonicalVoteMessage(bare), CanonicalVoteMessage(explicit)) {
t.Fatal("a bare position and the same position with canonical == outer sign different bytes")
}
}
// --- the cert frame -----------------------------------------------------------
// certVector assembles the documented cert frame by hand, with one vote.
func certVector() []byte {
var b []byte
b = append(b, 0x00, 0x03) // version:2
b = append(b, 0x01) // type:1 = QCFinality
b = append(b, 0x02) // tier:1 = Nova
b = append(b, bytes.Repeat([]byte{0x11}, 32)...)
b = append(b, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08) // height:8
b = append(b, 0x0A, 0x0B, 0x0C, 0x0D) // round:4
b = append(b, bytes.Repeat([]byte{0x22}, 32)...) // block_id
b = append(b, bytes.Repeat([]byte{0x33}, 32)...) // parent_id
b = append(b, bytes.Repeat([]byte{0x44}, 32)...) // canonical_block_id
b = append(b, bytes.Repeat([]byte{0x55}, 32)...) // parent_canonical_id
b = append(b, bytes.Repeat([]byte{0x66}, 32)...) // execution_state_root
b = append(b, bytes.Repeat([]byte{0x77}, 32)...) // payload_root
b = append(b, bytes.Repeat([]byte{0x88}, 32)...) // validator_set_root
b = append(b, 0x00, 0x00, 0x00, 0x07) // threshold:4
b = append(b, 0x00, 0x00, 0x00, 0x01) // vote_count:4
b = append(b, bytes.Repeat([]byte{0x99}, ids.NodeIDLen)...) // node_id:20
b = append(b, 0x01) // accept:1
b = append(b, 0x00, 0x00, 0x00, 0x03) // sig_len:4
b = append(b, 0xAB, 0xCD, 0xEF) // sig
return b
}
// TestQuorumCertCodec_WireVector decodes the hand-written frame and checks every
// field landed where the layout says it does.
func TestQuorumCertCodec_WireVector(t *testing.T) {
c, err := UnmarshalQuorumCert(certVector())
if err != nil {
t.Fatalf("the documented frame did not decode: %v", err)
}
pos := c.Position
switch {
case c.Version != 3:
t.Fatalf("version = %d", c.Version)
case c.Type != QCFinality:
t.Fatalf("type = %d", c.Type)
case c.Tier != Nova:
t.Fatalf("tier = %s", c.Tier)
case pos.ChainID != fill(0x11):
t.Fatalf("chain_id = %s", pos.ChainID)
case pos.Height != 0x0102030405060708:
t.Fatalf("height = %#x", pos.Height)
case pos.Round != 0x0A0B0C0D:
t.Fatalf("round = %#x", pos.Round)
case pos.BlockID != fill(0x22):
t.Fatalf("block_id = %s", pos.BlockID)
case pos.ParentID != fill(0x33):
t.Fatalf("parent_id = %s", pos.ParentID)
case pos.CanonicalID != fill(0x44):
t.Fatalf("canonical_block_id = %s", pos.CanonicalID)
case pos.ParentCanonicalID != fill(0x55):
t.Fatalf("parent_canonical_id = %s", pos.ParentCanonicalID)
case pos.ExecutionStateRoot != fill(0x66):
t.Fatalf("execution_state_root = %s", pos.ExecutionStateRoot)
case pos.PayloadRoot != fill(0x77):
t.Fatalf("payload_root = %s", pos.PayloadRoot)
case pos.ValidatorSetRoot != fill(0x88):
t.Fatalf("validator_set_root = %s", pos.ValidatorSetRoot)
case c.Threshold != 7:
t.Fatalf("threshold = %d", c.Threshold)
case len(c.Votes) != 1:
t.Fatalf("vote_count = %d", len(c.Votes))
case c.Votes[0].NodeID != fillNode(0x99):
t.Fatalf("node_id = %s", c.Votes[0].NodeID)
case !c.Votes[0].Accept:
t.Fatal("accept byte 0x01 did not decode as accept")
case !bytes.Equal(c.Votes[0].Signature, []byte{0xAB, 0xCD, 0xEF}):
t.Fatalf("signature = %x", c.Votes[0].Signature)
}
// The encoder must emit exactly what the spec says it emits.
got, err := c.MarshalBinary()
if err != nil {
t.Fatalf("marshal: %v", err)
}
if !bytes.Equal(got, certVector()) {
t.Fatalf("encoder output differs from the documented frame.\n got %x\nwant %x", got, certVector())
}
}
// TestQuorumCertCodec_NoTruncatedPrefixDecodes: no proper prefix of a valid frame
// may decode. A decoder that accepts a short read has silently substituted a
// zero for whatever the truncated field was.
func TestQuorumCertCodec_NoTruncatedPrefixDecodes(t *testing.T) {
full := certVector()
for n := 0; n < len(full); n++ {
if _, err := UnmarshalQuorumCert(full[:n]); err == nil {
t.Fatalf("a %d-byte prefix of a %d-byte cert decoded", n, len(full))
}
}
if _, err := UnmarshalQuorumCert(append(full, 0x00)); err == nil {
t.Fatal("a trailing byte after a complete frame was accepted")
}
}
// TestQuorumCertCodec_LengthCannotExceedTheFrame: a sig_len larger than what the
// frame can hold must be refused, and so must a vote_count whose minimum
// footprint exceeds the remaining bytes.
func TestQuorumCertCodec_LengthCannotExceedTheFrame(t *testing.T) {
full := certVector()
sigLenAt := len(full) - 3 - 4
over := append([]byte(nil), full...)
binary.BigEndian.PutUint32(over[sigLenAt:sigLenAt+4], 0xFFFFFFFF)
if _, err := UnmarshalQuorumCert(over); err == nil {
t.Fatal("a sig_len of 0xFFFFFFFF was accepted")
}
countAt := 2 + 1 + 1 + 32 + 8 + 4 + 32*7 + 4
huge := append([]byte(nil), full...)
binary.BigEndian.PutUint32(huge[countAt:countAt+4], 0xFFFFFFFF)
if _, err := UnmarshalQuorumCert(huge); err == nil {
t.Fatal("a vote_count of 0xFFFFFFFF was accepted")
}
}
// TestQuorumCertCodec_IsCanonical: exactly one byte string per cert. The fuzzer
// states this ("re-encoding must reproduce the SAME bytes the decoder accepted")
// and then only re-decodes, so nothing checks it.
//
// It matters wherever cert bytes are the unit of identity — served to peers from
// a per-block cache, compared for de-duplication, or recorded as evidence. A
// second spelling of one cert is a second entry.
func TestQuorumCertCodec_IsCanonical(t *testing.T) {
full := certVector()
acceptAt := len(full) - 3 - 4 - 1
variant := append([]byte(nil), full...)
variant[acceptAt] = 0x02 // any non-zero decodes as accept
c, err := UnmarshalQuorumCert(variant)
if err != nil {
return // refusing a non-canonical decision byte is the other valid answer
}
got, err := c.MarshalBinary()
if err != nil {
t.Fatalf("marshal: %v", err)
}
if !bytes.Equal(got, variant) {
t.Fatalf("the decoder accepted a frame it cannot reproduce: accept byte %#x decoded and "+
"re-encoded as %#x. Two distinct byte strings name one cert, so anything that treats "+
"cert bytes as identity — the per-block serving cache, a dedup, a digest — can be "+
"split in two by flipping a bit no field reads.",
variant[acceptAt], got[acceptAt])
}
}