P2SPKH.md raw

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).

  1. No annex, no control block, no script path.

Rationale for design choices

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.

convention (transactions, blocks, merkle trees), avoiding the surprise of a single-SHA256 witness program. Consistency over micro-optimization.

invent a new sighash algorithm for what is functionally the same operation (Schnorr signature over a transaction commitment).

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.

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(<x-only-pubkey-expression>)

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

ChainnStartTimenTimeoutThresholdPeriod
mainnetALWAYS_ACTIVENO_TIMEOUT1815 (90%)2016
testnet4ALWAYS_ACTIVENO_TIMEOUT1512 (75%)2016
testnet3ALWAYS_ACTIVENO_TIMEOUT1512 (75%)2016
signetALWAYS_ACTIVENO_TIMEOUT1815 (90%)2016
regtest0 (genesis)NO_TIMEOUT108 (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).

  1. P2SPKH spends without the flag return success (forward-compat, standard

segwit versioning).

  1. 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

#FileChange
1src/script/solver.hAdd WITNESS_V3_SPKHASH to TxoutType enum
2src/script/solver.cppAdd string name in GetTxnOutputType(); detection in Solver() for v3 + 32-byte program
3src/script/interpreter.hWITNESS_V3_SPKHASH_SIZE = 32 constant; SCRIPT_VERIFY_P2SPKH flag
4src/addresstype.hWitnessV3SpkHash struct wrapping uint256 (with XOnlyPubKey constructor computing HASH256); add to CTxDestination variant
5src/addresstype.cppExtractDestination(), CScriptVisitor (OP_3 <hash>), ValidDestinationVisitor
6src/key_io.cppDestinationEncoder for v3 (Bech32m); DecodeDestination v3+32 bytes -> WitnessV3SpkHash
7src/outputtype.hAdd OutputType::P2SPKH to enum and OUTPUT_TYPES array
8src/outputtype.cppOutputTypeFromDestination: WitnessV3SpkHash -> P2SPKH; ParseOutputType("p2spkh")

Consensus / validation

#FileChange
9src/script/interpreter.cppVerifyWitnessProgram(): v3+32-byte branch -- HASH256 pubkey check + CheckSchnorrSignature; ScriptFlagNamesToEnum(): add P2SPKH
10src/consensus/params.hAdd DEPLOYMENT_P2SPKH to DeploymentPos enum
11src/deploymentinfo.cppAdd "p2spkh" deployment info
12src/kernel/chainparams.cppDeployment params for mainnet, testnet4, testnet3, signet, regtest
13src/validation.cppGetBlockScriptFlags(): add SCRIPT_VERIFY_P2SPKH gated by DEPLOYMENT_P2SPKH

Signing

#FileChange
14src/script/sign.cppSignStep() case; ProduceSignature() build witness stack [sig, pubkey]

Signing provider

#FileChange
15src/script/signingprovider.hAdd GetSpkPubKey() virtual method + spk_keys map
16src/script/signingprovider.cppImplement FlatSigningProvider::GetSpkPubKey()

Policy

#FileChange
17src/policy/policy.cppIsWitnessStandard(): v3 rules (exactly 2 items, sig = 64 bytes, pubkey = 32 bytes)
18src/policy/policy.hAdd SCRIPT_VERIFY_P2SPKH to MANDATORY_SCRIPT_VERIFY_FLAGS

Wallet

#FileChange
19src/wallet/spend.cppGetOutputType(): WITNESS_V3_SPKHASH -> P2SPKH
20src/wallet/wallet.cppTransactionChangeType(): detect WitnessV3SpkHash recipients, return P2SPKH change
21src/wallet/scriptpubkeyman.cppAdd WITNESS_V3_SPKHASH to legacy wallet handling
22src/wallet/rpc/addresses.cppDescribeAddressVisitor for WitnessV3SpkHash
23src/wallet/walletutil.cppGenerateWalletDescriptor: P2SPKH -> spk(...) with 87h derivation

Descriptor

#FileChange
24src/script/descriptor.cppSPKDescriptor class: output type P2SPKH, script size 34, max satisfaction weight 98; ParseScriptContext::P2SPK; spk() function parser; InferScript() recognition

RPC

#FileChange
25src/rpc/rawtransaction.cppdecodescript RPC: WITNESS_V3_SPKHASH in switch cases
26src/rpc/util.cppDescribeAddressVisitor for WitnessV3SpkHash (JSON output)

Tests

#FileChange
27src/test/p2spkh_tests.cpp13 unit tests: Solver detection, address roundtrip, HASH256, signing roundtrip, wrong hash, wrong stack size, wrong pubkey, invalid sig, descriptor parsing, forward-compat without flag
28src/test/transaction_tests.cppUpdated CTxDestination variant size assert (9 -> 10); skip v3+32 in spendswitnessprog loop
29src/test/fuzz/util.cppAdd WitnessV3SpkHash to ConsumeTxDestination
30src/wallet/test/wallet_tests.cppBasicOutputTypesTest 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

spk() descriptor wallet, generate address, send, spend.

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.