Context
Some programmers adhere to the don’t repeat yourself principle. I ignored that rule in every part of this assignment. In this code I construct some Bitcoin transactions from different sets of byte arrays. Some were signatures, others were hashes, all of them made me feel like I was in hell. The result of my code was a transaction that recorded my name in the Chaincode Signet blockchain. My name is sadly recorded as rOB instead of ROB in ASCII. This was the week 2 exercise for the Chaincode: Start Your Career in FOSS program.
#![allow(unused)]
extern crate balance;
extern crate bitcoincore_rpc;
use balance::{OutgoingTx, WalletState};
use bitcoincore_rpc::bitcoin::{locktime, sighash};
use bitcoincore_rpc::{Auth, Client, RpcApi};
use bs58;
use hex;
use hmac_sha512::HMAC;
use num_bigint::BigUint; use ripemd::digest::{FixedOutput, Update};
use ripemd::digest::crypto_common::KeyInit;
// for modulus math on large numbers
use ripemd::{Ripemd160, Ripemd160Core};
use secp256k1::hashes::sha256;
use secp256k1::{Secp256k1, SecretKey, PublicKey, Message};
use sha2::{Sha256, Digest};
use std::borrow::Borrow;
use std::error::Error;
use std::hash::Hash;
use std::io::{Read, Write};
use std::ops::Add;
use std::path::PathBuf;
use std::str;
#[derive(Debug)]
pub enum SpendError {
MissingCodeCantRun,
// Add more relevant error variants
}
pub struct Utxo {
script_pubkey: Vec<u8>,
amount: u32,
}
pub struct Outpoint {
txid: [u8; 32],
index: u32,
}
// Given 2 compressed public keys as byte arrays, construct
// a 2-of-2 multisig output script. No length byte prefix is necessary.
fn create_multisig_script(keys: Vec<Vec<u8>>) -> Vec<u8> {
let pk1 = &keys[0];
let pk2 = &keys[1];
let mut script: Vec<u8> = vec![];
script.push(0x52); // PUSH 2
let pk_1_size = pk1.len() as u8;
script.push(pk_1_size);
script.extend(pk1.iter());
let pk_2_size = pk2.len() as u8;
script.push(pk_2_size);
script.extend(pk2.iter());
script.push(0x52); // PUSH 2
script.push(0xae); // check multisig
return script;
}
// Given an output script as a byte array, compute the p2wsh witness program
// This is a segwit version 0 pay-to-script-hash witness program.
// https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#p2wsh
fn get_p2wsh_program(script: &[u8], version: Option<u32>) -> Vec<u8> {
let script_hash = single_hash(script.to_vec());
if let Some(version) = version {
let mut p2wsh: Vec<u8> = vec![version.try_into().unwrap(), 0x20];
p2wsh.extend(script_hash.iter());
return p2wsh
} else {
let mut p2wsh = vec![0, 0x20];
p2wsh.extend(script_hash.iter());
return p2wsh
}
}
// Given an outpoint, return a serialized transaction input spending it
// Use hard-coded defaults for sequence and scriptSig
fn input_from_utxo(txid: &[u8], index: u32) -> Vec<u8> {
let mut input: Vec<u8> = vec![];
let position = index.to_le_bytes();
input.extend(txid.iter());
input.extend(position.iter());
input.push(0x00);
let seq = 0xffffffffu32.to_be_bytes();
input.extend(seq.iter());
// println!("Transaciton Input: {:?}", hex::encode(input.clone()));
return input
}
// Given an output script and value (in satoshis), return a serialized transaction output
fn output_from_options(script: &[u8], value: u32) -> Vec<u8> {
let mut ser_output: Vec<u8> = vec![];
let sats = value.to_le_bytes();
let pad = 0u32.to_be_bytes();
let l = script.len() as u8;
ser_output.extend(sats.iter());
ser_output.extend(pad.iter());
ser_output.push(l);
ser_output.extend(script.iter());
// println!("Output: {:?}", hex::encode(ser_output.clone()));
return ser_output
}
// Given a Utxo object, extract the public key hash from the output script
// and assemble the p2wpkh scriptcode as defined in BIP143
// <script length> OP_DUP OP_HASH160 <pubkey hash> OP_EQUALVERIFY OP_CHECKSIG
// https://github.com/bitcoin/bips/blob/master/bip-0143.mediawiki#specification
fn get_p2wpkh_scriptcode(utxo: Utxo) -> Vec<u8> {
let mut script_code: Vec<u8> = vec![];
let first_half: [u8; 4] = 0x1976a914u32.to_be_bytes();
script_code.extend(first_half.iter());
// println!("Script Code Prefix {:?}", hex::encode(first_half.clone()));
let pkh = &utxo.script_pubkey[2..];
// println!("Public Key Hash in Script Code: {:?}", hex::encode(pkh.clone()));
script_code.extend(pkh.iter());
script_code.push(0x88);
script_code.push(0xac);
return script_code
}
// Compute the commitment hash for a single input and return bytes to sign.
// This implements the BIP 143 transaction digest algorithm
// https://github.com/bitcoin/bips/blob/master/bip-0143.mediawiki#specification
// We assume only a single input and two outputs,
// as well as constant default values for sequence and locktime
fn get_commitment_hash(
outpoint: Outpoint,
scriptcode: &[u8],
value: u32,
outputs: Vec<Utxo>,
) -> Vec<u8> {
let mut commitment: Vec<u8> = vec![];
// println!("\n==========================\n");
// Version
let version = 2u32.to_le_bytes();
commitment.extend(version.iter()); //1
// println!("Version: {:?}", hex::encode(version));
// All TX input outpoints (only one in our case)
let mut input_preimage = vec![];
input_preimage.extend(outpoint.txid);
input_preimage.extend(outpoint.index.to_le_bytes());
let hash_prevouts = double_hash(input_preimage.clone());
commitment.extend(hash_prevouts.iter()); //2
// println!("Hash Prevouts: {:?}", hex::encode(hash_prevouts));
// All TX input sequences (only one for us, always default value)
let ser_input_seq = double_hash(0xffffffffu32.to_be_bytes().to_vec());
commitment.extend(ser_input_seq.iter()); //3
// println!("Hash Sequence: {:?}", hex::encode(ser_input_seq));
// Single outpoint being spent
commitment.extend(input_preimage.iter()); // 4
// println!("Outpoint: {:?}", hex::encode(input_preimage));
// Scriptcode (the scriptPubKey in/implied by the output being spent, see BIP 143)
commitment.extend(scriptcode.iter()); //5
// println!("Script code: {:?}", hex::encode(scriptcode));
// Value of output being spent
let amount = value.to_le_bytes();
let pad = 0u32.to_be_bytes();
commitment.extend(amount.iter()); //6
commitment.extend(pad.iter());
// print!("Value: {:?}", hex::encode(amount));
// println!("{:?}", hex::encode(pad));
// Sequence of output being spent (always default for us)
let n_seq = 0xffffffffu32.to_be_bytes();
commitment.extend(n_seq.iter()); //7
// println!("nSequence: {:?}", hex::encode(n_seq));
// All TX outputs
let mut output_preimage: Vec<u8> = vec![];
for output in outputs {
let amount = output.amount.to_le_bytes();
let pad = 0u32.to_be_bytes();
output_preimage.extend(amount.iter());
output_preimage.extend(pad.iter());
let output_script_len = output.script_pubkey.len() as u8;
output_preimage.push(output_script_len);
output_preimage.extend(output.script_pubkey.iter());
}
// println!("Output Preimage (???): {:?}", hex::encode(output_preimage.clone()));
let output_hash = double_hash(output_preimage);
commitment.extend(output_hash.iter()); //8
// Locktime (always default for us)
let locktime = 0x00000000u32.to_be_bytes();
commitment.extend(locktime.iter()); //9
// println!("Locktime: {:?}", hex::encode(locktime));
// SIGHASH_ALL (always default for us)
let sighash = 1u32.to_le_bytes();
commitment.extend(sighash.iter()); //10
// println!("Sighash: {:?}", hex::encode(sighash));
// println!("\nCommitment: {:?}", hex::encode(commitment.clone()));
let d256 = double_hash(commitment);
// println!("\nHash256: {:}\n", hex::encode(d256.clone()));
return d256
}
// Given a JSON utxo object and a list of all of our wallet's witness programs,
// return the index of the derived key that can spend the coin.
// This index should match the corresponding private key in our wallet's list.
fn get_key_index(utxo: Utxo, programs: Vec<&str>) -> u32 {
for (index, program) in programs.iter().enumerate() {
if hex::decode(program).expect("Could not decode program").eq(&utxo.script_pubkey) {
return index as u32
}
}
return 0
}
// Given a private key and message digest as bytes, compute the ECDSA signature.
// Bitcoin signatures:
// - Must be strict-DER encoded
// - Must have the SIGHASH_ALL byte (0x01) appended
// - Must have a low s value as defined by BIP 62:
// https://github.com/bitcoin/bips/blob/master/bip-0062.mediawiki#user-content-Low_S_values_in_signatures
fn sign(privkey: &[u8; 32], msg: Vec<u8>) -> Vec<u8> {
// Keep signing until we produce a signature with "low s value"
// We will have to decode the DER-encoded signature and extract the s value to check it
// Format: 0x30 [total-length] 0x02 [R-length] [R] 0x02 [S-length] [S] [sighash]
let secp = Secp256k1::new();
let k = SecretKey::from_slice(privkey).expect("Could not conform to SecretKey");
let message = Message::from_digest_slice(&msg).expect("Could not produce Message");
let mut sig = secp.sign_ecdsa(&message, &k);
sig.normalize_s();
let der = sig.serialize_der();
let mut strict_der_sig: Vec<u8> = vec![];
strict_der_sig.extend(der.to_vec().iter());
strict_der_sig.push(0x01);
return strict_der_sig
}
// Given a private key and transaction commitment hash to sign,
// compute the signature and assemble the serialized p2pkh witness
// as defined in BIP 141 (2 stack items: signature, compressed public key)
// https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#specification
fn get_p2wpkh_witness(privkey: &[u8; 32], msg: Vec<u8>) -> Vec<u8> {
let mut witness: Vec<u8> = vec![];
let sig = sign(privkey, msg);
let sig_size = sig.len() as u8;
// println!("Signature Size: {:?}", hex::encode([sig_size]));
witness.push(sig_size);
// println!("Signature: {:?}", hex::encode(sig.clone()));
witness.extend(sig.iter());
let pk = balance::derive_public_key_from_private(privkey);
let pub_key_size = pk.len() as u8;
// println!("Public Key Size: {:?}", hex::encode([pub_key_size]));
witness.push(pub_key_size);
// println!("Witness Public Key: {:?}", hex::encode(pk.clone()));
witness.extend(pk.iter());
return witness
}
// Given two private keys and a transaction commitment hash to sign,
// compute both signatures and assemble the serialized p2pkh witness
// as defined in BIP 141
// Remember to add a 0x00 byte as the first witness element for CHECKMULTISIG bug
// https://github.com/bitcoin/bips/blob/master/bip-0147.mediawiki
fn get_p2wsh_witness(privs: Vec<&[u8; 32]>, msg: Vec<u8>) -> Vec<u8> {
//probably wrong
let mut witness: Vec<u8> = vec![];
// witness.push(0x01);
witness.push(0x00);
let sig_1 = sign(privs[0], msg.clone());
let sig_1_len = sig_1.len() as u8;
witness.push(sig_1_len);
witness.extend(sig_1.iter());
let pk_1 = balance::derive_public_key_from_private(privs[0]);
let sig_2 = sign(privs[1], msg.clone());
let sig_2_len = sig_2.len() as u8;
witness.push(sig_2_len);
let pk_2 = balance::derive_public_key_from_private(privs[1]);
witness.extend(sig_2.iter());
let remaining_script = create_multisig_script(vec![pk_1.to_vec(), pk_2.to_vec()]);
let raw_script_len = remaining_script.len() as u8;
witness.push(raw_script_len);
witness.extend(remaining_script.iter());
return witness;
}
// Given arrays of inputs, outputs, and witnesses, assemble the complete
// transaction and serialize it for broadcast. Return bytes as hex-encoded string
// suitable to broadcast with Bitcoin Core RPC.
// https://en.bitcoin.it/wiki/Protocol_documentation#tx
fn assemble_transaction(
inputs: Vec<Vec<u8>>,
outputs: Vec<Utxo>,
witnesses: Vec<Vec<u8>>,
is_p2sh: bool,
) -> Vec<u8> {
let mut transaction: Vec<u8> = vec![];
let v1 = 2u32.to_le_bytes();
// println!("\n======================\n");
// println!("Version: {:?}", hex::encode(v1.clone()));
transaction.extend(v1.iter());
transaction.push(0x00);
transaction.push(0x01); // for witness
// println!("Marker and Flag: {:?}", hex::encode([0x00, 0x01]));
transaction.push(0x01); // num ins
// println!("Number of Inputs: {:?}", hex::encode([0x01]));
let first_only_input = &inputs[0];
transaction.extend(first_only_input.iter()); // first input ser
// println!("First Input: {:?}", hex::encode(first_only_input));
transaction.push(0x02); // num outs
// println!("Number of Outputs: {:?}", hex::encode([0x02]));
for output in outputs {
let out_ser = output_from_options(&output.script_pubkey, output.amount);
transaction.extend(out_ser.iter());
}
if is_p2sh {
transaction.push(0x04);
} else {
transaction.push(0x02);
}
// println!("Number of Witness Items: {:?}", hex::encode([0x02]));
// i hope this works :0
for witness in witnesses {
// let witness_len = witness.len() as u8;
// println!("Witness: {:?}", hex::encode(witness.clone()));
transaction.extend(witness.iter());
}
let locktime = 0x00000000u32.to_be_bytes();
// println!("Locktime: {:?}", hex::encode(locktime.clone()));
transaction.extend(locktime.iter());
return transaction;
}
// Given arrays of inputs and outputs (no witnesses!) compute the txid.
// Return the 32 byte txid as a *reversed* hex-encoded string.
// https://developer.bitcoin.org/reference/transactions.html#raw-transaction-format
fn get_txid(inputs: Vec<Vec<u8>>, outputs: Vec<Utxo>) -> [u8; 32] {
let mut preimage: Vec<u8> = vec![];
let v1 = 2u32.to_le_bytes();
preimage.extend(v1.iter());
let input_len = inputs.len() as u8;
preimage.push(input_len);
for input in inputs {
preimage.extend(input.iter());
}
let output_len = outputs.len() as u8;
preimage.push(output_len);
for output in outputs {
let ser_out = output_from_options(&output.script_pubkey, output.amount);
preimage.extend(ser_out.iter());
}
let lock_time = 0x00000000u32.to_be_bytes();
preimage.extend(lock_time.iter());
let mut internal = double_hash(preimage);
internal.reverse();
let txid_try: Result<[u8; 32], _> = internal.try_into();
let txid = txid_try.unwrap();
return txid
}
// Spend a p2wpkh utxo to a 2 of 2 multisig p2wsh and return the (txid, transaction) tupple
pub fn spend_p2wpkh(wallet_state: &WalletState) -> Result<([u8; 32], Vec<u8>), SpendError> {
// FEE = 1000
// AMT = 1000000
// Choose an unspent coin worth more than 0.01 BTC
let utxos = &wallet_state.utxos;
let mut selected_outpoint: Option<&OutgoingTx> = None;
for utxo in utxos {
if utxo.sats > 1_000_000 {
selected_outpoint = Some(utxo);
break;
}
}
// Create the input from the utxo
let outpoint = selected_outpoint.unwrap();
// println!("Input SPK: {:?}", hex::encode(outpoint.spk.clone()));
// Reverse the txid hash so it's little-endian
let input_external_txid = &outpoint.pointer;
let mut input_external_txid_bytes = hex::decode(input_external_txid).expect("Could not decode Input TXID.");
input_external_txid_bytes.reverse();
let attempt_internal: Result<[u8; 32], _> = input_external_txid_bytes.try_into();
let internal_repr = attempt_internal.expect("Could not fit TX hash to 32 bytes");
let input = input_from_utxo(&internal_repr, outpoint.out_point_index);
// Compute destination output script and output
let pk1 = &wallet_state.public_keys[0];
let pk2 = &wallet_state.public_keys[1];
let raw_script = create_multisig_script(vec![pk1.to_vec(), pk2.to_vec()]);
let multisig_spk = get_p2wsh_program(&raw_script, None);
// Compute change output script and output
let change = (outpoint.sats - 1_000_000 - 1_000) as u32;
let change_utxo_output = Utxo { script_pubkey: outpoint.spk.clone(), amount: change as u32 };
let c_hash_output = Outpoint { txid: internal_repr, index: outpoint.out_point_index };
let txo = Utxo { script_pubkey: outpoint.spk.clone(), amount: change as u32 };
let scriptcode = get_p2wpkh_scriptcode(txo);
let commited_outputs = vec![Utxo { script_pubkey: multisig_spk.clone(), amount: 1_000_000 },
change_utxo_output];
let commit_hash = get_commitment_hash(c_hash_output, &scriptcode, outpoint.sats as u32, commited_outputs);
// Fetch the private key we need to sign with
let mut index_to_take: usize = 0;
for (index, script) in wallet_state.witness_programs.iter().enumerate() {
if outpoint.spk.eq(script) { break; }
index_to_take = index_to_take + 1;
}
let priv_key = wallet_state.private_keys[index_to_take];
let found_pk = balance::derive_public_key_from_private(&priv_key.clone());
// println!("Found Private Key: {:?}", hex::encode(priv_key.clone()));
// println!("Found Public Key: {:?}", hex::encode(found_pk.clone()));
// println!("Confirming Witness Pubkey Matches: {:?}", hex::encode(balance::get_p2wpkh_program(found_pk)));
// Sign!
let p2pkh_witness = get_p2wpkh_witness(&priv_key, commit_hash);
// Assemble
let multisig_utxo_output = Utxo { script_pubkey: multisig_spk.clone(), amount: 1_000_000 };
let change_utxo_output = Utxo { script_pubkey: outpoint.spk.clone(), amount: change as u32 };
let rawtx = assemble_transaction(vec![input], vec![multisig_utxo_output, change_utxo_output], vec![p2pkh_witness], false);
// println!("Raw TX: {:?}\n\n", hex::encode(rawtx.clone()));
// Reserialize without witness data and double-SHA256 to get the txid
let input = input_from_utxo(&internal_repr, outpoint.out_point_index);
let multisig_utxo_output = Utxo { script_pubkey: multisig_spk.clone(), amount: 1_000_000 };
let change_utxo_output = Utxo { script_pubkey: outpoint.spk.clone(), amount: change as u32 };
let commited_outputs = vec![multisig_utxo_output, change_utxo_output];
let txid = get_txid(vec![input], commited_outputs);
// println!("{:?}", hex::encode(txid));
// For debugging you can use RPC `testmempoolaccept ["<final hex>"]` here
// return txid, final-tx
return Ok((txid, rawtx));
}
// Spend a 2-of-2 multisig p2wsh utxo and return the transaction
pub fn spend_p2wsh(wallet_state: &WalletState, txid: [u8; 32]) -> Result<([u8; 32], Vec<u8>), SpendError> {
// COIN_VALUE = 1000000
// FEE = 1000
// AMT = 0
// Create the input from the utxo
let pk1 = &wallet_state.public_keys[0];
let pk2 = &wallet_state.public_keys[1];
let raw_script = create_multisig_script(vec![pk1.to_vec(), pk2.to_vec()]);
let multisig_spk = get_p2wsh_program(&raw_script, None);
// Reverse the txid hash so it's little-endian
let mut input_external_txid_bytes = txid;
input_external_txid_bytes.reverse();
let attempt_internal: Result<[u8; 32], _> = input_external_txid_bytes.try_into();
let internal_repr = attempt_internal.expect("Could not fit TX hash to 32 bytes");
let tx_input = input_from_utxo(&internal_repr, 0);
// Compute destination output script and output
let mut op_return_spk = vec![];
op_return_spk.push(0x6a); // OP_RETURN
op_return_spk.push(0x03);
op_return_spk.push(0x72);
op_return_spk.push(0x4f);
op_return_spk.push(0x42);
let op_return_utxo = Utxo { script_pubkey: op_return_spk, amount: 0 };
// Compute change output script and output
let change_spk = balance::get_p2wpkh_program(pk1.clone());
let change_utxo = Utxo { script_pubkey: change_spk.to_vec(), amount: 1_000_000 - 1_000 };
// Get the message to sign
let mut scriptcode: Vec<u8> = vec![];
let output_l = raw_script.len() as u8;
scriptcode.push(output_l);
scriptcode.extend(raw_script.clone().iter());
let commitment = get_commitment_hash(Outpoint { txid: internal_repr, index: 0 }, &scriptcode, 1_000_000, vec![op_return_utxo, change_utxo]);
// Sign!
let priv_key_1 = wallet_state.private_keys[0];
let priv_key_2 = wallet_state.private_keys[1];
let witness = get_p2wsh_witness(vec![&priv_key_1, &priv_key_2], commitment);
// Assemble
let mut op_return_spk = vec![];
op_return_spk.push(0x6a); // OP_RETURN
op_return_spk.push(0x03);
op_return_spk.push(0x72);
op_return_spk.push(0x4f);
op_return_spk.push(0x42);
let op_return_utxo = Utxo { script_pubkey: op_return_spk, amount: 0 };
let change_spk = balance::get_p2wpkh_program(pk1.clone());
let change_utxo = Utxo { script_pubkey: change_spk.to_vec(), amount: 1_000_000 - 1_000 };
let rawtx = assemble_transaction(vec![tx_input], vec![op_return_utxo, change_utxo], vec![witness], true);
// For debugging you can use RPC `testmempoolaccept ["<final hex>"]` here
// println!("{}", hex::encode(rawtx.clone()));
// return txid final-tx
let tx_input = input_from_utxo(&internal_repr, 0);
let mut op_return_spk = vec![];
op_return_spk.push(0x6a); // OP_RETURN
op_return_spk.push(0x03);
op_return_spk.push(0x72);
op_return_spk.push(0x4f);
op_return_spk.push(0x42);
let op_return_utxo = Utxo { script_pubkey: op_return_spk, amount: 0 };
let change_spk = balance::get_p2wpkh_program(pk1.clone());
let change_utxo = Utxo { script_pubkey: change_spk.to_vec(), amount: 1_000_000 - 1_000 };
let txid = get_txid(vec![tx_input], vec![op_return_utxo, change_utxo]);
// println!("{}", hex::encode(txid));
Ok((txid, rawtx))
}