Manually Creating and Signing a Bitcoin Transaction

Step by Step

otto
7 min readSep 14, 2021

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

Testnet Addresses at Iancoleman’s
Generating Testnet 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:

Balance, public and private keys of the UTXOs

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:

Ask Satoshi!
Ask Satoshi!

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:

Notice filenames have changed!

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:

It worked!

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 than n/2 . Either sign again or fix it with s = 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.

--

--