arborist/bench/scripts/t3_bound_calculator.py
russell@unturf.com da62f8047c
ticket #000036 Tier-1: apply dav1d 2026-05-11 review polish (no math change)
dav1d's review (RESPONSE_1 + RESPONSE_2) returned 2026-05-11. This
lands the Tier-1 items — everything that doesn't change numeric
outputs or invalidate the KAT discipline. The Tier-2 B1 conservative-
envelope (v2 calculator) is a separate decision and stays a closure
blocker.

Calculator (bench/scripts/t3_bound_calculator.py):
- Recommendation wording: "M2's single-window guarantee is broken"
  → "this conservative bound CANNOT CERTIFY M2's residual". An upper
  bound exceeding 256 bits means we cannot certify, NOT that the
  adversary can steer 256 bits — the prior wording overclaimed.
- New structured output fields: b1_model ("effective_control_v1"),
  certification_status ∈ {CERTIFIED_BY_BOUND, NOT_CERTIFIED_BY_BOUND},
  certification_threshold_bits (256), model_assumptions[]. Callers
  read a machine-readable status, not just prose.
- Input validation hardening: _require_finite_float / _require_positive_int
  helpers reject bools (isinstance(True, int) is True in Python — a
  real leak risk for a security calculator) and NaN / ±inf for every
  numeric input and constant.
- gradient_fraction = 0 now accepted (no T2 surface; B1 = 0; T3's
  LR + batch-order channels still contribute) — improves component
  isolation. CLI help + module docstring updated accordingly.
- Numeric outputs UNCHANGED: baseline still 625.8716 / 292.4813 /
  300.0 / 33.3904; b1_model stays effective_control_v1; KAT discipline
  intact.

Tests (tests/test_t3_bound_calculator.py, 53 → 75):
- Hard-coded cwd="/home/fox/git/arborist" → pathlib.Path(__file__).
  resolve().parents[1] so the suite runs on any checkout.
- New: test_gradient_fraction_zero_accepted, test_bool_rejected_for_int_fields,
  test_bool_rejected_for_float_fields, test_nonfinite_numbers_rejected,
  test_output_carries_b1_model_and_certification_fields,
  test_certification_status_certified_below_threshold.
- test_recommendation_exceeds_sha256 now also asserts "CANNOT CERTIFY"
  + certification_status == NOT_CERTIFIED_BY_BOUND.

Doc (docs/soft-hash-channel-t3-bound.md):
- §0 reworked into a reviewer brief recording dav1d's findings
  (§2 accepted, §4 accepted, §5 accepted as model-bound, §3 = closure
  blocker, wording/validation = applied).
- New §3.1: the B1-double-g issue spelled out — effective_control_v1
  vs fraction_channels vs aggregate_bias vs max_envelope, with the
  baseline-spread table (292 / 1730 / 5850 / 5850 bits); v2 path
  described.
