Skip to content

Commit b3cade7

Browse files
committed
Added mini-case study OP_RETURN vs pay-to-contract.
1 parent 8931fb5 commit b3cade7

File tree

2 files changed

+329
-3
lines changed

2 files changed

+329
-3
lines changed

2.2-taptweak.ipynb

Lines changed: 261 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,15 @@
66
"metadata": {},
77
"outputs": [],
88
"source": [
9+
"from io import BytesIO\n",
910
"import random\n",
1011
"\n",
1112
"import util\n",
1213
"from test_framework.address import program_to_witness\n",
13-
"from test_framework.key import ECKey, SECP256K1_ORDER, generate_key_pair, generate_schnorr_nonce\n",
14+
"from test_framework.key import ECKey, ECPubKey, SECP256K1_ORDER, generate_key_pair, generate_schnorr_nonce\n",
1415
"from test_framework.messages import CScriptWitness, CTxInWitness, sha256\n",
1516
"from test_framework.musig import generate_musig_key, aggregate_schnorr_nonces, sign_musig, aggregate_musig_signatures\n",
16-
"from test_framework.script import TaprootSignatureHash, SIGHASH_ALL_TAPROOT"
17+
"from test_framework.script import TaprootSignatureHash, SIGHASH_ALL_TAPROOT, OP_RETURN, CTransaction"
1718
]
1819
},
1920
{
@@ -442,6 +443,262 @@
442443
"test.shutdown()"
443444
]
444445
},
446+
{
447+
"cell_type": "markdown",
448+
"metadata": {},
449+
"source": [
450+
"## Part 3 (Case Study): Contract Commitments\n",
451+
"\n",
452+
"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",
453+
"\n",
454+
"* Committing data to an OP_RETURN output requires an additional output with a zero amount, resulting in a higher transaction fees.\n",
455+
"* 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",
456+
"\n",
457+
"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."
458+
]
459+
},
460+
{
461+
"cell_type": "markdown",
462+
"metadata": {},
463+
"source": [
464+
"### Committing contract data to an OP_RETURN output\n",
465+
"\n",
466+
"We'll first show Alice's current setup: An OP_RETURN script containing commitment data."
467+
]
468+
},
469+
{
470+
"cell_type": "markdown",
471+
"metadata": {},
472+
"source": [
473+
"#### Example 2.2.9: Create the contract commitment"
474+
]
475+
},
476+
{
477+
"cell_type": "code",
478+
"execution_count": null,
479+
"metadata": {},
480+
"outputs": [],
481+
"source": [
482+
"contract_bytes = \"Alice pays 10 BTC to Bob\".encode('utf-8')\n",
483+
"commitment_bytes = sha256(contract_bytes)\n",
484+
"print(\"The contract commitment is: {}\".format(commitment_bytes.hex()))"
485+
]
486+
},
487+
{
488+
"cell_type": "markdown",
489+
"metadata": {},
490+
"source": [
491+
"#### Example 2.2.10: Create and broadcast a transaction with an OP_RETURN output\n",
492+
"\n",
493+
"We now construct a OP_RETURN output which contains the commitment data of Alice's contract with Bob, and then add it to a transaction with a regular pay-to-pubkey output. This way, the commitment can be done more efficiently, by sharing transaction data with another spendable output."
494+
]
495+
},
496+
{
497+
"cell_type": "code",
498+
"execution_count": null,
499+
"metadata": {},
500+
"outputs": [],
501+
"source": [
502+
"# Start node\n",
503+
"test = util.TestWrapper()\n",
504+
"test.setup()\n",
505+
"node = test.nodes[0]\n",
506+
"\n",
507+
"# Generate coins to a new wallet address\n",
508+
"coinbase_address = node.getnewaddress(address_type=\"bech32\")\n",
509+
"node.generatetoaddress(101, coinbase_address)\n",
510+
"balance = node.getbalance()\n",
511+
"assert balance > 1\n",
512+
"\n",
513+
"# Spend wallet utxo with a transaction containing two outputs\n",
514+
"# Output 0) OP_RETURN with Alice's commitment\n",
515+
"# Output 1) Segwit destination address\n",
516+
"unspent_txid = node.listunspent(1)[-1][\"txid\"]\n",
517+
"inputs = [{\"txid\": unspent_txid, \"vout\": 0}]\n",
518+
"address_alice = node.getnewaddress(address_type=\"bech32\")\n",
519+
"tx_hex = node.createrawtransaction(inputs=inputs, outputs=[{\"data\": commitment_bytes.hex()}, {address_alice : 1}])\n",
520+
"\n",
521+
"# Sign for transaction\n",
522+
"res = node.signrawtransactionwithwallet(hexstring=tx_hex)\n",
523+
"tx_hex = res[\"hex\"]\n",
524+
"assert res[\"complete\"]\n",
525+
"assert 'errors' not in res\n",
526+
"\n",
527+
"# Broadcast transaction\n",
528+
"txid = node.sendrawtransaction(hexstring=tx_hex, maxfeerate=0)\n",
529+
"tx_op_return_hex = node.getrawtransaction(txid)\n",
530+
"\n",
531+
"# Reconstruct wallet transaction locally\n",
532+
"tx_op_return = CTransaction()\n",
533+
"tx_op_return.deserialize(BytesIO(bytes.fromhex(tx_hex)))\n",
534+
"tx_op_return.rehash()\n",
535+
"\n",
536+
"# Confirm details of the OP_RETURN output\n",
537+
"output = tx_op_return.vout[0]\n",
538+
"print(\"The OP_RETURN output script is: {}\".format(output.scriptPubKey.hex()))\n",
539+
"print(\"The OP_RETURN output value is: {}\".format(output.nValue))\n",
540+
"\n",
541+
"# Note the total weight of the transaction with a dedicated OP_RETURN commitment output\n",
542+
"print(\"The total transaction weight is: {}\\n\".format(node.decoderawtransaction(tx_op_return_hex)['weight']))"
543+
]
544+
},
545+
{
546+
"cell_type": "markdown",
547+
"metadata": {},
548+
"source": [
549+
"### Committing contract data with the pay-to-contract scheme\n",
550+
"\n",
551+
"Next, we will commit Alice's contract to a spendable pay-to-pubkey output with the pay-to-contract commitment scheme."
552+
]
553+
},
554+
{
555+
"cell_type": "markdown",
556+
"metadata": {},
557+
"source": [
558+
"#### _Programming Exercise 2.2.11:_ Generate segwit v1 address for a pay-to-contract public key\n",
559+
"\n",
560+
"Commit the contract to Alice's public key with the pay-to-contract commitment scheme, and then generate the corresponding segwit v1 address."
561+
]
562+
},
563+
{
564+
"cell_type": "code",
565+
"execution_count": null,
566+
"metadata": {},
567+
"outputs": [],
568+
"source": [
569+
"# Generate a key pair\n",
570+
"privkey, pubkey = generate_key_pair()\n",
571+
"\n",
572+
"# Generate the pay-to-contract tweak\n",
573+
"contract_bytes = \"Alice pays 10 BTC to Bob\".encode('utf-8')\n",
574+
"tweak_private = # TODO: implement\n",
575+
"tweak_point = # TODO: implement\n",
576+
"\n",
577+
"# Tweak Alice's key pair with the pay-to-contract tweak\n",
578+
"tweaked_pubkey = # TODO: implement\n",
579+
"tweaked_privkey = # TODO: implement\n",
580+
"\n",
581+
"# Generate the segwit v1 address\n",
582+
"tweaked_pubkey_data = # TODO: implement\n",
583+
"tweaked_pubkey_program = # TODO: implement\n",
584+
"version = 1\n",
585+
"address = program_to_witness(version, tweaked_pubkey_program)\n",
586+
"print(\"Address encoding the segwit v1 output: \", address)"
587+
]
588+
},
589+
{
590+
"cell_type": "markdown",
591+
"metadata": {},
592+
"source": [
593+
"#### Example 2.1.12: Create a transaction with the Bitcoin Core wallet sending funds to the segwit v1 address\n"
594+
]
595+
},
596+
{
597+
"cell_type": "code",
598+
"execution_count": null,
599+
"metadata": {},
600+
"outputs": [],
601+
"source": [
602+
"test = util.TestWrapper()\n",
603+
"test.setup()\n",
604+
"node = test.nodes[0]\n",
605+
"\n",
606+
"# Generate coins and send coins to segwit v1 address containing the pay-to-contract public key\n",
607+
"tx = node.generate_and_send_coins(address)\n",
608+
"print(\"Transaction {}, output 0\\nSent to {}\\n\".format(tx.hash, address))\n",
609+
"print(\"Transaction weight with pay-to-contract: {}\".format(node.decoderawtransaction(tx.serialize().hex())['weight']))\n",
610+
"print(\"Transaction weight with OP_RETURN: {}\\n\".format(node.decoderawtransaction(tx_op_return_hex)['weight']))"
611+
]
612+
},
613+
{
614+
"cell_type": "markdown",
615+
"metadata": {},
616+
"source": [
617+
"#### _Programming Exercise 2.1.13:_ Verify that the contract between Alice and Bob is committed correctly\n",
618+
"\n",
619+
"Extract the witness program from the segwit v1 output and that verify the pay-to-contract commitment."
620+
]
621+
},
622+
{
623+
"cell_type": "code",
624+
"execution_count": null,
625+
"metadata": {},
626+
"outputs": [],
627+
"source": [
628+
"# Fetch output from tx\n",
629+
"v1_output = tx.vout[0]\n",
630+
"\n",
631+
"# Extract witness program from the output script\n",
632+
"program = # TODO: implement\n",
633+
"\n",
634+
"# Reconstruct pay-to-contract public key\n",
635+
"tweaked_pubkey_bytes = # TODO: implement\n",
636+
"tweaked_pubkey = # TODO: implement\n",
637+
"\n",
638+
"# Verify pay-to-contract commitment is correct\n",
639+
"contract_bytes = \"Alice pays 10 BTC to Bob\".encode('utf-8')\n",
640+
"ss = pubkey.get_bytes()\n",
641+
"ss += sha256(contract_bytes)\n",
642+
"t = sha256(ss)\n",
643+
"assert pubkey.tweak_add(t) == tweaked_pubkey\n",
644+
"print(\"Contract commitment is correct!\")"
645+
]
646+
},
647+
{
648+
"cell_type": "markdown",
649+
"metadata": {},
650+
"source": [
651+
"#### Example 2.1.14 : Construct a CTransaction to spend the pay-to-contract public key\n",
652+
"\n",
653+
"Unlike an OP_RETURN output, the tweaked pay-to-contract public key can be spent like a regular, untweaked public key. An on-chain observer cannot determine whether a commitment was made to the public key."
654+
]
655+
},
656+
{
657+
"cell_type": "code",
658+
"execution_count": null,
659+
"metadata": {},
660+
"outputs": [],
661+
"source": [
662+
"# Create a spending transaction, which sends funds back to the wallet\n",
663+
"spending_tx = test.create_spending_transaction(tx.hash)\n",
664+
"print(\"Spending transaction:\\n{}\\n\".format(spending_tx))\n",
665+
"\n",
666+
"# Create sighash for ALL (0x00)\n",
667+
"sighash = TaprootSignatureHash(spending_tx, [tx.vout[0]], SIGHASH_ALL_TAPROOT, input_index=0)\n",
668+
"\n",
669+
"# Create a valid transaction signature for the tweaked public key\n",
670+
"sig = tweaked_privkey.sign_schnorr(sighash)\n",
671+
"print(\"Signature of tweaked keypair is {}\\n\".format(sig.hex()))\n",
672+
"\n",
673+
"# Construct transaction witness\n",
674+
"witness = CScriptWitness()\n",
675+
"witness.stack.append(sig)\n",
676+
"witness_in = CTxInWitness()\n",
677+
"witness_in.scriptWitness = witness\n",
678+
"spending_tx.wit.vtxinwit.append(witness_in)\n",
679+
"\n",
680+
"# Test mempool acceptance\n",
681+
"assert node.test_transaction(spending_tx)\n",
682+
"print(\"Success!\")"
683+
]
684+
},
685+
{
686+
"cell_type": "markdown",
687+
"metadata": {},
688+
"source": [
689+
"#### _Shutdown TestWrapper_"
690+
]
691+
},
692+
{
693+
"cell_type": "code",
694+
"execution_count": null,
695+
"metadata": {},
696+
"outputs": [],
697+
"source": [
698+
"# Shutdown\n",
699+
"test.shutdown()"
700+
]
701+
},
445702
{
446703
"cell_type": "markdown",
447704
"metadata": {},
@@ -450,7 +707,8 @@
450707
"\n",
451708
"- Learned how to tweak a public/private key pair with a value.\n",
452709
"- 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",
453-
"- Sent coins to a segwit v1 output with a tweaked public key, and later spent that output by signing with the tweaked private key."
710+
"- 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",
711+
"- Improved cost and privacy of a contract commitment by moving it from an unspendable OP_RETURN output to a pay-to-contract public key."
454712
]
455713
}
456714
],

