diff --git a/dlc.md b/dlc.md new file mode 100644 index 00000000..320d3c95 --- /dev/null +++ b/dlc.md @@ -0,0 +1,675 @@ +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` +- 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 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]`. + +## 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 + b * G +``` + +...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 + +Payout structures are serialized dictionaries which map `xonly_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 +{ + "76cedb9a3b160db6a158ad4e468d2437f021293204b3cd4bf6247970d8aff54b": 3, + "8a36f0e6638ea7466665fe174d958212723019ec08f9ce6898d897f88e68aa5d": 2, + "b4ebb0dda3b9ad83b39e2e31024b777cc0ac205a96b9a6cfab3edea2912ed1b3": 1 +} +``` + +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. + +## 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] 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. + +## 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) +``` + +(where `||` denotes bytewise concatenation of the binary serialized points). + +The timeout condition (if applicable) is also added as a leaf. + +```python +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 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. + +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. + +## 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, + "max_payouts": , + "ttl": + "fees": { + "sat": { + "base": , + "ppk": + } + } + } + } +} +``` + +- `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. + +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 ([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 + +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 +{ + "registrations": [ + { + "dlc_root": "2db63c93043ab646836b38292ed4fcf209ba68307427a4b2a8621e8b1daeb8ed", + "funding_amount": , + "unit": "sat", + "inputs": [ + { + "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\"]}" + }, + ... + ] + }, + ... + ] +} +``` + +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. + +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 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 +{ + "funded": [ + { + "dlc_root": , + "funding_proof": { + "keyset": , + "signature": + } + }, + ... + ], + "errors": [ + { + "dlc_root": , + "bad_inputs": [ + { + "index": , + "detail": + }, + ... + ] + }, + ... + ] +} +``` + +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 every DLC in the `registrations` array is processed successfully, the mint must return a `200 OK` response with the following response body format: + +```json +{ + "funded": [ + { + "dlc_root": + "funding_proof": { + "keyset": , + "signature": + } + }, + ... + ] +} +``` + +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`.[^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: + +```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 + +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%): + +```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`. + +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 + b +``` +``` +ki_ * G = (ki + b) * G + = ki * G + b * G + = Ki + b * 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": "{\"361cd8bd1329fea797a6add1cf1990ffcf2270ceb9fc81eeee0e8e9c1bd0cdf5\":\"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, 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`: + +```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 +{ + "settlements": [ + { + "dlc_root": "2db63c93043ab646836b38292ed4fcf209ba68307427a4b2a8621e8b1daeb8ed", + "outcome": { + "timeout": 1716777419, + "P": "{\"361cd8bd1329fea797a6add1cf1990ffcf2270ceb9fc81eeee0e8e9c1bd0cdf5\":\"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(8, '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": + }, + ... + ] +} +``` + +### 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 +{ + "payouts": [ + { + "dlc_root": "2db63c93043ab646836b38292ed4fcf209ba68307427a4b2a8621e8b1daeb8ed", + "pubkey": "361cd8bd1329fea797a6add1cf1990ffcf2270ceb9fc81eeee0e8e9c1bd0cdf5", + "outputs": , + "witness": { + "secret": , + "signature": + } + }, + ... + ] +} +``` + +- `Payout.dlc_root` is a DLC merkle root hash. +- `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. Authenticate `Payout.witness`: + - 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 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. +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`: + +```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. [^7] + +[^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: + +```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 if the mint responds with a `400` error. + +### Checking the DLC Status + +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 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. + +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": { + "361cd8bd1329fea797a6add1cf1990ffcf2270ceb9fc81eeee0e8e9c1bd0cdf5": + } +} +``` + +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. + +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. + +## 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 +[NUT-12]: 12.md +[NUT-SCT]: sct.md +[BIP-340]: https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki 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\"]}" + } +} +```