Skip to content

Commit

Permalink
Merge branch 'main' into bolt11
Browse files Browse the repository at this point in the history
  • Loading branch information
callebtc authored Sep 24, 2023
2 parents 2988c43 + 64805e4 commit aebbe6b
Show file tree
Hide file tree
Showing 45 changed files with 2,704 additions and 1,475 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9"]
python-version: ["3.10.4"]
poetry-version: ["1.5.1"]
steps:
- uses: actions/checkout@v2
Expand All @@ -28,7 +28,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9"]
python-version: ["3.10.4"]
poetry-version: ["1.5.1"]
steps:
- uses: actions/checkout@v2
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest]
python-version: ["3.9.13"]
python-version: ["3.10.4"]
poetry-version: ["1.5.1"]
steps:
- name: Checkout repository and submodules
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ curl https://pyenv.run | bash
pyenv init

# restart your shell (or source your .rc file), then install python:
pyenv install 3.9.13
pyenv install 3.10.4

# install poetry
curl -sSL https://install.python-poetry.org | python3 -
Expand All @@ -72,7 +72,7 @@ source ~/.bashrc
# install cashu
git clone https://github.com/callebtc/cashu.git --recurse-submodules
cd cashu
pyenv local 3.9.13
pyenv local 3.10.4
poetry install
```

Expand Down Expand Up @@ -114,7 +114,7 @@ cashu info

Returns:
```bash
Version: 0.13.0
Version: 0.14.0
Debug: False
Cashu dir: /home/user/.cashu
Wallet: wallet
Expand Down
194 changes: 50 additions & 144 deletions cashu/core/base.py
Original file line number Diff line number Diff line change
@@ -1,157 +1,37 @@
import base64
import json
import time
from sqlite3 import Row
from typing import Any, Dict, List, Optional, Union
from typing import Dict, List, Optional, Union

from loguru import logger
from pydantic import BaseModel

from .crypto.keys import derive_keys, derive_keyset_id, derive_pubkeys
from .crypto.secp import PrivateKey, PublicKey
from .legacy import derive_keys_backwards_compatible_insecure_pre_0_12
from .p2pk import P2SHScript

# from .p2pk import sign_p2pk_sign

# ------- PROOFS -------


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


class SigFlags:
SIG_INPUTS = ( # require signatures only on the inputs (default signature flag)
"SIG_INPUTS"
)
SIG_ALL = "SIG_ALL" # require signatures on inputs and outputs


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

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

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

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:
all_tags.append(tag[1])
return all_tags


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

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

def serialize(self) -> str:
data_dict: Dict[str, Any] = {
"data": self.data,
"nonce": self.nonce or PrivateKey().serialize()[:32],
}
if self.tags and 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 = kwargs.pop("tags", None)
if tags_list:
tags = Tags(tags=tags_list)
else:
tags = None
logger.debug(f"Deserialized Secret: {kind}, {data}, {nonce}, {tags}")
return cls(kind=kind, data=data, nonce=nonce, tags=tags)

@property
def locktime(self) -> Union[None, int]:
if self.tags:
locktime = self.tags.get_tag("locktime")
if locktime:
return int(locktime)
return None

@property
def sigflag(self) -> Union[None, str]:
if self.tags:
sigflag = self.tags.get_tag("sigflag")
if sigflag:
return sigflag
return None

@property
def n_sigs(self) -> Union[None, int]:
if self.tags:
n_sigs = self.tags.get_tag("n_sigs")
if n_sigs:
return int(n_sigs)
return None

def get_p2pk_pubkey_from_secret(self) -> List[str]:
"""Gets the P2PK pubkey from a Secret depending on the locktime
Args:
secret (Secret): P2PK Secret in ecash token
Returns:
str: pubkey to use for P2PK, empty string if anyone can spend (locktime passed)
"""
pubkeys: List[str] = [self.data] # for now we only support one pubkey
# get all additional pubkeys from tags for multisig
if self.tags and self.tags.get_tag("pubkey"):
pubkeys += self.tags.get_tag_all("pubkey")

now = time.time()
if self.locktime and self.locktime < now:
logger.trace(f"p2pk locktime ran out ({self.locktime}<{now}).")
# check tags if a refund pubkey is present.
# If yes, we demand the signature to be from the refund pubkey
if self.tags:
refund_pubkey = self.tags.get_tag("refund")
if refund_pubkey:
pubkeys = [refund_pubkey]
return pubkeys
return []
return pubkeys


