heimdall-stake: sybil note and golden vectors
Some checks failed
CultMesh QUIC native / Build win32-x64 (pull_request) Has been skipped
CultMesh QUIC native / Build linux-x64 (pull_request) Has been skipped
CultMesh QUIC native / Release native binaries (pull_request) Has been skipped
CultMesh portability and performance / ubuntu-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest one-command multiplayer onboarding (pull_request) Has been cancelled
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Has been cancelled
CultMesh portability and performance / windows-latest reactive semantics and leases (pull_request) Has been cancelled
CultNet cross-runtime interop / C# / TypeScript / Rust / Python / Kotlin (pull_request) Has been cancelled
CultMesh QUIC native / Verify committed binaries match SHA256SUMS (pull_request) Has been cancelled

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 <noreply@anthropic.com>
This commit is contained in:
Libby 2026-11-05 21:27:41 +00:00
commit 6f68341edd
3 changed files with 70 additions and 0 deletions

View file

@ -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

View file

@ -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<i64> = (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");
}

View file

@ -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
1 # kind,base,unit_cents,amount_cents,k,expected_micro
2 # single: weight of one identity holding amount. split: budget (amount) split evenly over k identities (first amount%k hold one extra cent), summed.
3 # expected values computed independently with 60-digit decimal ln, rounded half up. No floor, cap or minimum.
4 single,10,100,100,1,1000000
5 single,10,100,1000,1,2000000
6 single,10,100,10000,1,3000000
7 single,10,100,1000000,1,5000000
8 single,10,100,50,1,698970
9 single,10,100,10,1,0
10 single,10,100,1,1,0
11 single,2,100,400,1,3000000
12 single,2,100,100,1,1000000
13 single,2,1,1,1,1000000
14 single,3,500,123456,1,6014537
15 single,7,1,987654321,1,11643268
16 single,10,1,1,1,1000000
17 single,16,250,99999,1,3160960
18 split,10,100,10000,1,3000000
19 split,10,100,10000,2,5397940
20 split,10,100,10000,10,20000000
21 split,10,100,10000,100,100000000
22 split,2,100,10000,1,7643856
23 split,2,100,10000,2,13287712
24 split,2,100,10000,10,43219280
25 split,2,100,10000,100,100000000
26 split,10,100,100000,1,4000000
27 split,10,100,100000,2,7397940
28 split,10,100,100000,10,30000000
29 split,10,100,100000,100,200000000