mirror of
https://github.com/aljazceru/nutshell.git
synced 2026-01-06 10:24:21 +01:00
Fix/nut 04 payment hash (#191)
* payment_hash -> hash * add aes encryption * urlsafe base64 for free * move files to crypto * use random hash instead of encryption * get rid of useless code * simplify
This commit is contained in:
@@ -6,8 +6,8 @@ from typing import Any, Dict, List, Optional, TypedDict, Union
|
||||
from loguru import logger
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ..core.crypto import derive_keys, derive_keyset_id, derive_pubkeys
|
||||
from ..core.secp import PrivateKey, PublicKey
|
||||
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
|
||||
|
||||
# ------- PROOFS -------
|
||||
@@ -94,7 +94,8 @@ class BlindedMessages(BaseModel):
|
||||
class Invoice(BaseModel):
|
||||
amount: int
|
||||
pr: str
|
||||
hash: Union[None, str] = None
|
||||
hash: str
|
||||
payment_hash: Union[None, str] = None
|
||||
preimage: Union[str, None] = None
|
||||
issued: Union[None, bool] = False
|
||||
paid: Union[None, bool] = False
|
||||
|
||||
0
cashu/core/crypto/__init__.py
Normal file
0
cashu/core/crypto/__init__.py
Normal file
@@ -31,6 +31,7 @@ If true, C must have originated from Bob
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
from typing import Optional
|
||||
|
||||
from secp256k1 import PrivateKey, PublicKey
|
||||
|
||||
@@ -41,8 +42,8 @@ def hash_to_curve(message: bytes) -> PublicKey:
|
||||
point = None
|
||||
msg_to_hash = message
|
||||
while point is None:
|
||||
_hash = hashlib.sha256(msg_to_hash).digest()
|
||||
try:
|
||||
_hash = hashlib.sha256(msg_to_hash).digest()
|
||||
point = PublicKey(b"\x02" + _hash, raw=True)
|
||||
except:
|
||||
msg_to_hash = _hash
|
||||
@@ -50,30 +51,30 @@ def hash_to_curve(message: bytes) -> PublicKey:
|
||||
|
||||
|
||||
def step1_alice(
|
||||
secret_msg: str, blinding_factor: bytes = None
|
||||
secret_msg: str, blinding_factor: Optional[bytes] = None
|
||||
) -> tuple[PublicKey, PrivateKey]:
|
||||
Y: PublicKey = hash_to_curve(secret_msg.encode("utf-8"))
|
||||
if blinding_factor:
|
||||
r = PrivateKey(privkey=blinding_factor, raw=True)
|
||||
else:
|
||||
r = PrivateKey()
|
||||
B_: PublicKey = Y + r.pubkey
|
||||
B_: PublicKey = Y + r.pubkey # type: ignore
|
||||
return B_, r
|
||||
|
||||
|
||||
def step2_bob(B_: PublicKey, a: PrivateKey) -> PublicKey:
|
||||
C_: PublicKey = B_.mult(a)
|
||||
C_: PublicKey = B_.mult(a) # type: ignore
|
||||
return C_
|
||||
|
||||
|
||||
def step3_alice(C_: PublicKey, r: PrivateKey, A: PublicKey) -> PublicKey:
|
||||
C: PublicKey = C_ - A.mult(r)
|
||||
C: PublicKey = C_ - A.mult(r) # type: ignore
|
||||
return C
|
||||
|
||||
|
||||
def verify(a: PrivateKey, C: PublicKey, secret_msg: str) -> bool:
|
||||
Y: PublicKey = hash_to_curve(secret_msg.encode("utf-8"))
|
||||
return C == Y.mult(a)
|
||||
return C == Y.mult(a) # type: ignore
|
||||
|
||||
|
||||
### Below is a test of a simple positive and negative case
|
||||
@@ -1,9 +1,10 @@
|
||||
import base64
|
||||
import hashlib
|
||||
import random
|
||||
from typing import Dict, List
|
||||
|
||||
from ..core.secp import PrivateKey, PublicKey
|
||||
from ..core.settings import settings
|
||||
from ..settings import settings
|
||||
from .secp import PrivateKey, PublicKey
|
||||
|
||||
# entropy = bytes([random.getrandbits(8) for i in range(16)])
|
||||
# mnemonic = bip39.mnemonic_from_bytes(entropy)
|
||||
@@ -52,3 +53,10 @@ def derive_keyset_id(keys: Dict[int, PublicKey]):
|
||||
return base64.b64encode(
|
||||
hashlib.sha256((pubkeys_concat).encode("utf-8")).digest()
|
||||
).decode()[:12]
|
||||
|
||||
|
||||
def random_hash() -> str:
|
||||
"""Returns a base64-urlsafe encoded random hash."""
|
||||
return base64.urlsafe_b64encode(
|
||||
bytes([random.getrandbits(8) for i in range(32)])
|
||||
).decode()
|
||||
@@ -154,14 +154,15 @@ async def store_lightning_invoice(
|
||||
await (conn or db).execute(
|
||||
f"""
|
||||
INSERT INTO {table_with_schema(db, 'invoices')}
|
||||
(amount, pr, hash, issued)
|
||||
VALUES (?, ?, ?, ?)
|
||||
(amount, pr, hash, issued, payment_hash)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
invoice.amount,
|
||||
invoice.pr,
|
||||
invoice.hash,
|
||||
invoice.issued,
|
||||
invoice.payment_hash,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -178,6 +179,7 @@ async def get_lightning_invoice(
|
||||
""",
|
||||
(hash,),
|
||||
)
|
||||
|
||||
return Invoice(**row) if row else None
|
||||
|
||||
|
||||
|
||||
@@ -3,9 +3,7 @@ from typing import Dict, List, Literal, Optional, Set, Union
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from ..core import b_dhke as b_dhke
|
||||
from ..core import bolt11 as bolt11
|
||||
from ..core import legacy as legacy
|
||||
from ..core import bolt11, legacy
|
||||
from ..core.base import (
|
||||
BlindedMessage,
|
||||
BlindedSignature,
|
||||
@@ -14,11 +12,12 @@ from ..core.base import (
|
||||
MintKeysets,
|
||||
Proof,
|
||||
)
|
||||
from ..core.crypto import derive_pubkey
|
||||
from ..core.crypto import b_dhke
|
||||
from ..core.crypto.keys import derive_pubkey, random_hash
|
||||
from ..core.crypto.secp import PublicKey
|
||||
from ..core.db import Database
|
||||
from ..core.helpers import fee_reserve, sum_proofs
|
||||
from ..core.script import verify_script
|
||||
from ..core.secp import PublicKey
|
||||
from ..core.settings import settings
|
||||
from ..core.split import amount_split
|
||||
from ..lightning.base import Wallet
|
||||
@@ -79,11 +78,12 @@ class Ledger:
|
||||
return keyset
|
||||
|
||||
async def init_keysets(self, autosave=True):
|
||||
"""Loads all keysets from db.
|
||||
"""Initializes all keysets of the mint from the db.
|
||||
|
||||
Args:
|
||||
autosave (bool, optional): Whether the keyset should be saved if it is
|
||||
not in the database yet. Will be passed to `self.load_keyset`. Defaults to True.
|
||||
autosave (bool, optional): Whether the current keyset should be saved if it is
|
||||
not in the database yet. Will be passed to `self.load_keyset` where it is
|
||||
generated from `self.derivation_path`. Defaults to True.
|
||||
"""
|
||||
# load all past keysets from db
|
||||
tmp_keysets: List[MintKeyset] = await self.crud.get_keyset(db=self.db)
|
||||
@@ -103,6 +103,7 @@ class Ledger:
|
||||
continue
|
||||
logger.debug(f"Generating keys for keyset {v.id}")
|
||||
v.generate_keys(self.master_key)
|
||||
|
||||
# load the current keyset
|
||||
self.keyset = await self.load_keyset(self.derivation_path, autosave)
|
||||
|
||||
@@ -279,14 +280,12 @@ class Ledger:
|
||||
) = await self.lightning.create_invoice(amount, "cashu deposit")
|
||||
return payment_request, checking_id
|
||||
|
||||
async def _check_lightning_invoice(
|
||||
self, amount: int, payment_hash: str
|
||||
) -> Literal[True]:
|
||||
"""Checks with the Lightning backend whether an invoice with this payment_hash was paid.
|
||||
async def _check_lightning_invoice(self, amount: int, hash: str) -> Literal[True]:
|
||||
"""Checks with the Lightning backend whether an invoice stored with `hash` was paid.
|
||||
|
||||
Args:
|
||||
amount (int): Amount of the outputs the wallet wants in return (in Satoshis).
|
||||
payment_hash (str): Payment hash of Lightning invoice (for lookup).
|
||||
hash (str): Hash to look up Lightning invoice by.
|
||||
|
||||
Raises:
|
||||
Exception: Invoice not found.
|
||||
@@ -299,17 +298,16 @@ class Ledger:
|
||||
bool: True if invoice has been paid, else False
|
||||
"""
|
||||
invoice: Union[Invoice, None] = await self.crud.get_lightning_invoice(
|
||||
hash=payment_hash, db=self.db
|
||||
hash=hash, db=self.db
|
||||
)
|
||||
if invoice is None:
|
||||
raise Exception("invoice not found.")
|
||||
if invoice.issued:
|
||||
raise Exception("tokens already issued for this invoice.")
|
||||
assert invoice.payment_hash, "invoice has no payment hash."
|
||||
|
||||
# set this invoice as issued
|
||||
await self.crud.update_lightning_invoice(
|
||||
hash=payment_hash, issued=True, db=self.db
|
||||
)
|
||||
await self.crud.update_lightning_invoice(hash=hash, issued=True, db=self.db)
|
||||
|
||||
try:
|
||||
if amount > invoice.amount:
|
||||
@@ -317,7 +315,7 @@ class Ledger:
|
||||
f"requested amount too high: {amount}. Invoice amount: {invoice.amount}"
|
||||
)
|
||||
|
||||
status = await self.lightning.get_invoice_status(payment_hash)
|
||||
status = await self.lightning.get_invoice_status(invoice.payment_hash)
|
||||
if status.paid:
|
||||
return status.paid
|
||||
else:
|
||||
@@ -325,7 +323,7 @@ class Ledger:
|
||||
except Exception as e:
|
||||
# unset issued
|
||||
await self.crud.update_lightning_invoice(
|
||||
hash=payment_hash, issued=False, db=self.db
|
||||
hash=hash, issued=False, db=self.db
|
||||
)
|
||||
raise e
|
||||
|
||||
@@ -522,31 +520,35 @@ class Ledger:
|
||||
Exception: Invoice creation failed.
|
||||
|
||||
Returns:
|
||||
Tuple[str, str]: Bolt11 invoice and payment hash (for looking it up later)
|
||||
Tuple[str, str]: Bolt11 invoice and a hash (for looking it up later)
|
||||
"""
|
||||
payment_request, checking_id = await self._request_lightning_invoice(amount)
|
||||
assert payment_request, Exception(
|
||||
payment_request, payment_hash = await self._request_lightning_invoice(amount)
|
||||
assert payment_request and payment_hash, Exception(
|
||||
"could not fetch invoice from Lightning backend"
|
||||
)
|
||||
|
||||
invoice = Invoice(
|
||||
amount=amount, pr=payment_request, hash=checking_id, issued=False
|
||||
amount=amount,
|
||||
hash=random_hash(),
|
||||
pr=payment_request,
|
||||
payment_hash=payment_hash, # what we got from the backend
|
||||
issued=False,
|
||||
)
|
||||
if not payment_request or not checking_id:
|
||||
raise Exception(f"Could not create Lightning invoice.")
|
||||
|
||||
await self.crud.store_lightning_invoice(invoice=invoice, db=self.db)
|
||||
return payment_request, checking_id
|
||||
return payment_request, invoice.hash
|
||||
|
||||
async def mint(
|
||||
self,
|
||||
B_s: List[BlindedMessage],
|
||||
payment_hash: Optional[str] = None,
|
||||
hash: Optional[str] = None,
|
||||
keyset: Optional[MintKeyset] = None,
|
||||
):
|
||||
"""Mints a promise for coins for B_.
|
||||
|
||||
Args:
|
||||
B_s (List[BlindedMessage]): Outputs (blinded messages) to sign.
|
||||
payment_hash (Optional[str], optional): Payment hash of (paid) Lightning invoice. Defaults to None.
|
||||
hash (Optional[str], optional): Hash of (paid) Lightning invoice. Defaults to None.
|
||||
keyset (Optional[MintKeyset], optional): Keyset to use. If not provided, uses active keyset. Defaults to None.
|
||||
|
||||
Raises:
|
||||
@@ -561,10 +563,10 @@ class Ledger:
|
||||
amount = sum(amounts)
|
||||
# check if lightning invoice was paid
|
||||
if settings.lightning:
|
||||
if not payment_hash:
|
||||
raise Exception("no payment_hash provided.")
|
||||
if not hash:
|
||||
raise Exception("no hash provided.")
|
||||
try:
|
||||
paid = await self._check_lightning_invoice(amount, payment_hash)
|
||||
paid = await self._check_lightning_invoice(amount, hash)
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
|
||||
@@ -146,3 +146,17 @@ async def m005_pending_proofs_table(db: Database) -> None:
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
async def m006_invoices_add_payment_hash(db: Database):
|
||||
"""
|
||||
Column that remembers the payment_hash as we're using
|
||||
the column hash as a random identifier now
|
||||
(see https://github.com/cashubtc/nuts/pull/14).
|
||||
"""
|
||||
await db.execute(
|
||||
f"ALTER TABLE {table_with_schema(db, 'invoices')} ADD COLUMN payment_hash TEXT"
|
||||
)
|
||||
await db.execute(
|
||||
f"UPDATE {table_with_schema(db, 'invoices')} SET payment_hash = hash"
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Dict, List, Union
|
||||
from typing import Dict, List, Optional, Union
|
||||
|
||||
from fastapi import APIRouter
|
||||
from secp256k1 import PublicKey
|
||||
@@ -99,9 +99,9 @@ async def request_mint(amount: int = 0) -> Union[GetMintResponse, CashuError]:
|
||||
"""
|
||||
if settings.mint_peg_out_only:
|
||||
return CashuError(code=0, error="Mint does not allow minting new tokens.")
|
||||
payment_request, payment_hash = await ledger.request_mint(amount)
|
||||
payment_request, hash = await ledger.request_mint(amount)
|
||||
print(f"Lightning invoice: {payment_request}")
|
||||
resp = GetMintResponse(pr=payment_request, hash=payment_hash)
|
||||
resp = GetMintResponse(pr=payment_request, hash=hash)
|
||||
return resp
|
||||
|
||||
|
||||
@@ -112,7 +112,8 @@ async def request_mint(amount: int = 0) -> Union[GetMintResponse, CashuError]:
|
||||
)
|
||||
async def mint(
|
||||
payload: PostMintRequest,
|
||||
payment_hash: Union[str, None] = None,
|
||||
hash: Optional[str] = None,
|
||||
payment_hash: Optional[str] = None,
|
||||
) -> Union[PostMintResponse, CashuError]:
|
||||
"""
|
||||
Requests the minting of tokens belonging to a paid payment request.
|
||||
@@ -122,7 +123,12 @@ async def mint(
|
||||
if settings.mint_peg_out_only:
|
||||
return CashuError(code=0, error="Mint does not allow minting new tokens.")
|
||||
try:
|
||||
promises = await ledger.mint(payload.outputs, payment_hash=payment_hash)
|
||||
# BEGIN: backwards compatibility < 0.12 where we used to lookup payments with payment_hash
|
||||
# We use the payment_hash to lookup the hash from the database and pass that one along.
|
||||
hash = payment_hash or hash
|
||||
# END: backwards compatibility < 0.12
|
||||
|
||||
promises = await ledger.mint(payload.outputs, hash=hash)
|
||||
blinded_signatures = PostMintResponse(promises=promises)
|
||||
return blinded_signatures
|
||||
except Exception as exc:
|
||||
|
||||
@@ -10,7 +10,6 @@ from typing import Dict, List, Optional
|
||||
import requests
|
||||
from loguru import logger
|
||||
|
||||
from ..core import b_dhke as b_dhke
|
||||
from ..core import bolt11 as bolt11
|
||||
from ..core.base import (
|
||||
BlindedMessage,
|
||||
@@ -36,6 +35,8 @@ from ..core.base import (
|
||||
WalletKeyset,
|
||||
)
|
||||
from ..core.bolt11 import Invoice as InvoiceBolt11
|
||||
from ..core.crypto import b_dhke
|
||||
from ..core.crypto.secp import PrivateKey, PublicKey
|
||||
from ..core.db import Database
|
||||
from ..core.helpers import sum_proofs
|
||||
from ..core.script import (
|
||||
@@ -44,7 +45,6 @@ from ..core.script import (
|
||||
step1_carol_create_p2sh_address,
|
||||
step2_carol_sign_tx,
|
||||
)
|
||||
from ..core.secp import PrivateKey, PublicKey
|
||||
from ..core.settings import settings
|
||||
from ..core.split import amount_split
|
||||
from ..tor.tor import TorProxy
|
||||
@@ -311,7 +311,7 @@ class LedgerAPI:
|
||||
return Invoice(amount=amount, pr=mint_response.pr, hash=mint_response.hash)
|
||||
|
||||
@async_set_requests
|
||||
async def mint(self, amounts, payment_hash=None):
|
||||
async def mint(self, amounts, hash=None):
|
||||
"""Mints new coins and returns a proof of promise."""
|
||||
secrets = [self._generate_secret() for s in range(len(amounts))]
|
||||
await self._check_used_secrets(secrets)
|
||||
@@ -320,7 +320,7 @@ class LedgerAPI:
|
||||
resp = self.s.post(
|
||||
self.url + "/mint",
|
||||
json=outputs_payload.dict(),
|
||||
params={"payment_hash": payment_hash},
|
||||
params={"hash": hash},
|
||||
)
|
||||
resp.raise_for_status()
|
||||
reponse_dict = resp.json()
|
||||
@@ -495,12 +495,12 @@ class Wallet(LedgerAPI):
|
||||
await store_lightning_invoice(db=self.db, invoice=invoice)
|
||||
return invoice
|
||||
|
||||
async def mint(self, amount: int, payment_hash: Optional[str] = None):
|
||||
async def mint(self, amount: int, hash: Optional[str] = None):
|
||||
"""Mint tokens of a specific amount after an invoice has been paid.
|
||||
|
||||
Args:
|
||||
amount (int): Total amount of tokens to be minted
|
||||
payment_hash (Optional[str], optional): Hash of the paid Lightning invoice. Defaults to None (for testing with LIGHTNING=False).
|
||||
hash (Optional[str], optional): Hash for looking up the paid Lightning invoice. Defaults to None (for testing with LIGHTNING=False).
|
||||
|
||||
Raises:
|
||||
Exception: Raises exception if no proofs have been provided
|
||||
@@ -509,25 +509,23 @@ class Wallet(LedgerAPI):
|
||||
List[Proof]: Newly minted proofs.
|
||||
"""
|
||||
split = amount_split(amount)
|
||||
proofs = await super().mint(split, payment_hash)
|
||||
proofs = await super().mint(split, hash)
|
||||
if proofs == []:
|
||||
raise Exception("received no proofs.")
|
||||
await self._store_proofs(proofs)
|
||||
if payment_hash:
|
||||
if hash:
|
||||
await update_lightning_invoice(
|
||||
db=self.db, hash=payment_hash, paid=True, time_paid=int(time.time())
|
||||
db=self.db, hash=hash, paid=True, time_paid=int(time.time())
|
||||
)
|
||||
self.proofs += proofs
|
||||
return proofs
|
||||
|
||||
async def mint_amounts(
|
||||
self, amounts: List[int], payment_hash: Optional[str] = None
|
||||
):
|
||||
async def mint_amounts(self, amounts: List[int], hash: Optional[str] = None):
|
||||
"""Similar to wallet.mint() but accepts a predefined list of amount to be minted.
|
||||
|
||||
Args:
|
||||
amounts (List[int]): List of amounts requested
|
||||
payment_hash (Optional[str], optional): Hash of the paid Lightning invoice. Defaults to None (for testing with LIGHTNING=False).
|
||||
hash (Optional[str], optional): Hash for looking up the paid Lightning invoice. Defaults to None (for testing with LIGHTNING=False).
|
||||
|
||||
Raises:
|
||||
Exception: Newly minted proofs.
|
||||
@@ -540,13 +538,13 @@ class Wallet(LedgerAPI):
|
||||
raise Exception(
|
||||
f"Can only mint amounts with 2^n up to {2**settings.max_order}."
|
||||
)
|
||||
proofs = await super().mint(amounts, payment_hash)
|
||||
proofs = await super().mint(amounts, hash)
|
||||
if proofs == []:
|
||||
raise Exception("received no proofs.")
|
||||
await self._store_proofs(proofs)
|
||||
if payment_hash:
|
||||
if hash:
|
||||
await update_lightning_invoice(
|
||||
db=self.db, hash=payment_hash, paid=True, time_paid=int(time.time())
|
||||
db=self.db, hash=hash, paid=True, time_paid=int(time.time())
|
||||
)
|
||||
self.proofs += proofs
|
||||
return proofs
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
# Notation
|
||||
|
||||
Sending user: `Alice`<br>
|
||||
Receiving user: `Carol`<br>
|
||||
Mint: `Bob`
|
||||
|
||||
## Bob (mint)
|
||||
- `k` private key of mint (one for each supported amount)
|
||||
- `K` public key of mint
|
||||
- `Q` promise (blinded signature)
|
||||
|
||||
## Alice (user)
|
||||
- `x` random string (secret message), corresponds to point `Y` on curve
|
||||
- `r` private key (blinding factor)
|
||||
- `T` blinded message
|
||||
- `Z` proof (unblinded signature)
|
||||
|
||||
# Blind Diffie-Hellman key exchange (BDH)
|
||||
- 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`
|
||||
- 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_curve(x) == Z`, and if so treats it as a valid spend of a token, adding `x` to the list of spent secrets.
|
||||
171
docs/specs/00.md
171
docs/specs/00.md
@@ -1,171 +0,0 @@
|
||||
# NUT-0: Notation and Models
|
||||
|
||||
Sending user: `Alice`
|
||||
Receiving user: `Carol`
|
||||
Mint: `Bob`
|
||||
|
||||
## Bob (mint)
|
||||
|
||||
- `k` private key of mint (one for each amount)
|
||||
- `K` public key of mint
|
||||
- `Q` promise (blinded signature)
|
||||
|
||||
## Alice (user)
|
||||
|
||||
- `x` random string (secret message), corresponds to point `Y` on curve
|
||||
- `r` private key (blinding factor)
|
||||
- `T` blinded message
|
||||
- `Z` proof (unblinded signature)
|
||||
|
||||
# Blind Diffie-Hellmann key exchange (BDHKE)
|
||||
|
||||
- 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 (**blinding**)
|
||||
- `Bob` sends back to `Alice` blinded key: `Q = kT` (these two steps are the DH key exchange) (**signing**)
|
||||
- `Alice` can calculate the unblinded key as `Q - rK = kY + krG - krG = kY = Z` (**unblinding**)
|
||||
- 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_curve(x) == Z` (**verification**), and if so treats it as a valid spend of a token, adding `x` to the list of spent secrets.
|
||||
|
||||
## 0.1 - Models
|
||||
|
||||
### `BlindedMessage`
|
||||
|
||||
An encrypted ("blinded") secret and an amount is sent from `Alice` to `Bob` for [minting tokens][04] or for [splitting tokens][06]. A `BlindedMessage` is also called an `output`.
|
||||
|
||||
```json
|
||||
{
|
||||
"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` is sent from `Bob` to `Alice` after [minting tokens][04] or after [splitting tokens][06]. A `BlindedSignature` is also called a `promise`.
|
||||
|
||||
```json
|
||||
{
|
||||
"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 sent to `Bob` for [melting tokens][05]. A `Proof` can also be sent from `Alice` to `Carol` for which it is first can be [serialized](#serialization-of-proofs). Upon receiving the token, `Carol` deserializes it and requests a [split][06] from `Bob` to receive new tokens.
|
||||
|
||||
```json
|
||||
{
|
||||
"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`
|
||||
|
||||
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 of proofs](#serialization-of-proofs)).
|
||||
|
||||
## 0.2 - Methods
|
||||
|
||||
### Serialization of `Proofs`
|
||||
|
||||
To send and receive `Proofs`, wallets serialize them in a `base64_urlsafe` format. There are two versions of the serialization format.
|
||||
|
||||
#### 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
|
||||
[
|
||||
{
|
||||
"id": "DSAl9nvvyfva",
|
||||
"amount": 8,
|
||||
"secret": "DbRKIya0etdwI5sFAN0AXQ",
|
||||
"C": "02df7f2fc29631b71a1db11c163b0b1cb40444aa2b3d253d43b68d77a72ed2d625"
|
||||
},
|
||||
{
|
||||
"id": "DSAl9nvvyfva",
|
||||
"amount": 16,
|
||||
"secret": "d_PPc5KpuAB2M60WYAW5-Q",
|
||||
"C": "0270e0a37f7a0b21eab43af751dd3c03f61f04c626c0448f603f1d1f5ae5a7d7e6"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
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 an array (list) of multiple mints from which the `proofs` are from. The `url` field is the URL of the mint. `ids` is a list of the keyset IDs belonging to this mint. It is important that 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": "EhpennC9qB3iFlW8FZ_pZw",
|
||||
"C": "02c020067db727d586bc3183aecf97fcb800c3f4cc4759f69c626c9db5d8f5b5d4"
|
||||
},
|
||||
{
|
||||
"id": "DSAl9nvvyfva",
|
||||
"amount": 8,
|
||||
"secret": "TmS6Cv0YT5PU_5ATVKnukw",
|
||||
"C": "02ac910bef28cbe5d7325415d5c263026f15f9b967a079ca9779ab6e5c2db133a7"
|
||||
}
|
||||
],
|
||||
"mints": [
|
||||
{
|
||||
"url": "https://8333.space:3338",
|
||||
"ids": ["DSAl9nvvyfva"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
When serialized, this becomes:
|
||||
|
||||
```
|
||||
eyJwcm9vZnMiOlt7ImlkIjoiRFNBbDludnZ5ZnZhIiwiYW1vdW50IjoyLCJzZWNyZXQiOiJFaHBlbm5DOXFCM2lGbFc4RlpfcFp3IiwiQyI6IjAyYzAyMDA2N2RiNzI3ZDU4NmJjMzE4M2FlY2Y5N2ZjYjgwMGMzZjRjYzQ3NTlmNjljNjI2YzlkYjVkOGY1YjVkNCJ9LHsiaWQiOiJEU0FsOW52dnlmdmEiLCJhbW91bnQiOjgsInNlY3JldCI6IlRtUzZDdjBZVDVQVV81QVRWS251a3ciLCJDIjoiMDJhYzkxMGJlZjI4Y2JlNWQ3MzI1NDE1ZDVjMjYzMDI2ZjE1ZjliOTY3YTA3OWNhOTc3OWFiNmU1YzJkYjEzM2E3In1dLCJtaW50cyI6W3sidXJsIjoiaHR0cHM6Ly84MzMzLnNwYWNlOjMzMzgiLCJpZHMiOlsiRFNBbDludnZ5ZnZhIl19XX0=
|
||||
```
|
||||
|
||||
[00]: 00.md
|
||||
[01]: 01.md
|
||||
[02]: 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
|
||||
@@ -1,61 +0,0 @@
|
||||
# NUT-1: Mint public key exchange
|
||||
|
||||
This describes the basic exchange of the public mint keys that the wallet user `Alice` uses to unblind `Bob`'s signature.
|
||||
|
||||
## Description
|
||||
|
||||
Wallet user `Alice` receives public keys from mint `Bob` via `GET /keys` and stores them in a key-value store like a dictionary. The set of all public keys for each supported amount is called a *keyset*.
|
||||
|
||||
Mint `Bob` responds with his *active* [keyset][02]. The active keyset is the keyset a mint currently uses to sign promises with. The active keyset can change over time, for example due to key rotation. A mint MAY support older keysets indefinetely. Note that a mint can support multiple keysets at the same time but will only respond with the active keyset on the endpoint `GET /keys`. A wallet can ask for the keys of a specific (non-active) keyset by using the endpint `GET /keys/{keyset_id}` (see #2 [TODO: Link #2]).
|
||||
|
||||
See [TODO: Link #2] for how a wallet deals with multiple keysets.
|
||||
|
||||
Keysets are received as a JSON of the form `{<amount_1> : <mint_pubkey_1>, <amount_2> : ...}` for each `<amount_i>` of the amounts the mint `Bob` supports and the corresponding public key `<mint_pubkey_1>`, that is `K_i` (see #0 [TODO: Link #0]).
|
||||
|
||||
## Example
|
||||
|
||||
Request of `Alice`:
|
||||
|
||||
```http
|
||||
GET https://mint.host:3338/keys
|
||||
```
|
||||
|
||||
With curl:
|
||||
|
||||
```bash
|
||||
curl -X GET https://mint.host:3338/keys
|
||||
```
|
||||
|
||||
Response of `Bob`:
|
||||
|
||||
```json
|
||||
{
|
||||
"1": "03a40f20667ed53513075dc51e715ff2046cad64eb68960632269ba7f0210e38bc",
|
||||
"2": "03fd4ce5a16b65576145949e6f99f445f8249fee17c606b688b504a849cdc452de",
|
||||
"4": "02648eccfa4c026960966276fa5a4cae46ce0fd432211a4f449bf84f13aa5f8303",
|
||||
"8": "02fdfd6796bfeac490cbee12f778f867f0a2c68f6508d17c649759ea0dc3547528",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
[00]: 00.md
|
||||
[01]: 01.md
|
||||
[02]: 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
|
||||
100
docs/specs/02.md
100
docs/specs/02.md
@@ -1,100 +0,0 @@
|
||||
# NUT-2: Keysets and keyset ID
|
||||
|
||||
A keyset is a set of public keys that the mint `Bob` generates and shares with its users. It refers to the set of public keys that each correspond to the amount values that the mint supports (e.g. 1, 2, 4, 8, ...) respectively.
|
||||
|
||||
## Requesting mint keyset IDs
|
||||
|
||||
A mint can have multiple keysets at the same time but **MUST** have only one *active* keyset (see #1 [TODO: Link #1]). A wallet can ask the mint for all active keyset IDs via the `GET /keysets` endpoint. A wallet **CAN** request the list of active keyset IDs from the mint upon startup and, if it does so, **MUST** choose only tokens from its database that have a keyset ID supported by the mint to interact with it.
|
||||
|
||||
This is useful in the case a wallet interacts with multiple mints. That way, a wallet always knows which tokens it can use with the mint it is currently interacting with.
|
||||
|
||||
## Example
|
||||
|
||||
Request of `Alice`:
|
||||
|
||||
```http
|
||||
GET https://mint.host:3338/keysets
|
||||
```
|
||||
|
||||
With curl:
|
||||
|
||||
```bash
|
||||
curl -X GET https://mint.host:3338/keysets
|
||||
```
|
||||
|
||||
Response of `Bob`:
|
||||
|
||||
```json
|
||||
{
|
||||
"keysets": [
|
||||
"DSAl9nvvyfva",
|
||||
...
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 2.1 - Generating a keyset
|
||||
|
||||
A keyset is generated by the mint by a single seed `s` from which the private keys `k_i` are derived, with `i` being the index of the amount value. The derivation for the elements `k_i` goes like this:
|
||||
|
||||
```python
|
||||
for i in range(MAX_ORDER):
|
||||
k_i = HASH_SHA256(s + D + i)[:32]
|
||||
```
|
||||
|
||||
Here, `MAX_ORDER` refers to the order of the maximum token value that the mint supports, i.e., `2^MAX_ORDER`. Typically, `MAX_ORDER = 64`. `D` refers to a derivation path that is chosen by the mint. The derivation path can be used to rotate keys over time or to service multiple parallel mints with a single instance. `i` is the string representation of the index of the amount value, i.e., `0`, `1`, and so on.
|
||||
|
||||
## 2.2 - Keyset ID
|
||||
|
||||
A keyset ID is an identifier for a specific keyset. It can be derived by anyone who knows the set of public keys of a mint. The keyset ID **CAN** be stored in a Cashu token [TODO: Link to definition of token] such that the token can be used to identify which mint or keyset it was generated from.
|
||||
|
||||
### 2.2.1 - Storing the keyset ID in a token
|
||||
|
||||
A wallet can use the keyset ID in a token to recognize a mint it was issued by. For example, a wallet might store the `MINT_URL` together with the `keyset_id` in its database the first time it receives a keyset from that mint. That way, a wallet can know which mint to contact when it receives a token with a `keyset_id` of a mint that it has interacted with before.
|
||||
|
||||
[TODO: V2 tokens include the `MINT_URL` to enable the first contact when a wallet recieves a token from a mint it has never met before.]
|
||||
|
||||
### 2.2.2 - Deriving the keyset ID
|
||||
|
||||
The mint and the wallets of its users can derive a keyset ID from the keyset of the mint. To derive the keyset ID of a keyset, execute the following steps:
|
||||
|
||||
```
|
||||
1 - sort keyset by amount
|
||||
2 - concatenate all (sorted) public keys to one string
|
||||
3 - HASH_SHA256 the concatenated public keys
|
||||
4 - take the first 12 characters of the hash
|
||||
```
|
||||
|
||||
An example implementation in Python:
|
||||
|
||||
```python
|
||||
def derive_keyset_id(keys: Dict[int, PublicKey]):
|
||||
"""Deterministic derivation keyset_id from set of public keys."""
|
||||
sorted_keys = dict(sorted(keys.items()))
|
||||
pubkeys_concat = "".join([p.serialize().hex() for _, p in sorted_keys.items()])
|
||||
return base64.b64encode(
|
||||
hashlib.sha256((pubkeys_concat).encode("utf-8")).digest()
|
||||
).decode()[:12]
|
||||
```
|
||||
|
||||
[00]: 00.md
|
||||
[01]: 01.md
|
||||
[02]: 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
|
||||
@@ -1,52 +0,0 @@
|
||||
# NUT-3: Request mint
|
||||
|
||||
Minting tokens is a two-step process: requesting a mint and minting the tokens. Here, we describe the first step. A wallet requests the minting of tokens in exchange for paying a bolt11 Lightning invoice (typically generated by the mint to add funds to its reserves, and typically paid with another Lightning wallet).
|
||||
|
||||
To request the minting of tokens, a wallet `Alice` sends a `GET /mint&amount=<amount_sat>` request with the requested amount `<amount_sat>` in satoshis. The mint `Bob` then responds with a Lightning invoice.
|
||||
|
||||
## Example
|
||||
|
||||
Request of `Alice`:
|
||||
|
||||
```http
|
||||
GET https://mint.host:3338/mint&amount=1000
|
||||
```
|
||||
|
||||
With curl:
|
||||
|
||||
```bash
|
||||
curl -X GET https://mint.host:3338/mint&amount=1000
|
||||
```
|
||||
|
||||
Response of `Bob`:
|
||||
|
||||
```json
|
||||
{
|
||||
"pr": "lnbc100n1p3kdrv5sp5lpdxzghe5j67q...",
|
||||
"hash": "67d1d9ea6ada225c115418671b64a..."
|
||||
}
|
||||
```
|
||||
|
||||
with `pr` being the bolt11 payment request and `hash` the hash of the invoice. A wallet **MUST** store the `hash` and `amount_sat` in its database to later request the tokens upon paying the invoice. A wallet **SHOULD** then present the payment request (for example via QR code) to the user such that they can pay the invoice with another Lightning wallet. After the user has paid the invoice, a wallet **MUST** continue with #4 - Minting tokens [TODO: Link to #4].
|
||||
|
||||
[00]: 00.md
|
||||
[01]: 01.md
|
||||
[02]: 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
|
||||
111
docs/specs/04.md
111
docs/specs/04.md
@@ -1,111 +0,0 @@
|
||||
# NUT-4: Mint tokens
|
||||
|
||||
After requesting a mint (see #3 [TODO: Link]) and paying the invoice that was returned by the mint, a wallet proceeds with requesting tokens from the mint in return for paying the invoice.
|
||||
|
||||
For that, a wallet sends a `POST /mint&payment_hash=<hash>` request with a JSON body to the mint. The body **MUST** include `BlindedMessages` that are worth a maximum of `<amount_sat>` [TODO: Refer to BlindedMessages]. If successful (i.e. the invoice has been previously paid and the `BlindedMessages` are valid), the mint responds with `Promises` [TODO: Link Promises].
|
||||
|
||||
## Example
|
||||
|
||||
Request of `Alice`:
|
||||
|
||||
```http
|
||||
POST https://mint.host:3338/mint&payment_hash=67d1d9ea6ada225c115418671b64a
|
||||
```
|
||||
|
||||
With the json payload data being of the form `PostMintRequest`:
|
||||
|
||||
```json
|
||||
{
|
||||
"outputs": `BlindedMessages`
|
||||
}
|
||||
```
|
||||
|
||||
`BlindedMessages` is a list (array) of `BlindedMessage`s (see [NUT-0][00]).
|
||||
|
||||
With curl:
|
||||
|
||||
```bash
|
||||
curl -X POST https://mint.host:3338/mint&payment_hash=67d1d9ea6ada225c115418671b64a -d \
|
||||
{
|
||||
"outputs":
|
||||
[
|
||||
{
|
||||
"amount": 2,
|
||||
"B_": "02634a2c2b34bec9e8a4aba4361f6bf202d7fa2365379b0840afe249a7a9d71239"
|
||||
},
|
||||
{
|
||||
"amount": 8,
|
||||
"B_": "03b54ab451b15005f2c64d38fc512fca695914c8fd5094ee044e5724ad41fda247"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Response of `Bob`:
|
||||
|
||||
If the invoice was successfully paid, `Bob` responds with a `PostMintResponse` which is a list of `BlindedSignature`'s (see [NUT-0][00]).
|
||||
|
||||
```json
|
||||
{
|
||||
"promises":
|
||||
[
|
||||
{
|
||||
"id": "DSAl9nvvyfva",
|
||||
"amount": 2,
|
||||
"C_": "03e61daa438fc7bcc53f6920ec6c8c357c24094fb04c1fc60e2606df4910b21ffb"
|
||||
},
|
||||
{
|
||||
"id": "DSAl9nvvyfva",
|
||||
"amount": 8,
|
||||
"C_": "03fd4ce5a16b65576145949e6f99f445f8249fee17c606b688b504a849cdc452de"
|
||||
},
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
If the invoice was not paid yet, `Bob` responds with an error. In that case, `Alice` **CAN** repeat the same response until the Lightning invoice is settled.
|
||||
|
||||
## Unblinding signatures
|
||||
|
||||
Upon receiving the `PostMintResponse` with the list of `BlindedSignature`'s from the mint `Bob`, a wallet `Alice` **MUST** then unblind the `BlindedSignature`'s from `Bob` (see BDHKE [NUT-0][00]) to generate a list of `Proof`'s. A `Proof` is effectively an ecash `Token` and can later be used to redeem the token. The wallet **MUST** store the `Proof` in its database.
|
||||
|
||||
A list multiple `Proof`'s is called `Proofs` and has the form:
|
||||
|
||||
```json
|
||||
{
|
||||
"proofs" :
|
||||
[
|
||||
{
|
||||
"id": "DSAl9nvvyfva",
|
||||
"amount": 2,
|
||||
"secret": "S+tDfc1Lfsrb06zaRdVTed6Izg",
|
||||
"C": "0242b0fb43804d8ba9a64ceef249ad7a60f42c15fe6d4907238b05e857527832a3"
|
||||
},
|
||||
{
|
||||
...
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
[00]: 00.md
|
||||
[01]: 01.md
|
||||
[02]: 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
|
||||
@@ -1,81 +0,0 @@
|
||||
# NUT-5: Melting tokens
|
||||
|
||||
Melting tokens is the opposite of minting them (see #4): the wallet `Alice` sends `Proofs` to the mint `Bob` together with a bolt11 Lightning invoice that `Alice` wants to be paid. To melt tokens, `Alice` sends a `POST /melt` request with a JSON body to the mint. The `Proofs` included in the request will be burned by the mint and the mint will pay the invoice in exchange.
|
||||
|
||||
`Alice`'s request **MUST** include a `PostMeltRequest` ([TODO: Link PostMeltRequest]) JSON body with `Proofs` that have at least the amount of the invoice to be paid.
|
||||
|
||||
## Example
|
||||
|
||||
**Request** of `Alice`:
|
||||
|
||||
```http
|
||||
POST https://mint.host:3338/melt
|
||||
```
|
||||
|
||||
With the data being of the form `PostMeltRequest`:
|
||||
|
||||
```json
|
||||
{
|
||||
"proofs":
|
||||
[
|
||||
Proof,
|
||||
...
|
||||
],
|
||||
"pr": str
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
With curl:
|
||||
|
||||
```bash
|
||||
curl -X POST https://mint.host:3338/mint&payment_hash=67d1d9ea6ada225c115418671b64a -d \
|
||||
{
|
||||
"proofs" :
|
||||
[
|
||||
{
|
||||
"id": "DSAl9nvvyfva",
|
||||
"amount": 2,
|
||||
"secret": "S+tDfc1Lfsrb06zaRdVTed6Izg",
|
||||
"C": "0242b0fb43804d8ba9a64ceef249ad7a60f42c15fe6d4907238b05e857527832a3"
|
||||
},
|
||||
{
|
||||
...
|
||||
}
|
||||
],
|
||||
"pr": "lnbc100n1p3kdrv5sp5lpdxzghe5j67q..."
|
||||
}
|
||||
```
|
||||
|
||||
**Response** `PostMeltResponse` from `Bob`:
|
||||
|
||||
```json
|
||||
{
|
||||
"paid": true,
|
||||
"preimage": "da225c115418671b64a67d1d9ea6a..."
|
||||
}
|
||||
```
|
||||
|
||||
Only if the `paid==true`, the wallet `Alice` **MUST** delete the `Proofs` from her database (or move them to a history). If `paid==false`, `Alice` **CAN** repeat the same multiple times until the payment is successful.
|
||||
|
||||
[00]: 00.md
|
||||
[01]: 01.md
|
||||
[02]: 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
|
||||
@@ -1,98 +0,0 @@
|
||||
# NUT-6: Split tokens
|
||||
|
||||
The split operation is the most important component of the Cashu system. The wallet `Alice` can use it to redeem tokens (i.e. receive new ones in return) that she received from `Carol`, or she can split her own tokens to a target amount she needs to send to `Carol`, if she does not have the necessary amounts to compose the target amount in her wallet already.
|
||||
|
||||
The basic idea is that `Alice` sends `Bob` a set of `Proof`'s and a set of `BlindedMessage`'s with an equal amount. Additionally, she specifies the `amount` at which she would like to have the split.
|
||||
|
||||
## 6.1 - Split to send
|
||||
|
||||
To make this more clear, we make an example of a typical case of sending tokens from `Alice` to `Carol`:
|
||||
|
||||
`Alice` has 64 satoshis in her wallet, composed of two tokens, one worth 32 sats and another two worth 16 sats. She wants to send `Carol` 40 sats but does not have the necessary tokens to combine them to reach the exact target amount of 40 sats. `Alice` requests a split from the mint. For that, she sends the mint `Bob` her tokens (`Proofs`) worth `[32, 16, 16]` and asks for a split at amount 40. The mint will then return her new tokens with the amounts `[32, 8, 16, 8]`. Notice that the first two tokens can now be combined to 40 sats. The original tokens that `Alice` sent to `Bob` are now invalidated.
|
||||
|
||||
## 6.2 - Split to receive
|
||||
|
||||
Another case of how split can be useful becomes apparent if we follow up the example above where `Alice` split her tokens ready to be sent to `Carol`. `Carol` can receive these tokens, which means to invalidate the tokens she receives and redeem them for new ones, using the same mechanism. Only if `Carol` redeems them for new tokens that only she can spend, `Alice` can't double-spend them anymore and this simple transaction can be considered settled. `Carol` requests a split of the tokens (`Proofs`) worth `[32, 8]` at the amount `40` (the total amount) to receive back new tokens with the same total amount.
|
||||
|
||||
## Example
|
||||
|
||||
**Request** of `Alice`:
|
||||
|
||||
```http
|
||||
POST https://mint.host:3338/split
|
||||
```
|
||||
|
||||
With the data being of the form `PostSplitRequest`:
|
||||
|
||||
```json
|
||||
{
|
||||
"proofs": Proofs,
|
||||
"outputs": BlindedMessages,
|
||||
"amount": int
|
||||
}
|
||||
```
|
||||
|
||||
`BlindedMessages` is a list (array) of `BlindedMessage`s (see [NUT-0][00]).
|
||||
|
||||
With curl:
|
||||
|
||||
```bash
|
||||
curl -X POST https://mint.host:3338/split -d \
|
||||
{
|
||||
"proofs":
|
||||
[
|
||||
{
|
||||
"id": "DSAl9nvvyfva",
|
||||
"amount": 2,
|
||||
"secret": "S+tDfc1Lfsrb06zaRdVTed6Izg",
|
||||
"C": "0242b0fb43804d8ba9a64ceef249ad7a60f42c15fe6d4907238b05e857527832a3"
|
||||
},
|
||||
{
|
||||
...
|
||||
}
|
||||
],
|
||||
"outputs":
|
||||
[
|
||||
{
|
||||
"amount": 2,
|
||||
"B_": "02634a2c2b34bec9e8a4aba4361f6bf202d7fa2365379b0840afe249a7a9d71239"
|
||||
},
|
||||
{
|
||||
...
|
||||
}
|
||||
],
|
||||
"amount": 40
|
||||
}
|
||||
```
|
||||
|
||||
If successful, `Bob` will respond with a `PostSplitResponse`
|
||||
|
||||
```python
|
||||
class PostSplitResponse(BaseModel):
|
||||
fst: BlindedSignatures
|
||||
snd: BlindedSignatures
|
||||
```
|
||||
|
||||
`BlindedSignatures` is a list (array) of `BlindedSignature`s (see [NUT-0][00]).
|
||||
|
||||
[00]: 00.md
|
||||
[01]: 01.md
|
||||
[02]: 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
|
||||
@@ -1,36 +0,0 @@
|
||||
# Cashu NUTs (Notation, Usage, and Terminology)
|
||||
|
||||
|
||||
| Number | Description | Wallets |
|
||||
|----------|-------------------------------------------------------------|---------|
|
||||
| [00][00] | Notation and Models | Python-CLI, Feni, LNbits
|
||||
| [01][01] | Mint public keys | Python-CLI, Feni, LNbits
|
||||
| [02][02] | Keysets and keyset IDs | Python-CLI, Feni, LNbits
|
||||
| [03][03] | Requesting a mint | Python-CLI, Feni, LNbits
|
||||
| [04][04] | Mint tokens | Python-CLI, Feni, LNbits
|
||||
| [05][05] | Melt tokens | Python-CLI, Feni, LNbits
|
||||
| [06][06] | Split tokens | Python-CLI, Feni, LNbits
|
||||
|
||||
|
||||
|
||||
[00]: 00.md
|
||||
[01]: 01.md
|
||||
[02]: 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
|
||||
@@ -1,125 +0,0 @@
|
||||
# Notation
|
||||
|
||||
Sending user: `Alice`
|
||||
Receivung user: `Carol`
|
||||
Mint: `Bob`
|
||||
|
||||
## Bob (mint)
|
||||
- `k` private key of mint (one for each amount)
|
||||
- `K` public key of mint
|
||||
- `Q` promise (blinded signature)
|
||||
|
||||
## Alice (user)
|
||||
- `x` random string (secret message), corresponds to point `Y` on curve
|
||||
- `r` private key (blinding factor)
|
||||
- `T` blinded message
|
||||
- `Z` proof (unblinded signature)
|
||||
|
||||
# Blind Diffie-Hellmann key exchange (BDH)
|
||||
- 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`
|
||||
- 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_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
|
||||
|
||||
## 1 - Request public keys from mint
|
||||
|
||||
`Alice` receives public keys from mint `Bob` via `GET /keys` and stores them in a key-value store like a dictionary. Keys are received as a JSON of the form `{<amount_1> : <mint_pubkey_1>, <amount_2> : ...}` for each `<amount_i>` of the amounts the mint `Bob` supports. [NOTE: `mint_pubkey` should be consistent with the notation above.]
|
||||
|
||||
## 2 - Mint tokens
|
||||
|
||||
### Step 1: `Alice` requests mint
|
||||
- `Alice` requests the minting of tokens of value `amount : int` via `GET /mint?amount=<amount>`
|
||||
- `Bob` responds with a JSON `{"pr": <payment_request>, "hash": <payment_hash>}` where `payment_request` is the bolt11 Lightning invoice that `Alice` needs to pay and `payment_hash` is the hash of the invoice necessary for alice to request minting of tokens later. `Alice` stores `payment_hash`. [NOTE: <payment_hash> does not need to be passed by Bob, can be derived from <payment_request>]
|
||||
- `Alice` pays bolt11 invoice `payment_request` using a Bitcoin Lightning wallet.
|
||||
|
||||
### Step 2: Request tokens
|
||||
- To request tokens of value `amount : int`, `Alice` decomposes `amount` into a sum of values of `2^n`, e.g. `13` is `amounts : List[int] = [1, 4, 8]`. This can be easily done by representing `amount` as binary and using each binary digit that is `1` as part of the sum, e.g. `13` would be `1101` wich is `2^0 + 2^2 + 2^3`. In this example, `Alice` will request `N = len(amounts) = 3` tokens.
|
||||
- `Alice` generates a random secret string `x_i` of `128` random bits with `i \in [0,..,N-1]`for each of the `N` requested tokens and encodes them in `base64`. [*TODO: remove index i*]
|
||||
- `Alice` remembers `x` for the construction of the proof in Step 5.
|
||||
|
||||
### Step 3: Generate blinded message
|
||||
Here we see how `Alice` generates `N` blinded messages `T_i`. The following steps are executed for each of the `N` tokens that `Alice` requests. The index `i` is dropped for simplicity. [*TODO: either write everything independent of i or not, don't mix*]
|
||||
- `Alice` generates a point `Y` on the elliptic curve from the secret `x` using the deterministic function `Y = hash_to_curve(hash(x : string)) : Point`.
|
||||
- `h = hash(x : string) : string` can be the `SHA256` hash function.
|
||||
- `Y = hash_to_curve(h : string) : Point` verifies that `Y` is an element of the elliptic curve.
|
||||
- `Alice` generates a random nonce `r : int` that is a private key and computes the public key from it using `r*G`.
|
||||
- `Alice` generates the blinded message `T = Y + r*G`
|
||||
- `Alice` remembers `r` for the construction of the proof in Step 5.
|
||||
|
||||
### Step 4: Request tokens
|
||||
- `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*]
|
||||
|
||||
### Step 5: Construct proofs
|
||||
Here, `Alice` construct proofs for each token using the tuple `(blinded_signature, r, s)`. Again, all steps are repeated for each token separately but we show it here for only one token.
|
||||
- `Alice` unblinds `blinded_signature` by subtracting `r*<mint_pubkey>` from it. Note that `<mint_pubkey>` must be according to the `<amount>` of the token. The result is the proof `Z`. [Note: in notation, this is Z = Q - r*K]
|
||||
- `Alice` constructs spendable token as a tuple `(<amount>, Z, s)` and stores it in her database.
|
||||
|
||||
## 3 - Send tokens
|
||||
Here we describe how `Alice` sends tokens to `Carol`.
|
||||
|
||||
### 3.1 – Split tokens to desired amount
|
||||
`Alice` wants to send tokens of total value `<total>` to `Carol` but doesn't necessarily have a set of tokens that sum to `<total>`. Say `Alice` has tokens of the amount `<alice_balance>` which is greater than `<total>` in here database. Note that `<alice_balance>` does not need to include all of `Alice`'s tokens but only at least tokens of a total amount of `<total>`. Therefore, `Alice` sends tokens of amount `<alice_balance>` to `Bob` asks `Bob` to issue two new sets of tokens of value `<total>` and `<alice_balance>-<total>` each.
|
||||
- `Alice` performs a split on the amounts `<total>` and `<alice_balance>-<total>` separately as in 2.2 - Request tokens. [*TODO: fix reference*]
|
||||
- `Alice` constructs two new sets of blinded messages like in 2.3 - Generate blind messages [*TODO: fix reference*], one for each of the two amounts `<total>` and `<alice_balance>-<total>`.
|
||||
- `Alice` concatenates both sets of blinded messages into the list `<blinded_messages>` [*TODO: list?*]
|
||||
- `Alice` constructs a JSON out of multiple tokens from her database that sum to `<alice_balance>` of the form `{"amount" : <total>, "proofs" : [{"amount" : <amount>, "secret" : s, "C" : Z}, ...], "outputs" : ["amount" : <amount>, "B_" : <blinded_message>]}`. The blinded messages in `"outputs"` are the list of concatenated blinded message from the previous step. [*TODO: refer to this as BlindMessages or something and reuse in Section 4 and 2*]
|
||||
|
||||
### 3.2 - Request new tokens for sending
|
||||
- `Alice` constructs a JSON out of multiple tokens of the form `[{"amount" : <amount>, "secret" : s, "C" : Z}, ...]` and serializes is as a Base64 string `TOKEN` which is then sent to `Carol` as a payment of value `sum(<amount_i>)`. [*NOTE: rename C, rewrite sum, find consistency in writing labels, values, TOKEN, in code this is called `Proof`*]
|
||||
- `Alice` requests new tokens via `POST /mint` with the JSON as the body of the request.
|
||||
- `Alice` receives a JSON of the form `{"fst" : <signatures_to_keep>}, "snd" : <signatures_to_send>` with both entries being of the type `List[BlindedSignature]`. `Alice` constructs proofs `<keep_proofs>` and `<send_proofs>` from both of these entries like in Step 2.5 [TODO: fix reference].
|
||||
- `Alice` stores the proofs `<keep_proofs>` and `<send_proofs>` in her database and flags `<send_proofs>` as `pending` (for example in a separate column).
|
||||
- `Alice` may also give the set of `<send_proofs>` a unique ID `send_id` so that she can later connect each set of pending tokens with every send attempt.
|
||||
|
||||
### 3.3 - Serialize tokens for sending
|
||||
Here, `Alice` serializes the proofs from the set `<send_proofs>` for sending to `Carol`.
|
||||
- `Alice` constructs a JSON of the form `[{"amount" : <amount>, "secret" : s, "C" : Z}, ...]` from `<send_proofs>` and encodes it as a Base64 string using url-safe Base64 encoder. [*NOTE: it probably doesn't need to be url-safe, maybe it shouldn't if this is not widespread or consistent across languages*]
|
||||
- `Alice` sends the resulting `TOKEN` as the string `W3siYW1vdW50IjogMiwgInNlY3...` to `Carol`.
|
||||
|
||||
## 4 - Receive new tokens
|
||||
Here we describe how `Carol` can redeem new tokens from `Bob` that she previously received from `Alice`. `Carol` receives tokens as a url-safe [*NOTE: remove url-safe?*] base64-encoded string `TOKEN` that, when decoded, is a JSON of the form `[{"amount" : <amount>, "secret" : s, "C" : Z}, ...]`. In the following, we will refer to the tuple `(<amount>, Z, s)` as a single token. [*NOTE: clarify whether a TOKEN is a single token or a list of tokens*] To redeem a token, `Carol` sends it to `Bob` and receives a one of the same value.
|
||||
|
||||
`Carol` essentially performs the same procedure to receive tokens as `Alice` did earlier when she prepared her tokens for sending: She sends constructs new blinded messages and sends them together with the tokens she received in order to receive a newly-issued set of tokens which settles the transaction between `Alice` and `Carol`.
|
||||
|
||||
Note that the following steps can also be performed by `Alice` herself if she wants to cancel the pending token transfer and claim them for herself.
|
||||
|
||||
- `Carol` constructs a list of `<blinded_message>`'s each with the same amount as the list list of tokens that she received. This can be done by the same procedure as during the minting of new tokens in Section 2 [*TODO: update ref*] or during sending in Section 3 [*TODO: update ref*] since the splitting into amounts is deterministic.
|
||||
- `Carol` performs the same steps as `Alice` when she split the tokens before sending it to her and calls the endpoint `POIT /split` with the JSON `PostSplitRequests` as the body of the request.
|
||||
|
||||
## 5 - Burn sent tokens
|
||||
Here we describe how `Alice` checks with the mint whether the tokens she sent `Carol` have been redeemed so she can safely delete them from her database. This step is optional but highly recommended so `Alice` can properly account for the tokens and adjust her balance accordingly.
|
||||
- `Alice` loads all `<send_proofs>` with `pending=True` from her database and might group them by the `send_id`.
|
||||
- `Alice` constructs a JSON of the form `{"proofs" : [{"amount" : <amount>, "secret" : s, "C" : Z}, ...]}` from these (grouped) tokens. [*TODO: this object is called CheckSpendableRequest*]
|
||||
- `Alice` sends them to the mint `Bob` via the endpoint `GET /check` with the JSON as the body of the request.
|
||||
- `Alice` receives a JSON of the form `{"1" : <spendable : bool>, "2" : ...}` where `"1"` is the index of the proof she sent to the mint before and `<spendable>` is a boolean that is `True` if the token has not been claimed yet by `Carol` and `False` if it has already been claimed.
|
||||
- If `<spendable>` is `False`, `Alice` removes the proof [*NOTE: consistent name?*] from her list of spendable proofs.
|
||||
|
||||
## 6 - Pay a Lightning invoice
|
||||
Here we describe how `Alice` can request from `Bob` to make a Lightning payment for her and burn an appropriate amount of tokens in return. `Alice` wants to pay a bolt11 invoice with the amount `<invoice_amount>`. She has to add a fee to the request to account for the possible Lightning fees which results in a request with tokens with the total amount of `<total>`.
|
||||
|
||||
- `Alice` wants to pay the bolt11 invoice `<invoice>`.
|
||||
- `Alice` asks `Bob` for the Lightning fee via `GET /checkfee` with the body `CheckFeeRequest` being the json `{pr : <invoice>}`
|
||||
- `Alice` receives the `CheckFeeResponse` in the form of the json `{"fee" : <fee>}` resulting in `<total> = <invoice_amount> + <fee>`.
|
||||
- `Alice` now performs the same set of instructions as in Step 3.1 and 3.2 and splits her spendable tokens into a set `<keep_proofs>` that she keeps and and a set `<send_proofs>` with a sum of at least `<total>` that she can send for making the Lightning payment.
|
||||
- `Alice` constructs the JSON `PostMeltRequest` of the form `{"proofs" : <List[Proof]>, "pr" : <invoice>}` [*NOTE: Maybe use notation List[Proof] everywhere. Used PostMeltRequest here, maybe define each payload at the beginning of each section.*]
|
||||
- `Alice` requests a payment from `Bob` via the endpoint `POST /melt` with the JSON as the body of the request.
|
||||
- `Alice` receives a JSON of the form `{"paid" : <status:bool>}` with `<status>` being `True` if the payment was successful and `False` otherwise.
|
||||
- If `<status> == True`, `Alice` removes `<send_proofs>` from her database of spendable tokens [*NOTE: called it tokens again*]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# Todo:
|
||||
- Call subsections 1. and 1.2 etc so they can be referenced
|
||||
- Define objets like `BlindedMessages` and `PostSplitRequests` once when they appear and reuse them.
|
||||
- Clarify whether a `TOKEN` is a single Proof or a list of Proofs
|
||||
@@ -1,7 +1,7 @@
|
||||
import pytest
|
||||
|
||||
from cashu.core.b_dhke import hash_to_curve, step1_alice, step2_bob, step3_alice
|
||||
from cashu.core.secp import PrivateKey, PublicKey
|
||||
from cashu.core.crypto.b_dhke import hash_to_curve, step1_alice, step2_bob, step3_alice
|
||||
from cashu.core.crypto.secp import PrivateKey, PublicKey
|
||||
|
||||
|
||||
def test_hash_to_curve():
|
||||
|
||||
Reference in New Issue
Block a user