Skip to content

11.4: Scripting a P2PKH

Though P2PKH are largely a thing of the past thanks to the advent of SegWit, they remain a great building block for understanding Bitcoin, and especially for understanding Bitcoin Script. So we're going to explore them here as a stepping stone before we get to the more abstract way that P2WPKH deals with its standard scripts.

Understand the Unlocking Script

We've long said that when funds are sent to a Bitcoin address, they're locked to the private key associated with that address. This is managed through the scriptPubKey of a P2PKH transaction, which is designed such that it requires the recipient to have the private key associated with the the P2PKH Bitcoin address. To be precise, the recipient must supply both the public key linked to the private key and a signature generated by the private key.

Take a look again at the transaction you created in §11.1:

{
  "txid": "fe8a790c21c289521a010f7fcaa66863b48eb956ed535f5c5d659ddf3dc0d45a",
  "hash": "fe8a790c21c289521a010f7fcaa66863b48eb956ed535f5c5d659ddf3dc0d45a",
  "version": 2,
  "size": 191,
  "vsize": 191,
  "weight": 764,
  "locktime": 0,
  "vin": [
    {
      "txid": "c7fd5419c9c16a577ca7919b2e2c3bce816753ddc3a26b50a55443522ccb4879",
      "vout": 1,
      "scriptSig": {
        "asm": "30440220252a4b161934f2fa3e00048735530dec19e8e66dabd702437d62ac100910432f022010deca96e14b96f467f42886bc18e1ce81639d69c14830d87e0c6a5c9ab05f3b[ALL] 0254e9741695210633a048c6193aa802630532eeeca6364cf4bb529d156aeee4aa",
        "hex": "4730440220252a4b161934f2fa3e00048735530dec19e8e66dabd702437d62ac100910432f022010deca96e14b96f467f42886bc18e1ce81639d69c14830d87e0c6a5c9ab05f3b01210254e9741695210633a048c6193aa802630532eeeca6364cf4bb529d156aeee4aa"
      },
      "sequence": 4294967293
    }
  ],
  "vout": [
    {
      "value": 0.00098000,
      "n": 0,
      "scriptPubKey": {
        "asm": "OP_DUP OP_HASH160 dd78bf31853bd9c0e7cb108fc98d799955d60987 OP_EQUALVERIFY OP_CHECKSIG",
        "desc": "addr(n1hzH5yyTPiAwkiFPpxcGQvsXNxc35dqNv)#w4xwe6te",
        "hex": "76a914dd78bf31853bd9c0e7cb108fc98d799955d6098788ac",
        "address": "n1hzH5yyTPiAwkiFPpxcGQvsXNxc35dqNv",
        "type": "pubkeyhash"
      }
    }
  ]
}

You can see that its scriptSig unlocking script has two values. That's a <signature> (and an [ALL]) and a <pubKey>:

30440220252a4b161934f2fa3e00048735530dec19e8e66dabd702437d62ac100910432f022010deca96e14b96f467f42886bc18e1ce81639d69c14830d87e0c6a5c9ab05f3b[ALL] 0254e9741695210633a048c6193aa802630532eeeca6364cf4bb529d156aeee4aa

That's all an unlock script is! (For a P2PKH.) You need to know the public key for the address and have signed the transaction with the associated private key.

Understand the Locking Script

Remember that each unlocking script unlocks a previous UTXO. In the above example, the vin reveals that it's actually unlocking vout 1 of txid c7fd5419c9c16a577ca7919b2e2c3bce816753ddc3a26b50a55443522ccb4879.

You can examine that UTXO with gettransaction.

bitcoin-cli gettransaction "c7fd5419c9c16a577ca7919b2e2c3bce816753ddc3a26b50a55443522ccb4879"

