From 29c75efa30267c9f1be2eeea742a6ae6c606872e Mon Sep 17 00:00:00 2001 From: conduition Date: Mon, 27 May 2024 15:47:22 +0000 Subject: [PATCH 01/18] Add spending condition trees to multiplex NUT-10 --- sct.md | 194 ++++++++++++++++++++++++++++++++++++++++++ tests/sct-tests.md.md | 158 ++++++++++++++++++++++++++++++++++ 2 files changed, 352 insertions(+) create mode 100644 sct.md create mode 100644 tests/sct-tests.md.md diff --git a/sct.md b/sct.md new file mode 100644 index 00000000..ccc32664 --- /dev/null +++ b/sct.md @@ -0,0 +1,194 @@ +NUT-SCT: Spending Condition Trees (SCT) +========================== + +`optional`, `depends on: NUT-10` + +--- + +This NUT describes a [NUT-10] spending condition called a Spending Condition Tree (SCT). An ecash token locked with an SCT is spendable under a set of conditions, but at spend-time only a single condition must be revealed to the mint. + +# Definitions + +The following section describes some functions which wallets and mints need to compose and verify Spending Condition Trees. Reference code in python is included for clarity. + +### `SHA256(data: bytes) -> bytes` + +The SHA256 hash function. + +```python +import hashlib + +def SHA256(data: bytes) -> bytes: + h = hashlib.sha256() + h.update(data) + return h.digest() +``` + +### `sorted_merkle_hash(left: bytes, right: bytes) -> bytes` + +Hashes two internal merkle node hashes, sorting them lexicographically so that left/right position in the tree is irrelevant. + +```python +def sorted_merkle_hash(left: bytes, right: bytes) -> bytes: + if right < left: + left, right = right, left + return SHA256(left + right) +``` + +### `merkle_root(leaf_hashes: List[bytes]) -> bytes` + +Compute the merkle root of a set of leaf hashes. Each branch hash is derived using `sorted_merkle_hash()`, so that left/right position in the tree is irrelevant. + +```python +def merkle_root(leaf_hashes: List[bytes]) -> bytes: + if len(leaf_hashes) == 0: + return b"" + elif len(leaf_hashes) == 1: + return leaf_hashes[0] + else: + split = len(leaf_hashes) // 2 + left = merkle_root(leaf_hashes[:split]) + right = merkle_root(leaf_hashes[split:]) + return sorted_merkle_hash(left, right) +``` + +### `merkle_verify(root: bytes, leaf_hash: bytes, proof: List[bytes]) -> bool` + +Verify a proof of membership in the given merkle root hash. Membership proofs are represented as a list of internal node hashes, in order from deepest to shallowest in the tree. + +To match the behavior of `merkle_root()`, each branch hash is derived using `sorted_merkle_hash()`, so that left/right position in the tree is irrelevant. + +```python +def merkle_verify(root: bytes, leaf_hash: bytes, proof: List[bytes]) -> bool: + h = leaf_hash + for branch_hash in proof: + h = sorted_merkle_hash(h, branch_hash) + return h == root +``` + +# Spending Condition Trees + +In its _expanded_ form, a Spending Condition Tree (SCT) is an ordered list of [NUT-00] secrets, `[x1, x2, ... xn]`. + +Each secret in the SCT is a UTF8-encoded string. Each may be a serialized JSON [NUT-10] secret, or a plain [NUT-00] bearer secret. Example: + +```json +[ + "[\"P2PK\",{\"tags\":[[\"sigflag\",\"SIG_INPUTS\"]],\"nonce\":\"859d4935c4907062a6297cf4e663e2835d90d97ecdd510745d32f6816323a41f\",\"data\":\"0249098aa8b9d2fbec49ff8598feb17b592b986e62319a4fa488a3dc36387157a7\"}]", + "[\"P2PK\",{\"tags\":[[\"sigflag\",\"SIG_ALL\"]],\"nonce\":\"ad4481ae666d97c347e2d737aaae159b30ac6d6fcef93cdca4395bb49d581f0e\",\"data\":\"0276cedb9a3b160db6a158ad4e468d2437f021293204b3cd4bf6247970d8aff54b\"}]", + "[\"P2PK\",{\"nonce\":\"f7325999fee4aacfcd7e6e8d54f651e4b518724c486178b6587ebce107119596\",\"data\":\"030d3f2ad7a4ca115712ff7f140434f802b19a4c9b2dd1c76f3e8e80c05c6a9310\"}]", + "9becd3a8ce24b53beaf8ffb20a497b683b55f87ef87e3814be43a5768bcfe69f" +] +``` + +Each secret in the SCT is one possible spending condition for an ecash token. Fulfilling _any_ of the spending conditions in the SCT is deemed sufficient to spend the token. + +However, only the wallet which creates the SCT will ever see this expanded form of the SCT. To avoid exposing the whole SCT structure to the mint, a wallet must compute the SCT root hash. + +```python +secrets: List[str] = [x1, x2, ... xn] +leaf_hashes = [SHA256(x.encode('utf8')) for x in secrets] +sct_root = merkle_root(leaf_hashes) +``` + +The `sct_root` is then used as a commitment to the list of secrets. + +## SCT + +[NUT-10] Secret `kind: SCT` + +If for a `Proof`, `Proof.secret` is a `Secret` of kind `SCT`. The hex-encoded merkle root hash of a Spending Condition Tree is in `Proof.secret.data`. + +The proof is unlocked by fulfilling ALL of the following conditions: + +1. `Proof.witness.leaf_secret` must be a UTF8 string, treated as a secret (possibly a [NUT-10] well-known secret). +1. `Proof.witness.merkle_proof` must be a valid proof (i.e. a list of hashes) demonstrating that `SHA256(Proof.witness.leaf_secret)` is a leaf hash of the SCT root (specified in `Proof.secret.data`). +1. (optional) if `Proof.witness.leaf_secret` encodes a [NUT-10] spending condition which requires a witness, then `Proof.witness.witness` must provide witness data which satisfies those conditions.[^1] + +[^1]: Note that the `SCT` well-known secret rules allow for nested SCTs, where a leaf node is itself another SCT. In this case, the `Proof.witness.witness` will itself be another `SCT` witness object. Recursion and self-referencing inside an SCT is not permitted. + +Example of a [NUT-10] `SCT` secret object: + +```json +[ + "SCT", + { + "nonce": "d426a2750847d5775f06560d973b484a5b6315e17efffecb1d8d518876c01615", + "data": "18065b939dbbb648749bd5532c740078bb757c3b9f81e0309350a1277fa9a39c" + } +] +``` + +We serialize this object to a JSON string, and get a blind signature on it, by the mint which is stored in `Proof.C` (see [NUT-03](03.md)). This commits the token created with this secret to knowing and (if applicable) passing [NUT-10] checks for at least one of the SCT's leaf secrets. + +### Spending + +To spend this ecash, the wallet must know a `leaf_secret` and a `merkle_proof` of inclusion in the SCT root hash. Here is an example `Proof` which spends a token locked with a well-known secret of kind `SCT`. The `leaf_secret` is a simple [NUT-00] bearer secret with no spending conditions. + +```json +{ + "amount": 1, + "secret": "[\"SCT\",{\"nonce\":\"d426a2750847d5775f06560d973b484a5b6315e17efffecb1d8d518876c01615\",\"data\":\"18065b939dbbb648749bd5532c740078bb757c3b9f81e0309350a1277fa9a39c\"}]", + "C": "02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904", + "id": "009a1f293253e41e", + "witness": { + "leaf_secret": "9becd3a8ce24b53beaf8ffb20a497b683b55f87ef87e3814be43a5768bcfe69f", + "merkle_proof": [ + "8da10ed117cad5e89c6131198ffe271166d68dff9ce961ff117bd84297133b77", + "2397636f1aff968e9f8177b8deaaf9514415126e45aa7755841f966f4eb2279f" + ] + } +} +``` + +Here is a different input, which references the same SCT root hash but uses a different leaf secret - this time a [NUT-10] `P2PK` secret. As `P2PK` requires signature validation, we must provide a `P2PKWitness` stored in `Proof.witness.witness` (See [NUT-11] for details). + +```json +{ + "amount": 1, + "secret": "[\"SCT\",{\"nonce\":\"d426a2750847d5775f06560d973b484a5b6315e17efffecb1d8d518876c01615\",\"data\":\"18065b939dbbb648749bd5532c740078bb757c3b9f81e0309350a1277fa9a39c\"}]", + "C": "02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904", + "id": "009a1f293253e41e", + "witness": { + "leaf_secret": "[\"P2PK\",{\"tags\":[[\"sigflag\",\"SIG_INPUTS\"]],\"nonce\":\"859d4935c4907062a6297cf4e663e2835d90d97ecdd510745d32f6816323a41f\",\"data\":\"0249098aa8b9d2fbec49ff8598feb17b592b986e62319a4fa488a3dc36387157a7\"}]", + "merkle_proof": [ + "6bad0d7d596cb9048754ee75daf13ee7e204c6e408b83ee67514369e3f8f3f96", + "4ac38d0dffb307a4d704c5c7cc28324fd3c151cfaaeddeaa695b890f1a24050b" + ], + "witness": "{\"signatures\":[\"9ef66b39609fe4b5653ee8cc1d4f7133ca16c6cf1862eca7df626c63d90f19f257241ebae3939baa837e1be25e2996b7062e16ba58877aa8318db20729184ff4\"]}" + } +} +``` + +Note that for the purpose of [NUT-11] input signature verification, the signature must be made over the **top-level secret**, which is of kind `SCT`. + +## Previous Work + +The design of SCTs was inspired by the Bitcoin TapRoot upgrade, specifically [BIP-0341](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki). Much like TapScript Trees, Cashu SCTs use merkle trees to prove that a spending condition was committed to when an ecash token was first created. + +# Appendix + +## Merkle Proof Generation + +Below is an example of a function which generates a proof that a specific leaf hash at the given position in the tree is a member of the SCT's merkle root hash. It requires knowledge of the full set of leaf hashes. + +This is only an example of a merkle-proof generation algorithm. Wallet implementations are free to implement proof construction in any way which passes the `merkle_verify` function. + +```python +def merkle_prove(leaf_hashes: List[bytes], position: int) -> List[bytes]: + if len(leaf_hashes) <= 1: + return [] + split = len(leaf_hashes) // 2 + if position < split: + proof = merkle_prove(leaf_hashes[:split], position) + proof.append(merkle_root(leaf_hashes[split:])) + return proof + else: + proof = merkle_prove(leaf_hashes[split:], position - split) + proof.append(merkle_root(leaf_hashes[:split])) + return proof +``` + +[NUT-00]: 00.md +[NUT-10]: 10.md +[NUT-11]: 11.md diff --git a/tests/sct-tests.md.md b/tests/sct-tests.md.md new file mode 100644 index 00000000..c3682f0c --- /dev/null +++ b/tests/sct-tests.md.md @@ -0,0 +1,158 @@ +# NUT-XX Test Vectors + +An SCT tree built with the following leaf secrets: + +```json +[ + "[\"P2PK\",{\"nonce\":\"ffd73b9125cc07cdbf2a750222e601200452316bf9a2365a071dd38322a098f0\",\"data\":\"028fab76e686161cc6daf78fea08ba29ce8895e34d20322796f35fec8e689854aa\",\"tags\":[[\"sigflag\",\"SIG_INPUTS\"]]}]", + "[\"P2PK\",{\"tags\":[[\"sigflag\",\"SIG_ALL\"]],\"nonce\":\"ad4481ae666d97c347e2d737aaae159b30ac6d6fcef93cdca4395bb49d581f0e\",\"data\":\"0276cedb9a3b160db6a158ad4e468d2437f021293204b3cd4bf6247970d8aff54b\"}]", + "[\"P2PK\",{\"nonce\":\"f7325999fee4aacfcd7e6e8d54f651e4b518724c486178b6587ebce107119596\",\"data\":\"030d3f2ad7a4ca115712ff7f140434f802b19a4c9b2dd1c76f3e8e80c05c6a9310\"}]", + "[\"HTLC\",{\"nonce\":\"83a2010affde58f99848e6c33906a0fc04b8fb4fd195e1467c5cb4dbfff43438\",\"data\":\"023192200a0cfd3867e48eb63b03ff599c7e46c8f4e41146b2d281173ca6c50c54\",\"tags\":[[\"pubkeys\",\"02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904\"],[\"locktime\",\"1689418329\"],[\"refund\",\"033281c37677ea273eb7183b783067f5244933ef78d8c3f15b1a77cb246099c26e\"]]}]", + "[\"HTLC\",{\"nonce\":\"99fc46253939ba6f057760b8b29b00c6f876050e32bd8ee95b5b223f8aa0ec90\",\"data\":\"02573b68784ceba9617bbcc7c9487836d296aa7c628c3199173a841e7a19798020\",\"tags\":[[\"pubkeys\",\"02fb58522cd662f2f8b042f8161caae6e45de98283f74d4e99f19b0ea85e08a56d\"],[\"locktime\",\"1689418329\"]]}]", + "9becd3a8ce24b53beaf8ffb20a497b683b55f87ef87e3814be43a5768bcfe69f", + "3838b0454a81511d33b608984c7f01a082f3a80830168f1f3e7d47d21420e8f9", +] +``` + +Will result in the following leaf hashes: + +```json +[ + "b43b79ed408d4cc0aa75ad0a97ab21e357ff7ee027300fb573833c568431e808", + "6bad0d7d596cb9048754ee75daf13ee7e204c6e408b83ee67514369e3f8f3f96", + "8da10ed117cad5e89c6131198ffe271166d68dff9ce961ff117bd84297133b77", + "7ec5a236d308d2c2bf800d81d3e3df89cc98f4f937d0788c302d2754ba28166a", + "e19353a94d1aaf56b150b1399b33cd4ef4096b086665945fbe96bd72c22097a7", + "cc655b7103c8b999b3fc292484bcb5a526e2d0cbf951f17fd7670fc05b1ff947", + "009ea9fae527f7914096da1f1ce2480d2e4cfea62480afb88da9219f1c09767f" +] +``` + +Which results in the following SCT merkle root hash: + +``` +71655cac0c83c6949169bcd6c82b309810138895f83b967089ffd9f64d109306 +``` + +Each leaf hash has the following proofs of inclusion: +```json +[ + [ + "7a56977edf9c299c1cfb14dfbeb2ab28d7b3d44b3c9cc6b7854f8a58acb3407d", + "7de4c7c75c8082ed9a2124ce8f027ed9a60f2236b6f50c62748a220086ed367b" + ], + [ + "8da10ed117cad5e89c6131198ffe271166d68dff9ce961ff117bd84297133b77", + "b43b79ed408d4cc0aa75ad0a97ab21e357ff7ee027300fb573833c568431e808", + "7de4c7c75c8082ed9a2124ce8f027ed9a60f2236b6f50c62748a220086ed367b" + ], + [ + "6bad0d7d596cb9048754ee75daf13ee7e204c6e408b83ee67514369e3f8f3f96", + "b43b79ed408d4cc0aa75ad0a97ab21e357ff7ee027300fb573833c568431e808", + "7de4c7c75c8082ed9a2124ce8f027ed9a60f2236b6f50c62748a220086ed367b" + ], + [ + "e19353a94d1aaf56b150b1399b33cd4ef4096b086665945fbe96bd72c22097a7", + "f583288c32937865b0c5c7d4a9262f65b7275f59c8796eb3e79de9e0b217d5e0", + "7ea48b9a4ad58f92c4cfa8e006afa98b2b05ac1b4de481e13088d26f672d8edc" + ], + [ + "7ec5a236d308d2c2bf800d81d3e3df89cc98f4f937d0788c302d2754ba28166a", + "f583288c32937865b0c5c7d4a9262f65b7275f59c8796eb3e79de9e0b217d5e0", + "7ea48b9a4ad58f92c4cfa8e006afa98b2b05ac1b4de481e13088d26f672d8edc" + ], + [ + "009ea9fae527f7914096da1f1ce2480d2e4cfea62480afb88da9219f1c09767f", + "2628c9759f0cecbb43b297b6eb0c268573d265730c2c9f6e194b4948f43d669d", + "7ea48b9a4ad58f92c4cfa8e006afa98b2b05ac1b4de481e13088d26f672d8edc" + ], + [ + "cc655b7103c8b999b3fc292484bcb5a526e2d0cbf951f17fd7670fc05b1ff947", + "2628c9759f0cecbb43b297b6eb0c268573d265730c2c9f6e194b4948f43d669d", + "7ea48b9a4ad58f92c4cfa8e006afa98b2b05ac1b4de481e13088d26f672d8edc" + ] +] +``` + + +## Proofs + +The following is a valid `Proof` object spending an `SCT` secret with a bearer leaf secret. + +```json +{ + "amount": 1, + "secret": "[\"SCT\",{\"nonce\":\"d426a2750847d5775f06560d973b484a5b6315e17efffecb1d8d518876c01615\",\"data\":\"71655cac0c83c6949169bcd6c82b309810138895f83b967089ffd9f64d109306\"}]", + "C": "02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904", + "id": "009a1f293253e41e", + "witness": { + "leaf_secret": "9becd3a8ce24b53beaf8ffb20a497b683b55f87ef87e3814be43a5768bcfe69f", + "merkle_proof": [ + "009ea9fae527f7914096da1f1ce2480d2e4cfea62480afb88da9219f1c09767f", + "2628c9759f0cecbb43b297b6eb0c268573d265730c2c9f6e194b4948f43d669d", + "7ea48b9a4ad58f92c4cfa8e006afa98b2b05ac1b4de481e13088d26f672d8edc" + ] + } +} +``` + +The following is a valid `Proof` object spending an `SCT` secret with a NUT-11 P2PK leaf secret. + +```json +{ + "amount": 1, + "secret": "[\"SCT\",{\"nonce\":\"d426a2750847d5775f06560d973b484a5b6315e17efffecb1d8d518876c01615\",\"data\":\"71655cac0c83c6949169bcd6c82b309810138895f83b967089ffd9f64d109306\"}]", + "C": "02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904", + "id": "009a1f293253e41e", + "witness": { + "leaf_secret": "[\"P2PK\",{\"nonce\":\"ffd73b9125cc07cdbf2a750222e601200452316bf9a2365a071dd38322a098f0\",\"data\":\"028fab76e686161cc6daf78fea08ba29ce8895e34d20322796f35fec8e689854aa\",\"tags\":[[\"sigflag\",\"SIG_INPUTS\"]]}]", + "merkle_proof": [ + "7a56977edf9c299c1cfb14dfbeb2ab28d7b3d44b3c9cc6b7854f8a58acb3407d", + "7de4c7c75c8082ed9a2124ce8f027ed9a60f2236b6f50c62748a220086ed367b" + ], + "witness": "{\"signatures\":[\"9ef66b39609fe4b5653ee8cc1d4f7133ca16c6cf1862eca7df626c63d90f19f257241ebae3939baa837e1be25e2996b7062e16ba58877aa8318db20729184ff4\"]}" + } +} +``` + +The secret key for the above pubkey is `8e935aec5668312be8f960a5ecc3c5dd290e39985970bfd093047df7f05cc9ec` + +### Invalid + +The following is an *invalid* `Proof` object which attempts to spend an `SCT` secret with a bearer leaf secret. The proof is invalid becase the `merkle_proof`'s first (deepest) hash is incorrect. + +```json +{ + "amount": 1, + "secret": "[\"SCT\",{\"nonce\":\"d426a2750847d5775f06560d973b484a5b6315e17efffecb1d8d518876c01615\",\"data\":\"71655cac0c83c6949169bcd6c82b309810138895f83b967089ffd9f64d109306\"}]", + "C": "02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904", + "id": "009a1f293253e41e", + "witness": { + "leaf_secret": "9becd3a8ce24b53beaf8ffb20a497b683b55f87ef87e3814be43a5768bcfe69f", + "merkle_proof": [ + "db7a191c4f3c112d7eb3ae9ee8fa9bd940fc4fed6ada9ba9ab2f102c3e3bbe80", + "2628c9759f0cecbb43b297b6eb0c268573d265730c2c9f6e194b4948f43d669d", + "7ea48b9a4ad58f92c4cfa8e006afa98b2b05ac1b4de481e13088d26f672d8edc" + ] + } +} +``` + +The following is an *invalid* `Proof` object spending an `SCT` secret with a NUT-11 P2PK leaf secret. The proof is invalid because the P2PK signature is made on the `leaf_secret` instead of the top-level `Proof.secret`. + +```json +{ + "amount": 1, + "secret": "[\"SCT\",{\"nonce\":\"d426a2750847d5775f06560d973b484a5b6315e17efffecb1d8d518876c01615\",\"data\":\"71655cac0c83c6949169bcd6c82b309810138895f83b967089ffd9f64d109306\"}]", + "C": "02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904", + "id": "009a1f293253e41e", + "witness": { + "leaf_secret": "[\"P2PK\",{\"nonce\":\"ffd73b9125cc07cdbf2a750222e601200452316bf9a2365a071dd38322a098f0\",\"data\":\"028fab76e686161cc6daf78fea08ba29ce8895e34d20322796f35fec8e689854aa\",\"tags\":[[\"sigflag\",\"SIG_INPUTS\"]]}]", + "merkle_proof": [ + "7a56977edf9c299c1cfb14dfbeb2ab28d7b3d44b3c9cc6b7854f8a58acb3407d", + "7de4c7c75c8082ed9a2124ce8f027ed9a60f2236b6f50c62748a220086ed367b" + ], + "witness": "{\"signatures\":[\"106b3df8cbe1b9e867ec5717f5018b42e388e8fce7de3b09da1da7c6ab1eaaa19ab7ab95a3bcb8af8d627214f339a594efa8aefa9db7f34de2ca0587f5693e46\"]}" + } +} +``` From 6aaaa6e73de85c0a3e6e3096ad7c094fd3a5f862 Mon Sep 17 00:00:00 2001 From: conduition Date: Mon, 27 May 2024 16:13:10 +0000 Subject: [PATCH 02/18] NUT for DLC execution --- dlc.md | 519 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 519 insertions(+) create mode 100644 dlc.md diff --git a/dlc.md b/dlc.md new file mode 100644 index 00000000..49b1d557 --- /dev/null +++ b/dlc.md @@ -0,0 +1,519 @@ +NUT-DLC: Discreet Log Contracts +========================== + +`optional`, `depends on: NUT-SCT` + +--- + +This NUT describes a standard for mints and wallets to support settlement of [Discreet Log Contracts](https://bitcoinops.org/en/topics/discreet-log-contracts/) using ecash. + +# DLCs + +A Discreet Log Contract (DLC) is a conditional payment which a set of _participants_ can atomically commit money to. They depend on the existence of a semi-trusted Oracle. For the sake of brevity, this document elides any full description of DLCs. Instead we abstract a DLC as a set of input parameters agreed upon in-advance, known to all DLC participants. + +These parameters are: + +- The number of possible DLC outcomes `n` +- A vector of `n` _outcome blinding secrets_ (scalars) `[b1, b2, ... bn]` [^1] +- A vector of `n` _outcome locking points_ `[K1, K2, ... Kn]` [^2] +- A vector of `n` _payout structures_ `[P1, P2, ... Pn]` +- An optional timeout timestamp `t` and accompanying timeout payout structure `Pt` + +[^1]: Wallet clients may opt out of outcome-blinding by setting all of the blinding secrets to zero. + +[^2]: The outcome locking point vector abstraction covers both [enum events](https://github.com/discreetlogcontracts/dlcspecs/blob/master/Oracle.md#simple-enumeration) and [digit-decomposition events](https://github.com/discreetlogcontracts/dlcspecs/blob/master/Oracle.md#digit-decomposition). For an enum event, participants simply compute each of locking points from the announced outcome messages and nonce, in a 1:1 mapping. For a digit-decomposition event, participants compute locking points for each relevant outcome _range_ on a per-digit basis, aggregating (summing) the locking points where necessary to produce `[K1, K2, ... Kn]`. + +## Locking Points + +The locking points `[K1, K2, ... Kn]` are elliptic curve points, encoded in SEC1 compressed binary format. + +The locking points are blinded by the participants, to obscure the nature of the DLC from the mint at settlement time. Blinded locking points are computed as: + +```python +Ki_ = Ki + bi * G +``` + +...for some blinding secret `bi` known to all DLC participants. Each locking point SHOULD be allocated a _unique_ blinding secret. + +## Payout Structures + +Payout structures are serialized dictionaries which map `pubkey -> weight`. + +```json +{ + : , + ... +} +``` + +This mapping defines how the money used to fund a DLC should be distributed if a particular outcome is settled. The pubkeys describe ownership rights and the weights describe how the funding amount must be allocated. + +### Example + +```json +{ + "03a40f20667ed53513075dc51e715ff2046cad64eb68960632269ba7f0210e38": 3, + "028a36f0e6638ea7466665fe174d958212723019ec08f9ce6898d897f88e68aa5d": 2, + "02b4ebb0dda3b9ad83b39e2e31024b777cc0ac205a96b9a6cfab3edea2912ed1b3": 1 +} +``` + +With this payout structure, the `03a40f...` pubkey is allocated 3 of the 6 available weight units (3+2+1), and so its owner should receive half of the DLC funding amount. Similarly, `028a36...` receives a third of the funding amount, and `02b4eb...` receives the remaining sixth. + +Weights must be positive integers. Negative weights or weights equal to zero are invalid, and render the whole payout structure invalid. + +## Timeouts + +The timeout timestamp `t` is an unsigned integer describing a unix-epoch offset in seconds. + +The payout structure for the timeout condition `Pt` is the same format as other payout structures defined above. + +The timeout condition may be omitted to enable DLCs which are indefinitely-locked. + +## Locking Ecash to a DLC + +[NUT-10] Secret `kind: DLC` + +We define a new [NUT-10] well-known `Secret` kind `DLC`. + +```json +[ + "DLC", + { + "nonce": "da62796403af76c80cd6ce9153ed3746", + "data": "2db63c93043ab646836b38292ed4fcf209ba68307427a4b2a8621e8b1daeb8ed", + "tags": [ + [ + "threshold", + "10000" + ] + ] + } +] +``` + +The `Secret.data` field is the root hash of a merkle tree which uniquely identifies a particular DLC (see next section for construction). + +Anyone can spend a `Proof` locked with the `DLC` secret kind, but can _only_ spend it in the process of _funding a DLC identified by the same root hash._ **A mint which supports this spec MUST NOT allow a `DLC` secret to be spent unless it is used for funding the indicated DLC.** + +The `threshold` tag is a required parameter which stipulates a minimum funding value which must be used to fund the DLC. [^3] + +[^3]: The `threshold` tag commits a `DLC` secret to funding only a DLC of at least a specific amount. This is a way of enforcing buy-in from all participants. + +## DLC Merkle Tree + +The particulars of a DLC are represented by a Merkle tree. The leaf hashes of the tree are constructed by hashing each of the blinded locking points `[K1_, K2_, ... Kn_]` with their corresponding payout structures `[P1, P2, ... Pn]`. + +```python +Ti = SHA256(Ki_ || Pi) +``` + +The timeout condition (if applicable) is also added as a leaf. + +```python +Tt = SHA256(hash_to_curve(t.to_bytes(4, 'big')) || Pt) +``` + +The `hash_to_curve` function is defined in [NUT-00]. [^4] `t` is encoded as a 32-bit big-endian integer before hashing. + +[^4]: When constructing `Tt`, we hash `t` to a curve point as a convenience, so that all leaf nodes can be represented as `(Point, Map)` data structures. + +Note that curve points are encoded in SEC1 compressed binary format, while each payout condition `Pi` is encoded as UTF8 JSON before being hashed. Because JSON maps are not ordered, all participants must agree ahead of time on a specific set of serialized payout structures. + +From the set of `leaf_hashes` `[T1, T2, ... Tn, Tt]`, participants compute the DLC root hash: + +```python +dlc_root = merkle_root([T1, T2, ... Tn, Tt]) +``` + +...where the `merkle_root()` function is defined in [NUT-SCT]. + +`dlc_root` may then be used as the `Secret.data` field for secrets of kind `DLC`. + +A wallet can prove `Ti` is a leaf of `dlc_root` by providing a list of merkle branch hashes. See [the appendix of NUT-SCT](sct.md#Appendix) for an example of how to build such a proof. + +## DLC Funding + +This section describes the DLC funding process. + +DLC participants who wish to jointly fund a DLC may mint (or swap for) ecash proofs which use the `DLC` well-known secret kind to commit the funds to a specific `dlc_root`. Wallets MUST ensure that the construction of `dlc_root` is fully validated - Even a single hidden leaf node could be used by a malicious participant to immediately sweep the whole DLC funding amount. + +Participants elect an untrusted _funder,_ whose is responsible for collecting `DLC`-locked ecash proofs from the participants, and then submitting them all en-masse to the mint. The funder need not be a DLC participant. Indeed, the mint itself may act as a funder, although this compromises privacy of the participants. + +### DLC Funding Token + +To ensure money is not lost if the funder disappears without submitting the proofs to the mint, participants should create each locked proof using a [Spending Condition Tree (SCT)](sct.md#SCT) which commits to at least two spending condition leaves: + +1. The `DLC` secret. +2. A backup secret which only the participant knows and can claim. + +The backup secret allows a participant to swap her exposed proof for a fresh one if the funder takes too long to register the DLC. + +A DLC Funding Token is structured and encoded in the same format as [NUT-00] tokens, but also specifies the `dlc_root` hash the token is intended to fund. + +Example: + +```json +{ + "token": [ + { + "mint": "https://8333.space:3338", + "proofs": [ + { + "amount": 4096, + "id": "009a1f293253e41e", + "secret": "[\"SCT\",{\"nonce\":\"d426a2750847d5775f06560d973b484a5b6315e17efffecb1d8d518876c01615\",\"data\":\"d7578cbc3d5d61a61cb46552f66d7d5fe92ea4606c778e14d662bbe3d887c0d1\"}]", + "C": "02bc9097997d81afb2cc7346b5e4345a9346bd2a506eb7958598a72f0cf85163ea", + "witness": { + "leaf_secret": "[\"DLC\",{\"nonce\":\"da62796403af76c80cd6ce9153ed3746\",\"data\":\"2db63c93043ab646836b38292ed4fcf209ba68307427a4b2a8621e8b1daeb8ed\",\"tags\":[[\"threshold\",\"10000\"]]}]", + "merkle_proof": ["009ea9fae527f7914096da1f1ce2480d2e4cfea62480afb88da9219f1c09767f"] + } + }, + ... + ] + } + ], + "unit": "sat", + "memo": "Bet", + "dlc_root": "2db63c93043ab646836b38292ed4fcf209ba68307427a4b2a8621e8b1daeb8ed" +} +``` + +The funder cannot use this ecash proof for anything _except_ for funding a DLC with root hash `2db63c...`, and doing so requires a minimum funding amount of 10,000 satoshis. The funder would need to find another 5,904 available satoshis in proofs to fund the DLC with this proof as an input. + + +### Mint Registration + +To register and fund a DLC on the mint, the funder issues a `POST /v1/dlc/fund` request to the mint, sending a request body of the following format. + +```json +{ + "atomic": , + "registrations": [ + { + "dlc_root": "2db63c93043ab646836b38292ed4fcf209ba68307427a4b2a8621e8b1daeb8ed", + "inputs": [ + { + "amount": 2, + "id": "009a1f293253e41e", + "secret": "[\"DLC\",{\"nonce\":\"da62796403af76c80cd6ce9153ed3746\",\"data\":\"2db63c93043ab646836b38292ed4fcf209ba68307427a4b2a8621e8b1daeb8ed\",\"tags\":[[\"threshold\",\"10000\"]]}]", + "C": "02bc9097997d81afb2cc7346b5e4345a9346bd2a506eb7958598a72f0cf85163ea" + }, + ... + ] + }, + ... + ] +} +``` + +For each new DLC submitted in `registrations`, the mint MUST process and then consume all of the `inputs` in the same way it would for a swap/melt operation, with one additional rule: **Proofs referencing a secret of kind `DLC` are now spendable, but only if `Proof.secret.data == dlc_root`.** + +If the optional `atomic` field in the request body is set to `true`, the mint must process _all_ DLCs in the `registrations` array, or else process none of them. + +If one or more inputs does NOT pass validation, the mint must return a response with a `400` status code, and a body of the following format: + +```json +{ + "processed": [, ...], + "errors": [ + { + "dlc_root": , + "bad_inputs": [ + { + "index": , + "detail": + }, + ... + ] + }, + ... + ] +} +``` + +The `processed` array tells the funder which DLCs have been successfully registered, while the `errors` field tells the funder which DLCs failed to register, and which specific input proofs for that DLC were faulty. This allows participants to resolve funding disputes. [^5] + +[^5]: To resolve a funding dispute where some _accused_ participants supply faulty DLC funding proofs, but refuse to acknowledge their mistake, the funder may broadcast the full set of (locked) DLC funding proofs to all _bystander_ participants. The accused participants also broadcast the proofs they supplied to the funder. The bystanders retry the `POST /v1/dlc/fund` request with both possible sets of input proofs to confirm the mint indeed reports the same failure on both. If the mint accepts any of the funding requests, then the dispute is resolved and the DLC has been funded. If the mint reports errors for both sets of input proofs, the bystanders can use the `bad_inputs` field determine who was behaving dishonestly and evict them from the group. + +If all the input proofs of a DLC registration object pass validation, the mint stores the `dlc_root` and the total `funding_amount` (i.e. the sum value of all `inputs`). **It is vital for the mint to remember this state,** as the `(dlc_root, funding_amount)` tuple is now the only valid record that exists anywhere of the participants' joint deposit. + +If every DLC in the `registrations` array is processed successfully, the mint must return a `200 OK` response with the following response body format: + +```json +{ + "processed": [ + { + "dlc_root": + }, + ... + ] +} +``` + +Each object in the `processed` array lists the root hash of a successfully registered DLC. + +### Settling the DLC + +When the DLC Oracle publishes her attestation, this reveals to the participants a scalar `ki`, the discrete log of `Ki` such that `ki * G = Ki`. + +Any DLC participant who knows the matching blinding secret `bi` can compute `ki_` - the discrete log of the blinded locking point `Ki_`. + +``` +ki_ = ki + bi +``` +``` +ki_ * G = (ki + bi) * G + = ki * G + bi * G + = Ki + bi * G + = Ki_ +``` + +To mark the DLC as settled on the mint, the wallet issues a `POST /v1/dlc/settle` request with the following body format. + +```json +{ + "settlements": [ + { + "dlc_root": "2db63c93043ab646836b38292ed4fcf209ba68307427a4b2a8621e8b1daeb8ed", + "outcome": { + "k": "8e935aec5668312be8f960a5ecc3c5dd290e39985970bfd093047df7f05cc9ec", + "P": "{\"03361cd8bd1329fea797a6add1cf1990ffcf2270ceb9fc81eeee0e8e9c1bd0cdf5\":\"10000\"}" + }, + "merkle_proof": [ + "5467757c899a46b847825e632cafc5e960a948045d12fc1143d17966c87ae351", + "a6df41f37b1b21ebc2b3d68fb8598450a07fb279e82e0e57bd04b926234f2f5f", + "1f8de293adb9301b16cb5bfb446960000602b74a3b7ed89aa8677323a6b39b8a" + ] + }, + ... + ] +} +``` + +- `Settlement.dlc_root` is the root hash of a funded and active DLC. +- `Settlement.outcome.k` is the blinded attestation secret `ki_` +- `Settlement.outcome.P` is the serialized payout structure `Pi` +- `Settlement.merkle_proof` is a list of hashes, which must prove that `SHA256(ki_ * G || Pi)` is a leaf hash of the `dlc_root` merkle hash. + +The mint verifies each settlement object as follows: + +1. If `Settlement.outcome.P` fails to parse as a JSON dictionary mapping pubkeys to positive integers, return an error. +1. If `Settlement.dlc_root` does not correspond to any known funded DLC, return an error. +1. If `Settlement.dlc_root` corresponds to a DLC, but that DLC has already been settled, return an error. +1. Verify `Settlement.merkle_proof`: + +```python +leaf_hash = SHA256(Settlement.outcome.k * G || Settlement.outcome.P) +assert merkle_verify(dlc_root, Settlement.merkle_proof, leaf_hash) +``` + +The mint uses the `merkle_verify()` function from [NUT-SCT] to verify the `merkle_proof`. + +#### Timeout Settlement + +If the mint's clock reaches the DLC timeout time `t`, any participant can settle the timeout branch of the DLC. To mark the DLC as settled on the mint using the timeout clause, the wallet issues a `POST /v1/dlc/settle` request with the following body format. + +```json +{ + "atomic": , + "settlements": [ + { + "dlc_root": "2db63c93043ab646836b38292ed4fcf209ba68307427a4b2a8621e8b1daeb8ed", + "outcome": { + "timeout": 1716777419, + "P": "{\"03361cd8bd1329fea797a6add1cf1990ffcf2270ceb9fc81eeee0e8e9c1bd0cdf5\":\"10000\"}" + }, + "merkle_proof": [ + "5467757c899a46b847825e632cafc5e960a948045d12fc1143d17966c87ae351", + "a0fc3d18e6baea50ce8fe8a12ad083fc37e07a68f083a1aafc377c60be999e5f" + ] + }, + ... + ] +} +``` + +`Settlement.outcome.timeout` is the timeout timestamp `t`. The steps for the mint to verify the settlement is the same as an outcome settlement (above), with one modification: `Settlement.merkle_proof` is verified differently. + +```python +T = hash_to_curve(Settlement.outcome.timeout.to_bytes(4, 'big')) +leaf_hash = SHA256(T || Settlement.outcome.P) +assert merkle_verify(dlc_root, Settlement.merkle_proof, leaf_hash) +``` + +If the above checks pass for a `Settlement` object, the mint marks the DLC as "settled". The mint can now use the original `funding_amount` to compute exactly how much each pubkey in `Settlement.outcome.P` is owed. This resulting map of pubkeys to amounts is denoted `debts`. + +```python +weights = json.loads(Settlement.outcome.P) +weight_sum = sum(weights.values()) +debts = dict(((pubkey, funding_amount * weight // weight_sum) for pubkey, weight in weights.items())) +``` + +The mint can now replace the `funding_amount` in its storage with the `debts` map. + +If all `settlements` validated correctly, the mint returns a `200 OK` response with the following body format: + +```json +{ + "settled": [ + { + "dlc_root": , + }, + ... + ] +} +``` + +The `settled` array indicates the set of DLCs which were successfully marked as settled. + +If some `settlements` failed, the mint must return a `400` response with the following body format: + +```json +{ + "settled": [ + { + "dlc_root": , + }, + ... + ], + "errors": [ + { + "dlc_root": , + "detail": + }, + ... + ] +} +``` + +If the wallet passes the optional `atomic` request parameter as `true`, then the mint must process ALL the requested `settlements` successfully, or else process none of them. + +### Claiming Payouts + +To claim a DLC payout, a participant issues a `POST /v1/dlc/payout` request to the mint with the following body format. + +```json +{ + "atomic": , + "payouts": [ + { + "dlc_root": "2db63c93043ab646836b38292ed4fcf209ba68307427a4b2a8621e8b1daeb8ed", + "pubkey": "03361cd8bd1329fea797a6add1cf1990ffcf2270ceb9fc81eeee0e8e9c1bd0cdf5", + "signature": "60f3c9b766770b46caac1d27e1ae6b77c8866ebaeba0b9489fe6a15a837eaa6fcd6eaa825499c72ac342983983fd3ba3a8a41f56677cc99ffd73da68b59e1383", + "outputs": , + }, + ... + ] +} +``` + +- `Payout.dlc_root` is a DLC merkle root hash. +- `Payout.signature` is a [BIP-340] signature made on the `dlc_root` hash, which should verify against `Payout.pubkey`. +- `Payout.outputs` is a set of blinded messages for the mint to sign. + +For each `Payout` object, the mint performs the following checks: + +1. Validate that `Payout.signature` is a valid [BIP-340] signature made by `Payout.pubkey` on `payout.dlc_root` +1. If `Payout.dlc_root` does not correspond to any known funded DLC, return an error. +1. If `Payout.dlc_root` corresponds to a known DLC, but that DLC has not been settled, return an error. +1. If `Payout.pubkey` is not a key in the `debts` map, return an error. +1. If `sum([out.amount for out in Payout.outputs]) != debts[Payout.pubkey]`, the mint must return an error to the client. + +If all `Payout` objects are validated successfully, the mint returns a `200` response with the blinded signatures on `Payout.outputs`: + +```json +{ + "paid": [ + { + "dlc_root": , + "signatures": + }, + ... + ] +} +``` + +For each `Payout` object whose outputs the mint signs, the mint must simultaneously delete `Payout.pubkey` from the `debts` map to prevent the wallet from claiming twice. [^6] + +[^6]: Network errors may cause the wallet not to safely receive the blinded signatures sent by the mint. To counteract this occurrence, mints are encouraged to set up idempotent request handlers which cache responses, so the wallet can replay its `POST /v1/dlc/payout` request if needed. + +If some `Payout` objects fail the validation checks, the mint returns a `400` response with the following format: + +```json +{ + "paid": [ + { + "dlc_root": , + "signatures": + }, + ... + ], + "errors": [ + { + "dlc_root": , + "detail": + }, + ... + ] +} +``` + +Wallets MUST collect and save the blinded signatures from each entry in the `paid` array, even though the mint responded with a `400` error. + +If a wallet needs payout processing to be atomic, they + +If the wallet passes the `atomic` parameter as `true` in their request body, then the mint must ensure that either _all_ payouts are processed, or else _none_ are. + +### Checking the DLC Status + +The participants need a way to verify: + +- if and when the funder has successfully funded the DLC +- if the DLC has been settled +- if the DLC has been paid out, and if so to whom + +To this end, a wallet issues a `GET /v1/dlc/status/{dlc_root}` request to the mint. + +If the DLC is not found, the mint responds with a `400` error. + +If the DLC is found and is active (not settled), the mint responds with: + +```json +{ + "settled": false, + "funding_amount": +} +``` + +If the DLC is found and is settled, the mint responds with: + +```json +{ + "settled": true, + "debts": { + "03361cd8bd1329fea797a6add1cf1990ffcf2270ceb9fc81eeee0e8e9c1bd0cdf5": + } +} +``` + +To prevent needless polling when waiting for a DLC to be funded, wallets MAY issue a `GET /v1/dlc/status/{dlc_root}?wait=true&timeout=30` request, adding the `?wait=true&timeout=30` query parameters. The mint SHOULD treat this request as a long-poll: If the `dlc_root` exists, return a response immediately. Otherwise, wait for `timeout` number of seconds to see if anyone registers the DLC, before eventually returning a `400` status if the DLC still has not been funded by then. + +## Tidying Up + +Once all payouts have been issued, the mint may purge the DLC from its storage, or it may retain the DLC and return `"debts": {}` in the `/v1/dlc/status/{dlc_root}` response body to indicate the DLC has been fully settled and paid. + +## TODO + +- prevent a mint from being DOS'd by using payout structure size limits, DLC lifetime limits +- allow mint to charge fees for DLC processing + +## Previous Work + +This NUT is inspired by [this original proposal](https://conduition.io/cryptography/ecash-dlc/) for DLC settlement with generic Chaumian Ecash. + +[NUT-00]: 00.md +[NUT-10]: 10.md +[NUT-12]: 12.md +[NUT-SCT]: sct.md +[BIP-340]: https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki From 37b985f85a22ea36b0cd8c43a93dd762ae026ab3 Mon Sep 17 00:00:00 2001 From: conduition Date: Mon, 27 May 2024 16:15:46 +0000 Subject: [PATCH 03/18] TODO about proof of registration --- dlc.md | 1 + 1 file changed, 1 insertion(+) diff --git a/dlc.md b/dlc.md index 49b1d557..8b3bed24 100644 --- a/dlc.md +++ b/dlc.md @@ -507,6 +507,7 @@ Once all payouts have been issued, the mint may purge the DLC from its storage, - prevent a mint from being DOS'd by using payout structure size limits, DLC lifetime limits - allow mint to charge fees for DLC processing +- give the funder a non-interactive proof of DLC registration so that participants don't have to poll ## Previous Work From f67643846f1a5f5b47d13492d45893c1d4a7d200 Mon Sep 17 00:00:00 2001 From: conduition Date: Tue, 28 May 2024 22:02:36 +0000 Subject: [PATCH 04/18] use actual point in example --- dlc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dlc.md b/dlc.md index 8b3bed24..102d72a4 100644 --- a/dlc.md +++ b/dlc.md @@ -52,7 +52,7 @@ This mapping defines how the money used to fund a DLC should be distributed if a ```json { - "03a40f20667ed53513075dc51e715ff2046cad64eb68960632269ba7f0210e38": 3, + "0276cedb9a3b160db6a158ad4e468d2437f021293204b3cd4bf6247970d8aff54b": 3, "028a36f0e6638ea7466665fe174d958212723019ec08f9ce6898d897f88e68aa5d": 2, "02b4ebb0dda3b9ad83b39e2e31024b777cc0ac205a96b9a6cfab3edea2912ed1b3": 1 } From 1822875fb654113e4a07d294604c8f9608f5ca84 Mon Sep 17 00:00:00 2001 From: conduition Date: Mon, 3 Jun 2024 19:58:40 +0000 Subject: [PATCH 05/18] example funding request should use SCT secret --- dlc.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/dlc.md b/dlc.md index 102d72a4..f26d064e 100644 --- a/dlc.md +++ b/dlc.md @@ -194,10 +194,16 @@ To register and fund a DLC on the mint, the funder issues a `POST /v1/dlc/fund` "dlc_root": "2db63c93043ab646836b38292ed4fcf209ba68307427a4b2a8621e8b1daeb8ed", "inputs": [ { - "amount": 2, + "amount": 4096, "id": "009a1f293253e41e", - "secret": "[\"DLC\",{\"nonce\":\"da62796403af76c80cd6ce9153ed3746\",\"data\":\"2db63c93043ab646836b38292ed4fcf209ba68307427a4b2a8621e8b1daeb8ed\",\"tags\":[[\"threshold\",\"10000\"]]}]", - "C": "02bc9097997d81afb2cc7346b5e4345a9346bd2a506eb7958598a72f0cf85163ea" + "secret": "[\"SCT\",{\"nonce\":\"d426a2750847d5775f06560d973b484a5b6315e17efffecb1d8d518876c01615\",\"data\":\"d7578cbc3d5d61a61cb46552f66d7d5fe92ea4606c778e14d662bbe3d887c0d1\"}]", + "C": "02bc9097997d81afb2cc7346b5e4345a9346bd2a506eb7958598a72f0cf85163ea", + "witness": { + "leaf_secret": "[\"DLC\",{\"nonce\":\"da62796403af76c80cd6ce9153ed3746\",\"data\":\"2db63c93043ab646836b38292ed4fcf209ba68307427a4b2a8621e8b1daeb8ed\",\"tags\":[[\"threshold\",\"10000\"]]}]", + "merkle_proof": [ + "009ea9fae527f7914096da1f1ce2480d2e4cfea62480afb88da9219f1c09767f" + ] + } }, ... ] From e44621bd6b825f0bcb61b5fa8c603d3a2a08cadb Mon Sep 17 00:00:00 2001 From: conduition Date: Tue, 4 Jun 2024 02:37:44 +0000 Subject: [PATCH 06/18] fees, offline funding proofs, and anti-DOS measures --- dlc.md | 126 ++++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 103 insertions(+), 23 deletions(-) diff --git a/dlc.md b/dlc.md index f26d064e..f1f0c73f 100644 --- a/dlc.md +++ b/dlc.md @@ -96,7 +96,7 @@ The `Secret.data` field is the root hash of a merkle tree which uniquely identif Anyone can spend a `Proof` locked with the `DLC` secret kind, but can _only_ spend it in the process of _funding a DLC identified by the same root hash._ **A mint which supports this spec MUST NOT allow a `DLC` secret to be spent unless it is used for funding the indicated DLC.** -The `threshold` tag is a required parameter which stipulates a minimum funding value which must be used to fund the DLC. [^3] +The `threshold` tag is a required parameter which stipulates a minimum funding value which must be used to fund the DLC. [^3] This threshold _does not include [fees charged by the mint](#fees)._ [^3]: The `threshold` tag commits a `DLC` secret to funding only a DLC of at least a specific amount. This is a way of enforcing buy-in from all participants. @@ -132,6 +132,39 @@ dlc_root = merkle_root([T1, T2, ... Tn, Tt]) A wallet can prove `Ti` is a leaf of `dlc_root` by providing a list of merkle branch hashes. See [the appendix of NUT-SCT](sct.md#Appendix) for an example of how to build such a proof. +## Mint Settings + +A mint which supports this NUT must expose some global parameters to wallets, via the [NUT-06] `GET /v1/info` response. + +```json +{ + ... + "nuts": { + "NUT-DLC": { + "supported": true, + "funding_proof_pubkey": , + "max_payouts": , + "ttl": + "fees": { + "sat": { + "base": , + "ppk": + } + } + } + } +} +``` + +- `funding_proof_pubkey` is a compressed public key with which participants may verify offline that the mint has indeed registered a DLC. +- `max_payouts` is the maximum size of payout structure this mint will accept. +- `ttl` is a duration in seconds, indicating how long the mint promises to store DLCs after registration. The mint may consider a DLC abandoned if the DLC lives longer than the `ttl` declared _at the time of registration._ If `ttl <= 0`, the mint claims it will remember registered DLCs forever. +- `fees` is a map describing fee structures for the mint to process DLCs on a per-currency basis. `fees` may be set to the empty object (`{}`) to explicitly disable fees for all currency units. + - `base` is an absolute fee amount applied to every DLC funded on the mint. + - `ppk` or "parts-per-kilo" (thousand units) describes a percentage relative to the DLC funding value, which will be charged by the mint as a fee. + +Mints should avoid changing these settings frequently. Wallets may cache them but should occasionally re-fetch a mint's DLC parameters, especially when actively participating in DLC creation. + ## DLC Funding This section describes the DLC funding process. @@ -179,8 +212,7 @@ Example: } ``` -The funder cannot use this ecash proof for anything _except_ for funding a DLC with root hash `2db63c...`, and doing so requires a minimum funding amount of 10,000 satoshis. The funder would need to find another 5,904 available satoshis in proofs to fund the DLC with this proof as an input. - +The funder cannot use this ecash proof for anything _except_ for funding a DLC with root hash `2db63c...`, and doing so requires a minimum funding amount of 10,000 satoshis ([after subtracting fees](#fees)). The funder would need to find at least another 5,904 available satoshis in proofs to fund the DLC with this proof as an input. ### Mint Registration @@ -192,6 +224,7 @@ To register and fund a DLC on the mint, the funder issues a `POST /v1/dlc/fund` "registrations": [ { "dlc_root": "2db63c93043ab646836b38292ed4fcf209ba68307427a4b2a8621e8b1daeb8ed", + "funding_amount": , "inputs": [ { "amount": 4096, @@ -215,13 +248,30 @@ To register and fund a DLC on the mint, the funder issues a `POST /v1/dlc/fund` For each new DLC submitted in `registrations`, the mint MUST process and then consume all of the `inputs` in the same way it would for a swap/melt operation, with one additional rule: **Proofs referencing a secret of kind `DLC` are now spendable, but only if `Proof.secret.data == dlc_root`.** +The `inputs` array must provide valid proofs whose value sums to `input_amount`, which is computed as: + +```python +input_amount = funding_amount + fees.base + (input_amount * fees.ppk // 1000) +assert sum(inputs) == input_amount +``` + +See [the fees section](#fees) for more details. + +Assuming the input proofs are all valid and sum to the correct amount, the mint stores the `(dlc_root, funding_amount)`, and marks the `inputs` proofs' secrets as spent. The DLC is then deemed to be funded. + If the optional `atomic` field in the request body is set to `true`, the mint must process _all_ DLCs in the `registrations` array, or else process none of them. -If one or more inputs does NOT pass validation, the mint must return a response with a `400` status code, and a body of the following format: +If one or more inputs does not pass validation, the mint must return a response with a `400` status code, and a body of the following format: ```json { - "processed": [, ...], + "funded": [ + { + "dlc_root": , + "signature": + }, + ... + ], "errors": [ { "dlc_root": , @@ -238,28 +288,63 @@ If one or more inputs does NOT pass validation, the mint must return a response } ``` -The `processed` array tells the funder which DLCs have been successfully registered, while the `errors` field tells the funder which DLCs failed to register, and which specific input proofs for that DLC were faulty. This allows participants to resolve funding disputes. [^5] +The `funded` array tells the funder which DLCs have been successfully registered, while the `errors` field tells the funder which DLCs failed to register, and which specific input proofs for that DLC were faulty. This allows participants to resolve funding disputes. [^5] [^5]: To resolve a funding dispute where some _accused_ participants supply faulty DLC funding proofs, but refuse to acknowledge their mistake, the funder may broadcast the full set of (locked) DLC funding proofs to all _bystander_ participants. The accused participants also broadcast the proofs they supplied to the funder. The bystanders retry the `POST /v1/dlc/fund` request with both possible sets of input proofs to confirm the mint indeed reports the same failure on both. If the mint accepts any of the funding requests, then the dispute is resolved and the DLC has been funded. If the mint reports errors for both sets of input proofs, the bystanders can use the `bad_inputs` field determine who was behaving dishonestly and evict them from the group. -If all the input proofs of a DLC registration object pass validation, the mint stores the `dlc_root` and the total `funding_amount` (i.e. the sum value of all `inputs`). **It is vital for the mint to remember this state,** as the `(dlc_root, funding_amount)` tuple is now the only valid record that exists anywhere of the participants' joint deposit. - If every DLC in the `registrations` array is processed successfully, the mint must return a `200 OK` response with the following response body format: ```json { - "processed": [ + "funded": [ { "dlc_root": + "signature": }, ... ] } ``` -Each object in the `processed` array lists the root hash of a successfully registered DLC. +Each object in the `funded` array represents a successfully registered DLC. The `signature` field in each object is a [BIP-340] signature on `dlc_root` (hex-decoded), issued by the mint's `funding_proof_pubkey` (specified in [the NUT-06 settings](#mint-settings). This signature is a non-interactive proof that the mint has registered the DLC. The funder may publish this signature to the other DLC participants as evidence that they accomplished their duty as the funder. + +## Fees + +Mints may charge fees to register DLCs. This section describes how wallets and mints must calculate fees. + +Implementations must look up the appropriate `fees` object [specified in the mint's NUT-DLC settings object](#mint-settings) based on the relevant `unit` in question. + +Let `funding_amount` represent the amount of money the funder specifies in a DLC funding request. The `total_fee` for the DLC is computed by the following formula. + +```python +total_fee = fees.base + (funding_amount * fees.ppk // 1000) +``` +... where the `//` operator represents remainder-discarding integer division. + +An example with a base fee of `50` and a parts-per-thousand fee rate of `35` (i.e. 3.5%): -### Settling the DLC +```python +funding_amount = 53122 + +total_fee = 50 + (53122 * 35 // 1000) + = 50 + (1859270 // 1000) + = 50 + 1859 + = 1909 +``` + +The `input_amount` which the mint requires to fund a DLC with this `funding_amount` is: + +```python +input_amount = funding_amount + total_fee + = 53122 + 1909 + = 55031 +``` + +> [!IMPORTANT] +> +> At funding time, the mint validates the `threshold` tag of the `DLC` well-known secret kind against the `funding_amount`, not including fees. + +## Settling the DLC When the DLC Oracle publishes her attestation, this reveals to the participants a scalar `ki`, the discrete log of `Ki` such that `ki * G = Ki`. @@ -304,7 +389,7 @@ To mark the DLC as settled on the mint, the wallet issues a `POST /v1/dlc/settle The mint verifies each settlement object as follows: -1. If `Settlement.outcome.P` fails to parse as a JSON dictionary mapping pubkeys to positive integers, return an error. +1. If `Settlement.outcome.P` fails to parse as a JSON dictionary mapping pubkeys to positive integers, or if the number of entries in that dictionary exceeds [the `max_payouts` setting](#mint-settings), return an error. 1. If `Settlement.dlc_root` does not correspond to any known funded DLC, return an error. 1. If `Settlement.dlc_root` corresponds to a DLC, but that DLC has already been settled, return an error. 1. Verify `Settlement.merkle_proof`: @@ -424,7 +509,7 @@ For each `Payout` object, the mint performs the following checks: 1. If `Payout.dlc_root` does not correspond to any known funded DLC, return an error. 1. If `Payout.dlc_root` corresponds to a known DLC, but that DLC has not been settled, return an error. 1. If `Payout.pubkey` is not a key in the `debts` map, return an error. -1. If `sum([out.amount for out in Payout.outputs]) != debts[Payout.pubkey]`, the mint must return an error to the client. +1. If `sum([out.amount for out in Payout.outputs]) != debts[Payout.pubkey]`, return an error. If all `Payout` objects are validated successfully, the mint returns a `200` response with the blinded signatures on `Payout.outputs`: @@ -465,21 +550,19 @@ If some `Payout` objects fail the validation checks, the mint returns a `400` re } ``` -Wallets MUST collect and save the blinded signatures from each entry in the `paid` array, even though the mint responded with a `400` error. - -If a wallet needs payout processing to be atomic, they +Wallets MUST collect and save the blinded signatures from each entry in the `paid` array, even if the mint responds with a `400` error. If the wallet passes the `atomic` parameter as `true` in their request body, then the mint must ensure that either _all_ payouts are processed, or else _none_ are. ### Checking the DLC Status -The participants need a way to verify: +The participants need a way to independently verify: - if and when the funder has successfully funded the DLC - if the DLC has been settled - if the DLC has been paid out, and if so to whom -To this end, a wallet issues a `GET /v1/dlc/status/{dlc_root}` request to the mint. +To this end, a wallet may issue a `GET /v1/dlc/status/{dlc_root}` request to the mint. If the DLC is not found, the mint responds with a `400` error. @@ -509,17 +592,14 @@ To prevent needless polling when waiting for a DLC to be funded, wallets MAY iss Once all payouts have been issued, the mint may purge the DLC from its storage, or it may retain the DLC and return `"debts": {}` in the `/v1/dlc/status/{dlc_root}` response body to indicate the DLC has been fully settled and paid. -## TODO - -- prevent a mint from being DOS'd by using payout structure size limits, DLC lifetime limits -- allow mint to charge fees for DLC processing -- give the funder a non-interactive proof of DLC registration so that participants don't have to poll +The mint may also purge the DLC from its storage if the DLC hasn't been settled and fully paid out by the `ttl` specified in the [NUT-06](#mint-settings) settings. ## Previous Work This NUT is inspired by [this original proposal](https://conduition.io/cryptography/ecash-dlc/) for DLC settlement with generic Chaumian Ecash. [NUT-00]: 00.md +[NUT-06]: 06.md [NUT-10]: 10.md [NUT-12]: 12.md [NUT-SCT]: sct.md From ff4295cfafea073815ed8d763242f145625073a0 Mon Sep 17 00:00:00 2001 From: conduition Date: Tue, 4 Jun 2024 02:58:22 +0000 Subject: [PATCH 07/18] outcome blinding can use a single secret --- dlc.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/dlc.md b/dlc.md index f1f0c73f..a5ec2601 100644 --- a/dlc.md +++ b/dlc.md @@ -14,12 +14,12 @@ A Discreet Log Contract (DLC) is a conditional payment which a set of _participa These parameters are: - The number of possible DLC outcomes `n` -- A vector of `n` _outcome blinding secrets_ (scalars) `[b1, b2, ... bn]` [^1] +- An _outcome blinding secret_ scalar `b` [^1] - A vector of `n` _outcome locking points_ `[K1, K2, ... Kn]` [^2] - A vector of `n` _payout structures_ `[P1, P2, ... Pn]` - An optional timeout timestamp `t` and accompanying timeout payout structure `Pt` -[^1]: Wallet clients may opt out of outcome-blinding by setting all of the blinding secrets to zero. +[^1]: Wallet clients may opt out of outcome-blinding by setting the blinding secret to zero. [^2]: The outcome locking point vector abstraction covers both [enum events](https://github.com/discreetlogcontracts/dlcspecs/blob/master/Oracle.md#simple-enumeration) and [digit-decomposition events](https://github.com/discreetlogcontracts/dlcspecs/blob/master/Oracle.md#digit-decomposition). For an enum event, participants simply compute each of locking points from the announced outcome messages and nonce, in a 1:1 mapping. For a digit-decomposition event, participants compute locking points for each relevant outcome _range_ on a per-digit basis, aggregating (summing) the locking points where necessary to produce `[K1, K2, ... Kn]`. @@ -30,10 +30,10 @@ The locking points `[K1, K2, ... Kn]` are elliptic curve points, encoded in SEC1 The locking points are blinded by the participants, to obscure the nature of the DLC from the mint at settlement time. Blinded locking points are computed as: ```python -Ki_ = Ki + bi * G +Ki_ = Ki + b * G ``` -...for some blinding secret `bi` known to all DLC participants. Each locking point SHOULD be allocated a _unique_ blinding secret. +...for some blinding secret `b` known to all DLC participants. The blinding secret should be randomly selected by any participant. It should NOT be derived deterministically from the oracle announcement. ## Payout Structures @@ -348,15 +348,15 @@ input_amount = funding_amount + total_fee When the DLC Oracle publishes her attestation, this reveals to the participants a scalar `ki`, the discrete log of `Ki` such that `ki * G = Ki`. -Any DLC participant who knows the matching blinding secret `bi` can compute `ki_` - the discrete log of the blinded locking point `Ki_`. +Any DLC participant who knows the DLC's blinding secret `b` can compute `ki_` - the discrete log of the blinded locking point `Ki_`. ``` -ki_ = ki + bi +ki_ = ki + b ``` ``` -ki_ * G = (ki + bi) * G - = ki * G + bi * G - = Ki + bi * G +ki_ * G = (ki + b) * G + = ki * G + b * G + = Ki + b * G = Ki_ ``` From 741eac645104cce312d64d5ab15b9b6473479e43 Mon Sep 17 00:00:00 2001 From: conduition Date: Tue, 4 Jun 2024 03:08:48 +0000 Subject: [PATCH 08/18] offer simpler ways to authenticate claims Not all clients will want to support BIP340 signatures, so we should offer a way for them to claim a DLC by simply exposing a secret key to the mint. --- dlc.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/dlc.md b/dlc.md index a5ec2601..4a3ea23c 100644 --- a/dlc.md +++ b/dlc.md @@ -491,8 +491,11 @@ To claim a DLC payout, a participant issues a `POST /v1/dlc/payout` request to t { "dlc_root": "2db63c93043ab646836b38292ed4fcf209ba68307427a4b2a8621e8b1daeb8ed", "pubkey": "03361cd8bd1329fea797a6add1cf1990ffcf2270ceb9fc81eeee0e8e9c1bd0cdf5", - "signature": "60f3c9b766770b46caac1d27e1ae6b77c8866ebaeba0b9489fe6a15a837eaa6fcd6eaa825499c72ac342983983fd3ba3a8a41f56677cc99ffd73da68b59e1383", "outputs": , + "witness": { + "secret": , + "signature": + } }, ... ] @@ -500,12 +503,15 @@ To claim a DLC payout, a participant issues a `POST /v1/dlc/payout` request to t ``` - `Payout.dlc_root` is a DLC merkle root hash. -- `Payout.signature` is a [BIP-340] signature made on the `dlc_root` hash, which should verify against `Payout.pubkey`. +- `Payout.witness` provides at least one method of authentication against `Payout.pubkey`. - `Payout.outputs` is a set of blinded messages for the mint to sign. For each `Payout` object, the mint performs the following checks: -1. Validate that `Payout.signature` is a valid [BIP-340] signature made by `Payout.pubkey` on `payout.dlc_root` +1. Authenticate `Payout.witness`: + - If present, validate `Payout.witness.secret` is the discrete log (private key) of `Payout.pubkey`. + - If present, validate `Payout.witness.signature` as a [BIP-340] signature made by `Payout.pubkey` on `payout.dlc_root` + - If any of the above fields are invalid, or if none are present, return an error. 1. If `Payout.dlc_root` does not correspond to any known funded DLC, return an error. 1. If `Payout.dlc_root` corresponds to a known DLC, but that DLC has not been settled, return an error. 1. If `Payout.pubkey` is not a key in the `debts` map, return an error. From a788a8ac05f5f50b090efaf15f2d456b6b02aaca Mon Sep 17 00:00:00 2001 From: conduition Date: Wed, 5 Jun 2024 01:07:42 +0000 Subject: [PATCH 09/18] use xonly pubkeys for BIP340 signatures --- dlc.md | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/dlc.md b/dlc.md index 4a3ea23c..abcd83da 100644 --- a/dlc.md +++ b/dlc.md @@ -37,11 +37,11 @@ Ki_ = Ki + b * G ## Payout Structures -Payout structures are serialized dictionaries which map `pubkey -> weight`. +Payout structures are serialized dictionaries which map `xonly_pubkey -> weight`. ```json { - : , + : , ... } ``` @@ -52,13 +52,13 @@ This mapping defines how the money used to fund a DLC should be distributed if a ```json { - "0276cedb9a3b160db6a158ad4e468d2437f021293204b3cd4bf6247970d8aff54b": 3, - "028a36f0e6638ea7466665fe174d958212723019ec08f9ce6898d897f88e68aa5d": 2, - "02b4ebb0dda3b9ad83b39e2e31024b777cc0ac205a96b9a6cfab3edea2912ed1b3": 1 + "76cedb9a3b160db6a158ad4e468d2437f021293204b3cd4bf6247970d8aff54b": 3, + "8a36f0e6638ea7466665fe174d958212723019ec08f9ce6898d897f88e68aa5d": 2, + "b4ebb0dda3b9ad83b39e2e31024b777cc0ac205a96b9a6cfab3edea2912ed1b3": 1 } ``` -With this payout structure, the `03a40f...` pubkey is allocated 3 of the 6 available weight units (3+2+1), and so its owner should receive half of the DLC funding amount. Similarly, `028a36...` receives a third of the funding amount, and `02b4eb...` receives the remaining sixth. +With this payout structure, the `76cedb...` pubkey is allocated 3 of the 6 available weight units (3+2+1), and so its owner should receive half of the DLC funding amount. Similarly, `8a36f0...` receives a third of the funding amount, and `b4ebb0...` receives the remaining sixth. Weights must be positive integers. Negative weights or weights equal to zero are invalid, and render the whole payout structure invalid. @@ -116,7 +116,7 @@ Tt = SHA256(hash_to_curve(t.to_bytes(4, 'big')) || Pt) The `hash_to_curve` function is defined in [NUT-00]. [^4] `t` is encoded as a 32-bit big-endian integer before hashing. -[^4]: When constructing `Tt`, we hash `t` to a curve point as a convenience, so that all leaf nodes can be represented as `(Point, Map)` data structures. +[^4]: When constructing `Tt`, we hash `t` to a curve point as a convenience, so that all leaf nodes can be represented as `(Point, str)` data structures. Note that curve points are encoded in SEC1 compressed binary format, while each payout condition `Pi` is encoded as UTF8 JSON before being hashed. Because JSON maps are not ordered, all participants must agree ahead of time on a specific set of serialized payout structures. @@ -142,7 +142,7 @@ A mint which supports this NUT must expose some global parameters to wallets, vi "nuts": { "NUT-DLC": { "supported": true, - "funding_proof_pubkey": , + "funding_proof_pubkey": , "max_payouts": , "ttl": "fees": { @@ -156,7 +156,7 @@ A mint which supports this NUT must expose some global parameters to wallets, vi } ``` -- `funding_proof_pubkey` is a compressed public key with which participants may verify offline that the mint has indeed registered a DLC. +- `funding_proof_pubkey` is a [BIP-340] X-Only public key with which participants may verify offline that the mint has indeed registered a DLC. - `max_payouts` is the maximum size of payout structure this mint will accept. - `ttl` is a duration in seconds, indicating how long the mint promises to store DLCs after registration. The mint may consider a DLC abandoned if the DLC lives longer than the `ttl` declared _at the time of registration._ If `ttl <= 0`, the mint claims it will remember registered DLCs forever. - `fees` is a map describing fee structures for the mint to process DLCs on a per-currency basis. `fees` may be set to the empty object (`{}`) to explicitly disable fees for all currency units. @@ -268,7 +268,7 @@ If one or more inputs does not pass validation, the mint must return a response "funded": [ { "dlc_root": , - "signature": + "funding_proof": }, ... ], @@ -299,14 +299,14 @@ If every DLC in the `registrations` array is processed successfully, the mint mu "funded": [ { "dlc_root": - "signature": + "funding_proof": }, ... ] } ``` -Each object in the `funded` array represents a successfully registered DLC. The `signature` field in each object is a [BIP-340] signature on `dlc_root` (hex-decoded), issued by the mint's `funding_proof_pubkey` (specified in [the NUT-06 settings](#mint-settings). This signature is a non-interactive proof that the mint has registered the DLC. The funder may publish this signature to the other DLC participants as evidence that they accomplished their duty as the funder. +Each object in the `funded` array represents a successfully registered DLC. The `funding_proof` field in each object is a [BIP-340] signature on `dlc_root` (hex-decoded), issued by the mint's `funding_proof_pubkey` (specified in [the NUT-06 settings](#mint-settings). This signature is a non-interactive proof that the mint has registered the DLC. The funder may publish this signature to the other DLC participants as evidence that they accomplished their duty as the funder. ## Fees @@ -369,7 +369,7 @@ To mark the DLC as settled on the mint, the wallet issues a `POST /v1/dlc/settle "dlc_root": "2db63c93043ab646836b38292ed4fcf209ba68307427a4b2a8621e8b1daeb8ed", "outcome": { "k": "8e935aec5668312be8f960a5ecc3c5dd290e39985970bfd093047df7f05cc9ec", - "P": "{\"03361cd8bd1329fea797a6add1cf1990ffcf2270ceb9fc81eeee0e8e9c1bd0cdf5\":\"10000\"}" + "P": "{\"361cd8bd1329fea797a6add1cf1990ffcf2270ceb9fc81eeee0e8e9c1bd0cdf5\":\"10000\"}" }, "merkle_proof": [ "5467757c899a46b847825e632cafc5e960a948045d12fc1143d17966c87ae351", @@ -413,7 +413,7 @@ If the mint's clock reaches the DLC timeout time `t`, any participant can settle "dlc_root": "2db63c93043ab646836b38292ed4fcf209ba68307427a4b2a8621e8b1daeb8ed", "outcome": { "timeout": 1716777419, - "P": "{\"03361cd8bd1329fea797a6add1cf1990ffcf2270ceb9fc81eeee0e8e9c1bd0cdf5\":\"10000\"}" + "P": "{\"361cd8bd1329fea797a6add1cf1990ffcf2270ceb9fc81eeee0e8e9c1bd0cdf5\":\"10000\"}" }, "merkle_proof": [ "5467757c899a46b847825e632cafc5e960a948045d12fc1143d17966c87ae351", @@ -490,7 +490,7 @@ To claim a DLC payout, a participant issues a `POST /v1/dlc/payout` request to t "payouts": [ { "dlc_root": "2db63c93043ab646836b38292ed4fcf209ba68307427a4b2a8621e8b1daeb8ed", - "pubkey": "03361cd8bd1329fea797a6add1cf1990ffcf2270ceb9fc81eeee0e8e9c1bd0cdf5", + "pubkey": "361cd8bd1329fea797a6add1cf1990ffcf2270ceb9fc81eeee0e8e9c1bd0cdf5", "outputs": , "witness": { "secret": , @@ -509,9 +509,9 @@ To claim a DLC payout, a participant issues a `POST /v1/dlc/payout` request to t For each `Payout` object, the mint performs the following checks: 1. Authenticate `Payout.witness`: - - If present, validate `Payout.witness.secret` is the discrete log (private key) of `Payout.pubkey`. + - If present, validate `Payout.witness.secret` is the discrete log (private key) of `Payout.pubkey` (either parity). - If present, validate `Payout.witness.signature` as a [BIP-340] signature made by `Payout.pubkey` on `payout.dlc_root` - - If any of the above fields are invalid, or if none are present, return an error. + - If either of the above fields are invalid, or if neither are present, return an error. 1. If `Payout.dlc_root` does not correspond to any known funded DLC, return an error. 1. If `Payout.dlc_root` corresponds to a known DLC, but that DLC has not been settled, return an error. 1. If `Payout.pubkey` is not a key in the `debts` map, return an error. @@ -587,7 +587,7 @@ If the DLC is found and is settled, the mint responds with: { "settled": true, "debts": { - "03361cd8bd1329fea797a6add1cf1990ffcf2270ceb9fc81eeee0e8e9c1bd0cdf5": + "361cd8bd1329fea797a6add1cf1990ffcf2270ceb9fc81eeee0e8e9c1bd0cdf5": } } ``` From e10d9f2b3e6cc13d2f931fb6affdbbaf8ed4ea0b Mon Sep 17 00:00:00 2001 From: conduition Date: Fri, 19 Jul 2024 06:10:34 +0000 Subject: [PATCH 10/18] specify currency-specific dlc funding parameters --- dlc.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dlc.md b/dlc.md index abcd83da..6ae0e197 100644 --- a/dlc.md +++ b/dlc.md @@ -225,6 +225,7 @@ To register and fund a DLC on the mint, the funder issues a `POST /v1/dlc/fund` { "dlc_root": "2db63c93043ab646836b38292ed4fcf209ba68307427a4b2a8621e8b1daeb8ed", "funding_amount": , + "unit": "sat", "inputs": [ { "amount": 4096, @@ -257,7 +258,9 @@ assert sum(inputs) == input_amount See [the fees section](#fees) for more details. -Assuming the input proofs are all valid and sum to the correct amount, the mint stores the `(dlc_root, funding_amount)`, and marks the `inputs` proofs' secrets as spent. The DLC is then deemed to be funded. +Proofs must be issued by a valid keyset denominated in the same `unit` specified in the request. + +Assuming the input proofs are all valid and sum to the correct amount, the mint stores the `(dlc_root, funding_amount, unit)`, and marks the `inputs` proofs' secrets as spent. The DLC is then deemed to be funded. If the optional `atomic` field in the request body is set to `true`, the mint must process _all_ DLCs in the `registrations` array, or else process none of them. From d1ffefa8d8eb15aac55468948d9075e09ab83494 Mon Sep 17 00:00:00 2001 From: conduition Date: Mon, 29 Jul 2024 16:49:54 +0000 Subject: [PATCH 11/18] Proof.witness must be a string to match other NUTs --- dlc.md | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/dlc.md b/dlc.md index 6ae0e197..e1a539b2 100644 --- a/dlc.md +++ b/dlc.md @@ -197,10 +197,7 @@ Example: "id": "009a1f293253e41e", "secret": "[\"SCT\",{\"nonce\":\"d426a2750847d5775f06560d973b484a5b6315e17efffecb1d8d518876c01615\",\"data\":\"d7578cbc3d5d61a61cb46552f66d7d5fe92ea4606c778e14d662bbe3d887c0d1\"}]", "C": "02bc9097997d81afb2cc7346b5e4345a9346bd2a506eb7958598a72f0cf85163ea", - "witness": { - "leaf_secret": "[\"DLC\",{\"nonce\":\"da62796403af76c80cd6ce9153ed3746\",\"data\":\"2db63c93043ab646836b38292ed4fcf209ba68307427a4b2a8621e8b1daeb8ed\",\"tags\":[[\"threshold\",\"10000\"]]}]", - "merkle_proof": ["009ea9fae527f7914096da1f1ce2480d2e4cfea62480afb88da9219f1c09767f"] - } + "witness": "{\"leaf_secret\":\"[\\\"DLC\\\",{\\\"nonce\\\":\\\"da62796403af76c80cd6ce9153ed3746\\\",\\\"data\\\":\\\"2db63c93043ab646836b38292ed4fcf209ba68307427a4b2a8621e8b1daeb8ed\\\",\\\"tags\\\":[[\\\"threshold\\\",\\\"10000\\\"]]}]\",\"merkle_proof\":[\"009ea9fae527f7914096da1f1ce2480d2e4cfea62480afb88da9219f1c09767f\"]}" }, ... ] @@ -232,12 +229,7 @@ To register and fund a DLC on the mint, the funder issues a `POST /v1/dlc/fund` "id": "009a1f293253e41e", "secret": "[\"SCT\",{\"nonce\":\"d426a2750847d5775f06560d973b484a5b6315e17efffecb1d8d518876c01615\",\"data\":\"d7578cbc3d5d61a61cb46552f66d7d5fe92ea4606c778e14d662bbe3d887c0d1\"}]", "C": "02bc9097997d81afb2cc7346b5e4345a9346bd2a506eb7958598a72f0cf85163ea", - "witness": { - "leaf_secret": "[\"DLC\",{\"nonce\":\"da62796403af76c80cd6ce9153ed3746\",\"data\":\"2db63c93043ab646836b38292ed4fcf209ba68307427a4b2a8621e8b1daeb8ed\",\"tags\":[[\"threshold\",\"10000\"]]}]", - "merkle_proof": [ - "009ea9fae527f7914096da1f1ce2480d2e4cfea62480afb88da9219f1c09767f" - ] - } + "witness": "{\"leaf_secret\":\"[\\\"DLC\\\",{\\\"nonce\\\":\\\"da62796403af76c80cd6ce9153ed3746\\\",\\\"data\\\":\\\"2db63c93043ab646836b38292ed4fcf209ba68307427a4b2a8621e8b1daeb8ed\\\",\\\"tags\\\":[[\\\"threshold\\\",\\\"10000\\\"]]}]\",\"merkle_proof\":[\"009ea9fae527f7914096da1f1ce2480d2e4cfea62480afb88da9219f1c09767f\"]}" }, ... ] From 568c092b810aa7e8c3787c4ba899b7106dff8176 Mon Sep 17 00:00:00 2001 From: conduition Date: Mon, 29 Jul 2024 17:20:36 +0000 Subject: [PATCH 12/18] Proof.witness must be a string to match other NUTs --- sct.md | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/sct.md b/sct.md index ccc32664..efda38fd 100644 --- a/sct.md +++ b/sct.md @@ -131,13 +131,7 @@ To spend this ecash, the wallet must know a `leaf_secret` and a `merkle_proof` o "secret": "[\"SCT\",{\"nonce\":\"d426a2750847d5775f06560d973b484a5b6315e17efffecb1d8d518876c01615\",\"data\":\"18065b939dbbb648749bd5532c740078bb757c3b9f81e0309350a1277fa9a39c\"}]", "C": "02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904", "id": "009a1f293253e41e", - "witness": { - "leaf_secret": "9becd3a8ce24b53beaf8ffb20a497b683b55f87ef87e3814be43a5768bcfe69f", - "merkle_proof": [ - "8da10ed117cad5e89c6131198ffe271166d68dff9ce961ff117bd84297133b77", - "2397636f1aff968e9f8177b8deaaf9514415126e45aa7755841f966f4eb2279f" - ] - } + "witness": "{\"leaf_secret\":\"9becd3a8ce24b53beaf8ffb20a497b683b55f87ef87e3814be43a5768bcfe69f\",\"merkle_proof\":[\"8da10ed117cad5e89c6131198ffe271166d68dff9ce961ff117bd84297133b77\",\"2397636f1aff968e9f8177b8deaaf9514415126e45aa7755841f966f4eb2279f\"]}" } ``` @@ -149,14 +143,7 @@ Here is a different input, which references the same SCT root hash but uses a di "secret": "[\"SCT\",{\"nonce\":\"d426a2750847d5775f06560d973b484a5b6315e17efffecb1d8d518876c01615\",\"data\":\"18065b939dbbb648749bd5532c740078bb757c3b9f81e0309350a1277fa9a39c\"}]", "C": "02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904", "id": "009a1f293253e41e", - "witness": { - "leaf_secret": "[\"P2PK\",{\"tags\":[[\"sigflag\",\"SIG_INPUTS\"]],\"nonce\":\"859d4935c4907062a6297cf4e663e2835d90d97ecdd510745d32f6816323a41f\",\"data\":\"0249098aa8b9d2fbec49ff8598feb17b592b986e62319a4fa488a3dc36387157a7\"}]", - "merkle_proof": [ - "6bad0d7d596cb9048754ee75daf13ee7e204c6e408b83ee67514369e3f8f3f96", - "4ac38d0dffb307a4d704c5c7cc28324fd3c151cfaaeddeaa695b890f1a24050b" - ], - "witness": "{\"signatures\":[\"9ef66b39609fe4b5653ee8cc1d4f7133ca16c6cf1862eca7df626c63d90f19f257241ebae3939baa837e1be25e2996b7062e16ba58877aa8318db20729184ff4\"]}" - } + "witness": "{\"leaf_secret\":\"[\\\"P2PK\\\",{\\\"tags\\\":[[\\\"sigflag\\\",\\\"SIG_INPUTS\\\"]],\\\"nonce\\\":\\\"859d4935c4907062a6297cf4e663e2835d90d97ecdd510745d32f6816323a41f\\\",\\\"data\\\":\\\"0249098aa8b9d2fbec49ff8598feb17b592b986e62319a4fa488a3dc36387157a7\\\"}]\",\"merkle_proof\":[\"6bad0d7d596cb9048754ee75daf13ee7e204c6e408b83ee67514369e3f8f3f96\",\"4ac38d0dffb307a4d704c5c7cc28324fd3c151cfaaeddeaa695b890f1a24050b\"],\"witness\":\"{\\\"signatures\\\":[\\\"9ef66b39609fe4b5653ee8cc1d4f7133ca16c6cf1862eca7df626c63d90f19f257241ebae3939baa837e1be25e2996b7062e16ba58877aa8318db20729184ff4\\\"]}\"}" } ``` From ce22a20b4de9374d14f2dfb387c4564885727be6 Mon Sep 17 00:00:00 2001 From: conduition Date: Sun, 4 Aug 2024 16:05:26 +0000 Subject: [PATCH 13/18] remove atomic flags Remove the atomic flags for client requests. It creates too much implementation complexity for too little use-case. --- dlc.md | 9 --------- 1 file changed, 9 deletions(-) diff --git a/dlc.md b/dlc.md index e1a539b2..9cfddf8e 100644 --- a/dlc.md +++ b/dlc.md @@ -217,7 +217,6 @@ To register and fund a DLC on the mint, the funder issues a `POST /v1/dlc/fund` ```json { - "atomic": , "registrations": [ { "dlc_root": "2db63c93043ab646836b38292ed4fcf209ba68307427a4b2a8621e8b1daeb8ed", @@ -254,8 +253,6 @@ Proofs must be issued by a valid keyset denominated in the same `unit` specified Assuming the input proofs are all valid and sum to the correct amount, the mint stores the `(dlc_root, funding_amount, unit)`, and marks the `inputs` proofs' secrets as spent. The DLC is then deemed to be funded. -If the optional `atomic` field in the request body is set to `true`, the mint must process _all_ DLCs in the `registrations` array, or else process none of them. - If one or more inputs does not pass validation, the mint must return a response with a `400` status code, and a body of the following format: ```json @@ -402,7 +399,6 @@ If the mint's clock reaches the DLC timeout time `t`, any participant can settle ```json { - "atomic": , "settlements": [ { "dlc_root": "2db63c93043ab646836b38292ed4fcf209ba68307427a4b2a8621e8b1daeb8ed", @@ -473,15 +469,12 @@ If some `settlements` failed, the mint must return a `400` response with the fol } ``` -If the wallet passes the optional `atomic` request parameter as `true`, then the mint must process ALL the requested `settlements` successfully, or else process none of them. - ### Claiming Payouts To claim a DLC payout, a participant issues a `POST /v1/dlc/payout` request to the mint with the following body format. ```json { - "atomic": , "payouts": [ { "dlc_root": "2db63c93043ab646836b38292ed4fcf209ba68307427a4b2a8621e8b1daeb8ed", @@ -553,8 +546,6 @@ If some `Payout` objects fail the validation checks, the mint returns a `400` re Wallets MUST collect and save the blinded signatures from each entry in the `paid` array, even if the mint responds with a `400` error. -If the wallet passes the `atomic` parameter as `true` in their request body, then the mint must ensure that either _all_ payouts are processed, or else _none_ are. - ### Checking the DLC Status The participants need a way to independently verify: From b67e62a5686e468513f1f76e4181983efba5ade1 Mon Sep 17 00:00:00 2001 From: conduition Date: Sun, 4 Aug 2024 16:06:48 +0000 Subject: [PATCH 14/18] encode timeout timestamp as a u64 --- dlc.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dlc.md b/dlc.md index 9cfddf8e..18ca047c 100644 --- a/dlc.md +++ b/dlc.md @@ -111,10 +111,10 @@ Ti = SHA256(Ki_ || Pi) The timeout condition (if applicable) is also added as a leaf. ```python -Tt = SHA256(hash_to_curve(t.to_bytes(4, 'big')) || Pt) +Tt = SHA256(hash_to_curve(t.to_bytes(8, 'big')) || Pt) ``` -The `hash_to_curve` function is defined in [NUT-00]. [^4] `t` is encoded as a 32-bit big-endian integer before hashing. +The `hash_to_curve` function is defined in [NUT-00]. [^4] `t` is encoded as a 64-bit big-endian integer before hashing. [^4]: When constructing `Tt`, we hash `t` to a curve point as a convenience, so that all leaf nodes can be represented as `(Point, str)` data structures. @@ -419,7 +419,7 @@ If the mint's clock reaches the DLC timeout time `t`, any participant can settle `Settlement.outcome.timeout` is the timeout timestamp `t`. The steps for the mint to verify the settlement is the same as an outcome settlement (above), with one modification: `Settlement.merkle_proof` is verified differently. ```python -T = hash_to_curve(Settlement.outcome.timeout.to_bytes(4, 'big')) +T = hash_to_curve(Settlement.outcome.timeout.to_bytes(8, 'big')) leaf_hash = SHA256(T || Settlement.outcome.P) assert merkle_verify(dlc_root, Settlement.merkle_proof, leaf_hash) ``` From f111e06cdbdf885c0fa9ec90466453ee812c1a71 Mon Sep 17 00:00:00 2001 From: conduition Date: Sun, 4 Aug 2024 16:57:43 +0000 Subject: [PATCH 15/18] add note about bytewise concatenation symbol --- dlc.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dlc.md b/dlc.md index 18ca047c..73b0bc6d 100644 --- a/dlc.md +++ b/dlc.md @@ -108,6 +108,8 @@ The particulars of a DLC are represented by a Merkle tree. The leaf hashes of th Ti = SHA256(Ki_ || Pi) ``` +(where `||` denotes bytewise concatenation of the binary serialized points). + The timeout condition (if applicable) is also added as a leaf. ```python From 6990d591e7cc1b844a6a52c392d34c609d79cebf Mon Sep 17 00:00:00 2001 From: conduition Date: Sun, 4 Aug 2024 17:56:43 +0000 Subject: [PATCH 16/18] add section describing replay attack mitigations --- dlc.md | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/dlc.md b/dlc.md index 73b0bc6d..4efaf384 100644 --- a/dlc.md +++ b/dlc.md @@ -592,6 +592,50 @@ The mint may also purge the DLC from its storage if the DLC hasn't been settled This NUT is inspired by [this original proposal](https://conduition.io/cryptography/ecash-dlc/) for DLC settlement with generic Chaumian Ecash. +## Appendix A - DLC Replay Attacks + +Bugs and vulnerabilities will arise if the same `dlc_root` hash can be reused. This section will discuss how client implementations should prevent this. + +### How It Would Happen + +Recall that the `dlc_root` is computed by a deterministic function of: + +- the blinded locking points `[K1_, K2_, ... Kn_]` +- the payout structures `[P1, P2, ... Pn]` +- the timeout timestamp `t` +- the timeout payout structure `Pt` + +```py +dlc_root = compute_dlc_root_hash( + [K1_, K2_, ... Kn_], + [P1, P2, ... Pn], + timeout=(t, Pt), +) +``` + +If these parameters are reused across multiple DLC setup procedures, they will result in the same `dlc_root`. This has bad consequences for naive client implementations. + +
+

