mirror of
https://github.com/logos-blockchain/research.git
synced 2026-08-07 11:43:20 +00:00
full_deanon counted any sender with an adversarial peer as identified. Capturing a
cascade tells the adversary WHICH message it is following, not WHO started it:
seeing an honest X transmit is equally consistent with X having received it from a
peer the adversary cannot watch. Separating the two gives
confidence = 1/(2 - a/d) = d/(2d - a)
for a adversarial peers of degree d. The path length does not enter -- the
conditioning event already fixes the relays as adversarial, so an honest X is not
one of them for this message.
The consequence is large. One peer of eight is worth 0.53, barely above the 0.5
prior, and 90% confidence needs a >= 8: every peer, which is the ECLIPSE condition
rather than the observation condition. Measured, attributable_frac_90 equals
eclipsed_frac exactly. At f_adv = 0.2, degree 8 that is 2.6e-6 against an
observed_frac of 0.83 -- the published figure overstates confident origination by
five orders of magnitude.
Stated in the report as a bracket rather than a replacement: full_deanon is the
upper bound on adversary capability, this is the lower bound, and the truth lies
between because the adversary also learns from the sender neighbourhood. Closing
that gap needs a k-hop observability model and is recorded as open in section 5.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
96 lines
3.7 KiB
Python
96 lines
3.7 KiB
Python
"""Flat parquet-row builders for the result tables."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from .config import SimConfig
|
|
|
|
|
|
def propagation_row(config: SimConfig, blend_hops: int, max_blend_delay: int,
|
|
unresponsive_frac: float, redundancy: int, prop: dict,
|
|
churn_mode: str | None = None) -> dict:
|
|
return {
|
|
"n_nodes": config.n_nodes,
|
|
"degree": config.degree,
|
|
"blend_hops": blend_hops,
|
|
"max_blend_delay": max_blend_delay,
|
|
"unresponsive_frac": unresponsive_frac,
|
|
"churn_mode": churn_mode or config.churn_mode,
|
|
"n_regions": config.n_regions,
|
|
"region_locality": config.region_locality,
|
|
"redundancy": redundancy,
|
|
"graph_seed": config.graph_seed,
|
|
"n_rounds": config.n_rounds,
|
|
"transport_jitter_mean_ms": config.transport_jitter_mean_ms,
|
|
"processing_lags_ms": str(tuple(config.processing_lags_ms)),
|
|
"processing_lag_probs": str(tuple(config.processing_lag_probs)),
|
|
**prop,
|
|
}
|
|
|
|
|
|
def traffic_row(config: SimConfig, blend_hops: int, max_blend_delay: int,
|
|
cover_rate_mult: float, traffic: dict, quota: dict) -> dict:
|
|
"""One cover-traffic cell: what the timeline measured, plus the epoch emission budget.
|
|
|
|
``traffic`` comes from the windowed simulation (blending, mixing, counts) and ``quota`` from
|
|
the epoch-scale emission budget, which needs no graph and so is computed separately.
|
|
"""
|
|
return {
|
|
"n_nodes": config.n_nodes,
|
|
"degree": config.degree,
|
|
"blend_hops": blend_hops,
|
|
"max_blend_delay": max_blend_delay,
|
|
"cover_rate_mult": cover_rate_mult,
|
|
"block_interval_slots": config.block_interval_slots,
|
|
"slots_per_epoch": config.slots_per_epoch,
|
|
"stake_dist": config.stake_dist,
|
|
"stake_inference_ratio": config.stake_inference_ratio,
|
|
"graph_seed": config.graph_seed,
|
|
"traffic_window_slots": config.traffic_window_slots,
|
|
**traffic,
|
|
**quota,
|
|
}
|
|
|
|
|
|
def adversary_row(config: SimConfig, f_adv: float, mode: str, placement_rep: int,
|
|
adv: dict) -> dict:
|
|
return {
|
|
"n_nodes": config.n_nodes,
|
|
"degree": config.degree,
|
|
"f_adv": f_adv,
|
|
"adversary_mode": mode,
|
|
"graph_seed": config.graph_seed,
|
|
"placement_rep": placement_rep,
|
|
**adv,
|
|
}
|
|
|
|
|
|
def deanon_row(config: SimConfig, blend_hops: int, f_adv: float, mode: str,
|
|
placement_rep: int, redundancy: int, adv: dict, deanon: dict,
|
|
att: dict | None = None) -> dict:
|
|
"""One row of the deanonymization table: a (placement x blend-path-length x redundancy) cell.
|
|
|
|
``blend_hops`` and ``redundancy`` come from the propagation grid, the rest from the adversary
|
|
placement; they are crossed here because deanonymization is where propagation paths meet the
|
|
adversary set.
|
|
"""
|
|
return {
|
|
"n_nodes": config.n_nodes,
|
|
"degree": config.degree,
|
|
"blend_hops": blend_hops,
|
|
"redundancy": redundancy,
|
|
"f_adv": f_adv,
|
|
"adversary_mode": mode,
|
|
"graph_seed": config.graph_seed,
|
|
"placement_rep": placement_rep,
|
|
"n_adv": adv["n_adv"],
|
|
"n_honest": adv["n_honest"],
|
|
"observed_frac": adv["observed_frac"],
|
|
**deanon,
|
|
**(att or {}),
|
|
# Confidence-weighted attribution: the whole-path capture rate times the share of nodes the
|
|
# adversary could actually name as originator at that confidence, rather than the binary
|
|
# "has an adversarial peer" that full_deanon_rate uses.
|
|
**({f"confident_deanon_{k.rsplit('_', 1)[1]}": deanon["deanon_rate"] * v
|
|
for k, v in att.items() if k.startswith("attributable_frac_")} if att else {}),
|
|
}
|