Merge pull request #93 from cashubtc/SpringClean

PostMintRequest format change
This commit is contained in:
calle
2023-01-14 20:24:07 +01:00
committed by GitHub
18 changed files with 400 additions and 228 deletions

View File

@@ -115,7 +115,7 @@ cashu info
Returns:
```bash
Version: 0.7.1
Version: 0.8
Debug: False
Cashu dir: /home/user/.cashu
Wallet: wallet

View File

@@ -6,6 +6,8 @@ from pydantic import BaseModel
from cashu.core.crypto import derive_keys, derive_keyset_id, derive_pubkeys
from cashu.core.secp import PrivateKey, PublicKey
# ------- PROOFS -------
class P2SHScript(BaseModel):
script: str
@@ -16,7 +18,7 @@ class P2SHScript(BaseModel):
class Proof(BaseModel):
id: Union[
None, str
] = "" # NOTE: None for backwards compatibility of old clients < 0.3
] = "" # NOTE: None for backwards compatibility for old clients that do not include the keyset id < 0.3
amount: int = 0
secret: str = ""
C: str = ""
@@ -39,10 +41,7 @@ class Proof(BaseModel):
self.__setattr__(key, val)
class Proofs(BaseModel):
"""TODO: Use this model"""
proofs: List[Proof]
# ------- LIGHTNING INVOICE -------
class Invoice(BaseModel):
@@ -56,9 +55,21 @@ class Invoice(BaseModel):
time_paid: Union[None, str, int, float] = ""
class BlindedMessage(BaseModel):
amount: int
B_: str
# ------- API -------
# ------- API: KEYS -------
class KeysResponse(BaseModel):
__root__: Dict[str, str]
class KeysetsResponse(BaseModel):
keysets: list[str]
# ------- API: MINT -------
class BlindedSignature(BaseModel):
@@ -67,8 +78,13 @@ class BlindedSignature(BaseModel):
C_: str
class MintRequest(BaseModel):
blinded_messages: List[BlindedMessage] = []
class PostMintResponseLegacy(BaseModel):
# NOTE: Backwards compability for < 0.8 where we used a simple list and not a key-value dictionary
__root__: List[BlindedSignature] = []
class PostMintResponse(BaseModel):
promises: List[BlindedSignature] = []
class GetMintResponse(BaseModel):
@@ -76,18 +92,38 @@ class GetMintResponse(BaseModel):
hash: str
# ------- API: MELT -------
class MeltRequest(BaseModel):
proofs: List[Proof]
invoice: str
class GetMeltResponse(BaseModel):
paid: Union[bool, None]
preimage: Union[str, None]
# ------- API: SPLIT -------
class BlindedMessage(BaseModel):
amount: int
B_: str
class BlindedMessages(BaseModel):
blinded_messages: List[BlindedMessage] = []
class SplitRequest(BaseModel):
proofs: List[Proof]
amount: int
output_data: Union[
MintRequest, None
] = None # backwards compatibility with clients < v0.2.2
outputs: Union[MintRequest, None] = None
BlindedMessages, None
] = None # backwards compatibility with clients that called this output_data and not outputs < v0.2.2
outputs: Union[BlindedMessages, None] = None
def __init__(self, **data):
super().__init__(**data)
@@ -105,6 +141,9 @@ class PostSplitResponse(BaseModel):
snd: List[BlindedSignature]
# ------- API: CHECK -------
class CheckRequest(BaseModel):
proofs: List[Proof]
@@ -117,29 +156,35 @@ class CheckFeesResponse(BaseModel):
fee: Union[int, None]
class MeltRequest(BaseModel):
proofs: List[Proof]
invoice: str
# ------- KEYSETS -------
class KeyBase(BaseModel):
"""
Public key from a keyset id for a given amount.
"""
id: str
amount: int
pubkey: str
class WalletKeyset:
id: str
public_keys: Dict[int, PublicKey]
"""
Contains the keyset from the wallets's perspective.
"""
id: Union[str, None]
public_keys: Union[Dict[int, PublicKey], None]
mint_url: Union[str, None] = None
valid_from: Union[str, None] = None
valid_to: Union[str, None] = None
first_seen: Union[str, None] = None
active: bool = True
active: Union[bool, None] = True
def __init__(
self,
pubkeys: Dict[int, PublicKey] = None,
public_keys=None,
mint_url=None,
id=None,
valid_from=None,
@@ -153,20 +198,24 @@ class WalletKeyset:
self.first_seen = first_seen
self.active = active
self.mint_url = mint_url
if pubkeys:
self.public_keys = pubkeys
if public_keys:
self.public_keys = public_keys
self.id = derive_keyset_id(self.public_keys)
class MintKeyset:
id: str
"""
Contains the keyset from the mint's perspective.
"""
id: Union[str, None]
derivation_path: str
private_keys: Dict[int, PrivateKey]
public_keys: Dict[int, PublicKey] = {}
public_keys: Union[Dict[int, PublicKey], None] = None
valid_from: Union[str, None] = None
valid_to: Union[str, None] = None
first_seen: Union[str, None] = None
active: bool = True
active: Union[bool, None] = True
version: Union[str, None] = None
def __init__(
@@ -194,37 +243,50 @@ class MintKeyset:
def generate_keys(self, seed):
"""Generates keys of a keyset from a seed."""
self.private_keys = derive_keys(seed, self.derivation_path)
self.public_keys = derive_pubkeys(self.private_keys)
self.id = derive_keyset_id(self.public_keys)
self.public_keys = derive_pubkeys(self.private_keys) # type: ignore
self.id = derive_keyset_id(self.public_keys) # type: ignore
def get_keybase(self):
assert self.id is not None
return {
k: KeyBase(id=self.id, amount=k, pubkey=v.serialize().hex())
for k, v in self.public_keys.items()
for k, v in self.public_keys.items() # type: ignore
}
class MintKeysets:
"""
Collection of keyset IDs and the corresponding keyset of the mint.
"""
keysets: Dict[str, MintKeyset]
def __init__(self, keysets: List[MintKeyset]):
self.keysets: Dict[str, MintKeyset] = {k.id: k for k in keysets}
self.keysets = {k.id: k for k in keysets} # type: ignore
def get_ids(self):
return [k for k, _ in self.keysets.items()]
class TokenMintJson(BaseModel):
# ------- TOKEN -------
class TokenV1(BaseModel):
# NOTE: not used in Pydantic validation
__root__: List[Proof]
class TokenMintV2(BaseModel):
url: str
ks: List[str]
class TokenJson(BaseModel):
tokens: List[Proof]
mints: Optional[Dict[str, TokenMintJson]] = None
class TokenV2(BaseModel):
proofs: List[Proof]
mints: Optional[Dict[str, TokenMintV2]] = None
def to_dict(self):
return dict(
tokens=[p.to_dict() for p in self.tokens],
proofs=[p.to_dict() for p in self.proofs],
mints={k: v.dict() for k, v in self.mints.items()}, # type: ignore
)

View File

@@ -2,25 +2,20 @@ from pydantic import BaseModel
class CashuError(BaseModel):
code = "000"
error = "CashuError"
code: int
error: str
# class CashuError(Exception, BaseModel):
# code = "000"
# error = "CashuError"
class MintException(CashuError):
code = 100
error = "Mint"
# class MintException(CashuError):
# code = 100
# error = "Mint"
class LightningException(MintException):
code = 200
error = "Lightning"
# class LightningException(MintException):
# code = 200
# error = "Lightning"
# class InvoiceNotPaidException(LightningException):
# code = 201
# error = "invoice not paid."
class InvoiceNotPaidException(LightningException):
code = 201
error = "invoice not paid."

View File

@@ -57,4 +57,4 @@ NOSTR_PRIVATE_KEY = env.str("NOSTR_PRIVATE_KEY", default=None)
NOSTR_RELAYS = env.list("NOSTR_RELAYS", default=["wss://nostr-pub.wellorder.net"])
MAX_ORDER = 64
VERSION = "0.7.1"
VERSION = "0.8"

View File

@@ -4,14 +4,17 @@ from fastapi import APIRouter
from secp256k1 import PublicKey
from cashu.core.base import (
BlindedMessages,
BlindedSignature,
CheckFeesRequest,
CheckFeesResponse,
CheckRequest,
GetMeltResponse,
GetMintResponse,
KeysetsResponse,
KeysResponse,
MeltRequest,
MintRequest,
PostMintResponse,
PostSplitResponse,
SplitRequest,
)
@@ -22,28 +25,31 @@ router: APIRouter = APIRouter()
@router.get("/keys")
async def keys() -> dict[int, str]:
async def keys() -> KeysResponse:
"""Get the public keys of the mint of the newest keyset"""
keyset = ledger.get_keyset()
return keyset
keys = KeysResponse.parse_obj(keyset)
return keys
@router.get("/keys/{idBase64Urlsafe}")
async def keyset_keys(idBase64Urlsafe: str) -> dict[int, str]:
async def keyset_keys(idBase64Urlsafe: str) -> KeysResponse:
"""
Get the public keys of the mint of a specificy keyset id.
The id is encoded in base64_urlsafe and needs to be converted back to
Get the public keys of the mint of a specific keyset id.
The id is encoded in idBase64Urlsafe and needs to be converted back to
normal base64 before it can be processed.
"""
id = idBase64Urlsafe.replace("-", "+").replace("_", "/")
keyset = ledger.get_keyset(keyset_id=id)
return keyset
keys = KeysResponse.parse_obj(keyset)
return keys
@router.get("/keysets")
async def keysets() -> dict[str, list[str]]:
"""Get all active keysets of the mint"""
return {"keysets": ledger.keysets.get_ids()}
async def keysets() -> KeysetsResponse:
"""Get all active keyset ids of the mint"""
keysets = KeysetsResponse(keysets=ledger.keysets.get_ids())
return keysets
@router.get("/mint")
@@ -62,9 +68,9 @@ async def request_mint(amount: int = 0) -> GetMintResponse:
@router.post("/mint")
async def mint(
mint_request: MintRequest,
mint_request: BlindedMessages,
payment_hash: Union[str, None] = None,
) -> Union[List[BlindedSignature], CashuError]:
) -> Union[PostMintResponse, CashuError]:
"""
Requests the minting of tokens belonging to a paid payment request.
@@ -74,9 +80,10 @@ async def mint(
promises = await ledger.mint(
mint_request.blinded_messages, payment_hash=payment_hash
)
return promises
blinded_signatures = PostMintResponse(promises=promises)
return blinded_signatures
except Exception as exc:
return CashuError(error=str(exc))
return CashuError(code=0, error=str(exc))
@router.post("/melt")
@@ -103,7 +110,7 @@ async def check_fees(payload: CheckFeesRequest) -> CheckFeesResponse:
This is can be useful for checking whether an invoice is internal (Cashu-to-Cashu).
"""
fees_msat = await ledger.check_fees(payload.pr)
return CheckFeesResponse(fee=fees_msat / 1000)
return CheckFeesResponse(fee=fees_msat // 1000)
@router.post("/split")
@@ -124,9 +131,9 @@ async def split(
try:
split_return = await ledger.split(proofs, amount, outputs)
except Exception as exc:
return CashuError(error=str(exc))
return CashuError(code=0, error=str(exc))
if not split_return:
return CashuError(error="there was an error with the split")
return CashuError(code=0, error="there was an error with the split")
frst_promises, scnd_promises = split_return
resp = PostSplitResponse(fst=frst_promises, snd=scnd_promises)
return resp

View File

@@ -48,7 +48,7 @@ from cashu.wallet.crud import (
)
from cashu.wallet.wallet import Wallet as Wallet
from .cli_helpers import (
from .clihelpers import (
get_mint_wallet,
print_mint_balances,
proofs_to_token,
@@ -309,6 +309,7 @@ async def send(ctx, amount: int, lock: str, legacy: bool):
@click.option("--lock", "-l", default=None, help="Lock tokens (P2SH).", type=str)
@click.option(
"--legacy",
"-l",
default=False,
is_flag=True,
help="Print legacy token without mint information.",
@@ -387,7 +388,11 @@ async def receive(ctx, token: str, lock: str):
# deserialize token
dtoken = json.loads(base64.urlsafe_b64decode(token))
assert "tokens" in dtoken, Exception("no proofs in token")
# backwards compatibility wallet to wallet < 0.8: V2 tokens renamed "tokens" field to "proofs"
if "tokens" in dtoken:
dtoken["proofs"] = dtoken.pop("tokens")
assert "proofs" in dtoken, Exception("no proofs in token")
includes_mint_info: bool = "mints" in dtoken and dtoken.get("mints") is not None
# if there is a `mints` field in the token
@@ -402,7 +407,7 @@ async def receive(ctx, token: str, lock: str):
await wallet.load_proofs()
else:
# no mint information present, we extract the proofs and use wallet's default mint
proofs = [Proof(**p) for p in dtoken["tokens"]]
proofs = [Proof(**p) for p in dtoken["proofs"]]
_, _ = await wallet.redeem(proofs, script, signature)
wallet.status()
@@ -494,15 +499,23 @@ async def burn(ctx, token: str, all: bool, force: bool):
else:
# check only the specified ones
proofs = [Proof(**p) for p in json.loads(base64.urlsafe_b64decode(token))]
wallet.status()
await wallet.invalidate(proofs)
wallet.status()
@cli.command("pending", help="Show pending tokens.")
@click.option(
"--legacy",
"-l",
default=False,
is_flag=True,
help="Print legacy token without mint information.",
type=bool,
)
@click.pass_context
@coro
async def pending(ctx):
async def pending(ctx, legacy):
wallet: Wallet = ctx.obj["WALLET"]
reserved_proofs = await get_reserved_proofs(wallet.db)
if len(reserved_proofs):
@@ -513,7 +526,7 @@ async def pending(ctx):
):
grouped_proofs = list(value)
token = await wallet.serialize_proofs(grouped_proofs)
token_hidden_secret = await wallet.serialize_proofs(grouped_proofs)
# token_hidden_secret = await wallet.serialize_proofs(grouped_proofs)
reserved_date = datetime.utcfromtimestamp(
int(grouped_proofs[0].time_reserved)
).strftime("%Y-%m-%d %H:%M:%S")
@@ -521,9 +534,15 @@ async def pending(ctx):
f"#{i} Amount: {sum_proofs(grouped_proofs)} sat Time: {reserved_date} ID: {key}\n"
)
print(f"{token}\n")
if legacy:
token_legacy = await wallet.serialize_proofs(
grouped_proofs,
legacy=True,
)
print(f"{token_legacy}\n")
print(f"--------------------------\n")
print("To remove all spent tokens use: cashu burn -a")
wallet.status()
@cli.command("lock", help="Generate receiving lock.")

View File

@@ -3,7 +3,7 @@ import urllib.parse
import click
from cashu.core.base import Proof, TokenJson, TokenMintJson, WalletKeyset
from cashu.core.base import Proof, TokenMintV2, TokenV2, WalletKeyset
from cashu.core.settings import CASHU_DIR, MINT_URL
from cashu.wallet.crud import get_keyset
from cashu.wallet.wallet import Wallet as Wallet
@@ -19,7 +19,7 @@ async def verify_mints(ctx, dtoken):
mint_url, os.path.join(CASHU_DIR, ctx.obj["WALLET_NAME"])
)
# make sure that this mint supports this keyset
mint_keysets = await keyset_wallet._get_keysets(mint_url)
mint_keysets = await keyset_wallet._get_keyset_ids(mint_url)
assert keyset in mint_keysets["keysets"], "mint does not have this keyset."
# we validate the keyset id by fetching the keys from the mint
@@ -63,7 +63,7 @@ async def redeem_multimint(ctx, dtoken, script, signature):
# redeem proofs of this keyset
redeem_proofs = [
Proof(**p) for p in dtoken["tokens"] if Proof(**p).id == keyset
Proof(**p) for p in dtoken["proofs"] if Proof(**p).id == keyset
]
_, _ = await keyset_wallet.redeem(
redeem_proofs, scnd_script=script, scnd_siganture=signature
@@ -119,6 +119,7 @@ async def get_mint_wallet(ctx):
mint_keysets: WalletKeyset = await get_keyset(mint_url=mint_url, db=mint_wallet.db) # type: ignore
# load the keys
assert mint_keysets.id
await mint_wallet.load_mint(keyset_id=mint_keysets.id)
return mint_wallet
@@ -151,7 +152,7 @@ async def proofs_to_token(wallet, proofs, url: str):
Ingests proofs and
"""
# and add url and keyset id to token
token: TokenJson = await wallet._make_token(proofs, include_mints=False)
token: TokenV2 = await wallet._make_token(proofs, include_mints=False)
token.mints = {}
# get keysets of proofs
@@ -161,12 +162,12 @@ async def proofs_to_token(wallet, proofs, url: str):
# check whether we know the mint urls for these proofs
for k in keysets:
ks = await get_keyset(id=k, db=wallet.db)
url = ks.mint_url if ks is not None else None
url = ks.mint_url if ks and ks.mint_url else ""
url = url or (
input(f"Enter mint URL (press enter for default {MINT_URL}): ") or MINT_URL
)
token.mints[url] = TokenMintJson(url=url, ks=keysets) # type: ignore
token.mints[url] = TokenMintV2(url=url, ks=keysets) # type: ignore
token_serialized = await wallet._serialize_token_base64(token)
return token_serialized

View File

@@ -14,17 +14,21 @@ import cashu.core.b_dhke as b_dhke
import cashu.core.bolt11 as bolt11
from cashu.core.base import (
BlindedMessage,
BlindedMessages,
BlindedSignature,
CheckFeesRequest,
CheckRequest,
GetMintResponse,
Invoice,
KeysetsResponse,
MeltRequest,
MintRequest,
P2SHScript,
PostMintResponse,
PostMintResponseLegacy,
Proof,
SplitRequest,
TokenJson,
TokenMintJson,
TokenMintV2,
TokenV2,
WalletKeyset,
)
from cashu.core.bolt11 import Invoice as InvoiceBolt11
@@ -130,6 +134,8 @@ class LedgerAPI:
keyset = await self._get_keys(self.url)
# store current keyset
assert keyset.public_keys
assert keyset.id
assert len(keyset.public_keys) > 0, "did not receive keys from mint."
# check if current keyset is in db
@@ -140,7 +146,7 @@ class LedgerAPI:
# get all active keysets of this mint
mint_keysets = []
try:
keysets_resp = await self._get_keysets(self.url)
keysets_resp = await self._get_keyset_ids(self.url)
mint_keysets = keysets_resp["keysets"]
# store active keysets
except:
@@ -160,7 +166,7 @@ class LedgerAPI:
assert len(amounts) == len(
secrets
), f"len(amounts)={len(amounts)} not equal to len(secrets)={len(secrets)}"
payloads: MintRequest = MintRequest()
payloads: BlindedMessages = BlindedMessages()
rs = []
for secret, amount in zip(secrets, amounts):
B_, r = b_dhke.step1_alice(secret)
@@ -198,7 +204,7 @@ class LedgerAPI:
int(amt): PublicKey(bytes.fromhex(val), raw=True)
for amt, val in keys.items()
}
keyset = WalletKeyset(pubkeys=keyset_keys, mint_url=url)
keyset = WalletKeyset(public_keys=keyset_keys, mint_url=url)
return keyset
async def _get_keyset(self, url: str, keyset_id: str):
@@ -217,18 +223,19 @@ class LedgerAPI:
int(amt): PublicKey(bytes.fromhex(val), raw=True)
for amt, val in keys.items()
}
keyset = WalletKeyset(pubkeys=keyset_keys, mint_url=url)
keyset = WalletKeyset(public_keys=keyset_keys, mint_url=url)
return keyset
async def _get_keysets(self, url: str):
async def _get_keyset_ids(self, url: str):
self.s = self._set_requests()
resp = self.s.get(
url + "/keysets",
)
resp.raise_for_status()
keysets = resp.json()
assert len(keysets), Exception("did not receive any keysets")
return keysets
keysets_dict = resp.json()
keysets = KeysetsResponse.parse_obj(keysets_dict)
assert len(keysets.keysets), Exception("did not receive any keysets")
return keysets.dict()
def request_mint(self, amount):
"""Requests a mint from the server and returns Lightning invoice."""
@@ -237,7 +244,8 @@ class LedgerAPI:
resp.raise_for_status()
return_dict = resp.json()
self.raise_on_error(return_dict)
return Invoice(amount=amount, pr=return_dict["pr"], hash=return_dict["hash"])
mint_response = GetMintResponse.parse_obj(return_dict)
return Invoice(amount=amount, pr=mint_response.pr, hash=mint_response.hash)
async def mint(self, amounts, payment_hash=None):
"""Mints new coins and returns a proof of promise."""
@@ -251,10 +259,17 @@ class LedgerAPI:
params={"payment_hash": payment_hash},
)
resp.raise_for_status()
promises_list = resp.json()
self.raise_on_error(promises_list)
reponse_dict = resp.json()
self.raise_on_error(reponse_dict)
try:
# backwards compatibility: parse promises < 0.8 with no "promises" field
promises = PostMintResponseLegacy.parse_obj(reponse_dict).__root__
logger.warning(
"Parsing token with no promises field. Please upgrade mint to 0.8"
)
except:
promises = PostMintResponse.parse_obj(reponse_dict).promises
promises = [BlindedSignature(**p) for p in promises_list]
return self._construct_proofs(promises, secrets, rs)
async def split(self, proofs, amount, scnd_secret: Optional[str] = None):
@@ -304,7 +319,7 @@ class LedgerAPI:
self.s = self._set_requests()
resp = self.s.post(
self.url + "/split",
json=split_payload.dict(include=_splitrequest_include_fields(proofs)),
json=split_payload.dict(include=_splitrequest_include_fields(proofs)), # type: ignore
)
resp.raise_for_status()
promises_dict = resp.json()
@@ -337,7 +352,7 @@ class LedgerAPI:
self.s = self._set_requests()
resp = self.s.post(
self.url + "/check",
json=payload.dict(include=_check_spendable_include_fields(proofs)),
json=payload.dict(include=_check_spendable_include_fields(proofs)), # type: ignore
)
resp.raise_for_status()
return_dict = resp.json()
@@ -375,7 +390,7 @@ class LedgerAPI:
self.s = self._set_requests()
resp = self.s.post(
self.url + "/melt",
json=payload.dict(include=_meltequest_include_fields(proofs)),
json=payload.dict(include=_meltequest_include_fields(proofs)), # type: ignore
)
resp.raise_for_status()
return_dict = resp.json()
@@ -411,7 +426,9 @@ class Wallet(LedgerAPI):
for id in set([p.id for p in proofs]):
if id is None:
continue
keyset: WalletKeyset = await get_keyset(id=id, db=self.db)
keyset_crud = await get_keyset(id=id, db=self.db)
assert keyset_crud is not None, "keyset not found"
keyset: WalletKeyset = keyset_crud
if keyset.mint_url not in ret:
ret[keyset.mint_url] = [p for p in proofs if p.id == id]
else:
@@ -489,27 +506,27 @@ class Wallet(LedgerAPI):
async def _make_token(self, proofs: List[Proof], include_mints=True):
"""
Takes list of proofs and produces a TokenJson by looking up
Takes list of proofs and produces a TokenV2 by looking up
the keyset id and mint URLs from the database.
"""
# build token
token = TokenJson(tokens=proofs)
token = TokenV2(proofs=proofs)
# add mint information to the token, if requested
if include_mints:
# hold information about the mint
mints: Dict[str, TokenMintJson] = dict()
# dummy object to hold information about the mint
mints: Dict[str, TokenMintV2] = dict()
# iterate through all proofs and add their keyset to `mints`
for proof in proofs:
if proof.id:
# load the keyset from the db
keyset = await get_keyset(id=proof.id, db=self.db)
if keyset and keyset.mint_url:
if keyset and keyset.mint_url and keyset.id:
# TODO: replace this with a mint pubkey
placeholder_mint_id = keyset.mint_url
if placeholder_mint_id not in mints:
# mint information
id = TokenMintJson(
id = TokenMintV2(
url=keyset.mint_url,
ks=[keyset.id],
)
@@ -518,13 +535,15 @@ class Wallet(LedgerAPI):
# if a mint has multiple keysets, append to the existing list
if keyset.id not in mints[placeholder_mint_id].ks:
mints[placeholder_mint_id].ks.append(keyset.id)
if len(mints) > 0:
# add dummy object to token
token.mints = mints
return token
async def _serialize_token_base64(self, token: TokenJson):
async def _serialize_token_base64(self, token: TokenV2):
"""
Takes a TokenJson and serializes it in urlsafe_base64.
Takes a TokenV2 and serializes it in urlsafe_base64.
"""
# encode the token as a base64 string
token_base64 = base64.urlsafe_b64encode(
@@ -588,7 +607,8 @@ class Wallet(LedgerAPI):
Splits proofs such that a Lightning invoice can be paid.
"""
amount, _ = await self.get_pay_amount_with_fees(invoice)
_, send_proofs = await self.split_to_send(self.proofs, amount)
# TODO: fix mypy asyncio return multiple values
_, send_proofs = await self.split_to_send(self.proofs, amount) # type: ignore
return send_proofs
async def split_to_send(

View File

@@ -17,10 +17,10 @@ Mint: `Bob`
- `T` blinded message
- `Z` proof (unblinded signature)
## Blind Diffie-Hellmann key exchange (BDHKE)
# Blind Diffie-Hellmann key exchange (BDHKE)
- Mint `Bob` publishes `K = kG`
- `Alice` picks secret `x` and computes `Y = hash_to_curve(x)`
- Mint `Bob` publishes `K = kG`
- `Alice` picks secret `x` and computes `Y = hash_to_curve(x)`
- `Alice` sends to `Bob`: `T = Y + rG` with `r` being a random nonce
- `Bob` sends back to `Alice` blinded key: `Q = kT` (these two steps are the DH key exchange)
- `Alice` can calculate the unblinded key as `Q - rK = kY + krG - krG = kY = Z`
@@ -31,52 +31,62 @@ Mint: `Bob`
### `BlindedMessage`
A encrypted ("blinded") secret and an amount sent from `Alice` to `Bob`.
An encrypted ("blinded") secret and an amount sent from `Alice` to `Bob` before [minting new tokens][04]
```json
{
"amount": int,
"B_": str
"amount": int,
"B_": str
}
```
`amount` is the value of the requested token and `B_` is the encrypted secret message generated by `Alice`.
### `BlindedSignature`
A signature on the `BlindedMessage` sent from `Bob` to `Alice`.
A signature on the `BlindedMessage` sent from `Bob` to `Alice` after [minting new tokens][04].
```json
{
"amount": int,
"C_": str,
"id": str | None
"amount": int,
"C_": str,
"id": str | None
}
```
`amount` is the value of the blinded token, `C_` is the blinded signature on the secret message `B_` sent in the previous step. `id` is the [keyset id][02] of the mint public keys that signed the token.
### `Proof`
A `Proof` is also called a `Token` and has the following form:
A `Proof` is also called a `Token` in its serialized form. `Alice` sends the serialized to `Carol` to initiate a payment. Upon receiving the token, `Carol` deserializes it and requests a split from `Bob` to exchange it for new `BlindedSignature`'s. `Carol` sends the `Proof` to `Bob` together with new `BlindedMessage`'s that she wants to have signed.
```json
{
"amount": int,
"secret": str,
"C": str,
"id": None | str,
"script": P2SHScript | None,
"amount": int,
"secret": str,
"C": str,
"id": None | str,
"script": P2SHScript | None,
}
```
`amount` is the value of the `Proof`, `secret` is the secret message, `C` is the unblinded signature on `secret`, `id` is the [keyset id][02] of the mint public keys that signed the token. `script` is a `P2SHScript` that specifies the spending condition for this `Proof` [TODO: P2SH documentation].
### `Proofs`
A list of `Proof`'s. In general, this will be used for most operations instead of a single `Proof`. `Proofs` can be serialized (see Methods/Serialization [TODO: Link Serialization])
An array (list) of `Proof`'s. In general, this will be used for most operations instead of a single `Proof`. `Proofs` must be serialized before sending between wallets (see [Serialization](#serialization-of-proofs).
## 0.2 - Methods
### Serialization of `Proofs`
To send and receive `Proofs`, wallets serialize them in a `base64_urlsafe` format.
To send and receive `Proofs`, wallets serialize them in a `base64_urlsafe` format. There are two versions of the serialization format.
Example:
#### 0.2.1 - V1 tokens
This token format is a list of `Proof`s. Each `Proof` contains the keyset id in the field `id` that can be used by a wallet to identify the mint of this token. A wallet that encounters an unknown `id`, it CAN ask the user to enter the mint url of this yet unknown mint. The wallet SHOULD explicity ask the user whether they trust the mint.
##### Example JSON:
```json
[
@@ -95,8 +105,66 @@ Example:
]
```
becomes
When serialized, this becomes:
```
W3siaWQiOiAiRFNBbDludnZ5ZnZhIiwgImFtb3VudCI6IDgsICJzZWNyZXQiOiAiRGJSS0l5YTBldGR3STVzRkFOMEFYUSIsICJDIjogIjAyZGY3ZjJmYzI5NjMxYjcxYTFkYjExYzE2M2IwYjFjYjQwNDQ0YWEyYjNkMjUzZDQzYjY4ZDc3YTcyZWQyZDYyNSJ9LCB7ImlkIjogIkRTQWw5bnZ2eWZ2YSIsICJhbW91bnQiOiAxNiwgInNlY3JldCI6ICJkX1BQYzVLcHVBQjJNNjBXWUFXNS1RIiwgIkMiOiAiMDI3MGUwYTM3ZjdhMGIyMWVhYjQzYWY3NTFkZDNjMDNmNjFmMDRjNjI2YzA0NDhmNjAzZjFkMWY1YWU1YTdkN2U2In1d
```
#### 0.2.2 - V2 tokens
This token format includes information about the mint as well. The field `proofs` is like a V1 token. Additionally, the field `mints` can include a list of multiple mints from which the `proofs` are from. The `url` field is the URL of the mint. `ks` is a list of the keyset IDs belonging to this mint. All keyset IDs of the `proofs` must be present here to allow a wallet to map each proof to a mint.
##### Example JSON:
```json
{
"proofs": [
{
"id": "DSAl9nvvyfva",
"amount": 2,
"secret": "bdYCbHGONundLeYvv1P5dQ",
"C": "02e6117fb1b1633a8c1657ed34ab25ecf8d4974091179c4773ec59f85f4e3991cf"
},
{
"id": "DSAl9nvvyfva",
"amount": 8,
"secret": "KxyUPt5Mur_-RV8pCECJ6A",
"C": "03b9dcdb7f195e07218b95b7c2dadc8289159fc44047439830f765b8c50bfb6bda"
}
],
"mints": {
"MINT_NAME": {
"url": "http://server.host:3339",
"ks": ["DSAl9nvvyfva"]
}
}
}
```
When serialized, this becomes:
```
eyJwcm9vZnMiOlt7ImlkIjoiRFNBbDludnZ5ZnZhIiwiYW1vdW50IjoyLCJzZWNyZXQiOiJiZFlDYkhHT051bmRMZVl2djFQNWRRIiwiQyI6IjAyZTYxMTdmYjFiMTYzM2E4YzE2NTdlZDM0YWIyNWVjZjhkNDk3NDA5MTE3OWM0NzczZWM1OWY4NWY0ZTM5OTFjZiJ9LHsiaWQiOiJEU0FsOW52dnlmdmEiLCJhbW91bnQiOjgsInNlY3JldCI6Ikt4eVVQdDVNdXJfLVJWOHBDRUNKNkEiLCJDIjoiMDNiOWRjZGI3ZjE5NWUwNzIxOGI5NWI3YzJkYWRjODI4OTE1OWZjNDQwNDc0Mzk4MzBmNzY1YjhjNTBiZmI2YmRhIn1dLCJtaW50cyI6eyJNSU5UX05BTUUiOnsidXJsIjoiaHR0cDovL3NlcnZlci5ob3N0OjMzMzkiLCJrcyI6WyJEU0FsOW52dnlmdmEiXX19fQ==
```
[00]: 00.md
[01]: 02.md
[03]: 03.md
[04]: 04.md
[05]: 05.md
[06]: 06.md
[07]: 07.md
[08]: 08.md
[09]: 09.md
[10]: 10.md
[11]: 11.md
[12]: 12.md
[13]: 13.md
[14]: 14.md
[15]: 15.md
[16]: 16.md
[17]: 17.md
[18]: 18.md
[19]: 19.md
[20]: 20.md

View File

@@ -22,8 +22,8 @@ Response of `Bob`:
```json
{
"pr": "lnbc100n1p3kdrv5sp5lpdxzghe5j67q...",
"hash": "67d1d9ea6ada225c115418671b64a..."
"pr": "lnbc100n1p3kdrv5sp5lpdxzghe5j67q...",
"hash": "67d1d9ea6ada225c115418671b64a..."
}
```

View File

@@ -12,15 +12,15 @@ Request of `Alice`:
POST https://mint.host:3338/mint&payment_hash=67d1d9ea6ada225c115418671b64a
```
With the data being of the form `MintRequest`:
With the data being of the form `BlindedMessages`:
```json
{
"blinded_messages":
[
BlindedMessage,
...
]
"blinded_messages":
[
BlindedMessage,
...
]
}
```
@@ -30,18 +30,18 @@ With curl:
```bash
curl -X POST https://mint.host:3338/mint&payment_hash=67d1d9ea6ada225c115418671b64a -d \
{
"blinded_messages":
[
{
"amount": 2,
"B_": "02634a2c2b34bec9e8a4aba4361f6bf202d7fa2365379b0840afe249a7a9d71239"
},
{
"amount": 8,
"B_": "03b54ab451b15005f2c64d38fc512fca695914c8fd5094ee044e5724ad41fda247"
}
]
"blinded_messages":
[
{
"amount": 2,
"B_": "02634a2c2b34bec9e8a4aba4361f6bf202d7fa2365379b0840afe249a7a9d71239"
},
{
"amount": 8,
"B_": "03b54ab451b15005f2c64d38fc512fca695914c8fd5094ee044e5724ad41fda247"
}
]
}
```
@@ -52,18 +52,18 @@ If the invoice was successfully paid, `Bob` responds with a `PostMintResponse` w
```json
{
"promises":
[
{
"id": "DSAl9nvvyfva",
"amount": 2,
"C_": "03e61daa438fc7bcc53f6920ec6c8c357c24094fb04c1fc60e2606df4910b21ffb"
},
{
"id": "DSAl9nvvyfva",
"amount": 8,
"C_": "03fd4ce5a16b65576145949e6f99f445f8249fee17c606b688b504a849cdc452de"
},
]
[
{
"id": "DSAl9nvvyfva",
"amount": 2,
"C_": "03e61daa438fc7bcc53f6920ec6c8c357c24094fb04c1fc60e2606df4910b21ffb"
},
{
"id": "DSAl9nvvyfva",
"amount": 8,
"C_": "03fd4ce5a16b65576145949e6f99f445f8249fee17c606b688b504a849cdc452de"
},
]
}
```
@@ -79,16 +79,16 @@ A list multiple `Proof`'s is called `Proofs` and has the form:
```json
{
"proofs" :
[
{
"id": "DSAl9nvvyfva",
"amount": 2,
"secret": "S+tDfc1Lfsrb06zaRdVTed6Izg",
"C": "0242b0fb43804d8ba9a64ceef249ad7a60f42c15fe6d4907238b05e857527832a3"
},
{
...
}
]
[
{
"id": "DSAl9nvvyfva",
"amount": 2,
"secret": "S+tDfc1Lfsrb06zaRdVTed6Izg",
"C": "0242b0fb43804d8ba9a64ceef249ad7a60f42c15fe6d4907238b05e857527832a3"
},
{
...
}
]
}
```

View File

@@ -16,12 +16,12 @@ With the data being of the form `MeltRequest`:
```json
{
"proofs":
[
Proof,
...
],
"invoice": str
"proofs":
[
Proof,
...
],
"invoice": str
}
```
@@ -32,17 +32,17 @@ With curl:
curl -X POST https://mint.host:3338/mint&payment_hash=67d1d9ea6ada225c115418671b64a -d \
{
"proofs" :
[
{
"id": "DSAl9nvvyfva",
"amount": 2,
"secret": "S+tDfc1Lfsrb06zaRdVTed6Izg",
"C": "0242b0fb43804d8ba9a64ceef249ad7a60f42c15fe6d4907238b05e857527832a3"
},
{
...
}
],
[
{
"id": "DSAl9nvvyfva",
"amount": 2,
"secret": "S+tDfc1Lfsrb06zaRdVTed6Izg",
"C": "0242b0fb43804d8ba9a64ceef249ad7a60f42c15fe6d4907238b05e857527832a3"
},
{
...
}
],
"invoice": "lnbc100n1p3kdrv5sp5lpdxzghe5j67q..."
}
```

View File

@@ -26,9 +26,9 @@ With the data being of the form `SplitRequest`:
```json
{
"proofs": Proofs,
"outputs": MintRequest,
"amount": int
"proofs": Proofs,
"outputs": BlindedMessages,
"amount": int
}
```
@@ -37,31 +37,31 @@ With curl:
```bash
curl -X POST https://mint.host:3338/split -d \
{
"proofs":
[
{
"id": "DSAl9nvvyfva",
"amount": 2,
"secret": "S+tDfc1Lfsrb06zaRdVTed6Izg",
"C": "0242b0fb43804d8ba9a64ceef249ad7a60f42c15fe6d4907238b05e857527832a3"
},
{
...
}
],
"outputs":{
"blinded_messages":
[
{
"amount": 2,
"B_": "02634a2c2b34bec9e8a4aba4361f6bf202d7fa2365379b0840afe249a7a9d71239"
},
{
...
}
]
},
"amount": 40
"proofs":
[
{
"id": "DSAl9nvvyfva",
"amount": 2,
"secret": "S+tDfc1Lfsrb06zaRdVTed6Izg",
"C": "0242b0fb43804d8ba9a64ceef249ad7a60f42c15fe6d4907238b05e857527832a3"
},
{
...
}
],
"outputs":{
"blinded_messages":
[
{
"amount": 2,
"B_": "02634a2c2b34bec9e8a4aba4361f6bf202d7fa2365379b0840afe249a7a9d71239"
},
{
...
}
]
},
"amount": 40
}
```

View File

@@ -52,8 +52,8 @@ Here we see how `Alice` generates `N` blinded messages `T_i`. The following step
- `Alice` remembers `r` for the construction of the proof in Step 5.
### Step 4: Request tokens
- `Alice` constructs JSON `MintRequest = {"blinded_messages" : ["amount" : <amount>, "B_" : <blinded_message>] }` [NOTE: rename "blinded_messages", rename "B_", rename "MintRequest"]
- `Alice` requests tokens via `POST /mint?payment_hash=<payment_hash>` with body `MintRequest` [NOTE: rename MintRequest]
- `Alice` constructs JSON `BlindedMessages = {"blinded_messages" : ["amount" : <amount>, "B_" : <blinded_message>] }` [NOTE: rename "blinded_messages", rename "B_", rename "BlindedMessages"]
- `Alice` requests tokens via `POST /mint?payment_hash=<payment_hash>` with body `BlindedMessages` [NOTE: rename BlindedMessages]
- `Alice` receives from `Bob` a list of blinded signatures `List[BlindedSignature]`, one for each token, e.g. `[{"amount" : <amount>, "C_" : <blinded_signature>}, ...]` [NOTE: rename C_]
- If an error occured, `Alice` receives JSON `{"error" : <error_reason>}}`[*TODO: Specify case of error*]
@@ -121,5 +121,5 @@ Here we describe how `Alice` can request from `Bob` to make a Lightning payment
# Todo:
- Call subsections 1. and 1.2 etc so they can be referenced
- Define objets like `MintRequest` and `SplitRequests` once when they appear and reuse them.
- Define objets like `BlindedMessages` and `SplitRequests` once when they appear and reuse them.
- Clarify whether a `TOKEN` is a single Proof or a list of Proofs

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "cashu"
version = "0.7.1"
version = "0.8"
description = "Ecash wallet and mint."
authors = ["calle <callebtc@protonmail.com>"]
license = "MIT"

View File

@@ -13,11 +13,11 @@ entry_points = {"console_scripts": ["cashu = cashu.wallet.cli:cli"]}
setuptools.setup(
name="cashu",
version="0.7.1",
description="Ecash wallet and mint with Bitcoin Lightning support",
version="0.8",
description="Ecash wallet and mint for Bitcoin Lightning",
long_description=long_description,
long_description_content_type="text/markdown",
url="https://github.com/callebtc/cashu",
url="https://github.com/cashubtc/cashu",
author="Calle",
author_email="calle@protonmail.com",
license="MIT",

View File

@@ -4,7 +4,7 @@ import requests
from cashu.tor.tor import TorProxy
# @pytest.mark.skip
@pytest.mark.skip
def test_tor_setup():
s = requests.Session()

View File

@@ -68,8 +68,8 @@ async def test_get_keyset(wallet1: Wallet):
@pytest.mark.asyncio
async def test_get_keysets(wallet1: Wallet):
keyset = await wallet1._get_keysets(wallet1.url)
async def test_get_keyset_ids(wallet1: Wallet):
keyset = await wallet1._get_keyset_ids(wallet1.url)
assert type(keyset) == dict
assert type(keyset["keysets"]) == list
assert len(keyset["keysets"]) > 0