diff --git a/cashu/core/base.py b/cashu/core/base.py index 4d6ccbdf..03228127 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -204,6 +204,30 @@ def to_dict(self, include_stamps=False): d["stamp"] = self.stamp.dict() return d + @classmethod + def from_row(cls, rowObj: Row): + row = dict(rowObj) + return cls( + id=row["id"], + amount=row["amount"], + secret=row["secret"], + C=row["C"], + stamp=StampSignature(e=row["stamp_e"], s=row["stamp_s"]) + if row["stamp_e"] and row["stamp_s"] + else None, + # p2pksigs=json.loads(row["p2pksigs"]) if row["p2pksigs"] else None, + # p2shscript=P2SHScript( + # script=row["script"], signature=row["signature"] + # ) # type: ignore + # if row["script"] + # else None, + reserved=row["reserved"], + send_id=row["send_id"], + time_created=row["time_created"], + time_reserved=row["time_reserved"], + derivation_path=row["derivation_path"], + ) + def __getitem__(self, key): return self.__getattribute__(key) diff --git a/cashu/core/crypto/b_dhke.py b/cashu/core/crypto/b_dhke.py index f4dc179a..e534217e 100644 --- a/cashu/core/crypto/b_dhke.py +++ b/cashu/core/crypto/b_dhke.py @@ -76,7 +76,7 @@ def verify(a: PrivateKey, C: PublicKey, secret_msg: str) -> bool: # stamps """ -Proves that a in A = a*G is the same as a in C_ = a*Y +Proves that a in A = a*G is the same as a in C = a*Y Bob: R1 = rG @@ -88,8 +88,11 @@ def verify(a: PrivateKey, C: PublicKey, secret_msg: str) -> bool: Y = hash_to_curve(x) R1 = sG - eA R2 = sY - eC + (eaY = eC, since C' - rA = aY + arG - arG = aY = C) + e == hash(R1, R2, Y, C) (verification) +If true, C must have originated from Bob with private key a """ @@ -129,7 +132,6 @@ def stamp_step2_alice_verify( assert s.pubkey R1: PublicKey = s.pubkey - A.mult(e) # type: ignore # R1 = sG - eA R2: PublicKey = Y.mult(s) - C.mult(e) # type: ignore # R2 = sY - eC - print(R1.serialize().hex(), R2.serialize().hex()) e_bytes = e.private_key return e_bytes == hash_e(R1, R2, Y, C) diff --git a/cashu/wallet/cli/cli.py b/cashu/wallet/cli/cli.py index c3e6b377..dea1b247 100644 --- a/cashu/wallet/cli/cli.py +++ b/cashu/wallet/cli/cli.py @@ -761,3 +761,39 @@ async def restore(ctx: Context, to: int, batch: int): await wallet.restore_wallet_from_mnemonic(mnemonic, to=to, batch=batch) await wallet.load_proofs() wallet.status() + + +@cli.command("stamp", help="Stamp tokens in wallet.") +@click.option( + "--max-amount", + "-m", + default=None, + help="Maximum amount to stamp.", + type=int, +) +@click.pass_context +@coro +async def stamp(ctx: Context, max_amount: int): + wallet: Wallet = ctx.obj["WALLET"] + await wallet.load_mint() + await wallet.load_proofs() + proofs_without_stamp = [p for p in wallet.proofs if not p.stamp] + if len(proofs_without_stamp) == 0: + print("All tokens in wallet are stamped.") + return + + if max_amount: + # sort proofs by amount and remove largest proofs until max_amount is reached + proofs_without_stamp = sorted( + proofs_without_stamp, key=lambda x: x.amount, reverse=True + ) + while sum_proofs(proofs_without_stamp) > max_amount: + proofs_without_stamp.pop(0) + if len(proofs_without_stamp) == 0: + print(f"No tokens smaller than {max_amount} sat in wallet.") + return + + print( + f"Requesting {len(proofs_without_stamp)} stamps for tokens worth {sum_proofs(proofs_without_stamp)} sat." + ) + await wallet.get_proofs_stamps(proofs_without_stamp) diff --git a/cashu/wallet/crud.py b/cashu/wallet/crud.py index 887db759..463c4d41 100644 --- a/cashu/wallet/crud.py +++ b/cashu/wallet/crud.py @@ -36,7 +36,7 @@ async def get_proofs( SELECT * from proofs """ ) - return [Proof(**dict(r)) for r in rows] + return [Proof.from_row(r) for r in rows] async def get_reserved_proofs( @@ -82,10 +82,13 @@ async def invalidate_proof( ) -async def update_proof_reserved( +async def update_proof( proof: Proof, - reserved: bool, - send_id: str = "", + *, + reserved: Optional[bool] = None, + send_id: Optional[str] = None, + stamp_e: Optional[str] = None, + stamp_s: Optional[str] = None, db: Optional[Database] = None, conn: Optional[Connection] = None, ): @@ -103,6 +106,11 @@ async def update_proof_reserved( clauses.append("time_reserved = ?") values.append(int(time.time())) + if stamp_e and stamp_s: + clauses.append("stamp_e = ?") + values.append(stamp_e) + clauses.append("stamp_s = ?") + values.append(stamp_s) await (conn or db).execute( # type: ignore f"UPDATE proofs SET {', '.join(clauses)} WHERE secret = ?", (*values, str(proof.secret)), diff --git a/cashu/wallet/migrations.py b/cashu/wallet/migrations.py index 78e8cfb3..c962da1b 100644 --- a/cashu/wallet/migrations.py +++ b/cashu/wallet/migrations.py @@ -197,6 +197,6 @@ async def m009_privatekey_and_determinstic_key_derivation(db: Database): async def m010_proofs_add_stamps(db: Database): await db.execute("ALTER TABLE proofs ADD COLUMN stamp_e TEXT") - await db.execute("ALTER TABLE proofs ADD COLUMN stamp_r TEXT") + await db.execute("ALTER TABLE proofs ADD COLUMN stamp_s TEXT") await db.execute("ALTER TABLE proofs_used ADD COLUMN stamp_e TEXT") - await db.execute("ALTER TABLE proofs_used ADD COLUMN stamp_r TEXT") + await db.execute("ALTER TABLE proofs_used ADD COLUMN stamp_s TEXT") diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index 5bd3f003..c2592843 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -78,7 +78,7 @@ store_proof, store_seed_and_mnemonic, update_lightning_invoice, - update_proof_reserved, + update_proof, ) from . import migrations @@ -1244,6 +1244,7 @@ async def get_proofs_stamps(self, proofs: List[Proof]): """ stamp_response = await super().get_proofs_stamps(proofs) + logger.trace(stamp_response) stamps = stamp_response.stamps for proof, stamp in zip(proofs, stamps): assert b_dhke.stamp_step2_alice_verify( @@ -1253,6 +1254,10 @@ async def get_proofs_stamps(self, proofs: List[Proof]): e=PrivateKey(bytes.fromhex(stamp.e), raw=True), A=self.public_keys[proof.amount], ), "stamp verification failed." + + await update_proof( + proof=proof, stamp_e=stamp.e, stamp_s=stamp.s, db=self.db + ) return True # ---------- TOKEN MECHANIS ---------- @@ -1472,9 +1477,7 @@ async def set_reserved(self, proofs: List[Proof], reserved: bool) -> None: uuid_str = str(uuid.uuid1()) for proof in proofs: proof.reserved = True - await update_proof_reserved( - proof, reserved=reserved, send_id=uuid_str, db=self.db - ) + await update_proof(proof, reserved=reserved, send_id=uuid_str, db=self.db) async def invalidate( self, proofs: List[Proof], check_spendable=True