-
-
Notifications
You must be signed in to change notification settings - Fork 97
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Discreet Log Contracts #576
base: main
Are you sure you want to change the base?
Changes from 78 commits
cc4aeb5
97edb79
383e032
3f44d81
619c778
c22b800
d1fd1d7
043556b
7d22656
5e11a99
a14707b
344cbff
2dfdafe
f45e5e5
258d4ae
77b2631
aa1af77
8b134dd
4c67c3d
e709af8
08529b5
32cc283
b0bfc0e
41ee5e1
ea56b57
6a3e8d3
9a741cb
0b89fb7
1bbca5a
fd12aa3
ffa6858
30ebc3f
ff125d6
4ce0b7b
a5b147b
1dd7abf
a01a77f
8dc0c71
932fa7a
10b6e9f
0da2683
cb7eea2
c6e06e7
2ba5f5b
6b1d04c
c58cf9b
97684a6
42964e7
0b5c191
1f287d3
e4463ac
0c810fa
162cd47
2a3049e
971b43e
47ace2f
c38b2ed
9213daf
18c5a34
a9fb148
be81d78
9c5bf34
62145e2
a5f7590
3ed21e4
0ad83a9
9f10d34
0e35fbf
ad65326
52729b2
231eb25
d561dbe
e35df9b
2b8fa8b
39aa849
cd2bf24
c58947f
58aaee9
a290313
5b35eb7
897141b
aa7e38e
6e204bc
a4084aa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
from hashlib import sha256 | ||
from typing import List, Optional, Tuple | ||
|
||
from secp256k1 import PrivateKey, PublicKey | ||
|
||
|
||
def sorted_merkle_hash(left: bytes, right: bytes) -> bytes: | ||
'''Sorts `left` and `right` in non-ascending order and | ||
computes the hash of their concatenation | ||
''' | ||
if right < left: | ||
left, right = right, left | ||
return sha256(left+right).digest() | ||
|
||
|
||
def merkle_root( | ||
leaf_hashes: List[bytes], | ||
track_branch: Optional[int] = None | ||
) -> Tuple[bytes, Optional[List[bytes]]]: | ||
'''Computes the root of a list of leaf hashes | ||
if `track_branch` is set, extracts the hashes for the branch that leads | ||
to `leaf_hashes[track_branch]` | ||
''' | ||
if track_branch is not None: | ||
if len(leaf_hashes) == 0: | ||
return b"", [] | ||
elif len(leaf_hashes) == 1: | ||
return leaf_hashes[0], [] | ||
else: | ||
split = len(leaf_hashes) // 2 | ||
left, left_branch_hashes = merkle_root(leaf_hashes[:split], | ||
track_branch if track_branch < split else None) | ||
right, right_branch_hashes = merkle_root(leaf_hashes[split:], | ||
track_branch-split if track_branch >= split else None) | ||
branch_hashes = (left_branch_hashes if | ||
track_branch < split else right_branch_hashes) | ||
hashh = sorted_merkle_hash(left, right) | ||
# Needed to pass mypy checks | ||
assert branch_hashes is not None, "merkle_root fail: branch_hashes == None" | ||
branch_hashes.append(right if track_branch < split else left) | ||
return hashh, branch_hashes | ||
else: | ||
if len(leaf_hashes) == 0: | ||
return b"", None | ||
elif len(leaf_hashes) == 1: | ||
return leaf_hashes[0], None | ||
else: | ||
split = len(leaf_hashes) // 2 | ||
left, _ = merkle_root(leaf_hashes[:split], None) | ||
right, _ = merkle_root(leaf_hashes[split:], None) | ||
hashh = sorted_merkle_hash(left, right) | ||
return hashh, None | ||
|
||
def merkle_verify(root: bytes, leaf_hash: bytes, proof: List[bytes]) -> bool: | ||
'''Verifies that `leaf_hash` belongs to a merkle tree | ||
that has `root` as root | ||
''' | ||
h = leaf_hash | ||
for branch_hash in proof: | ||
h = sorted_merkle_hash(h, branch_hash) | ||
return h == root | ||
|
||
def list_hash(leaves: List[str]) -> List[bytes]: | ||
return [sha256(leaf.encode()).digest() for leaf in leaves] | ||
|
||
def sign_dlc( | ||
dlc_root: str, | ||
funding_amount: int, | ||
privkey: PrivateKey, | ||
) -> bytes: | ||
message = ( | ||
bytes.fromhex(dlc_root) | ||
+funding_amount.to_bytes(8, "big") | ||
) | ||
return privkey.schnorr_sign(message, None, raw=True) | ||
|
||
def verify_dlc_signature( | ||
dlc_root: str, | ||
funding_amount: int, | ||
signature: bytes, | ||
pubkey: PublicKey, | ||
) -> bool: | ||
message = ( | ||
bytes.fromhex(dlc_root) | ||
+funding_amount.to_bytes(8, "big") | ||
) | ||
return pubkey.schnorr_verify(message, signature, None, raw=True) | ||
|
||
def verify_payout_signature( | ||
dlc_root: bytes, | ||
signature: bytes, | ||
pubkey: PublicKey, | ||
) -> bool: | ||
return pubkey.schnorr_verify(dlc_root, signature, None, raw=True) | ||
|
||
def verify_payout_secret( | ||
secret: bytes, | ||
pubkey: PublicKey, | ||
) -> bool: | ||
return pubkey == PrivateKey(secret, raw=True).pubkey |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,6 +6,14 @@ | |
BlindedMessage, | ||
BlindedMessage_Deprecated, | ||
BlindedSignature, | ||
DiscreetLogContract, | ||
DlcFundingAck, | ||
DlcFundingError, | ||
DlcPayout, | ||
DlcPayoutForm, | ||
DlcSettlement, | ||
DlcSettlementAck, | ||
DlcSettlementError, | ||
MeltQuote, | ||
MintQuote, | ||
Proof, | ||
|
@@ -339,3 +347,38 @@ class PostRestoreResponse(BaseModel): | |
def __init__(self, **data): | ||
super().__init__(**data) | ||
self.promises = self.signatures | ||
|
||
|
||
# ------- API: DLC REGISTRATION ------- | ||
|
||
class PostDlcRegistrationRequest(BaseModel): | ||
registrations: List[DiscreetLogContract] | ||
|
||
class PostDlcRegistrationResponse(BaseModel): | ||
funded: List[DlcFundingAck] = [] | ||
errors: Optional[List[DlcFundingError]] = None | ||
|
||
# ------- API: DLC SETTLEMENT ------- | ||
|
||
class PostDlcSettleRequest(BaseModel): | ||
settlements: List[DlcSettlement] | ||
|
||
class PostDlcSettleResponse(BaseModel): | ||
settled: List[DlcSettlementAck] = [] | ||
errors: Optional[List[DlcSettlementError]] = None | ||
|
||
# ------- API: DLC PAYOUT ------- | ||
class PostDlcPayoutRequest(BaseModel): | ||
payouts: List[DlcPayoutForm] | ||
|
||
class PostDlcPayoutResponse(BaseModel): | ||
paid: List[DlcPayout] | ||
errors: Optional[List[DlcPayout]] | ||
|
||
# ------- API: DLC STATUS ------- | ||
|
||
class GetDlcStatusResponse(BaseModel): | ||
settled: bool | ||
unit: Optional[str] = None | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is inconsistent from the spec. There is no unit field defined in the responses. I think including the keyset ID in the status response would make more sense than unit if anything. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Weird mistake. I'll be sure to fix that. |
||
funding_amount: Optional[int] = None | ||
debts: Optional[Dict[str, int]] = None |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,3 +11,4 @@ | |
DETERMINSTIC_SECRETS_NUT = 13 | ||
MPP_NUT = 15 | ||
WEBSOCKETS_NUT = 17 | ||
DLC_NUT = 99 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I noticed this conflicts with the spec (the witness in the spec was an object, not a string). I see
Proof.witness
is strictly typed to be anOptional[str]
in nutshell, so i changed the spec to match.cashubtc/nuts@d1ffefa