- §5: "B3 is a model-bound, not a directly-quoted theorem" note.
- §10: items 1-2 are now the closure blockers (B1 envelope v2; active
  KAT fixture); items 3-7 are tightening paths (#000043). New §10.1
  records what the 2026-05-11 hardening pass already landed.
- §11: calculator-output example updated to show the new fields +
  corrected recommendation wording.
- §12: references add the dav1d review + clarify Bottou-Bousquet
  "inspires" (not "underlies") the §5 model-bound.

Status (#000036 ticket + TICKETS.md row): review-returned + Tier-1-
applied; closure blockers = B1 v2 envelope (awaits fox go/no-go) +
active KAT fixture. R2's architectural integrations (Merkle audit-
event commitment, SQD canonicalization, CTI clause-lattice, 5F
trigger, ForkScore security-risk) noted as out-of-scope (separate
tickets if wanted).

AUTOCOUNT markers in docs/calculator-test-patterns.md +
docs/warrant-substrate-cookbook.md bumped 53 → 75.

Full suite: 2264 passed, 28 skipped.
2026-05-11 06:39:27 -04:00

315 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""T3 per-window covert-channel budget calculator (#000036 §11).
Implements the closed-form bound from
`docs/soft-hash-channel-t3-bound.md` §6:
I_window ≤ C_B1 · g · W · log₂(SNR_grad + 1)
+ C_B2 · ⌈W / K⌉ · log₂(R)
+ C_B3 · ⌈W / E⌉ · log₂(N_b · σ_grad / ‖∇L_max‖) / 2
Inputs are operator-measurable on a deployment:
g T2 gradient-fraction the adversary controls
‖∇L_max‖ gradient-norm cap (clipping ceiling)
σ_grad per-step gradient stddev (SGD noise floor)
K LR-decision interval (steps between LR changes)
R LR-grid size
W nonce-window length (steps before re-anchor)
N_b batches per epoch
E steps per epoch (typically equals N_b)
Conservative defaults: C_B1 = C_B2 = C_B3 = 1 (each by data-
processing inequality; see §6 + §10). Operators with deployment-
specific empirical measurements of the constants pass them via
``--c-b1`` / ``--c-b2`` / ``--c-b3`` flags.
**B1 model.** This v1 calculator uses ``b1_model="effective_control_v1"``:
``g`` is read as an *effective gradient-control coefficient* that
simultaneously bounds the fraction of steerable directions AND the
amplitude shrinkage of the aggregate adversarial gradient. Under that
reading the B1 formula ``C_B1 · g · W · log₂(1 + g·G/σ)`` is sound,
but it is **not the most-conservative gradient adversary** — see
#000036 §3 + §10 for the alternative ``b1_model="max_envelope"``
path (deferred to a v2 bump). For now ``certification_status`` is
relative to this v1 effective-control model.
Pure stdlib. No numpy / scipy dependency — the math is
arithmetic + math.log2.
Usage:
python -m bench.scripts.t3_bound_calculator \\
--gradient-fraction 0.05 \\
--gradient-norm-max 1.0 \\
--gradient-noise-stddev 0.1 \\
--lr-decision-interval 100 \\
--lr-grid-size 8 \\
--window-length 10000 \\
--batches-per-epoch 1024 \\
--steps-per-epoch 1024
"""
from __future__ import annotations
import argparse
import json
import math
import sys
CALCULATOR_VERSION = "t3-bound-v1-bottou-refinement"
B1_MODEL = "effective_control_v1"
CERTIFICATION_THRESHOLD_BITS = 256
def _require_finite_float(name: str, value: object) -> float:
"""Reject bools, non-numbers, NaN, and infinities; return float.
Python's ``isinstance(True, int)`` is True, so a bare ``isinstance``
check lets ``True`` / ``False`` leak into a numeric calculation —
unacceptable for a security calculator. Reject bools explicitly.
"""
if isinstance(value, bool) or not isinstance(value, (int, float)):
raise ValueError(f"{name} must be a finite number; got {value!r}")
fv = float(value)
if not math.isfinite(fv):
raise ValueError(f"{name} must be finite (no NaN / inf); got {value!r}")
return fv
def _require_positive_int(name: str, value: object) -> int:
"""Reject bools and non-ints; require value > 0.
``type(value) is int`` (not ``isinstance``) rejects ``True`` / ``False``.
"""
if type(value) is not int or value <= 0:
raise ValueError(f"{name} must be a positive int; got {value!r}")
return value
def t3_bound_bits(
*,
gradient_fraction: float, # g
gradient_norm_max: float, # ‖∇L_max‖
gradient_noise_stddev: float, # σ_grad
lr_decision_interval: int, # K
lr_grid_size: int, # R
window_length: int, # W
batches_per_epoch: int, # N_b
steps_per_epoch: int, # E
c_b1: float = 1.0,
c_b2: float = 1.0,
c_b3: float = 1.0,
) -> dict:
"""Compute the per-window mutual-information bound + per-bandwidth
contributions per #000036 §6.
Returns a dict with the total bound, three per-bandwidth
contributions, the constants in use, and a sentence of operator
guidance based on the bound vs the SHA-256 (256-bit) baseline.
"""
# Validation — reject bools / NaN / inf; coerce numeric types.
gradient_fraction = _require_finite_float("gradient_fraction", gradient_fraction)
if not 0 <= gradient_fraction <= 1:
# 0 is allowed: a deployment with no T2 gradient surface still
# has T3's LR + batch-order channels (B2, B3), so B1=0 is a
# meaningful component-isolation case.
raise ValueError(
f"gradient_fraction must be in [0, 1]; got {gradient_fraction}"
)
gradient_norm_max = _require_finite_float("gradient_norm_max", gradient_norm_max)
if gradient_norm_max <= 0:
raise ValueError(f"gradient_norm_max must be > 0; got {gradient_norm_max}")
gradient_noise_stddev = _require_finite_float(
"gradient_noise_stddev", gradient_noise_stddev
)
if gradient_noise_stddev <= 0:
raise ValueError(
f"gradient_noise_stddev must be > 0; got {gradient_noise_stddev}"
)
lr_decision_interval = _require_positive_int(
"lr_decision_interval", lr_decision_interval
)
lr_grid_size = _require_positive_int("lr_grid_size", lr_grid_size)
window_length = _require_positive_int("window_length", window_length)
batches_per_epoch = _require_positive_int("batches_per_epoch", batches_per_epoch)
steps_per_epoch = _require_positive_int("steps_per_epoch", steps_per_epoch)
c_b1 = _require_finite_float("c_b1", c_b1)
c_b2 = _require_finite_float("c_b2", c_b2)
c_b3 = _require_finite_float("c_b3", c_b3)
for name, c in [("c_b1", c_b1), ("c_b2", c_b2), ("c_b3", c_b3)]:
if not 0 <= c <= 1:
raise ValueError(f"{name} must be in [0, 1]; got {c}")
# B1 — gradient bias. Per-step discrete channel-capacity bound on
# the adversary-controlled parameter shift (NOT Fano's inequality —
# see #000036 §3): the per-step shift falls in one of ~SNR_grad+1
# distinguishable buckets, capacity ≤ log₂(SNR_grad+1). Under the
# b1_model="effective_control_v1" reading, g bounds both the
# steerable-direction fraction and the aggregate amplitude shrinkage.
snr_grad = gradient_fraction * gradient_norm_max / gradient_noise_stddev
b1_per_step = math.log2(snr_grad + 1)
b1 = c_b1 * gradient_fraction * window_length * b1_per_step
# B2 — LR selection. R-symbol categorical channel at every K-step
# decision point (#000036 §4).
decisions_in_window = -(-window_length // lr_decision_interval) # ceil
b2 = c_b2 * decisions_in_window * math.log2(lr_grid_size)
# B3 — Batch order. Bottou-Bousquet refinement: per-epoch contribution
# bounded by 0.5 · log₂(N_b · σ_grad / ‖∇L_max‖) (#000036 §5).
epochs_in_window = -(-window_length // steps_per_epoch)
b3_per_epoch_factor = batches_per_epoch * gradient_noise_stddev / gradient_norm_max
if b3_per_epoch_factor <= 1:
# log₂ of a value ≤ 1 is ≤ 0; the bound becomes trivial (the
# adversarial order can't move the parameter trajectory beyond
# the noise floor). Floor at 0 — adversary can't do worse than
# random shuffle in this regime.
b3_per_epoch = 0.0
else:
b3_per_epoch = math.log2(b3_per_epoch_factor) / 2.0
b3 = c_b3 * epochs_in_window * b3_per_epoch
total = b1 + b2 + b3
# Operator guidance — I_window is an UPPER BOUND. If it exceeds the
# 256-bit threshold we do NOT know the adversary can steer 256 bits;
# we know only that this bound is too loose to certify safety. The
# wording reflects that (per dav1d review 2026-05-11).
sha256_bits = CERTIFICATION_THRESHOLD_BITS
if total <= 0:
certification_status = "CERTIFIED_BY_BOUND"
recommendation = (
"I_window upper bound ≤ 0 (degenerate inputs); the bound is "
"vacuously below the 256-bit certification threshold."
)
elif total >= sha256_bits:
certification_status = "NOT_CERTIFIED_BY_BOUND"
recommendation = (
f"I_window upper bound ≈ {total:.1f} bits/window EXCEEDS the "
f"SHA-256 ({sha256_bits} bit) certification threshold. This "
f"conservative bound CANNOT CERTIFY M2's single-window "
f"residual at this W — it does not prove the adversary can "
f"steer {sha256_bits} bits, only that the bound is too loose "
f"to certify safety. Reduce W (or reduce g / R / increase K, "
f"or tighten C_B* empirically) until the certified bound is "
f"< {sha256_bits} bits/window."
)
else:
certification_status = "CERTIFIED_BY_BOUND"
windows_to_brute_force = 2 ** (sha256_bits - total)
recommendation = (
f"I_window upper bound ≈ {total:.1f} bits/window. Certified "
f"residual ≥ {sha256_bits - total:.1f} bits per independent "
f"nonce window under this leakage model; a {sha256_bits}-bit "
f"specific-target search remains bounded below by ≈ "
f"2^{sha256_bits - total:.1f}{windows_to_brute_force:.2e} "
f"independent windows (assumes fresh nonce per window, no "
f"structural advantage beyond the bounded channel)."
)
return {
"calculator_version": CALCULATOR_VERSION,
"b1_model": B1_MODEL,
"I_window_bits_upper_bound": round(total, 4),
"B1_contribution": round(b1, 4),
"B2_contribution": round(b2, 4),
"B3_contribution": round(b3, 4),
"snr_grad": round(snr_grad, 4),
"decisions_in_window": decisions_in_window,
"epochs_in_window": epochs_in_window,
"constants": {"C_B1": c_b1, "C_B2": c_b2, "C_B3": c_b3},
"certification_status": certification_status,
"certification_threshold_bits": sha256_bits,
"model_assumptions": [
"M2_nonce_per_window",
"SHA256_random_oracle_baseline",
"B1_effective_control_model_v1",
"B3_gradient_noise_refinement",
],
"inputs": {
"gradient_fraction": gradient_fraction,
"gradient_norm_max": gradient_norm_max,
"gradient_noise_stddev": gradient_noise_stddev,
"lr_decision_interval": lr_decision_interval,
"lr_grid_size": lr_grid_size,
"window_length": window_length,
"batches_per_epoch": batches_per_epoch,
"steps_per_epoch": steps_per_epoch,
},
"recommendation": recommendation,
}
def main(argv: list[str] | None = None) -> int:
p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
p.add_argument(
"--gradient-fraction", type=float, required=True,
help="g — effective gradient-control coefficient (b1_model=effective_control_v1); in [0, 1], 0 = no T2 surface",
)
p.add_argument(
"--gradient-norm-max", type=float, required=True,
help="‖∇L_max‖ — gradient-norm clipping ceiling (deployment param)",
)
p.add_argument(
"--gradient-noise-stddev", type=float, required=True,
help="σ_grad — per-step gradient noise stddev (SGD noise floor)",
)
p.add_argument(
"--lr-decision-interval", type=int, required=True,
help="K — steps between LR-schedule decision points",
)
p.add_argument(
"--lr-grid-size", type=int, required=True,
help="R — number of LR levels in the schedule grid",
)
p.add_argument(
"--window-length", type=int, required=True,
help="W — steps per nonce-window (M2 anchor re-randomization period)",
)
p.add_argument(
"--batches-per-epoch", type=int, required=True,
help="N_b — batch count per training epoch",
)
p.add_argument(
"--steps-per-epoch", type=int, required=True,
help="E — SGD steps per epoch (typically equals batches-per-epoch)",
)
p.add_argument(
"--c-b1", type=float, default=1.0,
help="C_B1 channel-efficiency constant; default 1.0 (data-processing-inequality ceiling)",
)
p.add_argument(
"--c-b2", type=float, default=1.0,
help="C_B2 constant; default 1.0",
)
p.add_argument(
"--c-b3", type=float, default=1.0,
help="C_B3 constant; default 1.0",
)
args = p.parse_args(argv)
try:
result = t3_bound_bits(
gradient_fraction=args.gradient_fraction,
gradient_norm_max=args.gradient_norm_max,
gradient_noise_stddev=args.gradient_noise_stddev,
lr_decision_interval=args.lr_decision_interval,
lr_grid_size=args.lr_grid_size,
window_length=args.window_length,
batches_per_epoch=args.batches_per_epoch,
steps_per_epoch=args.steps_per_epoch,
c_b1=args.c_b1,
c_b2=args.c_b2,
c_b3=args.c_b3,
)
except ValueError as e:
print(f"input error: {e}", file=sys.stderr)
return 2
print(json.dumps(result, indent=2, ensure_ascii=False))
return 0
if __name__ == "__main__":
sys.exit(main())