Skip to content

Go SDK 8/8: examples, README, manual chapter, release notes - #519

Open
bkeroack wants to merge 1 commit into
feat/go-sdk-parityfrom
feat/go-sdk-examples-docs
Open

Go SDK 8/8: examples, README, manual chapter, release notes#519
bkeroack wants to merge 1 commit into
feat/go-sdk-parityfrom
feat/go-sdk-examples-docs

Conversation

@bkeroack

@bkeroack bkeroack commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

8 of 8 in the Go SDK stack. Base: feat/go-sdk-prefixfeat/go-sdk-parity → this.
Merge order: #515#516#517#518 → this one.

Thirteen runnable examples, a real README, a manual chapter, changelog and
release-note entries, cross-links from the wire spec, and two fixes to the test
infrastructure that landed along the way.

Examples are their own module

Two of them (sp_wallet, sp_light_scan) do BIP 352 receiver derivation, which
needs real secp256k1 scalar and point arithmetic. A dependency declared in the
SDK's own go.mod reaches every consumer's module graph whether they import the
package or not — so btcec lives in a nested module, the same reason tools and
e2e are separate. The published SDK graph stays gRPC + protobuf.

The alternative, hand-rolling curve math with math/big inside an example,
would have been worse than the dependency. An example teaches, and that one
would teach the wrong thing; a real Go wallet already has a curve library.

The BIP 352 derivation is shared, and tested from the other side

examples/internal/bip352 is one implementation rather than a copy in each
example, because the label arm is exactly what integrators get wrong: omit the
label tweak and you derive a key that looks fine and does not control the
output, so change becomes silently unspendable.

Its tests play the sender, reaching the same output key by a genuinely
different route — the shared secret from the other side of the ECDH (a·B_scan
rather than b_scan·T), and the output key by point addition rather than the
scalar shortcut the receiver takes. A test that re-derived the receiver's way
would agree with any bug it had.

Perturbation: 10 cases, 10 caught — after a fix to the test itself. The
first version computed the label tweak by calling the function under test, so it
could not see that function keyed on the wrong secret (9/10). It now restates
BIP 352 §5 directly.

What the tests do not establish is stated in the file: they are algebra and
self-consistency checks, not BIP 352 known-answer vectors. The authoritative
check that this matches the deployed protocol is the node's own silent-payment
index matching real payments, which the E2E suite covers.

deposit_notify

The README quickstart, verbatim and compiled. The most-copied code in the
repository had nothing checking it.

Two infrastructure fixes

examples/ and e2e/ were silently unlinted. The analyzers resolve ./...
against the main module and stop at a nested go.mod, so lint.sh was checking
one of what are now four modules — e2e since the day it landed. Both are clean
once actually linted. e2e needs its build tag passed, or every analyzer
reports a clean run over zero files.

E2E deadlines — and a correction. I had planned to widen the tight 30s/60s
deadlines wholesale. Reading them showed two kinds that want opposite treatment:

  • A positive wait (recvMatching, awaitRW, a poll loop) returns the
    instant its condition holds, so its deadline is paid only when the test was
    going to fail anyway. Generous is free. Widened 30s → 60s, plus a fired-alarm
    prune poll 10s → 45s.
  • A negative window (collect, or a context deadline proving nothing
    re-fires) is paid in full on every run, because "nothing happened" can only
    be established by waiting. Widening one slows every green run to buy
    confidence in a claim already anchored by the positive wait ahead of it. Left
    alone, with the reasoning now in harness_test.go so the next flake does not
    get "fixed" by inflating the wrong kind.

Parity inventory (plan §3), walked item by item

  • §3.1 connection/transportDial + WithBearerToken / WithTLS /
    WithTLSCAPem / WithMTLS / WithTLSServerName / WithKeepalive /
    WithoutKeepalive / WithGRPCDialOption; Category* consts;
    MaxSPLabelsPerTarget, MaxSPTargetsPerConnection. Complete.
  • §3.2 streams & watch controlSubscribe/Stream.Recv, Watch
    (*WatchHandle, *Stream), all eight watch kinds with Add/Remove, plus
    SetCategories, SetWatchOptions, SetCursor, SetWatchSet, Rescan,
    SendControl; SilentPaymentTarget.Validate/ScanPubkey; Stream.Cursor.
    Complete.
  • §3.3 typed events — 31 concrete types, not the 30 the plan lists: the
    plan's enumeration omitted PrefixMatched. The count is not what guarantees
    completeness — the protobuf-reflection test walking the NodeEvent oneof
    descriptor is. All seven enums carry Known().
  • §3.4 resilienceCursorStore/FileCursorStore/NoopCursorStore,
    Backoff, LagPolicy, ResilientSubscription, ResilientWatch with the
    full add/remove set, SetCursor, Rescan, ReloadReloadSummary,
    WatchSet, WatchSetLoader. Complete.
  • §3.5 prefix privacy & helpersScripthashOf, PrefixOf,
    PrefixWatcher, FundingHit/SpendingHit/PrefixHits with
    IsMatch/HasUnresolved, DisplayHex, DisplayHexUnreversed, ParseTxid,
    TxidFromDisplayHex. Complete.
  • §3.6 examples — all twelve Rust counterparts, plus deposit_notify.

