[Wallet/mint] P2PK with timelocks (#270)

* p2pk with nostr privatekey and timelocks

* add p2pk

* fix test

* fix test with custom secret

* sign whole split transaction

* p2pk signature now commits to entire secret and thus to a nonce

* use schnorr signatures

* revamp P2SH and P2PK with new Secret model

* test p2pk

* add comments

* add nostr private key to tests

* fix nostr receive

* make format

* test redemption after timelock

* refactor Server.serialize()

* sign sha256(secret)

* add optional refund pubkey that triggers after timelock

* use nostr private key for now (including nsec parser)

* use nostr private key and fix tests

* bump version to 0.12.2
This commit is contained in:
callebtc
2023-07-02 01:56:05 +02:00
committed by GitHub
parent 4beaf8ff41
commit 01d498309b
15 changed files with 548 additions and 231 deletions

View File

@@ -17,7 +17,7 @@ TOR=TRUE
# NOSTR # NOSTR
# nostr private key to which to receive tokens to # nostr private key to which to receive tokens to
NOSTR_PRIVATE_KEY=nostr_privatekey_here_hex_or_bech32 NOSTR_PRIVATE_KEY=nostr_privatekey_here_hex_or_bech32_nsec
# nostr relays (comma separated list) # nostr relays (comma separated list)
NOSTR_RELAYS=["wss://nostr-pub.wellorder.net"] NOSTR_RELAYS=["wss://nostr-pub.wellorder.net"]

View File

@@ -114,7 +114,7 @@ cashu info
Returns: Returns:
```bash ```bash
Version: 0.12.1 Version: 0.12.2
Debug: False Debug: False
Cashu dir: /home/user/.cashu Cashu dir: /home/user/.cashu
Wallet: wallet Wallet: wallet

View File

@@ -9,13 +9,62 @@ from pydantic import BaseModel
from .crypto.keys import derive_keys, derive_keyset_id, derive_pubkeys from .crypto.keys import derive_keys, derive_keyset_id, derive_pubkeys
from .crypto.secp import PrivateKey, PublicKey from .crypto.secp import PrivateKey, PublicKey
from .legacy import derive_keys_backwards_compatible_insecure_pre_0_12 from .legacy import derive_keys_backwards_compatible_insecure_pre_0_12
from .p2pk import sign_p2pk_sign
# ------- PROOFS ------- # ------- PROOFS -------
class SecretKind:
P2SH = "P2SH"
P2PK = "P2PK"
class Tags(BaseModel):
__root__: List[List[str]]
def get_tag(self, tag_name: str) -> Union[str, None]:
for tag in self.__root__:
if tag[0] == tag_name:
return tag[1]
return None
class Secret(BaseModel):
"""Describes spending condition encoded in the secret field of a Proof."""
kind: str
data: str
nonce: Union[None, str] = None
timelock: Union[None, int] = None
tags: Union[None, Tags] = None
def serialize(self) -> str:
data_dict: Dict[str, Any] = {
"data": self.data,
"nonce": self.nonce or PrivateKey().serialize()[:32],
}
if self.timelock:
data_dict["timelock"] = self.timelock
if self.tags:
data_dict["tags"] = self.tags.__root__
logger.debug(
json.dumps(
[self.kind, data_dict],
)
)
return json.dumps(
[self.kind, data_dict],
)
@classmethod
def deserialize(cls, data: str):
kind, kwargs = json.loads(data)
return cls(kind=kind, **kwargs)
class P2SHScript(BaseModel): class P2SHScript(BaseModel):
""" """
Describes spending condition of a Proof Unlocks P2SH spending condition of a Proof
""" """
script: str script: str
@@ -34,7 +83,8 @@ class Proof(BaseModel):
amount: int = 0 amount: int = 0
secret: str = "" # secret or message to be blinded and signed secret: str = "" # secret or message to be blinded and signed
C: str = "" # signature on secret, unblinded by wallet C: str = "" # signature on secret, unblinded by wallet
script: Union[P2SHScript, None] = None # P2SH spending condition p2pksig: Optional[str] = None # P2PK signature
p2shscript: Union[P2SHScript, None] = None # P2SH spending condition
reserved: Union[ reserved: Union[
None, bool None, bool
] = False # whether this proof is reserved for sending, used for coin management in the wallet ] = False # whether this proof is reserved for sending, used for coin management in the wallet
@@ -174,6 +224,19 @@ class PostSplitRequest(BaseModel):
proofs: List[Proof] proofs: List[Proof]
amount: int amount: int
outputs: List[BlindedMessage] outputs: List[BlindedMessage]
# signature: Optional[str] = None
# def sign(self, private_key: PrivateKey):
# """
# Create a signed split request. The signature is over the `proofs` and `outputs` fields.
# """
# # message = json.dumps(self.proofs).encode("utf-8") + json.dumps(
# # self.outputs
# # ).encode("utf-8")
# message = json.dumps(self.dict(include={"proofs": ..., "outputs": ...})).encode(
# "utf-8"
# )
# self.signature = sign_p2pk_sign(message, private_key)
class PostSplitResponse(BaseModel): class PostSplitResponse(BaseModel):

39
cashu/core/p2pk.py Normal file
View File

@@ -0,0 +1,39 @@
import hashlib
from cashu.core.crypto.secp import PrivateKey, PublicKey
def sign_p2pk_sign(message: bytes, private_key: PrivateKey):
# ecdsa version
# signature = private_key.ecdsa_serialize(private_key.ecdsa_sign(message))
signature = private_key.schnorr_sign(
hashlib.sha256(message).digest(), None, raw=True
)
return signature.hex()
def verify_p2pk_signature(message: bytes, pubkey: PublicKey, signature: bytes):
# ecdsa version
# return pubkey.ecdsa_verify(message, pubkey.ecdsa_deserialize(signature))
return pubkey.schnorr_verify(
hashlib.sha256(message).digest(), signature, None, raw=True
)
if __name__ == "__main__":
# generate keys
private_key_bytes = b"12300000000000000000000000000123"
private_key = PrivateKey(private_key_bytes, raw=True)
print(private_key.serialize())
public_key = private_key.pubkey
assert public_key
print(public_key.serialize().hex())
# sign message (=pubkey)
message = public_key.serialize()
signature = private_key.ecdsa_serialize(private_key.ecdsa_sign(message))
print(signature.hex())
# verify
pubkey_verify = PublicKey(message, raw=True)
print(public_key.ecdsa_verify(message, pubkey_verify.ecdsa_deserialize(signature)))

View File

@@ -79,28 +79,28 @@ def step3_bob_verify_script(txin_signature, txin_redeemScript, tx):
raise Exception("Script execution failed:", e) raise Exception("Script execution failed:", e)
def verify_script(txin_redeemScript_b64, txin_signature_b64): def verify_bitcoin_script(txin_redeemScript_b64, txin_signature_b64):
txin_redeemScript = CScript(base64.urlsafe_b64decode(txin_redeemScript_b64)) txin_redeemScript = CScript(base64.urlsafe_b64decode(txin_redeemScript_b64))
print("Redeem script:", txin_redeemScript.__repr__()) # print("Redeem script:", txin_redeemScript.__repr__())
# txin_redeemScript = CScript([2, 3, OP_LESSTHAN, OP_VERIFY]) # txin_redeemScript = CScript([2, 3, OP_LESSTHAN, OP_VERIFY])
txin_signature = CScript(value=base64.urlsafe_b64decode(txin_signature_b64)) txin_signature = CScript(value=base64.urlsafe_b64decode(txin_signature_b64))
txin_p2sh_address = step1_carol_create_p2sh_address(txin_redeemScript) txin_p2sh_address = step1_carol_create_p2sh_address(txin_redeemScript)
print(f"Bob recreates secret: P2SH:{txin_p2sh_address}") # print(f"Bob recreates secret: P2SH:{txin_p2sh_address}")
# MINT checks that P2SH:txin_p2sh_address has not been spent yet # MINT checks that P2SH:txin_p2sh_address has not been spent yet
# ... # ...
tx, _ = step1_bob_carol_create_tx(txin_p2sh_address) tx, _ = step1_bob_carol_create_tx(txin_p2sh_address)
print( # print(
f"Bob verifies:\nscript: {txin_redeemScript_b64}\nsignature: {txin_signature_b64}\n" # f"Bob verifies:\nscript: {txin_redeemScript_b64}\nsignature: {txin_signature_b64}\n"
) # )
script_valid = step3_bob_verify_script(txin_signature, txin_redeemScript, tx) script_valid = step3_bob_verify_script(txin_signature, txin_redeemScript, tx)
# MINT redeems tokens and stores P2SH:txin_p2sh_address # MINT redeems tokens and stores P2SH:txin_p2sh_address
# ... # ...
if script_valid: # if script_valid:
print("Successfull.") # print("Successfull.")
else: # else:
print("Error.") # print("Error.")
return txin_p2sh_address, script_valid return txin_p2sh_address, script_valid

View File

@@ -8,7 +8,7 @@ from pydantic import BaseSettings, Extra, Field
env = Env() env = Env()
VERSION = "0.12.1" VERSION = "0.12.2"
def find_env_file(): def find_env_file():
@@ -98,9 +98,15 @@ class WalletSettings(CashuSettings):
] ]
) )
timelock_delta_seconds: int = Field(default=86400) # 1 day
class Settings( class Settings(
EnvSettings, MintSettings, MintInformation, WalletSettings, CashuSettings EnvSettings,
MintSettings,
MintInformation,
WalletSettings,
CashuSettings,
): ):
version: str = Field(default=VERSION) version: str = Field(default=VERSION)