| {
|   "amount": 0.00100000,
|   "confirmations": 14,
|   "blockhash": "00000006901bbcb7e1643776b8663ff85e87f856689f1bd2537333eaabacfce7",
|   "blockheight": 308456,
|   "blockindex": 5,
|   "blocktime": 1781205123,
|   "txid": "c7fd5419c9c16a577ca7919b2e2c3bce816753ddc3a26b50a55443522ccb4879",
|   "wtxid": "5ba981b75bfb945720387a97e4f9e623f2217a0cc61d6eb59c29ef7609e83310",
|   "walletconflicts": [
|   ],
|   "mempoolconflicts": [
|   ],
|   "time": 1781205123,
|   "timereceived": 1781207317,
|   "bip125-replaceable": "no",
|   "details": [
|     {
|       "address": "moNhddRsGhaF18L5e62vEisLZxnzEV2VbF",
|       "parent_descs": [
|         "pkh([001debc7/44h/1h/0h]tpubDDpenjqX1uXySci7zb7zzvWb2DTin1EHaC7pkAnXkWm6Tnm9cveCwae95yN9MQHsvS6qjmD7yYFEpCTN1nF8CEBo4uUUqCimRe1a719rR2D/0/*)#rvlryhnv"
|       ],
|       "category": "receive",
|       "amount": 0.00100000,
|       "label": "",
|       "vout": 1,
|       "abandoned": false
|     }
|   ],
|   "hex": "02000000000101ddca60be7057df124af6e526f1fca26babb633e22570f6341ac4612a3fe0b59d0000000000fdffffff02d17f0100000000001976a91495ce486460368cc57d53db5bf499c194d3b4121d88aca0860100000000001976a9145631705e141bcfb1c4f310cb7d55055f22ec55ef88ac02473044022058c5ce4b86092166ccea6951204431acf6f8e7bcb0a5c0d50631c573718b8ff4022041c644518ab96b29114cba3c3b308973dcd3c1085de348bf715162e83e364c79012102d3be0fab38f1c758441921088b1e7b4fbb26cbafa97a9b0fd4b0186ef195e46d00000000",
|   "lastprocessedblock": {
|     "hash": "00000011917c604b622cce3a4772a7e8487da43237ee2fe2fc894dbadc5cd5f7",
|     "height": 308469
|   }
| }

But as you can see, you didn't get the scriptPubKey with gettransaction. You need to take an additional step to retrieve that by examining the raw transaction info (that's the hex) with decoderawtransaction:

hex=$(bitcoin-cli gettransaction "c7fd5419c9c16a577ca7919b2e2c3bce816753ddc3a26b50a55443522ccb4879" | jq -r '.hex')
bitcoin-cli decoderawtransaction $hex

| {
|   "txid": "c7fd5419c9c16a577ca7919b2e2c3bce816753ddc3a26b50a55443522ccb4879",
|   "hash": "5ba981b75bfb945720387a97e4f9e623f2217a0cc61d6eb59c29ef7609e83310",
|   "version": 2,
|   "size": 228,
|   "vsize": 147,
|   "weight": 585,
|   "locktime": 0,
|   "vin": [
|     {
|       "txid": "9db5e03f2a61c41a34f67025e233b6ab6ba2fcf126e5f64a12df5770be60cadd",
|       "vout": 0,
|       "scriptSig": {
|         "asm": "",
|         "hex": ""
|       },
|       "txinwitness": [
|         "3044022058c5ce4b86092166ccea6951204431acf6f8e7bcb0a5c0d50631c573718b8ff4022041c644518ab96b29114cba3c3b308973dcd3c1085de348bf715162e83e364c7901",
|         "02d3be0fab38f1c758441921088b1e7b4fbb26cbafa97a9b0fd4b0186ef195e46d"
|       ],
|       "sequence": 4294967293
|     }
|   ],
|   "vout": [
|     {
|       "value": 0.00098257,
|       "n": 0,
|       "scriptPubKey": {
|         "asm": "OP_DUP OP_HASH160 95ce486460368cc57d53db5bf499c194d3b4121d OP_EQUALVERIFY OP_CHECKSIG",
|         "desc": "addr(muB46R4dst7Cfq6EtXZQkdAqFh89d6EHCz)#qukxw8qt",
|         "hex": "76a91495ce486460368cc57d53db5bf499c194d3b4121d88ac",
|         "address": "muB46R4dst7Cfq6EtXZQkdAqFh89d6EHCz",
|         "type": "pubkeyhash"
|       }
|     },
|     {
|       "value": 0.00100000,
|       "n": 1,
|       "scriptPubKey": {
|         "asm": "OP_DUP OP_HASH160 5631705e141bcfb1c4f310cb7d55055f22ec55ef OP_EQUALVERIFY OP_CHECKSIG",
|         "desc": "addr(moNhddRsGhaF18L5e62vEisLZxnzEV2VbF)#zpgu4vqd",
|         "hex": "76a9145631705e141bcfb1c4f310cb7d55055f22ec55ef88ac",
|         "address": "moNhddRsGhaF18L5e62vEisLZxnzEV2VbF",
|         "type": "pubkeyhash"
|       }
|     }
|   ]
| }

You can now look at vout 1 and see it was locked with the scriptPubKey of OP_DUP OP_HASH160 5631705e141bcfb1c4f310cb7d55055f22ec55ef OP_EQUALVERIFY OP_CHECKSIG. That's the standard locking methodology used for an older P2PKH address with the public key's hash (<pubKeyHash>) stuck in the middle.

Running it will demonstrate how it works.

Run a P2PKH Script

When you unlock a P2PKH UTXO, you (effectively) concatenate the unlocking (scriptSig) and locking (scriptPubKey) scripts. For a P2PKH address, like the example used in this chapter, that produces:

Script: <signature> <pubKey> OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG

With that combined together, you can examinine how a P2PKH UTXO is unlocked.

First, you place the initial constants on the stack, then make a duplicate of the pubKey with OP_DUP:

Script: <signature> <pubKey> OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG
Stack: [ ]

Script: <pubKey> OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG
Stack: [ <signature> ]

Script: OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG
Stack: [ <signature> <pubKey> ]

Script: OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG
Running: <pubKey> OP_DUP
Stack: [ <signature> <pubKey> <pubKey> ]

Why the duplicate? Because it's needed to check the two unlocking elements: the public key and the signature.

Next, OP_HASH160 pops the <pubKey> off the stack, hashes it, and puts the result back on the stack.

Script: <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG
Running: <pubKey> OP_HASH160
Stack: [ <signature> <pubKey> <pubKeyHash> ]

Then, you place the <pubKeyHash> that was in the locking script on the stack:

Script: OP_EQUALVERIFY OP_CHECKSIG
Stack: [ <signature> <pubKey> <pubKeyHash> <pubKeyHash> ]

OP_EQUALVERIFY is effectively two opcodes: OP_EQUAL, which pops two items from the stack and pushes True or False based on the comparison; and OP_VERIFY which pops that result and immediately marks the transaction as invalid if it's False. (Chapter 14 talks more about the use of OP_VERIFY as a conditional.)

Assuming the two <pubKeyHash>es are equal, you will have the following result:

Script: OP_CHECKSIG
Running: <pubKeyHash> <pubKeyHash> OP_EQUALVERIFY
Stack: [ <signature> <pubKey> ]

At this point you've proven that the <pubKey> supplied in the scriptSig hashes to the Bitcoin address in question, so you know that the redeemer knew the public key. But, they also need to prove knowledge of the private key, which is done with OP_CHECKSIG, which confirms that the unlocking script's signature matches that public key.

Script:
Running: <signature> <pubKey> OP_CHECKSIG
Stack: [ True ]

The Script now ends and if it was successful, the transaction is allowed to respend the UTXO in question.

Use btcdeb for a P2PKH Example

Testing out actual Bitcoin transactions with btcdeb is a bit trickier, because you need to know the public key and a signature to make everything work, and generating the latter is somewhat difficult. However, one way to test things is to let Bitcoin do the work for you in generating a transaction that would unlock a UTXO. That's what you've done above: generating the transaction to spend the UTXO caused bitcoin-cli to calculate the <signature> and <pubKey>. You then look at the raw transaction information of the UTXO to learn the locking script including the <pubKeyHash>

You can put together the locking script, the signature, and the pubkey using btcdeb, showing how simple a P2PKH script is.

btcdeb '[30440220252a4b161934f2fa3e00048735530dec19e8e66dabd702437d62ac100910432f022010deca96e14b96f467f42886bc18e1ce81639d69c14830d87e0c6a5c9ab05f3b 0254e9741695210633a048c6193aa802630532eeeca6364cf4bb529d156aeee4aa OP_DUP OP_HASH160 5631705e141bcfb1c4f310cb7d55055f22ec55ef OP_EQUALVERIFY OP_CHECKSIG]'

| btcdeb 5.0.24 -- type `btcdeb -h` for start up options
| LOG: signing segwit taproot
| notice: btcdeb has gotten quieter; use --verbose if necessary (this message is temporary)
7 op script loaded. type `help` for usage information

script                                                             |  stack 
-------------------------------------------------------------------+--------
30440220252a4b161934f2fa3e00048735530dec19e8e66dabd702437d62ac1... | 
0254e9741695210633a048c6193aa802630532eeeca6364cf4bb529d156aeee4aa | 
OP_DUP                                                             | 
OP_HASH160                                                         | 
5631705e141bcfb1c4f310cb7d55055f22ec55ef                           | 
OP_EQUALVERIFY                                                     | 
OP_CHECKSIG                                                        | 
#0000 30440220252a4b161934f2fa3e00048735530dec19e8e66dabd702437d62ac100910432f022010deca96e14b96f467f42886bc18e1ce81639d69c14830d87e0c6a5c9ab05f3b

You push the <signature> and <pubKey> onto the stack:

btcdeb> step
        <> PUSH stack 30440220252a4b161934f2fa3e00048735530dec19e8e66dabd702437d62ac100910432f022010deca96e14b96f467f42886bc18e1ce81639d69c14830d87e0c6a5c9ab05f3b
script                                                             |                                                             stack 
-------------------------------------------------------------------+-------------------------------------------------------------------
0254e9741695210633a048c6193aa802630532eeeca6364cf4bb529d156aeee4aa | 30440220252a4b161934f2fa3e00048735530dec19e8e66dabd702437d62ac1...
OP_DUP                                                             | 
OP_HASH160                                                         | 
5631705e141bcfb1c4f310cb7d55055f22ec55ef                           | 
OP_EQUALVERIFY                                                     | 
OP_CHECKSIG                                                        | 
#0001 0254e9741695210633a048c6193aa802630532eeeca6364cf4bb529d156aeee4aa

btcdeb> step
        <> PUSH stack 0254e9741695210633a048c6193aa802630532eeeca6364cf4bb529d156aeee4aa
script                                                             |                                                             stack 
-------------------------------------------------------------------+-------------------------------------------------------------------
OP_DUP                                                             | 0254e9741695210633a048c6193aa802630532eeeca6364cf4bb529d156aeee4aa
OP_HASH160                                                         | 30440220252a4b161934f2fa3e00048735530dec19e8e66dabd702437d62ac1...
5631705e141bcfb1c4f310cb7d55055f22ec55ef                           | 
OP_EQUALVERIFY                                                     | 
OP_CHECKSIG                                                        | 
#0002 OP_DUP

You OP_DUP and OP_HASH the <pubKey>:

btcdeb> step
        <> PUSH stack 0254e9741695210633a048c6193aa802630532eeeca6364cf4bb529d156aeee4aa
script                                                             |                                                             stack 
-------------------------------------------------------------------+-------------------------------------------------------------------
OP_HASH160                                                         | 0254e9741695210633a048c6193aa802630532eeeca6364cf4bb529d156aeee4aa
5631705e141bcfb1c4f310cb7d55055f22ec55ef                           | 0254e9741695210633a048c6193aa802630532eeeca6364cf4bb529d156aeee4aa
OP_EQUALVERIFY                                                     | 30440220252a4b161934f2fa3e00048735530dec19e8e66dabd702437d62ac1...
OP_CHECKSIG                                                        | 
#0003 OP_HASH160

btcdeb> step
        <> POP  stack
        <> PUSH stack 5631705e141bcfb1c4f310cb7d55055f22ec55ef
script                                                             |                                                             stack 
-------------------------------------------------------------------+-------------------------------------------------------------------
5631705e141bcfb1c4f310cb7d55055f22ec55ef                           |                           5631705e141bcfb1c4f310cb7d55055f22ec55ef
OP_EQUALVERIFY                                                     | 0254e9741695210633a048c6193aa802630532eeeca6364cf4bb529d156aeee4aa
OP_CHECKSIG                                                        | 30440220252a4b161934f2fa3e00048735530dec19e8e66dabd702437d62ac1...
#0004 5631705e141bcfb1c4f310cb7d55055f22ec55ef

You push the <pubKeyHash> from the locking script onto the stack and verify it:

