From d77f2f733308a6f5e2bbacca0ac44fa5d1f5500c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 14 May 2026 18:16:30 +0000 Subject: [PATCH] docs(onchain-zaps): record the hand-rolled Bitcoin consensus code decision Decision: keep the hand-rolled psbt/ + taproot/ consensus layer rather than adopting fr.acinq.bitcoin-kmp. Rationale: a deliberately small single-key-path P2TR subset, pinned to authoritative BIP-341/350 test vectors at every layer (sighash, tweak, witness signature bytes, addresses, tx serialization), and consistent with the project's minimal-dependency stance. Recorded in amethyst/plans/2026-05-14-onchain-zaps.md with the consequence spelled out (we own correctness; revisit if scope expands past single-key-path P2TR). The psbt/ and taproot/ packages now carry a pointer back to that decision so a future reader doesn't reflexively swap in a library. Doc-comment + plan-doc only; no logic change. --- amethyst/plans/2026-05-14-onchain-zaps.md | 34 ++++++++++++++++++- .../quartz/nipBCOnchainZaps/psbt/Psbt.kt | 6 ++++ .../taproot/TaprootAddress.kt | 5 +++ 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/amethyst/plans/2026-05-14-onchain-zaps.md b/amethyst/plans/2026-05-14-onchain-zaps.md index 0507bda2a..f61494e01 100644 --- a/amethyst/plans/2026-05-14-onchain-zaps.md +++ b/amethyst/plans/2026-05-14-onchain-zaps.md @@ -20,10 +20,42 @@ rest of the system. - **Chain backend.** User-configured Esplora-compatible API (mempool.space, blockstream.info, self-hosted). The configured server sees the user's UTXO queries — accepted tradeoff for v1. Header-only SPV mode is - a future-phase add. + a future-phase add. The explorer endpoint is shared with OpenTimestamps via + `BitcoinExplorerEndpoint`: same user-configured server, same Tor-aware + default selection. - **Scope.** Full send + receive + display loop on Android. Desktop is out of scope for v1. +### Architecture decision: hand-rolled Bitcoin consensus code (2026-05-14) + +**Decision:** keep the hand-rolled Bitcoin consensus layer in +`quartz/.../nipBCOnchainZaps/{psbt,taproot}/` — the transaction codec, +serialization/txid, BIP-341 sighash, BIP-174 PSBT codec/signer/finalizer, and +BIP-341/350 address derivation. Do **not** pull in `fr.acinq.bitcoin-kmp`. + +**Considered alternative:** replace the `psbt/` + transaction + sighash layer +with `fr.acinq.bitcoin-kmp` (mature, same vendor as the `secp256k1` binding +already in the build). + +**Rationale for keeping it hand-rolled:** +- It is a deliberately small, constrained subset — single-key-path P2TR only, + no script trees, one transaction shape. +- It is pinned to authoritative external test vectors at *every* layer: + BIP-341 sighash (all 7 vectors + ANYONECANPAY), the BIP-341 tweak, the full + BIP-341 witness *signature bytes*, the 7 BIP-341/350 P2TR mainnet addresses, + and tx serialization against the genesis coinbase. It is "matches the + authoritative vectors," not "trust our code." +- Consistent with the project's stance on minimal dependencies (cf. the + from-scratch `quic` module). +- No new transitive dependencies or version-conflict surface. + +**Consequence / what this commits us to:** we own the correctness of this code +forever. If the scope ever expands beyond single-key-path P2TR (script-path +spends, multisig, PSBT fields we don't model), revisit this decision — at that +point a vetted library is the better trade. The `nipBCOnchainZaps/{psbt,taproot}/` +packages carry a pointer back to this section. + + ## Architecture | Layer | Concerns | Location | diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBCOnchainZaps/psbt/Psbt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBCOnchainZaps/psbt/Psbt.kt index 5875b2c5d..9ac7c76ce 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBCOnchainZaps/psbt/Psbt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBCOnchainZaps/psbt/Psbt.kt @@ -97,6 +97,12 @@ class PsbtMap( * finalize the single-key-path P2TR spends NIP-BC needs. Unknown records are * preserved verbatim so the container round-trips even when fields aren't * modeled. + * + * This `psbt/` package is intentionally hand-rolled rather than delegated to a + * Bitcoin library. That is a recorded architecture decision — see + * `amethyst/plans/2026-05-14-onchain-zaps.md` ("Architecture decision: + * hand-rolled Bitcoin consensus code"). It holds only while the scope stays at + * single-key-path P2TR; expanding past that should revisit the decision. */ class Psbt( val global: PsbtMap, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBCOnchainZaps/taproot/TaprootAddress.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBCOnchainZaps/taproot/TaprootAddress.kt index ac5f04618..638f2bdbc 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBCOnchainZaps/taproot/TaprootAddress.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipBCOnchainZaps/taproot/TaprootAddress.kt @@ -37,6 +37,11 @@ import com.vitorpamplona.quartz.utils.sha256.sha256 * * where `bytes(P)` is the 32-byte x-only Nostr pubkey and `hashTapTweak` is * the BIP-340 tagged hash with tag `"TapTweak"`. + * + * This `taproot/` package is intentionally hand-rolled rather than delegated to + * a Bitcoin library — a recorded architecture decision, see + * `amethyst/plans/2026-05-14-onchain-zaps.md` ("Architecture decision: + * hand-rolled Bitcoin consensus code"). */ object TaprootAddress { private const val TAP_TWEAK_TAG = "TapTweak"