Commit 10b972e
fix: v22 — collapse microblock failover machinery to pure VRF + skip-slot
Architectural simplification of the microblock layer: a single
deterministic VRF leader per height plus a time-derived in-rotation
fallback computed from the chain-anchored `last_applied_block_timestamp`
and the network-corrected `effective_now()` (BFT-time median over the
32-sample on-chain timestamp ring).
The macroblock layer (90-block 2f+1 commit-reveal + view-change),
existing fork-choice (`chain_weight.rs` cumulative attestation weight),
and equivocation slashing (`record_block_equivocation` +
`analyze_chain_for_slashing` covering block-equivocation,
timeout-equivocation, and generic double-sign) are unchanged.
Forensic context
----------------
Three bugs observed on the 5-node testnet across v21..v22:
* h=360 macroblock-boundary cert-presence deadlock (12.7 h stall)
* h=367 split-brain fork (empty-slot attestation cascade racing
cert generation produced an alternate block at the same height)
* node_002 555K pipeline backlog (canonical peers pipeline-jammed
by the cert-presence gate, sync requests timed out, fell back to
the forked peer, hash_chain_break loop)
Root cause was four overlapping microblock failover mechanisms
accumulated over 55 commits (v15..v21) without removing previous ones:
rotation_round atomic + TimeoutVote chain + empty-slot attestation
cascade + heartbeat-fast-path detection. O(N^2) interaction surface
made races inevitable.
Changes
-------
block_pipeline.rs : +8 -160 net -152
node.rs : +372 -781 net -409
unified_p2p.rs : +0 -189 net -189
TOTAL 396 insertions(+), 1186 deletions(-) — NET -790 lines
Deleted (microblock failover machinery)
* Cert presence gate at block_pipeline.rs:1761 (incl. v21 A2 vote-pool
fallback — both inert once `mb.timeout_round` is always 0).
* Empty-slot attestation cascade body (~200 lines).
* Heartbeat-fast-path consensus integration.
* Pacemaker rotation_round combine + `set_timeout_round` storage.
* Vote-gate `is_synced_enough` block (was gating only the deleted emit).
* v21 (B1) heartbeat-driven TimeoutVote emit.
* Stall-driven TimeoutVote-microblock emit + producer-failover log.
* Adaptive timeout grace period (`base_grace + rotation_extra +
drift_grace`).
* `proposed_timeout_round` / `adopted_timeout_round` /
`timeout_round_for_rotation` / `f_plus_1` derivations.
* Per-tick `certified_timeout_round` read at the microblock layer.
* `effective_timeout_round_at_start` computation inside the microblock
struct construction.
* v21 vote-pool helpers `count_timeout_votes_in_pool` /
`has_two_f_plus_one_timeout_votes` and `tests_v21_a2_vote_pool` module.
* `reset_consecutive_empty_slots()` no-op shim from an earlier v22
draft.
* Residual `/* */` commented-out blocks left from incremental edits
(~360 lines physically removed).
Added (v22 production path)
* `MAX_CONSECUTIVE_EMPTY_SLOTS = 3` constant.
* `v22_compute_empty_slot_offset(last_applied_block_timestamp, now)`
pure helper:
seconds_silent = now - last_applied_block_timestamp - 1
offset = seconds_silent / MAX_CONSECUTIVE_EMPTY_SLOTS
Returns 0 under healthy production. Walks forward by 1 every
`MAX_CONSECUTIVE_EMPTY_SLOTS` silent seconds.
* Producer loop rewrite:
timeout_round = 0 always
primary_producer = VRF leader at round 0
empty_slot_offset = v22_compute_empty_slot_offset(
last_applied_ts,
effective_now()) // BFT-time, not raw clock
current_producer = if offset == 0 { primary }
else { VRF leader at round = offset }
* `microblock.timeout_round = 0` literal in the MicroBlock constructor.
* `tests_v22_slot_offset` — 6 regression tests pinning every transition
boundary plus the NTP-jitter spread bound.
Preserved (unchanged)
* Macroblock 2f+1 commit-reveal finality (commit_phase / reveal_phase /
finalize_round).
* Macroblock view-change via `emit_macroblock_view_change_vote` on
commit-phase or reveal-phase failure.
* `AggregatedTimeoutCert` storage + gossip handlers (consumed by
macroblock view-change only after v22).
* `HIGHEST_CERTIFIED_ROUND` DashMap (macroblock state).
* `chain_weight.rs` LMD-GHOST cumulative attestation fork choice.
* `record_block_equivocation` + `BLOCK_EQUIVOCATION_EVIDENCE` + all
slashing detection paths.
* `effective_now()` BFT-time median ring infrastructure.
* Median-past timestamp rule + `TIMESTAMP_FUTURE_TOLERANCE`.
* `observe_clock_drift` EMA monitor + `[WARN][DRIFT]` operator hint.
* v18 active sync + range-sync + parallel-sync (`MAX_PARALLEL_SYNC_PEERS=8`).
* v19 anti-spoof Dilithium handshake + v19.1 fresh-bootstrap auto-anchor.
* v20 PK registry scaling + LRU + 100K cap + env override.
* Chronic stall recovery (`> 120 s` peer-driven resync) — simplified
to drop the now-meaningless `certified_round == 0` predicate.
Why this is correct for PQC two-tier blockchain
-----------------------------------------------
Dilithium3 signatures have no aggregation (no BLS equivalent). At a
1000-validator committee, per-block 2/3 voting costs ~2.3 MB / block of
signatures alone — un-shippable. The two-tier amortisation (one signed
microblock per second; 2f+1 macroblock cert every 90 seconds) cuts the
sustained bandwidth to roughly 53 KB / sec at 1000 validators.
For the microblock tier, the universal pattern across multi-tier-finality
chains is optimistic apply with an empty slot tolerated when the
deterministic leader is silent. v22 adopts this pattern and adds an
in-rotation fallback bounded by `MAX_CONSECUTIVE_EMPTY_SLOTS` slots, so
the 30-block rotation window does not amplify a single failed primary
into 30 empty slots.
Safety properties preserved
* Single deterministic VRF leader per height — no two valid producers
can claim the same height legitimately.
* Fallback identity is the same `select_microblock_producer_with_round`
function with `empty_slot_offset` mixed into the seed; every honest
node observing the same `effective_now()` reaches the same fallback
identity.
* `effective_now()` = max(wall, median of 32-sample on-chain block
timestamps) — a node with a drifted local clock converges with the
network on the slot-offset computation as long as the chain itself
is making progress.
* All Dilithium3 signature verification gates intact at every block.
* 2f+1 macroblock commit-reveal supermajority preserved.
* Anti-replay via signed `(mb_idx, round, voter_id)` tuple in macroblock
TimeoutVote.
* Anti-spoof handshake binding (v19) + chain-registered PK (v20) intact.
Failure modes structurally impossible in v22
* Cascade livelock across microblock rounds (v15.0 h=2880-3150):
no microblock rounds → no per-voter round scatter.
* Split-brain producer at the same height (v15.13 h=556, v22 h=367):
single VRF leader; deterministic time-derived fallback with offset
spread ≤ 1 across honest nodes within ±2 s NTP jitter (pinned by
`tests_v22_slot_offset::offset_jitter_bound_within_two_seconds_ntp`).
* Macroblock-boundary cert deadlock (v21 h=360):
`mb.timeout_round == 0` always → cert presence gate (deleted) cannot
fire for any microblock.
* Empty-slot attestation race producing alternate-round block (v22
h=367): empty-slot cascade physically removed; no consensus state
advances on local empty-slot observation.
Scalability
-----------
Per-node steady-state cost: two timestamp loads + one integer division
per slot for the offset computation. Zero added bandwidth, zero added
storage, identical performance profile from 5 to 1M super-nodes.
1000-validator-per-macroblock committee cap unaffected.
Tests
-----
* `tests_v22_slot_offset` — 6 new tests in `node.rs`:
- offset_zero_under_healthy_production
- offset_zero_below_threshold
- offset_one_at_threshold
- offset_walks_forward_with_silence
- offset_saturates_on_backward_clock
- offset_jitter_bound_within_two_seconds_ntp
* `qnet-consensus` : 73 passed, 0 failed
* `qnet-integration` (serial) : 149 passed, 0 failed, 12 ignored
(hardware-bench)
* Total : 228 passed across both crates.
Build
-----
cargo build --release clean in 18m 47s, 0 warnings, 0 errors.
qnet-node.exe binary 22.3 MB optimised.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>1 parent b19c872 commit 10b972e
3 files changed
Lines changed: 396 additions & 1186 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1710 | 1710 | | |
1711 | 1711 | | |
1712 | 1712 | | |
1713 | | - | |
1714 | | - | |
1715 | | - | |
1716 | | - | |
1717 | | - | |
1718 | | - | |
1719 | | - | |
1720 | | - | |
1721 | | - | |
1722 | | - | |
1723 | | - | |
1724 | | - | |
1725 | | - | |
1726 | | - | |
1727 | | - | |
1728 | | - | |
1729 | | - | |
1730 | | - | |
1731 | | - | |
1732 | | - | |
1733 | | - | |
1734 | | - | |
1735 | | - | |
1736 | | - | |
1737 | | - | |
1738 | | - | |
1739 | | - | |
1740 | | - | |
1741 | | - | |
1742 | | - | |
1743 | | - | |
1744 | | - | |
1745 | | - | |
1746 | | - | |
1747 | | - | |
1748 | | - | |
1749 | | - | |
1750 | | - | |
1751 | | - | |
1752 | | - | |
1753 | | - | |
1754 | | - | |
1755 | | - | |
1756 | | - | |
1757 | | - | |
1758 | | - | |
1759 | | - | |
1760 | | - | |
1761 | | - | |
1762 | | - | |
1763 | | - | |
1764 | | - | |
1765 | | - | |
1766 | | - | |
1767 | | - | |
1768 | | - | |
1769 | | - | |
1770 | | - | |
1771 | | - | |
1772 | | - | |
1773 | | - | |
1774 | | - | |
1775 | | - | |
1776 | | - | |
1777 | | - | |
1778 | | - | |
1779 | | - | |
1780 | | - | |
1781 | | - | |
1782 | | - | |
1783 | | - | |
1784 | | - | |
1785 | | - | |
1786 | | - | |
1787 | | - | |
1788 | | - | |
1789 | | - | |
1790 | | - | |
1791 | | - | |
1792 | | - | |
1793 | | - | |
1794 | | - | |
1795 | | - | |
1796 | | - | |
1797 | | - | |
1798 | | - | |
1799 | | - | |
1800 | | - | |
1801 | | - | |
1802 | | - | |
1803 | | - | |
1804 | | - | |
1805 | | - | |
1806 | | - | |
1807 | | - | |
1808 | | - | |
1809 | | - | |
1810 | | - | |
1811 | | - | |
1812 | | - | |
1813 | | - | |
1814 | | - | |
1815 | | - | |
1816 | | - | |
1817 | | - | |
1818 | | - | |
1819 | | - | |
1820 | | - | |
1821 | | - | |
1822 | | - | |
1823 | | - | |
1824 | | - | |
1825 | | - | |
1826 | | - | |
1827 | | - | |
1828 | | - | |
1829 | | - | |
1830 | | - | |
1831 | | - | |
1832 | | - | |
1833 | | - | |
1834 | | - | |
1835 | | - | |
1836 | | - | |
1837 | | - | |
1838 | | - | |
1839 | | - | |
1840 | | - | |
1841 | | - | |
1842 | | - | |
1843 | | - | |
1844 | | - | |
1845 | | - | |
1846 | | - | |
1847 | | - | |
1848 | | - | |
1849 | | - | |
1850 | | - | |
1851 | | - | |
1852 | | - | |
1853 | | - | |
1854 | | - | |
1855 | | - | |
1856 | | - | |
1857 | | - | |
1858 | | - | |
1859 | | - | |
1860 | | - | |
1861 | | - | |
1862 | | - | |
1863 | | - | |
1864 | | - | |
1865 | | - | |
1866 | | - | |
1867 | | - | |
1868 | | - | |
1869 | | - | |
1870 | | - | |
1871 | | - | |
1872 | | - | |
1873 | | - | |
| 1713 | + | |
| 1714 | + | |
| 1715 | + | |
| 1716 | + | |
| 1717 | + | |
| 1718 | + | |
| 1719 | + | |
| 1720 | + | |
1874 | 1721 | | |
1875 | 1722 | | |
1876 | 1723 | | |
| |||
0 commit comments