diff --git a/README.md b/README.md index 361a058..46b163b 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,10 @@ # OutputZero -`OutputZero` is a proof of concept tool for proving Bitcoin UTXO set inclusion in -zero knowledge. + +`OutputZero` is a proof of concept tool for proving Bitcoin UTXO set inclusion +in zero knowledge. ## Applications -Since unspent transaction outputs is a scare resource, having a way of +Since unspent transaction outputs is a scarce resource, having a way of cryptographically prove you own one without revealing anything about the output is useful for all sorts of anti-DOS applications. @@ -20,20 +21,41 @@ The tool works with accumulators and proofs from a the [rpc-utreexo-bridge](https://github.com/Davidson-Souza/rpc-utreexo-bridge), which acts as a utreexo bridge to Bitcoin Core. -After being given the utreexo accumulator and proof, the prover signs a message -using the private key for the output with public key `P`, proving that he -controls the coins. +The prover starts by creating a regular Bitcoin Taproot public key `P`. This +could be any valid taproot internal key, for instance a Musig2 aggregate key. + +The prover then chooses a random blinding secret `r` that will be used to blind +the key before using it: + +``` +beta = hash(r || P) +P_out = P + beta * G +``` + +The `beta` acts as a obfuscator to the key that goes onchain, making it +impossible to derive the link between `P` and `P_out` without knowledge of +`beta`. + +Now the prover can send money to `P_out`, manifesting it as an output on-chain. + +Proving control of the UTXO now goes as follows: +- The prover can create a signature for an arbitrary message using public key + `P`, proving ownership. The prover then creates a ZK-STARK proof using the [Risc0 ZKVM](https://github.com/risc0/risc0) that proves the following: -- The prover has a valid signature for an arbitrary message for a public key - `P`, where `P = x * G`. The message and `hash(x)`is shown to the verifier. -- The prover has a proof showing that the public key P is found in the Utreexo - set. The Utreexo root is shown to the verifier. +- The prover has a secret `r` such that +``` +beta = hash(r || P) +P_out = P + beta * G +``` +- The prover has a proof showing that the public key `P_out` is found in the + Utreexo set. The Utreexo root hash is shown to the verifier. -This ZK-proof is convincing the verifier that the prover has the private key to -the output in the UTXO set. +This ZK-proof is convincing the verifier that the prover is able to sign for +the output in the UTXO set (if he can sign for `P` and knows `beta` then he can +also sign for `P_out`). ## Quick start @@ -48,7 +70,9 @@ $ bitcoind --signet --txindex Now we set ut a utreexo bridge that will index the chain and create the inclusion proofs we need: - Install the bridge according to - [rpc-utreexo-bridge](https://github.com/Davidson-Souza/rpc-utreexo-bridge). + [rpc-utreexo-bridge](https://github.com/Davidson-Souza/rpc-utreexo-bridge) + making sure using this revision: + https://github.com/halseth/rpc-utreexo-bridge/commit/4a49a589018c22da67061b5e233fe8ff45670f4a. - Set environment variables to match the bitcoind instance: ``` @@ -61,32 +85,52 @@ Start the bridge and let it index while you continue to the next step: $ bridge --network signet ``` -Now we can create an address using OutputZero, and send som signet coins to it: +Now create a public key and a random secret and pass it to OutputZero: ```bash -$ cargo run --release -- --priv-key "new" -priv: 6fc5d9e0dcd0cad79cea037a28850abe4a661d7a2c2de72311feea912acc5dbf -pub: bd70caa34056cc4bb2b66f44e038c52f1f87f4fb20703f6209617bb58a032a5d -address: tb1pnpvxrjhlwzn7rfggv2tvx508tuvha38ez3x993r865cxcn3xrexqn9t6jl +$ cargo run -- --pubkey "027536f0c851f239cca5d97f5c6af058fd07b7c688480b3619d764ef3ca89a63e4" --blind-secret-hex "a3b1c5d2e7f9a8b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4" --derive +sec1 pub: 027536f0c851f239cca5d97f5c6af058fd07b7c688480b3619d764ef3ca89a63e4 +blinded tap key : 02b123a2aa4184bacf8d7879023c6dc053aeddd64ed4ca3bc6ce3519c95e1b4c75 +xonly pub: b123a2aa4184bacf8d7879023c6dc053aeddd64ed4ca3bc6ce3519c95e1b4c75 +address: tb1pu3l57s32yhmegykrq6a7pe7e05ckvdwka9yvr72hlhx8dwh5kesqudhsq7 ``` You can now fund the given address with some signetBTC, then wait for the transaction to confirm and Bitcoin Core to sync to the block (feel free to use -the above private key or deposit tx for testing, but please don't spend the coins). +the above keys or deposit tx for testing). After having the coins confirmed, we will get the utreexo accumulator and -proofs from the bridge (TODO: show how to get leaf hash): +proofs from the bridge: ``` -$ curl http://127.0.0.1:3000/prove/3baea3c5fbc3afb0ec11379416a68a1e2a64df318ea611f58213e87c50d8ccd1 | jq -c '.data' > proof.json +$ curl http://127.0.0.1:3000/leaf/ad62462baf935489ab6633563c1b11859f292fe355eff7eb0c90b2a0a3e3ab0e:1 | jq +{ + "data": { + "hash": "600faadb708702e433dda62aee7aa1e712d7fa58c2886c7d0a273910724744db", + "leaf_data": { + "block_hash": "000000531547656158a86c79718462f348c7c2f5d7aff509905b4c0cc9fd79c1", + "block_height": 236206, + "hash": "600faadb708702e433dda62aee7aa1e712d7fa58c2886c7d0a273910724744db", + "is_coinbase": false, + "prevout": "ad62462baf935489ab6633563c1b11859f292fe355eff7eb0c90b2a0a3e3ab0e:1", + "utxo": { + "script_pubkey": "5120e47f4f422a25f79412c306bbe0e7d97d316635d6e948c1f957fdcc76baf4b660", + "value": 10000 + } + } + }, + "error": null +} +$ curl http://127.0.0.1:3000/prove/600faadb708702e433dda62aee7aa1e712d7fa58c2886c7d0a273910724744db | jq -c '.data' > proof.json $ curl http://127.0.0.1:3000/acc | jq -c '.data' > acc.json -$ bitcoin-cli --signet getrawtransaction 48356de0a84cd6022ff84a70f805922ec7c799c1a01d683b8c906d38824e71e2 > tx.hex +$ bitcoin-cli --signet getrawtransaction ad62462baf935489ab6633563c1b11859f292fe355eff7eb0c90b2a0a3e3ab0e > tx.hex ``` -Now we can run OutputZero with these proofs, in addition to some metadata about the tx and block it confirmed in: +Now we can run OutputZero with these proofs, in addition to some metadata about +the tx and block it confirmed in: ```bash -$ cargo run --release -- --utreexo-acc "`cat acc.json`" --utreexo-proof "`cat proof.json`" --leaf-hash '3baea3c5fbc3afb0ec11379416a68a1e2a64df318ea611f58213e87c50d8ccd1' --prove --priv-key '6fc5d9e0dcd0cad79cea037a28850abe4a661d7a2c2de72311feea912acc5dbf' --receipt-file 'receipt.bin' --msg 'this is message' --tx-hex "`cat tx.hex`" --vout 1 --block-height 226735 --block-hash '00000019cfb5ef098766c4602dbfbb7351ad61a71c2f451d80feb2eb65563b63' +$ cargo run --release -- --utreexo-acc "`cat acc.json`" --utreexo-proof "`cat proof.json`" --leaf-hash '600faadb708702e433dda62aee7aa1e712d7fa58c2886c7d0a273910724744db' --receipt-file 'receipt.bin' --tx-hex "`cat tx.hex`" --vout 1 --block-height 236206 --block-hash '000000531547656158a86c79718462f348c7c2f5d7aff509905b4c0cc9fd79c1' --proof-type 'default' --pubkey "027536f0c851f239cca5d97f5c6af058fd07b7c688480b3619d764ef3ca89a63e4" --blind-secret-hex "a3b1c5d2e7f9a8b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4" ``` This command will create a ZK proof as detailed in the Architecture section. @@ -97,22 +141,22 @@ independently. The proof can be verified using ```bash -cargo run --release -- --utreexo-acc "`cat acc.json`" --receipt-file 'receipt.bin' --msg 'this is message' +cargo run --release -- --utreexo-acc "`cat acc.json`" --receipt-file 'receipt.bin' --verify ``` -Note that the the accumulator needed to verify the proof is the same one needed -to create it. But since utreexo accumulators are deterministic, it can be +Note that the accumulator needed to verify the proof is the same one needed to +create it. But since utreexo accumulators are deterministic, it can be independently created by the verifier as long as it is communicated which block height one is using when creating the proof. -## Benchmarks, Apple M1 Max -- Proving time is about 48 seconds. -- Verification time is ~254 ms. -- Proof size is 1.4 MB. +## Benchmarks, Apple M1 Max (succint proof type) +- Proving time is about 15 seconds. +- Verification time is ~55 ms. +- Proof size is 223 kB. ## Limitations -This is a rough first draft of how a tool like this could look like. It has -plenty of known limitations and should absolutely not be used with private keys +This is a rough draft of how a tool like this could look like. It has +plenty of known limitations and should absolutely not be used with keys controlling real (mainnet) coins. A non-exhaustive list (some of these could be relatively easy to fix): @@ -121,8 +165,5 @@ A non-exhaustive list (some of these could be relatively easy to fix): - Only supports testnet3 and signet. - Only proving existence, selectively revealing more about the output is not supported. -- Proving time is not optimized. -- Proof size is not attempted optimized. -- Private key must be hot. - ... and many more. diff --git a/docs/ln_gossip.md b/docs/ln_gossip.md new file mode 100644 index 0000000..e85eff8 --- /dev/null +++ b/docs/ln_gossip.md @@ -0,0 +1,152 @@ +## Privacy preserving Lightning gossip + +This document describes a proposal for making Lightning channel gossip more +private, avoiding the need for revealing the channel outpoint. + +It is based on Utreexo and zero-knowledge proofs, and is accompanied with a +proof-of-concept Rust implementation. + +The proposal is created as an extension to the gossip v1.75 proposal for +taproot channel gossip and intended to be used as an optional feature for +privacy conscious users. + +### Privacy of Lightning channel gossip +TODO + +### Taproot gossip (gossip v1.75) +See Elle's deep dive here: [Updates to the Gossip 1.75 proposal post LN summit meeting](https://delvingbitcoin.org/t/updates-to-the-gossip-1-75-proposal-post-ln-summit-meeting/1202). + +Tl;dr: a new `channel_announcement_2` message that carries a Musig2 signature +proving the two nodes control a certain UTXO. + +Example `channel_announcement_2`: +```json +{ + "ChainHash": "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f", + "ShortChannelID": "1000:100:0", + "Capacity": 100000, + "NodeID1": "0246175dd633eaa1c1684a9e15d218d247e421b009642f3e82e9f138ad9d645e03", + "NodeID2": "02b651f157c5cf7bfbf07d52855c21aca861d7e3b8711d84cb04149e0a67151e16", + "BitcoinKey1": "027fc1e3f6f5a67b804cb86d813adf94f89ea1de63f630607d4c31242d58306043", + "BitcoinKey2": "03fef5084b98aa37757acce81d25148cfdb9592142567c6265e39b082e73c4d54", + "MerkleRootHash": null, + "Signature": { + "bytes": "5c14ad15b614c9f91fd5c66b7bfe3f3552427c6d5e6d598f5838c5d219cdd0b89c72ad6a3effe5d995387563b80dfb1b59da599c936c705ad8dfd6da8288b89b", + "sigType": 1 + }, +} +``` + +## ZK-gossip +What we propose is an extension to the taproot gossip proposal, that makes it +possible for the two channel parties to remove the link between the channel and +on-chain output. + +In order to still be able to protect the network from channel DOS attacks, we +require the channel annoucement message to include a ZK-proof that proves the +inclusion of the channel in the UTXO set, and that it is controlled by the two +nodes in the graph. + +In order to create the ZK proof with these properties, we start with the data +already contained in the regular taproot gossip channel announcment: + +1) `node_id_1`, `node_id_2` +2) `bitcoin_key_1`, `bitcoin_key_2` +3) `merkle_root_hash` +4) `signature` +5) `capacity` + +(we'll ignore the `merkle_root_hash` for now). + +In addition we assemble a Utreexo accumulator and a proof for the channel +output's inclusion in this accumulator. + +Using these pieces of data we create a ZK-proof that validates the following: + +1) `bitcoin_keys = MuSig2.KeySort(bitcoin_key_1, bitcoin_key_2)` +2) `P_agg_out = MuSig2.KeyAgg(bitcoin_keys)` +3) Check `capacity <= vout.value` +4) Check `P_agg_out = vout.script_pubkey` +3) Verify that `vout` is in the UTXO set using the utreexo accumulator and proof. +4) `P_agg = MuSig2.KeyAgg(MuSig2.KeySort(node_id_1, node_id_2, bitcoin_key_1, bitcoin_key_2))` +5) Verify the signaure against `P_agg` +6) `pk_hash = hash(bitcoin_keys[0] || bitcoin_keys[1])` + +We then output (or make public) the two `node_ids`, the signed data, utreexo accumulator and `pk_hash`. + +Now we can output a proof (ZK-SNARK, groth16) of 256 bytes, and assemble a new +`channel_announcement_zk` (since messages are TLV, this should really be +combined with the `channel_announcement_2` with appropriate fields set): + +```json +{ + "ChainHash": "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f", + "Capacity": 100000, + "NodeID1": "0246175dd633eaa1c1684a9e15d218d247e421b009642f3e82e9f138ad9d645e03", + "NodeID2": "02b651f157c5cf7bfbf07d52855c21aca861d7e3b8711d84cb04149e0a67151e16", + "UtreexoRoot": "4b07311e3e774f0db3bcedee47c56fe30da326d9271d1a2558a5975ab4214478", + "ZKType": "9bce41211c9d71e1ed07a2a5244f95ab98b0ba3a6e95dda9c87ba071ff871418", + "ZKProof": "6508d8476086238076bb673005f9ef3bfe7f0c198a1d4f6fcee65e19478b422c512aefd004f8f476d0ef5939dc4339e3e19347a6ab60fe5714e9d3e3e77417499dbf18da68dfd942d79c8bf4cf811f615334f4643befb267a189d8e6b05509760bfd7add9aa9ecbce38db277bf11b1b94e147b504e75be5405066421aad8e10b49d105a33241742bafe611b4025ffa35d066fc87e11df595030d18b962ad5917ef1f73c97d660c1e62c7e392d51821ec342b2faf763d2a9177d13471c8b2a829578fd401d76aa8ae5642937f48573e657a5af14fda5f7a39216dda05b183121913088d2d0e0c1902d1f656b5d769b95040a40ef5a9ffd87f550545b0a5bc2505", + "PKHash": "be7d602934c5ce95000ee989748f6c892ce16fb4276389ec15bc0764fbc4bea5" +} +``` + +`ZKType` is the unique identifier for the verification program, and is often a +unique hash of the binary representation of the verifier. This makes it easy to +move to a new proof type. `pk_hash` acts as a unique channel identifier. + +(note: the `pk_hash` is not unique if the two nodes reuse their public keys for +a new output. Maybe this can be used to move the channel to a bigger UTXO +without closing it...) + +### Creating proofs +When opening a channel, the two channel counterparties must create the ZK-proof +in order to announce the channel. In the current POC this is requires a decent +hardware setup in order to be effective (~minutes on a beefy laptop). + +It should be noted that only nodes announcing public channels need to do this, +and they usually require a certain level of hardware to be effective routers +anyway. + +It is also assumed that proving time will come down as advances are made in +proof systems and hardware acceleration. + +### Handling received channel_announcement_zk +When a node receives a `channel_announcement_zk` message, it will first use +the `pk_hash` to check whether this is a channel already known to the node. The +`pk_hash` is deterministic and unique per channel. It will then verify the +proof if it has a type known to the node. Otherwise it will ignore it. + +Since we can no longer detect channels closing on-chain, we must require +periodic refreshes of announcememnts, proving the channel is still in the utxo +set. We propose setting this to around two weeks. With legacy channels we +already have the problem of not knowing whether an channel unspent on-chain is +active, so some kind of liveness signal is needed regardless. + +### Caveats +- Proving is slow. + - One is not really in a hurry announcing a channel, so this is most likely not a problem as these get optimized. +- Proofs are large. + - STARKS (using Risc0 as in the proof-of-concept) are around 200kB. + - Wrapping them in groth16 brings this down to 200 bytes. +- Verification is slow-ish. + - This can likely be heavily optimized. + - groth16 proofs are much faster to verify than STARKS. + +### Wins +- This can be used today, no softforks needed :) +- Easier to be a light-node. + - Light clients need to get a periodic refresh of their Utreexp accumulator + (from a trusted source, or a semi-trusted source that can prove that the + accumulator is correctly crafted). + - With the accumulator they can validate the proofs just as a full node. + +### Proof-of-concept +I've preperad a branch with accompanying code and documents walking through the +process of creating a proof from the original channel announcment: [Musig2 +example](https://github.com/halseth/output-zero/blob/musig2/docs/musig2.md). + +It is based on RiscZero, a versatile framework for creating proofs of execution +for RISC-V binaries. This means that is easy to add more contrainsts to the +verification of the UTXOs if useful. + diff --git a/docs/musig2.md b/docs/musig2.md new file mode 100644 index 0000000..d32a070 --- /dev/null +++ b/docs/musig2.md @@ -0,0 +1,133 @@ +## Musig2 example + +As outlined in the [Private Lightning Gossip](./ln_gossip.md) doc, a potential +use case of OutputZero is to make LN gossip more private. In order for this to +be realized, verification of Musig2 key aggregation in the ZK environment is +performed. + +In this example we'll show how this is done. + +### Create private keys and sign +The Musig2 key used in Taproot Lightning gossip is aggergated from 4 individual public keys. We start by creating 4 keypairs and specifying a message to sign. + +```bash +$ cargo run --release -- --node-key-1-priv "new" --node-key-2-priv "new" --bitcoin-key-1-priv "new" --bitcoin-key-2-priv "new" --msg-hex "`echo "this message" | xxd -p`" --network "signet" +node_key_1: +priv: 121f7492040ebd1a484b433ba661258963d7d36cfaeab7084e520ecba4e4c1b4 +pubkey: 02081f8dbfea223289d2a160cf321bea267390550c3f32c372a655f2f907cea504 +xonly pub: 081f8dbfea223289d2a160cf321bea267390550c3f32c372a655f2f907cea504 +address: tb1ppqym9scg7p66pscvjlelcmx445vsh9lq9v3mv93232hqvd7dppesp3mr34 +node_key_2: +priv: 09951546ca93633974dc37dfc0fa3fba4fc882189a0c047eead735927aeb6ef5 +pubkey: 03b73eef25490c7f00afa268cac13ae55005c60bdf322dd0a1c54e5cd06da0ef7e +xonly pub: b73eef25490c7f00afa268cac13ae55005c60bdf322dd0a1c54e5cd06da0ef7e +address: tb1pkvt6fv629lcjptgfglftjxmnwpzs6c4detk2sa437wn5smuj54psx5nfyn +bitcoin_key_1: +priv: a7862a16105e330c3d31a584df5f41c5a43e457d2d1fdb73cee76f92754ae863 +pubkey: 027536f0c851f239cca5d97f5c6af058fd07b7c688480b3619d764ef3ca89a63e4 +xonly pub: 7536f0c851f239cca5d97f5c6af058fd07b7c688480b3619d764ef3ca89a63e4 +address: tb1prglt49pm50dtfj8wj3l7tqc94m8ytw4xm7x5jkn7famjx83nydpqkl08w7 +bitcoin_key_2: +priv: e40871a76761ae4554e2c2c07e2bc80e6e0dc6c1b25792e1851cf7deb369573d +pubkey: 03a048f128b57e5750f7dcb177d7f4280cf529301801cb3f028eb9746b9228f731 +xonly pub: a048f128b57e5750f7dcb177d7f4280cf529301801cb3f028eb9746b9228f731 +address: tb1pjrshj2h0yyds3c279xlz9e9uelvc7hzfdagamzrknp79xz0yr7ns7sj0wz +... +tap key : 0236827c6ebdd86cf2172a2caac10ef08ca3d7643021ccf16c8eca11b393ace4e8 +pub: 36827c6ebdd86cf2172a2caac10ef08ca3d7643021ccf16c8eca11b393ace4e8 +address: tb1px5m3w7t2v4nk8pxeaacgc3nn8vulqakfe2uzkavpy0q24ct3n36qghem5d +signing +... +aggregate key : 0205948efcf1da93342484e6df5000b9314765ac3566476ba3549cf4ecfa54fbf8 +musig sig: e9b9814e617421ca7d8c3c11979f5373fb6749d59f542edb5381be56682cd62f048988a148354c8e3cfabaf5126497d7ddff1c4e62efe5003629f358f9e47c75 +musig successfully verified +``` + +### Funding the output +Now that we have created the keys, we also have the possibility to send money +to them. So we'll go ahead and send some signet coins to the address of the +aggregate tap key (`agg[bitcoin_key1, bitcoin_key2]`): +`tb1px5m3w7t2v4nk8pxeaacgc3nn8vulqakfe2uzkavpy0q24ct3n36qghem5d`. + +In my case this resulted in the following transaction: [2a1550a17ec661037145443d3e6bbeb378ff0ee446ecd6ca85d866020c1435b6](https://mempool.space/signet/tx/2a1550a17ec661037145443d3e6bbeb378ff0ee446ecd6ca85d866020c1435b6). + +After this confirms we have an on-chain output we want to sign for _without +revealing which one it is._ + +### Getting the Utreexo proof +Make sure you have a running bitcoind on signet, and a [utreexo bridge +node](https://github.com/Davidson-Souza/rpc-utreexo-bridge) connected to it. + +We start by finding the _leaf hash_ for the output we just created. + +```bash +$ curl http://127.0.0.1:3000/leaf/2a1550a17ec661037145443d3e6bbeb378ff0ee446ecd6ca85d866020c1435b6:1 | jq +{ + "data": { + "hash": "31e66cb9812a9a66706696b226c175b7abbde6dfb98eae3e777162b7a1be731d", + "leaf_data": { + "block_hash": "000000038ea165485e344192ec434db1a90624dbc78bab14b6fe452645748923", + "block_height": 232415, + "hash": "31e66cb9812a9a66706696b226c175b7abbde6dfb98eae3e777162b7a1be731d", + "is_coinbase": false, + "prevout": "2a1550a17ec661037145443d3e6bbeb378ff0ee446ecd6ca85d866020c1435b6:1", + "utxo": { + "script_pubkey": "5120353717796a65676384d9ef708c46733b39f076c9cab82b758123c0aae1719c74", + "value": 553396 + } + } + }, + "error": null +} +``` + +We'll get the utreexo accumulator and the proof for the inclusion of this leaf +hash into the accumulator (we'll store these to file): + +```bash +$ curl http://127.0.0.1:3000/prove/31e66cb9812a9a66706696b226c175b7abbde6dfb98eae3e777162b7a1be731d | jq -c '.data' > proof_utreexo.json +$ curl http://127.0.0.1:3000/acc | jq -c '.data' > acc_utreexo.json +$ bitcoin-cli --signet getrawtransaction 2a1550a17ec661037145443d3e6bbeb378ff0ee446ecd6ca85d866020c1435b6 > tx.hex +``` + +### Generate the proof +Now we have all pieces ready to generate the full proof: + +```bash +$ cargo run --release -- --utreexo-acc "`cat acc_utreexo.json`" --utreexo-proof "`cat proof_utreexo.json`" --leaf-hash '31e66cb9812a9a66706696b226c175b7abbde6dfb98eae3e777162b7a1be731d' --prove --receipt-file 'receipt.bin' --msg-hex "`echo "this message" | xxd -p`" --tx-hex "`cat tx.hex`" --vout 1 --block-height 232415 --block-hash '000000038ea165485e344192ec434db1a90624dbc78bab14b6fe452645748923' --node-key-1 "02081f8dbfea223289d2a160cf321bea267390550c3f32c372a655f2f907cea504" --node-key-2 "03b73eef25490c7f00afa268cac13ae55005c60bdf322dd0a1c54e5cd06da0ef7e" --bitcoin-key-1 "027536f0c851f239cca5d97f5c6af058fd07b7c688480b3619d764ef3ca89a63e4" --bitcoin-key-2 "03a048f128b57e5750f7dcb177d7f4280cf529301801cb3f028eb9746b9228f731" --proof-type 'default' --musig-sig 'e9b9814e617421ca7d8c3c11979f5373fb6749d59f542edb5381be56682cd62f048988a148354c8e3cfabaf5126497d7ddff1c4e62efe5003629f358f9e47c75' +... +Proving took 84.877619s +committed node_key1 : 02081f8dbfea223289d2a160cf321bea267390550c3f32c372a655f2f907cea504 +committed node_key2 : 03b73eef25490c7f00afa268cac13ae55005c60bdf322dd0a1c54e5cd06da0ef7e +bitcoin keys hash: ec938aa2258d7369dada32facf0e8c10c67db48fc6c15a7bb2bbf931900e9d3e +signed msg: 74686973206d6573736167650a +stump hash: e48b939f7fe439c15cc0665d926f6d5f66a0355d60b1518ba7f2c0f79b233a15 +verified METHOD_ID=9bce41211c9d71e1ed07a2a5244f95ab98b0ba3a6e95dda9c87ba071ff871418 +receipt (2228856). seal size: 2225440. +``` + +### Proof types +Risc0 has support for a few different proof types. The deafault is a composite +proof, which can be reduced in size by using the compressed succint proof type. +These are both variants of ZK-STARKS. + +One can also specify the `groth16` proof type (currently only available on x86 +hardware), which will wrap the STARK proof in a ZK-SNARK and dramastically +reduce the proof size! We are talking a proof size of 256 bytes. More details +on the various proof types here: [Risc0 Proof System +Overview](https://dev.risczero.com/proof-system/). + +### Verification +We can verify the proof by ommitting the `--prove` flag: + +```bash +$ cargo run --release -- --receipt-file 'receipt.bin' +... +committed node_key1 : 02081f8dbfea223289d2a160cf321bea267390550c3f32c372a655f2f907cea504 +committed node_key2 : 03b73eef25490c7f00afa268cac13ae55005c60bdf322dd0a1c54e5cd06da0ef7e +bitcoin keys hash: ec938aa2258d7369dada32facf0e8c10c67db48fc6c15a7bb2bbf931900e9d3e +signed msg: 74686973206d6573736167650a +stump hash: e48b939f7fe439c15cc0665d926f6d5f66a0355d60b1518ba7f2c0f79b233a15 +verified METHOD_ID=9bce41211c9d71e1ed07a2a5244f95ab98b0ba3a6e95dda9c87ba071ff871418 +receipt verified in 411.941ms +``` diff --git a/host/Cargo.toml b/host/Cargo.toml index 995d61c..f493139 100644 --- a/host/Cargo.toml +++ b/host/Cargo.toml @@ -6,10 +6,10 @@ edition = "2021" [dependencies] methods = { path = "../methods" } shared = { path = "../shared" } -risc0-zkvm = { version = "1.2.0"} +risc0-zkvm = { version = "1.2.3" , features = ["unstable"]} tracing-subscriber = { version = "0.3", features = ["env-filter"] } serde = "1.0" -rustreexo = { version = "0.3.0", features = ["with-serde"] } +rustreexo = { git = "https://github.com/halseth/rustreexo", rev = "289b27b", features = ["with-serde"] } bitcoin = { version = "0.32.5", features = ["std", "rand-std", "serde"] } bincode = "1.3.3" hex = { version = "0.4.3", default-features = false, features = ["alloc"] } @@ -18,7 +18,9 @@ sha2 = "0.10.8" bitcoin_hashes = "0.14.0" k256 = { version = "0.13.3", features = ["serde"] } serde_json = "1.0.128" +musig2 = { version = "0.2.3", default-features = false, features = ["k256"] } +secp256k1 = "0.30.0" [features] cuda = ["risc0-zkvm/cuda"] -default = [] \ No newline at end of file +default = [] diff --git a/host/src/main.rs b/host/src/main.rs index ac2254c..7d7cca7 100644 --- a/host/src/main.rs +++ b/host/src/main.rs @@ -4,7 +4,6 @@ use std::fs::File; use methods::{METHOD_ELF, METHOD_ID}; use risc0_zkvm::{default_prover, ExecutorEnv, ProverOpts, Receipt}; -use bitcoin_hashes::sha256; use bitcoin_hashes::Hash as BitcoinHash; use clap::Parser; @@ -14,28 +13,27 @@ use rustreexo::accumulator::stump::Stump; use std::str::FromStr; use std::time::SystemTime; -use bitcoin::consensus::{deserialize}; -use bitcoin::key::Keypair; -use bitcoin::secp256k1::{rand, Message, Secp256k1, SecretKey, Signing}; -use bitcoin::{Address, BlockHash, Network, ScriptBuf, Transaction}; -use k256::schnorr; +use bitcoin::consensus::deserialize; +use bitcoin::secp256k1::{Secp256k1, Verification}; +use bitcoin::{Address, BlockHash, Network, ScriptBuf, TapTweakHash, Transaction, XOnlyPublicKey}; +use clap::builder::TypedValueParser; use k256::schnorr::signature::Verifier; use rustreexo::accumulator::proof::Proof; use serde::{Deserialize, Serialize}; -use shared::get_leaf_hashes; - -fn gen_keypair(secp: &Secp256k1) -> Keypair { - let sk = SecretKey::new(&mut rand::thread_rng()); - Keypair::from_secret_key(secp, &sk) -} +use k256::PublicKey; +use sha2::{Digest, Sha256}; +use shared::{get_leaf_hashes, tweak_pubkey}; /// utxozkp #[derive(Debug, Parser)] #[command(verbatim_doc_comment)] struct Args { #[arg(short, long, default_value_t = false)] - prove: bool, + verify: bool, + + #[arg(short, long, default_value_t = false)] + derive: bool, #[arg(long)] proof_type: Option, @@ -65,14 +63,11 @@ struct Args { #[arg(long)] vout: Option, - /// Message to sign. - #[arg(short, long)] - msg: Option, + #[arg(long)] + pubkey: Option, - /// Sign the message using the given private key. Pass "new" to generate one at random. Leave - /// this blank if verifying a receipt. #[arg(long)] - priv_key: Option, + blind_secret_hex: Option, /// Network to use. #[arg(long, default_value_t = Network::Testnet)] @@ -91,6 +86,25 @@ struct CliStump { pub leaves: u64, } +fn parse_pubkey(pub_str: &str) -> PublicKey { + let pk_bytes = hex::decode(pub_str).unwrap(); + let pk = PublicKey::from_sec1_bytes(&pk_bytes).unwrap(); + + println!("sec1 pub: {}", hex::encode(pk_bytes)); + + pk +} + +fn address(secp: &Secp256k1, pubkey: PublicKey, network: Network) { + let pub_bytes: [u8; 32] = pubkey.to_sec1_bytes()[1..].try_into().unwrap(); + let pubx = XOnlyPublicKey::from_slice(&pub_bytes).unwrap(); + + let script_buf = ScriptBuf::new_p2tr(&secp, pubx, None); + let addr = Address::from_script(script_buf.as_script(), network).unwrap(); + println!("xonly pub: {}", pubx); + println!("address: {}", addr); +} + fn main() { // Initialize tracing. In order to view logs, run `RUST_LOG=info cargo run` tracing_subscriber::fmt() @@ -99,48 +113,43 @@ fn main() { let args = Args::parse(); + // If not proving, simply verify the passed receipt using the loaded utxo set. + let start_time = SystemTime::now(); + if args.verify{ + let receipt_file = args.receipt_file.unwrap(); + let r = File::open(receipt_file).unwrap(); + let receipt: Receipt = bincode::deserialize_from(r).unwrap(); + verify_receipt(&receipt); + println!("receipt verified in {:?}", start_time.elapsed().unwrap()); + return; + } + let secp = Secp256k1::new(); let network = args.network; - // Generate a new keypair or use the given private key. - let keypair = match args.priv_key.as_deref() { - Some(priv_str) => { - let keypair = if priv_str == "new" { - gen_keypair(&secp) - } else { - let sk = SecretKey::from_str(&priv_str).unwrap(); - Keypair::from_secret_key(&secp, &sk) - }; - - let (internal_key, _parity) = keypair.x_only_public_key(); - let script_buf = ScriptBuf::new_p2tr(&secp, internal_key, None); - let addr = Address::from_script(script_buf.as_script(), network).unwrap(); - println!("priv: {}", hex::encode(keypair.secret_key().secret_bytes())); - println!("pub: {}", internal_key); - println!("address: {}", addr); - - if priv_str == "new" { - return; - } - - Some(keypair) - } - _ => { - if args.prove { - println!("priv key needed"); - return; - } - None - } - }; + let pub_bitcoin = parse_pubkey(&args.pubkey.unwrap()); + let blind_str = args.blind_secret_hex.unwrap(); + let blind_bytes: [u8; 32] = hex::decode(blind_str).unwrap().try_into().unwrap(); - let receipt_file = if args.prove { - let r = File::create(args.receipt_file.unwrap()).unwrap(); - r - } else { - let r = File::open(args.receipt_file.unwrap()).unwrap(); - r - }; + // Blinding beta = h(r || P) + let beta: [u8; 32] = Sha256::new() + .chain_update(blind_bytes) + .chain_update(pub_bitcoin.to_sec1_bytes()) + .finalize() + .try_into() + .unwrap(); + + let tap_blind_point = tweak_pubkey(pub_bitcoin, &beta); + let tap_blind_key: PublicKey = tap_blind_point.try_into().unwrap(); + println!( + "blinded tap key : {}", + hex::encode(&tap_blind_key.to_sec1_bytes()) + ); + address(&secp, tap_blind_key, network); + + if args.derive { + return; + } let acc: CliStump = serde_json::from_str(&args.utreexo_acc.unwrap()).unwrap(); let acc = Stump { @@ -152,16 +161,6 @@ fn main() { .collect(), }; - let start_time = SystemTime::now(); - - // If not proving, simply verify the passed receipt using the loaded utxo set. - if !args.prove { - let receipt: Receipt = bincode::deserialize_from(receipt_file).unwrap(); - verify_receipt(&receipt, &acc); - println!("receipt verified in {:?}", start_time.elapsed().unwrap()); - return; - } - let proof_type: ProverOpts = match args.proof_type.as_deref() { None => { println!("using default proof type"); @@ -193,12 +192,6 @@ fn main() { } }; - let msg_to_sign = args.msg.unwrap(); - let msg_bytes = msg_to_sign.as_bytes(); - let digest = sha256::Hash::hash(msg_bytes); - let digest_bytes = digest.to_byte_array(); - let msg = Message::from_digest(digest_bytes); - let proof: CliProof = serde_json::from_str(&args.utreexo_proof.unwrap()).unwrap(); let proof = Proof { targets: proof.targets, @@ -219,65 +212,36 @@ fn main() { let block_hash: BlockHash = BlockHash::from_str(&args.block_hash.unwrap()).unwrap(); let lh = get_leaf_hashes(&tx, vout, block_height, block_hash); - println!("lh: {:?}", lh); - let lh = NodeHash::from(lh); assert_eq!(lh, leaf_hash); // We will prove inclusion in the UTXO set of the key we control. - let (internal_key, _parity) = keypair.unwrap().x_only_public_key(); - let priv_bytes = keypair.unwrap().secret_key().secret_bytes(); - let priv_key = schnorr::SigningKey::from_bytes(&priv_bytes).unwrap(); - let script_pubkey = ScriptBuf::new_p2tr(&secp, internal_key, None); - + let tap_bytes = tap_blind_key.to_sec1_bytes(); + let internal_key = XOnlyPublicKey::from_slice(&tap_bytes[1..]).unwrap(); + println!("xonly tap key: {}", hex::encode(internal_key.serialize())); + + // Assume not tap tweak. + // TODO: add support for this. + let tweak_hash = TapTweakHash::from_key_and_tweak(internal_key, None); + println!("secp tweak hash: {}", tweak_hash); + + // Sanity check the two p2tr implementation. + let script_pubkey = shared::secp_new_p2tr(&secp, internal_key, None); + let script_pub2 = shared::new_p2tr(tap_blind_key, None); + assert_eq!(script_pub2, script_pubkey); assert_eq!(tx.output[vout as usize].script_pubkey, script_pubkey); println!("proving {}", leaf_hash); - println!("proof: {:?}", proof); assert_eq!(acc.verify(&proof, &[leaf_hash]), Ok(true)); println!("stump proof verified"); - // Sign using the tweaked key. - let sig = secp.sign_schnorr(&msg, &keypair.unwrap()); - - // Verify signature. - let (pubkey, _) = keypair.unwrap().x_only_public_key(); - println!("pubkey: {}", pubkey); - - let sig_bytes = sig.serialize(); - println!("secp signature: {}", hex::encode(sig_bytes)); - secp.verify_schnorr(&sig, &msg, &pubkey) - .expect("secp verification failed"); - - let pub_bytes = pubkey.serialize(); - - println!("creating verifying key"); - let verifying_key = schnorr::VerifyingKey::from_bytes(&pub_bytes).unwrap(); - println!( - "created verifying key: {}", - hex::encode(verifying_key.to_bytes()) - ); - - let schnorr_sig = schnorr::Signature::try_from(sig_bytes.as_slice()).unwrap(); - println!("schnorr signature: {}", hex::encode(schnorr_sig.to_bytes())); - - verifying_key - .verify(msg_bytes, &schnorr_sig) - .expect("schnorr verification failed"); - let start_time = SystemTime::now(); let env = ExecutorEnv::builder() - .write(&msg_bytes) - .unwrap() - .write(&priv_key) - .unwrap() .write(&acc) .unwrap() .write(&proof) .unwrap() - .write(&sig_bytes.as_slice()) - .unwrap() .write(&tx) .unwrap() .write(&vout) @@ -286,6 +250,12 @@ fn main() { .unwrap() .write(&block_hash) .unwrap() + // Pubkey + .write(&pub_bitcoin) + .unwrap() + // Blinding secret + .write(&blind_bytes) + .unwrap() .build() .unwrap(); @@ -294,30 +264,47 @@ fn main() { // Proof information by proving the specified ELF binary. // This struct contains the receipt along with statistics about execution of the guest - let prove_info = prover.prove_with_opts(env, METHOD_ELF, &proof_type).unwrap(); + let prove_info = prover + .prove_with_opts(env, METHOD_ELF, &proof_type) + .unwrap(); println!("Proving took {:?}", start_time.elapsed().unwrap()); // extract the receipt. let receipt = prove_info.receipt; - verify_receipt(&receipt, &acc); + verify_receipt(&receipt); let seal_size = receipt.seal_size(); let receipt_bytes = bincode::serialize(&receipt).unwrap(); println!("receipt ({}). seal size: {seal_size}.", receipt_bytes.len()); - bincode::serialize_into(receipt_file, &receipt).unwrap(); + let receipt_file = args.receipt_file.unwrap(); + let r = File::create(receipt_file).unwrap(); + bincode::serialize_into(r, &receipt).unwrap(); } -fn verify_receipt(receipt: &Receipt, s: &Stump) { - let (receipt_stump, sk_hash, msg): (Stump, String, String) = receipt.journal.decode().unwrap(); +fn verify_receipt(receipt: &Receipt) { + let (pubkey, stump_hash): (PublicKey, String) = receipt.journal.decode().unwrap(); - assert_eq!(&receipt_stump, s, "stumps not equal"); + println!( + "unblinded pubkey: {}", + hex::encode(&pubkey.to_sec1_bytes()) + ); + println!("stump hash: {}", stump_hash); - // The receipt was verified at the end of proving, but the below code is an - // example of how someone else could verify this receipt. receipt.verify(METHOD_ID).unwrap(); - println!("priv key hash: {}", sk_hash); - println!("signed msg: {}", msg); + println!("verified METHOD_ID={}", hex::encode(to_bytes(METHOD_ID))); +} + +fn to_bytes(h: [u32; 8]) -> [u8; 32] { + let mut buf = [0u8; 32]; + for i in 0..8 { + let b: [u8; 4] = h[i].to_be_bytes(); + for j in 0..4 { + buf[i * 4 + j] = b[j]; + } + } + + buf } diff --git a/methods/Cargo.toml b/methods/Cargo.toml index bed6bf8..623020b 100644 --- a/methods/Cargo.toml +++ b/methods/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [build-dependencies] -risc0-build = { version = "1.0.5" } +risc0-build = { version = "1.2.3" , features = ["unstable"]} [package.metadata.risc0] methods = ["guest"] diff --git a/methods/guest/Cargo.toml b/methods/guest/Cargo.toml index 162e9b3..75ccc49 100644 --- a/methods/guest/Cargo.toml +++ b/methods/guest/Cargo.toml @@ -7,18 +7,17 @@ edition = "2021" [dependencies] shared = { path = "../../shared" } -risc0-zkvm = { version = "1.2.0", default-features = false, features = ['std'] } -rustreexo = { version = "0.3.0", features = ["with-serde"] } +risc0-zkvm = { version = "1.2.3", default-features = false, features = ['std', 'unstable'] } +rustreexo = { git = "https://github.com/halseth/rustreexo", rev = "289b27b", features = ["with-serde"] } serde = "1.0" bitcoin = { version = "0.32.5", features = ["serde"] } hex = { version = "0.4.3", default-features = false, features = ["alloc"] } sha2 = "0.10.8" -k256 = { version = "=0.13.3", features = ["arithmetic", "serde", "expose-field", "std", "ecdsa", "pkcs8", "schnorr"], default-features = false } -bitcoin_hashes = "0.14.0" +k256 = { version = "=0.13.4", features = ["arithmetic", "serde", "expose-field", "std", "ecdsa", "pkcs8", "schnorr"], default-features = false } [patch.crates-io] # Placing these patch statement in the workspace Cargo.toml will add RISC Zero SHA-256 and bigint # multiplication accelerator support for all downstream usages of the following crates. sha2 = { git = "https://github.com/risc0/RustCrypto-hashes", tag = "sha2-v0.10.8-risczero.0" } -k256 = { git = "https://github.com/risc0/RustCrypto-elliptic-curves", tag = "k256/v0.13.3-risczero.0" } +k256 = { git = "https://github.com/risc0/RustCrypto-elliptic-curves", tag = "k256/v0.13.4-risczero.1" } crypto-bigint = { git = "https://github.com/risc0/RustCrypto-crypto-bigint", tag = "v0.5.5-risczero.0" } \ No newline at end of file diff --git a/methods/guest/src/main.rs b/methods/guest/src/main.rs index a4dca79..f4e5d26 100644 --- a/methods/guest/src/main.rs +++ b/methods/guest/src/main.rs @@ -4,103 +4,65 @@ use risc0_zkvm::guest::env; use rustreexo::accumulator::node_hash::NodeHash; use rustreexo::accumulator::proof::Proof; use rustreexo::accumulator::stump::Stump; -use sha2::{Digest, Sha512_256}; - -use bitcoin::key::{UntweakedPublicKey}; -use bitcoin::{ScriptBuf, Transaction, BlockHash, TapNodeHash, TapTweakHash, WitnessVersion, XOnlyPublicKey}; -use bitcoin::script::{Builder, PushBytes}; -use k256::schnorr; -use k256::schnorr::signature::Verifier; -use k256::elliptic_curve::sec1::ToEncodedPoint; - -use shared::get_leaf_hashes; - -pub fn new_p2tr( - internal_key: UntweakedPublicKey, - merkle_root: Option, -) -> ScriptBuf { - let output_key = tap_tweak(internal_key, merkle_root); - // output key is 32 bytes long, so it's safe to use `new_witness_program_unchecked` (Segwitv1) - new_witness_program_unchecked(WitnessVersion::V1, output_key.serialize()) -} - -fn new_witness_program_unchecked>( - version: WitnessVersion, - program: T, -) -> ScriptBuf { - let program = program.as_ref(); - debug_assert!(program.len() >= 2 && program.len() <= 40); - // In segwit v0, the program must be 20 or 32 bytes long. - debug_assert!(version != WitnessVersion::V0 || program.len() == 20 || program.len() == 32); - Builder::new().push_opcode(version.into()).push_slice(program).into_script() -} - - -fn tap_tweak( - internal_key: UntweakedPublicKey, - merkle_root: Option, -) -> XOnlyPublicKey { - let tweak = TapTweakHash::from_key_and_tweak(internal_key, merkle_root).to_scalar(); - - let pub_bytes = internal_key.serialize(); - let pub_key : k256::PublicKey = schnorr::VerifyingKey::from_bytes(&pub_bytes).unwrap().into(); - let pub_point = pub_key.to_projective(); - - let tweak_bytes = &tweak.to_be_bytes(); - let tweak_point = k256::SecretKey::from_bytes(tweak_bytes.into()).unwrap().public_key().to_projective(); - - let tweaked_point = pub_point + tweak_point; - let compressed = tweaked_point.to_encoded_point(true); - let x_coordinate = compressed.x().unwrap(); - - let ver_key = schnorr::VerifyingKey::from_bytes(&x_coordinate).unwrap(); - - let pubx = XOnlyPublicKey::from_slice(ver_key.to_bytes().as_slice()).unwrap(); - - pubx -} +use sha2::{Digest, Sha256}; +use bitcoin::{Transaction, BlockHash, XOnlyPublicKey}; +use k256::PublicKey; +use k256::SecretKey; + +use shared::{get_leaf_hashes, new_p2tr, tweak_pubkey}; + fn main() { + //TODO: take in nodeid1, nodeid2, bitcoinkey1, bitcoinkey2 need tweak? + // check combining bitcoin keys give a key that is in the UTXO set. + // combine all 4 keys and check that the signature is valid for the aggregate key + // How to avoid proof reuse? cannot do hash of priv key easily, since there are two nodes maybe + // do hash of the individual public keys? since they won't ever go onchain + + // read the input - let msg_bytes: Vec = env::read(); - let priv_key: schnorr::SigningKey = env::read(); let s: Stump = env::read(); let proof: Proof = env::read(); - let sig_bytes: Vec = env::read(); let tx: Transaction = env::read(); let vout: u32 = env::read(); let block_height: u32 = env::read(); let block_hash: BlockHash = env::read(); - let lh = get_leaf_hashes(&tx, vout, block_height, block_hash); - let leaf_hash = NodeHash::from(lh); + // P + blinding key + let p_out: PublicKey = env::read(); + let blind_secret_bytes: [u8; 32] = env::read(); + eprintln!("blind_secret_bytes: {}", hex::encode(blind_secret_bytes)); - let internal_key = priv_key.verifying_key(); + // Blinding beta = h(r || P) + let beta: [u8; 32] = Sha256::new() + .chain_update(blind_secret_bytes) + .chain_update(p_out.to_sec1_bytes()) + .finalize() + .try_into() + .unwrap(); + + let tap_point = tweak_pubkey(p_out, &beta); + let tap_pub: PublicKey = tap_point.try_into().unwrap(); + eprintln!("tap blind key : {}", hex::encode(&tap_pub.to_sec1_bytes())); // We'll check that the given public key corresponds to an output in the utxo set. - let pubx = XOnlyPublicKey::from_slice(internal_key.to_bytes().as_slice()).unwrap(); - let script_pubkey = new_p2tr(pubx, None); + let script_pubkey = new_p2tr(tap_pub, None); // assert internal key is in tx used to calc leaf hash assert_eq!(tx.output[vout as usize].script_pubkey, script_pubkey); + let lh = get_leaf_hashes(&tx, vout, block_height, block_hash); + let leaf_hash = NodeHash::from(lh); + // Assert it is in the set. assert_eq!(s.verify(&proof, &[leaf_hash]), Ok(true)); - let mut hasher = Sha512_256::new(); - hasher.update(&priv_key.to_bytes()); - let sk_hash = hex::encode(hasher.finalize()); - let msg = from_utf8(msg_bytes.as_slice()).unwrap(); - - let schnorr_sig = schnorr::Signature::try_from(sig_bytes.as_slice()).unwrap(); - - internal_key - .verify(msg_bytes.as_slice(), &schnorr_sig) - .expect("schnorr verification failed"); + let mut shasher = Sha256::new(); + s.serialize(&mut shasher).unwrap(); + let stump_hash = hex::encode(shasher.finalize()); // write public output to the journal - env::commit(&s); - env::commit(&sk_hash); - env::commit(&msg); + env::commit(&p_out); + env::commit(&stump_hash); } \ No newline at end of file diff --git a/shared/Cargo.toml b/shared/Cargo.toml index 90a457b..d2094dd 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -6,4 +6,5 @@ edition = "2021" [dependencies] bitcoin = { version = "0.32.5"} sha2 = "0.10.8" -bitcoin_hashes = "0.14.0" \ No newline at end of file +bitcoin_hashes = "0.14.0" +musig2 = { version = "0.2.3", default-features = false, features = ["k256"] } \ No newline at end of file diff --git a/shared/src/lib.rs b/shared/src/lib.rs index f1de370..463754d 100644 --- a/shared/src/lib.rs +++ b/shared/src/lib.rs @@ -1,10 +1,23 @@ -use bitcoin_hashes::sha256; +use bitcoin_hashes::HashEngine; use bitcoin_hashes::Hash as BitcoinHash; -use sha2::{Digest, Sha512_256}; +use sha2::{Digest, Sha256 }; use bitcoin::consensus::Encodable; -use bitcoin::{BlockHash, Transaction}; +use bitcoin::key::{ + Parity, Secp256k1, UntweakedPublicKey, Verification, +}; +use bitcoin::script::{Builder, PushBytes}; +use bitcoin::{ + BlockHash, ScriptBuf, TapNodeHash, TapTweakHash, Transaction, Txid, WitnessVersion, + XOnlyPublicKey, +}; +use k256::PublicKey; +use k256::ProjectivePoint; + +use musig2::k256::elliptic_curve::point::AffineCoordinates; +use musig2::k256::elliptic_curve::sec1::ToEncodedPoint; +use musig2::k256; pub const UTREEXO_TAG_V1: [u8; 64] = [ 0x5b, 0x83, 0x2d, 0xb8, 0xca, 0x26, 0xc2, 0x5b, 0xe1, 0xc5, 0x42, 0xd6, 0xcc, 0xed, 0xdd, 0xa8, @@ -18,7 +31,7 @@ pub fn get_leaf_hashes( vout: u32, height: u32, block_hash: BlockHash, -) -> sha256::Hash { +) -> [u8; 32] { let header_code = height << 1; let mut ser_utxo = Vec::new(); @@ -29,10 +42,10 @@ pub fn get_leaf_hashes( } else { header_code }; - let txid = transaction.compute_txid(); + let txid = compute_txid(&transaction); println!("txid: {txid}, block_hash: {block_hash} vout: {vout} height: {height}"); - let leaf_hash = Sha512_256::new() + let leaf_hash = Sha256::new() .chain_update(UTREEXO_TAG_V1) .chain_update(UTREEXO_TAG_V1) .chain_update(block_hash) @@ -41,5 +54,100 @@ pub fn get_leaf_hashes( .chain_update(header_code.to_le_bytes()) .chain_update(ser_utxo) .finalize(); - sha256::Hash::from_slice(leaf_hash.as_slice()).expect("parent_hash: Engines shouldn't be Err") + leaf_hash.try_into().unwrap() +} + +pub fn compute_txid(tx: &Transaction) -> Txid { + let mut enc = Vec::new(); + tx.version.consensus_encode(&mut enc).expect("engines don't error"); + tx.input.consensus_encode(&mut enc).expect("engines don't error"); + tx.output.consensus_encode(&mut enc).expect("engines don't error"); + tx.lock_time.consensus_encode(&mut enc).expect("engines don't error"); + + // Compute double SHA-256 hash + let hash_result = Sha256::digest(Sha256::digest(&enc)); + + // Convert the hash result to a Txid + Txid::from_slice(&hash_result).expect("hash should be valid Txid") +} + + +pub fn new_p2tr(internal_key: PublicKey, merkle_root: Option) -> ScriptBuf { + let output_key = tap_tweak(internal_key, merkle_root); + // output key is 32 bytes long, so it's safe to use `new_witness_program_unchecked` (Segwitv1) + new_witness_program_unchecked(WitnessVersion::V1, output_key) +} + +fn new_witness_program_unchecked>( + version: WitnessVersion, + program: T, +) -> ScriptBuf { + let program = program.as_ref(); + debug_assert!(program.len() >= 2 && program.len() <= 40); + // In segwit v0, the program must be 20 or 32 bytes long. + debug_assert!(version != WitnessVersion::V0 || program.len() == 20 || program.len() == 32); + Builder::new() + .push_opcode(version.into()) + .push_slice(program) + .into_script() +} + +pub fn secp_new_p2tr( + secp: &Secp256k1, + internal_key: UntweakedPublicKey, + merkle_root: Option, +) -> ScriptBuf { + let (output_key, _) = secp_tap_tweak(internal_key, secp, merkle_root); + // output key is 32 bytes long, so it's safe to use `new_witness_program_unchecked` (Segwitv1) + new_witness_program_unchecked(WitnessVersion::V1, output_key.serialize()) +} +fn secp_tap_tweak( + internal_key: UntweakedPublicKey, + secp: &Secp256k1, + merkle_root: Option, +) -> (XOnlyPublicKey, Parity) { + let tweak_hash = TapTweakHash::from_key_and_tweak(internal_key, merkle_root); + println!("secp tweak hash: {}", tweak_hash); + let tweak = tweak_hash.to_scalar(); + + let (output_key, parity) = internal_key + .add_tweak(secp, &tweak) + .expect("Tap tweak failed"); + + (output_key, parity) +} + +fn tap_tweak(internal_key: PublicKey, merkue_root: Option) -> [u8; 32] { + let x_only_bytes : [u8; 32]= internal_key.to_sec1_bytes()[1..].try_into().unwrap(); + let mut eng = TapTweakHash::engine(); + eng.input(&x_only_bytes); + let tweak_hash = TapTweakHash::from_engine(eng); + + + let tweak_bytes = tweak_hash.to_byte_array(); + + let tweaked_point = tweak_pubkey(internal_key, &tweak_bytes); + let compressed = tweaked_point.to_encoded_point(true); + let x_coordinate = compressed.x().unwrap(); + + let pubx: [u8; 32] = x_coordinate.as_slice().try_into().unwrap(); + + pubx +} + +pub fn tweak_pubkey(pubkey: PublicKey, tweak_bytes: &[u8; 32]) -> ProjectivePoint { + let tweak_point = k256::SecretKey::from_bytes(tweak_bytes.into()) + .unwrap() + .public_key() + .to_projective(); + + let pub_point = pubkey.to_projective(); + let pub_affine = pubkey.as_affine(); + let tweaked = if pub_affine.y_is_odd().unwrap_u8() == 1 { + tweak_point - pub_point + } else { + pub_point + tweak_point + }; + + tweaked }