NUT for DLC execution #128
No reviewers
Labels
No labels
breaking change
bug
documentation
enhancement
needs discussion
needs implementation
new nut
ready
wallet-only
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
forgejo-admin/nuts!128
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "dlcs"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Depends on https://github.com/cashubtc/nuts/pull/127
Closes #122
Adds a NUT which enables DLC execution using a cashu mint as a blind intermediary. Based on this proposal.
@ -0,0 +1,675 @@NUT-DLC: Discreet Log ContractsI think it should be safe to use a single blinding secret
bto mask every locking point, like thisThis would be easier on the client, as the wallet only needs to store one blinding secret per DLC instead of
nsecrets. We would not be exposing any additional information to the mint, because the mint only ever sees at most one blinded locking point, and thus could not use the common factor ofbfor any trickery.Can somebody check me on this?
@ -0,0 +1,675 @@NUT-DLC: Discreet Log ContractsThis could also be a hash preimage, or the dlog of
Payout.pubkey.@ -0,0 +394,4 @@"dlc_root": "2db63c93043ab646836b38292ed4fcf209ba68307427a4b2a8621e8b1daeb8ed","outcome": {"k": "8e935aec5668312be8f960a5ecc3c5dd290e39985970bfd093047df7f05cc9ec","P": "{\"361cd8bd1329fea797a6add1cf1990ffcf2270ceb9fc81eeee0e8e9c1bd0cdf5\":\"10000\"}"shouldn't the value here be 1 as the lone payout gets the entirety of the funding amount?
@ -0,0 +437,4 @@"dlc_root": "2db63c93043ab646836b38292ed4fcf209ba68307427a4b2a8621e8b1daeb8ed","outcome": {"timeout": 1716777419,"P": "{\"361cd8bd1329fea797a6add1cf1990ffcf2270ceb9fc81eeee0e8e9c1bd0cdf5\":\"10000\"}"Also here
@ -0,0 +220,4 @@"registrations": [{"dlc_root": "2db63c93043ab646836b38292ed4fcf209ba68307427a4b2a8621e8b1daeb8ed","funding_amount": <int>,So all the provided inputs must be signed by keys of the same denomination? Or is just funding_amount in sats?
@ -0,0 +394,4 @@"dlc_root": "2db63c93043ab646836b38292ed4fcf209ba68307427a4b2a8621e8b1daeb8ed","outcome": {"k": "8e935aec5668312be8f960a5ecc3c5dd290e39985970bfd093047df7f05cc9ec","P": "{\"361cd8bd1329fea797a6add1cf1990ffcf2270ceb9fc81eeee0e8e9c1bd0cdf5\":\"10000\"}"It can be any positive integer. Since payout values are computed by relative weights, this number can be anything if there is only one recipient in the payout weights map.
@ -0,0 +220,4 @@"registrations": [{"dlc_root": "2db63c93043ab646836b38292ed4fcf209ba68307427a4b2a8621e8b1daeb8ed","funding_amount": <int>,input proofs can be in any combination of denominations, as per the usual rules of swap/melt operations.
The new thing here is the
funding_amountwhich is an explicit indicator to the mint, saying "i want to fund this DLC with exactly this much money". The value of all the input proofs will need to meet that threshold - or more if the mint charges fees. (see L254 for how fees are computed)Note also that the
thresholdtag in theDLCsecret is compared against thefunding_amount@ -0,0 +220,4 @@"registrations": [{"dlc_root": "2db63c93043ab646836b38292ed4fcf209ba68307427a4b2a8621e8b1daeb8ed","funding_amount": <int>,Yes I understand this, but how do we know what denomination the funding amount is in? I, for now, just assumed it is always sats but maybe it should be specified in each registration as an additional field
@ -0,0 +220,4 @@"registrations": [{"dlc_root": "2db63c93043ab646836b38292ed4fcf209ba68307427a4b2a8621e8b1daeb8ed","funding_amount": <int>,Oh sorry, I see what you mean. I forgot about multi-currency support. Yes, good idea, I added a
unitfield to disambiguate that for the mint. This is also something the mint needs to store (probably easier to just have different tables for DLCs funded under differentunits than to concretely store the unit as a string)Note to self todo:
@ -0,0 +1,675 @@NUT-DLC: Discreet Log ContractsCan't we just do:
?
Also just noticed the unix epoch is cut to 4 bytes: this will bug in ~2035 when the epoch is a number bigger than 2^32
@ -0,0 +423,4 @@leaf_hash = SHA256(Settlement.outcome.k * G || Settlement.outcome.P)assert merkle_verify(dlc_root, Settlement.merkle_proof, leaf_hash)```see my other comment on the timeout redemption
@ -0,0 +1,675 @@NUT-DLC: Discreet Log ContractsCorrection: it will be a bug in 2106, since 0xFFFFFFFF is an unsigned integer. Although it probably won't matter, i'd be open to using a 64-bit encoding. Can't hurt
@ -0,0 +1,675 @@NUT-DLC: Discreet Log ContractsPossibly, yes, but I think my approach is faster (one fewer
hash_to_curveinvocations, and one less EC point addition op).It also lends itself better to formal proofs of security: The client's goal with this procedure is to prove to the mint that
Settlement.outcome.timeoutandSettlement.outcome.Pwere committed to at registration time (and also that they are one of the payees inP).If you define
leaf_hash = sha256(hash_to_curve(t)+hash_to_curve(P)), then a formal proof of soundness for the server would need an extra assumption - namely that for any pointE, it is infeasible to find two inputsxandysuch thatH'(x) + H'(y) = E(whereH'is thehash_to_curvefunction). I'm not sure whether that statement can be proven. Wagner's Algorithm could possibly be applied to reduce the amount of work needed to forge a fraudulent proof. I don't think it would be possible/practical but still... it'd be safer and simpler to just dosha256(T || P)instead. That is pretty easy to prove secure.@ -0,0 +1,675 @@NUT-DLC: Discreet Log ContractsI didn't mention this explicitly in the spec, but the reason I chose to do
hash_to_curve(timeout)is because it allows implementations to strongly type the leaves of the merkle tree as tuples of(Point, String). A simpler approach, such as:...would require leaf hashes be expressed as a union of either
(Time, string)or(Point, string).@ -0,0 +1,675 @@NUT-DLC: Discreet Log ContractsI think this is the assumption upon which the whole Chaumian e-cash scheme relies, without the addition. If you have a point
Eof which you know the discrete logarithm (eG = E), and you can find an inputxsuch thatH'(x) = Ethen you break the scheme because you can forge any proof you want.I am familiar with Wagner's algorithm.
Having said this, it's a non-issue. We can keep it like this. Better to have something formally provable.
@ -0,0 +1,675 @@NUT-DLC: Discreet Log ContractsUpdated to use a 64-bit timestamp for futureproofness
Added the following spec changes:
funding_proofsignature by the mint. It now commits to thefunding_amountexplicitly, and to the fundingunitimplicitly, by using a signing key from an active keyset instead of a dedicated key. https://github.com/cashubtc/nuts/pull/128/commits/95f47ba6f71ef2c525d06afc0df80e4d851ba7eb https://github.com/cashubtc/nuts/pull/128/commits/a86a4e8ce0b9a76ce9b242d6c2c2ab846b3e1955@ -0,0 +536,4 @@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.@conduition I think here it should be
sum([out.amount for out in Payout.outputs]) != eligible_amountwhereeligible_amountis calculated as follows:Also, I was thinking we can discriminate based on whether the requested amount is equal or less than the eligible amount:
pubkeyentry from the debts mappubkeyentry displays a weight value calculated such thatnew_eligible_amount == old_eligible_amount - requested_amount@ -0,0 +536,4 @@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.Earlier in the document, we define
debtsas follows:You can view
debtsas a mapping fromweights[pubkey]toweights[pubkey] / sum(weights.values()) * funding_amount. Becausesum((x / sum(set) for x in set)) == 1for anyset, we can assert:(approximately, due to integer division rounding)
In your suggestion you have:
But as i just showed,
sum(debts.values())is approximately equivalent tofunding_amount. Simplify your definition ofeligible_amountand you'll find it's the same asdebts[payout.pubkey]:@ -0,0 +536,4 @@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.Interesting. Is there any reason (other than a computational error) why a wallet wouldn't want to claim its full payout at once?
@ -0,0 +536,4 @@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.Aw, man. I think I might have misinterpreted/read too quickly that part of the spec. I thought the debts map was just the
Settlement.outcome.Ponce it was validated (BTW that's also what I have implemented).Any reason why it can't be?
[EDIT: forget it. much simplier your way]
@ -0,0 +536,4 @@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.It's just something that can happen. Do we let the mint just rug the claimant in that case? I thought it might be better not to.
@ -0,0 +536,4 @@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.Technically you could, but you'd also have to save the
weight_sum. It's simpler to calculate the debts at settlement time rather than at claim time while you still have access to the fullweightsmap.If the claimant's client is buggy for some reason and won't compute the correct debt amount on its own, then a claimant could just use
GET /v1/dlc/status/{dlc_root}to see the correctdebts[pubkey]value and withdraw directly using a different client. But integer arithmetic is deterministic regardless of programming language, so this shouldn't happen as long as clients follow the spec.As for specifically why i'm trying to avoid partial claims, it's because it would allow one more vector for clients to DoS the mint (by consuming space with pubkeys for cheap). This is something I was hoping to clean up once we had a working implementation we can test against.
@ -0,0 +536,4 @@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.Right now it should do everything. It surely has a million problems but you can test it if you want.
@ -0,0 +536,4 @@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.Right now if the DLC is overfunded, the mint automatically adjusts the funding amount. So one client might unknowingly provide an amount which doesn't exactly match their eligible amount, thereby failing the transaction.
@ -0,0 +536,4 @@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 a DLC is overfunded, all clients involved in the DLC must know about it. The
funding_amountis committed to in theDlcFundingProofthat the funder uses to prove DLC registration. If for some reason a funder goes silent after registering and overfunding a DLC, their peer clients can simply useGET /v1/dlc/status/{dlc_root}to see the true funding amount. This endpoint will also explicitly return thedebtsmap once the DLC is settled, allowing faulty clients to recover gracefully if thePOST /v1/dlc/payoutrequest fails due to a mismatching output sum.I just don't see the point in allowing partial payouts when all these options are available. It feels like an invitation to a class of "missing money" bugs in client implementations where clients withdraw some money and leave the rest on the table unknowingly. It also allows one more way to DoS the mint which we'll have to clean up later.
@ -0,0 +536,4 @@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.it's your call if you'd like to implement partial withdrawals (it's your code after all), but I would recommend against it.
@ -0,0 +536,4 @@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.Ok ok. I'll change it back.
@ -0,0 +37,4 @@## Payout StructuresPayout structures are serialized dictionaries which map `xonly_pubkey -> weight`.Can we make the public keys 33-bytes to be consistent with the rest of Cashu? @lollerfirst already implemented this with 33-byte keys.
@ -0,0 +37,4 @@## Payout StructuresPayout structures are serialized dictionaries which map `xonly_pubkey -> weight`.The use of 33-byte pubkeys for BIP340 signatures was a mistake. See https://github.com/cashubtc/nuts/issues/133. New NUTs should use xonly pubkeys when validating BIP340 signatures
Closing as there is no active work. Please reopen if work continues.
Pull request closed