From 525d081a3c710b823d214db72d101523f61b0f8b Mon Sep 17 00:00:00 2001 From: Marcin Pawlowski Date: Fri, 7 Aug 2026 17:53:47 +0200 Subject: [PATCH] Keep the deployed rule as the default; make the proposal an explicit arm I had planned to flip uncle_window_anchor's default to "parent" so new configs would measure the proposed design by default. Tried it, and it breaks a documented guarantee: key() appends the anchor only when it is not "uncle", so with "parent" as the default an --old run's key is no longer byte-identical to the pre-redesign key and --old stops bit-reproducing historical runs (sec 9). o.key() == o._base_key() fails outright. Reverted, and on reflection the default was wrong for a second reason anyway. The report's job is to describe the protocol as deployed and to RECOMMEND changes; the default should therefore be the deployed rule, with the proposal as an explicit arm. That is exactly the convention fixed_point already follows (default exact f = the analysis convention, explicit True = spec-faithful). Both reasons are recorded on the field. To make the distinction visible rather than implicit, the spec-as-is studies now pin uncle_window_anchor: uncle explicitly -- spec_point.py, spec_jitter.py and the three spec-point-*.yaml configs answer "what does the DEPLOYED chain do", so they must not drift onto a proposal if a default ever moves. Adds uncle_window_anchor as a sweep axis (SweepConfig field plus _SWEEP_AXES), and configs/absorption-window-anchor.yaml: the sec 3.4 absorption sweep re-run under both anchors at three delays. That study is the one that can move a recommendation -- the W >= 7/f floor was measured against the uncle gap, and the parent gap runs about one block-interval longer, so the floor should sit higher and the margin behind W = 10/f shrink. Co-Authored-By: Claude Fable 5 --- .../configs/absorption-window-anchor.yaml | 37 +++++++++++++++++++ .../configs/spec-point-jitter.yaml | 1 + .../configs/spec-point-n5000.yaml | 1 + .../configs/spec-point-window.yaml | 1 + .../tsi-sim-pernode/scripts/spec_jitter.py | 3 +- .../tsi/tsi-sim-pernode/scripts/spec_point.py | 3 +- .../tsi/tsi-sim-pernode/src/tsi_sim/config.py | 9 ++++- 7 files changed, 52 insertions(+), 3 deletions(-) create mode 100644 tools/simulators/tsi/tsi-sim-pernode/configs/absorption-window-anchor.yaml diff --git a/tools/simulators/tsi/tsi-sim-pernode/configs/absorption-window-anchor.yaml b/tools/simulators/tsi/tsi-sim-pernode/configs/absorption-window-anchor.yaml new file mode 100644 index 0000000..a1bdefd --- /dev/null +++ b/tools/simulators/tsi/tsi-sim-pernode/configs/absorption-window-anchor.yaml @@ -0,0 +1,37 @@ +# Does anchoring the reference window to the uncle's PARENT move the window floor? (§3.4, §6.12) +# +# The `W >= 7/f` recovery floor was measured against the UNCLE's slot. Under the §6.12 proposal +# the window is measured to the uncle's PARENT, and the parent gap runs about one block-interval +# longer (measured: median 54 slots against ~30), so the same numeric W is an effectively tighter +# window. The floor should therefore sit roughly one block-interval higher — which matters, +# because W = 10/f is a recommendation whose justification is its margin over that floor. If the +# floor moves 7 -> 8, the margin falls from ~1.43x to ~1.25x and the recommendation needs +# restating even though the value does not change. +# +# This is the direct re-measurement: the same absorption sweep, both anchors, side by side. +# U = 1 isolates the window — with more uncle slots the queue drains faster and the window stops +# being the binding constraint, which would hide the knee. +# +# Note the two anchors draw independent RNG streams (uncle_window_anchor is in the key when it is +# not the default), so this is an UNPAIRED comparison and the knee position, not a per-cell +# difference, is what it resolves. Latency is in SLOTS (1 slot = 1 s). +n_nodes: [1000] # network size +stake_dist: [pareto] # heavy-tailed (realistic) stake distribution +topology: [blend] # Blend mixnet (delay stresses the window) +degree: [6] # peering degree of the d-regular graph +link_latency_mean: [0.5] # natural geographic transport (sub-slot) +link_latency_dist: [geo] # real-world geographic band mixture +blend_hops: [3] # fixed hop count; delay is the swept knob +blend_delay_max: [4.0, 8.0, 16.0] # deployed point, design point, the load boundary +window_absorption: [3, 5, 6, 7, 8, 9, 10, 12, 15, 20] # W in block-intervals; w_u = W/f slots +uncle_window_anchor: [uncle, parent] # THE comparison: spec rule vs the §6.12 proposal +max_uncles: [1] # FIXED at one uncle: the question is about W +uncle_strategy: [oldest] # spec Uncle Selection +init_dest: [common] # per-node initial D_est from agreement +replicates: 12 # enough to place a knee to ~1 block-interval +base: # per-run settings shared by every cell + k: 2160 # true security parameter + epochs: 20 # equilibrium within ~2 epochs; burn 50% + f: 0.03333333333333333 # slot activation coefficient (1/30) + genesis_d_factor: 0.5 # start near true stake (cheap epoch 0) + early_stop: true diff --git a/tools/simulators/tsi/tsi-sim-pernode/configs/spec-point-jitter.yaml b/tools/simulators/tsi/tsi-sim-pernode/configs/spec-point-jitter.yaml index aad655b..aef94f4 100644 --- a/tools/simulators/tsi/tsi-sim-pernode/configs/spec-point-jitter.yaml +++ b/tools/simulators/tsi/tsi-sim-pernode/configs/spec-point-jitter.yaml @@ -34,6 +34,7 @@ blend_hops: [3] # the spec's Blend cascade length blend_delay_max: [4.0] # the spec's operating point (rho ~ 0.27) max_uncles: [0, 1, 2, 4] # 0 = negative control; 4 = the spec's MAX_UNCLES uncle_strategy: [oldest] # spec Uncle Selection +uncle_window_anchor: [uncle] # SPEC AS DEPLOYED, not the sec 6.12 proposal init_dest: [common] # per-node initial D_est from agreement replicates: 12 # exact-oracle runs are ~17 s each; 12 x 40 cells base: # per-run settings shared by every cell diff --git a/tools/simulators/tsi/tsi-sim-pernode/configs/spec-point-n5000.yaml b/tools/simulators/tsi/tsi-sim-pernode/configs/spec-point-n5000.yaml index 1e37307..c7670a1 100644 --- a/tools/simulators/tsi/tsi-sim-pernode/configs/spec-point-n5000.yaml +++ b/tools/simulators/tsi/tsi-sim-pernode/configs/spec-point-n5000.yaml @@ -25,6 +25,7 @@ blend_hops: [3] # the spec's Blend cascade length blend_delay_max: [4.0] # the spec's 2 s dwell, matched in the mean max_uncles: [0, 1, 2, 4] # 0 = the C3 column; 4 = the spec's MAX_UNCLES uncle_strategy: [oldest] # spec Uncle Selection +uncle_window_anchor: [uncle] # SPEC AS DEPLOYED, not the sec 6.12 proposal init_dest: [common] # per-node initial D_est from agreement replicates: 12 # N = 5000 at true k is heavy; 12 gives ~±0.2 pp base: # per-run settings shared by every cell diff --git a/tools/simulators/tsi/tsi-sim-pernode/configs/spec-point-window.yaml b/tools/simulators/tsi/tsi-sim-pernode/configs/spec-point-window.yaml index 6ed8c7c..b4277e1 100644 --- a/tools/simulators/tsi/tsi-sim-pernode/configs/spec-point-window.yaml +++ b/tools/simulators/tsi/tsi-sim-pernode/configs/spec-point-window.yaml @@ -24,6 +24,7 @@ blend_delay_max: [4.0] # the spec's operating point window_absorption: [1, 2, 3, 5, 7, 10, 15, 20] # W in expected block-intervals; w_u = W/f slots max_uncles: [1] # FIXED at one uncle: the question is about W uncle_strategy: [oldest] # spec Uncle Selection +uncle_window_anchor: [uncle] # SPEC AS DEPLOYED, not the sec 6.12 proposal init_dest: [common] # per-node initial D_est from agreement replicates: 12 # enough to place the knee, not to resolve 0.1 pp base: # per-run settings shared by every cell diff --git a/tools/simulators/tsi/tsi-sim-pernode/scripts/spec_jitter.py b/tools/simulators/tsi/tsi-sim-pernode/scripts/spec_jitter.py index e934738..b92265f 100644 --- a/tools/simulators/tsi/tsi-sim-pernode/scripts/spec_jitter.py +++ b/tools/simulators/tsi/tsi-sim-pernode/scripts/spec_jitter.py @@ -51,7 +51,8 @@ SPEC_POINT = dict(n_nodes=1000, stake_dist="pareto", topology="blend", degree=6, link_latency_mean=0.5, link_latency_dist="geo", blend_hops=3, blend_delay_max=4.0, uncle_strategy="oldest", window_absorption=10.0, k=2160, epochs=20, genesis_d_factor=0.5, early_stop=True, - windowed_fork_choice=False, prune_arrival=False) + windowed_fork_choice=False, prune_arrival=False, + uncle_window_anchor="uncle") # SPEC AS DEPLOYED, not the §6.12 proposal def _cell(model: str, jitter: float, u: int, rep: int) -> dict: diff --git a/tools/simulators/tsi/tsi-sim-pernode/scripts/spec_point.py b/tools/simulators/tsi/tsi-sim-pernode/scripts/spec_point.py index c419ad6..a9e1a47 100644 --- a/tools/simulators/tsi/tsi-sim-pernode/scripts/spec_point.py +++ b/tools/simulators/tsi/tsi-sim-pernode/scripts/spec_point.py @@ -48,7 +48,8 @@ SPEC_POINT = dict(n_nodes=1000, stake_dist="pareto", topology="blend", degree=6, link_latency_mean=0.5, link_latency_dist="geo", blend_hops=3, blend_delay_max=4.0, max_uncles=4, uncle_strategy="oldest", window_absorption=10.0, k=2160, epochs=EPOCHS, - genesis_d_factor=0.5, early_stop=True) + genesis_d_factor=0.5, early_stop=True, + uncle_window_anchor="uncle") # SPEC AS DEPLOYED, not the §6.12 proposal ARMS = [("exact f (report convention)", False, 1_000_000), ("spec today (PRECISION = 1e3)", True, 1_000), diff --git a/tools/simulators/tsi/tsi-sim-pernode/src/tsi_sim/config.py b/tools/simulators/tsi/tsi-sim-pernode/src/tsi_sim/config.py index 42ea6aa..494b538 100644 --- a/tools/simulators/tsi/tsi-sim-pernode/src/tsi_sim/config.py +++ b/tools/simulators/tsi/tsi-sim-pernode/src/tsi_sim/config.py @@ -144,7 +144,12 @@ class SimConfig: # take the oldest candidates first, deterministically, because an uncle expires w_u slots # after its own slot so the oldest are the closest to expiring. Every headline result uses it. uncle_strategy: UncleStrategy = "oldest" - uncle_window_anchor: UncleWindowAnchor = "uncle" # "parent" = the proposed rule + # DEFAULT is the deployed rule, so the simulator describes the protocol as it stands and the + # §6.12 proposal is an explicit arm — the same convention `fixed_point` follows. It must also + # stay "uncle" for a harder reason: key() appends the anchor only when it is not "uncle", so + # this default is what keeps an --old run's key byte-identical to the pre-redesign key and + # lets it bit-reproduce every historical run (§9). + uncle_window_anchor: UncleWindowAnchor = "uncle" # "random" is NOT a spec variant — it is the deviation probe: walk the same oldest-first # candidate order but include each candidate with probability uncle_random_p, so a lone # candidate is dropped half the time. Uncle selection is proposer-local and unvalidated, so a @@ -436,6 +441,7 @@ class SimConfig: # Axes that can be swept; every SimConfig field is legal here. _SWEEP_AXES = ( "n_nodes", "stake_dist", "latency", "max_uncles", "uncle_strategy", "uncle_window", + "uncle_window_anchor", "window_absorption", "topology", "degree", "link_latency_mean", "link_latency_dist", "blend_hops", "blend_delay_max", "init_dest", "f", @@ -452,6 +458,7 @@ class SweepConfig: max_uncles: list[int] = field(default_factory=lambda: [0, 1, 2, 4]) uncle_strategy: list[UncleStrategy] = field(default_factory=lambda: ["oldest"]) uncle_window: list[int] = field(default_factory=lambda: [constants.W_DEFAULT]) + uncle_window_anchor: list[UncleWindowAnchor] = field(default_factory=lambda: ["uncle"]) window_absorption: list[float] = field(default_factory=lambda: [constants.W_ABS_DEFAULT]) topology: list[Topology] = field(default_factory=lambda: ["regular"]) degree: list[int] = field(default_factory=lambda: [8])