View File

@@ -1,5 +1,7 @@
import asyncio import asyncio
import json
import math import math
import time
from typing import Dict, List, Literal, Optional, Set, Union from typing import Dict, List, Literal, Optional, Set, Union
from loguru import logger from loguru import logger
@@ -12,13 +14,16 @@ from ..core.base import (
MintKeyset, MintKeyset,
MintKeysets, MintKeysets,
Proof, Proof,
Secret,
SecretKind,
) )
from ..core.crypto import b_dhke from ..core.crypto import b_dhke
from ..core.crypto.keys import derive_pubkey, random_hash from ..core.crypto.keys import derive_pubkey, random_hash
from ..core.crypto.secp import PublicKey from ..core.crypto.secp import PublicKey
from ..core.db import Connection, Database from ..core.db import Connection, Database
from ..core.helpers import fee_reserve, sum_proofs from ..core.helpers import fee_reserve, sum_proofs
from ..core.script import verify_script from ..core.p2pk import verify_p2pk_signature
from ..core.script import verify_bitcoin_script
from ..core.settings import settings from ..core.settings import settings
from ..core.split import amount_split from ..core.split import amount_split
from ..lightning.base import Wallet from ..lightning.base import Wallet
@@ -185,7 +190,7 @@ class Ledger:
"""Verifies that a secret is present and is not too long (DOS prevention).""" """Verifies that a secret is present and is not too long (DOS prevention)."""
if proof.secret is None or proof.secret == "": if proof.secret is None or proof.secret == "":
raise Exception("no secret in proof.") raise Exception("no secret in proof.")
if len(proof.secret) > 64: if len(proof.secret) > 512:
raise Exception("secret too long.") raise Exception("secret too long.")
return True return True
@@ -209,35 +214,90 @@ class Ledger:
C = PublicKey(bytes.fromhex(proof.C), raw=True) C = PublicKey(bytes.fromhex(proof.C), raw=True)
return b_dhke.verify(private_key_amount, C, proof.secret) return b_dhke.verify(private_key_amount, C, proof.secret)
def _verify_script(self, idx: int, proof: Proof) -> bool: def _verify_spending_conditions(self, proof: Proof) -> bool:
""" """
Verify bitcoin script in proof.script commited to by <address> in proof.secret. Verify spending conditions:
proof.secret format: P2SH:<address>:<secret> Condition: P2SH - Witnesses proof.p2shscript
Condition: P2PK - Witness: proof.p2pksig
""" """
# if no script is given # P2SH
if ( try:
proof.script is None secret = Secret.deserialize(proof.secret)
or proof.script.script is None except Exception as e:
or proof.script.signature is None # secret is not a spending condition so we treat is a normal secret
):
if len(proof.secret.split("P2SH:")) == 2:
# secret indicates a script but no script is present
return False
else:
# secret indicates no script, so treat script as valid
return True return True
if secret.kind == SecretKind.P2SH:
# check if timelock is in the past
now = time.time()
if secret.timelock and secret.timelock < now:
logger.trace(f"p2sh timelock ran out ({secret.timelock}<{now}).")
return True
logger.trace(f"p2sh timelock still active ({secret.timelock}>{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 Exception("no script in proof.")
# execute and verify P2SH # execute and verify P2SH
txin_p2sh_address, valid = verify_script( txin_p2sh_address, valid = verify_bitcoin_script(
proof.script.script, proof.script.signature proof.p2shscript.script, proof.p2shscript.signature
) )
if valid: if not valid:
raise Exception("script invalid.")
# check if secret commits to script address # check if secret commits to script address
# format: P2SH:<address>:<secret> assert secret.data == str(
assert len(proof.secret.split(":")) == 3, "secret format wrong."
assert proof.secret.split(":")[1] == str(
txin_p2sh_address txin_p2sh_address
), f"secret does not contain correct P2SH address: {proof.secret.split(':')[1]} is not {txin_p2sh_address}." ), f"secret does not contain correct P2SH address: {secret.data} is not {txin_p2sh_address}."
return valid return True
# P2PK
if secret.kind == SecretKind.P2PK:
# check if timelock is in the past
now = time.time()
if secret.timelock and secret.timelock < now:
logger.trace(f"p2pk timelock ran out ({secret.timelock}<{now}).")
# check tags if a refund pubkey is present.
# If yes, we demand the signature to be from the refund pubkey
if secret.tags and secret.tags.get_tag("refund"):
signature_pubkey = secret.tags.get_tag("refund")
else:
# if no refund pubkey is present and the timelock has expired
# the token can be spent by anyone
return True
else:
# the timelock is still active, therefore we demand the signature
# to be from the pubkey in the data field
signature_pubkey = secret.data
logger.trace(f"p2pk timelock still active ({secret.timelock}>{now}).")
# now we check the signature
if not proof.p2pksig:
# no signature present although secret indicates one
raise Exception("no p2pk signature in proof.")
# we parse the secret as a P2PK commitment
# assert len(proof.secret.split(":")) == 5, "p2pk secret format invalid."
# check signature proof.p2pksig against pubkey
# we expect the signature to be on the pubkey (=message) itself
assert signature_pubkey, "no signature pubkey present."
assert verify_p2pk_signature(
message=secret.serialize().encode("utf-8"),
pubkey=PublicKey(bytes.fromhex(signature_pubkey), raw=True),
signature=bytes.fromhex(proof.p2pksig),
), "p2pk signature invalid."
logger.trace(proof.p2pksig)
logger.trace("p2pk signature valid.")
return True
# no spending contition
return True
def _verify_outputs( def _verify_outputs(
self, total: int, amount: int, outputs: List[BlindedMessage] self, total: int, amount: int, outputs: List[BlindedMessage]
@@ -509,7 +569,7 @@ class Ledger:
Exception: BDHKE verification failed. Exception: BDHKE verification failed.
""" """
# Verify scripts # Verify scripts
if not all([self._verify_script(i, p) for i, p in enumerate(proofs)]): if not all([self._verify_spending_conditions(p) for p in proofs]):
raise Exception("script validation failed.") raise Exception("script validation failed.")
# Verify secret criteria # Verify secret criteria
if not all([self._verify_secret_criteria(p) for p in proofs]): if not all([self._verify_secret_criteria(p) for p in proofs]):

View File

@@ -220,7 +220,6 @@ async def send_command(
@router.post("/receive", name="Receive tokens", response_model=ReceiveResponse) @router.post("/receive", name="Receive tokens", response_model=ReceiveResponse)
async def receive_command( async def receive_command(
token: str = Query(default=None, description="Token to receive"), token: str = Query(default=None, description="Token to receive"),
lock: str = Query(default=None, description="Unlock tokens"),
nostr: bool = Query(default=False, description="Receive tokens via nostr"), nostr: bool = Query(default=False, description="Receive tokens via nostr"),
all: bool = Query(default=False, description="Receive all pending tokens"), all: bool = Query(default=False, description="Receive all pending tokens"),
): ):
@@ -228,7 +227,7 @@ async def receive_command(
if token: if token:
tokenObj: TokenV3 = deserialize_token_from_string(token) tokenObj: TokenV3 = deserialize_token_from_string(token)
await verify_mints(wallet, tokenObj) await verify_mints(wallet, tokenObj)
balance = await receive(wallet, tokenObj, lock) balance = await receive(wallet, tokenObj)
elif nostr: elif nostr:
await receive_nostr(wallet) await receive_nostr(wallet)
balance = wallet.available_balance balance = wallet.available_balance
@@ -241,7 +240,7 @@ async def receive_command(
token = await wallet.serialize_proofs(proofs) token = await wallet.serialize_proofs(proofs)
tokenObj = deserialize_token_from_string(token) tokenObj = deserialize_token_from_string(token)
await verify_mints(wallet, tokenObj) await verify_mints(wallet, tokenObj)
balance = await receive(wallet, tokenObj, lock) balance = await receive(wallet, tokenObj)
else: else:
raise Exception("enter token or use either flag --nostr or --all.") raise Exception("enter token or use either flag --nostr or --all.")
assert balance assert balance
@@ -337,9 +336,8 @@ async def pending(
@router.get("/lock", name="Generate receiving lock", response_model=LockResponse) @router.get("/lock", name="Generate receiving lock", response_model=LockResponse)
async def lock(): async def lock():
p2shscript = await wallet.create_p2sh_lock() address = await wallet.create_p2sh_address_and_store()
txin_p2sh_address = p2shscript.address return LockResponse(P2SH=address)
return LockResponse(P2SH=txin_p2sh_address)
@router.get("/locks", name="Show unused receiving locks", response_model=LocksResponse) @router.get("/locks", name="Show unused receiving locks", response_model=LocksResponse)

View File

@@ -347,33 +347,25 @@ async def send_command(
@cli.command("receive", help="Receive tokens.") @cli.command("receive", help="Receive tokens.")
@click.argument("token", type=str, default="") @click.argument("token", type=str, default="")
@click.option("--lock", "-l", default=None, help="Unlock tokens.", type=str)
@click.option( @click.option(
"--nostr", "-n", default=False, is_flag=True, help="Receive tokens via nostr." "--nostr",
"-n",
default=False,
is_flag=True,
help="Receive tokens via nostr." "receive",
) )
@click.option( @click.option(
"--all", "-a", default=False, is_flag=True, help="Receive all pending tokens." "--all", "-a", default=False, is_flag=True, help="Receive all pending tokens."
) )
@click.option(
"--verbose",
"-v",
help="Display more information.",
is_flag=True,
default=False,
type=bool,
)
@click.pass_context @click.pass_context
@coro @coro
async def receive_cli( async def receive_cli(
ctx: Context, ctx: Context,
token: str, token: str,
lock: str,
nostr: bool, nostr: bool,
all: bool, all: bool,
verbose: bool,
): ):
wallet: Wallet = ctx.obj["WALLET"] wallet: Wallet = ctx.obj["WALLET"]
wallet.status()
if token: if token:
tokenObj = deserialize_token_from_string(token) tokenObj = deserialize_token_from_string(token)
@@ -385,9 +377,9 @@ async def receive_cli(
) )
await verify_mint(mint_wallet, mint_url) await verify_mint(mint_wallet, mint_url)
await receive(wallet, tokenObj, lock) await receive(wallet, tokenObj)
elif nostr: elif nostr:
await receive_nostr(wallet, verbose) await receive_nostr(wallet)
elif all: elif all:
reserved_proofs = await get_reserved_proofs(wallet.db) reserved_proofs = await get_reserved_proofs(wallet.db)
if len(reserved_proofs): if len(reserved_proofs):
@@ -402,7 +394,7 @@ async def receive_cli(
mint_url, os.path.join(settings.cashu_dir, wallet.name) mint_url, os.path.join(settings.cashu_dir, wallet.name)
) )
await verify_mint(mint_wallet, mint_url) await verify_mint(mint_wallet, mint_url)
await receive(wallet, tokenObj, lock) await receive(wallet, tokenObj)
else: else:
print("Error: enter token or use either flag --nostr or --all.") print("Error: enter token or use either flag --nostr or --all.")
@@ -515,24 +507,36 @@ async def pending(ctx: Context, legacy, number: int, offset: int):
@cli.command("lock", help="Generate receiving lock.") @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 @click.pass_context
@coro @coro
async def lock(ctx): async def lock(ctx, p2sh):
wallet: Wallet = ctx.obj["WALLET"] wallet: Wallet = ctx.obj["WALLET"]
p2shscript = await wallet.create_p2sh_lock() if p2sh:
txin_p2sh_address = p2shscript.address address = await wallet.create_p2sh_address_and_store()
lock_str = f"P2SH:{address}"
print("---- Pay to script hash (P2SH) ----\n") 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")
print("Use a lock to receive tokens that only you can unlock.") print("Use a lock to receive tokens that only you can unlock.")
print("") print("")
print(f"Public receiving lock: P2SH:{txin_p2sh_address}") print(f"Public receiving lock: {lock_str}")
print("") print("")
print( print(
f"Anyone can send tokens to this lock:\n\ncashu send <amount> --lock P2SH:{txin_p2sh_address}" f"Anyone can send tokens to this lock:\n\ncashu send <amount> --lock {lock_str}"
) )
print("") print("")
print( print(f"Only you can receive tokens from this lock: cashu receive <token>")
f"Only you can receive tokens from this lock:\n\ncashu receive <token> --lock P2SH:{txin_p2sh_address}\n"
)
@cli.command("locks", help="Show unused receiving locks.") @cli.command("locks", help="Show unused receiving locks.")
@@ -540,17 +544,21 @@ async def lock(ctx):
@coro @coro
async def locks(ctx): async def locks(ctx):
wallet: Wallet = ctx.obj["WALLET"] wallet: Wallet = ctx.obj["WALLET"]
# P2PK lock
pubkey = await wallet.create_p2pk_pubkey()
lock_str = f"P2PK:{pubkey}"
print("---- Pay to public key (P2PK) lock ----\n")
print(f"Lock: {lock_str}")
# P2SH locks
locks = await get_unused_locks(db=wallet.db) locks = await get_unused_locks(db=wallet.db)
if len(locks): if len(locks):
print("") print("")
print(f"--------------------------\n") print("---- Pay to script hash (P2SH) locks ----\n")
for l in locks: for l in locks:
print(f"Address: {l.address}") print(f"Lock: P2SH:{l.address}")
print(f"Script: {l.script}") print(f"Script: {l.script}")
print(f"Signature: {l.signature}") print(f"Signature: {l.signature}")
print("") print("")
print(f"Receive: cashu receive <token> --lock P2SH:{l.address}")
print("")
print(f"--------------------------\n") print(f"--------------------------\n")
else: else:
print("No locks found. Create one using: cashu lock") print("No locks found. Create one using: cashu lock")

View File

@@ -1,11 +1,13 @@
import base64 import base64
import json import json
import os import os
from datetime import datetime, timedelta
from typing import List, Optional
import click import click
from loguru import logger from loguru import logger
from ..core.base import TokenV1, TokenV2, TokenV3, TokenV3Token from ..core.base import Secret, SecretKind, TokenV1, TokenV2, TokenV3, TokenV3Token
from ..core.helpers import sum_proofs from ..core.helpers import sum_proofs
from ..core.migrations import migrate_databases from ..core.migrations import migrate_databases
from ..core.settings import settings from ..core.settings import settings
@@ -21,12 +23,7 @@ async def init_wallet(wallet: Wallet, load_proofs: bool = True):
await wallet.load_proofs(reload=True) await wallet.load_proofs(reload=True)
async def redeem_TokenV3_multimint( async def redeem_TokenV3_multimint(wallet: Wallet, token: TokenV3):
wallet: Wallet,
token: TokenV3,
script,
signature,
):
""" """
Helper function to iterate thruogh a token with multiple mints and redeem them from Helper function to iterate thruogh a token with multiple mints and redeem them from
these mints one keyset at a time. these mints one keyset at a time.
@@ -43,9 +40,7 @@ async def redeem_TokenV3_multimint(
await mint_wallet.load_mint() await mint_wallet.load_mint()
# redeem proofs of this keyset # redeem proofs of this keyset
redeem_proofs = [p for p in t.proofs if p.id == keyset] redeem_proofs = [p for p in t.proofs if p.id == keyset]
_, _ = await mint_wallet.redeem( _, _ = await mint_wallet.redeem(redeem_proofs)
redeem_proofs, scnd_script=script, scnd_siganture=signature
)
print(f"Received {sum_proofs(redeem_proofs)} sats") print(f"Received {sum_proofs(redeem_proofs)} sats")
@@ -113,20 +108,9 @@ def deserialize_token_from_string(token: str) -> TokenV3:
async def receive( async def receive(
wallet: Wallet, wallet: Wallet,
tokenObj: TokenV3, tokenObj: TokenV3,
lock: str,
): ):
# check for P2SH locks logger.debug(f"receive: {tokenObj}")
if lock: proofs = [p for t in tokenObj.token for p in t.proofs]
# load the script and signature of this address from the database
assert len(lock.split("P2SH:")) == 2, Exception(
"lock has wrong format. Expected P2SH:<address>."
)
address_split = lock.split("P2SH:")[1]
p2shscripts = await get_unused_locks(address_split, db=wallet.db)
assert len(p2shscripts) == 1, Exception("lock not found.")
script, signature = p2shscripts[0].script, p2shscripts[0].signature
else:
script, signature = None, None
includes_mint_info: bool = any([t.mint for t in tokenObj.token]) includes_mint_info: bool = any([t.mint for t in tokenObj.token])
@@ -135,13 +119,10 @@ async def receive(
await redeem_TokenV3_multimint( await redeem_TokenV3_multimint(
wallet, wallet,
tokenObj, tokenObj,
script,
signature,
) )
else: else:
# this is very legacy code, virtually any token should have mint information
# no mint information present, we extract the proofs and use wallet's default mint # no mint information present, we extract the proofs and use wallet's default mint
proofs = [p for t in tokenObj.token for p in t.proofs]
# first we load the mint URL from the DB # first we load the mint URL from the DB
keyset_in_token = proofs[0].id keyset_in_token = proofs[0].id
assert keyset_in_token assert keyset_in_token
@@ -155,7 +136,7 @@ async def receive(
os.path.join(settings.cashu_dir, wallet.name), os.path.join(settings.cashu_dir, wallet.name),
) )
await mint_wallet.load_mint(keyset_in_token) await mint_wallet.load_mint(keyset_in_token)
_, _ = await mint_wallet.redeem(proofs, script, signature) _, _ = await mint_wallet.redeem(proofs)
print(f"Received {sum_proofs(proofs)} sats") print(f"Received {sum_proofs(proofs)} sats")
# reload main wallet so the balance updates # reload main wallet so the balance updates
@@ -170,19 +151,34 @@ async def send(
""" """
Prints token to send to stdout. Prints token to send to stdout.
""" """
secret_lock = None
if lock: if lock:
assert len(lock) > 21, Exception( assert len(lock) > 21, Exception(
"Error: lock has to be at least 22 characters long." "Error: lock has to be at least 22 characters long."
) )
p2sh = False if not lock.startswith("P2SH:") and not lock.startswith("P2PK:"):
if lock and len(lock.split("P2SH:")) == 2: raise Exception("Error: lock has to start with P2SH: or P2PK:")
p2sh = True # we add a time lock to the P2PK lock by appending the current unix time + 14 days
# we use datetime because it's easier to read
if lock.startswith("P2PK:") or lock.startswith("P2SH:"):
logger.debug(f"Locking token to: {lock}")
logger.debug(
f"Adding a time lock of {settings.timelock_delta_seconds} seconds."
)
if lock.startswith("P2SH:"):
secret_lock = await wallet.create_p2sh_lock(
lock.split(":")[1], timelock=settings.timelock_delta_seconds
)
elif lock.startswith("P2PK:"):
secret_lock = await wallet.create_p2pk_lock(
lock.split(":")[1], timelock=settings.timelock_delta_seconds
)
await wallet.load_proofs() await wallet.load_proofs()
if split: if split:
await wallet.load_mint() await wallet.load_mint()
_, send_proofs = await wallet.split_to_send( _, send_proofs = await wallet.split_to_send(
wallet.proofs, amount, lock, set_reserved=True wallet.proofs, amount, secret_lock, set_reserved=True
) )
else: else:
# get a proof with specific amount # get a proof with specific amount

View File

@@ -3,6 +3,7 @@ import threading
import time import time
import click import click
from loguru import logger
from requests.exceptions import ConnectionError from requests.exceptions import ConnectionError
from ..core.settings import settings from ..core.settings import settings
@@ -10,7 +11,7 @@ from ..nostr.nostr.client.client import NostrClient
from ..nostr.nostr.event import Event from ..nostr.nostr.event import Event
from ..nostr.nostr.key import PublicKey from ..nostr.nostr.key import PublicKey
from .crud import get_nostr_last_check_timestamp, set_nostr_last_check_timestamp from .crud import get_nostr_last_check_timestamp, set_nostr_last_check_timestamp
from .helpers import receive from .helpers import deserialize_token_from_string, receive
from .wallet import Wallet from .wallet import Wallet
@@ -95,7 +96,6 @@ async def send_nostr(
async def receive_nostr( async def receive_nostr(
wallet: Wallet, wallet: Wallet,
verbose: bool = False,
): ):
if settings.nostr_private_key is None: if settings.nostr_private_key is None:
print( print(
@@ -107,34 +107,34 @@ async def receive_nostr(
private_key=settings.nostr_private_key, relays=settings.nostr_relays private_key=settings.nostr_private_key, relays=settings.nostr_relays
) )
print(f"Your nostr public key: {client.public_key.bech32()}") print(f"Your nostr public key: {client.public_key.bech32()}")
if verbose: # print(f"Your nostr private key (do not share!): {client.private_key.bech32()}")
print(f"Your nostr private key (do not share!): {client.private_key.bech32()}")
await asyncio.sleep(2) await asyncio.sleep(2)
def get_token_callback(event: Event, decrypted_content): def get_token_callback(event: Event, decrypted_content):
if verbose: logger.debug(
print(
f"From {event.public_key[:3]}..{event.public_key[-3:]}: {decrypted_content}" f"From {event.public_key[:3]}..{event.public_key[-3:]}: {decrypted_content}"
) )
try: try:
# call the receive method # call the receive method
tokenObj = deserialize_token_from_string(decrypted_content)
asyncio.run( asyncio.run(
receive( receive(
wallet, wallet,
decrypted_content, tokenObj,
"",
) )
) )
except Exception as e: except Exception as e:
logger.error(e)
pass pass
# determine timestamp of last check so we don't scan all historical DMs # determine timestamp of last check so we don't scan all historical DMs
last_check = await get_nostr_last_check_timestamp(db=wallet.db) last_check = await get_nostr_last_check_timestamp(db=wallet.db)
logger.debug(f"Last check: {last_check}")
if last_check: if last_check:
last_check -= 60 * 60 # 1 hour tolerance last_check -= 60 * 60 # 1 hour tolerance
await set_nostr_last_check_timestamp(timestamp=int(time.time()), db=wallet.db)
await set_nostr_last_check_timestamp(timestamp=int(time.time()), db=wallet.db)
logger.debug("Starting Nostr DM thread")
t = threading.Thread( t = threading.Thread(
target=client.get_dm, target=client.get_dm,
args=(client.public_key, get_token_callback, {"since": last_check}), args=(client.public_key, get_token_callback, {"since": last_check}),

View File

@@ -5,6 +5,7 @@ import math
import secrets as scrts import secrets as scrts
import time import time
import uuid import uuid
from datetime import datetime, timedelta
from itertools import groupby from itertools import groupby
from typing import Dict, List, Optional, Tuple, Union from typing import Dict, List, Optional, Tuple, Union
@@ -30,6 +31,9 @@ from ..core.base import (
PostMintResponseLegacy, PostMintResponseLegacy,
PostSplitRequest, PostSplitRequest,
Proof, Proof,
Secret,
SecretKind,
Tags,
TokenV2, TokenV2,
TokenV2Mint, TokenV2Mint,
TokenV3, TokenV3,
@@ -41,6 +45,7 @@ from ..core.crypto import b_dhke
from ..core.crypto.secp import PrivateKey, PublicKey from ..core.crypto.secp import PrivateKey, PublicKey
from ..core.db import Database from ..core.db import Database
from ..core.helpers import calculate_number_of_blank_outputs, sum_proofs from ..core.helpers import calculate_number_of_blank_outputs, sum_proofs
from ..core.p2pk import sign_p2pk_sign
from ..core.script import ( from ..core.script import (
step0_carol_checksig_redeemscrip, step0_carol_checksig_redeemscrip,
step0_carol_privkey, step0_carol_privkey,
@@ -49,10 +54,12 @@ from ..core.script import (
) )
from ..core.settings import settings from ..core.settings import settings
from ..core.split import amount_split from ..core.split import amount_split
from ..nostr.nostr.client.client import NostrClient
from ..tor.tor import TorProxy from ..tor.tor import TorProxy
from ..wallet.crud import ( from ..wallet.crud import (
get_keyset, get_keyset,
get_proofs, get_proofs,
get_unused_locks,
invalidate_proof, invalidate_proof,
secret_used, secret_used,
store_keyset, store_keyset,
@@ -259,20 +266,6 @@ class LedgerAPI:
raise Exception(f"secret already used: {s}") raise Exception(f"secret already used: {s}")
logger.trace("Secret check complete.") logger.trace("Secret check complete.")
def generate_secrets(self, secret, n) -> List[str]:
"""`secret` is the base string that will be tweaked n times
Args:
secret (str): Base secret
n (int): Number of secrets to generate
Returns:
List[str]: List of secrets
"""
if len(secret.split("P2SH:")) == 2:
return [f"{secret}:{self._generate_secret()}" for i in range(n)]
return [f"{i}:{secret}" for i in range(n)]
""" """
ENDPOINTS ENDPOINTS
""" """
@@ -438,14 +431,14 @@ class LedgerAPI:
@async_set_requests @async_set_requests
async def split( async def split(
self, proofs, amount, scnd_secret: Optional[str] = None self, proofs, amount, secret_lock: Optional[Secret] = None
) -> Tuple[List[Proof], List[Proof]]: ) -> Tuple[List[Proof], List[Proof]]:
"""Consume proofs and create new promises based on amount split. """Consume proofs and create new promises based on amount split.
If scnd_secret is None, random secrets will be generated for the tokens to keep (frst_outputs) If secret_lock is None, random secrets will be generated for the tokens to keep (frst_outputs)
and the promises to send (scnd_outputs). and the promises to send (scnd_outputs).
If scnd_secret is provided, the wallet will create blinded secrets with those to attach a If secret_lock is provided, the wallet will create blinded secrets with those to attach a
predefined spending condition to the tokens they want to send.""" predefined spending condition to the tokens they want to send."""
logger.debug("Calling split. POST /split") logger.debug("Calling split. POST /split")
total = sum_proofs(proofs) total = sum_proofs(proofs)
@@ -454,18 +447,18 @@ class LedgerAPI:
scnd_outputs = amount_split(scnd_amt) scnd_outputs = amount_split(scnd_amt)
amounts = frst_outputs + scnd_outputs amounts = frst_outputs + scnd_outputs
if scnd_secret is None: if secret_lock is None:
secrets = [self._generate_secret() for _ in range(len(amounts))] secrets = [self._generate_secret() for _ in range(len(amounts))]
else: else:
scnd_secrets = self.generate_secrets(scnd_secret, len(scnd_outputs)) secret_locks = [secret_lock.serialize() for i in range(len(scnd_outputs))]
logger.debug(f"Creating proofs with custom secrets: {scnd_secrets}") logger.debug(f"Creating proofs with custom secrets: {secret_locks}")
assert len(scnd_secrets) == len( assert len(secret_locks) == len(
scnd_outputs scnd_outputs
), "number of scnd_secrets does not match number of ouptus." ), "number of secret_locks does not match number of ouptus."
# append predefined secrets (to send) to random secrets (to keep) # append predefined secrets (to send) to random secrets (to keep)
secrets = [ secrets = [
self._generate_secret() for s in range(len(frst_outputs)) self._generate_secret() for s in range(len(frst_outputs))
] + scnd_secrets ] + secret_locks
assert len(secrets) == len( assert len(secrets) == len(
amounts amounts
@@ -477,7 +470,7 @@ class LedgerAPI:
# construct payload # construct payload
def _splitrequest_include_fields(proofs): def _splitrequest_include_fields(proofs):
"""strips away fields from the model that aren't necessary for the /split""" """strips away fields from the model that aren't necessary for the /split"""
proofs_include = {"id", "amount", "secret", "C", "script"} proofs_include = {"id", "amount", "secret", "C", "p2shscript", "p2pksig"}
return { return {
"amount": ..., "amount": ...,
"outputs": ..., "outputs": ...,
@@ -572,6 +565,8 @@ class LedgerAPI:
class Wallet(LedgerAPI): class Wallet(LedgerAPI):
"""Minimal wallet wrapper.""" """Minimal wallet wrapper."""
private_key: Optional[PrivateKey] = None
def __init__(self, url: str, db: str, name: str = "no_name"): def __init__(self, url: str, db: str, name: str = "no_name"):
super().__init__(url) super().__init__(url)
self.db = Database("wallet", db) self.db = Database("wallet", db)
@@ -579,6 +574,17 @@ class Wallet(LedgerAPI):
self.name = name self.name = name
logger.debug(f"Wallet initalized with mint URL {url}") logger.debug(f"Wallet initalized with mint URL {url}")
# temporarily, we use the NostrClient to generate keys
try:
nostr_pk = NostrClient(
private_key=settings.nostr_private_key, connect=False
).private_key
self.private_key = (
PrivateKey(bytes.fromhex(nostr_pk.hex()), raw=True) or None
)
except Exception as e:
pass
# ---------- API ---------- # ---------- API ----------
async def load_mint(self, keyset_id: str = ""): async def load_mint(self, keyset_id: str = ""):
@@ -648,27 +654,67 @@ class Wallet(LedgerAPI):
self.proofs += proofs self.proofs += proofs
return proofs return proofs
async def add_witnesses_to_proofs(self, proofs: List[Proof]):
"""Adds witnesses to proofs for P2SH or P2PK redemption."""
p2sh_script, p2sh_signature = None, None
p2pk_signatures = None
# iterate through proofs and produce witnesses for each
# first we check whether all tokens have serialized secrets as their secret
try:
for p in proofs:
Secret.deserialize(p.secret)
except:
# if not, we do not add witnesses (treat as regular token secret)
return proofs
# P2SH scripts
if all([Secret.deserialize(p.secret).kind == SecretKind.P2SH for p in proofs]):
# 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.p2shscript = P2SHScript(script=p2sh_script, signature=p2sh_signature)
# P2PK signatures
elif all(
[Secret.deserialize(p.secret).kind == SecretKind.P2PK for p in proofs]
):
p2pk_signatures = await self.sign_p2pk_with_privatekey(proofs)
logger.debug(f"Unlock signature: {p2pk_signatures}")
# attach unlock signatures to proofs
assert len(proofs) == len(p2pk_signatures), "wrong number of signatures"
for p, s in zip(proofs, p2pk_signatures):
p.p2pksig = s
return proofs
async def redeem( async def redeem(
self, self,
proofs: List[Proof], proofs: List[Proof],
scnd_script: Optional[str] = None,
scnd_siganture: Optional[str] = None,
): ):
if scnd_script and scnd_siganture: proofs = await self.add_witnesses_to_proofs(proofs)
logger.debug(f"Unlock script: {scnd_script}")
# attach unlock scripts to proofs
for p in proofs:
p.script = P2SHScript(script=scnd_script, signature=scnd_siganture)
return await self.split(proofs, sum_proofs(proofs)) return await self.split(proofs, sum_proofs(proofs))
async def split( async def split(
self, self,
proofs: List[Proof], proofs: List[Proof],
amount: int, amount: int,
scnd_secret: Optional[str] = None, secret_lock: Optional[Secret] = None,
): ):
assert len(proofs) > 0, ValueError("no proofs provided.") assert len(proofs) > 0, ValueError("no proofs provided.")
frst_proofs, scnd_proofs = await super().split(proofs, amount, scnd_secret) frst_proofs, scnd_proofs = await super().split(proofs, amount, secret_lock)
if len(frst_proofs) == 0 and len(scnd_proofs) == 0: if len(frst_proofs) == 0 and len(scnd_proofs) == 0:
raise Exception("received no splits.") raise Exception("received no splits.")
@@ -957,7 +1003,7 @@ class Wallet(LedgerAPI):
self, self,
proofs: List[Proof], proofs: List[Proof],
amount: int, amount: int,
scnd_secret: Optional[str] = None, secret_lock: Optional[Secret] = None,
set_reserved: bool = False, set_reserved: bool = False,
): ):
""" """
@@ -966,25 +1012,26 @@ class Wallet(LedgerAPI):
Args: Args:
proofs (List[Proof]): Proofs to split proofs (List[Proof]): Proofs to split
amount (int): Amount to split to amount (int): Amount to split to
scnd_secret (Optional[str], optional): If set, a custom secret is used to lock new outputs. Defaults to None. secret_lock (Optional[str], optional): If set, a custom secret is used to lock new outputs. Defaults to None.
set_reserved (bool, optional): If set, the proofs are marked as reserved. Should be set to False if a payment attempt set_reserved (bool, optional): If set, the proofs are marked as reserved. Should be set to False if a payment attempt
is made with the split that could fail (like a Lightning payment). Should be set to True if the token to be sent is is made with the split that could fail (like a Lightning payment). Should be set to True if the token to be sent is
displayed to the user to be then sent to someone else. Defaults to False. displayed to the user to be then sent to someone else. Defaults to False.
""" """
if scnd_secret: if secret_lock:
logger.debug(f"Spending conditions: {scnd_secret}") logger.debug(f"Spending conditions: {secret_lock}")
spendable_proofs = await self._select_proofs_to_send(proofs, amount) spendable_proofs = await self._select_proofs_to_send(proofs, amount)
keep_proofs, send_proofs = await self.split( keep_proofs, send_proofs = await self.split(
spendable_proofs, amount, scnd_secret spendable_proofs, amount, secret_lock
) )
if set_reserved: if set_reserved:
await self.set_reserved(send_proofs, reserved=True) await self.set_reserved(send_proofs, reserved=True)
return keep_proofs, send_proofs return keep_proofs, send_proofs
# ---------- P2SH ---------- # ---------- P2SH and P2PK ----------
async def create_p2sh_lock(self): 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() alice_privkey = step0_carol_privkey()
txin_redeemScript = step0_carol_checksig_redeemscrip(alice_privkey.pub) txin_redeemScript = step0_carol_checksig_redeemscrip(alice_privkey.pub)
txin_p2sh_address = step1_carol_create_p2sh_address(txin_redeemScript) txin_p2sh_address = step1_carol_create_p2sh_address(txin_redeemScript)
@@ -997,7 +1044,61 @@ class Wallet(LedgerAPI):
address=str(txin_p2sh_address), address=str(txin_p2sh_address),
) )
await store_p2sh(p2shScript, db=self.db) await store_p2sh(p2shScript, db=self.db)
return p2shScript assert p2shScript.address
return p2shScript.address
async def create_p2pk_pubkey(self):
assert (
self.private_key
), "No private key set in settings. Set NOSTR_PRIVATE_KEY in .env"
public_key = self.private_key.pubkey
# logger.debug(f"Private key: {self.private_key.bech32()}")
assert public_key
return public_key.serialize().hex()
async def create_p2pk_lock(
self,
pubkey: str,
timelock: Optional[int] = None,
tags: Optional[Tags] = None,
):
return Secret(
kind=SecretKind.P2PK,
data=pubkey,
timelock=int((datetime.now() + timedelta(seconds=timelock)).timestamp())
if timelock
else None,
tags=tags,
)
async def create_p2sh_lock(
self,
address: str,
timelock: Optional[int] = None,
tags: Optional[Tags] = None,
):
return Secret(
kind=SecretKind.P2SH,
data=address,
timelock=int((datetime.now() + timedelta(seconds=timelock)).timestamp())
if timelock
else None,
tags=tags,
)
async def sign_p2pk_with_privatekey(self, proofs: List[Proof]) -> List[str]:
assert (
self.private_key
), "No private key set in settings. Set NOSTR_PRIVATE_KEY in .env"
private_key = self.private_key
assert private_key.pubkey
return [
sign_p2pk_sign(
message=proof.secret.encode("utf-8"),
private_key=private_key,
)
for proof in proofs
]
# ---------- BALANCE CHECKS ---------- # ---------- BALANCE CHECKS ----------

View File

@@ -13,7 +13,7 @@ entry_points = {"console_scripts": ["cashu = cashu.wallet.cli.cli:cli"]}
setuptools.setup( setuptools.setup(
name="cashu", name="cashu",
version="0.12.1", version="0.12.2",
description="Ecash wallet and mint for Bitcoin Lightning", description="Ecash wallet and mint for Bitcoin Lightning",
long_description=long_description, long_description=long_description,
long_description_content_type="text/markdown", long_description_content_type="text/markdown",

View File

@@ -1,5 +1,6 @@
import multiprocessing import multiprocessing
import os import os
import secrets
import shutil import shutil
import time import time
from pathlib import Path from pathlib import Path
@@ -58,6 +59,7 @@ def mint():
settings.port = 3337 settings.port = 3337
settings.mint_url = "http://localhost:3337" settings.mint_url = "http://localhost:3337"
settings.port = settings.mint_listen_port settings.port = settings.mint_listen_port
settings.nostr_private_key = secrets.token_hex(32) # wrong private key
config = uvicorn.Config( config = uvicorn.Config(
"cashu.mint.app:app", "cashu.mint.app:app",
port=settings.mint_listen_port, port=settings.mint_listen_port,

View File

@@ -1,10 +1,12 @@
import time import asyncio
import secrets
from typing import List from typing import List
import pytest import pytest
import pytest_asyncio import pytest_asyncio
from cashu.core.base import Proof from cashu.core.base import Proof, Secret, SecretKind, Tags
from cashu.core.crypto.secp import PrivateKey, PublicKey
from cashu.core.helpers import async_unwrap, sum_proofs from cashu.core.helpers import async_unwrap, sum_proofs
from cashu.core.migrations import migrate_databases from cashu.core.migrations import migrate_databases
from cashu.core.settings import settings from cashu.core.settings import settings
@@ -20,9 +22,10 @@ async def assert_err(f, msg):
try: try:
await f await f
except Exception as exc: except Exception as exc:
assert exc.args[0] == msg, Exception( if str(exc.args[0]) != msg:
f"Expected error: {msg}, got: {exc.args[0]}" 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): def assert_amt(proofs: List[Proof], expected: int):
@@ -242,43 +245,113 @@ async def test_split_invalid_amount(wallet1: Wallet):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_split_with_secret(wallet1: Wallet): async def test_create_p2pk_pubkey(wallet1: Wallet):
await wallet1.mint(64) await wallet1.mint(64)
secret = f"asdasd_{time.time()}" pubkey = await wallet1.create_p2pk_pubkey()
w1_frst_proofs, w1_scnd_proofs = await wallet1.split( PublicKey(bytes.fromhex(pubkey), raw=True)
wallet1.proofs, 32, scnd_secret=secret
)
# check if index prefix is in secret
assert w1_scnd_proofs[0].secret == "0:" + secret
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_redeem_without_secret(wallet1: Wallet): async def test_p2pk(wallet1: Wallet, wallet2: Wallet):
await wallet1.mint(64) await wallet1.mint(64)
# strip away the secrets pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
w1_scnd_proofs_manipulated = wallet1.proofs.copy() # p2pk test
for p in w1_scnd_proofs_manipulated: secret_lock = await wallet1.create_p2pk_lock(pubkey_wallet2) # sender side
p.secret = "" _, send_proofs = await wallet1.split_to_send(
await assert_err( wallet1.proofs, 8, secret_lock=secret_lock
wallet1.redeem(w1_scnd_proofs_manipulated),
"Mint Error: no secret in proof.",
) )
await wallet2.redeem(send_proofs)
@pytest.mark.asyncio @pytest.mark.asyncio
async def no_test_p2sh(wallet1: Wallet, wallet2: Wallet): async def test_p2pk_receive_with_wrong_private_key(wallet1: Wallet, wallet2: Wallet):
await wallet1.mint(64) await wallet1.mint(64)
# p2sh test pubkey_wallet2 = await wallet2.create_p2pk_pubkey() # receiver side
p2shscript = await wallet1.create_p2sh_lock() # sender side
txin_p2sh_address = p2shscript.address secret_lock = await wallet1.create_p2pk_lock(pubkey_wallet2) # sender side
lock = f"P2SH:{txin_p2sh_address}" _, send_proofs = await wallet1.split_to_send(
_, send_proofs = await wallet1.split_to_send(wallet1.proofs, 8, lock) wallet1.proofs, 8, secret_lock=secret_lock
assert send_proofs[0].secret.startswith("P2SH:")
frst_proofs, scnd_proofs = await wallet2.redeem(
send_proofs, scnd_script=p2shscript.script, scnd_siganture=p2shscript.signature
) )
# receiver side: wrong private key
wallet1.private_key = PrivateKey() # wrong private key
await assert_err(wallet1.redeem(send_proofs), "Mint Error: p2pk signature invalid.")
@pytest.mark.asyncio
async def test_p2pk_short_timelock_receive_with_wrong_private_key(
wallet1: Wallet, wallet2: Wallet
):
await wallet1.mint(64)
pubkey_wallet2 = await wallet2.create_p2pk_pubkey() # receiver side
# sender side
secret_lock = await wallet1.create_p2pk_lock(
pubkey_wallet2, timelock=4
) # sender side
_, send_proofs = await wallet1.split_to_send(
wallet1.proofs, 8, secret_lock=secret_lock
)
# receiver side: wrong private key
wallet1.private_key = PrivateKey() # wrong private key
await assert_err(wallet1.redeem(send_proofs), "Mint Error: p2pk signature invalid.")
await asyncio.sleep(6)
await wallet1.redeem(send_proofs)
@pytest.mark.asyncio
async def test_p2pk_timelock_with_refund_pubkey(wallet1: Wallet, wallet2: Wallet):
await wallet1.mint(64)
pubkey_wallet2 = await wallet2.create_p2pk_pubkey() # receiver side
# sender side
garbage_pubkey = PrivateKey().pubkey
assert garbage_pubkey
secret_lock = await wallet1.create_p2pk_lock(
garbage_pubkey.serialize().hex(), # create lock to unspendable pubkey
timelock=4, # timelock
tags=Tags(__root__=[["refund", pubkey_wallet2]]), # refund pubkey
) # sender side
_, send_proofs = await wallet1.split_to_send(
wallet1.proofs, 8, secret_lock=secret_lock
)
# receiver side: can't redeem since we used a garbage pubkey
await assert_err(wallet2.redeem(send_proofs), "Mint Error: p2pk signature invalid.")
await asyncio.sleep(6)
# we can now redeem because of the refund timelock
await wallet2.redeem(send_proofs)
@pytest.mark.asyncio
async def test_p2pk_timelock_with_wrong_refund_pubkey(wallet1: Wallet, wallet2: Wallet):
await wallet1.mint(64)
pubkey_wallet2 = await wallet2.create_p2pk_pubkey() # receiver side
# sender side
garbage_pubkey = PrivateKey().pubkey
garbage_pubkey_2 = PrivateKey().pubkey
assert garbage_pubkey
assert garbage_pubkey_2
secret_lock = await wallet1.create_p2pk_lock(
garbage_pubkey.serialize().hex(), # create lock to unspendable pubkey
timelock=4, # timelock
tags=Tags(
__root__=[["refund", garbage_pubkey_2.serialize().hex()]]
), # refund pubkey
) # sender side
_, send_proofs = await wallet1.split_to_send(
wallet1.proofs, 8, secret_lock=secret_lock
)
# receiver side: can't redeem since we used a garbage pubkey
await assert_err(wallet2.redeem(send_proofs), "Mint Error: p2pk signature invalid.")
await asyncio.sleep(6)
# we still can't redeem it because we used garbage_pubkey_2 as a refund pubkey
await assert_err(wallet2.redeem(send_proofs), "Mint Error: p2pk signature invalid.")
@pytest.mark.asyncio
async def test_p2sh(wallet1: Wallet, wallet2: Wallet):
await wallet1.mint(64)
_ = 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(frst_proofs) == 0
assert len(scnd_proofs) == 1 assert len(scnd_proofs) == 1
assert sum_proofs(scnd_proofs) == 8 assert sum_proofs(scnd_proofs) == 8
@@ -286,40 +359,11 @@ async def no_test_p2sh(wallet1: Wallet, wallet2: Wallet):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_p2sh_receive_wrong_script(wallet1: Wallet, wallet2: Wallet): async def test_p2sh_receive_with_wrong_wallet(wallet1: Wallet, wallet2: Wallet):
await wallet1.mint(64) await wallet1.mint(64)
# p2sh test wallet1_address = await wallet1.create_p2sh_address_and_store() # receiver side
p2shscript = await wallet1.create_p2sh_lock() secret_lock = await wallet1.create_p2sh_lock(wallet1_address) # sender side
txin_p2sh_address = p2shscript.address _, send_proofs = await wallet1.split_to_send(
lock = f"P2SH:{txin_p2sh_address}" wallet1.proofs, 8, secret_lock
_, send_proofs = await wallet1.split_to_send(wallet1.proofs, 8, lock) # type: ignore ) # sender side
await assert_err(wallet2.redeem(send_proofs), "lock not found.") # wrong receiver
wrong_script = "asad" + p2shscript.script
await assert_err(
wallet2.redeem(
send_proofs, scnd_script=wrong_script, scnd_siganture=p2shscript.signature
),
"Mint Error: ('Script verification failed:', VerifyScriptError('scriptPubKey returned false'))",
)
assert wallet2.balance == 0
@pytest.mark.asyncio
async def test_p2sh_receive_wrong_signature(wallet1: Wallet, wallet2: Wallet):
await wallet1.mint(64)
# p2sh test
p2shscript = await wallet1.create_p2sh_lock()
txin_p2sh_address = p2shscript.address
lock = f"P2SH:{txin_p2sh_address}"
_, send_proofs = await wallet1.split_to_send(wallet1.proofs, 8, lock) # type: ignore
wrong_signature = "asda" + p2shscript.signature
await assert_err(
wallet2.redeem(
send_proofs, scnd_script=p2shscript.script, scnd_siganture=wrong_signature
),
"Mint Error: ('Script evaluation failed:', EvalScriptError('EvalScript: OP_RETURN called'))",
)
assert wallet2.balance == 0