Skip to content

Commit f8d765b

Browse files
docs(spec): count value by destination, and record the non-load-bearing summary
Co-Authored-By: Claude <noreply@anthropic.com>
1 parent dca98ac commit f8d765b

1 file changed

Lines changed: 39 additions & 8 deletions

File tree

SPEC.md

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -215,11 +215,19 @@ derived from the coins being spent, and this specification does not claim otherw
215215
mismatch. **The caller MUST construct the authorizer that matches the coins it is spending.**
216216
- **Vault protection is a property of the AUTHORIZER, not of the funds.** An implementation MUST NOT
217217
present these rules to a user as protection that attaches to money held in a vault.
218-
- **Only HINTED value is visible to the amount bounds** (§6.4). An un-hinted output is change and is
219-
excluded from `SpendSummary::recipients`, so no limit here weighs it; §5.2's change-ownership check
220-
bounds where such value may go (a puzzle hash under the same seed) but does not subject it to any
221-
policy. A future tier-to-coin linkage is tracked separately; until it exists, the guarantees above are
222-
the whole of what this layer provides.
218+
- **Value is counted by DESTINATION, never by hint status** (§6.4). An output is weighed unless it pays
219+
the exact puzzle hash of a coin the spend is itself spending — the one case where value demonstrably
220+
has not moved. Hint status is never consulted, so an author cannot move value out of the charged total
221+
or out of the vault destination rule by omitting a memo.
222+
- **This deliberately OVERCOUNTS change sent to a fresh derivation.** This layer holds no key, so it
223+
cannot distinguish a fresh derivation of the user's own wallet from a stranger's address. The only
224+
rule that could is "any owned derivation is change", and that is precisely an attacker's exfiltration
225+
target — an address the signer's `0..address_gap` window accepts. So a legitimate send whose change
226+
goes to a fresh address is counted in full and escalates to the human instead of auto-sending. This is
227+
intended, specified behaviour and MUST NOT be "fixed" by widening what counts as change: overcounting
228+
asks a person, undercounting signs.
229+
- A future tier-to-coin linkage is tracked separately; until it exists, the guarantees above are the
230+
whole of what this layer provides.
223231

224232
Two limitations previously recorded here — that enforcement was opt-in, and that the authorization was
225233
not bound to the signed bytes — are no longer true. §6.2 states what replaced them.
@@ -259,6 +267,15 @@ it of a host.** The enforcement is structural:
259267
| Not loggable | Neither type implements `Debug`. |
260268
| Not expiring | Deliberate. The rolling cap is charged when the approval is minted, so an aged approval cannot re-spend an allowance. A user re-locking DURING an async ceremony is a lock question, answered by building the signer after the ceremony from the live residency — not by dating the approval. |
261269

270+
**Safety comes from the approval OWNING its spends. A same-bytes comparison is NOT a guard, and this
271+
crate MUST NOT claim one as a custody property.** The approval carries a `TransactionSummary` because
272+
the dependency's signer takes it as a required parameter, and that signer compares it against its own
273+
re-derivation. Both sides descend from the same `coin_spends` the approval owns, so the comparison can
274+
only ever agree: it is structurally incapable of detecting anything, and its value here is zero. It is
275+
passed to satisfy a signature and is explicitly NON-LOAD-BEARING. A genuine second opinion would require
276+
an INDEPENDENT derivation — which is the two-answers-can-disagree shape §6.2 exists to remove, so no such
277+
comparison SHOULD be reintroduced as a substitute for ownership.
278+
262279
`SpendSummary::new` and `WalletOps::summarize` remain public and confer NO authority: since no API
263280
accepts a `&SpendSummary` for a custody decision, a hand-built summary is a display value with nowhere
264281
to go.
@@ -380,9 +397,23 @@ small allowance while the spend moves an enormous amount) and MUST NOT panic (`f
380397
`SpendSummary::native_total_mojos()` SATURATES at `u64::MAX` so no caller can observe a wrapped figure;
381398
`checked_native_total_mojos()` is the form every custody decision MUST use.
382399

383-
`SpendSummary` accounts for HINTED outputs plus the fee, so no bound here can see an un-hinted output.
384-
The complementary invariant — un-hinted value MUST NOT leave the wallet — is enforced by the money
385-
signer's change-ownership check (§5.2). Both layers are required.
400+
**The charged total MUST be computed by destination, not by hint status.** `SpendSummary` accounts for
401+
every created output — hinted or not — EXCEPT one paying the exact puzzle hash of a coin the spend is
402+
itself spending, plus the fee. Equivalently, and this is why the rule is stated this way: it charges
403+
`xch_in` minus change returning to a spent input's own puzzle hash. It never enumerates a subset of
404+
outputs chosen by the author, so it cannot be made incomplete by omitting a memo. §6.1.1 records the
405+
deliberate overcount this implies. The vault destination rule (§6.1) reads the same list, so an
406+
un-hinted vault outflow is subject to the hot-wallet-only rule exactly as a hinted one is. The money
407+
signer's change-ownership check (§5.2) remains a required second layer.
408+
409+
**The dependency's OUTPUT-amount accumulation is unchecked.** `dig-wallet-backend` 0.16 sums output
410+
amounts from CLVM conditions with an unchecked `+=` (`client/verify.rs:131`, `:165`); the checked input
411+
sum above bounds inputs only and does not bound these. `checked_native_total_mojos()` is therefore
412+
load-bearing, not defensive: it is the only place an unsummable output total becomes
413+
`PolicyIndeterminate` rather than a clamped figure judged against a limit. A test reaching it cannot yet
414+
be written — the same input panics inside the dependency at `verify.rs:165` before this guard evaluates
415+
(tracked as dig-wallet-backend #1708) — and a mock bypassing the dependency would prove nothing about
416+
the real path.
386417

387418
### 6.5 Vault to hot-wallet moves (`VaultMove`)
388419

0 commit comments

Comments
 (0)