Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ resolver = "2"
members = ["crates/dig-app-core", "crates/dig-app", "crates/diga"]

[workspace.package]
version = "15.5.1"
version = "15.5.2"
edition = "2021"
rust-version = "1.75"
license = "GPL-2.0-only"
Expand Down
533 changes: 533 additions & 0 deletions crates/dig-app-core/src/rewards/clawback.rs

Large diffs are not rendered by default.

42 changes: 37 additions & 5 deletions crates/dig-app-core/src/rewards/copy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,33 @@ pub const WARNING_CLOSING_LINE_EN: &str =
// ---------------------------------------------------------------------------------------------

/// Placeable: `epoch_index`.
pub const CLAWBACK_CONFIRM_TITLE: Msg = Msg::new("rewards-clawback-confirm-title");
///
/// `pub(super)` (dig_ecosystem#3281), not `pub`: dropping this constant's own path to `pub(super)`
/// narrows who can name THIS `Msg` handle to the `rewards` module. That is reach-narrowing, not
/// unreachability -- see `super::clawback`'s module doc for the same distinction stated in full.
/// [`crate::i18n::Msg::new`] is a public `const fn` and the fluent key is a plain `&'static str`
/// literal, so any crate that writes `Msg::new("rewards-clawback-confirm-title")` still renders
/// this sentence with any amounts and any hash it likes -- no caller "can't forge" it in the sense
/// of being unable to reproduce the value; nothing here stops that. What IS true: the only value
/// in this crate whose text is bound to a commitment a real wallet key was proved to control is a
/// [`super::clawback::ProvenClawback`], producible only through
/// [`super::clawback::ProvenClawback::open`], which requires a
/// [`super::clawback::ClawbackAuthority`] -- see that module's doc for how `open` is gated.
/// `ALL_KEYS` below is private for the same reach-narrowing reason and stays walked only by this
/// module's own 14-locale completeness and forbidden-phrase sweeps.
pub(super) const CLAWBACK_CONFIRM_TITLE: Msg = Msg::new("rewards-clawback-confirm-title");
/// Placeables: `slot_amount`, `epoch_index`, `epoch_start_date`, `returned_amount`,
/// `forfeited_amount`, `clawback_ph_short`. Every value MUST come from the parsed commitment slot,
/// never from pane state (DECISIONS Q3: "the confirm window reads NOTHING from the pane's state").
pub const CLAWBACK_CONFIRM_BODY: Msg = Msg::new("rewards-clawback-confirm-body");
///
/// `pub(super)`, same reasoning as [`CLAWBACK_CONFIRM_TITLE`] above.
pub(super) const CLAWBACK_CONFIRM_BODY: Msg = Msg::new("rewards-clawback-confirm-body");
/// Placeable: `returned_amount` — the approving click names the amount.
pub const CLAWBACK_WITHDRAW_BUTTON: Msg = Msg::new("rewards-clawback-withdraw-button");
pub const CLAWBACK_KEEP_BUTTON: Msg = Msg::new("rewards-clawback-keep-button");
///
/// `pub(super)`, same reasoning as [`CLAWBACK_CONFIRM_TITLE`] above.
pub(super) const CLAWBACK_WITHDRAW_BUTTON: Msg = Msg::new("rewards-clawback-withdraw-button");
/// `pub(super)`, same reasoning as [`CLAWBACK_CONFIRM_TITLE`] above.
pub(super) const CLAWBACK_KEEP_BUTTON: Msg = Msg::new("rewards-clawback-keep-button");

