Merge pull request #67 from cashubtc/mint/pending_proofs

check pending proofs
This commit is contained in:
calle
2022-12-15 00:11:55 +01:00
committed by GitHub
9 changed files with 192 additions and 46 deletions

View File

@@ -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:

View File

@@ -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

View File

@@ -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"

View File

@@ -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,

View File

@@ -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)

View File

@@ -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)
);
"""
)

View File

@@ -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"

View File

@@ -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",

View File

@@ -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