Skip to content
This repository was archived by the owner on Jul 20, 2026. It is now read-only.

feat(pouw): PoUW integration capstone — verify driver, quorum-driven disputes, declared-fault refund, demo#67

Merged
febuz merged 13 commits into
mainfrom
int/full-pouw-base
Jun 18, 2026
Merged

feat(pouw): PoUW integration capstone — verify driver, quorum-driven disputes, declared-fault refund, demo#67
febuz merged 13 commits into
mainfrom
int/full-pouw-base

Conversation

@febuz

@febuz febuz commented Jun 18, 2026

Copy link
Copy Markdown
Owner

Adopts the stacked PoUW integration work onto main. Merges cleanly into current main (bb37697); merged result is 425 tests green (393 → 425, +32) and the signed-record CID gate holds.

What lands

Why one PR

int/full-pouw-base is the capstone of the stack — it already contains the work of int/dispute-base (quorum-driven disputes + dispute tests) and int/verify-base (pouw/verify.py + verify test). Those two are intermediate bases, fully subsumed here, so they need no separate PRs.

Notes

🤖 Generated with Claude Code

febuz and others added 13 commits June 18, 2026 02:35
…-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>
@febuz

febuz commented Jun 18, 2026

Copy link
Copy Markdown
Owner Author

Reviewer note — soundness fundamentals verified ✅, one adversarial question on declared-fault timing

Reviewed 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:

  • Quorum safety invariant is correct and enforced at runtime. default_threshold(n) = ⌊2n/3⌋+1 (BFT supermajority), max_faulty = ⌊(n-1)/3⌋, and tally() raises if 2*k <= n. So a confirm-quorum and a mismatch-quorum can never both form — the protocol can't simultaneously release and slash. This is the property that matters most here, and it's guarded, not just documented.
  • Float-ban respected on the consensus path. Zero float literals in the added code; 44× fractions.Fraction instead (collateral 1.5× over-collateralization, sampling miss-probability). Exactly the right pattern for the determinism rule — the only N.N tokens are in comments.
  • CID-invariant holds. No kind / to_record / signing_bytes / canonical.encode changes in the diff — consistent with "no signed-record byte changes."

One thing to confirm before merge — declared-fault as a possible slash-escape:

tally() short-circuits:

if worker_declared_fault:
    outcome = DECLARED_FAULT      # refund, no slash — "regardless of verdicts"
elif confirms >= k:  ...
elif mismatches >= k:
    outcome = DETECTED_FAULT      # slash

Refund-regardless-of-verdicts is correct only if worker_declared_fault is pinned before the committee result is knowable. If a worker can set it after seeing a mismatch-quorum forming, it's a strict downgrade of a slash to a refund: cheat → about to be caught → declare fault → stake returned, no penalty. tally() is a pure function and can't enforce that ordering itself.

test_declared_fault_refund.py covers the happy-path refund mechanics and the conservation accounting, but not the adversarial ordering — each terminal outcome is tested on a separate submission, so "declare fault on a submission a quorum already mismatched" is never exercised.

Requests:

  1. Where is worker_declared_fault committed relative to sampling/verification? If it isn't already behind the commit-before-sample point (challenge.py), it should be — same guarantee the rest of the verify path leans on.
  2. Add a property test: a refund_declared_fault / declared-fault tally must not resolve a submission once a mismatch-quorum exists (or once dispute() is reachable) — i.e. declared-fault can't escape a detected fault.

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.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant