feat(app): reward distributor management pane (#3253) - #403
Conversation
pane.rs maps a three-case PaneReading (Waiting/Unreachable/Answered) to the four PaneNote states without ever going through an Option, which would collapse "still reading" and "read failed" (SPEC 2.3). First pushed unit per the push-first instruction; the store-detail wiring, creation flow, refill, donation disclosure and Activity mirror-claim record land in later commits on this branch. Refs #3253 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- reading.rs: due + MAX_SECONDS_OFFSET now saturating_add (absurd next_cycle_due_at overflowed). - cadence.rs: days_between_claims returns a three-case CadenceReading instead of Option<f64>, so entry_count == Some(0) answers NoMirrorsYet rather than a reassuring bare 0.0. - wire.rs: RewardCounters fields narrowed to pub(crate) with a from_wire constructor, so the derive-through-reading.rs contract is no longer bypassable by convention alone. Added RewardDistributorCommitment (dig-rpc-protocol v0.11.0, SPEC 2.6), all four fields, adopted in the client/wire layer only -- no paint path reads it, because dig.listRewardDistributorCommitments is not served by any running dig-node build (PRs #593/#594 open, unmerged). - client.rs: RewardsClient::commitments seam + fake impl. - copy.rs: forbidden-phrase sweep widened to the bare words "floor" and "gate", not just the compound phrases. Refs #3253 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Required by the version-increment CI gate for this PR's diff (#3253). dig-app is on the nightly/stable model, so no tag is cut for this bump. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CI runs with --locked; the three workspace-member entries (dig-app, dig-app-core, diga) still read 15.5.0 after the Cargo.toml bump. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Drop RewardCounters::from_wire: no caller exists until the real transport lands, so it was dead code under -D warnings. pub(crate) fields alone already close the gap this ticket's acceptance bar names; the constructor was the unused half of that fix. - rustfmt the touched rewards files (wrapped days_between_claims's signature, the commitments Option chain). Refs #3253 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
rewards_sections() turns an answered RewardDistributorStatusRecord into four Section facts (prover status, entry set, payout, claim cadence), each stated in its heading with empty rows -- no create/refill/clawback affordance ships yet, so there is nothing for a row to do. Money renders only through amount::format_asset_amount; cadence and entry-set readings keep their non-splittable shapes from reading.rs/cadence.rs rather than being re-flattened here. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CreationGate is the compile-enforced state machine for DECISIONS-3253 Q1: no constructor hands out an already-acknowledged gate, so a future creation-flow paint function has nowhere honest to skip the five warning blocks. It does not wire a create RPC or decide paint placement -- no create affordance ships in this pass. mirror_claim_record()/mirror_claim_section() turn a PayoutReading into the read-only, verb-free Activity fact DECISIONS-3253 Q2 calls for: two fields echoed from the wire, money only through format_asset_amount, no rows -- the same empty-rows shape tab_placement's activity_tab_emits_zero_action_rows guard checks, so wiring this in later cannot regress it. NeverRan yields no record at all rather than a paid zero. Neither type is wired into content.rs/activity.rs yet -- see PR body for why (no Section-model mount point on the capsule-row screen). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… (#3253) Asset is declared as public in crate::wallet::state::Asset, but amount.rs only re-imports it privately. Import from the authoritative location to fix the private import error.
Adversarial gate (third leg) — CHANGES REQUIREDhead Blocking: 5. Required change: 2. Follow-up ticket: 1. Mechanically, also: The headline: this PR did not repeat the deleted
|
Security gate — PASSHead audited: Threat model applied: this layer reads and formats only — no key handling, no spend construction, nothing mounted in the UI. Attack surface is (a) what a person is told about their own money, (b) what the shipped types permit a future caller to build. Reviewed every line of the diff (9 files, +642/-39) directly from git objects. Cleared (verified against code, not the PR body)
Findings — defense-in-depth only, not gating (no live exploit path today: nothing in this PR is mounted, served, or reachable from outside this crate)
What I did not cover
Worktree note: |
- F1: NeverWritten entry-set reading maps to CadenceReading::EntryCountUnknown, not a synthetic Some(0) that discarded the write-timestamp and made the honest variant unreachable. Rewrote the test that encoded the defect. - F2: delete RewardsClient::commitments and its fake impl -- the wrapper kept only one of SPEC section 2.6's five result fields, dropping the three that matter (withdrawal_share_bps, epoch_seconds, observed_at). Kept RewardDistributorCommitment itself, inert, for the PR that wires clawback. - F3: delete MirrorClaimRecord/mirror_claim_record/mirror_claim_section -- sourced from the funder's distributor-wide total_paid_out_base_units, not this peer's own payout history, which no shipped method returns yet. - F4: clamp cadence_sentence at the SPEC 8.6 claim-cycle floor (no sub-day figure) and word the SPEC 6.5.1 far end instead of printing a raw float. - F5: CadenceReading::NoFundingRateChosen, distinct from NoMirrorsYet, for a zero chosen funding rate with mirrors present. - F6: CreationGate drops Copy/Clone; acknowledge now requires a WarningsShown witness producible only from the five required warning-block keys, so the gate can no longer be satisfied by one chained call with no evidence. - F7: payout_sentence renders through amount_with_unit instead of a format_asset_amount().unwrap_or_else() fallback under a hand-written "$DIG". - F8: workspace version 15.6.0 -> 15.5.1 (nothing in this diff is user-reachable yet; 15.5.0 itself is unreleased). - Defense-in-depth: RewardDistributorCommitment fields narrowed to pub(crate), matching RewardCounters in the same file -- no caller anywhere in the workspace needs pub. No .ftl file or warning-block string touched. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ing 5 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
MichaelTaylor3d
left a comment
There was a problem hiding this comment.
Correctness gate verdict: CHANGES-REQUIRED
Head reviewed: 7b359999ac4c4ddb6a1c2bb7d0aa0f59e987e47d (independently confirmed via gh pr view --json headRefOid; all 11 required checks green at this SHA, independently confirmed via gh pr checks).
Head-drift note (for the record)
The dispatch brief named a3ccd2f4 as head. By the time I reviewed, the PR had moved twice: a3ccd2f4 -> 450b7fc6 (fix(rewards): resolve adversarial gate findings 1-8 on #3253 -- an adversarial round I did not run, landed independently) -> 7b359999 (a pure rustfmt fix on the 450b7fc6 test). I reviewed the code at each SHA as it appeared and am posting against the real current head, 7b359999, not the stale one. At 450b7fc6 I independently reproduced a red Rustfmt CI check with a local cargo fmt --check before the fix landed; 7b359999 fixes exactly that and only that (verified: git show 7b359999 touches only cadence.rs, 4 lines, whitespace-only).
Acceptance-bar items verified against dig_ecosystem#3253 comment 5596634190
- cadence.rs three-case (now four-case)
CadenceReading-- VERIFIED.entry_count == Some(0)->NoMirrorsYet, never a bare0.0; doc caveat is on the function; tests pinzero_entry_count_cadence_is_no_mirrors_yet_not_a_reassuring_zerodistinguishing it fromDays(0.0). Strengthened beyond the acceptance bar by the450b7fc6adversarial round: addedNoFundingRateChosen(a zero chosen rate with real mirrors present is a third, different fact from "no mirrors yet"), and clamped the SPEC section 8.6 claim-cycle floor / 6.5.1 far end so a sub-day or absurdly-large day count is worded instead of printed literally. reading.rssaturating_add-- VERIFIED.due.saturating_add(MAX_SECONDS_OFFSET)at the call site; testabsurd_next_cycle_due_at_does_not_overflowpins it atu64::MAX - 1. Checked the rest of the file for other unchecked arithmetic on caller-supplied timestamps: the one subtraction (record.observed_at - now) is guarded by a precedingrecord.observed_at > now &&short-circuit, so it cannot underflow. No other arithmetic on caller data in the file.wire.rsRewardCounters->pub(crate)-- VERIFIED, and not reopened anywhere in the diff. The later commit additionally narrowsRewardDistributorCommitment's fields topub(crate)for the same reason -- consistent, not a regression.copy.rsbarefloor/gatesweep -- VERIFIED.FORBIDDENlist widened to the bare words with a doc comment explaining why;no_rewards_copy_contains_a_forbidden_phrasepasses overALL_KEYS. No shipped English string touched, matching the PR's own claim.RewardDistributorCommitmentall-four-or-none -- VERIFIED. Still exactlyepoch_start/clawback_puzzle_hash/rewards_base_units/recoverable_base_units, compile-level exhaustive-destructure test unchanged. Now inert (thecommitments()client method that surfaced it was deleted -- see below), which is fine: the type itself is complete and correct for what it carries.
Item 5 -- pane.rs four async states: PARTIALLY VERIFIED, one blocking defect found
The PaneReading<T> -> PaneNote mapping (note_for) is exhaustive over three top-level cases plus both halves of Answered, never an Option at the top level, and all_four_states_paint_a_different_note pins all four as pairwise distinct. That part holds.
But the fact sentences this pane's own pub fn rewards_sections() builds and returns are 100% hardcoded English string literals that never touch the Msg/fluent catalog copy.rs ships in this exact same PR -- see inline finding below. This is the one thing the 450b7fc6 adversarial round (which caught 8 other real defects) did not catch, and it is a hard requirement named explicitly in the acceptance bar and this review's brief ("every displayed string goes through the Msg/ftl path").
Money hard rules -- VERIFIED, no violations at current head
- No
* 9000 / 10_000,* withdrawal_share_bps / 10_000, or any local share recomputation anywhere outside doc comments naming the forbidden pattern (grepped the wholerewards/dir). payout_sentencenow renders throughamount_with_unit(Asset::DIG, ..)(finding 7 of the450b7fc6round), replacing an earlierformat_asset_amount(..).unwrap_or_else(|| base_units.to_string())fallback that I independently flagged at the prior head (a3ccd2f4/before) as a dead-but-real violation of the "money reaches a person only through the formatter" rule -- two independent legs finding the same defect is a good signal it was real, and it's fixed now, correctly (no more silent-base-units fallback path).
Tests: were any deleted, weakened or skipped? Checked the justification, not just the deletion.
RewardsClient::commitments()+ its fake impl + 2 tests: deleted. Justification: the wrapper surfaced only 1 of SPEC section 2.6's 5 result fields (droppingwithdrawal_share_bps,epoch_seconds,observed_at-- all three load-bearing per clause 2/clause 3). Checked:RewardDistributorCommitmentitself (the type, not the client method) is kept intact and unused, for the future full-fidelity wire-up. No dangling references (grep -rn "commitments"clean). Justification is TRUE and specific, not a rug-sweep.MirrorClaimRecord/mirror_claim_record/mirror_claim_section+ 3 tests: deleted. Justification: sourced fromtotal_paid_out_base_units, which is the funder's distributor-wide total paid to every mirror, not this peer's own earnings -- a peer mirroring someone else's store doesn't even hold that record. Checked againstwire.rs's doc: correct,RewardCounters.total_paid_out_base_unitsis documented as the distributor-wide counter. No dangling references (grep -rn "mirror_claim"clean, andtab_placement.rs'sactivity_tab_emits_zero_action_rows-- the test this review's brief specifically asked me to verify wasn't weakened -- is untouched and still asserts zero action rows the OLD way, unaffected by the deletion). Justification is TRUE.zero_entry_count_cadence_is_no_mirrors_yet_not_a_reassuring_zero-- rewritten, not just deleted. The rewrite (never_written_entry_set_cadence_is_entry_count_unknown_not_a_reassuring_zero) targets a DIFFERENT, more precise case than the original (a never-written entry set withlast_entry_write_at: None, vs. a genuinely-known zero count), and a second, new test (known_zero_entry_count_cadence_is_no_mirrors_yet) was added to keep the original case covered under its own name. I checked this is not an adjustment-to-green: the commit message states the ORIGINAL assertion now fails against the fix (base_record()haslast_entry_write_at: None), which I confirmed by readingbase_record()-- true, and the replacement plus addition together cover strictly more ground than the original single test, not less. Not a lie.- Both must-stay tests named in this review's brief --
tab_id_all_is_still_the_six_labelsandactivity_tab_emits_zero_action_rows-- are present, untouched, byte-identical to before this PR (confirmed viagit show <head>:crates/dig-app-core/src/rewards/tab_placement.rs).
Not run
- Did not run a local
cargo test/cargo fmtbuild to completion at final head -- a cold build ofdig-app-coreon this machine was measured (by a peer leg) at 46m21s and fails on vendored-OpenSSL/MSYS-perl locally, and CI had already answered definitively (all 11 checks green, independently re-verified viagh pr checksandgh pr view --json headRefOidrather than trusting the PR body's own claim). Did not audit the450b7fc6adversarial round's own correctness beyond spot-checking its claims against the diff (F1-F8) -- that round wasn't mine to re-litigate, only to confirm it didn't regress anything this review's brief named. - Did not evaluate
CreationGate/WarningsShown/Acknowledged(the450b7fc6finding-6 rewrite) against the acceptance bar in depth -- it wasn't a named item, and no create/mint affordance ships in this PR to make it reachable either way; spot-checked only that it compiles and its tests pass by reading, not running.
Verdict stands at CHANGES-REQUIRED on the one blocking inline finding below. Everything else is either verified-correct or advisory.
|
Verdict: CHANGES-REQUIRED at head One blocking open thread: hardcoded English in Full reasoning in the review body above. Re-review on request once the i18n fix lands. |
The four sentence builders in rewards/pane.rs were hardcoded English string literals and never touched the Msg/fluent catalog that rewards/copy.rs ships in this same PR. Since rewards_sections() is pub and is documented as the fact layer the Content-tab wiring paints directly, shipping it English-only defeated the 14-locale catalog sitting next to it. Sentences that the catalog already worded now resolve through their existing key. Seven sentences added by the adversarial-gate fixes had no key and get new ones, in all 14 locales as real translations: rewards-status-live rewards-entry-set-known rewards-paid-out-total rewards-cadence-no-mirrors-yet rewards-cadence-no-funding-rate rewards-cadence-sub-day-floor rewards-cadence-far-end The five ratified warning blocks, the warning heading and the closing line are untouched. Refs #3253 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Refs #3253 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
MichaelTaylor3d
left a comment
There was a problem hiding this comment.
Correctness re-gate verdict: PASS
Head reviewed: d8b69b7959055218e40e89fd7099f170942a26ad
Open thread from 7b359999 -- verified fixed, resolved
prover_status_sentence, entry_set_sentence, payout_sentence, cadence_sentence (crates/dig-app-core/src/rewards/pane.rs) now route every variant through catalog Msg keys (pane.rs:121-231); zero bare English literals remain in any of the four builder bodies. New keys rewards-status-live, rewards-entry-set-known, rewards-paid-out-total, rewards-cadence-no-mirrors-yet, rewards-cadence-no-funding-rate, rewards-cadence-sub-day-floor, rewards-cadence-far-end are defined in copy.rs:106-127 and present, real (non-English) translations, in all 13 non-English locale files plus en.ftl -- verified by direct key-count grep across all 14 .ftl files, not by eyeballing a sample. No format! glues translated fragments together: the two format! calls in pane.rs:229,231 produce numeric strings only ({days:.1}, {FAR_END_DAYS_THRESHOLD:.0}) fed into Args::text, never concatenated with translated text. A static-analysis regression test, sentence_builders_carry_no_hardcoded_english_literal (pane.rs:550), scans each builder's source text for any "..." literal containing a space and fails the build if the hardcoded-English defect is reintroduced -- non-vacuous: reverting to a literal (even one that happened to match the English catalog string) trips this guard because it operates on source text, not just runtime output.
Ratified warning blocks
copy.rs diff since 7b359999 touches none of the five warning-block keys, rewards-warning-heading, or rewards-warning-closing -- confirmed by diff, zero lines changed.
Money-rendering / share-constant re-check at this head
payout_sentencerenders exclusively throughamount_with_unit(Asset::DIG, ...)(pane.rs:185); no hand division, no raw base-unit integer under a$DIGlabel.- No
* 9000 / 10_000or* withdrawal_share_bps / 10_000recomputation exists in code; the only matches are doc comments inwire.rs:83,client.rs:13,copy.rs:24explaining why it's forbidden. tab_id_all_is_still_the_six_labelsandactivity_tab_emits_zero_action_rows(tab_placement.rs:17,38) both present, untouched since7b359999.
Test changes since 7b359999
None deleted, weakened or skipped. Diff is additive only (+312/-66, and the deletions are the old hardcoded-literal function bodies being replaced by the catalog-routed versions plus a widened test module): new tests every_prover_reading_resolves_through_the_catalog, every_entry_set_reading_resolves_through_the_catalog, every_payout_reading_resolves_through_the_catalog, every_cadence_reading_resolves_through_the_catalog, sentence_builders_carry_no_hardcoded_english_literal. No #[ignore] introduced anywhere in the diff.
Not re-litigated (settled, out of scope)
Pane-mount location, no-clawback-affordance shape, screenshot acceptance, and the 15.5.1 version were not revisited.
What I did not run
No local build (cold build measured 46m21s and dies on vendored OpenSSL under MSYS perl, per brief) -- relying on CI green (11/11, including Test + coverage (>=80% lines, gated)) as authoritative at this head.
Verdict: PASS at d8b69b7959055218e40e89fd7099f170942a26ad. Open thread resolved. Zero threads remain open.
Security re-gate — PASSHead audited: Re-audited the full delta since the prior PASS at Cleared by name
Not re-litigated (per brief)Pane-mount status, clawback affordance absence, screenshots, version — as instructed. Scope audited
Not coveredDid not independently re-run the full workspace test suite locally (large multi-crate build exceeded the session's practical wait; relied on the green CI run at this exact head for build/test proof). No live-node exercise — none exists yet (transport unwired by design). |
Adversarial gate (3rd leg) — CHANGES-REQUIRED @
|
F1 (blocking): rewards-cadence-no-mirrors-yet displayed the
never-admitted-vs-evicted-after-settlement distinction SPEC §12.5
clause 7 forbids ("...yet"), in all 14 locales. Reworded to state
only the absence, with a mechanical regression test asserting the
rendered sentence carries no temporal/mechanism word ("yet", "still",
"never", "evict", "admit", "remov").
F2 (blocking): the only guard against re-hardcoding English in the
four sentence builders read freed memory via `unsafe { transmute }`
on a dropped local buffer. string_literals() now returns owned
Strings; unsafe removed.
F3: rewards-paid-out-total named no subject, in all 14 locales,
risking the ~250x owner-attribution defect already deleted once from
this PR. Reworded to name the distributor as the payer of mirrors.
F4: WarningsShown::having_displayed's doc claimed only paint code
could produce a witness, which is false given REQUIRED_WARNING_KEYS
is pub. Doc corrected to state what it actually guarantees (exact
five keys, no partial/no-arg forge); Copy dropped.
F5: the far-end cadence sentence said "far more than N days" but
fires at N+0.1; softened to "more than N days" in all 14 locales.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
MichaelTaylor3d
left a comment
There was a problem hiding this comment.
Correctness re-gate: PASS
Head: 61321969 (6132196905320a89255ec116b072916dccb9b347)
Diff since d8b69b79 re-verified: 14 locale .ftl files + crates/dig-app-core/src/rewards/pane.rs only (95+/59- across 15 files) — nothing else moved.
Checklist:
rewards-cadence-no-mirrors-yetreworded in all 14 catalogs — checked each locale's line by hand (the guard test only checks the active/English render, so this needed a manual pass, not just re-running the test): en/de/es/fr/pt-BR "yet/noch/todavía/encore/ainda" all dropped; ja "まだ" dropped; zh-CN/zh-TW "目前还/還" dropped; ru "Пока" dropped; tr "Henüz" dropped; vi "Chưa" dropped; hi "अभी तक" dropped; ko "아직" dropped; id "Belum"→"Tidak". No temporal/reversibility claim survives in any of the 14.rewards-paid-out-totalnames the distributor as subject in all 14 (verified each line: "This distributor has paid…" / "Dieser Distributor hat…" / "Este distribuidor…" / "Ce distributeur…" / "इस डिस्ट्रिब्यूटर…" / "Distributor ini…" / "このディストリビューターは…" / "이 배포자는…" / "Este distribuidor…" / "Этот дистрибьютор…" / "Bu dağıtıcı…" / "Nhà phân phối này…" / "该分发者…" / "該分發者…"), all still route money through the$amountplaceable, no re-derivation.pane.rs:613-644(string_literals) —unsafe { std::mem::transmute }removed, function now returns ownedStrings instead of&strborrowed from a buffer that dropped at function end (was UB, not just style). Guard re-validated non-vacuous: reverting only theen.ftlfix (reintroducing "yet") makes the new testno_mirrors_yet_sentence_carries_no_never_admitted_vs_evicted_distinction(pane.rs:382-393) fail on the"yet"needle — confirmed by reading the assertion against the reverted string, not run in a sandbox per the no-local-build constraint. The other four sentence-builder / no-bare-literal tests are untouched and still exercisestring_literals.WarningsShown(pane.rs ~649-668):Copydropped (nowCloneonly); doc comment rewritten to state only what it actually proves — that the caller named the exact five required keys — and explicitly disclaims the old false claim ("only paint code that rendered them can produce a witness"). Grepped the whole crate:WarningsShownhas no call sites outsidepane.rs, so droppingCopybreaks nothing.- Confirmed untouched/still true: no bare English literal in any sentence builder, no
format!gluing translated fragments, the five ratified warning blocks /rewards-warning-heading/rewards-warning-closingbyte-identical, money only viaamount::format_asset_amount/amount_with_unit, no share recomputed from a compiled-in constant,tab_id_all_is_still_the_six_labelsandactivity_tab_emits_zero_action_rowspresent and unmodified in this diff. - No test deleted, weakened,
#[ignore]d or skipped sinced8b69b79— the only test-file touched (pane.rs) has a net +1 test (the new F1 guard) and the one existing test's doc comment updated to match the new (non-temporal) English string; no#[test]/#[ignore]lines removed anywhere in the diff.
CI: 10/11 green; Native confirmer (windows-latest) queued/pending, not failing — not treated as a blocker per the brief. Coverage gate ("Test + coverage (>=80% lines, gated)") passed.
Review threads: 2 threads on this PR, both already isResolved: true (one outdated from an earlier round, one current on pane.rs:51). Zero open threads — nothing new to file; this round produced no findings.
Not re-litigated: unmounted pane, #3273 mount split, no-clawback affordance, screenshots, version bump (15.6.0 — the ticket's stated 15.5.1 appears stale against this PR's actual bump commit, not re-opened here since instructed not to litigate version).
Not run: no local build/test execution (46m cold build, dies on vendored OpenSSL under MSYS perl per instruction) — relied on CI's own green "Test + coverage" run for this head plus static re-reading of the guard-test logic against a manually-reverted string.
Adversarial gate (third leg) — PASS at
|
Security re-gate — PASSHead audited: FindingsNone live. Five items checked, all closed correctly:
Cleared previously (d8b69b7), not re-derivedNo compiled-in share bps; no unformatted base units on the payout path; no Not coveredCI's queued |
WarningsShown claims in its doc comment to prevent witnesses being cloned, but Clone was still derived. Remove it to align behaviour with the documentation and enforce re-validation via having_displayed. Closes #3253
|
PASS Delta vs 6132196 verified narrow: Judged the one-line change: removing Confirmed what was already recorded:
Cleared by name (mine, this pass): delta-narrowness (single line, single file); the Scope audited: |
Adversarial third leg — PASS at
|
| Sentence / figure | Subject asserted | Wire field that proves it |
|---|---|---|
rewards-paid-out-total |
"This distributor has paid … to mirrors" | counters.total_paid_out_base_units on the distributor's own record — subject repaired, no longer readable as this operator's earnings |
rewards-entry-set-known / -stale |
the distributor's entry set | counters.entry_count + last_entry_write_at, no possessive |
all four rewards-cadence-*, rewards-refill-cadence |
"a mirror" — indefinite, hypothetical | funding rate + PAYOUT_THRESHOLD_BASE_UNITS; deliberately never "you" |
rewards-status-heartbeat-* |
the local node's own prover | observed_at vs local clock |
rewards-donation-body / -confirm-last-line |
"your control", "by you or anyone else" | the viewer is the spender in the flow that ships it; true |
rewards-warning-block-2..5 |
"your data", "you are committing", "the manager key you are about to create" | true by construction of the creation flow — see the scope hazard below |
rewards-clawback-confirm-body |
"{returned} returns to this wallet" | nothing — see F7 |
Two things I went looking for and did not find:
- A relocated share constant.
warning-block-3states 90%/10% as prose with no$variable, which is the same numberwire.rs:82forbids BY NAME from being computed (committed * 9000 / 10_000). This is not the banned defect: the creation copy states a share the app is itself setting on a distributor it is creating, while the observation copy (clawback-confirm-body) carries no percentage at all and parameterizes{returned_amount}/{forfeited_amount}instead. The codebase already draws exactly the constant-vs-wire line I would have demanded.git grep '9000|9_000|withdrawal_share|10_000'oversrc/rewards— zero hits. And all 14 locales say 90/10 (tras%90/%10); no locale invents a different split. - A live reader of the inert type.
clawback_puzzle_hashandrecoverable_base_unitshave zero readers outsidewire.rs's own field-completeness test. The type really is dead, and its four fields are correct and complete for what they carry.
F7 (NEW, NON-BLOCKING) — RewardDistributorCommitment carries no proof the viewer owns the commitment, and the copy already claims they do
crates/dig-app-core/src/rewards/wire.rs:104-109
dig.listRewardDistributorCommitments is a public read: clawback_puzzle_hash is a 32-byte hash on a public record, and nothing in the record says the viewer controls the key behind it. Meanwhile rewards-clawback-confirm-body (en.ftl:17) already asserts "{returned_amount} returns to this wallet" and rewards-clawback-withdraw-button renders "Withdraw {returned_amount}". Paint those against any commitment the record can return and you get this ticket's false-subject class in its strongest form — a money figure, with a button on it, attributing another party's recoverable funds to the viewer's wallet.
Not exploitable at this head, and that is why it does not block: nothing paints the pane (#3273), RewardsClient does not adopt the RPC (the trait method was deleted at the round-2 gate), and no clawback affordance ships — which the brief correctly holds as right for this PR.
But it stops being theoretical soon. With #595 terminal green at 134864a9 and #594 green at 5f729d1b, the first paint of these four strings is the first moment a false subject can reach a person. The obligation the type must carry before then, and does not carry now:
- render no
rewards-clawback-*string unlessclawback_puzzle_hashmatches a puzzle hash the viewer's own keys derive; and - source
$returned_amountfromrecoverable_base_unitsonly — never from the 90% in the warning copy, which is authored for a distributor being created, not observed.
Cheapest fix is a doc obligation on RewardDistributorCommitment itself, where the next reader arrives — the type's doc comment today explains the recompute ban and the five-field seam but never says ownership of clawback_puzzle_hash is unproven. Sequencing: land it before whichever comes first of #595 merging or a mount landing under #3273 — not in this PR.
Scope note (not a finding)
warning-block-2..5 are owner-scoped copy ("your data", "you are committing"), and WarningsShown/CreationGate gate acknowledgement, not whose distributor is on screen. True by construction while the only caller is the creation flow. If that copy is ever reused on an observation surface, it inherits F7's class for free.
Verdict
PASS at 9e5497e0. F6 resolved, the delta is exactly the one-line derive removal and nothing else, F1–F5 and every prior survived item unchanged. F7 is recorded as a non-blocking obligation with a sequencing deadline. Nothing here blocks merge.
MichaelTaylor3d
left a comment
There was a problem hiding this comment.
Correctness re-gate: PASS
Head: 9e5497e0 (9e5497e0147348a33a4bb43a695716191f2f6ef6). 11 green / 0 failing / 0 pending.
Delta verified against the claim, not taken on trust
git diff 61321969 9e5497e0 --stat:
crates/dig-app-core/src/rewards/pane.rs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
git rev-list --count 61321969..9e5497e0 = 1. Only change: #[derive(Debug, Clone, PartialEq, Eq)] -> #[derive(Debug, PartialEq, Eq)] on WarningsShown (pane.rs:660). No cargo fmt hunk anywhere else — the claimed scope is exact.
Doc claim now true, no caller broken
grep -rn WarningsShown across the tree: 11 sites, all in pane.rs. Zero .clone() calls on the type (checked file-wide). Both construction sites (having_displayed at pane.rs:763, :790) mint a fresh witness rather than duplicating one. CreationGate::acknowledge(self, _shown: WarningsShown) (pane.rs:725) consumes the witness by value. With Clone gone, the doc's claim at pane.rs:656 — "a caller cannot mint a second witness from a first without calling having_displayed again" — is now true as written. No caller was relying on .clone().
Nothing else regressed (by construction, since the diff is one line)
- All 7
rewards-*key families present in all 14.ftllocales (rewards-cadence-no-mirrors-yet,rewards-paid-out-total,rewards-warning-heading,rewards-warning-closing: 14/14 each) — untouched since prior PASS. tab_id_all_is_still_the_six_labelsandactivity_tab_emits_zero_action_rowspresent intab_placement.rs, untouched.- The five ratified warning blocks and the four sentence builders (
prover_status_sentence,entry_set_sentence,payout_sentence,cadence_sentence) are outside the touched line range — byte-identical to61321969. git diff 61321969 9e5497e0 --statshows zero test files touched; nothing deleted, weakened, skipped or#[ignore]d since the prior PASS.
Version manifest (correction carried forward)
Root Cargo.toml at this head reads 15.5.1; origin/main reads 15.5.0. Confirmed by reading the manifest directly (not the commit log) — the increment is correct, not re-litigated further.
Not re-litigated
Unmounted pane, #3273 mount split, no-clawback affordance, screenshots, version 15.5.1 — out of scope per brief.
Not run
Local build/test — cold build measured 46m21s and dies on vendored OpenSSL under MSYS perl; CI (11/11 green at this head) is authoritative.
Verdict: PASS at 9e5497e0147348a33a4bb43a695716191f2f6ef6.
DO NOT MERGE — gate round in progress
Refs #3253
What this PR builds
The Rewards section's data/reading/copy/model layer:
crates/dig-app-core/src/rewards/pane.rs,client.rs,wire.rs,reading.rs,cadence.rs,tab_placement.rs, plus the workspace-rootversion bump. Clawback ships no affordance -- the serving RPC (
dig.listRewardDistributorCommitments)is defined in dig-rpc-protocol v0.11.0 but not served by any running dig-node build (PRs #593/#594);
a control fed by an unserved RPC is a false statement about the operator's money.
Shipped, green in CI (Clippy/Rustfmt/Headless/Doc-hygiene/version-gate/Test+coverage/Native
confirmer win+mac all pass at 39a07d5)
pane.rsskeleton -- four async states (PaneReading->PaneNote), exhaustive, testedclient.rs/wire.rs:RewardDistributorCommitmentadopted verbatim (all 4 SPEC §2.6 fieldsor none), client/wire layer only, never painted
cadence.rs: three-caseCadenceReadingcloses theentry_count == Some(0)reassuring-zerodefect
reading.rs:saturating_addfix fordue + MAX_SECONDS_OFFSEToverflowwire.rs:RewardCountersfields narrowed topub(crate)floor/gatepane::rewards_sections(): prover status / entry-set / payout / cadence as 4Sectionfacts,money only via
amount::format_asset_amount, no rows (no affordance ships)pane::CreationGate: compile-enforced state machine for the Q1 warning-acknowledgment gate --no constructor hands out an already-acknowledged gate
pane::mirror_claim_record/mirror_claim_section: read-only, verb-free Activity fact forbeing paid as a mirror, echoed from
PayoutReading, no rowsExplicitly NOT shipped in this PR, and why
(
confirm/gui/window/pane/content.rs'scapsule_row/capsules_card) is hand-paintedimmediate-mode egui with manual
Pos2/Rectlayout math -- it does not use thewindow_model::Section/MenuRowmodel this pane's data layer targets, has no expand/collapsestate anywhere, and
HostedStorecarries no distributor reference. This is a feature-shapeddata-layer + paint change (a lookup keyed by
store_id, expand state threaded through thecaller, hand-painted fact lines matching this file's exact pixel conventions), not a "mount a
section" edit, and CI cannot catch a layout defect in it -- a wrong-but-compiling paint is
green for the wrong reason on a money surface. Recommend it ship as its own reviewed unit;
asked the parent lane, who agreed and is ticketing it separately.
point to call it from)
CommitIncentives,COMMITMENT_DEPTH_EPOCHS = 2) paintcopy.rs; nothing renders them yet)activity.rsdoesn't call it yet)with no Content-tab mount point, the pane cannot be reached in a running app, so a finished
release build would not produce them. Not attempting a mock-up, a reduced-zoom capture, or a
synthetic-input capture on a money surface. Started, then stopped, the local release build for
this reason (scoop-perl / vendored-OpenSSL workaround confirmed working, but pointless without
a mount point).
listRewardDistributorCommitmentsisserved (needs dig-rewards-coin 0.2.0 per the parent lane)
§12.5 note carried forward for the wiring lane
dig-rewards-coin SPEC 0.1.3 §12.5:
dig.listRewardDistributorsanswers{funded, claimable}overRewardDistributorRef{launcher_id, store_id, root}only -- list membership must never be read as anentitlement, an accrual, or a boolean claim-status, and an absent entry slot must never be
distinguished from an eviction nor rendered as a bare zero. Nothing in this PR touches list-membership
inference (rewards_sections/mirror_claim_record both operate on the status/payout record, not on
claimable), but the wiring lane will hit this the moment it readslistRewardDistributors-- flaggingso it isn't re-derived.
🤖 Generated with Claude Code