Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
c8f3f1f
working musig
halseth Jan 13, 2025
f31c889
working musig with guest proving
halseth Jan 13, 2025
33995ca
remove single sig, move to using pk hash as unique ID
halseth Jan 13, 2025
860014f
dep: use rustreexo with fixex Stump serialization
halseth Jan 15, 2025
5f8d2c1
host+guest: check stump hash instead of full stump
halseth Jan 15, 2025
5d60e00
dep: use main rusttreexo
halseth Jan 16, 2025
c0108f8
working musig
halseth Jan 19, 2025
62542f9
guest: fix index into correct vector
halseth Jan 23, 2025
4effe93
make node_keys public
halseth Jan 23, 2025
075434b
start gossip doc
halseth Jan 23, 2025
962d336
print full pubkey
halseth Jan 27, 2025
8d24d54
move verification early
halseth Jan 27, 2025
007924d
print METHOD_ID
halseth Jan 27, 2025
a5ab45b
remove stump assert from verify_receipt
halseth Jan 27, 2025
a4f185c
print agg key
halseth Jan 27, 2025
836dcc3
docs: add musig2 verification example
halseth Jan 27, 2025
15cfb6a
docs: update ln_gossip
halseth Jan 28, 2025
bbf0105
dep: bump k256 lib
halseth Feb 11, 2025
55263b2
dep: update zkvm
halseth Feb 11, 2025
408e21f
dep: update musig2
halseth Feb 11, 2025
ae251d1
update risc0-build
halseth Feb 11, 2025
1bfa7d8
bump k256 precompile
halseth Feb 11, 2025
a76bf63
refactor imports + format
halseth Feb 12, 2025
c93c0df
shared: remove uneccesary pubkey parse
halseth Feb 12, 2025
fa8e6e7
shared: remove scalar conversion
halseth Feb 12, 2025
36287f8
shared: remove pubkey parsing
halseth Feb 12, 2025
3b67d8c
shared: remove x coordinate parsing
halseth Feb 12, 2025
4c31e6d
shared: remove ineffective patch
halseth Feb 12, 2025
8a2342d
docs/ln_gossip fixups
halseth Feb 13, 2025
50cb2c2
blind using beta*G
halseth Feb 18, 2025
3ee4630
shared: fix even/odd tweak add
halseth Feb 18, 2025
b01bdb7
shared: use custom txid calc
halseth Feb 18, 2025
235d8fc
input blind secret
halseth Feb 18, 2025
efacfca
bump rustreexo to v0.4.0
halseth Feb 19, 2025
1f91f11
shared: commit imports
halseth Feb 19, 2025
c85c919
shared: add secp_new_p2tr
halseth Feb 19, 2025
b8aedb9
shared: remove print
halseth Feb 19, 2025
ce75d42
change to sha256 leaf hashes
halseth Feb 20, 2025
505744f
shared: split tap_tweak into reusable tweak method
halseth Feb 20, 2025
c088178
f change to sha256
halseth Feb 20, 2025
7f7830f
change blinding to P = Q + h(r|Q)
halseth Feb 20, 2025
ce99972
cleanup, remove musig references
halseth Feb 20, 2025
ed69565
main: create derive option
halseth Feb 20, 2025
5d0010a
README: update to latest API
halseth Feb 20, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 77 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.

Expand All @@ -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

Expand All @@ -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:

```
Expand All @@ -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.
Expand All @@ -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):
Expand All @@ -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.

152 changes: 152 additions & 0 deletions docs/ln_gossip.md
Original file line number Diff line number Diff line change
@@ -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.

Loading