From 6f68341edd26a8d0bfa3368b418c103ee949acdf Mon Sep 17 00:00:00 2001 From: Libby Date: Thu, 5 Nov 2026 21:27:41 +0000 Subject: [PATCH] heimdall-stake: sybil note and golden vectors README: the constant 1 in weight = 1 + log_b(amount/unit) is paid once per identity, so splitting always pays until the log goes negative and no base fixes it; the defence lives outside the function. Golden vectors (single and split k=1,2,10,100, three bases) computed with 60-digit decimal ln, pinned by tests/golden.rs. Co-Authored-By: Claude Sonnet 5.5 --- packages/heimdall-stake/README.md | 11 +++++++ packages/heimdall-stake/tests/golden.rs | 30 +++++++++++++++++++ .../tests/vectors/stake_golden.csv | 29 ++++++++++++++++++ 3 files changed, 70 insertions(+) create mode 100644 packages/heimdall-stake/tests/golden.rs create mode 100644 packages/heimdall-stake/tests/vectors/stake_golden.csv diff --git a/packages/heimdall-stake/README.md b/packages/heimdall-stake/README.md index 03b8dc32..5c8ae095 100644 --- a/packages/heimdall-stake/README.md +++ b/packages/heimdall-stake/README.md @@ -63,6 +63,17 @@ as a cliff, not a slope: with a 50 EUR minimum, 100 EUR split in two still gains in the example), and split in three weighs 0. So the minimum has to sit near the size of a typical budget to deter splitting, and the report is how to see where it does. +## Why the sybil defence cannot live here + +`weight = 1 + log_b(amount / unit)` pays the constant 1 once per identity. Splitting a budget into +k identities pays that 1 k times and costs only the logarithm's shrinkage, so splitting wins until +the log term goes negative: with unit 1 EUR and b = 10, 100 EUR weighs 3 as one identity, 5.40 as +two, 20 as ten and 100 as one hundred (`tests/vectors/stake_golden.csv`, pinned by +`tests/golden.rs`). No choice of `b` fixes this: `b` scales the log term and never the constant. +A cap or a per-identity minimum only moves the cliff (see Splitting gain). So the defence belongs +outside the function: non-transferable or vesting stake, a per-person cap on total stake, and a +cost to create an identity. This crate states the incentive; it cannot remove it. + ## Where this sits in the estate rule This crate does not normalize. The estate rule normalizes weights above it; the weights here are diff --git a/packages/heimdall-stake/tests/golden.rs b/packages/heimdall-stake/tests/golden.rs new file mode 100644 index 00000000..d742aa5a --- /dev/null +++ b/packages/heimdall-stake/tests/golden.rs @@ -0,0 +1,30 @@ +use heimdall_stake::*; + +const VECTORS: &str = include_str!("vectors/stake_golden.csv"); + +/// Pins `weight` and `total_weight` to values computed outside this crate (see the fixture header). +/// Split rows are the sybil argument in numbers: the same budget in k identities weighs more. +#[test] +fn golden_vectors() { + let mut rows = 0; + let mut split_rows = 0; + for line in VECTORS.lines().filter(|l| !l.starts_with('#') && !l.trim().is_empty()) { + let f: Vec<&str> = line.split(',').collect(); + let (base, unit, amount, k, expected): (u32, u64, i64, usize, u64) = + (f[1].parse().unwrap(), f[2].parse().unwrap(), f[3].parse().unwrap(), f[4].parse().unwrap(), f[5].parse().unwrap()); + let p = Params { base, unit_cents: unit, floor_cents: 0, per_identity_cap: None, min_amount_per_identity_cents: 0 }; + let got = match f[0] { + "single" => weight(&p, amount).unwrap(), + "split" => { + split_rows += 1; + let (q, r) = (amount / k as i64, (amount % k as i64) as usize); + let shares: Vec = (0..k).map(|i| q + i64::from(i < r)).collect(); + total_weight(&p, &shares).unwrap() + } + other => panic!("unknown vector kind {other}"), + }; + assert_eq!(got, expected, "vector `{line}`"); + rows += 1; + } + assert!(rows >= 20 && split_rows >= 12, "fixture shrank: {rows} rows, {split_rows} split"); +} diff --git a/packages/heimdall-stake/tests/vectors/stake_golden.csv b/packages/heimdall-stake/tests/vectors/stake_golden.csv new file mode 100644 index 00000000..26f8978a --- /dev/null +++ b/packages/heimdall-stake/tests/vectors/stake_golden.csv @@ -0,0 +1,29 @@ +# kind,base,unit_cents,amount_cents,k,expected_micro +# single: weight of one identity holding amount. split: budget (amount) split evenly over k identities (first amount%k hold one extra cent), summed. +# expected values computed independently with 60-digit decimal ln, rounded half up. No floor, cap or minimum. +single,10,100,100,1,1000000 +single,10,100,1000,1,2000000 +single,10,100,10000,1,3000000 +single,10,100,1000000,1,5000000 +single,10,100,50,1,698970 +single,10,100,10,1,0 +single,10,100,1,1,0 +single,2,100,400,1,3000000 +single,2,100,100,1,1000000 +single,2,1,1,1,1000000 +single,3,500,123456,1,6014537 +single,7,1,987654321,1,11643268 +single,10,1,1,1,1000000 +single,16,250,99999,1,3160960 +split,10,100,10000,1,3000000 +split,10,100,10000,2,5397940 +split,10,100,10000,10,20000000 +split,10,100,10000,100,100000000 +split,2,100,10000,1,7643856 +split,2,100,10000,2,13287712 +split,2,100,10000,10,43219280 +split,2,100,10000,100,100000000 +split,10,100,100000,1,4000000 +split,10,100,100000,2,7397940 +split,10,100,100000,10,30000000 +split,10,100,100000,100,200000000