mirror of
https://github.com/aljazceru/nutshell.git
synced 2026-01-06 10:24:21 +01:00
Merge pull request #67 from cashubtc/mint/pending_proofs
check pending proofs
This commit is contained in:
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@@ -7,7 +7,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
os: [ubuntu-latest]
|
||||
python-version: ["3.9"]
|
||||
poetry-version: ["1.2.1"]
|
||||
steps:
|
||||
|
||||
@@ -106,7 +106,7 @@ cashu info
|
||||
|
||||
Returns:
|
||||
```bash
|
||||
Version: 0.5.5
|
||||
Version: 0.6.0
|
||||
Debug: False
|
||||
Cashu dir: /home/user/.cashu
|
||||
Wallet: wallet
|
||||
|
||||
@@ -53,4 +53,4 @@ LNBITS_ENDPOINT = env.str("LNBITS_ENDPOINT", default=None)
|
||||
LNBITS_KEY = env.str("LNBITS_KEY", default=None)
|
||||
|
||||
MAX_ORDER = 64
|
||||
VERSION = "0.5.5"
|
||||
VERSION = "0.6.0"
|
||||
|
||||
@@ -29,6 +29,18 @@ class LedgerCrud:
|
||||
|
||||
return await invalidate_proof(*args, **kwags)
|
||||
|
||||
async def get_proofs_pending(*args, **kwags):
|
||||
|
||||
return await get_proofs_pending(*args, **kwags)
|
||||
|
||||
async def set_proof_pending(*args, **kwags):
|
||||
|
||||
return await set_proof_pending(*args, **kwags)
|
||||
|
||||
async def unset_proof_pending(*args, **kwags):
|
||||
|
||||
return await unset_proof_pending(*args, **kwags)
|
||||
|
||||
async def store_keyset(*args, **kwags):
|
||||
|
||||
return await store_keyset(*args, **kwags)
|
||||
@@ -102,6 +114,55 @@ async def invalidate_proof(
|
||||
)
|
||||
|
||||
|
||||
async def get_proofs_pending(
|
||||
db: Database,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
|
||||
rows = await (conn or db).fetchall(
|
||||
f"""
|
||||
SELECT * from {table_with_schema(db, 'proofs_pending')}
|
||||
"""
|
||||
)
|
||||
return [Proof(**r) for r in rows]
|
||||
|
||||
|
||||
async def set_proof_pending(
|
||||
db: Database,
|
||||
proof: Proof,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
|
||||
# we add the proof and secret to the used list
|
||||
await (conn or db).execute(
|
||||
f"""
|
||||
INSERT INTO {table_with_schema(db, 'proofs_pending')}
|
||||
(amount, C, secret)
|
||||
VALUES (?, ?, ?)
|
||||
""",
|
||||
(
|
||||
proof.amount,
|
||||
str(proof.C),
|
||||
str(proof.secret),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def unset_proof_pending(
|
||||
proof: Proof,
|
||||
db: Database,
|
||||
conn: Optional[Connection] = None,
|
||||
):
|
||||
|
||||
await (conn or db).execute(
|
||||
f"""
|
||||
DELETE FROM {table_with_schema(db, 'proofs_pending')}
|
||||
WHERE secret = ?
|
||||
""",
|
||||
(str(proof["secret"]),),
|
||||
)
|
||||
|
||||
|
||||
async def store_lightning_invoice(
|
||||
db: Database,
|
||||
invoice: Invoice,
|
||||
|
||||
@@ -110,12 +110,14 @@ class Ledger:
|
||||
return not proof.secret in self.proofs_used
|
||||
|
||||
def _verify_secret_criteria(self, proof: Proof):
|
||||
"""Verifies that a secret is present"""
|
||||
"""Verifies that a secret is present and is not too long (DOS prevention)."""
|
||||
if proof.secret is None or proof.secret == "":
|
||||
raise Exception("no secret in proof.")
|
||||
if len(proof.secret) > 64:
|
||||
raise Exception("secret too long.")
|
||||
return True
|
||||
|
||||
def _verify_proof(self, proof: Proof):
|
||||
def _verify_proof_bdhke(self, proof: Proof):
|
||||
"""Verifies that the proof of promise was issued by this ledger."""
|
||||
if not self._check_spendable(proof):
|
||||
raise Exception(f"tokens already spent. Secret: {proof.secret}")
|
||||
@@ -179,10 +181,13 @@ class Ledger:
|
||||
given = [o.amount for o in outputs]
|
||||
return given == expected
|
||||
|
||||
def _verify_no_duplicates(self, proofs: List[Proof], outputs: List[BlindedMessage]):
|
||||
def _verify_no_duplicate_proofs(self, proofs: List[Proof]):
|
||||
secrets = [p.secret for p in proofs]
|
||||
if len(secrets) != len(list(set(secrets))):
|
||||
return False
|
||||
return True
|
||||
|
||||
def _verify_no_duplicate_outputs(self, outputs: List[BlindedMessage]):
|
||||
B_s = [od.B_ for od in outputs]
|
||||
if len(B_s) != len(list(set(B_s))):
|
||||
return False
|
||||
@@ -258,7 +263,10 @@ class Ledger:
|
||||
return ok, preimage
|
||||
|
||||
async def _invalidate_proofs(self, proofs: List[Proof]):
|
||||
"""Adds secrets of proofs to the list of knwon secrets and stores them in the db."""
|
||||
"""
|
||||
Adds secrets of proofs to the list of known secrets and stores them in the db.
|
||||
Removes proofs from pending table.
|
||||
"""
|
||||
# Mark proofs as used and prepare new promises
|
||||
proof_msgs = set([p.secret for p in proofs])
|
||||
self.proofs_used |= proof_msgs
|
||||
@@ -266,6 +274,53 @@ class Ledger:
|
||||
for p in proofs:
|
||||
await self.crud.invalidate_proof(proof=p, db=self.db)
|
||||
|
||||
async def _set_proofs_pending(self, proofs: List[Proof]):
|
||||
"""
|
||||
If none of the proofs is in the pending table (_validate_proofs_pending), adds proofs to
|
||||
the list of pending proofs or removes them. Used as a mutex for proofs.
|
||||
"""
|
||||
# first we check whether these proofs are pending aready
|
||||
await self._validate_proofs_pending(proofs)
|
||||
for p in proofs:
|
||||
try:
|
||||
await self.crud.set_proof_pending(proof=p, db=self.db)
|
||||
except:
|
||||
raise Exception("proofs already pending.")
|
||||
|
||||
async def _unset_proofs_pending(self, proofs: List[Proof]):
|
||||
"""Deletes proofs from pending table."""
|
||||
# we try: except: this block in order to avoid that any errors here
|
||||
# could block the _invalidate_proofs() call that happens afterwards.
|
||||
try:
|
||||
for p in proofs:
|
||||
await self.crud.unset_proof_pending(proof=p, db=self.db)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
pass
|
||||
|
||||
async def _validate_proofs_pending(self, proofs: List[Proof]):
|
||||
"""Checks if any of the provided proofs is in the pending proofs table. Raises exception for at least one match."""
|
||||
proofs_pending = await self.crud.get_proofs_pending(db=self.db)
|
||||
for p in proofs:
|
||||
for pp in proofs_pending:
|
||||
if p.secret == pp.secret:
|
||||
raise Exception("proofs are pending.")
|
||||
|
||||
async def _verify_proofs(self, proofs: List[Proof]):
|
||||
"""Checks a series of criteria for the verification of proofs."""
|
||||
# Verify scripts
|
||||
if not all([self._verify_script(i, p) for i, p in enumerate(proofs)]):
|
||||
raise Exception("script validation failed.")
|
||||
# Verify secret criteria
|
||||
if not all([self._verify_secret_criteria(p) for p in proofs]):
|
||||
raise Exception("secrets do not match criteria.")
|
||||
# verify that only unique proofs were used
|
||||
if not self._verify_no_duplicate_proofs(proofs):
|
||||
raise Exception("duplicate proofs.")
|
||||
# Verify proofs
|
||||
if not all([self._verify_proof_bdhke(p) for p in proofs]):
|
||||
raise Exception("could not verify proofs.")
|
||||
|
||||
# Public methods
|
||||
def get_keyset(self, keyset_id: str = None):
|
||||
keyset = self.keysets.keysets[keyset_id] if keyset_id else self.keyset
|
||||
@@ -308,24 +363,33 @@ class Ledger:
|
||||
|
||||
async def melt(self, proofs: List[Proof], invoice: str):
|
||||
"""Invalidates proofs and pays a Lightning invoice."""
|
||||
# Verify proofs
|
||||
if not all([self._verify_proof(p) for p in proofs]):
|
||||
raise Exception("could not verify proofs.")
|
||||
|
||||
total_provided = sum_proofs(proofs)
|
||||
invoice_obj = bolt11.decode(invoice)
|
||||
amount = math.ceil(invoice_obj.amount_msat / 1000)
|
||||
fees_msat = await self.check_fees(invoice)
|
||||
assert total_provided >= amount + fees_msat / 1000, Exception(
|
||||
"provided proofs not enough for Lightning payment."
|
||||
)
|
||||
# validate and set proofs as pending
|
||||
await self._set_proofs_pending(proofs)
|
||||
|
||||
try:
|
||||
await self._verify_proofs(proofs)
|
||||
|
||||
total_provided = sum_proofs(proofs)
|
||||
invoice_obj = bolt11.decode(invoice)
|
||||
amount = math.ceil(invoice_obj.amount_msat / 1000)
|
||||
fees_msat = await self.check_fees(invoice)
|
||||
assert total_provided >= amount + fees_msat / 1000, Exception(
|
||||
"provided proofs not enough for Lightning payment."
|
||||
)
|
||||
|
||||
if LIGHTNING:
|
||||
status, preimage = await self._pay_lightning_invoice(invoice, fees_msat)
|
||||
else:
|
||||
status, preimage = True, "preimage"
|
||||
if status == True:
|
||||
await self._invalidate_proofs(proofs)
|
||||
except Exception as e:
|
||||
raise e
|
||||
finally:
|
||||
# delete proofs from pending list
|
||||
await self._unset_proofs_pending(proofs)
|
||||
|
||||
if LIGHTNING:
|
||||
status, preimage = await self._pay_lightning_invoice(invoice, fees_msat)
|
||||
else:
|
||||
status, preimage = True, "preimage"
|
||||
if status == True:
|
||||
await self._invalidate_proofs(proofs)
|
||||
return status, preimage
|
||||
|
||||
async def check_spendable(self, proofs: List[Proof]):
|
||||
@@ -354,29 +418,32 @@ class Ledger:
|
||||
keyset: MintKeyset = None,
|
||||
):
|
||||
"""Consumes proofs and prepares new promises based on the amount split."""
|
||||
|
||||
# set proofs as pending
|
||||
await self._set_proofs_pending(proofs)
|
||||
|
||||
total = sum_proofs(proofs)
|
||||
|
||||
# verify that amount is kosher
|
||||
self._verify_split_amount(amount)
|
||||
# verify overspending attempt
|
||||
if amount > total:
|
||||
raise Exception("split amount is higher than the total sum.")
|
||||
try:
|
||||
# verify that amount is kosher
|
||||
self._verify_split_amount(amount)
|
||||
# verify overspending attempt
|
||||
if amount > total:
|
||||
raise Exception("split amount is higher than the total sum.")
|
||||
|
||||
# Verify scripts
|
||||
if not all([self._verify_script(i, p) for i, p in enumerate(proofs)]):
|
||||
raise Exception("script verification failed.")
|
||||
# Verify secret criteria
|
||||
if not all([self._verify_secret_criteria(p) for p in proofs]):
|
||||
raise Exception("secrets do not match criteria.")
|
||||
# verify that only unique proofs and outputs were used
|
||||
if not self._verify_no_duplicates(proofs, outputs):
|
||||
raise Exception("duplicate proofs or promises.")
|
||||
# verify that outputs have the correct amount
|
||||
if not self._verify_outputs(total, amount, outputs):
|
||||
raise Exception("split of promises is not as expected.")
|
||||
# Verify proofs
|
||||
if not all([self._verify_proof(p) for p in proofs]):
|
||||
raise Exception("could not verify proofs.")
|
||||
await self._verify_proofs(proofs)
|
||||
|
||||
# verify that only unique outputs were used
|
||||
if not self._verify_no_duplicate_outputs(outputs):
|
||||
raise Exception("duplicate promises.")
|
||||
# verify that outputs have the correct amount
|
||||
if not self._verify_outputs(total, amount, outputs):
|
||||
raise Exception("split of promises is not as expected.")
|
||||
except Exception as e:
|
||||
raise e
|
||||
finally:
|
||||
# delete proofs from pending list
|
||||
await self._unset_proofs_pending(proofs)
|
||||
|
||||
# Mark proofs as used and prepare new promises
|
||||
await self._invalidate_proofs(proofs)
|
||||
|
||||
@@ -128,3 +128,21 @@ async def m004_keysets_add_version(db: Database):
|
||||
await db.execute(
|
||||
f"ALTER TABLE {table_with_schema(db, 'keysets')} ADD COLUMN version TEXT"
|
||||
)
|
||||
|
||||
|
||||
async def m005_pending_proofs_table(db: Database) -> None:
|
||||
"""
|
||||
Store pending proofs.
|
||||
"""
|
||||
await db.execute(
|
||||
f"""
|
||||
CREATE TABLE IF NOT EXISTS {table_with_schema(db, 'proofs_pending')} (
|
||||
amount INTEGER NOT NULL,
|
||||
C TEXT NOT NULL,
|
||||
secret TEXT NOT NULL,
|
||||
|
||||
UNIQUE (secret)
|
||||
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "cashu"
|
||||
version = "0.5.5"
|
||||
version = "0.6.0"
|
||||
description = "Ecash wallet and mint."
|
||||
authors = ["calle <callebtc@protonmail.com>"]
|
||||
license = "MIT"
|
||||
|
||||
2
setup.py
2
setup.py
@@ -13,7 +13,7 @@ entry_points = {"console_scripts": ["cashu = cashu.wallet.cli:cli"]}
|
||||
|
||||
setuptools.setup(
|
||||
name="cashu",
|
||||
version="0.5.5",
|
||||
version="0.6.0",
|
||||
description="Ecash wallet and mint with Bitcoin Lightning support",
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/markdown",
|
||||
|
||||
@@ -137,7 +137,7 @@ async def test_duplicate_proofs_double_spent(wallet1: Wallet):
|
||||
doublespend = await wallet1.mint(64)
|
||||
await assert_err(
|
||||
wallet1.split(wallet1.proofs + doublespend, 20),
|
||||
"Mint Error: duplicate proofs or promises.",
|
||||
"Mint Error: proofs already pending.",
|
||||
)
|
||||
assert wallet1.balance == 64
|
||||
assert wallet1.available_balance == 64
|
||||
|
||||
Reference in New Issue
Block a user