feat(pouw): PoUW integration capstone — verify driver, quorum-driven disputes, declared-fault refund, demo#67
Conversation
…-vs-detected fault asymmetry (Sprint 2) The next Sprint-2 increment after #32's dispute window (which itself flags this as 'the next increment'). A single verifier is a single point of corruption: a lazy one rubber-stamps fraud, a malicious one slashes honest work. This aggregates many independent challenge verdicts (pouw/challenge.py) into one decision so no verifier minority can force a false confirm OR a false slash. - Default quorum k = floor(2n/3)+1 (BFT supermajority), tolerant to f = floor((n-1)/3) adversaries: f alone cannot reach k, the honest n-f always can, and 2k>n makes confirm/mismatch quorums mutually exclusive (never self-contradictory). - Declared-vs-detected asymmetry: a worker that self-declares a fault is refunded, never slashed (DECLARED_FAULT); slashing is reserved for a quorum of mismatches against a worker that claimed success (DETECTED_FAULT). Removes the incentive to hide a known-bad result and gamble on not being sampled. - INCONCLUSIVE on a non-quorum: nothing paid or slashed; wait / re-sample. - Pure integer counting — no floats, no crypto-path or signed-record changes; result is advisory and drives the existing pouw/dispute.py release/dispute actions. Disjoint lane: only adds src/knitweb/pouw/quorum.py + its test (does not touch the files in the in-flight #49 consistency pass). Proof: PYTHONPATH=src pytest -q -> 290 passed (20 new quorum proofs incl. the mutual-exclusion, adversary-minority, and honest-supermajority properties across n=1..49). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… 2 escrow economics) The dispute window (#32) can slash a worker's stake, but slashing only deters fraud if the stake covers the value at risk. This adds the missing sizing rule (B6; EigenLayer's 'collateral must cover value at risk'). The at-risk amount is CUMULATIVE: a worker may have several submissions pending inside their open dispute windows, and one fraud-and-flee could try to collect ALL their escrows before any slash lands. So: payout_at_risk = sum(escrow of pending submissions) required_collateral = ceil(payout_at_risk * margin) # margin >= 1 sufficient <=> collateral >= required_collateral With collateral >= payout_at_risk (margin 1:1, the minimum), a detected fraud loses at least what it could gain -> fraud is net <= 0 -> honest work dominates. Margin is an exact integer ratio (no float on the PLS-wei path), amounts round UP so a short stake is never 'enough'; max_backed_payout is the (round-down) inverse to cap how much escrow a given stake may back. Pure policy module — imports nothing from job/escrow/quorum; a caller checks is_sufficiently_collateralized before admitting a submission into pouw/dispute.py. Disjoint lane: adds only src/knitweb/pouw/collateral.py + its test (no overlap with the in-flight #49 consistency pass or #50 quorum). Proof: PYTHONPATH=src pytest -q -> 284 passed (14 new proofs incl. the core invariant 'sufficient collateral => fraud non-profitable' and the tight max_backed_payout inverse). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…un (audit soundness) challenge.py re-executes k sampled blocks but leaves k to the caller, yet the entire soundness of sampled re-execution rests on that number. This sizes it exactly. If a worker corrupts of n blocks, a k-sample without replacement misses all of them with hypergeometric probability C(n-corrupt,k)/C(n,k) = prod (n-corrupt-i)/(n-i), so it catches the fraud with 1 - miss. required_samples inverts that: the smallest k driving miss <= a target (e.g. Fraction(1,100) for >=99% catch). Audit-side companion to pouw/collateral.py (#51): collateral makes DETECTED fraud unprofitable; sampling makes UNDETECTED fraud improbable. Together a rational worker's expected cheating payoff is negative. All exact rational arithmetic (fractions.Fraction) — no floats, no approximation, the threshold comparison is exact. Advisory policy: touches neither the canonical/hash path nor any signed record; doesn't even import challenge.py (standalone combinatorics). Disjoint lane: adds only src/knitweb/pouw/sampling.py + its test. #49 touches pouw/digest/job/scheduler (not sampling); no overlap with #50/#51/#52/#53/#54. Proof: PYTHONPATH=src pytest -q -> 286 passed (16 new, incl. the telescoping single- corrupt identity miss(k)=(n-k)/n, monotonicity, minimality of required_samples, and the k<=n guarantee across cases). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…jury for a job The missing first stage of the PoUW pipeline. challenge.py re-executes samples, sampling.py sizes them, quorum.py aggregates verdicts, dispute.py settles/slashes — but nothing SELECTS which verifiers check a job. Left to the worker it is trivially gamed (pick a friendly jury), so, like Algorand VRF committees and Ethereum attestation committees, the committee must be unpredictable before a fresh seed and verifiable after. select_committee(seed, eligible, k, exclude=worker) deterministically draws k distinct verifiers via the same SHA-256 counter stream challenge.sample_indices uses for block sampling. The eligible set is de-duplicated and sorted before indexing, so the result depends only on the SET and the seed (never input order) and two honest nodes always agree. The worker is excluded from its own jury; the draw clamps to the pool size. Returns the committee in deterministic selection order so callers may assign priority by position. Pure hash/integer policy; no floats, no canonical/signed-record changes; does not import any unmerged module (standalone over crypto.sha256). Disjoint lane: adds only src/knitweb/pouw/committee.py + its test. #49 touches pouw/digest/job/scheduler (not committee); no overlap with #50-#57. Proof: PYTHONPATH=src pytest -q -> 282 passed (12 new, incl. determinism, input-order independence, distinct membership, worker exclusion, pool-size clamping, seed sensitivity, and the full-pool-draw-is-a-permutation property). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ion: #51 -> #32) First integration increment. The dispute window (#32) tracked a per-submission collateral field but never checked it was adequate; collateral sizing (#51) defines the invariant. This wires them together. - DisputeWindowLedger gains an opt-in enforce_collateral flag (default False -> existing #32 behaviour and tests unchanged) plus an optional margin (default 1:1). - When on, submit() rejects an UnderCollateralizedError when a worker's total staked collateral cannot cover the CUMULATIVE escrow at risk across all its still-open windows (the #51 point: a single fraud-and-flee could collect every pending escrow). Resolved (slashed/released) submissions drop out of the risk pool; workers are independent. Stacked on #51 (base branch pouw/escrow-economics) — imports pouw/collateral.py. Touches only pouw/dispute.py (+ a new test); dispute.py is in no other open PR, and #49 does not touch it. Proof: PYTHONPATH=src pytest -q -> 292 passed. New: 8 integration proofs (single + cumulative sufficiency, resolved-drops-out, per-worker independence, margin>1, constructor validation). Back-compat: the 18 existing dispute-window proofs stay green (enforcement off by default). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…a lone mismatch (integration #50->#32) Third integration increment. dispute.py slashed on a SINGLE detected mismatch, which lets one malicious verifier slash honest work. This wires the k-of-n quorum (#50) in. dispute_by_quorum(sid, verdicts, beat, *, worker_declared_fault, threshold) aggregates a committee's verdicts via quorum.tally and slashes ONLY on a genuine DETECTED_FAULT (a mismatch quorum against a worker that claimed success), reusing dispute()'s window timing + slashing. CONFIRMED / INCONCLUSIVE / DECLARED_FAULT never slash — honest work and owned-up faults survive (the declared-fault refund is a separate settlement path, noted). Window-timing still applies: a quorum slash after the window closes is rejected. Stacked on the #59+#50 combination (base branch int/dispute-base): dispute.py now carries both collateral enforcement (#59) and quorum-driven disputes. Touches only pouw/dispute.py (+ test). Proof: PYTHONPATH=src pytest -q -> 319 passed. New: 7 integration proofs (mismatch-quorum slashes; confirm/inconclusive/declared no-slash; a lone mismatch among confirms cannot slash; post-window rejected; custom unanimity threshold blocks a split). Back-compat: 26 existing dispute + dispute-collateral proofs stay green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…, vote (integration capstone) Composes the standalone PoUW primitives into the runnable verification path; until now each existed in isolation: committee.select_committee (#58) -> who verifies (unpredictable, verifiable jury) sampling.required_samples (#55) -> how many blocks each verifier re-checks challenge.verify_response (core) -> the worker's reveals are authentic to its commitment + re-execution comparison -> the sampled blocks match an honest recompute => a quorum.Verdict per verifier -> exactly what quorum.tally / dispute_by_quorum consumes This is the UPSTREAM half (produce verdicts); dispute_by_quorum (#61) is the downstream half (act on them). A verifier CONFIRMs only if every sampled block matches both the worker's signed commitment AND its own honest re-execution; a forged reveal or a wrong block is a MISMATCH. Sizing k via sampling is what makes a sampled (not full) re-check sound. - plan_verification(seed, eligible, worker, n_blocks, committee_size, corrupt_hypothesis, max_miss) -> VerificationPlan(committee, k): ties #58 + #55, excludes the worker from its own jury. - verifier_verdict(commitment, salt, k, reveals, recomputed_blocks) -> Verdict. - run_committee(...) -> list[Verdict]: drives a whole committee into a verdict stream. Stacked on the #50+#55+#58 combination (base int/verify-base). New file pouw/verify.py + test only. Proof: PYTHONPATH=src pytest -q -> 326 passed. New: 8 integration proofs (committee+k planning; honest-confirms; cheated-block-caught-when-sampled; forged-reveal-rejected; full committee run confirms honest + quorum CONFIRMED; heavy corruption -> all MISMATCH + quorum DETECTED_FAULT; full audit always catches a single corrupted block). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…pletes the verdict space) quorum (#50) produces three verdicts but the dispute ledger settled only two: DETECTED_FAULT -> dispute() slashes, CONFIRMED -> release() pays the worker, and DECLARED_FAULT (an honest self-report) had NO settlement path. refund_declared_fault closes the gap. - refund_declared_fault(sid, beat): refund the escrow to the consumer and return the worker's collateral UNSLASHED (you are never slashed for a fault you owned up to), paying the worker nothing. Allowed while pending; sets status "refunded"; surfaced in stats(). - Pairs with dispute_by_quorum's DECLARED_FAULT signal (which deliberately does not slash): the caller settles a declared fault via this method. dispute_by_quorum is unchanged, so #61's proofs are untouched. Stacked linearly on the dispute train (base #61 pouw/dispute-quorum-wiring, which carries #59 collateral + #61 quorum) to keep dispute.py a single linear history (no merge conflict). Touches only pouw/dispute.py (+ test). Proof: PYTHONPATH=src pytest -q -> 325 passed. New: 6 proofs (refund mechanics: consumer whole + stake returned + zero slash; all-three-outcomes distinct + conservation of every escrow/collateral exactly once; declared-fault-quorum-then-refund flow; no double-settlement; refund-before-submit rejected). Back-compat: 33 dispute + collateral + quorum proofs stay green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…settle (acceptance capstone) (#64) A runnable acceptance demo tying the whole PoUW subsystem into one flow across the three settlement outcomes the protocol must handle: 1. honest worker -> committee CONFIRMS -> RELEASED (paid) 2. cheating worker -> committee MISMATCH -> SLASHED (stake burned, escrow refunded) 3. declared fault -> owned-up, no slash -> REFUNDED (stake returned, consumer made whole) Each run: consumer escrows; worker stakes collateral (enforced, sized so fraud can't pay, #51/#59); a verifier committee is selected unpredictably (#58) and sized (#55); each verifier re-executes sampled blocks (#62 over challenge) and votes; the quorum (#50) drives the dispute ledger (#61 + #63 refund) to slash/release/refund. Shows fraud never pays. Composes the full verify stack (#62) + dispute train (#63: #51->#59->#50->#61->#63). Stacked on int/full-pouw-base (= #63 union #62). Adds examples/pouw_demo.py + an acceptance test only. Proof: PYTHONPATH=src pytest -q -> 364 passed. New: 3 acceptance proofs (all three settlement paths settle correctly; fraud-never-pays invariant; main() smoke). The demo also runs standalone: PYTHONPATH=src python3 examples/pouw_demo.py. Co-authored-by: febuz <you@example.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reviewer note — soundness fundamentals verified ✅, one adversarial question on declared-fault timingReviewed the consensus-critical parts statically against the diff (didn't re-run the suite; trusting the stated 425-green). The fundamentals hold up well: Verified good:
One thing to confirm before merge — declared-fault as a possible slash-escape:
if worker_declared_fault:
outcome = DECLARED_FAULT # refund, no slash — "regardless of verdicts"
elif confirms >= k: ...
elif mismatches >= k:
outcome = DETECTED_FAULT # slashRefund-regardless-of-verdicts is correct only if
Requests:
Not blocking the quorum/collateral/verify core, which is solid — just want the third outcome closed against the obvious griefing path before it's the acceptance capstone. |
Adopts the stacked PoUW integration work onto
main. Merges cleanly into currentmain(bb37697); merged result is 425 tests green (393 → 425, +32) and the signed-record CID gate holds.What lands
pouw/verify.py) — select → size → re-execute → voteexamples/pouw_demo.py) — select → verify → quorum → settle (acceptance capstone, feat(examples): end-to-end PoUW demo — select → verify → quorum → settle (acceptance capstone) #64)Why one PR
int/full-pouw-baseis the capstone of the stack — it already contains the work ofint/dispute-base(quorum-driven disputes + dispute tests) andint/verify-base(pouw/verify.py+ verify test). Those two are intermediate bases, fully subsumed here, so they need no separate PRs.Notes
int/feed-safety-base(feat(p2p): policing — wire equivocation/conflict detection to reputation (integration #56+#57) #60 p2p policing) — also merges clean; can be adopted as its own PR if wanted.🤖 Generated with Claude Code