// ---------------------------------------------------------------------------------------------
// Q3 — the irrevocable donation (`AddIncentives`) disclosure. Never on/adjacent to the fund
Expand Down Expand Up @@ -129,7 +148,20 @@ pub const CADENCE_FAR_END: Msg = Msg::new("rewards-cadence-far-end");
/// Every key this module defines, for the exhaustiveness/render/sweep tests below. Keeping this
/// list here (rather than re-deriving it per test) is the one place a new key must be added or the
/// tests that iterate "every rewards key" silently stop covering it.
pub const ALL_KEYS: &[Msg] = &[
///
/// `#[cfg(test)]`, not `pub` (dig_ecosystem#3281 S2): its only callers are this module's own
/// `every_copy_key_renders_in_the_active_language` and `no_rewards_copy_contains_a_forbidden_phrase`
/// tests below (verified: no other file in the crate names `ALL_KEYS`), so gating it to test
/// builds costs nothing -- and is required, not just tidier: a plain private (non-`pub`) const
/// with no non-test caller is genuinely dead code in a `--no-default-features` release build and
/// trips `-D warnings`' dead-code lint. A `pub` `ALL_KEYS` re-exported all four `CLAWBACK_*` keys
/// BY VALUE regardless of their own `pub(super)`, so `ALL_KEYS[8].with(..)` rendered the full
/// confirm body from outside this crate with no [`super::clawback::ClawbackAuthority`] witness at
/// all -- narrower than a forbidden literal or constant name, so a source-scan guard could not see
/// it. Narrowing this is reach-narrowing too: a caller can still write the literal key and call
/// the public [`Msg::new`] directly (see [`CLAWBACK_CONFIRM_TITLE`]'s doc above).
#[cfg(test)]
const ALL_KEYS: &[Msg] = &[
WARNING_HEADING,
WARNING_BLOCK_1,
WARNING_BLOCK_2,
Expand Down
3 changes: 3 additions & 0 deletions crates/dig-app-core/src/rewards/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,12 @@
//! is attempted vs. not.

pub mod cadence;
pub mod clawback;
pub mod client;
pub mod copy;
pub mod pane;
pub mod reading;
pub mod tab_placement;
#[cfg(test)]
pub(crate) mod test_scan;
pub mod wire;
47 changes: 4 additions & 43 deletions crates/dig-app-core/src/rewards/pane.rs
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ pub fn rewards_sections(
#[cfg(test)]
mod rewards_sections_tests {
use super::*;
use crate::rewards::test_scan::{function_body, string_literals};
use crate::rewards::wire::{ProverState, RewardCounters};

fn base_record() -> RewardDistributorStatusRecord {
Expand Down Expand Up @@ -595,49 +596,9 @@ mod rewards_sections_tests {
}
}

/// Slices `src` from `start_marker` (a `fn ...` signature) to `end_marker` (the next
/// function's signature) -- an explicit pair per builder, deliberately not a generic "next
/// `fn`" scan (see the guard above for why).
fn function_body<'a>(src: &'a str, start_marker: &str, end_marker: &str) -> &'a str {
let start = src
.find(start_marker)
.unwrap_or_else(|| panic!("{start_marker} not found in pane.rs"));
let rest = &src[start..];
let end = rest
.find(end_marker)
.unwrap_or_else(|| panic!("{end_marker} not found after {start_marker} in pane.rs"));
&rest[..end]
}

/// Every `"..."` string literal in `body`'s CODE lines, naively (no escape handling -- none
/// of this module's literals need it). Comment lines (`//`/`///`) are skipped first -- a
/// quoted phrase inside a doc comment (e.g. this very module's own prose) is not a Rust string
/// literal and must not trip the guard.
///
/// Returns owned `String`s, not `&str`s borrowed from the local `code_only` buffer: an
/// earlier revision borrowed from that buffer via `unsafe { std::mem::transmute }` to escape
/// the borrow checker, which is undefined behaviour -- `code_only` drops at the end of this
/// function, so every caller was reading freed memory (dig_ecosystem#3253 adversarial gate,
/// finding 2). There is no reason to borrow here at all; owning the substrings costs a few
/// allocations in a `#[cfg(test)]`-only helper and removes the `unsafe` entirely.
fn string_literals(body: &str) -> Vec<String> {
let code_only: String = body
.lines()
.filter(|line| !line.trim_start().starts_with("//"))
.collect::<Vec<_>>()
.join("\n");
let mut out = Vec::new();
let mut rest: &str = &code_only;
while let Some(start) = rest.find('"') {
let after = &rest[start + 1..];
let Some(end) = after.find('"') else {
break;
};
out.push(after[..end].to_string());
rest = &after[end + 1..];
}
out
}
// `function_body`/`string_literals` moved to `super::super::test_scan` (dig_ecosystem#3281):
// `clawback`'s key-isolation guard needs the same string-literal extractor, and the plan calls
// for reusing it rather than writing a second one. Imported at the top of this module.
}

/// Evidence that a caller supplied exactly the five required warning-block keys to
Expand Down
49 changes: 49 additions & 0 deletions crates/dig-app-core/src/rewards/test_scan.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
//! Shared `#[cfg(test)]` source-scanning helpers used by more than one `rewards` test module --
//! [`super::pane`]'s hardcoded-English guard and [`super::clawback`]'s key-isolation guard
//! (dig_ecosystem#3281 plan step 6: "an in-file source-scanning string-literal extractor already
//! exists ... reuse it, do not write a second"). Moved out of `pane.rs` verbatim so both share the
//! ONE implementation rather than each carrying its own.

#![cfg(test)]

/// Slices `src` from `start_marker` (a `fn ...` signature) to `end_marker` (the next function's
/// signature) -- an explicit pair per caller, deliberately not a generic "next `fn`" scan (see
/// [`string_literals`]'s doc for why).
pub(crate) fn function_body<'a>(src: &'a str, start_marker: &str, end_marker: &str) -> &'a str {
let start = src
.find(start_marker)
.unwrap_or_else(|| panic!("{start_marker} not found in source"));
let rest = &src[start..];
let end = rest
.find(end_marker)
.unwrap_or_else(|| panic!("{end_marker} not found after {start_marker} in source"));
&rest[..end]
}

