mirror of
https://github.com/luxfi/node.git
synced 2026-07-27 03:39:39 +00:00
A validator that fell behind during consensus (mainnet luxd-0/2 at 1082780 while peers reached 1082797) could not recover at runtime: it received no gossip, and a restart only bootstraps to its own height. Two node-side gaps, now closed: GAP 1 — frontier-sync was dead. The GetAcceptedFrontier/AcceptedFrontier responders existed but HandleInbound had no case for either op, so both fell through (dropped) and nothing ever ASKED. Add the two cases (mirroring GetContext/Context) and a slow frontier poller (runFrontierPoller, 15s, small peer sample) so a node proactively learns it is behind and pulls the gap. GAP 2 — catch-up could not accept already-finalized blocks. handleContext routed fetched ancestors through the VOTING Put, but the network will not re-vote a decided height → the gap blocks wedged. Now each catch-up container pairs the block with its finality cert (encodeCatchupEntry: magic|blockLen|block|certLen|cert, carried by the existing Ancestors flattener) and the requester finalizes via engine.AcceptCatchupBlock (the verified-cert path) when a cert is present, voting only on certless/legacy entries. The 'LCU2' magic makes a cross-version exchange fail CLEANLY (a legacy decoder self-rejects; the v2 decoder treats a legacy raw block as legacy). Pins consensus to the local engine commit (replace ../consensus) for the branch; reviewer swaps to a pseudo-version. Does not weaken the cert-gate.
111 lines
4.6 KiB
Go
111 lines
4.6 KiB
Go
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
// catchup_frame_test.go — the CERT-CARRYING catch-up wire format. These prove the
|
|
// load-bearing property: a v2 (block,cert) entry round-trips, an entry with no cert
|
|
// routes to the vote path, and a cross-version exchange fails CLEANLY (a legacy
|
|
// decoder cannot misparse a v2 frame, and the v2 decoder treats a legacy raw block
|
|
// as legacy — never a partial/garbage parse). The cert-accept SEMANTICS are proven
|
|
// in the consensus engine tests; here we pin only the framing.
|
|
package chains
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"testing"
|
|
)
|
|
|
|
func TestCatchupEntry_RoundTrip(t *testing.T) {
|
|
block := []byte("\xf9\x02\x00 a realistic-ish block body")
|
|
cert := []byte("a-marshaled-quorum-cert")
|
|
|
|
blk, crt, ok := decodeCatchupEntry(encodeCatchupEntry(block, cert))
|
|
if !ok {
|
|
t.Fatal("a v2 entry must decode as a v2 entry")
|
|
}
|
|
if !bytes.Equal(blk, block) {
|
|
t.Fatalf("block bytes corrupted: got %q want %q", blk, block)
|
|
}
|
|
if !bytes.Equal(crt, cert) {
|
|
t.Fatalf("cert bytes corrupted: got %q want %q", crt, cert)
|
|
}
|
|
}
|
|
|
|
func TestCatchupEntry_EmptyCertRoutesToVotePath(t *testing.T) {
|
|
// A still-pending block served on the live missing-parent path carries no cert.
|
|
// It must decode as a v2 entry with an EMPTY cert — the requester then votes on
|
|
// it (handleContext routes certLen==0 to Put), the legacy behaviour.
|
|
block := []byte("pending-block-no-cert")
|
|
blk, crt, ok := decodeCatchupEntry(encodeCatchupEntry(block, nil))
|
|
if !ok {
|
|
t.Fatal("a v2 entry with empty cert must still decode as v2")
|
|
}
|
|
if !bytes.Equal(blk, block) {
|
|
t.Fatalf("block bytes corrupted: got %q", blk)
|
|
}
|
|
if len(crt) != 0 {
|
|
t.Fatalf("cert must be empty, got %d bytes", len(crt))
|
|
}
|
|
}
|
|
|
|
func TestCatchupEntry_LegacyRawBlockIsNotV2(t *testing.T) {
|
|
// A legacy responder sends the raw block as the container (no magic). The v2
|
|
// decoder must report ok=false so handleContext treats it as a raw block (Put),
|
|
// never as a malformed v2 entry. Cover several real block-prefix shapes.
|
|
for _, raw := range [][]byte{
|
|
{0xf9, 0x02, 0x00, 0x11, 0x22}, // EVM/RLP list header
|
|
{0x00, 0x00, 0x00, 0x2a}, // P/X-chain codec version prefix
|
|
{}, // empty
|
|
[]byte("LCU"), // 3 bytes — too short to even hold the magic
|
|
} {
|
|
if _, _, ok := decodeCatchupEntry(raw); ok {
|
|
t.Fatalf("legacy raw block %x must NOT decode as a v2 entry", raw)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCatchupEntry_MagicIsNotAPlausibleLength(t *testing.T) {
|
|
// CROSS-VERSION SAFETY (new responder → legacy requester): a legacy decoder reads
|
|
// the first 4 bytes of a v2 entry as a uint32 length. The magic must be so large
|
|
// that it always exceeds the remaining buffer, so the legacy loop rejects the
|
|
// frame (0 blocks processed) rather than consuming garbage. 0x4C435532 ≈ 1.28 GB.
|
|
asLen := binary.BigEndian.Uint32(catchupEntryMagic[:])
|
|
if asLen < (1 << 30) {
|
|
t.Fatalf("magic read as a length (%d) is too small — a legacy decoder could misparse a v2 frame", asLen)
|
|
}
|
|
// And a full v2 frame's leading length-word (the magic) dwarfs the frame itself,
|
|
// so the legacy `blockLen > remaining` guard always fires.
|
|
frame := encodeCatchupEntry([]byte("blk"), []byte("crt"))
|
|
if uint64(binary.BigEndian.Uint32(frame[:4])) <= uint64(len(frame)) {
|
|
t.Fatal("magic-as-length must exceed the frame length so a legacy decoder self-rejects")
|
|
}
|
|
}
|
|
|
|
func TestCatchupEntry_CorruptV2FramesRejected(t *testing.T) {
|
|
good := encodeCatchupEntry([]byte("block-body"), []byte("cert-body"))
|
|
|
|
// Truncations inside the v2 structure must fail to ok=false (never a partial
|
|
// parse): drop the trailing cert byte, drop the certLen word, etc.
|
|
for _, bad := range [][]byte{
|
|
good[:len(good)-1], // last cert byte missing → certLen != remaining
|
|
good[:len(good)-5], // certLen word + cert missing
|
|
good[:8], // magic + blockLen only, block missing
|
|
good[:6], // magic + 2 bytes of blockLen
|
|
append(append([]byte(nil), good...), 0x00), // trailing byte → does not consume exactly
|
|
} {
|
|
if _, _, ok := decodeCatchupEntry(bad); ok {
|
|
t.Fatalf("a corrupt v2 frame (len %d) must be rejected, not partial-parsed", len(bad))
|
|
}
|
|
}
|
|
|
|
// An overflowing blockLen (claims more block than the buffer holds) is rejected.
|
|
overflow := append([]byte(nil), catchupEntryMagic[:]...)
|
|
var u32 [4]byte
|
|
binary.BigEndian.PutUint32(u32[:], 0xFFFFFFFF)
|
|
overflow = append(overflow, u32[:]...)
|
|
overflow = append(overflow, []byte("tiny")...)
|
|
if _, _, ok := decodeCatchupEntry(overflow); ok {
|
|
t.Fatal("a v2 frame with an overflowing blockLen must be rejected")
|
|
}
|
|
}
|