Remove P2SH (#341)

* remove p2sh

* Fix WalletAPI LockResponse
This commit is contained in:
callebtc
2023-10-13 21:41:54 +02:00
committed by GitHub
parent d827579e65
commit 744807b6f4
16 changed files with 120 additions and 521 deletions

View File

@@ -79,12 +79,8 @@ class Proof(BaseModel):
secret: str = "" # secret or message to be blinded and signed
C: str = "" # signature on secret, unblinded by wallet
dleq: Union[DLEQWallet, None] = None # DLEQ proof
witness: Union[None, str] = "" # witness for spending condition
# p2pksigs: Union[List[str], None] = [] # P2PK signature
# p2shscript: Union[P2SHWitness, None] = None # P2SH spending condition
# htlcpreimage: Union[str, None] = None # HTLC unlocking preimage
# htlcsignature: Union[str, None] = None # HTLC unlocking signature
# whether this proof is reserved for sending, used for coin management in the wallet
reserved: Union[None, bool] = False
# unique ID of send attempt, used for grouping pending tokens in the wallet

View File

@@ -1,168 +0,0 @@
import base64
import hashlib
import random
from bitcoin.core import CMutableTxIn, CMutableTxOut, COutPoint, CTransaction, lx
from bitcoin.core.script import OP_CHECKSIG, SIGHASH_ALL, CScript, SignatureHash
from bitcoin.core.scripteval import (
SCRIPT_VERIFY_P2SH,
EvalScriptError,
VerifyScript,
VerifyScriptError,
)
from bitcoin.wallet import CBitcoinSecret, P2SHBitcoinAddress
COIN = 100_000_000
TXID = "bff785da9f8169f49be92fa95e31f0890c385bfb1bd24d6b94d7900057c617ae"
SEED = b"__not__used"
def step0_carol_privkey():
"""Private key"""
# h = hashlib.sha256(SEED).digest()
h = hashlib.sha256(str(random.getrandbits(256)).encode()).digest()
seckey = CBitcoinSecret.from_secret_bytes(h)
return seckey
def step0_carol_checksig_redeemscript(carol_pubkey):
"""Create script"""
txin_redeemScript = CScript([carol_pubkey, OP_CHECKSIG]) # type: ignore
# txin_redeemScript = CScript([-123, OP_CHECKLOCKTIMEVERIFY])
# txin_redeemScript = CScript([3, 3, OP_LESSTHAN, OP_VERIFY])
return txin_redeemScript
def step1_carol_create_p2sh_address(txin_redeemScript):
"""Create address (serialized scriptPubKey) to share with Alice"""
txin_p2sh_address = P2SHBitcoinAddress.from_redeemScript(txin_redeemScript)
return txin_p2sh_address
def step1_bob_carol_create_tx(txin_p2sh_address):
"""Create transaction"""
txid = lx(TXID)
vout = 0
txin = CMutableTxIn(COutPoint(txid, vout))
txout = CMutableTxOut(
int(0.0005 * COIN),
P2SHBitcoinAddress(str(txin_p2sh_address)).to_scriptPubKey(),
)
tx = CTransaction([txin], [txout])
return tx, txin
def step2_carol_sign_tx(txin_redeemScript, privatekey):
"""Sign transaction with private key"""
txin_p2sh_address = step1_carol_create_p2sh_address(txin_redeemScript)
tx, txin = step1_bob_carol_create_tx(txin_p2sh_address)
sighash = SignatureHash(txin_redeemScript, tx, 0, SIGHASH_ALL)
sig = privatekey.sign(sighash) + bytes([SIGHASH_ALL])
txin.scriptSig = CScript([sig, txin_redeemScript]) # type: ignore
return txin
def step3_bob_verify_script(txin_signature, txin_redeemScript, tx):
txin_scriptPubKey = txin_redeemScript.to_p2sh_scriptPubKey()
try:
VerifyScript(
txin_signature, txin_scriptPubKey, tx, 0, flags=[SCRIPT_VERIFY_P2SH]
)
return True
except VerifyScriptError as e:
raise Exception("Script verification failed:", e)
except EvalScriptError as e:
print(f"Script: {txin_scriptPubKey.__repr__()}")
raise Exception("Script evaluation failed:", e)
except Exception as e:
raise Exception("Script execution failed:", e)
def verify_bitcoin_script(txin_redeemScript_b64, txin_signature_b64):
txin_redeemScript = CScript(base64.urlsafe_b64decode(txin_redeemScript_b64))
# print("Redeem script:", txin_redeemScript.__repr__())
# txin_redeemScript = CScript([2, 3, OP_LESSTHAN, OP_VERIFY])
txin_signature = CScript(value=base64.urlsafe_b64decode(txin_signature_b64))
txin_p2sh_address = step1_carol_create_p2sh_address(txin_redeemScript)
# print(f"Bob recreates secret: P2SH:{txin_p2sh_address}")
# MINT checks that P2SH:txin_p2sh_address has not been spent yet
# ...
tx, _ = step1_bob_carol_create_tx(txin_p2sh_address)
# print(
# f"Bob verifies:\nscript: {txin_redeemScript_b64}\nsignature: {txin_signature_b64}\n"
# )
script_valid = step3_bob_verify_script(txin_signature, txin_redeemScript, tx)
# MINT redeems tokens and stores P2SH:txin_p2sh_address
# ...
# if script_valid:
# print("Successfull.")
# else:
# print("Error.")
return txin_p2sh_address, script_valid
# simple test case
if __name__ == "__main__":
# https://github.com/romanz/python-bitcointx/blob/master/examples/spend-p2sh-txout.py
# CAROL shares txin_p2sh_address with ALICE:
# ---------
# CAROL defines scripthash and ALICE mints them
alice_privkey = step0_carol_privkey()
txin_redeemScript = step0_carol_checksig_redeemscript(alice_privkey.pub)
print("Script:", txin_redeemScript.__repr__())
txin_p2sh_address = step1_carol_create_p2sh_address(txin_redeemScript)
print(f"Carol sends Alice secret = P2SH:{txin_p2sh_address}")
print("")
# ---------
# ALICE: mint tokens with secret P2SH:txin_p2sh_address
print(f"Alice mints tokens with secret = P2SH:{txin_p2sh_address}")
print("")
# ...
# ---------
# CAROL redeems with MINT
# CAROL PRODUCES txin_redeemScript and txin_signature to send to MINT
txin_redeemScript = step0_carol_checksig_redeemscript(alice_privkey.pub)
txin_signature = step2_carol_sign_tx(txin_redeemScript, alice_privkey).scriptSig
txin_redeemScript_b64 = base64.urlsafe_b64encode(txin_redeemScript).decode()
txin_signature_b64 = base64.urlsafe_b64encode(txin_signature).decode()
print(
f"Carol to Bob:\nscript: {txin_redeemScript.__repr__()}\nscript:"
f" {txin_redeemScript_b64}\nsignature: {txin_signature_b64}\n"
)
print("")
# ---------
# MINT verifies SCRIPT and SIGNATURE and mints tokens
# MINT receives txin_redeemScript_b64 and txin_signature_b64 fom CAROL:
txin_redeemScript = CScript(base64.urlsafe_b64decode(txin_redeemScript_b64))
txin_redeemScript_p2sh = txin_p2sh_address.to_redeemScript()
print("Redeem script:", txin_redeemScript.__repr__())
print("P2SH:", txin_redeemScript_p2sh.__repr__())
# txin_redeemScript = CScript([2, 3, OP_LESSTHAN, OP_VERIFY])
txin_signature = CScript(value=base64.urlsafe_b64decode(txin_signature_b64))
txin_p2sh_address = step1_carol_create_p2sh_address(txin_redeemScript)
print(f"Bob recreates secret: P2SH:{txin_p2sh_address}")
# MINT checks that P2SH:txin_p2sh_address has not been spent yet
# ...
tx, _ = step1_bob_carol_create_tx(txin_p2sh_address)
print(
f"Bob verifies:\nscript: {txin_redeemScript_b64}\nsignature:"
f" {txin_signature_b64}\n"
)
script_valid = step3_bob_verify_script(txin_signature, txin_redeemScript, tx)
# MINT redeems tokens and stores P2SH:txin_p2sh_address
# ...
if script_valid:
print("Successfull.")
else:
print("Error.")

View File

@@ -8,7 +8,6 @@ from .crypto.secp import PrivateKey
class SecretKind:
P2SH = "P2SH"
P2PK = "P2PK"
HTLC = "HTLC"

View File

@@ -15,7 +15,6 @@ from ..core.p2pk import (
SigFlags,
verify_p2pk_signature,
)
from ..core.script import verify_bitcoin_script
from ..core.secret import Secret, SecretKind
@@ -23,11 +22,10 @@ class LedgerSpendingConditions:
def _verify_input_spending_conditions(self, proof: Proof) -> bool:
"""
Verify spending conditions:
Condition: P2SH - Witnesses proof.p2shscript
Condition: P2PK - Witness: proof.p2pksigs
Condition: HTLC - Witness: proof.htlcpreimage, proof.htlcsignature
"""
# P2SH
try:
secret = Secret.deserialize(proof.secret)
logger.trace(f"proof.secret: {proof.secret}")
@@ -35,35 +33,6 @@ class LedgerSpendingConditions:
except Exception:
# secret is not a spending condition so we treat is a normal secret
return True
if secret.kind == SecretKind.P2SH:
p2pk_secret = P2PKSecret.from_secret(secret)
# check if locktime is in the past
now = time.time()
if p2pk_secret.locktime and p2pk_secret.locktime < now:
logger.trace(f"p2sh locktime ran out ({p2pk_secret.locktime}<{now}).")
return True
logger.trace(f"p2sh locktime still active ({p2pk_secret.locktime}>{now}).")
if (
proof.p2shscript is None
or proof.p2shscript.script is None
or proof.p2shscript.signature is None
):
# no script present although secret indicates one
raise TransactionError("no script in proof.")
# execute and verify P2SH
txin_p2sh_address, valid = verify_bitcoin_script(
proof.p2shscript.script, proof.p2shscript.signature
)
if not valid:
raise TransactionError("script invalid.")
# check if secret commits to script address
assert secret.data == str(txin_p2sh_address), (
f"secret does not contain correct P2SH address: {secret.data} is not"
f" {txin_p2sh_address}."
)
return True
# P2PK
if secret.kind == SecretKind.P2PK:

View File

@@ -2,7 +2,7 @@ from typing import Dict, List, Optional
from pydantic import BaseModel
from ...core.base import Invoice, P2SHWitness
from ...core.base import Invoice
class PayResponse(BaseModel):
@@ -50,11 +50,11 @@ class PendingResponse(BaseModel):
class LockResponse(BaseModel):
P2SH: Optional[str]
P2PK: Optional[str]
class LocksResponse(BaseModel):
locks: List[P2SHWitness]
locks: List[str]
class InvoicesResponse(BaseModel):

View File

@@ -13,7 +13,7 @@ from ...core.helpers import sum_proofs
from ...core.settings import settings
from ...nostr.nostr.client.client import NostrClient
from ...tor.tor import TorProxy
from ...wallet.crud import get_lightning_invoices, get_reserved_proofs, get_unused_locks
from ...wallet.crud import get_lightning_invoices, get_reserved_proofs
from ...wallet.helpers import (
deserialize_token_from_string,
init_wallet,
@@ -213,7 +213,7 @@ async def balance():
async def send_command(
amount: int = Query(default=..., description="Amount to send"),
nostr: str = Query(default=None, description="Send to nostr pubkey"),
lock: str = Query(default=None, description="Lock tokens (P2SH)"),
lock: str = Query(default=None, description="Lock tokens (P2PK)"),
mint: str = Query(
default=None,
description="Mint URL to send from (None for default mint)",
@@ -354,14 +354,14 @@ async def pending(
@router.get("/lock", name="Generate receiving lock", response_model=LockResponse)
async def lock():
address = await wallet.create_p2sh_address_and_store()
return LockResponse(P2SH=address)
pubkey = await wallet.create_p2pk_pubkey()
return LockResponse(P2PK=pubkey)
@router.get("/locks", name="Show unused receiving locks", response_model=LocksResponse)
async def locks():
locks = await get_unused_locks(db=wallet.db)
return LocksResponse(locks=locks)
pubkey = await wallet.create_p2pk_pubkey()
return LocksResponse(locks=[pubkey])
@router.get(

View File

@@ -23,7 +23,6 @@ from ...wallet.crud import (
get_lightning_invoices,
get_reserved_proofs,
get_seed_and_mnemonic,
get_unused_locks,
)
from ...wallet.wallet import Wallet as Wallet
from ..api.api_server import start_api_server
@@ -351,7 +350,7 @@ async def balance(ctx: Context, verbose):
help="Send to nostr pubkey.",
type=str,
)
@click.option("--lock", "-l", default=None, help="Lock tokens (P2SH).", type=str)
@click.option("--lock", "-l", default=None, help="Lock tokens (P2PK).", type=str)
@click.option(
"--dleq",
"-d",
@@ -583,26 +582,14 @@ async def pending(ctx: Context, legacy, number: int, offset: int):
@cli.command("lock", help="Generate receiving lock.")
@click.option(
"--p2sh",
"-p",
default=False,
is_flag=True,
help="Create P2SH lock.",
type=bool,
)
@click.pass_context
@coro
async def lock(ctx, p2sh):
async def lock(ctx):
wallet: Wallet = ctx.obj["WALLET"]
if p2sh:
address = await wallet.create_p2sh_address_and_store()
lock_str = f"P2SH:{address}"
print("---- Pay to script hash (P2SH) ----\n")
else:
pubkey = await wallet.create_p2pk_pubkey()
lock_str = f"P2PK:{pubkey}"
print("---- Pay to public key (P2PK) ----\n")
pubkey = await wallet.create_p2pk_pubkey()
lock_str = f"P2PK:{pubkey}"
print("---- Pay to public key (P2PK) ----\n")
print("Use a lock to receive tokens that only you can unlock.")
print("")
@@ -626,19 +613,6 @@ async def locks(ctx):
print("---- Pay to public key (P2PK) lock ----\n")
print(f"Lock: {lock_str}")
print("")
print("To see more information enter: cashu lock")
# P2SH locks
locks = await get_unused_locks(db=wallet.db)
if len(locks):
print("")
print("---- Pay to script hash (P2SH) locks ----\n")
for lock in locks:
print(f"Lock: P2SH:{lock.address}")
print(f"Script: {lock.script}")
print(f"Signature: {lock.signature}")
print("")
print("--------------------------\n")
return True

View File

@@ -2,7 +2,7 @@ import json
import time
from typing import Any, List, Optional, Tuple
from ..core.base import Invoice, P2SHWitness, Proof, WalletKeyset
from ..core.base import Invoice, Proof, WalletKeyset
from ..core.db import Connection, Database
@@ -122,71 +122,6 @@ async def secret_used(
return rows is not None
async def store_p2sh(
p2sh: P2SHWitness,
db: Database,
conn: Optional[Connection] = None,
) -> None:
await (conn or db).execute(
"""
INSERT INTO p2sh
(address, script, signature, used)
VALUES (?, ?, ?, ?)
""",
(
p2sh.address,
p2sh.script,
p2sh.signature,
False,
),
)
async def get_unused_locks(
address: str = "",
db: Optional[Database] = None,
conn: Optional[Connection] = None,
) -> List[P2SHWitness]:
clause: List[str] = []
args: List[str] = []
clause.append("used = 0")
if address:
clause.append("address = ?")
args.append(address)
where = ""
if clause:
where = f"WHERE {' AND '.join(clause)}"
rows = await (conn or db).fetchall( # type: ignore
f"""
SELECT * from p2sh
{where}
""",
tuple(args),
)
return [P2SHWitness(**r) for r in rows]
async def update_p2sh_used(
p2sh: P2SHWitness,
used: bool,
db: Optional[Database] = None,
conn: Optional[Connection] = None,
) -> None:
clauses = []
values = []
clauses.append("used = ?")
values.append(used)
await (conn or db).execute( # type: ignore
f"UPDATE proofs SET {', '.join(clauses)} WHERE address = ?",
(*values, str(p2sh.address)),
)
async def store_keyset(
keyset: WalletKeyset,
mint_url: str = "",

View File

@@ -176,25 +176,20 @@ async def send(
assert len(lock) > 21, Exception(
"Error: lock has to be at least 22 characters long."
)
if not lock.startswith("P2SH:") and not lock.startswith("P2PK:"):
raise Exception("Error: lock has to start with P2SH: or P2PK:")
if not lock.startswith("P2PK:"):
raise Exception("Error: lock has to start with P2PK:")
# we add a time lock to the P2PK lock by appending the current unix time + 14 days
if lock.startswith("P2PK:") or lock.startswith("P2SH:"):
else:
logger.debug(f"Locking token to: {lock}")
logger.debug(
f"Adding a time lock of {settings.locktime_delta_seconds} seconds."
)
if lock.startswith("P2SH:"):
secret_lock = await wallet.create_p2sh_lock(
lock.split(":")[1], locktime=settings.locktime_delta_seconds
)
elif lock.startswith("P2PK:"):
secret_lock = await wallet.create_p2pk_lock(
lock.split(":")[1],
locktime_seconds=settings.locktime_delta_seconds,
sig_all=True,
n_sigs=1,
)
secret_lock = await wallet.create_p2pk_lock(
lock.split(":")[1],
locktime_seconds=settings.locktime_delta_seconds,
sig_all=True,
n_sigs=1,
)
await wallet.load_proofs()
if split:

View File

@@ -73,19 +73,19 @@ async def m003_add_proofs_sendid_and_timestamps(db: Database):
async def m004_p2sh_locks(db: Database):
"""
Stores P2SH addresses and unlock scripts.
DEPRECATED: Stores P2SH addresses and unlock scripts.
"""
await db.execute("""
CREATE TABLE IF NOT EXISTS p2sh (
address TEXT NOT NULL,
script TEXT NOT NULL,
signature TEXT NOT NULL,
used BOOL NOT NULL,
# await db.execute("""
# CREATE TABLE IF NOT EXISTS p2sh (
# address TEXT NOT NULL,
# script TEXT NOT NULL,
# signature TEXT NOT NULL,
# used BOOL NOT NULL,
UNIQUE (address, script, signature)
# UNIQUE (address, script, signature)
);
""")
# );
# """)
async def m005_wallet_keysets(db: Database):

View File

@@ -1,4 +1,3 @@
import base64
from datetime import datetime, timedelta
from typing import List, Optional
@@ -8,7 +7,6 @@ from ..core import bolt11 as bolt11
from ..core.base import (
BlindedMessage,
P2PKWitness,
P2SHWitness,
Proof,
)
from ..core.crypto.secp import PrivateKey
@@ -18,41 +16,14 @@ from ..core.p2pk import (
SigFlags,
sign_p2pk_sign,
)
from ..core.script import (
step0_carol_checksig_redeemscript,
step0_carol_privkey,
step1_carol_create_p2sh_address,
step2_carol_sign_tx,
)
from ..core.secret import Secret, SecretKind, Tags
from ..wallet.crud import (
get_unused_locks,
store_p2sh,
)
from .protocols import SupportsDb, SupportsPrivateKey
class WalletP2PK(SupportsPrivateKey, SupportsDb):
db: Database
private_key: Optional[PrivateKey] = None
# ---------- P2SH and P2PK ----------
async def create_p2sh_address_and_store(self) -> str:
"""Creates a P2SH lock script and stores the script and signature in the database."""
alice_privkey = step0_carol_privkey()
txin_redeemScript = step0_carol_checksig_redeemscript(alice_privkey.pub)
txin_p2sh_address = step1_carol_create_p2sh_address(txin_redeemScript)
txin_signature = step2_carol_sign_tx(txin_redeemScript, alice_privkey).scriptSig
txin_redeemScript_b64 = base64.urlsafe_b64encode(txin_redeemScript).decode()
txin_signature_b64 = base64.urlsafe_b64encode(txin_signature).decode()
p2shScript = P2SHWitness(
script=txin_redeemScript_b64,
signature=txin_signature_b64,
address=str(txin_p2sh_address),
)
await store_p2sh(p2shScript, db=self.db)
assert p2shScript.address
return p2shScript.address
# ---------- P2PK ----------
async def create_p2pk_pubkey(self):
assert (
@@ -89,23 +60,6 @@ class WalletP2PK(SupportsPrivateKey, SupportsDb):
tags=tags,
)
async def create_p2sh_lock(
self,
address: str,
locktime: Optional[int] = None,
tags: Tags = Tags(),
) -> Secret:
if locktime:
tags["locktime"] = str(
(datetime.now() + timedelta(seconds=locktime)).timestamp()
)
return Secret(
kind=SecretKind.P2SH,
data=address,
tags=tags,
)
async def sign_p2pk_proofs(self, proofs: List[Proof]) -> List[str]:
assert (
self.private_key
@@ -187,24 +141,6 @@ class WalletP2PK(SupportsPrivateKey, SupportsDb):
outputs = await self.add_p2pk_witnesses_to_outputs(outputs)
return outputs
async def add_p2sh_witnesses_to_proofs(
self: SupportsDb, proofs: List[Proof]
) -> List[Proof]:
# Quirk: we use a single P2SH script and signature pair for all tokens in proofs
address = Secret.deserialize(proofs[0].secret).data
p2shscripts = await get_unused_locks(address, db=self.db)
assert len(p2shscripts) == 1, Exception("lock not found.")
p2sh_script, p2sh_signature = (
p2shscripts[0].script,
p2shscripts[0].signature,
)
logger.debug(f"Unlock script: {p2sh_script} signature: {p2sh_signature}")
# attach unlock scripts to proofs
for p in proofs:
p.witness = P2SHWitness(script=p2sh_script, signature=p2sh_signature).json()
return proofs
async def add_p2pk_witnesses_to_proofs(self, proofs: List[Proof]) -> List[Proof]:
p2pk_signatures = await self.sign_p2pk_proofs(proofs)
logger.debug(f"Unlock signatures for {len(proofs)} proofs: {p2pk_signatures}")
@@ -221,14 +157,12 @@ class WalletP2PK(SupportsPrivateKey, SupportsDb):
return proofs
async def add_witnesses_to_proofs(self, proofs: List[Proof]) -> List[Proof]:
"""Adds witnesses to proofs for P2SH or P2PK redemption.
"""Adds witnesses to proofs for P2PK redemption.
This method parses the secret of each proof and determines the correct
witness type and adds it to the proof if we have it available.
Note: In order for this method to work, all proofs must have the same secret type.
This is because we use a single P2SH script and signature pair for all tokens in proofs.
For P2PK, we use an individual signature for each token in proofs.
Args:
@@ -248,15 +182,8 @@ class WalletP2PK(SupportsPrivateKey, SupportsDb):
# if not, we do not add witnesses (treat as regular token secret)
return proofs
logger.debug("Spending conditions detected.")
# P2SH scripts
if all([Secret.deserialize(p.secret).kind == SecretKind.P2SH for p in proofs]):
logger.debug("P2SH redemption detected.")
proofs = await self.add_p2sh_witnesses_to_proofs(proofs)
# P2PK signatures
elif all(
[Secret.deserialize(p.secret).kind == SecretKind.P2PK for p in proofs]
):
if all([Secret.deserialize(p.secret).kind == SecretKind.P2PK for p in proofs]):
logger.debug("P2PK redemption detected.")
proofs = await self.add_p2pk_witnesses_to_proofs(proofs)

View File

@@ -707,7 +707,7 @@ class Wallet(LedgerAPI, WalletP2PK, WalletHTLC, WalletSecrets):
if secret_lock is None:
secrets, rs, derivation_paths = await self.generate_n_secrets(len(amounts))
else:
# NOTE: we use random blinding factors for P2SH, we won't be able to
# NOTE: we use random blinding factors for locks, we won't be able to
# restore these tokens from a backup
rs = []
# generate secrets for receiver

96
poetry.lock generated
View File

@@ -1,9 +1,10 @@
# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand.
# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand.
[[package]]
name = "anyio"
version = "3.7.1"
description = "High level compatibility layer for multiple asynchronous event loop implementations"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -25,6 +26,7 @@ trio = ["trio (<0.22)"]
name = "asn1crypto"
version = "1.5.1"
description = "Fast ASN.1 parser and serializer with definitions for private keys, public keys, certificates, CRL, OCSP, CMS, PKCS#3, PKCS#7, PKCS#8, PKCS#12, PKCS#5, X.509 and TSP"
category = "main"
optional = false
python-versions = "*"
files = [
@@ -36,6 +38,7 @@ files = [
name = "attrs"
version = "23.1.0"
description = "Classes Without Boilerplate"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -54,6 +57,7 @@ tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pyte
name = "base58"
version = "2.1.1"
description = "Base58 and Base58Check implementation."
category = "main"
optional = false
python-versions = ">=3.5"
files = [
@@ -68,6 +72,7 @@ tests = ["PyHamcrest (>=2.0.2)", "mypy", "pytest (>=4.6)", "pytest-benchmark", "
name = "bech32"
version = "1.2.0"
description = "Reference implementation for Bech32 and segwit addresses."
category = "main"
optional = false
python-versions = ">=3.5"
files = [
@@ -79,6 +84,7 @@ files = [
name = "bip32"
version = "3.4"
description = "Minimalistic implementation of the BIP32 key derivation scheme"
category = "main"
optional = false
python-versions = "*"
files = [
@@ -94,6 +100,7 @@ coincurve = ">=15.0,<19"
name = "bitstring"
version = "3.1.9"
description = "Simple construction, analysis and modification of binary data."
category = "main"
optional = false
python-versions = "*"
files = [
@@ -106,6 +113,7 @@ files = [
name = "black"
version = "23.7.0"
description = "The uncompromising code formatter."
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@@ -152,6 +160,7 @@ uvloop = ["uvloop (>=0.15.2)"]
name = "certifi"
version = "2023.7.22"
description = "Python package for providing Mozilla's CA Bundle."
category = "main"
optional = false
python-versions = ">=3.6"
files = [
@@ -163,6 +172,7 @@ files = [
name = "cffi"
version = "1.15.1"
description = "Foreign Function Interface for Python calling C code."
category = "main"
optional = false
python-versions = "*"
files = [
@@ -239,6 +249,7 @@ pycparser = "*"
name = "cfgv"
version = "3.4.0"
description = "Validate configuration and produce human readable error messages."
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@@ -250,6 +261,7 @@ files = [
name = "charset-normalizer"
version = "3.2.0"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
category = "main"
optional = false
python-versions = ">=3.7.0"
files = [
@@ -334,6 +346,7 @@ files = [
name = "click"
version = "8.1.7"
description = "Composable command line interface toolkit"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -348,6 +361,7 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""}
name = "coincurve"
version = "18.0.0"
description = "Cross-platform Python CFFI bindings for libsecp256k1"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -403,6 +417,7 @@ cffi = ">=1.3.0"
name = "colorama"
version = "0.4.6"
description = "Cross-platform colored terminal text."
category = "main"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
files = [
@@ -414,6 +429,7 @@ files = [
name = "coverage"
version = "7.3.0"
description = "Code coverage measurement for Python"
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@@ -481,6 +497,7 @@ toml = ["tomli"]
name = "cryptography"
version = "41.0.3"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -526,6 +543,7 @@ test-randomorder = ["pytest-randomly"]
name = "distlib"
version = "0.3.7"
description = "Distribution utilities"
category = "dev"
optional = false
python-versions = "*"
files = [
@@ -537,6 +555,7 @@ files = [
name = "ecdsa"
version = "0.18.0"
description = "ECDSA cryptographic signature library (pure python)"
category = "main"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
files = [
@@ -555,6 +574,7 @@ gmpy2 = ["gmpy2"]
name = "environs"
version = "9.5.0"
description = "simplified environment variable parsing"
category = "main"
optional = false
python-versions = ">=3.6"
files = [
@@ -576,6 +596,7 @@ tests = ["dj-database-url", "dj-email-url", "django-cache-url", "pytest"]
name = "exceptiongroup"
version = "1.1.3"
description = "Backport of PEP 654 (exception groups)"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -590,6 +611,7 @@ test = ["pytest (>=6)"]
name = "fastapi"
version = "0.101.1"
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -609,6 +631,7 @@ all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)"
name = "filelock"
version = "3.12.2"
description = "A platform independent file lock."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@@ -624,6 +647,7 @@ testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "p
name = "h11"
version = "0.14.0"
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -635,6 +659,7 @@ files = [
name = "httpcore"
version = "0.17.3"
description = "A minimal low-level HTTP client."
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -646,16 +671,17 @@ files = [
anyio = ">=3.0,<5.0"
certifi = "*"
h11 = ">=0.13,<0.15"
sniffio = "==1.*"
sniffio = ">=1.0.0,<2.0.0"
[package.extras]
http2 = ["h2 (>=3,<5)"]
socks = ["socksio (==1.*)"]
socks = ["socksio (>=1.0.0,<2.0.0)"]
[[package]]
name = "httpx"
version = "0.24.1"
description = "The next generation HTTP client."
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -671,14 +697,15 @@ sniffio = "*"
[package.extras]
brotli = ["brotli", "brotlicffi"]
cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"]
cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<14)"]
http2 = ["h2 (>=3,<5)"]
socks = ["socksio (==1.*)"]
socks = ["socksio (>=1.0.0,<2.0.0)"]
[[package]]
name = "identify"
version = "2.5.27"
description = "File identification library for Python"
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@@ -693,6 +720,7 @@ license = ["ukkonen"]
name = "idna"
version = "3.4"
description = "Internationalized Domain Names in Applications (IDNA)"
category = "main"
optional = false
python-versions = ">=3.5"
files = [
@@ -704,6 +732,7 @@ files = [
name = "importlib-metadata"
version = "6.8.0"
description = "Read metadata from Python packages"
category = "main"
optional = false
python-versions = ">=3.8"
files = [
@@ -723,6 +752,7 @@ testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs
name = "iniconfig"
version = "2.0.0"
description = "brain-dead simple config-ini parsing"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@@ -734,6 +764,7 @@ files = [
name = "loguru"
version = "0.7.0"
description = "Python logging made (stupidly) simple"
category = "main"
optional = false
python-versions = ">=3.5"
files = [
@@ -752,6 +783,7 @@ dev = ["Sphinx (==5.3.0)", "colorama (==0.4.5)", "colorama (==0.4.6)", "freezegu
name = "marshmallow"
version = "3.20.1"
description = "A lightweight library for converting complex datatypes to and from native Python datatypes."
category = "main"
optional = false
python-versions = ">=3.8"
files = [
@@ -772,6 +804,7 @@ tests = ["pytest", "pytz", "simplejson"]
name = "mnemonic"
version = "0.20"
description = "Implementation of Bitcoin BIP-0039"
category = "main"
optional = false
python-versions = ">=3.5"
files = [
@@ -783,6 +816,7 @@ files = [
name = "mypy"
version = "1.5.1"
description = "Optional static typing for Python"
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@@ -829,6 +863,7 @@ reports = ["lxml"]
name = "mypy-extensions"
version = "1.0.0"
description = "Type system extensions for programs checked with the mypy type checker."
category = "dev"
optional = false
python-versions = ">=3.5"
files = [
@@ -840,6 +875,7 @@ files = [
name = "nodeenv"
version = "1.8.0"
description = "Node.js virtual environment builder"
category = "dev"
optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*"
files = [
@@ -854,6 +890,7 @@ setuptools = "*"
name = "outcome"
version = "1.2.0"
description = "Capture the outcome of Python function calls."
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -868,6 +905,7 @@ attrs = ">=19.2.0"
name = "packaging"
version = "23.1"
description = "Core utilities for Python packages"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -879,6 +917,7 @@ files = [
name = "pathspec"
version = "0.11.2"
description = "Utility library for gitignore style pattern matching of file paths."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@@ -890,6 +929,7 @@ files = [
name = "platformdirs"
version = "3.10.0"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@@ -905,6 +945,7 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-co
name = "pluggy"
version = "1.2.0"
description = "plugin and hook calling mechanisms for python"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@@ -920,6 +961,7 @@ testing = ["pytest", "pytest-benchmark"]
name = "pre-commit"
version = "3.3.3"
description = "A framework for managing and maintaining multi-language pre-commit hooks."
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@@ -938,6 +980,7 @@ virtualenv = ">=20.10.0"
name = "psycopg2-binary"
version = "2.9.7"
description = "psycopg2 - Python-PostgreSQL Database Adapter"
category = "main"
optional = true
python-versions = ">=3.6"
files = [
@@ -1007,6 +1050,7 @@ files = [
name = "pycparser"
version = "2.21"
description = "C parser in Python"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
files = [
@@ -1018,6 +1062,7 @@ files = [
name = "pycryptodomex"
version = "3.18.0"
description = "Cryptographic library for Python"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
files = [
@@ -1059,6 +1104,7 @@ files = [
name = "pydantic"
version = "1.10.12"
description = "Data validation and settings management using python type hints"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -1111,6 +1157,7 @@ email = ["email-validator (>=1.0.3)"]
name = "pysocks"
version = "1.7.1"
description = "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information."
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
files = [
@@ -1123,6 +1170,7 @@ files = [
name = "pytest"
version = "7.4.0"
description = "pytest: simple powerful testing with Python"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@@ -1145,6 +1193,7 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no
name = "pytest-asyncio"
version = "0.21.1"
description = "Pytest support for asyncio"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@@ -1163,6 +1212,7 @@ testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy
name = "pytest-cov"
version = "4.1.0"
description = "Pytest plugin for measuring coverage."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@@ -1177,21 +1227,11 @@ pytest = ">=4.6"
[package.extras]
testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"]
[[package]]
name = "python-bitcoinlib"
version = "0.12.2"
description = "The Swiss Army Knife of the Bitcoin protocol."
optional = false
python-versions = "*"
files = [
{file = "python-bitcoinlib-0.12.2.tar.gz", hash = "sha256:c65ab61427c77c38d397bfc431f71d86fd355b453a536496ec3fcb41bd10087d"},
{file = "python_bitcoinlib-0.12.2-py3-none-any.whl", hash = "sha256:2f29a9f475f21c12169b3a6cc8820f34f11362d7ff1200a5703dce3e4e903a44"},
]
[[package]]
name = "python-dotenv"
version = "1.0.0"
description = "Read key-value pairs from a .env file and set them as environment variables"
category = "main"
optional = false
python-versions = ">=3.8"
files = [
@@ -1206,6 +1246,7 @@ cli = ["click (>=5.0)"]
name = "pyyaml"
version = "6.0.1"
description = "YAML parser and emitter for Python"
category = "dev"
optional = false
python-versions = ">=3.6"
files = [
@@ -1255,6 +1296,7 @@ files = [
name = "represent"
version = "1.6.0.post0"
description = "Create __repr__ automatically or declaratively."
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
files = [
@@ -1272,6 +1314,7 @@ test = ["ipython", "mock", "pytest (>=3.0.5)"]
name = "requests"
version = "2.31.0"
description = "Python HTTP for Humans."
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -1293,6 +1336,7 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
name = "ruff"
version = "0.0.284"
description = "An extremely fast Python linter, written in Rust."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@@ -1319,6 +1363,7 @@ files = [
name = "secp256k1"
version = "0.14.0"
description = "FFI bindings to libsecp256k1"
category = "main"
optional = false
python-versions = "*"
files = [
@@ -1354,6 +1399,7 @@ cffi = ">=1.3.0"
name = "setuptools"
version = "68.1.2"
description = "Easily download, build, install, upgrade, and uninstall Python packages"
category = "main"
optional = false
python-versions = ">=3.8"
files = [
@@ -1370,6 +1416,7 @@ testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (
name = "six"
version = "1.16.0"
description = "Python 2 and 3 compatibility utilities"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
files = [
@@ -1381,6 +1428,7 @@ files = [
name = "sniffio"
version = "1.3.0"
description = "Sniff out which async library your code is running under"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -1392,6 +1440,7 @@ files = [
name = "sqlalchemy"
version = "1.3.24"
description = "Database Abstraction Library"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
files = [
@@ -1447,6 +1496,7 @@ pymysql = ["pymysql", "pymysql (<1)"]
name = "sqlalchemy-aio"
version = "0.17.0"
description = "Async support for SQLAlchemy."
category = "main"
optional = false
python-versions = ">=3.6"
files = [
@@ -1468,6 +1518,7 @@ trio = ["trio (>=0.15)"]
name = "starlette"
version = "0.27.0"
description = "The little ASGI library that shines."
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -1486,6 +1537,7 @@ full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyam
name = "tomli"
version = "2.0.1"
description = "A lil' TOML parser"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@@ -1497,6 +1549,7 @@ files = [
name = "types-requests"
version = "2.31.0.2"
description = "Typing stubs for requests"
category = "dev"
optional = false
python-versions = "*"
files = [
@@ -1511,6 +1564,7 @@ types-urllib3 = "*"
name = "types-urllib3"
version = "1.26.25.14"
description = "Typing stubs for urllib3"
category = "dev"
optional = false
python-versions = "*"
files = [
@@ -1522,6 +1576,7 @@ files = [
name = "typing-extensions"
version = "4.7.1"
description = "Backported and Experimental Type Hints for Python 3.7+"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -1533,6 +1588,7 @@ files = [
name = "urllib3"
version = "2.0.4"
description = "HTTP library with thread-safe connection pooling, file post, and more."
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -1550,6 +1606,7 @@ zstd = ["zstandard (>=0.18.0)"]
name = "uvicorn"
version = "0.18.3"
description = "The lightning-fast ASGI server."
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -1568,6 +1625,7 @@ standard = ["colorama (>=0.4)", "httptools (>=0.4.0)", "python-dotenv (>=0.13)",
name = "virtualenv"
version = "20.24.3"
description = "Virtual Python Environment builder"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@@ -1588,6 +1646,7 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess
name = "websocket-client"
version = "1.6.2"
description = "WebSocket client for Python with low level API options"
category = "main"
optional = false
python-versions = ">=3.8"
files = [
@@ -1604,6 +1663,7 @@ test = ["websockets"]
name = "wheel"
version = "0.41.2"
description = "A built-package format for Python"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@@ -1618,6 +1678,7 @@ test = ["pytest (>=6.0.0)", "setuptools (>=65)"]
name = "win32-setctime"
version = "1.1.0"
description = "A small Python utility to set file creation time on Windows"
category = "main"
optional = false
python-versions = ">=3.5"
files = [
@@ -1632,6 +1693,7 @@ dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"]
name = "zipp"
version = "3.16.2"
description = "Backport of pathlib-compatible object wrapper for zip files"
category = "main"
optional = false
python-versions = ">=3.8"
files = [
@@ -1649,4 +1711,4 @@ pgsql = ["psycopg2-binary"]
[metadata]
lock-version = "2.0"
python-versions = "^3.8.1"
content-hash = "b801dad77ac4c6b9aca8f25c4fe4d8bab9cb998a2e12d91d9bbf69cb66bb3085"
content-hash = "c8a62986bb458c849aabdd3e2f1e1534e4af6093863177fc27630fc9daa5410a"

View File

@@ -20,7 +20,6 @@ ecdsa = "^0.18.0"
bitstring = "^3.1.9"
secp256k1 = "^0.14.0"
sqlalchemy-aio = "^0.17.0"
python-bitcoinlib = "^0.12.2"
h11 = "^0.14.0"
PySocks = "^1.7.1"
cryptography = "^41.0.3"

View File

@@ -30,7 +30,6 @@ pycparser==2.21 ; python_full_version >= "3.8.1" and python_full_version < "4.0.
pycryptodomex==3.18.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
pydantic==1.10.12 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
pysocks==1.7.1 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
python-bitcoinlib==0.12.2 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
python-dotenv==1.0.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
represent==1.6.0.post0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"
requests==2.31.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0"

View File

@@ -1,88 +0,0 @@
import secrets
from typing import List
import pytest
import pytest_asyncio
from cashu.core.base import Proof
from cashu.core.crypto.secp import PrivateKey, PublicKey
from cashu.core.helpers import sum_proofs
from cashu.core.migrations import migrate_databases
from cashu.wallet import migrations
from cashu.wallet.wallet import Wallet
from cashu.wallet.wallet import Wallet as Wallet1
from cashu.wallet.wallet import Wallet as Wallet2
from tests.conftest import SERVER_ENDPOINT
async def assert_err(f, msg):
"""Compute f() and expect an error message 'msg'."""
try:
await f
except Exception as exc:
if str(exc.args[0]) != msg:
raise Exception(f"Expected error: {msg}, got: {exc.args[0]}")
return
raise Exception(f"Expected error: {msg}, got no error")
def assert_amt(proofs: List[Proof], expected: int):
"""Assert amounts the proofs contain."""
assert [p.amount for p in proofs] == expected
@pytest_asyncio.fixture(scope="function")
async def wallet1(mint):
wallet1 = await Wallet1.with_db(
SERVER_ENDPOINT, "test_data/wallet_p2sh_1", "wallet1"
)
await migrate_databases(wallet1.db, migrations)
await wallet1.load_mint()
wallet1.status()
yield wallet1
@pytest_asyncio.fixture(scope="function")
async def wallet2(mint):
wallet2 = await Wallet2.with_db(
SERVER_ENDPOINT, "test_data/wallet_p2sh_2", "wallet2"
)
await migrate_databases(wallet2.db, migrations)
wallet2.private_key = PrivateKey(secrets.token_bytes(32), raw=True)
await wallet2.load_mint()
wallet2.status()
yield wallet2
@pytest.mark.asyncio
async def test_create_p2pk_pubkey(wallet1: Wallet):
invoice = await wallet1.request_mint(64)
await wallet1.mint(64, hash=invoice.hash)
pubkey = await wallet1.create_p2pk_pubkey()
PublicKey(bytes.fromhex(pubkey), raw=True)
@pytest.mark.asyncio
async def test_p2sh(wallet1: Wallet, wallet2: Wallet):
invoice = await wallet1.request_mint(64)
await wallet1.mint(64, hash=invoice.hash)
_ = await wallet1.create_p2sh_address_and_store() # receiver side
_, send_proofs = await wallet1.split_to_send(wallet1.proofs, 8) # sender side
frst_proofs, scnd_proofs = await wallet2.redeem(send_proofs) # receiver side
assert len(frst_proofs) == 0
assert len(scnd_proofs) == 1
assert sum_proofs(scnd_proofs) == 8
assert wallet2.balance == 8
@pytest.mark.asyncio
async def test_p2sh_receive_with_wrong_wallet(wallet1: Wallet, wallet2: Wallet):
invoice = await wallet1.request_mint(64)
await wallet1.mint(64, hash=invoice.hash)
wallet1_address = await wallet1.create_p2sh_address_and_store() # receiver side
secret_lock = await wallet1.create_p2sh_lock(wallet1_address) # sender side
_, send_proofs = await wallet1.split_to_send(
wallet1.proofs, 8, secret_lock
) # sender side
await assert_err(wallet2.redeem(send_proofs), "lock not found.") # wrong receiver