# P2SPKH: Pay to Schnorr Public Key Hash ## Abstract P2SPKH is a native SegWit output type using witness version 3. It encodes HASH256 of a BIP-340 x-only public key as the witness program. Spending reveals the public key and provides a Schnorr signature under BIP-341 sighash rules. It extends the design space by separating key commitment (hashed) from key spend (revealed at spend time), using modern Schnorr signatures without the complexity of MAST or tapscript. ## Motivation Taproot (BIP-341) bundles three concerns into a single output type: key-path spends, script-path spends via MAST, and the upgrade mechanism itself. For simple monetary transfers, the MAST machinery is dead weight. P2SPKH provides a minimal Schnorr-based single-key output -- the Schnorr equivalent of P2WPKH - at a separate witness version. It uses HASH256 (double SHA256) for familiarity with Limenka's existing hash convention, and BIP-341 sighash for compatibility with existing signing infrastructure. Separating this into its own witness version (3) keeps the concerns orthogonal: v0 for ECDSA, v1 for full taproot, v3 for bare Schnorr key hashes. V2 remains available for other uses. ## Specification ### ScriptPubKey A 34-byte witness program: ``` OP_3 <32-byte HASH256(x-only-pubkey)> ``` Where `HASH256(x) = SHA256(SHA256(x))`. ### Witness Exactly 2 stack items: ``` <64-byte BIP-340 Schnorr signature> <32-byte x-only public key> ``` ### Validation 1. Witness stack must have exactly 2 items. 2. SIGLEN must be 64 bytes (BIP-340 standard length, no high-S). 3. PUBKEY must be a valid x-only key (32 bytes). 4. HASH256(PUBKEY) must equal the witness program. 5. `VerifySchnorrSignature(SIG, PUBKEY, sighash)` must pass, where `sighash` is computed per BIP-341 (tagged hash over prevouts, sequences, amounts, etc., using SigVersion::TAPROOT). 6. No annex, no control block, no script path. ### Rationale for design choices - **Witness version 3**: v0 is ECDSA (P2WPKH/P2WSH), v1 is full taproot with MAST, v2 is unallocated and may serve a different purpose. v3 explicitly signals "bare Schnorr key hash" with no MAST upgrade path -- it is intentionally terminal. - **HASH256 over single SHA256**: Matches Limenka's pervasive double-hash convention (transactions, blocks, merkle trees), avoiding the surprise of a single-SHA256 witness program. Consistency over micro-optimization. - **BIP-341 sighash**: Already implemented, tested, and deployed. No need to invent a new sighash algorithm for what is functionally the same operation (Schnorr signature over a transaction commitment). - **No script path, no MAST, no annex**: This is a monetary transaction output. Adding unused spending paths increases on-chain footprint and verification cost for no benefit to the simple transfer use case. The upgrade path is to use a different witness version. - **Bech32m encoding**: Witness version >= 1 uses Bech32m per BIP-350. The version byte disambiguates v3-SPKH from v1-TR. Same address format, different consensus rules -- this is already the design of segwit versioning. ### Address encoding Bech32m with witness version 3. Mainnet addresses begin with `bc1p`. ### Descriptor ``` spk() ``` `spk()` accepts any Miniscript-compatible key expression that resolves to an x-only public key, e.g. `spk(tpub...)`, `spk(02feed...)`, or `spk(xpub.../87h/0h/0h/0/0)`. ## Consensus activation P2SPKH activates via UASF (User Activated Soft Fork) using versionbits deployment `DEPLOYMENT_P2SPKH` (bit 2). ### Deployment parameters | Chain | nStartTime | nTimeout | Threshold | Period | |-------|-----------|----------|-----------|--------| | mainnet | ALWAYS_ACTIVE | NO_TIMEOUT | 1815 (90%) | 2016 | | testnet4 | ALWAYS_ACTIVE | NO_TIMEOUT | 1512 (75%) | 2016 | | testnet3 | ALWAYS_ACTIVE | NO_TIMEOUT | 1512 (75%) | 2016 | | signet | ALWAYS_ACTIVE | NO_TIMEOUT | 1815 (90%) | 2016 | | regtest | 0 (genesis) | NO_TIMEOUT | 108 (75%) | 144 | `ALWAYS_ACTIVE` (-1) means the deployment is active from genesis on upgraded nodes. This is a UASF design: when a node runs this software, P2SPKH rules are enforced on all blocks. Historical blocks are unaffected because they contain no P2SPKH transactions. ### Upgrade flow 1. Node operators upgrade to a version supporting P2SPKH. 2. `SCRIPT_VERIFY_P2SPKH` becomes active at `GetBlockScriptFlags()` via `DeploymentActiveAt(..., DEPLOYMENT_P2SPKH)`. 3. P2SPKH spends without the flag return success (forward-compat, standard segwit versioning). 4. With the flag, all 6 validation rules are enforced. `SCRIPT_VERIFY_P2SPKH` is also included in `MANDATORY_SCRIPT_VERIFY_FLAGS` (superset of `STANDARD_SCRIPT_VERIFY_FLAGS`) so the mempool accepts P2SPKH transactions and wallet operations correctly validate P2SPKH signatures. ### Forward compatibility Without `SCRIPT_VERIFY_P2SPKH`, witness version 3 + 32-byte programs return success (like any undefined witness version). This preserves the standard segwit soft-fork upgrade path: old nodes accept blocks containing P2SPKH spends, new nodes enforce the rules. No chain split occurs. ## Implementation A 24-file change across the codebase. ### Script type plumbing | # | File | Change | |---|------|--------| | 1 | `src/script/solver.h` | Add `WITNESS_V3_SPKHASH` to `TxoutType` enum | | 2 | `src/script/solver.cpp` | Add string name in `GetTxnOutputType()`; detection in `Solver()` for v3 + 32-byte program | | 3 | `src/script/interpreter.h` | `WITNESS_V3_SPKHASH_SIZE = 32` constant; `SCRIPT_VERIFY_P2SPKH` flag | | 4 | `src/addresstype.h` | `WitnessV3SpkHash` struct wrapping `uint256` (with `XOnlyPubKey` constructor computing HASH256); add to `CTxDestination` variant | | 5 | `src/addresstype.cpp` | `ExtractDestination()`, `CScriptVisitor` (`OP_3 `), `ValidDestinationVisitor` | | 6 | `src/key_io.cpp` | `DestinationEncoder` for v3 (Bech32m); `DecodeDestination` v3+32 bytes -> `WitnessV3SpkHash` | | 7 | `src/outputtype.h` | Add `OutputType::P2SPKH` to enum and `OUTPUT_TYPES` array | | 8 | `src/outputtype.cpp` | `OutputTypeFromDestination`: `WitnessV3SpkHash` -> `P2SPKH`; `ParseOutputType("p2spkh")` | ### Consensus / validation | # | File | Change | |---|------|--------| | 9 | `src/script/interpreter.cpp` | `VerifyWitnessProgram()`: v3+32-byte branch -- HASH256 pubkey check + `CheckSchnorrSignature`; `ScriptFlagNamesToEnum()`: add `P2SPKH` | | 10 | `src/consensus/params.h` | Add `DEPLOYMENT_P2SPKH` to `DeploymentPos` enum | | 11 | `src/deploymentinfo.cpp` | Add `"p2spkh"` deployment info | | 12 | `src/kernel/chainparams.cpp` | Deployment params for mainnet, testnet4, testnet3, signet, regtest | | 13 | `src/validation.cpp` | `GetBlockScriptFlags()`: add `SCRIPT_VERIFY_P2SPKH` gated by `DEPLOYMENT_P2SPKH` | ### Signing | # | File | Change | |---|------|--------| | 14 | `src/script/sign.cpp` | `SignStep()` case; `ProduceSignature()` build witness stack [sig, pubkey] | ### Signing provider | # | File | Change | |---|------|--------| | 15 | `src/script/signingprovider.h` | Add `GetSpkPubKey()` virtual method + `spk_keys` map | | 16 | `src/script/signingprovider.cpp` | Implement `FlatSigningProvider::GetSpkPubKey()` | ### Policy | # | File | Change | |---|------|--------| | 17 | `src/policy/policy.cpp` | `IsWitnessStandard()`: v3 rules (exactly 2 items, sig = 64 bytes, pubkey = 32 bytes) | | 18 | `src/policy/policy.h` | Add `SCRIPT_VERIFY_P2SPKH` to `MANDATORY_SCRIPT_VERIFY_FLAGS` | ### Wallet | # | File | Change | |---|------|--------| | 19 | `src/wallet/spend.cpp` | `GetOutputType()`: `WITNESS_V3_SPKHASH` -> `P2SPKH` | | 20 | `src/wallet/wallet.cpp` | `TransactionChangeType()`: detect `WitnessV3SpkHash` recipients, return `P2SPKH` change | | 21 | `src/wallet/scriptpubkeyman.cpp` | Add `WITNESS_V3_SPKHASH` to legacy wallet handling | | 22 | `src/wallet/rpc/addresses.cpp` | `DescribeAddressVisitor` for `WitnessV3SpkHash` | | 23 | `src/wallet/walletutil.cpp` | `GenerateWalletDescriptor`: `P2SPKH` -> `spk(...)` with `87h` derivation | ### Descriptor | # | File | Change | |---|------|--------| | 24 | `src/script/descriptor.cpp` | `SPKDescriptor` class: output type `P2SPKH`, script size 34, max satisfaction weight 98; `ParseScriptContext::P2SPK`; `spk()` function parser; `InferScript()` recognition | ### RPC | # | File | Change | |---|------|--------| | 25 | `src/rpc/rawtransaction.cpp` | `decodescript` RPC: `WITNESS_V3_SPKHASH` in switch cases | | 26 | `src/rpc/util.cpp` | `DescribeAddressVisitor` for `WitnessV3SpkHash` (JSON output) | ### Tests | # | File | Change | |---|------|--------| | 27 | `src/test/p2spkh_tests.cpp` | 13 unit tests: Solver detection, address roundtrip, HASH256, signing roundtrip, wrong hash, wrong stack size, wrong pubkey, invalid sig, descriptor parsing, forward-compat without flag | | 28 | `src/test/transaction_tests.cpp` | Updated `CTxDestination` variant size assert (9 -> 10); skip v3+32 in spends_witness_prog loop | | 29 | `src/test/fuzz/util.cpp` | Add `WitnessV3SpkHash` to `ConsumeTxDestination` | | 30 | `src/wallet/test/wallet_tests.cpp` | `BasicOutputTypesTest` now includes `P2SPKH` in OUTPUT_TYPES | ### Derivation path ``` m/87h/0h/0h/0/* ``` Using purpose `87h` following the BIP43/BIP44 convention. Extended key expressions in `spk()` descriptors derive x-only public keys from this path. ## Unimplemented - `test/functional/feature_p2spkh.py` -- end-to-end functional test: create `spk()` descriptor wallet, generate address, send, spend. - Default wallet address type change to `P2SPKH`. - PSBT (BIP-174) integration -- P2SPKH is a new input/output type. - Miniscript and policy language support. - Taproot->P2SPKH migration tooling. ## Open questions 1. Minimum dust threshold (same as P2TR at 34-byte scriptPubKey?). 2. Interaction with PSBT (BIP-174). 3. Miniscript and policy language support.