mirror of
https://github.com/luxfi/consensus.git
synced 2026-09-24 00:26:58 +00:00
pathFromTip resolved every intermediate ancestor by its OUTER proposervm-wrapper id with no canonical fallback — the one finality path the mainnet-644 canonicalization missed (vote placement, cert's-own-block, and the equivocation key were all canonicalized; this walk was not). Under sustained saturation the fleet tracks distinct wrappers of the same inner block (aliases, not forks); a verified cert threading a peer's wrapper could not be applied by a node holding the identical inner execution under its own wrapper, so finality livelocked on an unservable outer id (~1174 finality-guard refusals/min, tip frozen ~2h, safety held — no fork). Carry the signature-covered ParentCanonical through the fold Cert (it was dropped at the engine.go seam); on an outer-id miss, collapse to a locally-held sibling wrapper of the same inner commitment (WrapperByCanonical) before the fail-closed defer, rebasing the child's parent link so sibling-pruning keys on the held envelope. A genuinely unheld inner block still returns ErrAncestorNotTracked (behind-node defer preserved). Bare chains and the normal tracked-outer path are byte-for-byte unchanged (empty ParentCanonical ⇒ outer-id-only walk; the miss branch is additive). Alias-collapse is sound because canonicalRep = CanonicalID() is a collision-resistant hash of the inner execution (binds inner parent + payload + state root) that no unverified block can spoof — every tracked block is Verify()'d before AddBlock, and the matched parent commitment is signature-covered in the cert. It can therefore only ever stand in a byte-identical execution, never a divergent state: no fork risk HEAD lacked, livelock closed. RED-verified SHIP: 0 critical / 0 high / 0 medium (2 optional-cleanup LOWs), 12 adversarial tests + the 107-test finality suite green, go build + vet clean. Regression guards: intermediate_sibling_livelock_test.go (repro FAIL->PASS), intermediate_sibling_adversarial_test.go, wrapper_bycanonical_adversarial_test.go. Co-authored-by: Hanzo Dev <[email protected]>
108 lines
4.5 KiB
Go
108 lines
4.5 KiB
Go
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
// wrapper_bycanonical_adversarial_test.go — RED-TEAM probes of the PRODUCTION
|
|
// blocksAncestry.WrapperByCanonical scan (topological.go), which the pure-fold
|
|
// harness stubs out. Targets: the height+canonicalRep filter is exact (no cross-height
|
|
// or cross-execution collapse), and the "prefer accepted" tiebreak lands on the
|
|
// committed wrapper.
|
|
package chain
|
|
|
|
import "testing"
|
|
import "github.com/luxfi/ids"
|
|
|
|
func mkBlock(id, parent ids.ID, height uint64, canonical, parentCanonical ids.ID, accepted bool) *Block {
|
|
return &Block{
|
|
id: id,
|
|
parentID: parent,
|
|
height: height,
|
|
canonicalID: canonical,
|
|
parentCanonicalID: parentCanonical,
|
|
accepted: accepted,
|
|
}
|
|
}
|
|
|
|
// #1/#4 — the scan matches ONLY on (height, canonicalRep). A block with a DIFFERENT
|
|
// canonical at the same height, or the SAME canonical at a different height, is never
|
|
// returned. This is the binding boundary in the real scan.
|
|
func TestAdvProd_WrapperByCanonical_ExactHeightAndCanonical(t *testing.T) {
|
|
tip := ids.GenerateTestID()
|
|
inner6 := ids.GenerateTestID()
|
|
inner6b := ids.GenerateTestID() // different execution
|
|
inner7 := ids.GenerateTestID()
|
|
|
|
wrap6 := ids.GenerateTestID()
|
|
wrap6diff := ids.GenerateTestID()
|
|
wrap7same := ids.GenerateTestID() // same canonical value as target but WRONG height
|
|
|
|
blocks := map[ids.ID]*Block{
|
|
wrap6: mkBlock(wrap6, tip, 6, inner6, tip, false),
|
|
wrap6diff: mkBlock(wrap6diff, tip, 6, inner6b, tip, false),
|
|
// Deliberately reuse inner6 as a canonical at height 7 to test the height filter.
|
|
wrap7same: mkBlock(wrap7same, wrap6, 7, inner6, inner6, false),
|
|
}
|
|
a := blocksAncestry{blocks: blocks}
|
|
|
|
// Exact match at height 6 returns wrap6 (the only inner6 wrapper at h6).
|
|
if got, ok := a.WrapperByCanonical(inner6, 6); !ok || got != wrap6 {
|
|
t.Fatalf("expected wrap6 for (inner6,6), got %v ok=%v", got, ok)
|
|
}
|
|
// A different canonical at h6 (inner7, no wrapper) misses.
|
|
if got, ok := a.WrapperByCanonical(inner7, 6); ok {
|
|
t.Fatalf("no wrapper of inner7 at h6 exists; must miss, got %v", got)
|
|
}
|
|
// SAME canonical value (inner6) but at height 7: must NOT return wrap6 (height filter);
|
|
// it returns wrap7same only because wrap7same is literally at h7 with canonical inner6.
|
|
if got, ok := a.WrapperByCanonical(inner6, 7); !ok || got != wrap7same {
|
|
t.Fatalf("height filter: (inner6,7) must return wrap7same, not the h6 wrapper, got %v ok=%v", got, ok)
|
|
}
|
|
// Cross-height leak check: (inner6,6) must never leak wrap7same.
|
|
if got, _ := a.WrapperByCanonical(inner6, 6); got == wrap7same {
|
|
t.Fatalf("cross-height leak: returned a height-7 wrapper for a height-6 query")
|
|
}
|
|
}
|
|
|
|
// #4 — "prefer accepted" tiebreak: when several aliases of one inner block are tracked,
|
|
// the accepted one (the wrapper the VM committed) is returned deterministically.
|
|
func TestAdvProd_WrapperByCanonical_PrefersAccepted(t *testing.T) {
|
|
tip := ids.GenerateTestID()
|
|
inner6 := ids.GenerateTestID()
|
|
|
|
accepted6 := ids.GenerateTestID()
|
|
pending6a := ids.GenerateTestID()
|
|
pending6b := ids.GenerateTestID()
|
|
|
|
blocks := map[ids.ID]*Block{
|
|
pending6a: mkBlock(pending6a, tip, 6, inner6, tip, false),
|
|
accepted6: mkBlock(accepted6, tip, 6, inner6, tip, true),
|
|
pending6b: mkBlock(pending6b, tip, 6, inner6, tip, false),
|
|
}
|
|
a := blocksAncestry{blocks: blocks}
|
|
// Repeat to defeat map-iteration randomness: accepted must ALWAYS win.
|
|
for i := 0; i < 64; i++ {
|
|
got, ok := a.WrapperByCanonical(inner6, 6)
|
|
if !ok || got != accepted6 {
|
|
t.Fatalf("iter %d: accepted wrapper must always be preferred, got %v ok=%v", i, got, ok)
|
|
}
|
|
}
|
|
}
|
|
|
|
// #1 — bare-block degrade: canonicalID Empty ⇒ canonicalRep falls back to the outer id,
|
|
// so a bare chain's WrapperByCanonical is keyed on the outer id (no two envelopes share
|
|
// it ⇒ collapse is inert), exactly as claimed.
|
|
func TestAdvProd_WrapperByCanonical_BareDegrade(t *testing.T) {
|
|
tip := ids.GenerateTestID()
|
|
bare := ids.GenerateTestID()
|
|
blocks := map[ids.ID]*Block{
|
|
bare: mkBlock(bare, tip, 6, ids.Empty, ids.Empty, false), // canonicalID Empty ⇒ rep == id
|
|
}
|
|
a := blocksAncestry{blocks: blocks}
|
|
if got, ok := a.WrapperByCanonical(bare, 6); !ok || got != bare {
|
|
t.Fatalf("bare block must resolve by its own outer id, got %v ok=%v", got, ok)
|
|
}
|
|
// A random inner id never matches a bare block (no aliasing on a bare chain).
|
|
if _, ok := a.WrapperByCanonical(ids.GenerateTestID(), 6); ok {
|
|
t.Fatalf("bare chain must not alias-collapse on an arbitrary canonical")
|
|
}
|
|
}
|