Skip to content

Commit b259f1d

Browse files
committed
Add mini-case study: OP_RETURN vs pay-to-contract
1 parent 7125edf commit b259f1d

File tree

2 files changed

+302
-3
lines changed

2 files changed

+302
-3
lines changed

2.2-taptweak.ipynb

Lines changed: 234 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@
1010
"\n",
1111
"import util\n",
1212
"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",
13+
"from test_framework.key import ECKey, ECPubKey, SECP256K1_ORDER, generate_key_pair, generate_schnorr_nonce\n",
1414
"from test_framework.messages import CTxInWitness, sha256\n",
1515
"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"
16+
"from test_framework.script import CTransaction, OP_RETURN, SIGHASH_ALL_TAPROOT, TaprootSignatureHash"
1717
]
1818
},
1919
{
@@ -453,6 +453,236 @@
453453
"test.shutdown()"
454454
]
455455
},
456+
{
457+
"cell_type": "markdown",
458+
"metadata": {},
459+
"source": [
460+
"## Part 3 (Case Study): Contract Commitments\n",
461+
"\n",
462+
"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",
463+
"\n",
464+
"* Committing data to an OP_RETURN output requires an additional output with a zero amount, resulting in a higher transaction fees.\n",
465+
"* 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",
466+
"\n",
467+
"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."
468+
]
469+
},
470+
{
471+
"cell_type": "markdown",
472+
"metadata": {},
473+
"source": [
474+
"### Committing contract data to an OP_RETURN output\n",
475+
"\n",
476+
"We'll first show Alice's current setup: An OP_RETURN script containing commitment data."
477+
]
478+
},
479+
{
480+
"cell_type": "markdown",
481+
"metadata": {},
482+
"source": [
483+
"#### Example 2.2.10: Create the contract commitment"
484+
]
485+
},
486+
{
487+
"cell_type": "code",
488+
"execution_count": null,
489+
"metadata": {},
490+
"outputs": [],
491+
"source": [
492+
"contract_bytes = \"Alice pays 10 BTC to Bob\".encode('utf-8')\n",
493+
"commitment_bytes = sha256(contract_bytes)\n",
494+
"print(\"The contract commitment is: {}\".format(commitment_bytes.hex()))"
495+
]
496+
},
497+
{
498+
"cell_type": "markdown",
499+
"metadata": {},
500+
"source": [
501+
"#### Example 2.2.11: Create and broadcast a transaction with an OP_RETURN output\n",
502+
"\n",
503+
"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 P2WPKH output. This way, the commitment can be done more efficiently, by sharing transaction data with another spendable output."
504+
]
505+
},
506+
{
507+
"cell_type": "code",
508+
"execution_count": null,
509+
"metadata": {},
510+
"outputs": [],
511+
"source": [
512+
"# Start node\n",
513+
"test = util.TestWrapper()\n",
514+
"test.setup()\n",
515+
"node = test.nodes[0]\n",
516+
"\n",
517+
"# Construct a transaction with two outputs:\n",
518+
"# Output 0) Alice's destination address\n",
519+
"# Output 1) OP_RETURN with Alice's commitment\n",
520+
"address_alice = node.getnewaddress(address_type=\"bech32\")\n",
521+
"op_return_tx = node.generate_and_send_coins(address_alice, data=commitment_bytes.hex())\n",
522+
"\n",
523+
"# Confirm details of the OP_RETURN output\n",
524+
"data_output = op_return_tx.vout[1]\n",
525+
"print(\"The OP_RETURN output script is: {}\".format(data_output.scriptPubKey.hex()))\n",
526+
"print(\"The OP_RETURN output value is: {}\".format(data_output.nValue))\n",
527+
"\n",
528+
"# Note the total weight of the transaction with a dedicated OP_RETURN commitment output\n",
529+
"op_return_tx_hex = op_return_tx.serialize().hex()\n",
530+
"print(\"The total transaction weight is: {}\\n\".format(node.decoderawtransaction(op_return_tx_hex)['weight']))"
531+
]
532+
},
533+
{
534+
"cell_type": "markdown",
535+
"metadata": {},
536+
"source": [
537+
"### Committing contract data with the pay-to-contract scheme\n",
538+
"\n",
539+
"Next, we will commit Alice's contract to a spendable pay-to-pubkey output with the pay-to-contract commitment scheme."
540+
]
541+
},
542+
{
543+
"cell_type": "markdown",
544+
"metadata": {},
545+
"source": [
546+
"#### _Programming Exercise 2.2.12:_ Generate segwit v1 address for a pay-to-contract public key\n",
547+
"\n",
548+
"Commit the contract to Alice's public key with the pay-to-contract commitment scheme, and then generate the corresponding segwit v1 address."
549+
]
550+
},
551+
{
552+
"cell_type": "code",
553+
"execution_count": null,
554+
"metadata": {},
555+
"outputs": [],
556+
"source": [
557+
"# Generate a key pair\n",
558+
"privkey, pubkey = generate_key_pair()\n",
559+
"\n",
560+
"# Generate the pay-to-contract tweak\n",
561+
"contract_bytes = \"Alice pays 10 BTC to Bob\".encode('utf-8')\n",
562+
"tweak_private = # TODO: implement\n",
563+
"tweak_point = # TODO: implement\n",
564+
"\n",
565+
"# Tweak Alice's key pair with the pay-to-contract tweak\n",
566+
"tweaked_pubkey = # TODO: implement\n",
567+
"tweaked_privkey = # TODO: implement\n",
568+
"\n",
569+
"# Generate the segwit v1 address\n",
570+
"tweaked_pubkey_data = # TODO: implement\n",
571+
"tweaked_pubkey_program = # TODO: implement\n",
572+
"version = 1\n",
573+
"address = program_to_witness(version, tweaked_pubkey_program)\n",
574+
"print(\"Address encoding the segwit v1 output: \", address)"
575+
]
576+
},
577+
{
578+
"cell_type": "markdown",
579+
"metadata": {},
580+
"source": [
581+
"#### Example 2.2.13: Create a transaction with the Bitcoin Core wallet sending funds to the segwit v1 address\n"
582+
]
583+
},
584+
{
585+
"cell_type": "code",
586+
"execution_count": null,
587+
"metadata": {},
588+
"outputs": [],
589+
"source": [
590+
"test = util.TestWrapper()\n",
591+
"test.setup()\n",
592+
"node = test.nodes[0]\n",
593+
"\n",
594+
"# Generate coins and send coins to segwit v1 address containing the pay-to-contract public key\n",
595+
"tx = node.generate_and_send_coins(address)\n",
596+
"print(\"Transaction {}, output 0\\nSent to {}\\n\".format(tx.hash, address))\n",
597+
"print(\"Transaction weight with pay-to-contract: {}\".format(node.decoderawtransaction(tx.serialize().hex())['weight']))\n",
598+
"print(\"Transaction weight with OP_RETURN: {}\\n\".format(node.decoderawtransaction(op_return_tx_hex)['weight']))"
599+
]
600+
},
601+
{
602+
"cell_type": "markdown",
603+
"metadata": {},
604+
"source": [
605+
"#### _Programming Exercise 2.2.14:_ Verify that the contract between Alice and Bob is committed correctly\n",
606+
"\n",
607+
"Extract the witness program from the segwit v1 output and verify the pay-to-contract commitment."
608+
]
609+
},
610+
{
611+
"cell_type": "code",
612+
"execution_count": null,
613+
"metadata": {},
614+
"outputs": [],
615+
"source": [
616+
"# Fetch output from tx\n",
617+
"v1_output = tx.vout[0]\n",
618+
"\n",
619+
"# Extract witness program from the output script\n",
620+
"program = # TODO: implement\n",
621+
"\n",
622+
"# Reconstruct pay-to-contract public key\n",
623+
"tweaked_pubkey_bytes = # TODO: implement\n",
624+
"tweaked_pubkey = # TODO: implement\n",
625+
"\n",
626+
"# Verify pay-to-contract commitment is correct\n",
627+
"contract_bytes = \"Alice pays 10 BTC to Bob\".encode('utf-8')\n",
628+
"ss = pubkey.get_bytes()\n",
629+
"ss += sha256(contract_bytes)\n",
630+
"t = sha256(ss)\n",
631+
"assert pubkey.tweak_add(t) == tweaked_pubkey\n",
632+
"print(\"Contract commitment is correct!\")"
633+
]
634+
},
635+
{
636+
"cell_type": "markdown",
637+
"metadata": {},
638+
"source": [
639+
"#### Example 2.2.15: Construct a CTransaction to spend the pay-to-contract public key\n",
640+
"\n",
641+
"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."
642+
]
643+
},
644+
{
645+
"cell_type": "code",
646+
"execution_count": null,
647+
"metadata": {},
648+
"outputs": [],
649+
"source": [
650+
"# Create a spending transaction, which sends funds back to the wallet\n",
651+
"spending_tx = test.create_spending_transaction(tx.hash)\n",
652+
"print(\"Spending transaction:\\n{}\\n\".format(spending_tx))\n",
653+
"\n",
654+
"# Create sighash for ALL (0x00)\n",
655+
"sighash = TaprootSignatureHash(spending_tx, [tx.vout[0]], SIGHASH_ALL_TAPROOT, input_index=0)\n",
656+
"\n",
657+
"# Create a valid transaction signature for the tweaked public key\n",
658+
"sig = tweaked_privkey.sign_schnorr(sighash)\n",
659+
"print(\"Signature of tweaked keypair is {}\\n\".format(sig.hex()))\n",
660+
"\n",
661+
"# Construct transaction witness\n",
662+
"spending_tx.wit.vtxinwit.append(CTxInWitness([sig]))\n",
663+
"\n",
664+
"# Test mempool acceptance\n",
665+
"assert node.test_transaction(spending_tx)\n",
666+
"print(\"Success!\")"
667+
]
668+
},
669+
{
670+
"cell_type": "markdown",
671+
"metadata": {},
672+
"source": [
673+
"#### _Shutdown TestWrapper_"
674+
]
675+
},
676+
{
677+
"cell_type": "code",
678+
"execution_count": null,
679+
"metadata": {},
680+
"outputs": [],
681+
"source": [
682+
"# Shutdown\n",
683+
"test.shutdown()"
684+
]
685+
},
456686
{
457687
"cell_type": "markdown",
458688
"metadata": {},
@@ -461,7 +691,8 @@
461691
"\n",
462692
"- Learned how to tweak a public/private key pair with a value.\n",
463693
"- 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",
464-
"- Sent coins to a segwit v1 output with a tweaked public key, and later spent that output by signing with the tweaked private key."
694+
"- 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",
695+
"- Improved cost and privacy of a contract commitment by moving it from an unspendable OP_RETURN output to a pay-to-contract public key."
465696
]
466697
}
467698
],

solutions/2.2-taptweak-solutions.ipynb

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

0 commit comments

Comments
 (0)