|
28 | 28 | "\n", |
29 | 29 | "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", |
30 | 30 | "\n", |
31 | | - "## Tweaking the Public Key\n", |
| 31 | + "## Part 1: Tweaking the Public Key\n", |
32 | 32 | "\n", |
33 | 33 | "Instead of using our original public key as the witness program, we use a tweaked public key.\n", |
34 | 34 | "\n", |
|
152 | 152 | "\n", |
153 | 153 | "\n", |
154 | 154 | "\n", |
155 | | - "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.**" |
| 155 | + "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.** " |
156 | 156 | ] |
157 | 157 | }, |
158 | 158 | { |
159 | 159 | "cell_type": "markdown", |
160 | 160 | "metadata": {}, |
161 | 161 | "source": [ |
162 | | - "#### Example 2.2.3: modifying the tweak for a tweaked public key Q\n", |
| 162 | + "#### Example 2.2.3: Tweaking a public key Q with commitment data\n", |
163 | 163 | "\n", |
164 | | - "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'`." |
| 164 | + "In this example we demonstrate an insecure commitment scheme. The committed value `c` can be trivially modified to `c'`. The public key point equation `Q = x'G + c'G` still holds and is equal to the same point Q. An alternative secret `x` can also be solved for.\n", |
| 165 | + "\n", |
| 166 | + "First, we commit a contract between Alice and Bob and then demonstrate how this unsafe commitment can be changed.\n", |
| 167 | + "\n", |
| 168 | + "* The initial committed contract is: `Alice pays 10 BTC to Bob`" |
165 | 169 | ] |
166 | 170 | }, |
167 | 171 | { |
|
170 | 174 | "metadata": {}, |
171 | 175 | "outputs": [], |
172 | 176 | "source": [ |
173 | | - "# Generate a key pair\n", |
174 | | - "x, P = generate_key_pair()\n", |
175 | | - "print(\"Private key: {}\\nPublic key: {}\\n\".format(x.secret, P.get_bytes().hex()))\n", |
| 177 | + "# Alice generates a key pair\n", |
| 178 | + "x_key, P_key = generate_key_pair()\n", |
| 179 | + "print(\"Private key: {}\\nPublic key: {}\\n\".format(x_key.secret, P_key.get_bytes().hex()))\n", |
176 | 180 | "\n", |
177 | | - "# Tweak the public key\n", |
178 | | - "t = random.randrange(1, SECP256K1_ORDER)\n", |
179 | | - "print(\"Tweak: {}\".format(t))\n", |
180 | | - "Q = P.tweak_add(t)\n", |
| 181 | + "# Alice generates the tweak from the contract\n", |
| 182 | + "contract = \"Alice agrees to pay 10 BTC to Bob\"\n", |
| 183 | + "t = sha256(contract.encode('utf-8'))\n", |
| 184 | + "print(\"Tweak from original contract: {}\\n\".format(t.hex()))\n", |
| 185 | + "\n", |
| 186 | + "# Alice tweaks her key pair\n", |
| 187 | + "Q_key = P_key.tweak_add(t)\n", |
| 188 | + "q_key = x_key.add(t)\n", |
| 189 | + "print(\"Tweaked private key: {}\\nTweaked public key: {}\\n\".format(q_key.secret, Q_key.get_bytes().hex()))\n", |
181 | 190 | "\n", |
182 | | - "# Create a fake tweak\n", |
183 | | - "t2 = random.randrange(1, SECP256K1_ORDER)\n", |
184 | | - "print(\"Tweak 2: {}\\n\".format(t2))\n", |
| 191 | + "# Alice produces a valid signature for this tweaked public key\n", |
| 192 | + "msg = sha256(b'I agree to the committed contract')\n", |
| 193 | + "sig = q_key.sign_schnorr(msg)\n", |
185 | 194 | "\n", |
186 | | - "# Solve: x` = x - t' + t\n", |
187 | | - "x_int = x.as_int()\n", |
188 | | - "x2_int = (x_int - t2 + t) % SECP256K1_ORDER\n", |
| 195 | + "# Given P, Q and the contract, Bob believes he can verify the following:\n", |
189 | 196 | "\n", |
190 | | - "x2_key = x = ECKey().set(x2_int, True)\n", |
191 | | - "P2 = x2_key.get_pubkey()\n", |
192 | | - "Q2 = P2.tweak_add(t2)\n", |
| 197 | + "# 1) The contract 'Alice agrees to pay 10 BTC to Bob' is committed to public key Q\n", |
| 198 | + "print(\"'Alice agrees to pay 10 BTC to Bob' appears to be committed to Q: {}\".format(\\\n", |
| 199 | + " P_key.tweak_add(sha256(contract.encode('utf-8'))) == Q_key))\n", |
193 | 200 | "\n", |
194 | | - "print(\"Tweaked pubkey for x tweaked by t: {}\".format(Q.get_bytes().hex()))\n", |
195 | | - "print(\"Tweaked pubkey for x2 tweaked by t2: {}\".format(Q2.get_bytes().hex()))" |
| 201 | + "# 2) Alice is the owner of the public key Q = P + t*G\n", |
| 202 | + "print(\"Alice has produced a valid signature for Q: {}\".format(Q_key.verify_schnorr(sig, msg)))" |
196 | 203 | ] |
197 | 204 | }, |
198 | 205 | { |
199 | 206 | "cell_type": "markdown", |
200 | 207 | "metadata": {}, |
201 | 208 | "source": [ |
202 | | - "#### Example 2.2.4 - Tweaking the pubkey with `H(P|msg)`\n", |
| 209 | + "#### Example 2.2.4: Modifying the commitment tweak of public key Q\n", |
203 | 210 | "\n", |
204 | | - "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'`." |
| 211 | + "However, note that is possible for Alice to modify this insecure commitment without changing the value of pub key `Q`.\n", |
| 212 | + "* The committed contract is changed to : `Alice pays 0.1 BTC to Bob`" |
205 | 213 | ] |
206 | 214 | }, |
207 | 215 | { |
|
210 | 218 | "metadata": {}, |
211 | 219 | "outputs": [], |
212 | 220 | "source": [ |
213 | | - "# Key pair generation\n", |
214 | | - "privkey1, pubkey1 = generate_key_pair()\n", |
215 | | - "print(\"Private key: {}\\nPublic key: {}\\n\".format(x.secret, pubkey.get_bytes().hex()))\n", |
| 221 | + "# Alice modifies the contract and produces an alternative tweak\n", |
| 222 | + "alternative_contract = \"Alice agrees to pay 0.1 BTC to Bob\"\n", |
| 223 | + "t2 = sha256(alternative_contract.encode('utf-8'))\n", |
| 224 | + "print(\"Tweak from original contract: {}\".format(t.hex()))\n", |
| 225 | + "print(\"Tweak from modified contract: {}\\n\".format(t2.hex()))\n", |
| 226 | + "\n", |
| 227 | + "# Alice modifies her original secret and public key\n", |
| 228 | + "# x` = x - t' + t\n", |
| 229 | + "x_int = x_key.as_int()\n", |
| 230 | + "t_int = int.from_bytes(t, \"big\") \n", |
| 231 | + "t2_int = int.from_bytes(t2, \"big\") \n", |
| 232 | + "x2_int = (x_int - t2_int + t_int) % SECP256K1_ORDER\n", |
| 233 | + "x2_key = ECKey().set(x2_int, True)\n", |
| 234 | + "P2_key = x2_key.get_pubkey()\n", |
| 235 | + "\n", |
| 236 | + "# The resulting tweaked public Q key remains the same\n", |
| 237 | + "Q2_key = P2_key.tweak_add(t2)\n", |
| 238 | + "print(\"Tweaked public key from original tweak t: {}\".format(Q_key.get_bytes().hex()))\n", |
| 239 | + "print(\"Tweaked public key from modified tweak t2: {}\\n\".format(Q2_key.get_bytes().hex()))\n", |
| 240 | + "\n", |
| 241 | + "# So Alice can still produce a valid signature for Q\n", |
| 242 | + "msg2 = sha256(b'I agree to the committed contract')\n", |
| 243 | + "sig2 = q_key.sign_schnorr(msg2)\n", |
| 244 | + "\n", |
| 245 | + "# Given P2, Q and the modified contract, \n", |
| 246 | + "# Alice can demonstrate the following to invalidate the original contract:\n", |
| 247 | + "\n", |
| 248 | + "# 1) The modified contract 'Alice agrees to pay 0.1 BTC to Bob' appears to be committed to public key Q\n", |
| 249 | + "print(\"'Alice agrees to pay 0.1 BTC to Bob' appears to be committed to Q: {}\"\\\n", |
| 250 | + " .format(P2_key.tweak_add(sha256(alternative_contract.encode('utf-8'))) == Q_key))\n", |
| 251 | + "\n", |
| 252 | + "# 2) Alice is still the owner of the public key Q = P2 + t2*G\n", |
| 253 | + "print(\"Alice has produced a valid signature for Q: {}\".format(Q_key.verify_schnorr(sig2, msg)))" |
| 254 | + ] |
| 255 | + }, |
| 256 | + { |
| 257 | + "cell_type": "markdown", |
| 258 | + "metadata": {}, |
| 259 | + "source": [ |
| 260 | + "#### Summary of 2.2.3, 2.2.4: Insecure practice of tweaking a public key with commitment data\n", |
| 261 | + "\n", |
| 262 | + "We have demonstrated how a simple key tweak with commitment data does not work as a commitment scheme.\n", |
| 263 | + "* Tweaking the original public key `P` with commitment data hides the commitment.\n", |
| 264 | + "* However, the original public key `P` can be recomputed (`P2`) for any modified commitment, without altering the tweaked public key `Q`.\n", |
| 265 | + "\n", |
| 266 | + "To any observer, both original and modified \"commitments\" appear to be valid for the same public key `Q`." |
| 267 | + ] |
| 268 | + }, |
| 269 | + { |
| 270 | + "cell_type": "markdown", |
| 271 | + "metadata": {}, |
| 272 | + "source": [ |
| 273 | + "#### Example 2.2.5 - Pay-to-contract: Tweaking the pubkey with `H(P|msg)`\n", |
| 274 | + "\n", |
| 275 | + "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 `c` by modifying `x'`.\n", |
216 | 276 | "\n", |
217 | | - "# Compute the tweak from H(P|msg)\n", |
218 | | - "commitment = b'commitment'\n", |
219 | | - "ss = sha256(pubkey.get_bytes())\n", |
220 | | - "ss += sha256(commitment)\n", |
| 277 | + "* Alice can now no longer invalidate her previous contract commitment with Bob." |
| 278 | + ] |
| 279 | + }, |
| 280 | + { |
| 281 | + "cell_type": "code", |
| 282 | + "execution_count": null, |
| 283 | + "metadata": {}, |
| 284 | + "outputs": [], |
| 285 | + "source": [ |
| 286 | + "# Alice generates a key pair\n", |
| 287 | + "x_key, P_key = generate_key_pair()\n", |
| 288 | + "print(\"Private key: {}\\nPublic key: {}\\n\".format(x_key.secret, P_key.get_bytes().hex()))\n", |
| 289 | + "\n", |
| 290 | + "# Alice computes the tweak from H(P|msg)\n", |
| 291 | + "contract = \"Alice agrees to pay 10 BTC to Bob\"\n", |
| 292 | + "ss = P_key.get_bytes()\n", |
| 293 | + "ss += sha256(contract.encode('utf-8'))\n", |
221 | 294 | "t = sha256(ss)\n", |
222 | 295 | "\n", |
223 | | - "# Determine tweak point\n", |
224 | | - "tweak = ECKey().set(t, True)\n", |
225 | | - "tweak_point = tweak.get_pubkey()\n", |
226 | | - "print(\"Tweak scalar: {}\\nTweak point: {}\\n\".format(tweak.secret, tweak_point.get_bytes().hex()))\n", |
| 296 | + "# Alice tweaks her key pair\n", |
| 297 | + "t_key = ECKey().set(t, True)\n", |
| 298 | + "T_key = t_key.get_pubkey()\n", |
| 299 | + "q_key = x_key + t_key\n", |
| 300 | + "Q_key = P_key + T_key\n", |
| 301 | + "print(\"Tweak scalar: {}\\nTweak point: {}\\n\".format(t_key.secret, T_key.get_bytes().hex()))\n", |
227 | 302 | "\n", |
228 | | - "privkey_tweaked = privkey + tweak\n", |
229 | | - "pubkey_tweaked = pubkey + tweak_point\n", |
| 303 | + "# Alice signs a valid message\n", |
| 304 | + "msg = sha256(b'I agree to the committed contract')\n", |
| 305 | + "sig = q_key.sign_schnorr(msg)\n", |
230 | 306 | "\n", |
231 | | - "# Sign message and verify signature\n", |
232 | | - "msg = sha256(b'msg')\n", |
233 | | - "sig = privkey_tweaked.sign_schnorr(msg)\n", |
| 307 | + "# Given pubkey1, pubkey_tweaked and the contract, Bob can verify the following:\n", |
234 | 308 | "\n", |
235 | | - "assert pubkey_tweaked.verify_schnorr(sig, msg)\n", |
236 | | - "print(\"Success!\")" |
| 309 | + "# 1) The contract 'Alice agrees to pay 10 BTC to Bob' is committed to public key Q\n", |
| 310 | + "print(\"'Alice agrees to pay 10 BTC to Bob' is committed to Q: {}\"\\\n", |
| 311 | + " .format(P_key.tweak_add(sha256(P_key.get_bytes() + sha256(contract.encode('utf-8')))) == Q_key))\n", |
| 312 | + "\n", |
| 313 | + "# 2) Alice is the owner of the public key P + t*G = Q\n", |
| 314 | + "print(\"Alice has produced a valid signature for Q: {}\".format(Q_key.verify_schnorr(sig, msg)))" |
| 315 | + ] |
| 316 | + }, |
| 317 | + { |
| 318 | + "cell_type": "markdown", |
| 319 | + "metadata": {}, |
| 320 | + "source": [ |
| 321 | + "**Note: A pay-to-contract commitment does not require the schnorr signature scheme.** Since we are tweaking both private and public keys with the same tweak, the key pair remains valid for any compatible signature scheme, such as ECDSA." |
237 | 322 | ] |
238 | 323 | }, |
239 | 324 | { |
240 | 325 | "cell_type": "markdown", |
241 | 326 | "metadata": {}, |
242 | 327 | "source": [ |
243 | | - "## Spending a taproot output along the key path\n", |
| 328 | + "## Part 2: Spending a taproot output along the key path\n", |
244 | 329 | "\n", |
245 | | - "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", |
| 330 | + "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", |
246 | 331 | "\n", |
247 | | - "Such as spend does not reveal the committed tweak to the observer and is indistinguishable any other key path spend." |
| 332 | + "Such as spend does not reveal the committed tweak to the observer and is indistinguishable from any other key path spend." |
248 | 333 | ] |
249 | 334 | }, |
250 | 335 | { |
251 | 336 | "cell_type": "markdown", |
252 | 337 | "metadata": {}, |
253 | 338 | "source": [ |
254 | | - "#### _Programming Exercise 2.2.5:_ Construct taproot output with tweaked public key" |
| 339 | + "#### _Programming Exercise 2.2.6:_ Construct taproot output with tweaked public key" |
255 | 340 | ] |
256 | 341 | }, |
257 | 342 | { |
|
284 | 369 | "cell_type": "markdown", |
285 | 370 | "metadata": {}, |
286 | 371 | "source": [ |
287 | | - "#### Example 2.2.6: Start Bitcoin Core node and send coins to the taproot address\n", |
| 372 | + "#### Example 2.2.7: Start Bitcoin Core node and send coins to the taproot address\n", |
288 | 373 | "\n", |
289 | 374 | "Only run setup once, or after a clean shutdown." |
290 | 375 | ] |
|
309 | 394 | "cell_type": "markdown", |
310 | 395 | "metadata": {}, |
311 | 396 | "source": [ |
312 | | - "#### Example 2.2.7: Construct `CTransaction` and populate inputs\n", |
| 397 | + "#### Example 2.2.8: Construct `CTransaction` and populate inputs\n", |
313 | 398 | "\n", |
314 | 399 | "We use the `create_spending_transaction(node, txid)` convenience function." |
315 | 400 | ] |
|
329 | 414 | "cell_type": "markdown", |
330 | 415 | "metadata": {}, |
331 | 416 | "source": [ |
332 | | - "#### _Programming Exercise 2.2.8:_ Spend taproot output with key path" |
| 417 | + "#### _Programming Exercise 2.2.9:_ Spend taproot output with key path" |
333 | 418 | ] |
334 | 419 | }, |
335 | 420 | { |
|
0 commit comments