From 396d32353f17d617cfdc7f7f6daf93495fb5a432 Mon Sep 17 00:00:00 2001 From: Gregory Sanders Date: Wed, 23 Oct 2019 10:17:51 -0400 Subject: [PATCH] Add proposal Liquid tapscript case study --- 3.2-liquid-tapscript-case-study.ipynb | 491 ++++++++++++++++++++++++++ 1 file changed, 491 insertions(+) create mode 100644 3.2-liquid-tapscript-case-study.ipynb diff --git a/3.2-liquid-tapscript-case-study.ipynb b/3.2-liquid-tapscript-case-study.ipynb new file mode 100644 index 000000000..7b8a26ced --- /dev/null +++ b/3.2-liquid-tapscript-case-study.ipynb @@ -0,0 +1,491 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3.2 Liquid Tapscript Case Study\n", + "\n", + "Similar to multisig case study in 3.1:\n", + "\n", + "* n-of-n MuSig signing policy on outer taproot pubkey\n", + "* Taptree, with n-choose-k k-of-k MuSig pubkey tapleaves, weighted greedily to reduce expected witness depth.\n", + "* One additional f-of-e checksigadd script tapleaf, behind CSV policy of 1 day\n", + "\n", + "First, let's import everything we need and set parameters to your liking:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Parameters to play with \n", + "\n", + "k = 3 # threshold for regular keys\n", + "n = 5 # regular keys\n", + "e = 3 # emergency keys\n", + "f = 2 # threshold for emergency keys\n", + "\n", + "import hashlib\n", + "from io import BytesIO\n", + "\n", + "import util\n", + "from test_framework.address import program_to_witness\n", + "from test_framework.key import ECKey, ECPubKey, generate_key_pair, generate_schnorr_nonce\n", + "from test_framework.messages import CTransaction, COutPoint, CTxIn, CTxOut, CScriptWitness, CTxInWitness, ser_string, sha256\n", + "from test_framework.script import TapTree, TapLeaf, Node, CScript, OP_1, TaprootSignatureHash, OP_CHECKSIG, SIGHASH_ALL_TAPROOT\n", + "from test_framework.util import assert_equal\n", + "from test_framework.musig import generate_musig_key, aggregate_schnorr_nonces, sign_musig, aggregate_musig_signatures\n", + "\n", + "import random\n", + "from itertools import combinations" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Next, we will generate all the \"base\" untweaked keys that each of the n signing participants will use, combine them to generate the \"inner\" MuSig key, along with the associated tweaked keys that are put into the MuSig." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Generate all n inner keypairs\n", + "priv_keys_base = []\n", + "pub_keys_base = []\n", + "for i in range(n):\n", + " privkey, pubkey = generate_key_pair()\n", + " priv_keys_base.append(privkey)\n", + " pub_keys_base.append(pubkey)\n", + "\n", + "print(\"Create MuSig inner key\")\n", + "c_map, inner_pubkey = generate_musig_key(pub_keys_base)\n", + "\n", + "print(\"Apply tweaks to both priv and public parts\")\n", + "priv_keys_tweaked = []\n", + "pub_keys_tweaked = []\n", + "for pubkey, privkey in zip(pub_keys_base, priv_keys_base):\n", + " priv_keys_tweaked.append(privkey.mul(c_map[pubkey]))\n", + " pub_keys_tweaked.append(pubkey.mul(c_map[pubkey]))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Before making the Taproot output pubkey, we need to generate our taptree, starting with the k-of-k MuSig tapscript-based leaves:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "tapscript_leaves = []\n", + "tapscript_leaf_signers = []\n", + "leaf_tweaks = []\n", + "tapscript_leaf_pubkey = []\n", + "\n", + "print(\"Making k-of-k taptree leaves\")\n", + "counts = [0]*((2**n))\n", + "\n", + "# This counts the number of times a specific subset\n", + "# would be chosen canonically to sign from the given\n", + "# subsets of signers that are up, over every subset\n", + "# combination. We later use these as weights to prioritize\n", + "# placement in the tree.\n", + "# TODO: sort the pubkeys lexicographically\n", + "for i in range(len(counts)):\n", + " avail = i\n", + " ones = bin(avail).count(\"1\")\n", + " while ones > k:\n", + " avail &= (avail-1)\n", + " ones -= 1\n", + " counts[avail] += 1\n", + "\n", + "for i, weight in enumerate(counts):\n", + " # We only captured values exactly at k\n", + " # signing participants, and only want those\n", + " if bin(i).count(\"1\") != k:\n", + " continue\n", + " subset = []\n", + " subset_index = []\n", + " # pad out binary string for this value\n", + " i_str = format(i, '#0'+str(n+2)+'b')[2:]\n", + " for j, binary in enumerate(i_str):\n", + " if binary == '1':\n", + " subset.append(pub_keys_base[j])\n", + " subset_index.append(j)\n", + "\n", + " c_map, tapscript_pubkey = generate_musig_key(subset)\n", + " leaf_tweaks.append(c_map)\n", + " pk_tapscript = TapLeaf()\n", + " pk_tapscript.construct_pk(tapscript_pubkey)\n", + " tapscript_leaves.append((weight, pk_tapscript)) # try squaring the weight value etc to change tree\n", + " tapscript_leaf_signers.append(subset_index)\n", + " tapscript_leaf_pubkey.append(tapscript_pubkey)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And finally the emergency 2-of-3 condition:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(\"Generate emergency keys\")\n", + "emergency_privkeys = []\n", + "emergency_pubkeys = []\n", + "for i in range(e):\n", + " privkey, pubkey = generate_key_pair()\n", + " emergency_privkeys.append(privkey)\n", + " emergency_pubkeys.append(pubkey)\n", + "\n", + "print(\"Generate taptree emergency spending condition\")\n", + "csa_tapscript = TapLeaf()\n", + "csv_delay = 144 # One day, just to make this test fast\n", + "csa_tapscript.construct_csaolder(f, emergency_pubkeys, csv_delay)\n", + "tapscript_leaves.append((0 ,csa_tapscript)) # Low priority, \"should never happen\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Compute the taptree root, then the Taproot output scriptPubKey itself:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(\"Generate taptree root\")\n", + "taptree = TapTree()\n", + "taptree.key = inner_pubkey\n", + "taptree.huffman_constructor(tapscript_leaves)\n", + "print(\"final taptree descriptor: {}\\n\".format(taptree.desc))\n", + "\n", + "# Tweak inner_pubkey by taptree root to create outer taproot key\n", + "taproot_script, tapscript_tweak, control_map = taptree.construct()\n", + "print(taproot_script.hex())\n", + "\n", + "program = bytes(taproot_script[2:])\n", + "address = program_to_witness(1, program)\n", + "print(\"Address: {}\".format(address))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now that we have a Taproot output script to spend \"from\", fire up the Bitcoin Core test suite and stage a transaction to spend it, which we will satisfy in 3 different ways with different signatures:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "test = util.TestWrapper()\n", + "test.setup()\n", + "\n", + "test.nodes[0].generate(101)\n", + "balance = test.nodes[0].getbalance()\n", + "print('Balance: {}'.format(balance))\n", + "\n", + "assert balance > 1\n", + "\n", + "# Send wallet transaction to segwit address\n", + "amount_btc = 0.05\n", + "txid = test.nodes[0].sendtoaddress(address, amount_btc)\n", + "\n", + "# Decode wallet transaction\n", + "tx_hex = test.nodes[0].getrawtransaction(txid) \n", + "decoded_tx = test.nodes[0].decoderawtransaction(tx_hex)\n", + "\n", + "print(\"Transaction:\\n{}\\n\".format(decoded_tx))\n", + "\n", + "# Reconstruct wallet transaction locally\n", + "tx = CTransaction()\n", + "tx.deserialize(BytesIO(bytes.fromhex(tx_hex)))\n", + "tx.rehash()\n", + "\n", + "# We can check if the transaction was correctly deserialized\n", + "assert txid == decoded_tx[\"txid\"]\n", + "\n", + "# The wallet randomizes the change output index for privacy\n", + "# Loop through the outputs and return the first where the scriptPubKey matches the segwit v1 output\n", + "output_index, output = next(out for out in enumerate(tx.vout) if out[1].scriptPubKey == CScript([OP_1, program]))\n", + "\n", + "print(\"Segwit v1 output is {}\".format(output))\n", + "print(\"Segwit v1 output value is {}\".format(output.nValue))\n", + "print(\"Segwit v1 output index is {}\".format(output_index))\n", + "\n", + "# Construct transaction\n", + "spending_tx = CTransaction()\n", + "\n", + "# Populate the transaction version\n", + "spending_tx.nVersion = 1\n", + "\n", + "# Populate the locktime\n", + "spending_tx.nLockTime = 0\n", + "\n", + "# Populate the transaction inputs\n", + "outpoint = COutPoint(tx.sha256, output_index)\n", + "spending_tx_in = CTxIn(outpoint = outpoint)\n", + "spending_tx.vin = [spending_tx_in]\n", + "\n", + "print(\"Spending transaction:\\n{}\".format(spending_tx))\n", + "\n", + "# Generate new Bitcoin Core wallet address\n", + "dest_addr = test.nodes[0].getnewaddress(address_type=\"bech32\")\n", + "scriptpubkey = bytes.fromhex(test.nodes[0].getaddressinfo(dest_addr)['scriptPubKey'])\n", + "\n", + "# Determine minimum fee required for mempool acceptance\n", + "min_fee = int(test.nodes[0].getmempoolinfo()['mempoolminfee'] * 100000000)\n", + "\n", + "# Complete output which returns funds to Bitcoin Core wallet\n", + "amount_sat = int(amount_btc * 100000000)\n", + "dest_output = CTxOut(nValue=amount_sat - min_fee, scriptPubKey=scriptpubkey)\n", + "spending_tx.vout = [dest_output]\n", + "\n", + "print(\"Spending transaction:\\n{}\".format(spending_tx))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "1) Spend the transaction using n-of-n Taproot output pubkey spending path:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Generate the taproot signature hash for signing output key\n", + "sighash_musig = TaprootSignatureHash(spending_tx,\n", + " [output],\n", + " SIGHASH_ALL_TAPROOT)\n", + "\n", + "# Generate nonces for all signers (re-using multiple times... bad!!!)\n", + "nonces = [generate_schnorr_nonce() for i in range(len(pub_keys_base))]\n", + "R_agg, negated = aggregate_schnorr_nonces([nonce.get_pubkey() for nonce in nonces])\n", + "if negated:\n", + " for nonce in nonces:\n", + " nonce.negate()\n", + "\n", + "output_pubkey = inner_pubkey.tweak_add(tapscript_tweak)\n", + "\n", + "signatures = []\n", + "for i, (key, nonce) in enumerate(zip(priv_keys_tweaked, nonces)):\n", + " # One key must be tweaked\n", + " if i == 0:\n", + " key_tweaked = key.add(tapscript_tweak)\n", + " signatures.append(sign_musig(key_tweaked, nonce, R_agg, output_pubkey, sighash_musig))\n", + " else:\n", + " signatures.append(sign_musig(key, nonce, R_agg, output_pubkey, sighash_musig))\n", + "\n", + "aggregated_sig = aggregate_musig_signatures(signatures, R_agg)\n", + "\n", + "assert output_pubkey.verify_schnorr(aggregated_sig, sighash_musig)\n", + "\n", + "# Construct transaction witness\n", + "witness = CScriptWitness()\n", + "witness.stack.append(aggregated_sig)\n", + "witness_in = CTxInWitness()\n", + "witness_in.scriptWitness = witness\n", + "spending_tx.wit.vtxinwit.append(witness_in)\n", + " \n", + "print(\"spending_tx: {}\\n\".format(spending_tx))\n", + "\n", + "# Test mempool acceptance\n", + "spending_tx_str = spending_tx.serialize().hex()\n", + "assert test.nodes[0].testmempoolaccept([spending_tx_str])[0]['allowed']\n", + "\n", + "print(\"Key path spending transaction weight: {}\".format(test.nodes[0].decoderawtransaction(spending_tx_str)['weight']))\n", + "\n", + "print(\"Success!\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "2) Spend the output using a randomly-chosen k-of-k MuSig spending path. Note that this is not optimal for production; the least-deep viable spending path should be published to reduce transaction weight:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Generate the taproot signature hash for a random tapscript MuSig path\n", + "\n", + "# Pick random path, excluding the emergency leaf\n", + "chosen_leaf_index = random.randint(0, len(tapscript_leaves)-1)\n", + "chosen_signer_indices = tapscript_leaf_signers[chosen_leaf_index]\n", + "chosen_leaf = tapscript_leaves[chosen_leaf_index][1]\n", + "chosen_key_map = leaf_tweaks[chosen_leaf_index]\n", + "chosen_pubkey = tapscript_leaf_pubkey[chosen_leaf_index]\n", + "\n", + "sighash_musig_leaf = TaprootSignatureHash(spending_tx,\n", + " [output],\n", + " SIGHASH_ALL_TAPROOT,\n", + " 0,\n", + " scriptpath=True,\n", + " tapscript=chosen_leaf.script)\n", + "\n", + "# Compute tweaked keys for the signing subset\n", + "priv_keys_subset_base = [priv_keys_base[i] for i in chosen_signer_indices]\n", + "pub_keys_subset_base = [pub_keys_base[i] for i in chosen_signer_indices]\n", + "priv_keys_subset_tweaked = []\n", + "pub_keys_subset_tweaked = []\n", + "for pubkey, privkey in zip(pub_keys_subset_base, priv_keys_subset_base):\n", + " priv_keys_subset_tweaked.append(privkey.mul(chosen_key_map[pubkey]))\n", + " pub_keys_subset_tweaked.append(pubkey.mul(chosen_key_map[pubkey]))\n", + "\n", + "# Generate nonces for all signers (re-using multiple times... bad!!!)\n", + "subset_nonces = [generate_schnorr_nonce() for i in range(len(pub_keys_subset_base))]\n", + "R_agg, negated = aggregate_schnorr_nonces([nonce.get_pubkey() for nonce in subset_nonces])\n", + "if negated:\n", + " for nonce in subset_nonces:\n", + " nonce.negate()\n", + "\n", + "subset_signatures = []\n", + "for key, nonce in zip(priv_keys_subset_tweaked, subset_nonces):\n", + " subset_signatures.append(sign_musig(key, nonce, R_agg, chosen_pubkey, sighash_musig_leaf))\n", + "\n", + "aggregated_subset_sig = aggregate_musig_signatures(subset_signatures, R_agg)\n", + "\n", + "assert chosen_pubkey.verify_schnorr(aggregated_subset_sig, sighash_musig_leaf)\n", + "\n", + "# Construct transaction witness\n", + "witness = CScriptWitness()\n", + "witness.stack = [aggregated_subset_sig] + [chosen_leaf.script, control_map[chosen_leaf.script]]\n", + "witness_in = CTxInWitness()\n", + "witness_in.scriptWitness = witness\n", + "spending_tx.wit.vtxinwit[0] = witness_in # replace old witness\n", + " \n", + "print(\"spending_tx: {}\\n\".format(spending_tx))\n", + "\n", + "# Test mempool acceptance\n", + "spending_tx_str = spending_tx.serialize().hex()\n", + "assert test.nodes[0].testmempoolaccept([spending_tx_str])[0]['allowed']" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Last, we spend the transaction using the emergency keys after the transaction output becomes the correct age:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Next we attempt to spend the timelock tapscript path\n", + "# Construct transaction\n", + "spending_tx = CTransaction()\n", + "\n", + "spending_tx.nVersion = 2\n", + "spending_tx.nLockTime = 0\n", + "outpoint = COutPoint(tx.sha256, output_index)\n", + "spending_tx_in = CTxIn(outpoint=outpoint, nSequence=csv_delay)\n", + "spending_tx.vin = [spending_tx_in]\n", + "spending_tx.vout = [dest_output]\n", + "\n", + "csv_sighash = TaprootSignatureHash(spending_tx, [output], SIGHASH_ALL_TAPROOT, 0, scriptpath=True, tapscript=csa_tapscript.script)\n", + "\n", + "witness_elements = []\n", + "\n", + "# Add signatures to the witness\n", + "# Remember to reverse the order of signatures\n", + "witness_elements = [key.sign_schnorr(csv_sighash) for key in emergency_privkeys[::-1]]\n", + "# We only need f of the e sigs, blank the rest\n", + "for i in range(e-f):\n", + " witness_elements[i] = b''\n", + "\n", + "# Construct transaction witness\n", + "witness = CScriptWitness()\n", + "witness.stack = witness_elements + [csa_tapscript.script, control_map[csa_tapscript.script]]\n", + "witness_in = CTxInWitness()\n", + "witness_in.scriptWitness = witness\n", + "spending_tx.wit.vtxinwit.append(witness_in)\n", + "spending_tx_str = spending_tx.serialize().hex()\n", + "print(\"Testing timelock transaction without moving chain forward.\")\n", + "# Test timelock\n", + "assert_equal(\n", + " [{'txid': spending_tx.rehash(), 'allowed': False, 'reject-reason': '64: non-BIP68-final'}],\n", + " test.nodes[0].testmempoolaccept([spending_tx_str])\n", + ")\n", + "print(\"Generating blocks to move chain forward.\")\n", + "test.nodes[0].generate(csv_delay)\n", + "assert test.nodes[0].testmempoolaccept([spending_tx_str])[0][\"allowed\"]\n", + "\n", + "print(\"Long delay script path spending transaction weight: {}\".format(test.nodes[0].decoderawtransaction(spending_tx_str)['weight']))\n", + "\n", + "print(\"Success!\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "All done! Shut down the test environment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "test.shutdown()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.8" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +}