mirror of
https://github.com/aljazceru/nutshell.git
synced 2025-12-23 19:54:18 +01:00
determinstic secrets for multiple tokens
This commit is contained in:
@@ -150,7 +150,7 @@ class Ledger:
|
|||||||
"""Checks with the Lightning backend whether an invoice with this payment_hash was paid."""
|
"""Checks with the Lightning backend whether an invoice with this payment_hash was paid."""
|
||||||
invoice: Invoice = await get_lightning_invoice(payment_hash, db=self.db)
|
invoice: Invoice = await get_lightning_invoice(payment_hash, db=self.db)
|
||||||
if invoice.issued:
|
if invoice.issued:
|
||||||
raise Exception("tokens already issued for this invoice")
|
raise Exception("tokens already issued for this invoice.")
|
||||||
status = await WALLET.get_invoice_status(payment_hash)
|
status = await WALLET.get_invoice_status(payment_hash)
|
||||||
if status.paid:
|
if status.paid:
|
||||||
await update_lightning_invoice(payment_hash, issued=True, db=self.db)
|
await update_lightning_invoice(payment_hash, issued=True, db=self.db)
|
||||||
@@ -239,7 +239,7 @@ class Ledger:
|
|||||||
raise Exception("split amount is higher than the total sum.")
|
raise Exception("split amount is higher than the total sum.")
|
||||||
# verify that only unique proofs and outputs were used
|
# verify that only unique proofs and outputs were used
|
||||||
if not self._verify_no_duplicates(proofs, outputs):
|
if not self._verify_no_duplicates(proofs, outputs):
|
||||||
raise Exception("duplicate proofs or promises")
|
raise Exception("duplicate proofs or promises.")
|
||||||
# verify that outputs have the correct amount
|
# verify that outputs have the correct amount
|
||||||
if not self._verify_outputs(total, amount, outputs):
|
if not self._verify_outputs(total, amount, outputs):
|
||||||
raise Exception("split of promises is not as expected.")
|
raise Exception("split of promises is not as expected.")
|
||||||
|
|||||||
@@ -100,19 +100,19 @@ async def balance(ctx):
|
|||||||
|
|
||||||
@cli.command("send", help="Send tokens.")
|
@cli.command("send", help="Send tokens.")
|
||||||
@click.argument("amount", type=int)
|
@click.argument("amount", type=int)
|
||||||
@click.option("--secret", "-s", default="", help="Token spending condition.", type=str)
|
@click.option(
|
||||||
|
"--secret", "-s", default=None, help="Token spending condition.", type=str
|
||||||
|
)
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
@coro
|
@coro
|
||||||
async def send(ctx, amount: int, secret: str):
|
async def send(ctx, amount: int, secret: str):
|
||||||
wallet: Wallet = ctx.obj["WALLET"]
|
wallet: Wallet = ctx.obj["WALLET"]
|
||||||
wallet.load_mint()
|
wallet.load_mint()
|
||||||
wallet.status()
|
wallet.status()
|
||||||
# TODO: remove this list hack
|
_, send_proofs = await wallet.split_to_send(wallet.proofs, amount, secret)
|
||||||
secrets = [secret] if secret else None
|
|
||||||
_, send_proofs = await wallet.split_to_send(wallet.proofs, amount, secrets)
|
|
||||||
await wallet.set_reserved(send_proofs, reserved=True)
|
await wallet.set_reserved(send_proofs, reserved=True)
|
||||||
token = await wallet.serialize_proofs(
|
token = await wallet.serialize_proofs(
|
||||||
send_proofs, hide_secrets=True if secrets else False
|
send_proofs, hide_secrets=True if secret else False
|
||||||
)
|
)
|
||||||
print(token)
|
print(token)
|
||||||
wallet.status()
|
wallet.status()
|
||||||
@@ -127,10 +127,8 @@ async def receive(ctx, token: str, secret: str):
|
|||||||
wallet: Wallet = ctx.obj["WALLET"]
|
wallet: Wallet = ctx.obj["WALLET"]
|
||||||
wallet.load_mint()
|
wallet.load_mint()
|
||||||
wallet.status()
|
wallet.status()
|
||||||
# TODO: remove this list hack
|
|
||||||
secrets = [secret] if secret else None
|
|
||||||
proofs = [Proof.from_dict(p) for p in json.loads(base64.urlsafe_b64decode(token))]
|
proofs = [Proof.from_dict(p) for p in json.loads(base64.urlsafe_b64decode(token))]
|
||||||
_, _ = await wallet.redeem(proofs, secrets)
|
_, _ = await wallet.redeem(proofs, secret)
|
||||||
wallet.status()
|
wallet.status()
|
||||||
|
|
||||||
|
|
||||||
@@ -175,17 +173,22 @@ async def pending(ctx):
|
|||||||
reserved_proofs = await get_reserved_proofs(wallet.db)
|
reserved_proofs = await get_reserved_proofs(wallet.db)
|
||||||
if len(reserved_proofs):
|
if len(reserved_proofs):
|
||||||
sorted_proofs = sorted(reserved_proofs, key=itemgetter("send_id"))
|
sorted_proofs = sorted(reserved_proofs, key=itemgetter("send_id"))
|
||||||
for key, value in groupby(sorted_proofs, key=itemgetter("send_id")):
|
grouped_proofs = groupby(sorted_proofs, key=itemgetter("send_id"))
|
||||||
|
for i, (key, value) in enumerate(grouped_proofs):
|
||||||
grouped_proofs = list(value)
|
grouped_proofs = list(value)
|
||||||
token = await wallet.serialize_proofs(grouped_proofs)
|
token = await wallet.serialize_proofs(grouped_proofs)
|
||||||
|
token_hidden_secret = await wallet.serialize_proofs(
|
||||||
|
grouped_proofs, hide_secrets=True
|
||||||
|
)
|
||||||
reserved_date = datetime.utcfromtimestamp(
|
reserved_date = datetime.utcfromtimestamp(
|
||||||
int(grouped_proofs[0].time_reserved)
|
int(grouped_proofs[0].time_reserved)
|
||||||
).strftime("%Y-%m-%d %H:%M:%S")
|
).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
print(
|
print(
|
||||||
f"Amount: {sum([p['amount'] for p in grouped_proofs])} sat Sent: {reserved_date} ID: {key}\n"
|
f"Amount: {sum([p['amount'] for p in grouped_proofs])} sat Sent: {reserved_date} ID: {key} #{i+1}/{len(grouped_proofs)}\n"
|
||||||
)
|
)
|
||||||
print(token)
|
print(f"With secret: {token}\n\nSecretless: {token_hidden_secret}\n")
|
||||||
print("")
|
if i < len(grouped_proofs) - 1:
|
||||||
|
print(f"--------------------------\n")
|
||||||
wallet.status()
|
wallet.status()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -97,3 +97,19 @@ async def update_proof_reserved(
|
|||||||
f"UPDATE proofs SET {', '.join(clauses)} WHERE secret = ?",
|
f"UPDATE proofs SET {', '.join(clauses)} WHERE secret = ?",
|
||||||
(*values, str(proof.secret)),
|
(*values, str(proof.secret)),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def secret_used(
|
||||||
|
secret: str,
|
||||||
|
db: Database,
|
||||||
|
conn: Optional[Connection] = None,
|
||||||
|
):
|
||||||
|
|
||||||
|
rows = await (conn or db).fetchone(
|
||||||
|
"""
|
||||||
|
SELECT * from proofs
|
||||||
|
WHERE secret = ?
|
||||||
|
""",
|
||||||
|
(secret),
|
||||||
|
)
|
||||||
|
return rows is not None
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ from cashu.wallet.crud import (
|
|||||||
invalidate_proof,
|
invalidate_proof,
|
||||||
store_proof,
|
store_proof,
|
||||||
update_proof_reserved,
|
update_proof_reserved,
|
||||||
|
secret_used,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -97,9 +98,15 @@ class LedgerAPI:
|
|||||||
payloads.blinded_messages.append(payload)
|
payloads.blinded_messages.append(payload)
|
||||||
return payloads, rs
|
return payloads, rs
|
||||||
|
|
||||||
def mint(self, amounts, payment_hash=None):
|
async def _check_used_secrets(self, secrets):
|
||||||
|
for s in secrets:
|
||||||
|
if await secret_used(s, db=self.db):
|
||||||
|
raise Exception(f"secret already used: {s}")
|
||||||
|
|
||||||
|
async def mint(self, amounts, payment_hash=None):
|
||||||
"""Mints new coins and returns a proof of promise."""
|
"""Mints new coins and returns a proof of promise."""
|
||||||
secrets = [self._generate_secret() for s in range(len(amounts))]
|
secrets = [self._generate_secret() for s in range(len(amounts))]
|
||||||
|
await self._check_used_secrets(secrets)
|
||||||
payloads, rs = self._construct_outputs(amounts, secrets)
|
payloads, rs = self._construct_outputs(amounts, secrets)
|
||||||
|
|
||||||
resp = requests.post(
|
resp = requests.post(
|
||||||
@@ -120,12 +127,12 @@ class LedgerAPI:
|
|||||||
promises = [BlindedSignature.from_dict(p) for p in promises_list]
|
promises = [BlindedSignature.from_dict(p) for p in promises_list]
|
||||||
return self._construct_proofs(promises, secrets, rs)
|
return self._construct_proofs(promises, secrets, rs)
|
||||||
|
|
||||||
def split(self, proofs, amount, snd_secrets: List[str] = None):
|
async def split(self, proofs, amount, snd_secret: str = None):
|
||||||
"""Consume proofs and create new promises based on amount split.
|
"""Consume proofs and create new promises based on amount split.
|
||||||
If snd_secrets is None, random secrets will be generated for the tokens to keep (fst_outputs)
|
If snd_secret is None, random secrets will be generated for the tokens to keep (fst_outputs)
|
||||||
and the promises to send (snd_outputs).
|
and the promises to send (snd_outputs).
|
||||||
|
|
||||||
If snd_secrets is provided, the wallet will create blinded secrets with those to attach a
|
If snd_secret is provided, the wallet will create blinded secrets with those to attach a
|
||||||
predefined spending condition to the tokens they want to send."""
|
predefined spending condition to the tokens they want to send."""
|
||||||
|
|
||||||
total = sum([p["amount"] for p in proofs])
|
total = sum([p["amount"] for p in proofs])
|
||||||
@@ -134,10 +141,13 @@ class LedgerAPI:
|
|||||||
snd_outputs = amount_split(snd_amt)
|
snd_outputs = amount_split(snd_amt)
|
||||||
|
|
||||||
amounts = fst_outputs + snd_outputs
|
amounts = fst_outputs + snd_outputs
|
||||||
if snd_secrets is None:
|
if snd_secret is None:
|
||||||
secrets = [self._generate_secret() for s in range(len(amounts))]
|
logger.debug("Generating random secrets.")
|
||||||
|
secrets = [self._generate_secret() for _ in range(len(amounts))]
|
||||||
else:
|
else:
|
||||||
logger.debug("Creating proofs with spending condition.")
|
logger.debug(f"Creating proofs with custom secret: {snd_secret}")
|
||||||
|
# TODO: serialize them here
|
||||||
|
snd_secrets = [f"{snd_secret}_{i}" for i in range(len(snd_outputs))]
|
||||||
assert len(snd_secrets) == len(
|
assert len(snd_secrets) == len(
|
||||||
snd_outputs
|
snd_outputs
|
||||||
), "number of snd_secrets does not match number of ouptus."
|
), "number of snd_secrets does not match number of ouptus."
|
||||||
@@ -149,7 +159,7 @@ class LedgerAPI:
|
|||||||
assert len(secrets) == len(
|
assert len(secrets) == len(
|
||||||
amounts
|
amounts
|
||||||
), "number of secrets does not match number of outputs"
|
), "number of secrets does not match number of outputs"
|
||||||
|
await self._check_used_secrets(secrets)
|
||||||
payloads, rs = self._construct_outputs(amounts, secrets)
|
payloads, rs = self._construct_outputs(amounts, secrets)
|
||||||
|
|
||||||
split_payload = SplitPayload(proofs=proofs, amount=amount, output_data=payloads)
|
split_payload = SplitPayload(proofs=proofs, amount=amount, output_data=payloads)
|
||||||
@@ -221,27 +231,27 @@ class Wallet(LedgerAPI):
|
|||||||
|
|
||||||
async def mint(self, amount: int, payment_hash: str = None):
|
async def mint(self, amount: int, payment_hash: str = None):
|
||||||
split = amount_split(amount)
|
split = amount_split(amount)
|
||||||
proofs = super().mint(split, payment_hash)
|
proofs = await super().mint(split, payment_hash)
|
||||||
if proofs == []:
|
if proofs == []:
|
||||||
raise Exception("received no proofs.")
|
raise Exception("received no proofs.")
|
||||||
await self._store_proofs(proofs)
|
await self._store_proofs(proofs)
|
||||||
self.proofs += proofs
|
self.proofs += proofs
|
||||||
return proofs
|
return proofs
|
||||||
|
|
||||||
async def redeem(self, proofs: List[Proof], snd_secrets: List[str] = None):
|
async def redeem(self, proofs: List[Proof], snd_secret: str = None):
|
||||||
if snd_secrets:
|
if snd_secret:
|
||||||
logger.debug(f"Redeption secrets: {snd_secrets}")
|
logger.debug(f"Redeption secret: {snd_secret}")
|
||||||
|
# TODO: serialize them here
|
||||||
|
snd_secrets = [f"{snd_secret}_{i}" for i in range(len(proofs))]
|
||||||
assert len(proofs) == len(snd_secrets)
|
assert len(proofs) == len(snd_secrets)
|
||||||
# overload proofs with custom secrets for redemption
|
# overload proofs with custom secrets for redemption
|
||||||
for p, s in zip(proofs, snd_secrets):
|
for p, s in zip(proofs, snd_secrets):
|
||||||
p.secret = s
|
p.secret = s
|
||||||
return await self.split(proofs, sum(p["amount"] for p in proofs))
|
return await self.split(proofs, sum(p["amount"] for p in proofs))
|
||||||
|
|
||||||
async def split(
|
async def split(self, proofs: List[Proof], amount: int, snd_secret: str = None):
|
||||||
self, proofs: List[Proof], amount: int, snd_secrets: List[str] = None
|
|
||||||
):
|
|
||||||
assert len(proofs) > 0, ValueError("no proofs provided.")
|
assert len(proofs) > 0, ValueError("no proofs provided.")
|
||||||
fst_proofs, snd_proofs = super().split(proofs, amount, snd_secrets)
|
fst_proofs, snd_proofs = await super().split(proofs, amount, snd_secret)
|
||||||
if len(fst_proofs) == 0 and len(snd_proofs) == 0:
|
if len(fst_proofs) == 0 and len(snd_proofs) == 0:
|
||||||
raise Exception("received no splits.")
|
raise Exception("received no splits.")
|
||||||
used_secrets = [p["secret"] for p in proofs]
|
used_secrets = [p["secret"] for p in proofs]
|
||||||
@@ -274,16 +284,14 @@ class Wallet(LedgerAPI):
|
|||||||
).decode()
|
).decode()
|
||||||
return token
|
return token
|
||||||
|
|
||||||
async def split_to_send(
|
async def split_to_send(self, proofs: List[Proof], amount, snd_secret: str = None):
|
||||||
self, proofs: List[Proof], amount, snd_secrets: List[str] = None
|
|
||||||
):
|
|
||||||
"""Like self.split but only considers non-reserved tokens."""
|
"""Like self.split but only considers non-reserved tokens."""
|
||||||
if snd_secrets:
|
if snd_secret:
|
||||||
logger.debug(f"Spending conditions: {snd_secrets}")
|
logger.debug(f"Spending conditions: {snd_secret}")
|
||||||
if len([p for p in proofs if not p.reserved]) <= 0:
|
if len([p for p in proofs if not p.reserved]) <= 0:
|
||||||
raise Exception("balance too low.")
|
raise Exception("balance too low.")
|
||||||
return await self.split(
|
return await self.split(
|
||||||
[p for p in proofs if not p.reserved], amount, snd_secrets
|
[p for p in proofs if not p.reserved], amount, snd_secret
|
||||||
)
|
)
|
||||||
|
|
||||||
async def set_reserved(self, proofs: List[Proof], reserved: bool):
|
async def set_reserved(self, proofs: List[Proof], reserved: bool):
|
||||||
@@ -321,7 +329,7 @@ class Wallet(LedgerAPI):
|
|||||||
|
|
||||||
def status(self):
|
def status(self):
|
||||||
print(
|
print(
|
||||||
f"Balance: {self.balance} sat (Available: {self.available_balance} sat in {len([p for p in self.proofs if not p.reserved])} tokens)"
|
f"Balance: {self.balance} sat (available: {self.available_balance} sat in {len([p for p in self.proofs if not p.reserved])} tokens)"
|
||||||
)
|
)
|
||||||
|
|
||||||
def proof_amounts(self):
|
def proof_amounts(self):
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import time
|
||||||
|
from re import S
|
||||||
from cashu.core.helpers import async_unwrap
|
from cashu.core.helpers import async_unwrap
|
||||||
from cashu.core.migrations import migrate_databases
|
from cashu.core.migrations import migrate_databases
|
||||||
from cashu.wallet import migrations
|
from cashu.wallet import migrations
|
||||||
@@ -108,11 +110,12 @@ async def run_test():
|
|||||||
assert wallet2.proof_amounts() == [4, 16]
|
assert wallet2.proof_amounts() == [4, 16]
|
||||||
|
|
||||||
# manipulate the proof amount
|
# manipulate the proof amount
|
||||||
w1_fst_proofs2[0]["amount"] = 123
|
# w1_fst_proofs2_manipulated = w1_fst_proofs2.copy()
|
||||||
await assert_err(
|
# w1_fst_proofs2_manipulated[0]["amount"] = 123
|
||||||
wallet1.split(w1_fst_proofs2, 20),
|
# await assert_err(
|
||||||
"Error: 123",
|
# wallet1.split(w1_fst_proofs2_manipulated, 20),
|
||||||
)
|
# "Error: 123",
|
||||||
|
# )
|
||||||
|
|
||||||
# try to split an invalid amount
|
# try to split an invalid amount
|
||||||
await assert_err(
|
await assert_err(
|
||||||
@@ -120,6 +123,37 @@ async def run_test():
|
|||||||
"Error: invalid split amount: -500",
|
"Error: invalid split amount: -500",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# mint with secrets
|
||||||
|
secret = f"asdasd_{time.time()}"
|
||||||
|
w1_fst_proofs, w1_snd_proofs = await wallet1.split(
|
||||||
|
wallet1.proofs, 65, snd_secret=secret
|
||||||
|
)
|
||||||
|
|
||||||
|
# strip away the secrets
|
||||||
|
w1_snd_proofs_manipulated = w1_snd_proofs.copy()
|
||||||
|
for p in w1_snd_proofs_manipulated:
|
||||||
|
p.secret = ""
|
||||||
|
await assert_err(
|
||||||
|
wallet2.redeem(w1_snd_proofs_manipulated),
|
||||||
|
"Error: duplicate proofs or promises.",
|
||||||
|
)
|
||||||
|
|
||||||
|
# redeem with wrong secret
|
||||||
|
await assert_err(
|
||||||
|
wallet2.redeem(w1_snd_proofs_manipulated, f"{secret}_asd"),
|
||||||
|
"Error: could not verify proofs.",
|
||||||
|
)
|
||||||
|
|
||||||
|
# redeem with correct secret
|
||||||
|
await wallet2.redeem(w1_snd_proofs_manipulated, secret)
|
||||||
|
|
||||||
|
# try to redeem them again
|
||||||
|
# NOTE: token indexing suffix _0
|
||||||
|
await assert_err(
|
||||||
|
wallet2.redeem(w1_snd_proofs_manipulated, secret),
|
||||||
|
f"Error: tokens already spent. Secret: {secret}_0",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
async_unwrap(run_test())
|
async_unwrap(run_test())
|
||||||
|
|||||||
Reference in New Issue
Block a user