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.
This commit is contained in:
@@ -20,10 +20,42 @@ rest of the system.
|
|||||||
- **Chain backend.** User-configured Esplora-compatible API
|
- **Chain backend.** User-configured Esplora-compatible API
|
||||||
(mempool.space, blockstream.info, self-hosted). The configured server sees
|
(mempool.space, blockstream.info, self-hosted). The configured server sees
|
||||||
the user's UTXO queries — accepted tradeoff for v1. Header-only SPV mode is
|
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.** Full send + receive + display loop on Android. Desktop is out of
|
||||||
scope for v1.
|
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
|
## Architecture
|
||||||
|
|
||||||
| Layer | Concerns | Location |
|
| Layer | Concerns | Location |
|
||||||
|
|||||||
@@ -97,6 +97,12 @@ class PsbtMap(
|
|||||||
* finalize the single-key-path P2TR spends NIP-BC needs. Unknown records are
|
* 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
|
* preserved verbatim so the container round-trips even when fields aren't
|
||||||
* modeled.
|
* 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(
|
class Psbt(
|
||||||
val global: PsbtMap,
|
val global: PsbtMap,
|
||||||
|
|||||||
+5
@@ -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
|
* where `bytes(P)` is the 32-byte x-only Nostr pubkey and `hashTapTweak` is
|
||||||
* the BIP-340 tagged hash with tag `"TapTweak"`.
|
* 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 {
|
object TaprootAddress {
|
||||||
private const val TAP_TWEAK_TAG = "TapTweak"
|
private const val TAP_TWEAK_TAG = "TapTweak"
|
||||||
|
|||||||
Reference in New Issue
Block a user