Follow-up to #67 (merged) and my review there. The quorum/declared-fault design is sound — the declared-vs-detected asymmetry ("never slashed for a fault you owned up to; slash only a detected fault against a worker that claimed success") is correct game theory, and quorum.py explicitly reasons about removing the incentive to "hide a known-bad result and gamble on not being sampled."
The gap is enforcement, not intent.
What I found (verified against origin/main)
worker_declared_fault is supplied at settlement time, not pinned at submit:
# dispute.py
def dispute_by_quorum(self, sid, verdicts, *, beat,
worker_declared_fault: bool = False, threshold=None): ...
And Submission has no worker_declared_fault field:
class Submission:
sid; worker; consumer; escrow; collateral; submit_beat
status = "pending"; resolved_beat = None
The merged test encodes the escape as intended behaviour:
# a mismatch-quorum [M, M, M] + worker_declared_fault=True → NOT slashed
led.dispute_by_quorum("s1", [M, M, M], beat=12, worker_declared_fault=True)
Why it matters
The documented soundness rests on declared-fault being an honest up-front self-report ("a worker that claimed success" is the one that gets slashed). But nothing binds the flag to submit time: it's a free boolean passed after the verdicts are known. A caller — or a worker influencing the settlement call — can pass worker_declared_fault=True once [M, M, M] is visible, converting a slash (collateral burned) into a refund (collateral returned). That's a strict downgrade of the penalty exactly when fraud was detected — the "gamble on not being sampled" the docstring says it removes.
Not claiming a live exploit (the protocol-level caller flow that sets this flag isn't in the diff) — it's a structural weakness: the invariant is documented but not enforced by the type/state.
Suggested fix (clean, integer-only, no signed-record impact)
- Add
worker_declared_fault: bool = False to Submission, set at submit() and immutable thereafter.
- Have
dispute_by_quorum read it from the submission instead of accepting it as a parameter.
- Add a property test: a submission that
submit()ted with worker_declared_fault=False and then draws a mismatch-quorum must slash — passing True at dispute time must be impossible (no longer an accepted argument).
That makes the documented "claimed-success vs owned-up" distinction structurally true, before this is the acceptance capstone for mainnet.
Follow-up to #67 (merged) and my review there. The quorum/declared-fault design is sound — the declared-vs-detected asymmetry ("never slashed for a fault you owned up to; slash only a detected fault against a worker that claimed success") is correct game theory, and
quorum.pyexplicitly reasons about removing the incentive to "hide a known-bad result and gamble on not being sampled."The gap is enforcement, not intent.
What I found (verified against
origin/main)worker_declared_faultis supplied at settlement time, not pinned at submit:And
Submissionhas noworker_declared_faultfield:The merged test encodes the escape as intended behaviour:
Why it matters
The documented soundness rests on declared-fault being an honest up-front self-report ("a worker that claimed success" is the one that gets slashed). But nothing binds the flag to submit time: it's a free boolean passed after the verdicts are known. A caller — or a worker influencing the settlement call — can pass
worker_declared_fault=Trueonce[M, M, M]is visible, converting a slash (collateral burned) into a refund (collateral returned). That's a strict downgrade of the penalty exactly when fraud was detected — the "gamble on not being sampled" the docstring says it removes.Not claiming a live exploit (the protocol-level caller flow that sets this flag isn't in the diff) — it's a structural weakness: the invariant is documented but not enforced by the type/state.
Suggested fix (clean, integer-only, no signed-record impact)
worker_declared_fault: bool = FalsetoSubmission, set atsubmit()and immutable thereafter.dispute_by_quorumread it from the submission instead of accepting it as a parameter.submit()ted withworker_declared_fault=Falseand then draws a mismatch-quorum must slash — passingTrueat dispute time must be impossible (no longer an accepted argument).That makes the documented "claimed-success vs owned-up" distinction structurally true, before this is the acceptance capstone for mainnet.