Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Segwit Wallet

livebooks/segwit_wallet/index.livemd

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 a signature, and the second one a public key.
  • The HASH160 of the public key must match the 20-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

Source

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.

Here is an example of 10000 testnet sats, or 0.0001 tBTC, being ready to spend with 14 confirmations.

confirmed transaction

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:

transaction details

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.