Segwit Wallet
Mix.install([
{:bitcoinlib, "~> 0.2.4"}
])
alias BitcoinLib.Key.{PrivateKey, PublicKey, PublicKeyHash, Address}
alias BitcoinLib.Key.HD.SeedPhrase
alias BitcoinLib.Transaction
alias BitcoinLib.Script
Notes
Based on Bip141
Trigger
Triggered when a scriptPubKey is a P2SH script, and the BIP16 redeemScript pushed in the scriptSig is exactly a push of a version byte plus a push of a witness program. The scriptSig must be exactly a push of the BIP16 redeemScript or validation fails. (“P2SH witness program”)
Witness program
The version byte is 0
, and the witness program is 20 bytes
:
-
It is interpreted as a pay-to-witness-public-key-hash (
P2WPKH
) program. -
The witness must consist of exactly
2 items
(≤ 520 bytes each). The first one asignature
, and the second one apublic key
. -
The
HASH160 of the public
key must match the20-byte witness program
. After normal script evaluation, the signature is verified against the public key with CHECKSIG operation. The verification must result in a single TRUE on the stack.
About the version byte
If the version byte is 0
, but the witness program is neither 20 nor 32 bytes
, the script must fail.
If the version byte is 1 to 16
, no further interpretation
of the witness program or witness stack happens, and there is no size restriction for the witness stack
. These versions are reserved for future extensions.
Example
The following example is the same P2WPKH, but nested in a BIP16 P2SH output.
witness:
scriptSig: <0 <20-byte-key-hash>>
(0x160014{20-byte-key-hash})
scriptPubKey: HASH160 <20-byte-script-hash> EQUAL
(0xA914{20-byte-script-hash}87)
Keys creation
🎲 Create a seed phrase from dice rolls.
To do so, just throw 50 dice, and make a string of 50 chars containing the results, so it can be converted into a 12 words seed phrase
.
{:ok, seed_phrase} =
SeedPhrase.from_dice_rolls("12345612345612345612345612345612345612345612345612")
🔑 Derive the master private key
from the seed phrase.
master_private_key = PrivateKey.from_seed_phrase(seed_phrase)
Derive the receive private key
and extract a P2SH public key hash
that will be useful later.
To keep things simple, just know that it’s the 44'
in the derivation path that makes it a P2PKH
transaction. This is a bit outside of the exercise’s scope, so let’s continue.
receive_private_key =
master_private_key
|> PrivateKey.from_derivation_path!("m/49'/1'/0'/0/0")
receive_public_key_hash =
receive_private_key
|> PublicKey.from_private_key()
|> PublicKeyHash.from_public_key()
Convert the public key hash
to an address
, which will be needed by faucets to receive funds
receive_address = Address.from_script_hash(receive_public_key_hash, :testnet)
Request some funds
👁️ Monitor the address
Go to mempool.space
by directly forging the URL with the address and wait transactions being confirmed.
Ex: https://mempool.space/testnet/address/2MzhhVCh8GvuXyzbc7wJae9WbYyCH9CUytY
Now leave that browser tab lying around. Mempool is supposed to issue a cashier bell sound 🔔 when it detects new incoming transactions to that address. When funds become spendable, you’ll also hear somewhat of a magic wand sound. 🪄
🚰 Request some testnet Bitcoin from faucets
Go to a Bitcoin Testnet faucet and use the above address to request funds… any amount would do, even tiny fractions.
- https://coinfaucet.eu/en/btc-testnet/
- https://bitcoinfaucet.uo1.net/
- https://testnet.qc.to/
- https://onchain.io/bitcoin-testnet-faucet
Here is an example of 10000 testnet sats, or 0.0001 tBTC, being ready to spend with 14 confirmations.
Examine transactions
Click on the transaction ID.
There is a Inputs & Outputs
section, with inputs on the left and outputs in the right… here, our address is the first output. In computer science, 0
represents the first value, and we associate it to vout
. This will be useful later to point to the right output inside the transaction, which happens to have two here.
Clicking on the Details
button reveals this:
Here lies the script pub key
, also known as the locking script
. This is what prevents funds from being spent, unless it is being used with the right parameters. That is exactly what’s going to happen later.
The following code is defining variables containing what we just talked about.
transaction_id = "420c552d7821da5da61be91dffe984537f460dc668f0766f6c2e1c7a10287610"
vout = 0
script_pub_key = "76a914b3ceddcbe5fa8aea1f672a4e543e57a49da179e388ac"
Extract parameters
The above script pub key
is locking what we call a UTXO which stands for Unspent Transaction Output. The sum of all UTXOs in a wallet is what can be considered as the balance.
Spending from a UTXO invalidates it, creating new ones in the process.
The current UTXO contains 10000
sats. We intend to send 5000
sats to a new address. This leaves 5000
sats remaining. They must end up in two other destinations: the change
and the transaction fee
. The change is another UTXO and we’ll keep 4000
sats. The fee is what’s leftover and the miner who will include the transaction in a new block will keep it. It is thus implied from the three previous amounts:
original_amount = 10000
destination_amount = 5000
change_amount = 4000
fee = original_amount - destination_amount - change_amount
Let’s create two public key hashes: a destination
and a change
.
destination_public_key_hash =
master_private_key
|> PrivateKey.from_derivation_path!("m/44'/1'/0'/0/1")
|> PublicKey.from_private_key()
|> PublicKeyHash.from_public_key()
change_public_key_hash =
master_private_key
|> PrivateKey.from_derivation_path!("m/44'/1'/0'/1/1")
|> PublicKey.from_private_key()
|> PublicKeyHash.from_public_key()
Create a transaction
Now we’ve got everything we need to create a new transaction and sign it. Let’s kill two birds in one shot. The following code will create a transaction specification and sign it with the help of the private key.
%Transaction.Spec{}
|> Transaction.Spec.add_input(
txid: transaction_id,
vout: vout,
redeem_script: script_pub_key
)
|> Transaction.Spec.add_output(
Script.Types.P2pkh.create(destination_public_key_hash),
destination_amount
)
|> Transaction.Spec.add_output(
Script.Types.P2pkh.create(change_public_key_hash),
change_amount
)
|> Transaction.Spec.sign_and_encode(receive_private_key)
Move funds
Now that we’ve got an encoded transaction, it is time to include it in the blockchain. To do so, we can use mempool.space
again by inserting it in this form: https://mempool.space/testnet/tx/push.
In the present context, it returned 05517750a78fb8c38346b1bf5908d71abe728811b643105be6595e11a9392373
. This is the transaction that spends the fund and can be seen on the transaction explorer
Conclusion
From now on, the previous 10000 sats transaction is considered as spent and can no longer be called a UTXO. Two new ones have been created:
- The destination containing 5000 sats
- The change containing 4000 sats
The remaining 1000 sats have been donated to the miner. It can be seen on mempool.space
in the header of the transaction screen.