Progress pill
Creation of Bitcoin Wallets

Address Derivation

Bitcoin Wallet Architecture

Address Derivation

  • Public Key Compression
  • Derivation of a SegWit v0 (bech32) address
  • Derivation of a SegWit v1 (bech32m) Address
Let's explore together how to generate a receiving address from a pair of keys located, for example, at depth 5 of an HD wallet. This address can then be used in a wallet software to lock a UTXO.
Since the process of generating an address depends on the script model adopted, let's focus on two specific cases: generating a SegWit v0 address in P2WPKH and a SegWit v1 address in P2TR. These two types of addresses cover the vast majority of uses today.

Public Key Compression

After performing all the derivation steps from the master key to depth 5 using the appropriate indexes, we obtain a pair of keys (, ) with . Although it is possible to use this public key as is to lock funds with the P2PK standard, that is not our goal here. Instead, we aim to create an address in P2WPKH in the first instance, and then in P2TR for another example.
The first step is to compress the public key . To understand this process well, let's first recall some fundamentals covered in part 3. A public key in Bitcoin is a point located on an elliptic curve. It is represented in the form , where and are the coordinates of the point. In its uncompressed form, this public key measures 520 bits: 8 bits for a prefix (initial value of 0x04), 256 bits for the coordinate, and 256 bits for the coordinate. However, elliptic curves have a symmetry property with respect to the x-axis: for a given coordinate, there are only two possible values for : and . These two points are located on either side of the x-axis. In other words, if we know , it is sufficient to specify whether is even or odd to identify the exact point on the curve.
To compress a public key, only is encoded, which occupies 256 bits, and a prefix is added to specify the parity of . This method reduces the size of the public key to 264 bits instead of the initial 520. The prefix 0x02 indicates that is even, and the prefix 0x03 indicates that is odd.
Let's take an example to understand well, with a raw public key in uncompressed representation:
K = 04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f
If we decompose this key, we have:
  • The prefix: 04;
  • : 678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb6;
  • : 49f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f
The last hexadecimal character of is f. In base 10, f = 15, which corresponds to an odd number. Therefore, is odd, and the prefix will be 0x03 to indicate this.
The compressed public key becomes:
K = 03678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb6
This operation applies to all script models based on ECDSA, that is, all except P2TR which uses Schnorr. In the case of Schnorr, as explained in part 3, we only retain the value of , without adding a prefix to indicate the parity of , unlike ECDSA. This is made possible by the fact that a unique parity is arbitrarily chosen for all keys. This allows for a slight reduction in the storage space required for public keys.

Derivation of a SegWit v0 (bech32) address

Now that we have obtained our compressed public key, we can derive a SegWit v0 receiving address from it.
The first step is to apply the HASH160 hash function to the compressed public key. HASH160 is a composition of two successive hash functions: SHA256, followed by RIPEMD160:
First, we pass the key through SHA256:
SHA256(K) = C489EBD66E4103B3C4B5EAFF462B92F5847CA2DCE0825F4997C7CF57DF35BF3A
Then we pass the result through RIPEMD160:
RIPEMD160(SHA256(K)) = 9F81322CC88622CA4CCB2A52A21E2888727AA535
We have obtained a 160-bit hash of the public key, which constitutes what is called the payload of the address. This payload represents the central and most important part of the address. It is also used in the scriptPubKey to lock the UTXOs.
However, to make this payload more easily usable by humans, metadata is added to it. The next step involves encoding this hash into groups of 5 bits in decimal. This decimal transformation will be useful for conversion into bech32, used by post-SegWit addresses. The 160-bit binary hash is thus divided into 32 groups of 5 bits:
So, we have:
HASH = 19 30 00 19 04 11 06 08 16 24 17 12 20 19 06 11 05 09 09 10 04 07 17 08 17 01 25 07 21 09 09 21
Once the hash is encoded into groups of 5 bits, a checksum is added to the address. This checksum is used to verify that the payload of the address has not been altered during its storage or transmission. For example, it allows a wallet software to ensure that you have not made a typo when entering a receiving address. Without this verification, you could accidentally send bitcoins to an incorrect address, resulting in a permanent loss of funds, as you do not own the associated public or private key. Therefore, the checksum is a protection against human errors.
For the old Bitcoin Legacy addresses, the checksum was simply calculated from the beginning of the address hash with the HASH256 function. With the introduction of SegWit and the bech32 format, BCH codes (Bose, Ray-Chaudhuri, and Hocquenghem) are now used. These error-correcting codes are used to detect and correct errors in data sequences. They ensure that the transmitted information arrives intact at its destination, even in the case of minor alterations. BCH codes are used in many fields, such as SSDs, DVDs, and QR codes. For example, thanks to these BCH codes, a partially obscured QR code can still be read and decoded.
In the context of Bitcoin, BCH codes offer a better compromise between size and error detection capability compared to the simple hash functions used for Legacy addresses. However, in Bitcoin, BCH codes are used only for error detection, not correction. Thus, wallet software will signal an incorrect receiving address but will not automatically correct it. This limitation is deliberate: allowing automatic correction would reduce the error detection capability.
To calculate the checksum with BCH codes, we need to prepare several elements.
  • The HRP (Human Readable Part): For the Bitcoin mainnet, the HRP is bc;
The HRP must be expanded by separating each character into two parts:
  • Taking the characters of the HRP in ASCII:
    • b: 01100010
    • c: 01100011
  • Extracting the 3 most significant bits and the 5 least significant bits:
    • 3 most significant bits: 011 (3 in decimal)
    • 3 most significant bits: 011 (3 in decimal)
    • 5 least significant bits: 00010 (2 in decimal)
    • 5 least significant bits: 00011 (3 in decimal)
With the separator 0 between the two characters, the HRP extension is therefore:
03 03 00 02 03
  • The witness version: For SegWit version 0, it's 00;
  • The payload: The decimal values of the public key hash;
  • The reservation for the checksum: We add 6 zeros [0, 0, 0, 0, 0, 0] at the end of the sequence.
All the data combined to input into the program to calculate the checksum are as follows:
HRP = 03 03 00 02 03 SEGWIT v0 = 00 HASH = 19 30 00 19 04 11 06 08 16 24 17 12 20 19 06 11 05 09 09 10 04 07 17 08 17 01 25 07 21 09 09 21 CHECKSUM = 00 00 00 00 00 00 INPUT = 03 03 00 02 03 00 19 30 00 19 04 11 06 08 16 24 17 12 20 19 06 11 05 09 09 10 04 07 17 08 17 01 25 07 21 09 09 21 00 00 00 00 00 00
The calculation of the checksum is quite complex. It involves polynomial finite field arithmetic. We will not detail this calculation here and will move directly to the result. In our example, the checksum obtained in decimal is:
10 16 11 04 13 18
We can now construct the receiving address by concatenating in order the following elements:
  • The SegWit version: 00
  • The payload: The public key hash
  • The checksum: The values obtained in the previous step (10 16 11 04 13 18)
This gives us in decimal:
00 19 30 00 19 04 11 06 08 16 24 17 12 20 19 06 11 05 09 09 10 04 07 17 08 17 01 25 07 21 09 09 21 10 16 11 04 13 18
Then, each decimal value must be mapped to its bech32 character using the following conversion table:
To convert a value into a bech32 character using this table, simply locate the values in the first column and the first row which, when added together, yield the desired result. Then, retrieve the corresponding character. For example, the decimal number 19 will be converted into the letter n, because .
By mapping all our values, we obtain the following address:
qn7qnytxgsc3v5nxt9ff2y83g3pe84ff42stydj
All that remains is to add the HRP bc, which indicates that it is an address for the Bitcoin mainnet, as well as the separator 1, to obtain the complete receiving address:
bc1qn7qnytxgsc3v5nxt9ff2y83g3pe84ff42stydj
The particularity of this bech32 alphabet is that it includes all alphanumeric characters except for 1, b, i, and o to avoid visual confusion between similar characters, especially during their entry or reading by humans.
To summarize, here is the derivation process:
This is how to derive a P2WPKH (SegWit v0) receiving address from a pair of keys. Let's now move on to P2TR (SegWit v1 / Taproot) addresses and discover their generation process.

Derivation of a SegWit v1 (bech32m) Address

For Taproot addresses, the generation process differs slightly. Let's look at this together!
From the step of public key compression, a first distinction appears compared to ECDSA: the public keys used for Schnorr in Bitcoin are represented only by their abscissa (). Therefore, there is no prefix, and the compressed key measures exactly 256 bits. As we saw in the previous chapter, a P2TR script locks bitcoins on a unique Schnorr public key, designated by . This key is an aggregate of two public keys: , a main internal public key, and , a public key derived from the Merkle root of a list of scriptPubKey. The bitcoins locked with this type of script can be spent in two ways:
  • By publishing a signature for the public key (key path);
  • By satisfying one of the scripts included in the Merkle tree (script path).
In reality, these two keys are not truly "aggregated." The key is instead tweaked by the key . In cryptography, to "tweak" a public key means to modify this key by applying an additive value called a "tweak." This operation allows the modified key to remain compatible with the original private key and the tweak. Technically, a tweak is a scalar value that is added to the initial public key. If is the original public key, the tweaked key becomes:
Where is the generator of the elliptic curve used. This operation produces a new public key derived from the original key, while retaining cryptographic properties allowing its use.
If you do not need to add alternative scripts (spending exclusively via the key path), you can generate a Taproot address established solely on the public key present at depth 5 of your wallet. In this case, it is necessary to create a non-spendable script for the script path, in order to satisfy the requirements of the structure. The tweak is then calculated by applying a tagged hash function, TapTweak, on the internal public key :
where:
  • is a SHA256 hash function tagged with the tag TapTweak. If you are not familiar with what a tagged hash function is, I invite you to consult chapter 3.3;
  • is the internal public key, represented in its compressed 256-bit format, using only the coordinate.
The Taproot public key is then calculated by adding the tweak , multiplied by the elliptic curve generator , to the internal public key :
Once the Taproot public key is obtained, we can generate the corresponding receiving address. Unlike other formats, Taproot addresses are not established on a hash of the public key. Therefore, the key is inserted directly into the address, in a raw manner.
To begin, we extract the coordinate of the point to obtain a compressed public key. On this payload, a checksum is calculated using BCH codes, as with SegWit v0 addresses. However, the program used for Taproot addresses differs slightly. Indeed, after the introduction of the bech32 format with SegWit, a bug was discovered: when the last character of an address is a p, inserting or removing qs just before this p does not make the checksum invalid. Although this bug does not have consequences on SegWit v0 (thanks to a size constraint), it could pose a problem in the future. This bug has therefore been corrected for Taproot addresses, and the new corrected format is called "bech32m".
The Taproot address is generated by encoding the coordinate of in the bech32m format, with the following elements:
  • The HRP (Human Readable Part): bc, to indicate the main Bitcoin network;
  • The version: 1 to indicate Taproot / SegWit v1;
  • The checksum.
The final address will therefore have the format:
bc1p[Qx][checksum]
On the other hand, if you wish to add alternative scripts in addition to spending with the internal public key (script path), the calculation of the receiving address will be slightly different. You will need to include the hash of the alternative scripts in the calculation of the tweak. In Taproot, each alternative script, located at the end of the Merkle tree, is called a "leaf".
Once the different alternative scripts are written, you must pass them individually through a tagged hash function TapLeaf, accompanied by some metadata:
With:
  • : the script version number (default 0xC0 for Taproot);
  • : the size of the script encoded in CompactSize format;
  • : the script.
The different script hashes () are first sorted in lexicographical order. Then, they are concatenated in pairs and passed through a tagged hash function TapBranch. This process is repeated iteratively to build, step by step, the Merkle tree:
We then continue by concatenating the results two by two, passing them at each step through the tagged hash function TapBranch, until we obtain the Merkle tree root:
Once the Merkle root is calculated, we can calculate the tweak. For this, we concatenate the internal public key of the wallet with the root , and then pass the whole through the tagged hash function TapTweak:
Finally, as before, the Taproot public key is obtained by adding the internal public key to the product of the tweak by the generator point :
Then, the generation of the address follows the same process, using the raw public key as the payload, accompanied by some additional metadata.
And there you have it! We have reached the end of this CYP201 course. If you found this course helpful, I would be very grateful if you could take a few moments to give it a good rating in the following evaluation chapter. Feel free to also share it with your loved ones or on your social networks. Finally, if you wish to obtain your diploma for this course, you can take the final exam right after the evaluation chapter.
Quiz
Quiz1/5
What operation is necessary to calculate a Taproot public key Q?