Docs

  • clients/go/README.md — rewritten; deposit-notification quickstart first,
    byte order above the fold, surface-selection table, delivery guarantees.
  • docs/manual/src/go-sdk.md — new chapter, in SUMMARY.md. Includes the
    Rust → Go mapping table, so "idiomatic" never silently means "missing".
  • CHANGELOG.md + docs/release-notes/0.5.0-pre.md — SDK and parity-harness
    entries, plus a Highlights bullet.
  • docs/api/streaming.md — a "first-party clients" note in §1 and Go
    counterparts where the spec named only the Rust SDK.

Verification

  • ./lint.sh clean across all four modules
  • go test ./... (SDK) and go build ./... && go test ./... (examples) green
  • Go E2E suite green against a live regtest node, 268s
  • mdbook builds; the new chapter renders
  • 10/10 perturbations caught on the new BIP 352 assertions

After this merges: tag clients/go/v0.1.0.

🤖 Generated with Claude Code

https://claude.ai/code/session_01HZw7Sf8PZj3qPJ6orxDH17

The last PR of the Go SDK stack. Thirteen runnable examples, a real README, a
manual chapter, changelog and release-note entries, and cross-links from the
wire spec.

Examples are their own module. Two of them (sp_wallet, sp_light_scan) do BIP 352
receiver derivation, which needs real secp256k1 scalar and point arithmetic —
and a dependency declared in the SDK's go.mod reaches every consumer's module
graph whether they import the package or not. A nested module keeps btcec where
it belongs, the same reason `tools` and `e2e` are separate. The alternative,
hand-rolling curve math with math/big in an example, would have been worse than
the dependency: an example teaches, and that one would teach the wrong thing.

The BIP 352 derivation is shared between those two examples rather than copied,
because the label arm is exactly what integrators get wrong — omit the label
tweak and you derive a key that looks fine and does not control the output, so
change silently becomes unspendable. One reviewed implementation with tests
beats two copy-pasteable ones.

Those tests play the SENDER, deriving the same output key by a genuinely
different route: the shared secret from the other side of the ECDH (a·B_scan
rather than b_scan·T), and the output key by point addition rather than the
scalar shortcut the receiver takes. A test that re-derived the receiver's way
would agree with any bug it had. Ten perturbations, ten caught — including the
first attempt at this file, which computed the label tweak by calling the
function under test and so could not see that function keyed on the wrong
secret. The test now restates BIP 352 §5 directly.

What these tests do NOT establish is stated in the file: they are
self-consistency and algebra checks, not BIP 352 known-answer vectors. The
authoritative check that this agrees with the deployed protocol is the node's
own silent-payment index matching real payments, which the E2E suite covers.

deposit_notify is the README quickstart, verbatim and compiled. The most-copied
code in the repository is the code that must not rot, and nothing was compiling
it.

Also in this PR:

- lint.sh now runs vet/staticcheck/errcheck in every module. The analyzers
  resolve ./... against the main module and stop at a nested go.mod, so
  `examples` and `e2e` were silently unlinted — e2e since it landed. Both are
  clean; e2e needs its build tag or the analyzers report a clean run over
  nothing at all.

- E2E deadlines: positive waits widened 30s → 60s, and a fired-alarm prune poll
  10s → 45s. These return the instant their condition holds, so their deadline
  is paid only when the test was going to fail anyway — generous costs nothing
  and buys headroom on a loaded runner.

  The negative windows (collect, and the context deadline proving nothing
  re-fires) are deliberately left alone, and harness_test.go now says why:
  those are paid in full on every run, since "nothing happened" can only be
  established by waiting. Widening one slows every green run to buy confidence
  in a claim already anchored by the positive wait ahead of it. I had planned
  to widen "the tight 30s/60s deadlines" wholesale; reading them showed the two
  kinds want opposite treatment.

Parity inventory (plan §3), walked item by item:

- §3.1 connection/transport: Dial + WithBearerToken / WithTLS / WithTLSCAPem /
  WithMTLS / WithTLSServerName / WithKeepalive / WithoutKeepalive /
  WithGRPCDialOption; Category* consts; MaxSPLabelsPerTarget,
  MaxSPTargetsPerConnection. Complete.
- §3.2 streams and watch control: Subscribe/Stream.Recv, Watch →
  (*WatchHandle, *Stream), all eight watch kinds with Add/Remove, plus
  SetCategories, SetWatchOptions, SetCursor, SetWatchSet, Rescan, SendControl;
  SilentPaymentTarget.Validate/ScanPubkey; Stream.Cursor. Complete.
- §3.3 typed events: 31 concrete types, not the 30 the plan lists — the plan's
  enumeration omitted PrefixMatched. The count is not what guarantees
  completeness; the protobuf-reflection test walking the NodeEvent oneof
  descriptor is. All seven enums carry Known().
- §3.4 resilience: CursorStore/FileCursorStore/NoopCursorStore, Backoff,
  LagPolicy, ResilientSubscription, ResilientWatch with the full add/remove set,
  SetCursor, Rescan, Reload → ReloadSummary, WatchSet, WatchSetLoader. Complete.
- §3.5 prefix privacy and helpers: ScripthashOf, PrefixOf, PrefixWatcher,
  FundingHit/SpendingHit/PrefixHits with IsMatch/HasUnresolved, DisplayHex,
  DisplayHexUnreversed, ParseTxid, TxidFromDisplayHex. Complete.
- §3.6 examples: all twelve Rust counterparts, plus deposit_notify.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HZw7Sf8PZj3qPJ6orxDH17
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