diff --git a/sct.md b/sct.md new file mode 100644 index 00000000..efda38fd --- /dev/null +++ b/sct.md @@ -0,0 +1,181 @@ +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\"]}" + } +} +```