From 4d1c96adc051b4725b755dd0802b1c1ec1aa229e Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 12 Oct 2022 23:17:21 +0200 Subject: [PATCH] allow external migration modules --- lnbits/commands.py | 16 +- lnbits/extensions/cashu/ledger.py | 351 ------------------------------ lnbits/helpers.py | 4 + 3 files changed, 14 insertions(+), 357 deletions(-) delete mode 100644 lnbits/extensions/cashu/ledger.py diff --git a/lnbits/commands.py b/lnbits/commands.py index 83003314..1e0a98a1 100644 --- a/lnbits/commands.py +++ b/lnbits/commands.py @@ -69,14 +69,14 @@ async def migrate_databases(): (db_name, version, version), ) - async def run_migration(db, migrations_module): - db_name = migrations_module.__name__.split(".")[-2] + async def run_migration(db, migrations_module, db_name): for key, migrate in migrations_module.__dict__.items(): match = match = matcher.match(key) if match: version = int(match.group(1)) if version > current_versions.get(db_name, 0): logger.debug(f"running migration {db_name}.{version}") + logger.debug(f"db = {db}") await migrate(db) if db.schema == None: @@ -101,20 +101,24 @@ async def migrate_databases(): rows = await (await conn.execute("SELECT * FROM dbversions")).fetchall() current_versions = {row["db"]: row["version"] for row in rows} matcher = re.compile(r"^m(\d\d\d)_") - await run_migration(conn, core_migrations) + db_name = core_migrations.__name__.split(".")[-2] + await run_migration(conn, core_migrations, db_name) for ext in get_valid_extensions(): try: - ext_migrations = importlib.import_module( - f"lnbits.extensions.{ext.code}.migrations" + + module_str = ( + ext.migration_module or f"lnbits.extensions.{ext.code}.migrations" ) + ext_migrations = importlib.import_module(module_str) ext_db = importlib.import_module(f"lnbits.extensions.{ext.code}").db + db_name = ext.db_name or module_str.split(".")[-2] except ImportError: raise ImportError( f"Please make sure that the extension `{ext.code}` has a migrations file." ) async with ext_db.connect() as ext_conn: - await run_migration(ext_conn, ext_migrations) + await run_migration(ext_conn, ext_migrations, db_name) logger.info("✔️ All migrations done.") diff --git a/lnbits/extensions/cashu/ledger.py b/lnbits/extensions/cashu/ledger.py deleted file mode 100644 index a28dc97a..00000000 --- a/lnbits/extensions/cashu/ledger.py +++ /dev/null @@ -1,351 +0,0 @@ -import hashlib -from typing import List, Set - -from fastapi import Query -from secp256k1 import PrivateKey, PublicKey - -from lnbits.core.services import check_transaction_status, create_invoice - -from .crud import get_cashu -from .models import BlindedMessage, BlindedSignature, Invoice, Proof - - -def _derive_keys(master_key: str, cashu_id: str = Query(None)): - """Deterministic derivation of keys for 2^n values.""" - return { - 2 - ** i: PrivateKey( - hashlib.sha256((str(master_key) + str(i)).encode("utf-8")) - .hexdigest() - .encode("utf-8")[:32], - raw=True, - ) - for i in range(MAX_ORDER) - } - - -def _derive_pubkeys(keys: List[PrivateKey], cashu_id: str = Query(None)): - return {amt: keys[amt].pubkey for amt in [2**i for i in range(MAX_ORDER)]} - - -async def _generate_promises( - amounts: List[int], B_s: List[str], cashu_id: str = Query(None) -): - """Generates promises that sum to the given amount.""" - return [ - await self._generate_promise(amount, PublicKey(bytes.fromhex(B_), raw=True)) - for (amount, B_) in zip(amounts, B_s) - ] - - -async def _generate_promise(amount: int, B_: PublicKey, cashu_id: str = Query(None)): - """Generates a promise for given amount and returns a pair (amount, C').""" - secret_key = self.keys[amount] # Get the correct key - C_ = step2_bob(B_, secret_key) - await store_promise(amount, B_=B_.serialize().hex(), C_=C_.serialize().hex()) - return BlindedSignature(amount=amount, C_=C_.serialize().hex()) - - -def _check_spendable(proof: Proof, cashu_id: str = Query(None)): - """Checks whether the proof was already spent.""" - return not proof.secret in self.proofs_used - - -def _verify_proof(proof: Proof, cashu_id: str = Query(None)): - """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}") - secret_key = self.keys[proof.amount] # Get the correct key to check against - C = PublicKey(bytes.fromhex(proof.C), raw=True) - return verify(secret_key, C, proof.secret) - - -def _verify_outputs( - total: int, - amount: int, - output_data: List[BlindedMessage], - cashu_id: str = Query(None), -): - """Verifies the expected split was correctly computed""" - fst_amt, snd_amt = total - amount, amount # we have two amounts to split to - fst_outputs = amount_split(fst_amt) - snd_outputs = amount_split(snd_amt) - expected = fst_outputs + snd_outputs - given = [o.amount for o in output_data] - return given == expected - - -def _verify_no_duplicates( - proofs: List[Proof], output_data: List[BlindedMessage], cashu_id: str = Query(None) -): - secrets = [p.secret for p in proofs] - if len(secrets) != len(list(set(secrets))): - return False - B_s = [od.B_ for od in output_data] - if len(B_s) != len(list(set(B_s))): - return False - return True - - -def _verify_split_amount(amount: int, cashu_id: str = Query(None)): - """Split amount like output amount can't be negative or too big.""" - try: - self._verify_amount(amount) - except: - # For better error message - raise Exception("invalid split amount: " + str(amount)) - - -def _verify_amount(amount: int, cashu_id: str = Query(None)): - """Any amount used should be a positive integer not larger than 2^MAX_ORDER.""" - valid = isinstance(amount, int) and amount > 0 and amount < 2**MAX_ORDER - if not valid: - raise Exception("invalid amount: " + str(amount)) - return amount - - -def _verify_equation_balanced( - proofs: List[Proof], outs: List[BlindedMessage], cashu_id: str = Query(None) -): - """Verify that Σoutputs - Σinputs = 0.""" - sum_inputs = sum(self._verify_amount(p.amount) for p in proofs) - sum_outputs = sum(self._verify_amount(p.amount) for p in outs) - assert sum_outputs - sum_inputs == 0 - - -def _get_output_split(amount: int, cashu_id: str): - """Given an amount returns a list of amounts returned e.g. 13 is [1, 4, 8].""" - self._verify_amount(amount) - bits_amt = bin(amount)[::-1][:-2] - rv = [] - for (pos, bit) in enumerate(bits_amt): - if bit == "1": - rv.append(2**pos) - return rv - - -async def _invalidate_proofs(proofs: List[Proof], cashu_id: str = Query(None)): - """Adds secrets of proofs to the list of knwon secrets and stores them in the db.""" - # Mark proofs as used and prepare new promises - proof_msgs = set([p.secret for p in proofs]) - self.proofs_used |= proof_msgs - # store in db - for p in proofs: - await invalidate_proof(p) - - -def get_pubkeys(cashu_id: str = Query(None)): - """Returns public keys for possible amounts.""" - return {a: p.serialize().hex() for a, p in self.pub_keys.items()} - - -async def request_mint(amount, cashu_id: str = Query(None)): - cashu = await get_cashu(cashu_id) - if not cashu: - raise Exception(f"Could not find Cashu") - - """Returns Lightning invoice and stores it in the db.""" - payment_hash, payment_request = await create_invoice( - wallet_id=cashu.wallet, - amount=amount, - memo=cashu.name, - unhashed_description=cashu.name.encode("utf-8"), - extra={"tag": "Cashu"}, - ) - - invoice = Invoice( - amount=amount, pr=payment_request, hash=payment_hash, issued=False - ) - if not payment_request or not payment_hash: - raise Exception(f"Could not create Lightning invoice.") - return payment_request, payment_hash - - -async def mint( - B_s: List[PublicKey], - amounts: List[int], - payment_hash: str = Query(None), - cashu_id: str = Query(None), -): - cashu = await get_cashu(cashu_id) - if not cashu: - raise Exception(f"Could not find Cashu") - - """Mints a promise for coins for B_.""" - # check if lightning invoice was paid - if payment_hash: - if not await check_transaction_status( - wallet_id=cashu.wallet, payment_hash=payment_hash - ): - raise Exception("Lightning invoice not paid yet.") - - for amount in amounts: - if amount not in [2**i for i in range(MAX_ORDER)]: - raise Exception(f"Can only mint amounts up to {2**MAX_ORDER}.") - - promises = [ - await self._generate_promise(amount, B_) for B_, amount in zip(B_s, amounts) - ] - return promises - - -async def melt( - proofs: List[Proof], amount: int, invoice: str, cashu_id: str = Query(None) -): - cashu = await get_cashu(cashu_id) - if not cashu: - raise Exception(f"Could not find Cashu") - - """Invalidates proofs and pays a Lightning invoice.""" - # if not LIGHTNING: - total = sum([p["amount"] for p in proofs]) - # check that lightning fees are included - assert total + fee_reserve(amount * 1000) >= amount, Exception( - "provided proofs not enough for Lightning payment." - ) - - status, payment_hash = await pay_invoice( - wallet_id=link.wallet, - payment_request=invoice, - max_sat=amount, - extra={"tag": "Ecash melt"}, - ) - - if status == True: - await self._invalidate_proofs(proofs) - return status, payment_hash - - -async def check_spendable(proofs: List[Proof], cashu_id: str = Query(None)): - cashu = await get_cashu(cashu_id) - if not cashu: - raise Exception(f"Could not find Cashu") - - """Checks if all provided proofs are valid and still spendable (i.e. have not been spent).""" - return {i: self._check_spendable(p) for i, p in enumerate(proofs)} - - -async def split( - proofs: List[Proof], - amount: int, - output_data: List[BlindedMessage], - cashu_id: str = Query(None), -): - cashu = await get_cashu(cashu_id) - if not cashu: - raise Exception(f"Could not find Cashu") - - """Consumes proofs and prepares new promises based on the amount split.""" - self._verify_split_amount(amount) - # Verify proofs are valid - if not all([self._verify_proof(p) for p in proofs]): - return False - - total = sum([p.amount for p in proofs]) - - if not self._verify_no_duplicates(proofs, output_data): - raise Exception("duplicate proofs or promises") - if amount > total: - raise Exception("split amount is higher than the total sum") - if not self._verify_outputs(total, amount, output_data): - raise Exception("split of promises is not as expected") - - # Mark proofs as used and prepare new promises - await self._invalidate_proofs(proofs) - - outs_fst = amount_split(total - amount) - outs_snd = amount_split(amount) - B_fst = [od.B_ for od in output_data[: len(outs_fst)]] - B_snd = [od.B_ for od in output_data[len(outs_fst) :]] - prom_fst, prom_snd = await self._generate_promises( - outs_fst, B_fst - ), await self._generate_promises(outs_snd, B_snd) - self._verify_equation_balanced(proofs, prom_fst + prom_snd) - return prom_fst, prom_snd - - -async def fee_reserve(amount_msat: int, cashu_id: str = Query(None)): - cashu = await get_cashu(cashu_id) - if not cashu: - raise Exception(f"Could not find Cashu") - - """Function for calculating the Lightning fee reserve""" - return max( - int(LIGHTNING_RESERVE_FEE_MIN), int(amount_msat * LIGHTNING_FEE_PERCENT / 100.0) - ) - - -async def amount_split(amount, cashu_id: str): - cashu = await get_cashu(cashu_id) - if not cashu: - raise Exception(f"Could not find Cashu") - - """Given an amount returns a list of amounts returned e.g. 13 is [1, 4, 8].""" - bits_amt = bin(amount)[::-1][:-2] - rv = [] - for (pos, bit) in enumerate(bits_amt): - if bit == "1": - rv.append(2**pos) - return rv - - -async def hash_to_point(secret_msg, cashu_id: str = Query(None)): - cashu = await get_cashu(cashu_id) - if not cashu: - raise Exception(f"Could not find Cashu") - - """Generates x coordinate from the message hash and checks if the point lies on the curve. - If it does not, it tries computing again a new x coordinate from the hash of the coordinate.""" - point = None - msg = secret_msg - while point is None: - _hash = hashlib.sha256(msg).hexdigest().encode("utf-8") - try: - # We construct compressed pub which has x coordinate encoded with even y - _hash = list(_hash[:33]) # take the 33 bytes and get a list of bytes - _hash[0] = 0x02 # set first byte to represent even y coord - _hash = bytes(_hash) - point = PublicKey(_hash, raw=True) - except: - msg = _hash - - return point - - -async def step1_alice(secret_msg, cashu_id: str = Query(None)): - cashu = await get_cashu(cashu_id) - if not cashu: - raise Exception(f"Could not find Cashu") - - secret_msg = secret_msg.encode("utf-8") - Y = hash_to_point(secret_msg) - r = PrivateKey() - B_ = Y + r.pubkey - return B_, r - - -async def step2_bob(B_, a, cashu_id: str = Query(None)): - cashu = await get_cashu(cashu_id) - if not cashu: - raise Exception(f"Could not find Cashu") - - C_ = B_.mult(a) - return C_ - - -async def step3_alice(C_, r, A, cashu_id: str = Query(None)): - cashu = await get_cashu(cashu_id) - if not cashu: - raise Exception(f"Could not find Cashu") - - C = C_ - A.mult(r) - return C - - -async def verify(a, C, secret_msg, cashu_id: str = Query(None)): - cashu = await get_cashu(cashu_id) - if not cashu: - raise Exception(f"Could not find Cashu") - - Y = hash_to_point(secret_msg.encode("utf-8")) - return C == Y.mult(a) diff --git a/lnbits/helpers.py b/lnbits/helpers.py index 06d407ab..8d3099e4 100644 --- a/lnbits/helpers.py +++ b/lnbits/helpers.py @@ -20,6 +20,8 @@ class Extension(NamedTuple): icon: Optional[str] = None contributors: Optional[List[str]] = None hidden: bool = False + migration_module: Optional[str] = None + db_name: Optional[str] = None class ExtensionManager: @@ -64,6 +66,8 @@ class ExtensionManager: config.get("icon"), config.get("contributors"), config.get("hidden") or False, + config.get("migration_module"), + config.get("db_name"), ) )