solutions/2.2-taptweak-solutions.ipynb

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,74 @@
124124
"assert node.test_transaction(spending_tx)\n",
125125
"print(\"Success!\")"
126126
]
127+
},
128+
{
129+
"cell_type": "markdown",
130+
"metadata": {},
131+
"source": [
132+
"#### _Programming Exercise 2.2.11:_ Generate segwit v1 address for a pay-to-contract public key"
133+
]
134+
},
135+
{
136+
"cell_type": "code",
137+
"execution_count": null,
138+
"metadata": {},
139+
"outputs": [],
140+
"source": [
141+
"# Generate a key pair\n",
142+
"privkey, pubkey = generate_key_pair()\n",
143+
"\n",
144+
"# Generate the pay-to-contract tweak\n",
145+
"contract_bytes = \"Alice pays 10 BTC to Bob\".encode('utf-8')\n",
146+
"ss = pubkey.get_bytes()\n",
147+
"ss += sha256(contract_bytes)\n",
148+
"t = sha256(ss)\n",
149+
"tweak_private = ECKey().set(t, True)\n",
150+
"tweak_point = tweak_private.get_pubkey()\n",
151+
"\n",
152+
"# Tweak Alice's key pair with the pay-to-contract tweak\n",
153+
"tweaked_pubkey = pubkey + tweak_point\n",
154+
"tweaked_privkey = privkey + tweak_private\n",
155+
"\n",
156+
"# Generate the segwit v1 address\n",
157+
"tweaked_pubkey_data = tweaked_pubkey.get_bytes()\n",
158+
"tweaked_pubkey_program = bytes([tweaked_pubkey_data[0] & 1]) + tweaked_pubkey_data[1:]\n",
159+
"version = 1\n",
160+
"address = program_to_witness(version, tweaked_pubkey_program)\n",
161+
"print(\"Address encoding the segwit v1 output: \", address)"
162+
]
163+
},
164+
{
165+
"cell_type": "markdown",
166+
"metadata": {},
167+
"source": [
168+
"#### _Programming Exercise 2.1.13:_ Verify that the contract between Alice and Bob is committed correctly"
169+
]
170+
},
171+
{
172+
"cell_type": "code",
173+
"execution_count": null,
174+
"metadata": {},
175+
"outputs": [],
176+
"source": [
177+
"# Fetch output from tx\n",
178+
"v1_output = tx.vout[0]\n",
179+
"\n",
180+
"# Extract witness program from the output script\n",
181+
"program = v1_output.scriptPubKey[2:]\n",
182+
"\n",
183+
"# Reconstruct pay-to-contract public key\n",
184+
"tweaked_pubkey_bytes = bytes([program[0] + 2]) + program[1:]\n",
185+
"tweaked_pubkey = ECPubKey().set(tweaked_pubkey_bytes)\n",
186+
"\n",
187+
"# Verify pay-to-contract commitment is correct\n",
188+
"contract_bytes = \"Alice pays 10 BTC to Bob\".encode('utf-8')\n",
189+
"ss = pubkey.get_bytes()\n",
190+
"ss += sha256(contract_bytes)\n",
191+
"t = sha256(ss)\n",
192+
"assert pubkey.tweak_add(t) == tweaked_pubkey\n",
193+
"print(\"Contract commitment is correct!\")"
194+
]
127195
}
128196
],
129197
"metadata": {

0 commit comments

Comments
 (0)