feat(control): serve control.wallet.coinSpend and control.wallet.coinsByParent - #211
Merged
Conversation
…sByParent Both are OPEN, caller-addressed chain reads: each names its subject with a public coin id and discloses no node-to-address association. Implementation notes: - every answer is bound to the request (coin id recomputed, puzzle reveal tree-hash verified, every child's parent_coin_info asserted) - an unreachable chain is never collapsed into an absence - coinsByParent is paged per the published contract; complete is derived from what REMAINS, never from whether the page filled Refs dig_ecosystem#2572
…r the two chain reads Refs dig_ecosystem#2572
Refs dig_ecosystem#2572
… set Salvage commit: this work was complete and compiling in the worktree but uncommitted when the lane stalled, and only pushed state survives. Adds the control-plane wire-shape tests for `control.wallet.coinSpend` and `control.wallet.coinsByParent`, and extends the written-out open-read list with both. The list stays a literal rather than a derivation from `is_open_read`, so it pins the SET and cannot be satisfied by the code it is meant to constrain. Refs #2572 Co-Authored-By: Claude <noreply@anthropic.com>
…te before the network Also gate the FallbackCoinSpend import behind cfg(test): only the test doubles name the type, and clippy runs with -D warnings. Refs dig_ecosystem#2572
…g contract feat: bump to 0.110.0 -- additive control methods, no existing behaviour changed Refs dig_ecosystem#2572
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
DO NOT MERGE — DRAFT pending the gate round (§2.4a).
Closes DIG-Network/dig_ecosystem#2572. Part of the dig-profile epic #2398.
Serves two OPEN control reads so a client can implement a full
ChainSourceover the node'scontrol plane, which is what makes a dig-profile mint possible through the node instead of via
third-party HTTPS.
control.wallet.coinSpend— the spend that spent a coin (coin + puzzle reveal + solution),named by that coin's own id.
spend: nullmeans a chain answered and holds no spend.control.wallet.coinsByParent— one PAGE of the direct children a coin's spend created.One hop, never a walk.
Both are open for the same reason
control.wallet.coinByIdis: the caller NAMES the subject with apublic coin id, so the answer discloses no node-to-address association.
Release-first
dig-node-control-interface0.10.0 is published; the pin atcrates/dig-node-service/Cargo.tomlmoved
"0.9"to"0.10"andCargo.lockresolves it from the registry. No git dependency remains.The pin is load-bearing rather than cosmetic: with it in place, removing either method from
is_open_control_readnow turnsthe_node_and_the_contract_agree_on_the_token_less_wallet_surfaceRED (verified, then restored). That failure was structurally unreachable at
"0.9"— theconformance suite iterates the CONTRACT, so a method the pinned version had never heard of could not
be compared at all.
How each answer is bound to the request
The tier underneath answers from one unauthenticated, DNS-discovered peer that never hashes what it
forwards, so every answer is checked locally before it is served:
SHA256(parent || puzzle_hash || amount)) and compared to the id asked forfallback.rscoin_spendWALLET_READ_FAILED— never that coin's spend, never absencepuzzle_revealtree-hashed and compared to the spent coin's ownpuzzle_hashfallback.rsverified_revealWALLET_READ_FAILED, and identically for a reveal that will not PARSE (fail closed)parent_coin_infoequals the requested parentfallback.rscoin_records_by_parentrpc.rscoin_spendWALLET_READ_FAILEDrather than a spend carrying an invented or absentspent_heightThe two spend bindings are independent and catch different lies: the id check says WHICH coin the
answer describes, the tree hash says the program really is that coin's puzzle. A substitution passing
one still fails the other — the reveal test's fixture has a genuine coin id precisely so only the
hash comparison can reject it.
No error is ever collapsed into an absence. Three values throughout: a value, a legitimate
absent, an error.
spend: nulltells a caller the coin is unspent — which is the go-ahead to spendit — so a read failure wearing that shape invites a double-spend; an empty child page reads as that
spend created nothing and ends a lineage walk. The bad precedent in this repo,
ChiaQueryLineage::parent_spendswallowing every error asOk(None), is deliberately not copied.Paging and the truncation signal
The contract paged this read, so the node does too rather than capping it:
after_coin_id+limit(1..=1000, default 100), children ASCENDING bycoin_id, sorted node-side because the tierunderneath merges peer and coinset answers and promises no order.
completederives from what REMAINS, never from whether the page filled. The naivecoins.len() < limitagrees on every input except a child count that is an exact multiple of thepage size, where it declares a truncated page whole.
complete_distinguishes_a_full_page_from_the_whole_child_setuses 4 children at a limit of 2 forexactly that reason; a 5-and-2 fixture would pass under both derivations and prove nothing.
{complete: false, cursor: null}is unrepresentable — the contract permits it by omission andit would leave a caller looping forever or silently restarting. Asserted across every page of a
real 7-child walk, not one sampled page.
limitis refused, never clamped, per the contract: the page boundary is whatthe caller resumes from. Pinned from BOTH sides — 1000 passes, 1001 and 0 fail.
Both reads consult the shared
fallback_ratebucket (#1957).Blast radius
gitnexus is not indexed for this worktree, so the radius was derived with ripgrep + direct reads —
the §2.0 sanctioned fallback, stated here because the obligation is the analysis, not the tool.
wallet_coin_id_param— the one symbol with existing callers that this PR refactors. Callers:1 production (
wallet_coin_by_id) + 3 test sites, all insidecontrol.rs. Behaviour preserved: itnow delegates to a shared
coin_id_field, and its three existing tests still pass unchanged.ChainFallback— two methods added with NO default, so all six implementors must decideexplicitly (
CoinsetFallback,ChainTransport,EmptyFallback, and three test doubles). Adefaulted method is what would let a future double silently answer a money-critical read.
§908 holds: both are pure chain reads. One hex string in; no address, no key, no seed, no
signature. A puzzle reveal is a program the chain already published.
Registration — 13 points, derived from the enforcing tests
OWNED_CONTROL_METHODS·CONTROL_METHODS·is_open_control_read(+ its written-out expectedvector) · the
dispatch_ownedarm · the handler · the param validator · the wire mapper ·ControlAction·method()·wire_params()·cli_covered_control_methods()· the CLI humansummary · clap
WalletCommand+ itswallet_action()arm ·SPEC.md(method table, open-read list,CLI verb table, error table). Contract pin + version bump on top.
One correction to the brief's checklist:
coinsByParentcould not reuse a plain single-coin-idvalidator, because the published params carry three fields, not one.
The CLI-parity gate had a real hole:
every_wallet_control_method_is_reachable_from_a_real_command_lineproves each LISTED verb parses and says nothing about one never listed —
control.wallet.coinByIdand
control.wallet.syncStatushad both been missing from it for months while every gate stayedgreen. Its table is now hoisted out and a second test asserts the table COVERS the declared wallet
surface, so a new method fails until a real command line exercises its verb.
Evidence
cargo test --workspace— green, exit 0 (811 + 347 + the integration suites; 0 failures).cargo clippy --workspace --all-targets— clean;cargo fmt --allapplied.binding · reveal tree-hash · foreign-child refusal · the naive
completederivation · theascending sort · the CLI-table completeness gate · the conformance gate at the 0.10 pin. The first
attempt at the coin-id proof did NOT apply its own edit and passed vacuously; every proof after it
chains a grep against the reverted line, so a no-op edit cannot read as a load-bearing test.
Two findings reported, not fixed
ws_dispatch(server.rs:1305)token-gates every
control.*without consultingis_open_control_read, while the HTTP pathchecks it at
server.rs:926. Both new reads are therefore open over HTTP and gated over WS.Confirmed still present.
every_open_read_that_leaves_the_node_passes_the_same_rate_boundENUMERATES rather thanderives. A new read that skipped the bucket would be absent from it, not caught by it. The two
new reads were added to it and the limitation is now stated in the test itself; deriving the list
is not possible today because these are inherent methods, not a trait a test could iterate.