diff --git a/2.2-taptweak.ipynb b/2.2-taptweak.ipynb index 99e3aad60..63bca8d05 100644 --- a/2.2-taptweak.ipynb +++ b/2.2-taptweak.ipynb @@ -7,13 +7,14 @@ "outputs": [], "source": [ "import random\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, SECP256K1_ORDER, generate_key_pair, generate_schnorr_nonce\n", - "from test_framework.messages import CTxInWitness, sha256\n", + "from test_framework.key import ECKey, ECPubKey, SECP256K1_ORDER, generate_key_pair, generate_schnorr_nonce\n", + "from test_framework.messages import COutPoint, CTxIn, CTxInWitness, CTxOut, sha256\n", "from test_framework.musig import generate_musig_key, aggregate_schnorr_nonces, sign_musig, aggregate_musig_signatures, musig_digest\n", - "from test_framework.script import TaprootSignatureHash, SIGHASH_ALL_TAPROOT" + "from test_framework.script import CScript, CTransaction, OP_RETURN, SIGHASH_ALL_TAPROOT, TaprootSignatureHash" ] }, { @@ -22,13 +23,20 @@ "source": [ "# 2.2 TapTweak\n", "\n", - "* Tweaking the Public Key\n", - "* Commitment Schemes with Tweaks\n", - "* Spending a (tweaked) taproot output along the key path\n", + "* Part 1: Tweaking the public key; commitment schemes with tweaks\n", + "* Part 2: Spending a (tweaked) taproot output along the key path\n", + "* Part 3 (Case Study): contract commitments\n", "\n", - "The linear property of bip-schnorr means that we can encode a commitment into a public key, and then reveal that commitment when signing with the private key. We do that by _tweaking_ the private key with the commitment, and using the associated _tweaked_ pubkey. When signing, we can reveal that the original private key was tweaked by the commitment.\n", + "The linear property of bip-schnorr means that we can encode a commitment into a public key, and then reveal that commitment when signing with the private key. We do that by _tweaking_ the private key with the commitment, and using the associated _tweaked_ pubkey. When signing, we can reveal that the original keys were tweaked by the commitment.\n", "\n", - "## Tweaking the Public Key\n", + "In part 1, we'll learn about how private/public key pairs can be tweaked, and how we can use that to create a secure commitment scheme. In part 2, we'll create a segwit v1 output and spend it along the key path, using a tweaked private and public key. Part 3 of this chapter is a case study, showing how pay-to-contract with tweaked keys can be used instead of OP_RETURN outputs to create timestamped commitments." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Part 1: Tweaking the public key\n", "\n", "Instead of using our original public key as the witness program, we use a tweaked public key.\n", "\n", @@ -70,9 +78,9 @@ "\n", "print(\"Private key: {}\\nPublic key: {}\\n\".format(privkey.secret, pubkey.get_bytes().hex()))\n", "\n", - "# Generate a random tweak 0 < t < SECP256K1_ORDER and its associated point\n", + "# Generate a random tweak scalar 0 < t < SECP256K1_ORDER and derive its associated tweak point\n", "tweak = random.randrange(1, SECP256K1_ORDER)\n", - "tweak_private = ECKey().set(tweak, True)\n", + "tweak_private = ECKey().set(tweak)\n", "tweak_point = tweak_private.get_pubkey()\n", "print(\"Tweak scalar: {}\\nTweak point: {}\\n\".format(tweak_private.secret, tweak_point.get_bytes().hex()))\n", "\n", @@ -96,7 +104,7 @@ "\n", "In this exercise, we tweak an MuSig aggregate pubkey, and then sign for it using the individual participant keys. The MuSig pubkey aggregation step is done for you.\n", "\n", - "_Question: Which participant(s) need to tweak their private keys?_" + "_Question: How is the tweak incorporated into the final signature?_" ] }, { @@ -156,16 +164,20 @@ "\n", "![test](images/taptweak0.jpg)\n", "\n", - "Instead, the committed value must first be hashed with the untweaked public key point. **This prevents modification of both untweaked secret and tweak for a given tweaked pubkey point Q.**" + "Instead, the committed value must first be hashed with the untweaked public key point. This commitment scheme is called *pay-to-contract*. **It does not allow the modification of a committed value for a given public key point Q.**" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "#### Example 2.2.3: modifying the tweak for a tweaked public key Q\n", + "#### Example 2.2.3: Tweaking a public key Q with commitment data\n", "\n", - "In this example we demonstrate an insecure commitment scheme. Simply tweaking the private key with a value `c` allows the pubkey equation `Q = x'G + c'G` to be solved for any `c'` by modifying `x'`." + "In this example we demonstrate an insecure commitment scheme. The committed value `c` can be trivially modified to `c'`, and by setting `x'` to `x + c - c'`, the public key point equation `Q = x'G + c'G` still holds.\n", + "\n", + "First, we commit a contract between Alice and Bob and then demonstrate how this unsafe commitment can be changed.\n", + "\n", + "* The initial committed contract is: `Alice agrees to pay 10 BTC to Bob`" ] }, { @@ -174,38 +186,99 @@ "metadata": {}, "outputs": [], "source": [ - "# Generate a key pair\n", - "x, P = generate_key_pair()\n", - "print(\"Private key: {}\\nPublic key: {}\\n\".format(x.secret, P.get_bytes().hex()))\n", - "\n", - "# Tweak the public key\n", - "t = random.randrange(1, SECP256K1_ORDER)\n", - "print(\"Tweak: {}\".format(t))\n", - "Q = P.tweak_add(t)\n", - "\n", - "# Create a fake tweak\n", - "t2 = random.randrange(1, SECP256K1_ORDER)\n", - "print(\"Tweak 2: {}\\n\".format(t2))\n", + "# Alice generates a key pair\n", + "x_key, P_key = generate_key_pair()\n", + "print(\"Private key: {}\\nPublic key: {}\\n\".format(x_key.secret, P_key.get_bytes().hex()))\n", + "\n", + "# Alice generates the tweak from the contract\n", + "contract = \"Alice agrees to pay 10 BTC to Bob\"\n", + "t = sha256(contract.encode('utf-8'))\n", + "print(\"Tweak from original contract: {}\\n\".format(t.hex()))\n", + "\n", + "# Alice tweaks her key pair\n", + "Q_key = P_key.tweak_add(t)\n", + "q_key = x_key.add(t)\n", + "print(\"Tweaked private key: {}\\nTweaked public key: {}\\n\".format(q_key.secret, Q_key.get_bytes().hex()))\n", + "\n", + "# Alice produces a valid signature for this tweaked public key\n", + "msg = sha256(b'I agree to the committed contract')\n", + "sig = q_key.sign_schnorr(msg)\n", + "\n", + "# Bob can verify that sig is a valid signature for the public key Q:\n", + "verify_sig = Q_key.verify_schnorr(sig, msg)\n", + "print(\"Alice has produced a valid signature for Q: {}\".format(verify_sig))\n", + "\n", + "# Alice provides the untweaked public key P to Bob.\n", + "# Bob believes he can verify that the signature committed to the tweak t:\n", + "verify_tweak = P_key.tweak_add(sha256(contract.encode('utf-8'))) == Q_key\n", + "print(\"The signature appears to commit to '{}': {}\".format(contract, verify_tweak))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Example 2.2.4: Modifying the commitment tweak of public key Q\n", "\n", - "# Solve: x` = x - t' + t\n", - "x_int = x.as_int()\n", - "x2_int = (x_int - t2 + t) % SECP256K1_ORDER\n", + "However, note that is possible for Alice to modify this insecure commitment without changing the value of pub key `Q`.\n", + "* The committed contract is changed to : `Alice agrees to pay 0.1 BTC to Bob`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Alice modifies the contract and produces an alternative tweak\n", + "alternative_contract = \"Alice agrees to pay 0.1 BTC to Bob\"\n", + "t2 = sha256(alternative_contract.encode('utf-8'))\n", + "print(\"Tweak from original contract: {}\".format(t.hex()))\n", + "print(\"Tweak from modified contract: {}\\n\".format(t2.hex()))\n", + "\n", + "# Alice modifies her original private key and public key\n", + "# x2 = x - t2 + t\n", + "x_int = x_key.as_int()\n", + "t_int = int.from_bytes(t, \"big\") \n", + "t2_int = int.from_bytes(t2, \"big\") \n", + "x2_key, P2_key = generate_key_pair((x_int - t2_int + t_int) % SECP256K1_ORDER)\n", + "\n", + "# Alice can still produce a valid signature for Q\n", + "msg2 = sha256(b'I agree to the committed contract')\n", + "sig2 = q_key.sign_schnorr(msg2)\n", + "\n", + "# Bob can verify that sig is a valid signature for the public key Q:\n", + "verify_sig = Q_key.verify_schnorr(sig, msg)\n", + "print(\"Alice has produced a valid signature for Q: {}\".format(verify_sig))\n", + "\n", + "# Alice claims that P2 is the untweaked public key.\n", + "# Bob believes he can verify that the signature committed to the tweak t:\n", + "verify_tweak = P2_key.tweak_add(sha256(alternative_contract.encode('utf-8'))) == Q_key\n", + "print(\"The signature appears to commit to '{}': {}\".format(alternative_contract, verify_tweak))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Summary of 2.2.3, 2.2.4: Insecure practice of tweaking a public key with commitment data\n", "\n", - "x2_key = x = ECKey().set(x2_int, True)\n", - "P2 = x2_key.get_pubkey()\n", - "Q2 = P2.tweak_add(t2)\n", + "We have demonstrated how a simple key tweak with commitment data does not work as a commitment scheme.\n", + "* Tweaking the original public key `P` with commitment data hides the commitment.\n", + "* However, the original public key `P` can be recomputed (`P2`) for any modified commitment, without altering the tweaked public key `Q`.\n", "\n", - "print(\"Tweaked pubkey for x tweaked by t: {}\".format(Q.get_bytes().hex()))\n", - "print(\"Tweaked pubkey for x2 tweaked by t2: {}\".format(Q2.get_bytes().hex()))" + "To any observer, both original and modified \"commitments\" appear to be valid for the same public key `Q`." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "#### Example 2.2.4 - Tweaking the pubkey with `H(P|msg)`\n", + "#### Example 2.2.5 - Pay-to-contract: Tweaking the pubkey with `H(P|msg)`\n", "\n", - "In this example, we demonstrate a _secure_ commitment scheme. The private key is tweaked with the scalar `H(P|c)`. Since `P` appears both inside and outside the hash, it isn't possible to solve for a different `c` by modifying `x'`." + "In this example, we demonstrate a _secure_ commitment scheme called pay-to-contract. The private key is tweaked with the scalar `H(P|c)`. Since `P` appears both inside and outside the hash, it isn't possible to solve for a different contract `c` by modifying `x`.\n", + "\n", + "* Alice can now no longer invalidate her previous contract commitment with Bob." ] }, { @@ -214,48 +287,51 @@ "metadata": {}, "outputs": [], "source": [ - "# Key pair generation\n", - "privkey1, pubkey1 = generate_key_pair()\n", - "print(\"Private key: {}\\nPublic key: {}\\n\".format(x.secret, pubkey.get_bytes().hex()))\n", - "\n", - "# Compute the tweak from H(P|msg)\n", - "commitment = b'commitment'\n", - "ss = sha256(pubkey.get_bytes())\n", - "ss += sha256(commitment)\n", + "# Alice generates a key pair\n", + "x_key, P_key = generate_key_pair()\n", + "print(\"Private key: {}\\nPublic key: {}\\n\".format(x_key.secret, P_key.get_bytes().hex()))\n", + "\n", + "# Alice computes the tweak from H(P|msg)\n", + "contract = \"Alice agrees to pay 10 BTC to Bob\"\n", + "ss = P_key.get_bytes()\n", + "ss += sha256(contract.encode('utf-8'))\n", "t = sha256(ss)\n", "\n", - "# Determine tweak point\n", - "tweak = ECKey().set(t, True)\n", - "tweak_point = tweak.get_pubkey()\n", - "print(\"Tweak scalar: {}\\nTweak point: {}\\n\".format(tweak.secret, tweak_point.get_bytes().hex()))\n", + "# Alice tweaks her key pair\n", + "Q_key = P_key.tweak_add(t)\n", + "q_key = x_key.add(t)\n", + "print(\"Tweaked private key: {}\\nTweaked public key: {}\\n\".format(q_key.secret, Q_key.get_bytes().hex()))\n", "\n", - "privkey_tweaked = privkey + tweak\n", - "pubkey_tweaked = pubkey + tweak_point\n", + "# Alice signs a valid message\n", + "msg = sha256(b'I agree to the committed contract')\n", + "sig = q_key.sign_schnorr(msg)\n", "\n", - "# Sign message and verify signature\n", - "msg = sha256(b'msg')\n", - "sig = privkey_tweaked.sign_schnorr(msg)\n", + "# Bob can verify that sig is a valid signature for the public key Q:\n", + "verify_sig = Q_key.verify_schnorr(sig, msg)\n", + "print(\"Alice has produced a valid signature for Q: {}\".format(verify_sig))\n", "\n", - "assert pubkey_tweaked.verify_schnorr(sig, msg)\n", - "print(\"Success!\")" + "# Alice provides the untweaked public key P to Bob.\n", + "# Bob believes he can verify that the signature committed to the tweak t:\n", + "verify_tweak = P_key.tweak_add(sha256(P_key.get_bytes() + sha256(contract.encode('utf-8')))) == Q_key\n", + "print(\"The signature commits to '{}': {}\".format(contract, verify_tweak))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Spending a taproot output along the key path\n", + "## Part 2: Spending a (tweaked) taproot output along the key path\n", "\n", - "In this exercise, we'll create a segwit version 1 output that sends to a tweaked public key. We'll them spend that output along the key path using the tweaked private key.\n", + "In this exercise, we'll create a segwit v1 output that sends to a tweaked public key. We'll then spend that output along the key path using the tweaked private key.\n", "\n", - "Such as spend does not reveal the committed tweak to the observer and is indistinguishable any other key path spend." + "Such as spend does not reveal the committed tweak to the observer and is indistinguishable from any other key path spend." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "#### _Programming Exercise 2.2.5:_ Construct taproot output with tweaked public key" + "#### _Programming Exercise 2.2.6:_ Construct taproot output with tweaked public key" ] }, { @@ -265,7 +341,7 @@ "outputs": [], "source": [ "# Example key pair\n", - "privkey = ECKey().set(102118636618570133408735518698955378316807974995033705330357303547139065928052, True)\n", + "privkey = ECKey().set(102118636618570133408735518698955378316807974995033705330357303547139065928052)\n", "internal_pubkey = privkey.get_pubkey()\n", "\n", "# Example tweak\n", @@ -288,7 +364,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "#### Example 2.2.6: Start Bitcoin Core node and send coins to the taproot address\n", + "#### Example 2.2.7: Start Bitcoin Core node and send coins to the taproot address\n", "\n", "Only run setup once, or after a clean shutdown." ] @@ -313,7 +389,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "#### Example 2.2.7: Construct `CTransaction` and populate inputs\n", + "#### Example 2.2.8: Construct `CTransaction` and populate inputs\n", "\n", "We use the `create_spending_transaction(node, txid)` convenience function." ] @@ -333,7 +409,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "#### _Programming Exercise 2.2.8:_ Spend taproot output with key path" + "#### _Programming Exercise 2.2.9:_ Spend taproot output with key path" ] }, { @@ -372,6 +448,211 @@ "test.shutdown()" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Part 3 (Case Study): Contract commitments\n", + "\n", + "Alice currently commits contracts with Bob to unspendable OP_RETURN outputs, which contain 32B proof-of-existence commitments. Although this is a standard output with a zero amount, several disadvantages remain:\n", + "\n", + "* Committing data to an OP_RETURN output requires an additional output with a zero amount, resulting in a higher transaction fees.\n", + "* The OP_RETURN output reveals the presence of a data commitment to any on-chain observer. This reduces the privacy of Alice's commitments.\n", + "\n", + "In this chapter, we'll show how Alice can move her contract commitments to public key tweaks to reduce fees and improve the privacy of her commitments." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Committing contract data to an OP_RETURN output\n", + "\n", + "We'll first show Alice's current setup: An OP_RETURN script containing commitment data." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Example 2.2.10: Create the contract commitment" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "contract_bytes = \"Alice pays 10 BTC to Bob\".encode('utf-8')\n", + "commitment_bytes = sha256(contract_bytes)\n", + "print(\"The contract commitment is: {}\".format(commitment_bytes.hex()))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Example 2.2.11: Start Bitcoin Core node and construct an unspent output\n", + "\n", + "Only run once, or after a clean shutdown. This constructs an unspent outpoint for example 2.2.12. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Start node\n", + "test = util.TestWrapper()\n", + "test.setup()\n", + "node = test.nodes[0]\n", + "\n", + "# Generate coins and send these to a new wallet address\n", + "node.generatetoaddress(101, node.getnewaddress(address_type=\"bech32\"))\n", + "\n", + "# Fetch the oldest unspent outpoint in the Bitcoin Core wallet\n", + "unspent_txid = node.listunspent(1)[-1][\"txid\"]\n", + "unspent_outpoint = COutPoint(int(unspent_txid,16), 0)\n", + "\n", + "print(\"Unspent coin: txid:{}, n:{}\".format(unspent_outpoint.hash, unspent_outpoint.n))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Example 2.2.12: Create and broadcast a transaction with an OP_RETURN output\n", + "\n", + "We now construct a zero-value OP_RETURN output which contains the commitment data of Alice's contract with Bob. We also add a regular P2WPKH output back to Alice to return the funds from the transaction input (less the transaction fee)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Construct transaction spending previously generated outpoint\n", + "op_return_tx = CTransaction()\n", + "op_return_tx.nVersion = 1\n", + "op_return_tx.nLockTime = 0\n", + "op_return_tx_in = CTxIn(outpoint=unspent_outpoint, nSequence=0)\n", + "op_return_tx.vin = [op_return_tx_in]\n", + "\n", + "# Output 0) Alice's change address\n", + "address_alice = node.getnewaddress(address_type=\"bech32\")\n", + "p2wpkh_output_script = bytes.fromhex(node.getaddressinfo(address_alice)['scriptPubKey'])\n", + "p2wpkh_output_amount_sat = 4_950_000_000 # remove transaction fee from output amount\n", + "p2wpkh_output = CTxOut(nValue=p2wpkh_output_amount_sat, scriptPubKey=p2wpkh_output_script)\n", + "\n", + "# Output 1) OP_RETURN with Alice's commitment\n", + "op_return_output_script = CScript([OP_RETURN, commitment_bytes])\n", + "op_return_output = CTxOut(nValue=0, scriptPubKey=op_return_output_script)\n", + "\n", + "# Populate transaction with p2pkh and OP_RETURN outputs and add valid witness\n", + "op_return_tx.vout = [p2wpkh_output, op_return_output]\n", + "op_return_tx_hex_signed = node.signrawtransactionwithwallet(hexstring=op_return_tx.serialize().hex())['hex']\n", + "\n", + "# Confirm details of the OP_RETURN output\n", + "op_return_tx_decoded = node.decoderawtransaction(op_return_tx_hex_signed)\n", + "op_return_vout = op_return_tx_decoded['vout'][1]\n", + "print(\"The OP_RETURN output script is: {}\".format(op_return_vout['scriptPubKey']['asm']))\n", + "print(\"The OP_RETURN output value is: {}\".format(int(op_return_vout['value'])))\n", + "\n", + "# Note the total weight of the transaction with a dedicated OP_RETURN commitment output\n", + "print(\"The total transaction weight is: {}\\n\".format(op_return_tx_decoded['weight']))\n", + "\n", + "# Test mempool acceptance\n", + "print(node.testmempoolaccept(rawtxs=[op_return_tx_hex_signed], maxfeerate=0))\n", + "assert node.testmempoolaccept(rawtxs=[op_return_tx_hex_signed], maxfeerate=0)[0]['allowed']\n", + "print(\"Success!\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Committing contract data with the pay-to-contract scheme\n", + "\n", + "Next, we will commit Alice's contract to a spendable pay-to-pubkey output with the pay-to-contract commitment scheme." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### _Programming Exercise 2.2.13:_ Generate segwit v1 address for a pay-to-contract public key\n", + "\n", + "Commit the contract to Alice's public key with the pay-to-contract commitment scheme, and then generate the corresponding segwit v1 address." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Generate a key pair\n", + "privkey, pubkey = generate_key_pair()\n", + "\n", + "# Generate the pay-to-contract tweak\n", + "contract_bytes = \"Alice pays 10 BTC to Bob\".encode('utf-8')\n", + "tweak_private = # TODO: implement\n", + "tweak_point = # TODO: implement\n", + "\n", + "# Tweak Alice's key pair with the pay-to-contract tweak\n", + "tweaked_pubkey = # TODO: implement\n", + "tweaked_privkey = # TODO: implement\n", + "\n", + "# Generate the segwit v1 address\n", + "tweaked_pubkey_data = # TODO: implement\n", + "tweaked_pubkey_program = # TODO: implement\n", + "version = 1\n", + "address = program_to_witness(version, tweaked_pubkey_program)\n", + "print(\"Address encoding the segwit v1 output: \", address)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Example 2.2.14: Create a transaction with the Bitcoin Core wallet sending funds to the segwit v1 address\n", + "\n", + "The pay-to-contract output encoded in the segwit v1 address holds spendable value just like a regular, untweaked public key. It can be spent with the tweaked private key, as we learned in part 2 of this chapter." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Generate coins and send to segwit v1 address containing the pay-to-contract public key\n", + "tx = node.generate_and_send_coins(address)\n", + "print(\"Transaction {}, output 0\\nSent to {}\\n\".format(tx.hash, address))\n", + "print(\"Transaction weight with pay-to-contract: {}\".format(node.decoderawtransaction(tx.serialize().hex())['weight']))\n", + "print(\"Transaction weight with OP_RETURN: {}\\n\".format(op_return_tx_decoded['weight']))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### _Shutdown TestWrapper_" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Shutdown\n", + "test.shutdown()" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -380,7 +661,8 @@ "\n", "- Learned how to tweak a public/private key pair with a value.\n", "- Created an _insecure_ commitment scheme (by tweaking the keys with the raw commitment value) and a _secure_ commitment scheme (by tweaking with a hash of the commitment and the public key).\n", - "- Sent coins to a segwit v1 output with a tweaked public key, and later spent that output by signing with the tweaked private key." + "- Sent coins to a segwit v1 output with a tweaked public key, and later spent that output by signing with the tweaked private key.\n", + "- Improved cost and privacy of a contract commitment by moving it from an unspendable OP_RETURN output to a pay-to-contract public key." ] } ], diff --git a/images/taptweak0.jpg b/images/taptweak0.jpg index 739fbded5..c7220e988 100644 Binary files a/images/taptweak0.jpg and b/images/taptweak0.jpg differ diff --git a/images/taptweak0.svg b/images/taptweak0.svg index 9030f7643..ec1ba19fd 100644 --- a/images/taptweak0.svg +++ b/images/taptweak0.svg @@ -2,40 +2,41 @@ - Commitments with Pubkey Tweaks + Commitments with Pubkey Tweaks - + - + - Commitment Scheme + Pay-to-Contract Commitment - - Q = x’G + c’G + + Q = x’G + c’G - Cannot solvefor x + Cannot solve + for x - - Solve for x’ + + Solve for x’ - Q = P + cG + Q = P + cG - - x + + x - Q = P + H(P|c)G + Q = P + H(P|c)G - - Q != x’G + H(x’G|c’)G + + Q != x’G + H(x’G|c’)G + c0,1.406,1.149,2.556,2.557,2.556h28.346c1.406,0,2.558,1.15,2.558,2.556v4.694"/> + c0,1.406-1.15,2.556-2.558,2.556h-28.345c-1.406,0-2.557,1.15-2.557,2.556v4.694"/> @@ -75,7 +76,7 @@ - Modify c’ + Modify c’ @@ -83,7 +84,7 @@ - Modify c’ + Modify c’ @@ -91,7 +92,7 @@ - Public Key Tweak + Public Key Tweak @@ -107,7 +108,7 @@ - + diff --git a/solutions/2.2-taptweak-solutions.ipynb b/solutions/2.2-taptweak-solutions.ipynb index 00479af5f..277771095 100644 --- a/solutions/2.2-taptweak-solutions.ipynb +++ b/solutions/2.2-taptweak-solutions.ipynb @@ -50,6 +50,8 @@ "# needs to be added to the list of partial signatures\n", "# Method: sign_musig(private_key, nonce_key, nonce_point, public_key, msg)\n", "# Method: aggregate_musig_signatures(partial_signature_list, aggregate nonce)\n", + "# Note: The tweak multiplied by the musig_digest is added to the partial signature list.\n", + "# This is equivalent to tweaking the private key for a single signer.\n", "e = musig_digest(R_agg, agg_pubkey_tweaked, msg)\n", "s1 = sign_musig(privkey1_c, k1, R_agg, agg_pubkey_tweaked, msg)\n", "s2 = sign_musig(privkey2_c, k2, R_agg, agg_pubkey_tweaked, msg)\n", @@ -63,7 +65,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "#### _Programming Exercise 2.2.5:_ Construct taproot output with tweaked public key" + "#### _Programming Exercise 2.2.6:_ Construct taproot output with tweaked public key" ] }, { @@ -73,7 +75,7 @@ "outputs": [], "source": [ "# Example key pair\n", - "privkey = ECKey().set(102118636618570133408735518698955378316807974995033705330357303547139065928052, True)\n", + "privkey = ECKey().set(102118636618570133408735518698955378316807974995033705330357303547139065928052)\n", "internal_pubkey = privkey.get_pubkey()\n", "\n", "# Example tweak\n", @@ -97,7 +99,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "#### _Programming Exercise 2.2.8:_ Spend taproot output with key path" + "#### _Programming Exercise 2.2.9:_ Spend taproot output with key path" ] }, { @@ -119,6 +121,42 @@ "assert node.test_transaction(spending_tx)\n", "print(\"Success!\")" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### _Programming Exercise 2.2.13:_ Generate segwit v1 address for a pay-to-contract public key" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Generate a key pair\n", + "privkey, pubkey = generate_key_pair()\n", + "\n", + "# Generate the pay-to-contract tweak\n", + "contract_bytes = \"Alice pays 10 BTC to Bob\".encode('utf-8')\n", + "ss = pubkey.get_bytes()\n", + "ss += sha256(contract_bytes)\n", + "t = sha256(ss)\n", + "tweak_private = ECKey().set(t, True)\n", + "tweak_point = tweak_private.get_pubkey()\n", + "\n", + "# Tweak Alice's key pair with the pay-to-contract tweak\n", + "tweaked_pubkey = pubkey + tweak_point\n", + "tweaked_privkey = privkey + tweak_private\n", + "\n", + "# Generate the segwit v1 address\n", + "tweaked_pubkey_data = tweaked_pubkey.get_bytes()\n", + "tweaked_pubkey_program = bytes([tweaked_pubkey_data[0] & 1]) + tweaked_pubkey_data[1:]\n", + "version = 1\n", + "address = program_to_witness(version, tweaked_pubkey_program)\n", + "print(\"Address encoding the segwit v1 output: \", address)" + ] } ], "metadata": {