diff --git a/cashu/core/b_dhke.py b/cashu/core/b_dhke.py index be9a141..5e9d2e5 100644 --- a/cashu/core/b_dhke.py +++ b/cashu/core/b_dhke.py @@ -6,7 +6,7 @@ Alice: A = a*G return A Bob: -Y = hash_to_point(secret_message) +Y = hash_to_curve(secret_message) r = random blinding factor B'= Y + r*G return B' @@ -20,7 +20,7 @@ C = C' - r*A (= a*Y) return C, secret_message Alice: -Y = hash_to_point(secret_message) +Y = hash_to_curve(secret_message) C == a*Y If true, C must have originated from Alice """ @@ -30,28 +30,22 @@ import hashlib from secp256k1 import PrivateKey, PublicKey -def hash_to_point(secret_msg): - """Generates x coordinate from the message hash and checks if the point lies on the curve. - If it does not, it tries computing again a new x coordinate from the hash of the coordinate.""" +def hash_to_curve(message: bytes): + """Generates a point from the message hash and checks if the point lies on the curve. + If it does not, it tries computing a new point from the hash.""" point = None - msg = secret_msg + msg_to_hash = message while point is None: - _hash = hashlib.sha256(msg).hexdigest().encode("utf-8") try: - # We construct compressed pub which has x coordinate encoded with even y - _hash = list(_hash[:33]) # take the 33 bytes and get a list of bytes - _hash[0] = 0x02 # set first byte to represent even y coord - _hash = bytes(_hash) - point = PublicKey(_hash, raw=True) + _hash = hashlib.sha256(msg_to_hash).digest() + point = PublicKey(b"\x02" + _hash, raw=True) except: - msg = _hash - + msg_to_hash = _hash return point -def step1_alice(secret_msg): - secret_msg = secret_msg.encode("utf-8") - Y = hash_to_point(secret_msg) +def step1_alice(secret_msg: str): + Y = hash_to_curve(secret_msg.encode("utf-8")) r = PrivateKey() B_ = Y + r.pubkey return B_, r @@ -68,7 +62,7 @@ def step3_alice(C_, r, A): def verify(a, C, secret_msg): - Y = hash_to_point(secret_msg.encode("utf-8")) + Y = hash_to_curve(secret_msg.encode("utf-8")) return C == Y.mult(a) diff --git a/cashu/core/base.py b/cashu/core/base.py index b76b6c4..189335b 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -247,7 +247,7 @@ class MintKeyset: first_seen=None, active=None, seed: Union[None, str] = None, - derivation_path: str = "0", + derivation_path: str = None, ): self.derivation_path = derivation_path self.id = id diff --git a/cashu/core/legacy.py b/cashu/core/legacy.py new file mode 100644 index 0000000..6abd9c1 --- /dev/null +++ b/cashu/core/legacy.py @@ -0,0 +1,27 @@ +import hashlib + +from secp256k1 import PublicKey + + +def hash_to_point_pre_0_3_3(secret_msg): + """Generates x coordinate from the message hash and checks if the point lies on the curve. + If it does not, it tries computing again a new x coordinate from the hash of the coordinate.""" + point = None + msg = secret_msg + while point is None: + _hash = hashlib.sha256(msg).hexdigest().encode("utf-8") + try: + # We construct compressed pub which has x coordinate encoded with even y + _hash = list(_hash[:33]) # take the 33 bytes and get a list of bytes + _hash[0] = 0x02 # set first byte to represent even y coord + _hash = bytes(_hash) + point = PublicKey(_hash, raw=True) + except: + msg = _hash + + return point + + +def verify_pre_0_3_3(a, C, secret_msg): + Y = hash_to_point_pre_0_3_3(secret_msg.encode("utf-8")) + return C == Y.mult(a) diff --git a/cashu/core/settings.py b/cashu/core/settings.py index 3df4eac..30f2270 100644 --- a/cashu/core/settings.py +++ b/cashu/core/settings.py @@ -48,4 +48,4 @@ LNBITS_ENDPOINT = env.str("LNBITS_ENDPOINT", default=None) LNBITS_KEY = env.str("LNBITS_KEY", default=None) MAX_ORDER = 64 -VERSION = "0.3.2" +VERSION = "0.3.3" diff --git a/cashu/mint/__init__.py b/cashu/mint/__init__.py index cfe9d4c..50d1a38 100644 --- a/cashu/mint/__init__.py +++ b/cashu/mint/__init__.py @@ -1,4 +1,4 @@ from cashu.core.settings import MINT_PRIVATE_KEY from cashu.mint.ledger import Ledger -ledger = Ledger(MINT_PRIVATE_KEY, "data/mint") +ledger = Ledger(MINT_PRIVATE_KEY, "data/mint", derivation_path="0/0/0/0") diff --git a/cashu/mint/app.py b/cashu/mint/app.py index f0a6b73..002d03e 100644 --- a/cashu/mint/app.py +++ b/cashu/mint/app.py @@ -1,19 +1,30 @@ -import asyncio import logging import sys from fastapi import FastAPI from loguru import logger +from starlette.middleware import Middleware +from starlette.middleware.base import BaseHTTPMiddleware +from starlette_context import context +from starlette_context.middleware import RawContextMiddleware from cashu.core.settings import DEBUG, VERSION -from cashu.lightning import WALLET -from cashu.mint.migrations import m001_initial -from . import ledger from .router import router from .startup import load_ledger +class CustomHeaderMiddleware(BaseHTTPMiddleware): + """ + Middleware for starlette that can set the context from request headers + """ + + async def dispatch(self, request, call_next): + context["client-version"] = request.headers.get("Client-version") + response = await call_next(request) + return response + + def create_app(config_object="core.settings") -> FastAPI: def configure_logger() -> None: class Formatter: @@ -49,6 +60,13 @@ def create_app(config_object="core.settings") -> FastAPI: configure_logger() + middleware = [ + Middleware( + RawContextMiddleware, + ), + Middleware(CustomHeaderMiddleware), + ] + app = FastAPI( title="Cashu Mint", description="Ecash wallet and mint with Bitcoin Lightning support.", @@ -57,8 +75,8 @@ def create_app(config_object="core.settings") -> FastAPI: "name": "MIT License", "url": "https://raw.githubusercontent.com/callebtc/cashu/main/LICENSE", }, + middleware=middleware, ) - return app diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py index 73126c5..2e2f0bb 100644 --- a/cashu/mint/ledger.py +++ b/cashu/mint/ledger.py @@ -6,9 +6,11 @@ import math from typing import Dict, List, Set from loguru import logger +from starlette_context import context import cashu.core.b_dhke as b_dhke import cashu.core.bolt11 as bolt11 +import cashu.core.legacy as legacy from cashu.core.base import ( BlindedMessage, BlindedSignature, @@ -55,11 +57,12 @@ class Ledger: seed=self.master_key, derivation_path=self.derivation_path ) # check if current keyset is stored in db and store if not + logger.debug(f"Loading keyset {self.keyset.id} from db.") current_keyset_local: List[MintKeyset] = await get_keyset( id=self.keyset.id, db=self.db ) if not len(current_keyset_local): - logger.debug(f"Storing keyset {self.keyset.id}") + logger.debug(f"Storing keyset {self.keyset.id}.") await store_keyset(keyset=self.keyset, db=self.db) # load all past keysets from db @@ -112,6 +115,11 @@ class Ledger: secret_key = self.keysets.keysets[proof.id].private_keys[proof.amount] C = PublicKey(bytes.fromhex(proof.C), raw=True) + + # backwards compatibility with old hash_to_curve + # old clients do not send a version + if not context.get("client-version"): + return legacy.verify_pre_0_3_3(secret_key, C, proof.secret) return b_dhke.verify(secret_key, C, proof.secret) def _verify_script(self, idx: int, proof: Proof): diff --git a/cashu/mint/router.py b/cashu/mint/router.py index 260d8bd..db64ea0 100644 --- a/cashu/mint/router.py +++ b/cashu/mint/router.py @@ -20,6 +20,10 @@ from cashu.mint import ledger router: APIRouter = APIRouter() +from starlette.requests import Request +from starlette_context import context + + @router.get("/keys") def keys(): """Get the public keys of the mint""" @@ -49,7 +53,6 @@ async def request_mint(amount: int = 0): @router.post("/mint") async def mint( payloads: MintRequest, - bolt11: Union[str, None] = None, payment_hash: Union[str, None] = None, ): """ @@ -70,7 +73,7 @@ async def mint( @router.post("/melt") -async def melt(payload: MeltRequest): +async def melt(request: Request, payload: MeltRequest): """ Requests tokens to be destroyed and sent out via Lightning. """ @@ -97,7 +100,7 @@ async def check_fees(payload: CheckFeesRequest): @router.post("/split") -async def split(payload: SplitRequest): +async def split(request: Request, payload: SplitRequest): """ Requetst a set of tokens with amount "total" to be split into two newly minted sets with amount "split" and "total-split". diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index 1ba984e..8c95034 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -30,7 +30,7 @@ from cashu.core.script import ( step2_carol_sign_tx, ) from cashu.core.secp import PublicKey -from cashu.core.settings import DEBUG +from cashu.core.settings import DEBUG, VERSION from cashu.core.split import amount_split from cashu.wallet.crud import ( get_keyset, @@ -52,7 +52,10 @@ class LedgerAPI: self.url = url async def _get_keys(self, url): - resp = requests.get(url + "/keys").json() + resp = requests.get( + url + "/keys", + headers={"Client-version": VERSION}, + ).json() keys = resp assert len(keys), Exception("did not receive any keys") keyset_keys = { @@ -63,7 +66,10 @@ class LedgerAPI: return keyset async def _get_keysets(self, url): - keysets = requests.get(url + "/keysets").json() + keysets = requests.get( + url + "/keysets", + headers={"Client-version": VERSION}, + ).json() assert len(keysets), Exception("did not receive any keysets") return keysets @@ -177,6 +183,7 @@ class LedgerAPI: self.url + "/mint", json=payloads.dict(), params={"payment_hash": payment_hash}, + headers={"Client-version": VERSION}, ) resp.raise_for_status() try: @@ -235,6 +242,7 @@ class LedgerAPI: resp = requests.post( self.url + "/split", json=split_payload.dict(include=_splitrequest_include_fields(proofs)), + headers={"Client-version": VERSION}, ) resp.raise_for_status() try: @@ -260,6 +268,7 @@ class LedgerAPI: resp = requests.post( self.url + "/check", json=payload.dict(), + headers={"Client-version": VERSION}, ) resp.raise_for_status() return_dict = resp.json() @@ -272,6 +281,7 @@ class LedgerAPI: resp = requests.post( self.url + "/checkfees", json=payload.dict(), + headers={"Client-version": VERSION}, ) resp.raise_for_status() @@ -293,6 +303,7 @@ class LedgerAPI: resp = requests.post( self.url + "/melt", json=payload.dict(include=_meltequest_include_fields(proofs)), + headers={"Client-version": VERSION}, ) resp.raise_for_status() diff --git a/docs/README.md b/docs/README.md index 6e3f5b3..851d5d4 100644 --- a/docs/README.md +++ b/docs/README.md @@ -17,9 +17,9 @@ Mint: `Bob` # Blind Diffie-Hellmann key exchange (BDH) - Mint `Bob` publishes `K = kG` -- `Alice` picks secret `x` and computes `Y = hash_to_point(x)` +- `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` - Alice can take the pair `(x, Z)` as a token and can send it to `Carol`. -- `Carol` can send `(x, Z)` to `Bob` who then checks that `k*hash_to_point(x) == Z`, and if so treats it as a valid spend of a token, adding `x` to the list of spent secrets. \ No newline at end of file +- `Carol` can send `(x, Z)` to `Bob` who then checks that `k*hash_to_curve(x) == Z`, and if so treats it as a valid spend of a token, adding `x` to the list of spent secrets. \ No newline at end of file diff --git a/docs/specs/cashu_client_spec.md b/docs/specs/cashu_client_spec.md index d449804..bf537b2 100644 --- a/docs/specs/cashu_client_spec.md +++ b/docs/specs/cashu_client_spec.md @@ -17,12 +17,12 @@ Mint: `Bob` # Blind Diffie-Hellmann key exchange (BDH) - Mint `Bob` publishes `K = kG` -- `Alice` picks secret `x` and computes `Y = hash_to_point(x)` +- `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` - Alice can take the pair `(x, Z)` as a token and can send it to `Carol`. -- `Carol` can send `(x, Z)` to `Bob` who then checks that `k*hash_to_point(x) == Z`, and if so treats it as a valid spend of a token, adding `x` to the list of spent secrets. +- `Carol` can send `(x, Z)` to `Bob` who then checks that `k*hash_to_curve(x) == Z`, and if so treats it as a valid spend of a token, adding `x` to the list of spent secrets. # Cashu client protocol diff --git a/poetry.lock b/poetry.lock index 7d8c34d..c7eb74e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -167,8 +167,8 @@ starlette = "0.19.1" [package.extras] all = ["email_validator (>=1.1.1,<2.0.0)", "itsdangerous (>=1.1.0,<3.0.0)", "jinja2 (>=2.11.2,<4.0.0)", "orjson (>=3.2.1,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "pyyaml (>=5.3.1,<7.0.0)", "requests (>=2.24.0,<3.0.0)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)", "uvicorn[standard] (>=0.12.0,<0.18.0)"] dev = ["autoflake (>=1.4.0,<2.0.0)", "flake8 (>=3.8.3,<6.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "pre-commit (>=2.17.0,<3.0.0)", "python-jose[cryptography] (>=3.3.0,<4.0.0)", "uvicorn[standard] (>=0.12.0,<0.18.0)"] -doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.2)", "mkdocs-material (>=8.1.4,<9.0.0)", "pyyaml (>=5.3.1,<7.0.0)", "typer (>=0.4.1,<0.5.0)"] -test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==22.3.0)", "databases[sqlite] (>=0.3.2,<0.6.0)", "email_validator (>=1.1.1,<2.0.0)", "flake8 (>=3.8.3,<6.0.0)", "flask (>=1.1.2,<3.0.0)", "httpx (>=0.14.0,<0.19.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.910)", "orjson (>=3.2.1,<4.0.0)", "peewee (>=3.13.3,<4.0.0)", "pytest (>=6.2.4,<7.0.0)", "pytest-cov (>=2.12.0,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "requests (>=2.24.0,<3.0.0)", "sqlalchemy (>=1.3.18,<1.5.0)", "types-dataclasses (==0.6.5)", "types-orjson (==3.6.2)", "types-ujson (==4.2.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)"] +doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pyyaml (>=5.3.1,<7.0.0)", "typer (>=0.4.1,<0.5.0)"] +test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==22.3.0)", "databases[sqlite] (>=0.3.3,<0.6.0)", "email_validator (>=1.1.1,<2.0.0)", "flake8 (>=3.8.3,<6.0.0)", "flask (>=1.1.2,<3.0.0)", "httpx (>=0.14.0,<0.19.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.910)", "orjson (>=3.2.1,<4.0.0)", "peewee (>=3.13.3,<4.0.0)", "pytest (>=6.2.4,<7.0.0)", "pytest-cov (>=2.12.0,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "requests (>=2.24.0,<3.0.0)", "sqlalchemy (>=1.3.18,<1.5.0)", "types-dataclasses (==0.6.5)", "types-orjson (==3.6.2)", "types-ujson (==4.2.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)"] [[package]] name = "h11" @@ -553,6 +553,17 @@ typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\"" [package.extras] full = ["itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests"] +[[package]] +name = "starlette-context" +version = "0.3.4" +description = "Access context in Starlette" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +starlette = "*" + [[package]] name = "tomli" version = "2.0.1" @@ -632,7 +643,7 @@ testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>= [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "b4e980ee90226bab07750b1becc8c69df7752f6d168d200a79c782aa1efe61da" +content-hash = "14ff9c57ca971c645f1a075b5c6fa0a84a38eaf6399d14afa724136728a3da03" [metadata.files] anyio = [ @@ -999,6 +1010,10 @@ starlette = [ {file = "starlette-0.19.1-py3-none-any.whl", hash = "sha256:5a60c5c2d051f3a8eb546136aa0c9399773a689595e099e0877704d5888279bf"}, {file = "starlette-0.19.1.tar.gz", hash = "sha256:c6d21096774ecb9639acad41b86b7706e52ba3bf1dc13ea4ed9ad593d47e24c7"}, ] +starlette-context = [ + {file = "starlette_context-0.3.4-py37-none-any.whl", hash = "sha256:b16bf17bd3ead7ded2f458aebf7f913744b9cf28305e16c69b435a6c6ddf1135"}, + {file = "starlette_context-0.3.4.tar.gz", hash = "sha256:2d28e1838302fb5d5adacadc10fb73fb2d5cca1f0aa1e279698701cc96f1567c"}, +] tomli = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, diff --git a/pyproject.toml b/pyproject.toml index 8eef93e..b8622c3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "cashu" -version = "0.3.2" +version = "0.3.3" description = "Ecash wallet and mint." authors = ["calle "] license = "MIT" @@ -22,6 +22,7 @@ bitstring = "^3.1.9" secp256k1 = "^0.14.0" sqlalchemy-aio = "^0.17.0" python-bitcoinlib = "^0.11.2" +starlette-context = "^0.3.4" [tool.poetry.dev-dependencies] black = {version = "^22.8.0", allow-prereleases = true} diff --git a/setup.py b/setup.py index e944e11..ac45661 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ entry_points = {"console_scripts": ["cashu = cashu.wallet.cli:cli"]} setuptools.setup( name="cashu", - version="0.3.2", + version="0.3.3", description="Ecash wallet and mint with Bitcoin Lightning support", long_description=long_description, long_description_content_type="text/markdown",