An Example

+ +For instance, consider a pair of clients who have created DLC `A`. The pair want to double-down on DLC `A`, adding more money to the "pot", as it were. Since DLCs registered on Cashu are immutable until settlement, the clients must instead create and fund a _new_ DLC `B`. When computing the new `dlc_root` to register, the clients might naively reuse the parameters of DLC `A`, computing the same `dlc_root` for DLCs `A` and `B`. At this point, whichever client is designated as the Funder can abuse the other for a free option. + +Name the naive peer Alice and the malicious peer Eve. Eve is designated as the Funder. Once Alice sends Eve a DLC Funding Token, Eve can _reuse_ the `funding_proof` from DLC `A` to falsely convince Alice that DLC `B` was funded, as long as the `funding_amount` for DLCs `A` and `B` are the same (indeed this means `A = B` exactly). Alice now believes the DLC is funded and that her token proofs were spent, but in actuality Eve has retained Alice's `kind: DLC` proofs intended to fund DLC `B`. + +Eve must then wait until the Oracle releases their attestation (or times out), at which point DLC `A` will be settled. After DLC `A` is fully settled and paid out, the mint purges `A` from its memory and the `dlc_root` becomes 'available' in the mint's registry again. Now Eve has a free option: If DLC `A` resolved in Alice's favor, Eve will do nothing, in which case she loses no money. But if DLC `A` resolved in Eve's favor, Eve can re-register the `dlc_root` by spending Alice's proofs for `B`, and then immediately settle it (in Eve's favor) using the already-published attestation. + +If Alice realizes she has been defrauded before settlement time, she can reclaim her proofs through the backup Spending Condition Tree branch. But this assumes Alice is not a naive implementation, in which case this situation shouldn't have occurred in the first place. + +
+ + +### Recommendations for Clients + +The easiest way for clients to prevent replay attacks is to generate or derive at least one unique public key to use for the payout structures of every new DLC they participate in. This ensures every DLC with at least one honest and non-naive participant will derive a unique `dlc_root`. + +Alternatively clients can reject DLCs which reuse parameters from a previous DLC, but this necessitates the client maintains a perfect memory of every DLC it has ever been involved in. + + [NUT-00]: 00.md [NUT-06]: 06.md [NUT-10]: 10.md From 95f47ba6f71ef2c525d06afc0df80e4d851ba7eb Mon Sep 17 00:00:00 2001 From: conduition Date: Sun, 4 Aug 2024 18:02:09 +0000 Subject: [PATCH 17/18] funding proofs use a key from an active keyset instead of dedicated key Also ensure proofs commit to the funding_amount, so that clients can verify the mint registered a DLC of the correct amount. --- dlc.md | 39 ++++++++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/dlc.md b/dlc.md index 4efaf384..3879071a 100644 --- a/dlc.md +++ b/dlc.md @@ -144,7 +144,6 @@ A mint which supports this NUT must expose some global parameters to wallets, vi "nuts": { "NUT-DLC": { "supported": true, - "funding_proof_pubkey": , "max_payouts": , "ttl": "fees": { @@ -158,7 +157,6 @@ A mint which supports this NUT must expose some global parameters to wallets, vi } ``` -- `funding_proof_pubkey` is a [BIP-340] X-Only public key with which participants may verify offline that the mint has indeed registered a DLC. - `max_payouts` is the maximum size of payout structure this mint will accept. - `ttl` is a duration in seconds, indicating how long the mint promises to store DLCs after registration. The mint may consider a DLC abandoned if the DLC lives longer than the `ttl` declared _at the time of registration._ If `ttl <= 0`, the mint claims it will remember registered DLCs forever. - `fees` is a map describing fee structures for the mint to process DLCs on a per-currency basis. `fees` may be set to the empty object (`{}`) to explicitly disable fees for all currency units. @@ -262,7 +260,10 @@ If one or more inputs does not pass validation, the mint must return a response "funded": [ { "dlc_root": , - "funding_proof": + "funding_proof": { + "keyset": , + "signature": + } }, ... ], @@ -293,14 +294,42 @@ If every DLC in the `registrations` array is processed successfully, the mint mu "funded": [ { "dlc_root": - "funding_proof": + "funding_proof": { + "keyset": , + "signature": + } }, ... ] } ``` -Each object in the `funded` array represents a successfully registered DLC. The `funding_proof` field in each object is a [BIP-340] signature on `dlc_root` (hex-decoded), issued by the mint's `funding_proof_pubkey` (specified in [the NUT-06 settings](#mint-settings). This signature is a non-interactive proof that the mint has registered the DLC. The funder may publish this signature to the other DLC participants as evidence that they accomplished their duty as the funder. +Each object in the `funded` array represents a successfully registered DLC. + +### Funding Proofs + +The `funding_proof` field in each `Funded` object provides a [BIP-340] signature issued by the mint. This signature is a non-interactive proof that the mint has registered the DLC with a specific funding amount. The funder may publish this proof object to the other DLC participants as evidence that they accomplished their duty as the funder. + +The key used to create the signature is the _lowest denomination_ key from the keyset indicated by `funding_proof.keyset`. This keyset MUST be active. + +The message to be [BIP-340]-signed by the mint is the following byte-string: + +```py +dlc_root || funding_amount.to_bytes(8, 'big') +``` + +### Verifying `funding_proof` + +Clients MUST verify all of the following assertions: + +- The keyset is active (`mint.keysets[funding_proof.keyset].active` is true). +- The keyset's unit matches the `unit` the client expects (`mint.keysets[funding_proof.keyset].unit == unit`). +- The [BIP-340] signature on the `dlc_root` and `funding_amount` is valid. +- The `funding_amount` matches the client's expectations (use-case dependent). + +If all the above checks pass, then the client can be confident that the given `dlc_root` was funded on the mint. + +Note however that the `funding_proof.signature` doesn't commit to any specific timestamp, and so can be reused ad-nauseum. For this reason, it is important that client implementations [ensure their `dlc_root` must be fresh and random](#appendix-a---dlc-replay-attacks). ## Fees From a86a4e8ce0b9a76ce9b242d6c2c2ab846b3e1955 Mon Sep 17 00:00:00 2001 From: conduition Date: Sun, 4 Aug 2024 18:21:11 +0000 Subject: [PATCH 18/18] add footnote about security of key reuse --- dlc.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/dlc.md b/dlc.md index 3879071a..320d3c95 100644 --- a/dlc.md +++ b/dlc.md @@ -310,7 +310,9 @@ Each object in the `funded` array represents a successfully registered DLC. The `funding_proof` field in each `Funded` object provides a [BIP-340] signature issued by the mint. This signature is a non-interactive proof that the mint has registered the DLC with a specific funding amount. The funder may publish this proof object to the other DLC participants as evidence that they accomplished their duty as the funder. -The key used to create the signature is the _lowest denomination_ key from the keyset indicated by `funding_proof.keyset`. This keyset MUST be active. +The key used to create the signature is the _lowest denomination_ key from the keyset indicated by `funding_proof.keyset`.[^6] This keyset MUST be active. + +[^6]: It is safe to reuse a keyset key for [BIP-340] signing, as a Schnorr signature reveals only `seckey * G * int(bip340_hash(...))` to the verifier. Contrastingly, an ecash proof is computed as `seckey * hash_to_curve(x)`. Abusing a BIP-340 signature to forge a valid proof would thus require finding `(x, y)` such that `bip340_hash(x) * G == hash_to_curve(y)`, which is even less feasible than finding `(d, y)` such that `d * G == hash_to_curve(y)`. The message to be [BIP-340]-signed by the mint is the following byte-string: @@ -550,9 +552,9 @@ If all `Payout` objects are validated successfully, the mint returns a `200` res } ``` -For each `Payout` object whose outputs the mint signs, the mint must simultaneously delete `Payout.pubkey` from the `debts` map to prevent the wallet from claiming twice. [^6] +For each `Payout` object whose outputs the mint signs, the mint must simultaneously delete `Payout.pubkey` from the `debts` map to prevent the wallet from claiming twice. [^7] -[^6]: Network errors may cause the wallet not to safely receive the blinded signatures sent by the mint. To counteract this occurrence, mints are encouraged to set up idempotent request handlers which cache responses, so the wallet can replay its `POST /v1/dlc/payout` request if needed. +[^7]: Network errors may cause the wallet not to safely receive the blinded signatures sent by the mint. To counteract this occurrence, mints are encouraged to set up idempotent request handlers which cache responses, so the wallet can replay its `POST /v1/dlc/payout` request if needed. If some `Payout` objects fail the validation checks, the mint returns a `400` response with the following format: