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

PoUW: worker_declared_fault is settlement-time input, not pinned at submit — slash-escape (follow-up #67) #71

Description

@febuz

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)

  1. Add worker_declared_fault: bool = False to Submission, set at submit() and immutable thereafter.
  2. Have dispute_by_quorum read it from the submission instead of accepting it as a parameter.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions