Manually Creating and Signing a Bitcoin Transaction
In this tutorial, we’re going to create a testnet Bitcoin transaction without using any libraries. Although not advisable, you can do it on mainnet too, since the procedure is exactly the same.
We will also use P2PKH addresses only. I might write a SegWit version of the guide later.
Update (5 Oct 2021): SegWit version.
If you don’t have any testnet coins, you can generate addresses at iancoleman BIP39. Just select BTC — Bitcoin Testnet, click Generate and scroll down to find your addresses and keys (or just reuse my addresses).
Then get some tBTC at the Testnet Faucet, Bitcoin Testnet Faucet or Coin Faucet.
1. Creating the Transaction
The raw transaction format is described in the Bitcoin Developer Reference, under Raw Transaction Format.
First, we start by gathering information about the UTXOs or coins we will use. We need two UTXOs for this example. Using a single one would be simpler, but the process of signing a transaction with multiple inputs is tricky. Tutorials can be very frustrating when they cover only the most basic case, so I decided to spend two instead.
UTXOs
My first UTXO is at mgWqtfrTjQ5y7GMLH8aHAdsPTwMm7sg3Tq and the second one is at mra9JYc7m4FriFeUNsEwKdcPRgSv82LoRz.
We can use Bitaddress Testnet Edition, option Wallet Details and paste the WIF private keys to find out what are the actual public and private keys:
Filling the Transaction Fields
First, transaction version. Currently, transactions can be version 1 or 2. This is a 4-byte little-endian field. We’ll use version 2, so our transaction starts like this:
02000000
Inputs
The number of inputs field is a varint. We have two inputs, so 0x02
.
0200000002
Now, for each input, we need to add the previous transaction hash, UTXO index, scriptSig and sequence.
Input #0
The previous transaction ID can be obtained from mempool.space. A neat way of converting it to little-endian is using tac
:
That transaction only has one output, so the 4-byte, little-endian index is 00000000
. We then have to add the scriptSig size and the script itself. Since we can’t sign anything by now, let’s include the size 0x00
and leave the script empty. Finally, the sequence. We won’t be using RBF or locktime, so ffffffff
. This is our first, incomplete input:
55a736179f5ee498660f33ca6f4ce017ed8ad4bd286c162400d215f3c5a876af 00000000 00 <scriptSig> ffffffff
Input #1
Nothing new here. Just note our UTXO this time is the 3rd: 02000000
. Second input:
4d89764cf5490ac5023cb55cd2a0ecbfd238a216de62f4fd49154253f1a75092 02000000 00 <scriptSig> ffffffff
Transaction:
020000000255a736179f5ee498660f33ca6f4ce017ed8ad4bd286c162400d215f3c5a876af0000000000<scriptSig>ffffffff4d89764cf5490ac5023cb55cd2a0ecbfd238a216de62f4fd49154253f1a750920200000000<scriptSig>ffffffff
Outputs
We have 373,500 satoshis. Let’s send 320,000 sats to address n2ozAmaunMGwPDjtxmZsyxDRjYAJqmZ6Dk. The change, 52,000 sats, will be sent to mxFEHeSxxKjy9YcmFzXNpuE3FFJyby56jA and we will leave 1500 as miner fee. So, two outputs: 0x02
. For each output, we need to add the amount and the scriptPubKey.
Output #0
First, the amount. An 8-byte little-endian for 320000. I don’t know any bash commands for that, so I’ve used Python:
>>> int.to_bytes(320000, 8, "little").hex()
'00e2040000000000'
The standard P2PKH scriptPubKey is OP_DUP OP_HASH160 <pubkey hash> OP_EQUALVERIFY OP_CHECKSIG
. Address map in Bitcoin Wiki describes how to convert from public key hash to address and vice versa. We have to decode from base58check, then drop the leading 0x00
and the trailing 4 bytes, which is the checksum (that should be verified in a real situation, of course).
One way of doing it is using base58
of bitcoin-bash-tools (xxd
is being used to convert from binary to hex):
$ echo n2ozAmaunMGwPDjtxmZsyxDRjYAJqmZ6Dk | base58 -d | xxd -p | cut -c 3-42
e993470936b573678dc3b997e56db2f9983cb0b4
Or a Web tool, like Go Bitcoin Tests (fill input9 and look for the hash160
on input 3). It will display an error message about the checksum, because it expects mainnet input, but it decodes it correctly.
We also need to add the opcodes. Bitcoin Wiki’s Script is handful. Our first scriptPubKey is:
76 a9 14 e993470936b573678dc3b997e56db2f9983cb0b4 88 ac
If you’re wondering what is that 0x14
, that’s 20 in decimal, because we’re pushing 20 bytes to the stack. We also need to include the script size, which is 0x19
(25 bytes). The first output is:
00e2040000000000 19 76 a9 14 e993470936b573678dc3b997e56db2f9983cb0b4 88 ac
Output #1
The change output is the same as the payment one, except, of course, for the amount and pubkey hash. Change output:
20cb000000000000 19 76 a9 14 b780d54c6b03b053916333b50a213d566bbedd13 88 ac
Transaction, after adding 0x02
and both outputs:
020000000255a736179f5ee498660f33ca6f4ce017ed8ad4bd286c162400d215f3c5a876af0000000000<scriptSig>ffffffff4d89764cf5490ac5023cb55cd2a0ecbfd238a216de62f4fd49154253f1a750920200000000<scriptSig>ffffffff0200e20400000000001976a914e993470936b573678dc3b997e56db2f9983cb0b488ac20cb0000000000001976a914b780d54c6b03b053916333b50a213d566bbedd1388ac
Locktime
We won’t be using locktime, so just append 00000000
to the transaction:
020000000255a736179f5ee498660f33ca6f4ce017ed8ad4bd286c162400d215f3c5a876af0000000000<scriptSig>ffffffff4d89764cf5490ac5023cb55cd2a0ecbfd238a216de62f4fd49154253f1a750920200000000<scriptSig>ffffffff0200e20400000000001976a914e993470936b573678dc3b997e56db2f9983cb0b488ac20cb0000000000001976a914b780d54c6b03b053916333b50a213d566bbedd1388ac00000000
Awesome! Now we have to sign the transaction.
2. Signing the Transaction
Now comes the hard part. First, in order to sign transactions with OpenSSL, we need our private keys complying with the ASN.1 structure. We can’t simply use hex private keys.
Converting Hex Keys to PEM
To do so, we have to prepend the pre-string 302e0201010420
to our private key and append it with the secp256k1 ID a00706052b8104000a
, as described in WIF to PEM:
Nice! We have private keys priv1.pem
and priv2.pem
.
Signing Inputs
Things get trickier. We have to sign inputs separately. For each input, we have to:
- Copy the scriptPubKey of the previous transaction to the scriptSig of the input;
- Sign that input;
- Clear the scriptSig;
Why copy the scriptPubKey? Pieter Wuille answers it:
Before starting the signing process, we have to insert the signature hash type at the end of the transaction. We will be using SIGHASH_ALL, so add 01000000
there and we have:
020000000255a736179f5ee498660f33ca6f4ce017ed8ad4bd286c162400d215f3c5a876af0000000000<scriptSig>ffffffff4d89764cf5490ac5023cb55cd2a0ecbfd238a216de62f4fd49154253f1a750920200000000<scriptSig>ffffffff0200e20400000000001976a914e993470936b573678dc3b997e56db2f9983cb0b488ac20cb0000000000001976a914b780d54c6b03b053916333b50a213d566bbedd1388ac0000000001000000
Signing Input #0
Let’s copy the scriptPubKey of the previous transaction. If you’re using mempool.space, click on Details.
The script is 76a9140af2c7db949861cdc7767ad211432789f1852e9488ac
. Replace the first<scriptSig>
with the scriptPubKey. Remember to also replace the script size: 0x00
with 0x19
and remove the 2nd <scriptSig>
(leave only its size). Our transaction becomes:
020000000255a736179f5ee498660f33ca6f4ce017ed8ad4bd286c162400d215f3c5a876af000000001976a9140af2c7db949861cdc7767ad211432789f1852e9488acffffffff4d89764cf5490ac5023cb55cd2a0ecbfd238a216de62f4fd49154253f1a750920200000000ffffffff0200e20400000000001976a914e993470936b573678dc3b997e56db2f9983cb0b488ac20cb0000000000001976a914b780d54c6b03b053916333b50a213d566bbedd1388ac0000000001000000
Let’s hash256
the transaction and sign it with the first private key:
The pkeyutl
argument tells OpenSSL to not hash the data. If you use dgst
, then OpenSSL will hash our transaction for the third time. Alternatively, you could hash it once and then use dgst
to have it hashed again.
Now, things become even trickier. To counter transaction malleability, BIP 66 has enforced some rules OpenSSL’s signature might not always conform to. That’s why Bitcoin uses the secp256k1 library nowadays.
We’re doing it the hard way, so let’s decode our DER signature:
30 - indicates compound structure
46 - total length
02 - integer type
21 - length of <r>
00f33bb5984ca59d24fc032fe9903c1a8cb750e809c3f673d71131b697fd132894 - <r>
02 - integer type
21 - length of <s>
00e2c8d13849239025b6208f65b4ac2ccca9ef36c7a702bd1682fe7abea448bfc3 - <s>
Both r
and s
have to be positive numbers, so their first byte can’t be greater than 0x7f
. If that happens, they must be prepended with 0x00
. Those are the only allowed leading zeros. OpenSSL already does that.
Finally, s
can’t be greater than n/2
, n
being the order of the curve, which value is 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141
. Again, I use Python’s REPL to check the value of s
:
If we use that signature, our transaction will be rejected with this error message: Non-canonical signature: S value is unnecessarily high. We can either generate more signatures until we get a valid s
or we can fix it:
We can replace the old s
with the new one. But, guess what? This new value is 32 bytes long and the old one had 33 bytes. We have to subtract 1 from the length of s
and from the total length:
30 - indicates compound structure
45 - total length
02 - integer type
21 - length of <r>
00f33bb5984ca59d24fc032fe9903c1a8cb750e809c3f673d71131b697fd132894 - <r>
02 - integer type
20 - length of <s>
1d372ec7b6dc6fda49df709a4b53d33210bfa61f0845e3253cd3e3ce2bed817e - <s>
We finally append 0x01
to the signature, which is the sighash. Our final signature is:
3045022100b8e920e1573578b5c2dd84864fce6f0681d7753b266c59682179a00c05c76d8d02201d372ec7b6dc6fda49df709a4b53d33210bfa61f0845e3253cd3e3ce2bed817e01
Creating the scriptSig
This is easy. The script is just <sig> <public key>
. Since the signature’s length is 72, we have to add a 0x48
push operation. The public key has 33 bytes, so we add 0x21
. Finally, the whole script has a length of 107, which is 0x6b
. Our first scriptSig with size is:
6b 48 3045022100f33bb5984ca59d24fc032fe9903c1a8cb750e809c3f673d71131b697fd13289402201d372ec7b6dc6fda49df709a4b53d33210bfa61f0845e3253cd3e3ce2bed817e01 21 02EE04998F8DBD9819D0391A5AA38DB1331B0274F64ABC3BC66D69EE61DB913459
As I said before, we can’t add it to the transaction yet. If we do, the 2nd signature will be signing this signature and signing order would matter.
Signing Input #1
Get the scriptPubKey from the previous transaction: 76a9147943d227e90eed9549503b32ae140b8a12ff44ae88ac
. Then, get the transaction as it was before we inserted the first scriptPubKey, right after we added the sighash. This is it, so you don’t have to scroll up:
020000000255a736179f5ee498660f33ca6f4ce017ed8ad4bd286c162400d215f3c5a876af0000000000<scriptSig>ffffffff4d89764cf5490ac5023cb55cd2a0ecbfd238a216de62f4fd49154253f1a750920200000000<scriptSig>ffffffff0200e20400000000001976a914e993470936b573678dc3b997e56db2f9983cb0b488ac20cb0000000000001976a914b780d54c6b03b053916333b50a213d566bbedd1388ac0000000001000000
Remove the first <scriptSig>
and replace the second and its 0x00
size with 0x19
and the scriptPubKey:
020000000255a736179f5ee498660f33ca6f4ce017ed8ad4bd286c162400d215f3c5a876af0000000000ffffffff4d89764cf5490ac5023cb55cd2a0ecbfd238a216de62f4fd49154253f1a75092020000001976a9147943d227e90eed9549503b32ae140b8a12ff44ae88acffffffff0200e20400000000001976a914e993470936b573678dc3b997e56db2f9983cb0b488ac20cb0000000000001976a914b780d54c6b03b053916333b50a213d566bbedd1388ac0000000001000000
Again, double hash and sign:
This signature has no issues (I actually kept signing until I got a valid one). This is the scriptSig with size:
6a 47 304402201f055eb8374aca9b779dd7f8dc91e0afb609ac61cd5cb9ad1f9ca0359c3d134a022019c45145919394096e42963b7e9b6538cdb303a30c6ff0f17b8b0cfb1e897f5a01 21 0333D23631BC450AAF925D685794903576BBC8B20007CF334C0EA6C7E2C0FAB2BA
3. Finalizing the Transaction
We’re almost done. Just replace both <scriptSig>
and their 0x00
sizes with our scriptSigs. Also, remove the sighash from the end. This is our final transaction:
020000000255a736179f5ee498660f33ca6f4ce017ed8ad4bd286c162400d215f3c5a876af000000006b483045022100f33bb5984ca59d24fc032fe9903c1a8cb750e809c3f673d71131b697fd13289402201d372ec7b6dc6fda49df709a4b53d33210bfa61f0845e3253cd3e3ce2bed817e012102EE04998F8DBD9819D0391A5AA38DB1331B0274F64ABC3BC66D69EE61DB913459ffffffff4d89764cf5490ac5023cb55cd2a0ecbfd238a216de62f4fd49154253f1a75092020000006a47304402201f055eb8374aca9b779dd7f8dc91e0afb609ac61cd5cb9ad1f9ca0359c3d134a022019c45145919394096e42963b7e9b6538cdb303a30c6ff0f17b8b0cfb1e897f5a01210333D23631BC450AAF925D685794903576BBC8B20007CF334C0EA6C7E2C0FAB2BAffffffff0200e20400000000001976a914e993470936b573678dc3b997e56db2f9983cb0b488ac20cb0000000000001976a914b780d54c6b03b053916333b50a213d566bbedd1388ac00000000
If you have a testnet node, you can test if the transaction is ok:
If you don’t have a node, you can try to push or decode the transaction at BlockCypher or Blockstream. Some common error messages you might get:
- Non-canonical signature: S value is unnecessarily high: your
s
is greater thann/2
. Either sign again or fix it withs = n - s
; - Non-canonical DER signature: signatures are zero-padded or start with a byte greater than
0x7f
; - Signature must be zero for failed CHECK(MULTI)SIG operation: one or more signatures failed. Follow the steps carefully again. It’s pretty easy to screw up. I got this error multiple times while writing this. That’s why we use wallets instead of manually sign transactions :)
4. Conclusion
We manually created a transaction with multiple inputs and outputs using only Bash and Python’s REPL. We then signed it using OpenSSL.
If you were brave enough to follow along until the end, congrats! I hope you had fun and learned more about Bitcoin transactions.