btcdeb> step
        <> PUSH stack 5631705e141bcfb1c4f310cb7d55055f22ec55ef
script                                                             |                                                             stack 
-------------------------------------------------------------------+-------------------------------------------------------------------
OP_EQUALVERIFY                                                     |                           5631705e141bcfb1c4f310cb7d55055f22ec55ef
OP_CHECKSIG                                                        |                           5631705e141bcfb1c4f310cb7d55055f22ec55ef
                                                                   | 0254e9741695210633a048c6193aa802630532eeeca6364cf4bb529d156aeee4aa
                                                                   | 30440220252a4b161934f2fa3e00048735530dec19e8e66dabd702437d62ac1...
#0005 OP_EQUALVERIFY

btcdeb> step
        <> POP  stack
        <> POP  stack
        <> PUSH stack 01
        <> POP  stack
script                                                             |                                                             stack 
-------------------------------------------------------------------+-------------------------------------------------------------------
OP_CHECKSIG                                                        | 0254e9741695210633a048c6193aa802630532eeeca6364cf4bb529d156aeee4aa
                                                                   | 30440220252a4b161934f2fa3e00048735530dec19e8e66dabd702437d62ac1...
#0006 OP_CHECKSIG

And that point, all that's required is the OP_CHECKSIG:

btcdeb> step
EvalChecksig() sigversion=0
Eval Checksig Pre-Tapscript
error: Signature is found in scriptCode

(Unfortunately this checking may or may not be working at any point due to vagaries of the Bitcoin Core and btcdeb code.)

As is shown, a P2PKH is quite simple: its protection comes about through the strength of its cryptography.

How to Look Up a Pub Key & Signature by Hand

What if you wanted to generate the <signature> and <PubKey> information needed to unlock a UTXO yourself, without leaning on bitcoin-cli to create a transaction?

It turns out that it's pretty easy to get a <pubKey> You just need to use getaddressinfo to examine the address where the UTXO is currently sitting:

bitcoin-cli getaddressinfo moNhddRsGhaF18L5e62vEisLZxnzEV2VbF

| {
|   "address": "moNhddRsGhaF18L5e62vEisLZxnzEV2VbF",
|   "scriptPubKey": "76a9145631705e141bcfb1c4f310cb7d55055f22ec55ef88ac",
|   "ismine": true,
|   "solvable": true,
|   "desc": "pkh([001debc7/44h/1h/0h/0/0]0254e9741695210633a048c6193aa802630532eeeca6364cf4bb529d156aeee4aa)#gl2gg008",
|   "parent_desc": "pkh([001debc7/44h/1h/0h]tpubDDpenjqX1uXySci7zb7zzvWb2DTin1EHaC7pkAnXkWm6Tnm9cveCwae95yN9MQHsvS6qjmD7yYFEpCTN1nF8CEBo4uUUqCimRe1a719rR2D/0/*)#rvlryhnv",
|   "iswatchonly": false,
|   "isscript": false,
|   "iswitness": false,
|   "pubkey": "0254e9741695210633a048c6193aa802630532eeeca6364cf4bb529d156aeee4aa",
|   "iscompressed": true,
|   "ischange": false,
|   "timestamp": 1781204055,
|   "hdkeypath": "m/44h/1h/0h/0/0",
|   "hdseedid": "0000000000000000000000000000000000000000",
|   "hdmasterfingerprint": "001debc7",
|   "labels": [
|     ""
|   ]
| }

It's shown in both the pubkey variable and as part of the desc.

Figuring out that signature, however, requires really understanding the nuts and bolts of how Bitcoin transactions are created. So we leave that as advanced study for the reader: creating a bitcoin-cli transaction to "solve" a UTXO is the best solution to that for the moment.

Summary: Scripting a Pay to Public Key Hash

Sending to a P2PKH address was relatively easy when you were just using bitcoin-cli. Examining the Bitcoin Script underlying it lays bare the cryptographic functions that were implicit in funding that transaction:

  • The unlocking script supplied a public key and a signature.
  • The locking script verified that the public key matched the address (which includes a hash of the key).
  • The locking script verified that the public key matched the signature generated by the private key.

What's Next?

Continue "Introducing Bitcoin Scripts" with §11.5: Scripting a P2WPKH.