Skip to content

feat(wallet): declare control.wallet.operatorAddress, the node's own MACHINE wallet - #42

Merged
MichaelTaylor3d merged 1 commit into
mainfrom
loop/operator-address
Sep 1, 2026
Merged

feat(wallet): declare control.wallet.operatorAddress, the node's own MACHINE wallet#42
MichaelTaylor3d merged 1 commit into
mainfrom
loop/operator-address

Conversation

@MichaelTaylor3d

Copy link
Copy Markdown
Contributor

Why

A node has an operator wallet of its own — machine-custody, autoseed-derived — and it is the wallet that pays mirror-coin collateral. Nothing on any surface named it, and the cost of that was measured today: a node reported three mirror bonds unfunded, short 1010 while the operator's own wallet held 1,015,000 base units of $DIG. Both statements were true, each was about a different wallet, and no surface let a person tell which. Someone wanting to fund the machine wallet had no way to obtain its address.

dig-app#339 splits the Wallet tab into User wallet and Machine wallet; it cannot render an address it has no way to fetch. This is that fetch.

What

control.wallet.operatorAddress — no params, returns {state:"known", address, puzzle_hash} or {state:"unavailable", reason:"not_initialized"|"unreadable"}.

Four properties, each stated normatively in SPEC.md and on the trait, and each the reason a wrong version would be worse than no method:

  1. A DESTINATION and nothing that can spend from it. Address plus puzzle hash, both public in exactly the sense a coin id is: they say where money can be sent, never how it can be spent. No seed, mnemonic, private or extended key, or derivation material, in any encoding. §908 — the node signs its own mirror spends and no key leaves it; a method exporting one moves the node from machine custody to none.
  2. TOKEN-GATED, by the same rule the existing control.wallet.arrivals gating is written on: the caller does not name the address, so the node volunteers its own node-to-address association. That mapping is not public and no open read discloses it.
  3. OWNED, never delegated. The question is which wallet this node spends from. Forwarding it upstream returns another machine's address under field names that still say "operator" — not a degraded answer but a confident wrong one, and someone funding "the machine wallet" from it sends money to a machine they do not run. That is the exact confusion the method exists to end, reintroduced by the routing table.
  4. unavailable { reason }, never a blank or placeholder address, and no mutation on read — it must not create, initialise, unseal-and-cache or rotate a wallet as a side effect. An address a client renders is an address somebody may send money to, so a fabricated one is a money statement of the worst kind. not_initialized is not a fault; unreadable is, and such a node cannot pay collateral either.

Version — 0.29.0, not 0.28.0

The instruction asked for this in the same 0.28.0 as the unadvertised variant. 0.28.0 had already merged and published to crates.io while that message was in flight (#41, 4e346a3), and a published version is immutable. So this is 0.29.0 — still one additive MINOR, still the same single-writer lane, and dig-node adopts 0.29.0 in one dep bump rather than two.

Minor rather than major follows this crate's own precedent: mirror_bond_states was added as a required trait method in the 0.25.0 → 0.26.0 minor (b3804b5). dig-node is the sole implementor and adopts in the same family.

Blast radius

gitnexus was NOT used, and I state that rather than implying a check I did not run. Its registered index for this repo is stale by construction and a stale index answers a false-safe impactedCount: 0. This is git grep plus direct read.

ControlMethod has several total matches, so the new variant surfaced as a compile error at each: name, category, summary (method.rs), and the dispatcher (traits.rs). Nothing absorbed it silently. Two classification functions were deliberately not edited — is_open_read and the routing Delegated list — which is what makes the method gated and owned; both are asserted rather than left implicit.

The trait gained a required method, so every implementor must add it. In-repo that is the KAT handler; ecosystem-wide it is dig-node, adopting next in this same family.

Verification

cargo test --lib190 passed, 0 failed, 0 filtered out. Count checked, not exit status: cargo surfaced through this harness as [exited with code 0] while failing, three times during this work.

Two EXISTING guards fired before I touched the docs, which is the best evidence they are load-bearing:

failures:
    kats::the_spec_and_readme_name_every_catalogued_method
    method::tests::the_gated_wallet_methods_are_the_push_the_cursor_and_enrolment
test result: FAILED. 188 passed; 2 failed

The second is a hand-written enumeration precisely so that a method joining the gated side is a deliberate edit. It was.

Both new KATs were proven load-bearing by mutation, committed first so the revert could not eat uncommitted work:

mutation result
add WalletOperatorAddress to the routing Delegated list the_operator_address_is_token_gated_and_answered_by_this_node FAILED
grow the Known arm a derivation_index field the_operator_address_result_carries_a_destination_and_no_spending_material FAILED

The second mutation is the one that matters. The result is asserted as exact JSON bytes, not round-tripped, because round-tripping is satisfied by any struct that agrees with itself — including one that has grown a seed, a mnemonic or a derivation field. The first attempt at that mutation failed to compile rather than failing the test, which proves nothing, so it was completed and re-run until the assertion itself was the thing that fired.

The routing KAT also carries honest controlsWalletPeak asserted open and delegated beside it — so neither assertion can be satisfied by every method happening to have those properties, and a blanket change to the wallet category cannot make it vacuous.

cargo clippy --all-targets -- -D warnings clean; cargo fmt --check clean.

Coherence

SPEC.md gains the method-table row and a normative section stating all four properties; README.md gains its catalogue row. Both in this PR (§4.2), and both enforced by the catalogue KAT rather than by memory.

Next

dig-node bumps to 0.29.0 and serves it from operator_wallet.rs, in the same family as the unadvertised adoption already on dig-node#484.

…MACHINE wallet

A node has an operator wallet of its own -- machine-custody, autoseed-derived
-- and it is the wallet that pays mirror-coin collateral. Nothing named it, and
the cost of that was measured: a node reported three mirror bonds `unfunded,
short 1010` while the operator's own wallet held 1,015,000 base units of $DIG.
Both statements were true and each was about a different wallet, and no surface
let a person tell which. Somebody wanting to FUND the machine wallet had no way
to obtain its address.

Four properties, each of them the reason a wrong version would be worse than no
method at all:

- It returns a DESTINATION and nothing that can spend from it. An address and
  its puzzle hash, both public in the sense a coin id is. No seed, mnemonic,
  private or extended key, or derivation material, in any encoding (908). The
  node signs its own mirror spends and no key leaves it; a method exporting one
  would move the node from machine custody to none.
- TOKEN-GATED, by the same rule as `control.wallet.arrivals`: the caller does
  not name the address, so the node volunteers its own node-to-address
  association, which no open read discloses.
- OWNED, never delegated. Forwarding it upstream answers with another machine's
  address under field names that still say "operator" -- a confident wrong
  answer, and exactly the confusion the method exists to end.
- `unavailable { reason }` rather than a blank or placeholder address, and no
  mutation on read.

SPEC.md and README.md carry it in the same change; the existing catalogue
guards proved load-bearing, failing on both until they did.

Co-Authored-By: Claude <noreply@anthropic.com>
@MichaelTaylor3d
MichaelTaylor3d merged commit 941eb4d into main Sep 1, 2026
8 checks passed
@MichaelTaylor3d
MichaelTaylor3d deleted the loop/operator-address branch September 1, 2026 18:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant