Uh oh!
There was an error while loading. Please reload this page.
feat(collateral): declare the epoch collateral requirement + safety margin control methods - #33
Conversation
…ety margin methods Refs: #32 Co-Authored-By: Claude <noreply@anthropic.com>
…IP, test target red) Salvaged from a lane the session cap killed mid-implementation, with 462 insertions uncommitted across five files. Pushed so the work is durable. State, stated honestly so the next lane does not mistake this for finished: * `cargo check` on the LIB passes. * `cargo test` does NOT compile: `src/kats.rs` is missing `use crate::params;` (E0433). The lane was mid-edit on the handler trait when it died. So this is work-in-progress, not a green branch. The next step is that import and then whatever the kats need to actually exercise the new methods. Refs #32 Co-Authored-By: Claude <noreply@anthropic.com>
…ol methods Three token-gated, shell-owned methods: control.collateral.requirement -- this epoch's per-store requirement + census inputs control.collateral.margin.get -- the local safety margin, in basis points control.collateral.margin.set -- set it, bounded and refused rather than clamped Unknown is a tagged variant carrying its reason, so no representable state renders an absent requirement as a zero cost. The protocol version that computed the epoch travels with the figure. The margin is u64 basis points and is never converted. Refs: #32 Co-Authored-By: Claude <noreply@anthropic.com>
…ods normatively SPEC.md gains the three catalog rows and section 4.2e: the margin is never a consensus input, is basis points and never converted, is refused rather than clamped above the bound, defaults to 100bp rather than 0 for a config predating the field, and an unknown requirement is stated as unknown with a named reason rather than a zero. Co-Authored-By: Claude <noreply@anthropic.com>
…argin bounds Co-Authored-By: Claude <noreply@anthropic.com>
Minor: three new methods, one new Category variant, two new result types and two new constants. ControlMethod, Category and ControlErrorCode are all #[non_exhaustive], so a downstream match already carries a wildcard arm and no existing field changes meaning. Co-Authored-By: Claude <noreply@anthropic.com>
MichaelTaylor3d
commented
Aug 28, 2026
IN PROGRESS — not the verdict. Gate on head Confirmed so far:
Still verifying: the error-code claim about |
MichaelTaylor3d
left a comment
There was a problem hiding this comment.
Gate round 1 (head 5991ff6) — one GATING inline finding attached; verdict comment posted separately.
Uh oh!
There was an error while loading. Please reload this page.
MichaelTaylor3d
left a comment
There was a problem hiding this comment.
Gate round 1 — second inline finding.
Uh oh!
There was an error while loading. Please reload this page.
MichaelTaylor3d
left a comment
There was a problem hiding this comment.
Gate round 1 — third inline finding.
Uh oh!
There was an error while loading. Please reload this page.
MichaelTaylor3d
commented
Aug 28, 2026
CHANGES-REQUIREDHead reviewed: Three GATING findings, each posted as its own inline thread. None makes the shipped behaviour wrong today — the contract's semantics are right, and most of what the brief asked me to check came back clean. What is wrong is that two of the guards meant to keep it right do not, and one of the three public enums is missing the attribute that makes this a legitimate minor. Ranked findings
What I verified CLEAN
The two external findings — accurately stated, with one correctionBoth confirmed against
Re-gate scopeFindings 1 and 3 are test/comment-only; finding 2 is a one-line attribute plus its doc. None touches the wire shape, the auth tier or the refusal logic, all of which I have cleared. So the re-gate should be one correctness pass over the fix diff, not a fresh full round. Threads: 3 open, all mine, all genuine blockers. No GHAS or third-party threads exist on this PR. The PR is correctly still a draft. Probes ran in an isolated worktree cut from the PR head under |
…d mark Category non_exhaustive Three gate findings on PR #33: - kats: the_published_margin_bounds_match_the_declared_constants matched the constant's DIGITS anywhere in SPEC.md, so it caught a missing row and never a wrong one. It now reads the figure out of the `NAME` (N bp sentence and compares it as a number, checking every occurrence. - method: Category gains a variant in a minor release, so it takes #[non_exhaustive] like its ControlMethod and ControlErrorCode siblings. - traits: the collateral arms were inserted under SpendsList's comment, which said deserialization already validated. It does not for CollateralMarginSetParams; the arms now say validated() is the sole enforcement of MAX_SAFETY_MARGIN_BP. Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Uh oh!
There was an error while loading. Please reload this page.
DRAFT — DO NOT MERGE. Gate round has not returned.
Closes#32. Child of epic https://github.com/DIG-Network/dig_ecosystem/issues/3173.
What this declares
Three shell-owned, token-gated methods in a new
Category::Collateral:control.collateral.requirementCollateralRequirementResultcontrol.collateral.margin.get{margin_bp:u64}control.collateral.margin.set{margin_bp:u64}{margin_bp:u64}CollateralRequirementResultis a#[serde(tag = "state")]union:known—epoch,protocol_version,required_per_store_dig_base_units, and the census inputsbehind the figure:
stores,owners,multiplier_micros,handicap_dig_base_units.unknown— areasonfromnot_censused|behind_finality_depth|record_unreadable|no_chain_source.Unknown is a variant, not an optional number. There is no representable value carrying a figure
the node was not given, which is what dig-app
SPEC.md§3.7b requires when it forbids any path thatrenders an absent requirement as a zero cost. The four reasons name different missing facts with
different remedies and are deliberately not collapsed.
The protocol version that COMPUTED the epoch travels with the figure, and a reader REFUSES a
knownrequirement that omits it rather than defaulting — the model is versioned and a client thatknows only the number cannot tell a rule change from a disagreement.
The margin is
u64basis points under the keymargin_bp, never converted, matching dig-appSPEC.md§3.7b and the unitdig_mirror_collateral::apply_safety_margintakes. Two constants arepublished:
DEFAULT_SAFETY_MARGIN_BP = 100(+1%) andMAX_SAFETY_MARGIN_BP = 10_000(+100%).Above the bound the request is REFUSED as
-32602, not clamped. The asymmetry with dig-app isdeliberate and stated in
SPEC.md§4.2e: dig-app clamps a value already on disk because refusing itwould leave the node on the lower posting, whereas a
.setcaller is stating an intent right now andsilently applying a different number would leave stored intent and node behaviour disagreeing about
money. The bound exists because
.setis a money-path mutation reachable with an ordinary pairedtoken and the margin arithmetic saturates rather than failing.
The requirement returns the PRE-margin figure and never the margin, so a node cannot present its
own preference as the network's price. The held-store count is not served here — a client
assembling the recommended-$DIG buffer reads it from
control.hostedStores.list, rather than thiscreating a second source of truth for an input to a money calculation.
dig-mirror-collateralis deliberately NOT a dependency: it sits at the SAME crate level as thiscontract and a same-level edge is forbidden (CLAUDE.md Appendix B). The two constants are restated
here with that reason recorded in their doc comments.
Error codes: NONE minted, and why
No new error code. Every failure this method set has is already covered: an out-of-range margin
is a malformed param (
-32602 INVALID_PARAMS), a persistence failure is-32032 CONTROL_ERROR, abuild without the feature is
-32031 NOT_SUPPORTED, and an unknown requirement is deliberately notan error at all but a result variant — which is precisely what stops a client rendering it as zero.
How the space was checked anyway, since #27 records that
-32044already collided. This crateowns
-3204x; declared here are-32040..-32044and-32046..-32048. A grep of every-320xxliteral in dig-node returns
-32000..-32017,-32020..-32022,-32030..-32033,-32040..-32052,-32060,-32099.The apparent gap at
-32045is NOT free space — it is a stale claim.dig-node/SPEC.md:5877still states normatively that
-32045isWALLET_RESERVATIONS_UNAVAILABLE, which #28 moved to-32047.-32049..-32052are dig-node's chat band (NO_IDENTITY = -32050, undeclared upstream).Minting
-32045here would have re-created exactly the-32044collision #27 records. Logged forthe adopter below rather than fixed in this repo.
Blast radius
Purely ADDITIVE — no existing symbol was edited, renamed or deleted, and no field changed
meaning.
gitnexuswas not run: this is a fresh worktree and an index build is the ~10-minuteoperation §2.0 caps, for a change with no edited symbol to analyse. The radius was measured by
grep + direct read instead, which §2.0 sanctions and which is recorded here.
Consumers of
dig_node_control_interfaceacross the ecosystem:dig-node-service(
control.rs,control_cli.rs,meta.rs,server.rs, two test files) anddig-wallet.ControlMethod,CategoryandControlErrorCodeare all#[non_exhaustive], so a downstreammatch already carries a wildcard.
control.rs:869,control_cli.rs:478) dispatch on&str, not the enum, so a new method cannot break them at all.control_contract_conformance.rs:124filters oncategory() == Category::Wallet; a newCategoryvariant does not enter that filter.the_node_serves_every_control_method_the_contract_publishescomparesControlMethod::ALLagainst
CONTROL_METHODS, so on adoption dig-node must either serve the three methods or listthem in
KNOWN_PREEXISTING_DRIFT. That is the intended pressure, not a regression.Risk: LOW. No HIGH/CRITICAL finding.
SemVer
0.22.0 → 0.23.0, minor. Additive only: three methods, one
Categoryvariant, two result types,two constants, three trait methods. Adding a method is minor; a changed field meaning would be
breaking and none changed. The trait gains required methods, which is a break for an out-of-tree
implementor — the only implementors are dig-node and this crate's own mock, and every prior method
addition in this crate has shipped the same way.
Verification
cargo test— 168 unit + 9 doc-tests green.cargo clippy --all-targets -D warningsclean.SPEC.md§4.2e added with the three catalog rows;README.mdrows added. The existingthe_spec_and_readme_name_every_catalogued_methodguard was RED until both were written, and theerror-code table guard from feat(spends): declare control.spends.list, the sanctioned reader for the spend audit record #31 passes unchanged.
Every normative MUST in §4.2e is true of the code in this diff, and the four load-bearing ones
were each proved by reverting ONLY their subject (committed first, restored after):
.setstops persistingsetting_the_margin_persists_it_and_getting_it_back_agreesthe_margin_ceiling_is_pinned_from_both_sidesprotocol_versiongiven#[serde(default)]a_known_requirement_must_declare_its_protocol_versionCollateralRequirementadded tois_open_readthe_collateral_methods_are_named_categorised_and_gatedFixture notes, since narrowness is where false greens are born:
100.100is +1% and survives a basis-points-to-percentconversion as the plausible integer
1; 1 bp collapses to0under any such conversion, which isthe silent no-margin the round-up exists to prevent.
tested only from below is satisfied by an implementation with no bound at all.
.setthen.getare two separate dispatcher calls. Assertingresult == paramsin one callis satisfied by a handler that echoes and stores nothing.
knownfixture gives every field a different value, so a transposition of any pair failsrather than passing on a shape they share.
the_published_margin_bounds_match_the_declared_constantsties the numbers in normativeSPEC.mdprose to the constants, so the two cannot drift.
Dependencies (§2.4b)
This crate declares no
dig-*and nochia-*dependencies — onlyserde,serde_json,async-trait,semver,futures. Nothing to bring forward;cargo update -plocks 0 changes.For the adopter, not for this PR
dig-node-control-interface = "0.21"and does not servecontrol.spends.listfrom 0.22 (dig-node#385). This method set lands in that same adoption.dig-node/SPEC.md:5877is stale: it still names-32045 WALLET_RESERVATIONS_UNAVAILABLEafter feat(wallet)!: move the reservation codes off -32044, which dig-node already owns #28 moved it to
-32047. A reimplementer building from that sentence mints a collidingcode. Worth folding into the same adoption.