/// Every `"..."` string literal in `body`'s CODE lines, naively (no escape handling -- none of this
/// crate's guarded literals need it). Comment lines (`//`/`///`) are skipped first -- a quoted
/// phrase inside a doc comment is not a Rust string literal and must not trip a caller's guard.
///
/// Returns owned `String`s, not `&str`s borrowed from the local `code_only` buffer: an earlier
/// revision of this function (when it lived only in `pane.rs`) borrowed from that buffer via
/// `unsafe { std::mem::transmute }` to escape the borrow checker, which is undefined behaviour --
/// `code_only` drops at the end of the function, so every caller was reading freed memory
/// (dig_ecosystem#3253 adversarial gate, finding 2). There is no reason to borrow here at all.
pub(crate) fn string_literals(body: &str) -> Vec<String> {
let code_only: String = body
.lines()
.filter(|line| !line.trim_start().starts_with("//"))
.collect::<Vec<_>>()
.join("\n");
let mut out = Vec::new();
let mut rest: &str = &code_only;
while let Some(start) = rest.find('"') {
let after = &rest[start + 1..];
let Some(end) = after.find('"') else {
break;
};
out.push(after[..end].to_string());
rest = &after[end + 1..];
}
out
}
19 changes: 12 additions & 7 deletions crates/dig-app-core/src/rewards/wire.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,13 +85,18 @@ pub struct RewardDistributorStatusRecord {
/// whatever bps this crate happens to have compiled in. This type carries the chain's own already-
/// computed answer instead, so there is nothing here to recompute.
///
/// This type is inert in this pass: nothing constructs, reads or paints from it.
/// [`super::client::RewardsClient`] does not adopt `dig.listRewardDistributorCommitments` (deleted
/// per the dig_ecosystem#3253 adversarial gate's finding 2 — the trait method wrapped only this
/// type's `Vec`, dropping three of the SPEC §2.6 result's five fields), and nothing in dig-node
/// serves the RPC yet either (PRs #593/#594 open, unmerged). This type itself stays: all four
/// fields are correct and complete for what THEY carry, and it is where the full five-field seam
/// lands once clawback is actually wired.
/// This type is no longer inert: [`super::clawback::ClawbackAuthority::prove`] (dig_ecosystem#3281)
/// constructs it into the witness it proves against, and [`super::clawback::ProvenClawback::open`]
/// reads `rewards_base_units`, `recoverable_base_units` and their difference through
/// `amount_with_unit` and paints all three as `$DIG`. The next reader must not assume no custody
/// gate reads this type — the clawback authority gate does, today. What is still true: no
/// constructor here takes a `dig.listRewardDistributorCommitments` RPC response —
/// [`super::client::RewardsClient`] does not adopt that method (deleted per the dig_ecosystem#3253
/// adversarial gate's finding 2 — the trait method wrapped only this type's `Vec`, dropping three
/// of the SPEC §2.6 result's five fields), and no constructor of this type anywhere in this crate
/// sits outside `#[cfg(test)]` code. So this type has no production value at this head, only
/// test fixtures — see dig_ecosystem#3294 for why that gap matters to [`super::clawback`]'s proof,
/// and where closing it lands once the transport is wired.
///
/// # Fields are `pub(crate)`, not `pub`
///
Expand Down
Loading