Repository navigation
Conversation
R17 / ADR 0004: the lock key of a name sits at m/44'/<coin>'/<account>'/2'/<i>' with every level hardened, where i is the first four bytes of the name hash masked to 31 bits, so no account xpub can compute it. lock_key_index and lock_key_path return a Result: they hash through names::hash_name, which refuses a name that can never be on chain, and lock_key_path refuses an account at or above 2^31 instead of clamping it. The hsd vector gains a second name, namehold: the 0x7fffffff mask is invisible on dexreviews (its hash has the top bit clear), so only a name with the top bit set can pin it. The generator also emits nameHashPrefix, index and lockPub. R17's Pinned line gains lock_key_of_an_invalid_name_is_refused. Gates: shakedex_lock_key_tests, hsd_parity, shakedex_vector_tests, full cargo test, clippy, prettier.
Pins an existing rule (R17: coins someone else sends to a lock address are not counted as spendable) through the two mechanisms that hold it today. Coin selection INNER JOINs tracked_utxos to derived_addresses, so a coin at an address that is not a derived address is never offered, even if it were recorded as liquid HNS. Node sync only asks the node for derived addresses, and derived_addresses.branch has CHECK (branch IN (0, 1)), so a lock address can never become one. No production change. Mutation check: turning the JOIN in load_spendable_coins into a LEFT JOIN with COALESCE(d.branch, 0), COALESCE(d.child_index, 0) makes the test fail on the coin count (2 instead of 1). The sync assertion documents the other mechanism and has no one-line mutation, so it is not mutation-checked.
Adds `Transaction::sign_p2wsh_input`, which signs an input as a spend of a witness script and returns the 65-byte witness signature. It leaves the witness alone: the stack differs per spend (a price step's purchase carries `[signature, script]`, a FINALIZE out of a lock carries `[script]`), so the caller sets it. The signing core is now a private `sign_sighash` shared with `sign_p2wpkh_input`; every hsd-parity signed hex stays identical. The new `p2wshSpend` vector is generated by hsd 8.0.0 (fixed key 0x11..., script `<pub> OP_CHECKSIG`, SIGHASH_ALL, verified by hsd's interpreter). `sign_p2wsh_input_matches_hsd` pins sighash, signature, signed hex and txid. Regenerate with `cd src-tauri/tests/vectors && node gen_hsd_vectors.js`; only the new key is added to `vectors.json`. Mutations that fail the test: using the P2WPKH script code instead of the script, and zeroing the sighash type byte (the latter also fails four hsd-parity vectors through the shared core). Gates: cargo fmt, clippy -D warnings, full cargo test, prettier.
R21: `reserve_receive_address` allocates the next unused receive address of a profile and marks it used at once, so the next allocation returns another one. Listing payment and cancel destinations will be reserved this way, because nothing reaches them until a buyer pays or a cancel is mined. The shared `ADDRESS_USED_PREDICATE` now reads `derived_addresses.used`. Without it, marking an address used had no effect on allocation or on the receive-address list, which only looked at tracked UTXOs, bid commitments and Shakedex purchase destinations. `used = 1` is set by node sync only for addresses that also get a `tracked_utxos` row, so existing wallets are unaffected. The reservation runs under a SAVEPOINT instead of its own BEGIN: it is atomic on its own and still works inside a caller's transaction. It takes no `account` parameter: the account, network and xpub come from the profile row, so a caller cannot persist addresses under an account that disagrees with the stored xpub. Gates: cargo fmt, clippy -D warnings, full cargo test --locked.
A restore provisions a fixed window of address_gap_limit addresses and does not extend it, so a reserved address is found after a restore only while its index is inside that window. The doc of reserve_receive_address and spec R21 claimed the gap-limit scan always reaches it; the matching test assertion held whatever the reservation did and is removed. Window extension is recorded as a known gap in the spec. The SAVEPOINT cleanup now runs ROLLBACK TO and RELEASE as separate statements so RELEASE always runs, and logs a rollback error instead of dropping it. The mark_address_used doc notes its use for reservations.
compute_balances summed every tracked UTXO, so a coin at a Shakedex lock address showed as spendable although coin selection refuses it. It now joins derived_addresses as load_spendable_coins does. Also: lock_key_path takes a Network instead of a raw coin type, the golden-vector test is renamed to state its behaviour, the address-used docs list everything ADDRESS_USED_PREDICATE checks, and the two copies of the in-memory profile fixture are one shared helper.
DimazzzZ
added this pull request to stack #85
October 8, 2026 20:52
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.
First PR of the Shakedex selling stack (spec
docs/specs/2026-10-05-shakedex-name-sales.md, ticket T1a): lock keys from the seed, a P2WSH signing primitive, and reserved receive addresses. Nothing here is user-visible yet. Base:main.What changes
noncustodial/shakedex/lock_key.rs:lock_key_index(SHA3-256 of the name, first four bytes big-endian, masked to 31 bits),lock_key_path(network, account, name)=m/44'/<coin>'/<account>'/2'/<i>', every level hardened, andderive_lock_keyreturning the key, its lock script, program and address. Pinned by R17's golden table and by hsd-generatedlockPathvectors, including a name whose hash has the top bit set so the mask is exercised.derived_addresses;compute_balancesnow does too, so a coin someone sends to a lock address is neither spendable nor counted in the balance. Production only ever records coins at the profile's own addresses, so existing balances do not change; 19 test fixtures that put coins at addresses the profile did not own now own them.Transaction::sign_p2wsh_inputsigns with the whole witness script as the script code (matches hsd for scripts without OP_CODESEPARATOR, as the lock script is) and returns the signature; P2WPKH signing shares the samesign_sighashhelper, byte-for-byte unchanged. Pinned against a P2WSH spend signed and verified by hsd 8.0.0.derivation::reserve_receive_addressallocates the next receive address and marks it used under a SAVEPOINT (nests inside a caller's transaction);ADDRESS_USED_PREDICATEnow honoursderived_addresses.used, so a reserved address is never reissued.address_gap_limitaddresses (the window is never extended — a pre-existing gap, tracked separately).Checks
cargo fmt --check,cargo clippy --locked --all-targets -D warnings,cargo test --locked2969 passed / 0 failed,pnpm test, prettier on tracked files — all pass.live_suite withHNS_IT_SHAKEDEX=1and the second node without--index-tx— 99 passed, 0 failed, 0 skipped (chain 11065 → 22130). No live test is new in this PR; the run checks nothing regressed.node gen_hsd_vectors.jsregeneratesvectors.jsonidentically.git diffclean): hardened bit dropped on the lock branch →lock_key_matches_the_r17_golden_vector,every_level_is_hardened,lock_key_is_not_derivable_from_the_account_xpubfail; 31-bit mask dropped → golden vector fails; account ≥ 2^31 refusal removed →every_level_is_hardenedfails; raw SHA3 instead ofnames::hash_name→lock_key_of_an_invalid_name_is_refusedfails;JOIN→LEFT JOINinload_spendable_coins→coins_at_a_lock_address_are_not_spendablefails; JOIN removed fromcompute_balances→ same test fails on the balance; P2WPKH script code insign_p2wsh_input→sign_p2wsh_input_matches_hsdfails; sighash byte zeroed insign_sighash→ P2WSH and four P2WPKH parity vectors fail;d.used = 1or themark_address_usedcall removed →reserved_addresses_are_not_reissuedfails; SAVEPOINT replaced by a transaction → the nested case fails.Not checked: the rollback path of
reserve_receive_addresshas no test.