Attribution
For "who did what" to hold, three things must be true at once: key custody, deterministic sampling, and memory that follows the event stream. If any one is missing, attribution stops at "some key signed this entry" instead of "some participant did this."
#The three conditions for attribution
| Condition | Current state | Evidence |
|---|---|---|
| Key custody | Does not hold | In the current product path the private key is a plaintext file readable by the same OS user, with no hardware protection |
| Deterministic sampling | Partly holds | Deterministic replay is a real anchor, but the sampling seed is hard-coded and publicly predictable |
| Memory follows the event stream | Holds | The accounting unit of reputation is the event stream; if you switch the model or wipe the memory, the credit does not carry over |
This sentence appears again in multi-party execution, where its concrete consequences are laid out.
#Twin identity
vacant_id is derived from the public key, not looked up:
vacant_id = "z" + base58btc( multihash( SHA-256( public key ) ) )
So anyone can recompute the identifier from the public key, without querying a central registry. This is the concrete form of the principle "no central referee."
#Three breaks in the binding
The independent review (2026-07-26, target commit f9a381a) found three breaks in the binding chain of vacant_id. The third is still unfixed; it is an open item the patch branch deliberately left.
#① vacant_id → stream_id is not verified at all
registry.py:78-82 recomputes public key ↔ vacant_id, but for vacant_id ↔ stream_id there is not a single check. CapabilityCard even has a stream_id field, and the registry never reads it.
#② Impersonation can inherit reputation
Anyone can claim someone else's stream_id and then inherit that party's reputation. The attack PoC measured: 0.50 → 0.7417. This is a direct failure of the necessary condition "the signal cannot be cheaply forged."
#③ slash is a reward for low scorers
slash preserves the evidence ratio — mathematically equivalent to "ageing the record by a full half of a half-life." For a high-scoring saboteur, UBB even rises: 1.0698 → 1.0895. For a low-scoring saboteur, it climbs all the way: 0.0161 → 0.0312 → 0.0588 → 0.1053.
After the patch, the UCB ceiling for unproven reviewers is pressed down to 0.1/k. But before the patch, a candidate with zero observations got a UCB of 20,380.9 (compared with 1.04 for a proven expert).
#Decay uses a global clock
Reputation decay uses a global clock. The consequence: as long as someone floods 1,500 unrelated reviews, the observation count of every high-reputation node gets reset — measured 0.7878 / obs 2.71 → 0.5037 / obs 0.01. An attacker can spend a little to churn every good participant's record into noise.
After the patch, the zero-observation candidate's UCB was pressed down from 20,380.9, but the structural problem of the global clock remains.
#The statistics tooling itself got it wrong
The normal-approximation branch of wilcoxon_signed_rank_exact double-counted the tie correction. With n=30, 20 positive and 10 negative, the code said p=0.0288 (significant); the exact value is 0.0987 (not significant).
This was added to the methodology's list of pitfalls, for this reason: a bug in the statistics tooling changes conclusions just as quietly as a bug in the mechanism.
In addition, the bootstrap CI used hash() as its seed, which is not reproducible across processes: the same data produced two numbers, 53,478 and 1,628.
#After the patch
Patch branch fix/trust-layer-p0: 4 P0 items + 6 P1 items, TDD (21 red / 5 green when work started).
| Tests | 361 passed / 1 skipped (baseline 330) |
| Tests mapped to review comments | 29/29 |
| Attacks blocked | 12/12 each on Mac and on the VM |
| Rejected suggestions | 1 |
| Structural gaps deliberately left open | 6, the first being "identity is still free" |
#Next
- How reputation turns into work: Registry and reputation
- How audits confirm a fault: Peer review and audits
- What this machinery measured on the defense axis: The defense axis

