Skip to content

Commit

Permalink
HTLCs (#325)
Browse files Browse the repository at this point in the history
* add htlc files

* refactor mint into several components

* add hash lock signatures

* add refund signature checks

* simplify hash lock signature check

* clean up
  • Loading branch information
callebtc authored Sep 23, 2023
1 parent 6282e0a commit f1b621f
Show file tree
Hide file tree
Showing 14 changed files with 865 additions and 430 deletions.
2 changes: 2 additions & 0 deletions cashu/core/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ class Proof(BaseModel):

p2pksigs: Union[List[str], None] = [] # P2PK signature
p2shscript: Union[P2SHScript, None] = None # P2SH spending condition
htlcpreimage: Union[str, None] = None # HTLC unlocking preimage
htlcsignature: Union[str, None] = None # HTLC unlocking signature
# whether this proof is reserved for sending, used for coin management in the wallet
reserved: Union[None, bool] = False
# unique ID of send attempt, used for grouping pending tokens in the wallet
Expand Down
17 changes: 17 additions & 0 deletions cashu/core/htlc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from typing import Union

from .secret import Secret, SecretKind


class HTLCSecret(Secret):
@classmethod
def from_secret(cls, secret: Secret):
assert secret.kind == SecretKind.HTLC, "Secret is not a HTLC secret"
# NOTE: exclude tags in .dict() because it doesn't deserialize it properly
# need to add it back in manually with tags=secret.tags
return cls(**secret.dict(exclude={"tags"}), tags=secret.tags)

@property
def locktime(self) -> Union[None, int]:
locktime = self.tags.get_tag("locktime")
return int(locktime) if locktime else None
72 changes: 2 additions & 70 deletions cashu/core/p2pk.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
import hashlib
import json
import time
from typing import Any, Dict, List, Optional, Union
from typing import List, Union

from loguru import logger
from pydantic import BaseModel

from .crypto.secp import PrivateKey, PublicKey


class SecretKind:
P2SH = "P2SH"
P2PK = "P2PK"
from .secret import Secret, SecretKind


class SigFlags:
Expand All @@ -21,69 +16,6 @@ class SigFlags:
SIG_ALL = "SIG_ALL" # require signatures on inputs and outputs


class Tags(BaseModel):
"""
Tags are used to encode additional information in the Secret of a Proof.
"""

__root__: List[List[str]] = []

def __init__(self, tags: Optional[List[List[str]]] = None, **kwargs):
super().__init__(**kwargs)
self.__root__ = tags or []

def __setitem__(self, key: str, value: str) -> None:
self.__root__.append([key, value])

def __getitem__(self, key: str) -> Union[str, None]:
return self.get_tag(key)

def get_tag(self, tag_name: str) -> Union[str, None]:
for tag in self.__root__:
if tag[0] == tag_name:
return tag[1]
return None

def get_tag_all(self, tag_name: str) -> List[str]:
all_tags = []
for tag in self.__root__:
if tag[0] == tag_name:
for t in tag[1:]:
all_tags.append(t)
return all_tags


class Secret(BaseModel):
"""Describes spending condition encoded in the secret field of a Proof."""

kind: str
data: str
tags: Tags
nonce: Union[None, str] = None

def serialize(self) -> str:
data_dict: Dict[str, Any] = {
"data": self.data,
"nonce": self.nonce or PrivateKey().serialize()[:32],
}
if self.tags.__root__:
logger.debug(f"Serializing tags: {self.tags.__root__}")
data_dict["tags"] = self.tags.__root__
return json.dumps(
[self.kind, data_dict],
)

@classmethod
def deserialize(cls, from_proof: str):
kind, kwargs = json.loads(from_proof)
data = kwargs.pop("data")
nonce = kwargs.pop("nonce")
tags_list: List = kwargs.pop("tags", None)
tags = Tags(tags=tags_list)
logger.debug(f"Deserialized Secret: {kind}, {data}, {nonce}, {tags}")
return cls(kind=kind, data=data, nonce=nonce, tags=tags)


class P2PKSecret(Secret):
@classmethod
def from_secret(cls, secret: Secret):
Expand Down
4 changes: 2 additions & 2 deletions cashu/core/script.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def step0_carol_privkey():

def step0_carol_checksig_redeemscript(carol_pubkey):
"""Create script"""
txin_redeemScript = CScript([carol_pubkey, OP_CHECKSIG])
txin_redeemScript = CScript([carol_pubkey, OP_CHECKSIG]) # type: ignore
# txin_redeemScript = CScript([-123, OP_CHECKLOCKTIMEVERIFY])
# txin_redeemScript = CScript([3, 3, OP_LESSTHAN, OP_VERIFY])
return txin_redeemScript
Expand Down Expand Up @@ -58,7 +58,7 @@ def step2_carol_sign_tx(txin_redeemScript, privatekey):
tx, txin = step1_bob_carol_create_tx(txin_p2sh_address)
sighash = SignatureHash(txin_redeemScript, tx, 0, SIGHASH_ALL)
sig = privatekey.sign(sighash) + bytes([SIGHASH_ALL])
txin.scriptSig = CScript([sig, txin_redeemScript])
txin.scriptSig = CScript([sig, txin_redeemScript]) # type: ignore
return txin


Expand Down
76 changes: 76 additions & 0 deletions cashu/core/secret.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import json
from typing import Any, Dict, List, Optional, Union

from loguru import logger
from pydantic import BaseModel

from .crypto.secp import PrivateKey


class SecretKind:
P2SH = "P2SH"
P2PK = "P2PK"
HTLC = "HTLC"


class Tags(BaseModel):
"""
Tags are used to encode additional information in the Secret of a Proof.
"""

__root__: List[List[str]] = []

def __init__(self, tags: Optional[List[List[str]]] = None, **kwargs):
super().__init__(**kwargs)
self.__root__ = tags or []

def __setitem__(self, key: str, value: str) -> None:
self.__root__.append([key, value])

def __getitem__(self, key: str) -> Union[str, None]:
return self.get_tag(key)

def get_tag(self, tag_name: str) -> Union[str, None]:
for tag in self.__root__:
if tag[0] == tag_name:
return tag[1]
return None

def get_tag_all(self, tag_name: str) -> List[str]:
all_tags = []
for tag in self.__root__:
if tag[0] == tag_name:
for t in tag[1:]:
all_tags.append(t)
return all_tags


class Secret(BaseModel):
"""Describes spending condition encoded in the secret field of a Proof."""

kind: str
data: str
tags: Tags
nonce: Union[None, str] = None

def serialize(self) -> str:
data_dict: Dict[str, Any] = {
"data": self.data,
"nonce": self.nonce or PrivateKey().serialize()[:32],
}
if self.tags.__root__:
logger.debug(f"Serializing tags: {self.tags.__root__}")
data_dict["tags"] = self.tags.__root__
return json.dumps(
[self.kind, data_dict],
)

@classmethod
def deserialize(cls, from_proof: str):
kind, kwargs = json.loads(from_proof)
data = kwargs.pop("data")
nonce = kwargs.pop("nonce")
tags_list: List = kwargs.pop("tags", None)
tags = Tags(tags=tags_list)
logger.debug(f"Deserialized Secret: {kind}, {data}, {nonce}, {tags}")
return cls(kind=kind, data=data, nonce=nonce, tags=tags)
Loading

0 comments on commit f1b621f

Please sign in to comment.