class P2SHScript(BaseModel):
class DLEQWallet(BaseModel):
"""
Unlocks P2SH spending condition of a Proof
Discrete Log Equality (DLEQ) Proof
"""

script: str
signature: str
address: Union[str, None] = None
e: str
s: str
r: str # blinding_factor, unknown to mint but sent from wallet to wallet for DLEQ proof


# ------- PROOFS -------


class Proof(BaseModel):
Expand All @@ -164,8 +44,12 @@ class Proof(BaseModel):
amount: int = 0
secret: str = "" # secret or message to be blinded and signed
C: str = "" # signature on secret, unblinded by wallet
dleq: Union[DLEQWallet, None] = None # DLEQ proof

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 All @@ -174,7 +58,28 @@ class Proof(BaseModel):
time_reserved: Union[None, str] = ""
derivation_path: Union[None, str] = "" # derivation path of the proof

def to_dict(self):
@classmethod
def from_dict(cls, proof_dict: dict):
if proof_dict.get("dleq"):
proof_dict["dleq"] = DLEQWallet(**json.loads(proof_dict["dleq"]))
c = cls(**proof_dict)
return c

def to_dict(self, include_dleq=False):
# dictionary without the fields that don't need to be send to Carol
if not include_dleq:
return dict(id=self.id, amount=self.amount, secret=self.secret, C=self.C)

assert self.dleq, "DLEQ proof is missing"
return dict(
id=self.id,
amount=self.amount,
secret=self.secret,
C=self.C,
dleq=self.dleq.dict(),
)

def to_dict_no_dleq(self):
# dictionary without the fields that don't need to be send to Carol
return dict(id=self.id, amount=self.amount, secret=self.secret, C=self.C)

Expand Down Expand Up @@ -209,9 +114,10 @@ class BlindedSignature(BaseModel):
Blinded signature or "promise" which is the signature on a `BlindedMessage`
"""

id: Union[str, None] = None
id: str
amount: int
C_: str # Hex-encoded signature
dleq: Optional[DLEQ] = None # DLEQ proof


class BlindedMessages(BaseModel):
Expand Down Expand Up @@ -436,7 +342,7 @@ class MintKeyset:
Contains the keyset from the mint's perspective.
"""

id: Union[str, None]
id: str
derivation_path: str
private_keys: Dict[int, PrivateKey]
public_keys: Union[Dict[int, PublicKey], None] = None
Expand All @@ -448,7 +354,7 @@ class MintKeyset:

def __init__(
self,
id=None,
id="",
valid_from=None,
valid_to=None,
first_seen=None,
Expand Down Expand Up @@ -551,8 +457,8 @@ class TokenV3Token(BaseModel):
mint: Optional[str] = None
proofs: List[Proof]

def to_dict(self):
return_dict = dict(proofs=[p.to_dict() for p in self.proofs])
def to_dict(self, include_dleq=False):
return_dict = dict(proofs=[p.to_dict(include_dleq) for p in self.proofs])
if self.mint:
return_dict.update(dict(mint=self.mint)) # type: ignore
return return_dict
Expand All @@ -566,8 +472,8 @@ class TokenV3(BaseModel):
token: List[TokenV3Token] = []
memo: Optional[str] = None

def to_dict(self):
return_dict = dict(token=[t.to_dict() for t in self.token])
def to_dict(self, include_dleq=False):
return_dict = dict(token=[t.to_dict(include_dleq) for t in self.token])
if self.memo:
return_dict.update(dict(memo=self.memo)) # type: ignore
return return_dict
Expand All @@ -594,14 +500,14 @@ def deserialize(cls, tokenv3_serialized: str) -> "TokenV3":
token = json.loads(base64.urlsafe_b64decode(token_base64))
return cls.parse_obj(token)

def serialize(self) -> str:
def serialize(self, include_dleq=False) -> str:
"""
Takes a TokenV3 and serializes it as "cashuA<json_urlsafe_base64>.
"""
prefix = "cashuA"
tokenv3_serialized = prefix
# encode the token as a base64 string
tokenv3_serialized += base64.urlsafe_b64encode(
json.dumps(self.to_dict()).encode()
json.dumps(self.to_dict(include_dleq)).encode()
).decode()
return tokenv3_serialized
Loading

0 comments on commit aebbe6b

Please sign in to comment.