clients/go: the watch surface - #514
Open
bkeroack wants to merge 1 commit into
Open
Conversation
The bidirectional Watch stream and every watch kind on it: scripts (with per-script min-value floors), outpoints, transaction lifecycles, depth alarms, descriptors, privacy-preserving script prefixes, and BIP 352 scan-key targets — plus set_categories, set_watch_options, set_cursor, rescan, and the atomic set_watch_set replace. `WatchHandle` is safe for concurrent use (a gRPC stream permits one Send at a time, so sends are serialized behind a context-aware lock) and maps a torn-down stream to the control-closed error class, which is the signal the resilience layer keys its re-registration off. Two details the wire makes easy to get wrong, and that the tests pin: - Lifecycle watches and depth alarms are ONE message dispatched on `min_depths`. An empty vector selects the lifecycle primitive, so an all-invalid depth-alarm call must send NOTHING rather than an empty `min_depths` — which would silently register a different, quota-charging watch than the caller asked for. - `min_values` is either empty or exactly parallel to `scripthashes`, with 0 standing in for unfloored entries. `WatchSet` is the declarative snapshot: full desired membership per kind, no deltas, rendered in deterministic (sorted) order rather than Go's randomized map order — the differential parity harness diffs this against the Rust mirror, which renders from ordered maps. Silent payments carry the SDK's only crypto. Rather than depend on btcec — which would put two third-party crypto modules in every consumer's graph and force MVS bumps on btcd/lnd-ecosystem applications — the scan-key identity b_scan*G and the compressed-point check are in-tree math/big: a double-and-add-always ladder and Euler's criterion, pinned against the published 1G..5G vectors. The scope note is explicit in the source: math/big is not constant-time, and this is acceptable only because b_scan is a watch credential the client discloses to the node by design. The scan secret is redacted by the type's String method (so a stray %v cannot log it) and scrubbed by Zero. Testing: - Unit: an in-process gRPC server records what each helper puts on the wire, because the control path has no per-message ack — being the server is the only way to assert the message shape. Covers every kind, the no-op contract, prefix width validation, batch-level rejection, and cancellation. - Crypto: known-vector and curve-equation tests for the scalar multiplication, rejection of non-points (including the x=1 near-miss, whose x^3+7 IS a residue), the label cap, and secret redaction. - E2E: every watch kind against a live node, including descriptor attribution (branch and derivation index), the min-value floor actually suppressing, raw_tx byte-matching getrawtransaction, the atomic replace's in-band accept/reject with its counts, in-band cursor acks with replay, and a bounded rescan clamped to the tip and terminated. Scan-key registration is verified through SetWatchSet — the one control message with a deterministic ack — since constructing a real BIP 352 payment is out of scope for a suite that deliberately carries no Bitcoin library. Every new assertion was perturbed once and observed to fail before landing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GJqZbJtTUo7K7G9wJcrvHv
This was referenced Aug 5, 2026
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.
PR 3 of the Go SDK stack. Stacked on #513 (which is stacked on #512) — merge bottom-up.
What this lands
The bidirectional
Watchstream and every watch kind on it: scripts (with per-script min-value floors), outpoints, transaction lifecycles, depth alarms, descriptors, privacy-preserving script prefixes, and BIP 352 scan-key targets — plusSetCategories,SetWatchOptions,SetCursor,Rescan, and the atomicSetWatchSetreplace.WatchHandleis safe for concurrent use (a gRPC stream permits oneSendat a time, so sends serialize behind a context-aware lock) and maps a torn-down stream to the control-closed error class — the signal the resilience layer keys re-registration off.Two details the wire makes easy to get wrong
Both pinned by tests:
min_depths. An empty vector selects the lifecycle primitive, so an all-invalid depth-alarm call must send nothing rather than an emptymin_depths— which would silently register a different, quota-charging watch than the caller asked for.min_valuesis either empty or exactly parallel toscripthashes, with 0 standing in for unfloored entries.WatchSetis the declarative snapshot — full desired membership per kind, no deltas — rendered in deterministic sorted order rather than Go's randomized map order, because the differential parity harness (PR 7) diffs it against the Rust mirror, which renders from ordered maps.Silent payments: no btcec
This is the SDK's only crypto. Depending on
btcecwould put two third-party crypto modules in every consumer's graph (btcec/v2wraps Decred'sdcrec/secp256k1/v4) and force MVS bumps on btcd/lnd-ecosystem applications — to validate a public key and multiply the base point once per target. Instead, in-treemath/big: a double-and-add-always ladder forb_scan*G, and Euler's criterion for the compressed-point check (no decompression, no square root).The scope note is explicit in the source: math/big is not constant-time, and that is acceptable only because
b_scanis a watch credential the client discloses to the node by design — it confers no spend authority. Do not reuse the code for a spending key. The scan secret is redacted by the type'sStringmethod (a stray%vcannot log it) and scrubbed byZero.Testing
raw_txbyte-matchinggetrawtransaction, the atomic replace's in-band accept/reject with its counts, in-band cursor acks followed by replay, and a bounded rescan clamped to the tip and terminated.Scan-key registration is verified through
SetWatchSet— the one control message with a deterministic ack — since constructing a real BIP 352 payment is out of scope for a suite that deliberately carries no Bitcoin library.Negative verification: every new assertion was perturbed once and observed to fail before landing (floor removed, opt-in skipped, lifecycle substituted for a depth alarm, wrong descriptor key).
🤖 Generated with Claude Code
https://claude.ai/code/session_01GJqZbJtTUo7K7G9wJcrvHv