diff --git a/.env.example b/.env.example
index c1506f5..893ed44 100644
--- a/.env.example
+++ b/.env.example
@@ -17,7 +17,7 @@ TOR=TRUE
# NOSTR
# 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=["wss://nostr-pub.wellorder.net"]
diff --git a/README.md b/README.md
index d5c1d82..d197cd1 100644
--- a/README.md
+++ b/README.md
@@ -114,7 +114,7 @@ cashu info
Returns:
```bash
-Version: 0.12.1
+Version: 0.12.2
Debug: False
Cashu dir: /home/user/.cashu
Wallet: wallet
diff --git a/cashu/core/base.py b/cashu/core/base.py
index 11eb042..4945b81 100644
--- a/cashu/core/base.py
+++ b/cashu/core/base.py
@@ -9,13 +9,62 @@ from pydantic import BaseModel
from .crypto.keys import derive_keys, derive_keyset_id, derive_pubkeys
from .crypto.secp import PrivateKey, PublicKey
from .legacy import derive_keys_backwards_compatible_insecure_pre_0_12
+from .p2pk import sign_p2pk_sign
# ------- 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):
"""
- Describes spending condition of a Proof
+ Unlocks P2SH spending condition of a Proof
"""
script: str
@@ -34,7 +83,8 @@ class Proof(BaseModel):
amount: int = 0
secret: str = "" # secret or message to be blinded and signed
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[
None, bool
] = 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]
amount: int
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):
diff --git a/cashu/core/p2pk.py b/cashu/core/p2pk.py
new file mode 100644
index 0000000..4a2e4d0
--- /dev/null
+++ b/cashu/core/p2pk.py
@@ -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)))
diff --git a/cashu/core/script.py b/cashu/core/script.py
index 6961cef..8b33f5b 100644
--- a/cashu/core/script.py
+++ b/cashu/core/script.py
@@ -79,28 +79,28 @@ def step3_bob_verify_script(txin_signature, txin_redeemScript, tx):
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))
- print("Redeem script:", txin_redeemScript.__repr__())
+ # 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}")
+ # 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"
- )
+ # 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.")
+ # if script_valid:
+ # print("Successfull.")
+ # else:
+ # print("Error.")
return txin_p2sh_address, script_valid
diff --git a/cashu/core/settings.py b/cashu/core/settings.py
index 6d7616c..80583b3 100644
--- a/cashu/core/settings.py
+++ b/cashu/core/settings.py
@@ -8,7 +8,7 @@ from pydantic import BaseSettings, Extra, Field
env = Env()
-VERSION = "0.12.1"
+VERSION = "0.12.2"
def find_env_file():
@@ -98,9 +98,15 @@ class WalletSettings(CashuSettings):
]
)
+ timelock_delta_seconds: int = Field(default=86400) # 1 day
+
class Settings(
- EnvSettings, MintSettings, MintInformation, WalletSettings, CashuSettings
+ EnvSettings,
+ MintSettings,
+ MintInformation,
+ WalletSettings,
+ CashuSettings,
):
version: str = Field(default=VERSION)
diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py
index 2010f48..fe5d903 100644
--- a/cashu/mint/ledger.py
+++ b/cashu/mint/ledger.py
@@ -1,5 +1,7 @@
import asyncio
+import json
import math
+import time
from typing import Dict, List, Literal, Optional, Set, Union
from loguru import logger
@@ -12,13 +14,16 @@ from ..core.base import (
MintKeyset,
MintKeysets,
Proof,
+ Secret,
+ SecretKind,
)
from ..core.crypto import b_dhke
from ..core.crypto.keys import derive_pubkey, random_hash
from ..core.crypto.secp import PublicKey
from ..core.db import Connection, Database
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.split import amount_split
from ..lightning.base import Wallet
@@ -185,7 +190,7 @@ class Ledger:
"""Verifies that a secret is present and is not too long (DOS prevention)."""
if proof.secret is None or proof.secret == "":
raise Exception("no secret in proof.")
- if len(proof.secret) > 64:
+ if len(proof.secret) > 512:
raise Exception("secret too long.")
return True
@@ -209,35 +214,90 @@ class Ledger:
C = PublicKey(bytes.fromhex(proof.C), raw=True)
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
in proof.secret.
- proof.secret format: P2SH::
+ Verify spending conditions:
+ Condition: P2SH - Witnesses proof.p2shscript
+ Condition: P2PK - Witness: proof.p2pksig
+
"""
- # if no script is given
- if (
- proof.script is None
- or proof.script.script is None
- or proof.script.signature is None
- ):
- 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
+ # P2SH
+ try:
+ secret = Secret.deserialize(proof.secret)
+ except Exception as e:
+ # secret is not a spending condition so we treat is a normal secret
+ 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
- # execute and verify P2SH
- txin_p2sh_address, valid = verify_script(
- proof.script.script, proof.script.signature
- )
- if valid:
+ 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
+ txin_p2sh_address, valid = verify_bitcoin_script(
+ proof.p2shscript.script, proof.p2shscript.signature
+ )
+ if not valid:
+ raise Exception("script invalid.")
# check if secret commits to script address
- # format: P2SH::
- assert len(proof.secret.split(":")) == 3, "secret format wrong."
- assert proof.secret.split(":")[1] == str(
+ assert secret.data == str(
txin_p2sh_address
- ), f"secret does not contain correct P2SH address: {proof.secret.split(':')[1]} is not {txin_p2sh_address}."
- return valid
+ ), f"secret does not contain correct P2SH address: {secret.data} is not {txin_p2sh_address}."
+ 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(
self, total: int, amount: int, outputs: List[BlindedMessage]
@@ -509,7 +569,7 @@ class Ledger:
Exception: BDHKE verification failed.
"""
# 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.")
# Verify secret criteria
if not all([self._verify_secret_criteria(p) for p in proofs]):
diff --git a/cashu/wallet/api/router.py b/cashu/wallet/api/router.py
index fec9f61..c12f910 100644
--- a/cashu/wallet/api/router.py
+++ b/cashu/wallet/api/router.py
@@ -220,7 +220,6 @@ async def send_command(
@router.post("/receive", name="Receive tokens", response_model=ReceiveResponse)
async def receive_command(
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"),
all: bool = Query(default=False, description="Receive all pending tokens"),
):
@@ -228,7 +227,7 @@ async def receive_command(
if token:
tokenObj: TokenV3 = deserialize_token_from_string(token)
await verify_mints(wallet, tokenObj)
- balance = await receive(wallet, tokenObj, lock)
+ balance = await receive(wallet, tokenObj)
elif nostr:
await receive_nostr(wallet)
balance = wallet.available_balance
@@ -241,7 +240,7 @@ async def receive_command(
token = await wallet.serialize_proofs(proofs)
tokenObj = deserialize_token_from_string(token)
await verify_mints(wallet, tokenObj)
- balance = await receive(wallet, tokenObj, lock)
+ balance = await receive(wallet, tokenObj)
else:
raise Exception("enter token or use either flag --nostr or --all.")
assert balance
@@ -337,9 +336,8 @@ async def pending(
@router.get("/lock", name="Generate receiving lock", response_model=LockResponse)
async def lock():
- p2shscript = await wallet.create_p2sh_lock()
- txin_p2sh_address = p2shscript.address
- return LockResponse(P2SH=txin_p2sh_address)
+ address = await wallet.create_p2sh_address_and_store()
+ return LockResponse(P2SH=address)
@router.get("/locks", name="Show unused receiving locks", response_model=LocksResponse)
diff --git a/cashu/wallet/cli/cli.py b/cashu/wallet/cli/cli.py
index b983534..1314a85 100644
--- a/cashu/wallet/cli/cli.py
+++ b/cashu/wallet/cli/cli.py
@@ -347,33 +347,25 @@ async def send_command(
@cli.command("receive", help="Receive tokens.")
@click.argument("token", type=str, default="")
-@click.option("--lock", "-l", default=None, help="Unlock tokens.", type=str)
@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(
"--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
@coro
async def receive_cli(
ctx: Context,
token: str,
- lock: str,
nostr: bool,
all: bool,
- verbose: bool,
):
wallet: Wallet = ctx.obj["WALLET"]
- wallet.status()
if token:
tokenObj = deserialize_token_from_string(token)
@@ -385,9 +377,9 @@ async def receive_cli(
)
await verify_mint(mint_wallet, mint_url)
- await receive(wallet, tokenObj, lock)
+ await receive(wallet, tokenObj)
elif nostr:
- await receive_nostr(wallet, verbose)
+ await receive_nostr(wallet)
elif all:
reserved_proofs = await get_reserved_proofs(wallet.db)
if len(reserved_proofs):
@@ -402,7 +394,7 @@ async def receive_cli(
mint_url, os.path.join(settings.cashu_dir, wallet.name)
)
await verify_mint(mint_wallet, mint_url)
- await receive(wallet, tokenObj, lock)
+ await receive(wallet, tokenObj)
else:
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.")
+@click.option(
+ "--p2sh",
+ "-p",
+ default=False,
+ is_flag=True,
+ help="Create P2SH lock.",
+ type=bool,
+)
@click.pass_context
@coro
-async def lock(ctx):
+async def lock(ctx, p2sh):
wallet: Wallet = ctx.obj["WALLET"]
- p2shscript = await wallet.create_p2sh_lock()
- txin_p2sh_address = p2shscript.address
- print("---- Pay to script hash (P2SH) ----\n")
+ 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")
+
print("Use a lock to receive tokens that only you can unlock.")
print("")
- print(f"Public receiving lock: P2SH:{txin_p2sh_address}")
+ print(f"Public receiving lock: {lock_str}")
print("")
print(
- f"Anyone can send tokens to this lock:\n\ncashu send --lock P2SH:{txin_p2sh_address}"
+ f"Anyone can send tokens to this lock:\n\ncashu send --lock {lock_str}"
)
print("")
- print(
- f"Only you can receive tokens from this lock:\n\ncashu receive --lock P2SH:{txin_p2sh_address}\n"
- )
+ print(f"Only you can receive tokens from this lock: cashu receive ")
@cli.command("locks", help="Show unused receiving locks.")
@@ -540,17 +544,21 @@ async def lock(ctx):
@coro
async def locks(ctx):
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)
if len(locks):
print("")
- print(f"--------------------------\n")
+ print("---- Pay to script hash (P2SH) locks ----\n")
for l in locks:
- print(f"Address: {l.address}")
+ print(f"Lock: P2SH:{l.address}")
print(f"Script: {l.script}")
print(f"Signature: {l.signature}")
print("")
- print(f"Receive: cashu receive --lock P2SH:{l.address}")
- print("")
print(f"--------------------------\n")
else:
print("No locks found. Create one using: cashu lock")
diff --git a/cashu/wallet/helpers.py b/cashu/wallet/helpers.py
index 5eb2963..421ba9f 100644
--- a/cashu/wallet/helpers.py
+++ b/cashu/wallet/helpers.py
@@ -1,11 +1,13 @@
import base64
import json
import os
+from datetime import datetime, timedelta
+from typing import List, Optional
import click
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.migrations import migrate_databases
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)
-async def redeem_TokenV3_multimint(
- wallet: Wallet,
- token: TokenV3,
- script,
- signature,
-):
+async def redeem_TokenV3_multimint(wallet: Wallet, token: TokenV3):
"""
Helper function to iterate thruogh a token with multiple mints and redeem them from
these mints one keyset at a time.
@@ -43,9 +40,7 @@ async def redeem_TokenV3_multimint(
await mint_wallet.load_mint()
# redeem proofs of this keyset
redeem_proofs = [p for p in t.proofs if p.id == keyset]
- _, _ = await mint_wallet.redeem(
- redeem_proofs, scnd_script=script, scnd_siganture=signature
- )
+ _, _ = await mint_wallet.redeem(redeem_proofs)
print(f"Received {sum_proofs(redeem_proofs)} sats")
@@ -113,20 +108,9 @@ def deserialize_token_from_string(token: str) -> TokenV3:
async def receive(
wallet: Wallet,
tokenObj: TokenV3,
- lock: str,
):
- # check for P2SH locks
- if lock:
- # 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_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
+ logger.debug(f"receive: {tokenObj}")
+ proofs = [p for t in tokenObj.token for p in t.proofs]
includes_mint_info: bool = any([t.mint for t in tokenObj.token])
@@ -135,13 +119,10 @@ async def receive(
await redeem_TokenV3_multimint(
wallet,
tokenObj,
- script,
- signature,
)
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
-
- proofs = [p for t in tokenObj.token for p in t.proofs]
# first we load the mint URL from the DB
keyset_in_token = proofs[0].id
assert keyset_in_token
@@ -155,7 +136,7 @@ async def receive(
os.path.join(settings.cashu_dir, wallet.name),
)
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")
# reload main wallet so the balance updates
@@ -170,19 +151,34 @@ async def send(
"""
Prints token to send to stdout.
"""
+ secret_lock = None
if lock:
assert len(lock) > 21, Exception(
"Error: lock has to be at least 22 characters long."
)
- p2sh = False
- if lock and len(lock.split("P2SH:")) == 2:
- p2sh = True
+ if not lock.startswith("P2SH:") and not lock.startswith("P2PK:"):
+ raise Exception("Error: lock has to start with P2SH: or P2PK:")
+ # 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()
if split:
await wallet.load_mint()
_, send_proofs = await wallet.split_to_send(
- wallet.proofs, amount, lock, set_reserved=True
+ wallet.proofs, amount, secret_lock, set_reserved=True
)
else:
# get a proof with specific amount
diff --git a/cashu/wallet/nostr.py b/cashu/wallet/nostr.py
index b2260cc..1ea1918 100644
--- a/cashu/wallet/nostr.py
+++ b/cashu/wallet/nostr.py
@@ -3,6 +3,7 @@ import threading
import time
import click
+from loguru import logger
from requests.exceptions import ConnectionError
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.key import PublicKey
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
@@ -95,7 +96,6 @@ async def send_nostr(
async def receive_nostr(
wallet: Wallet,
- verbose: bool = False,
):
if settings.nostr_private_key is None:
print(
@@ -107,34 +107,34 @@ async def receive_nostr(
private_key=settings.nostr_private_key, relays=settings.nostr_relays
)
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)
def get_token_callback(event: Event, decrypted_content):
- if verbose:
- print(
- f"From {event.public_key[:3]}..{event.public_key[-3:]}: {decrypted_content}"
- )
+ logger.debug(
+ f"From {event.public_key[:3]}..{event.public_key[-3:]}: {decrypted_content}"
+ )
try:
# call the receive method
-
+ tokenObj = deserialize_token_from_string(decrypted_content)
asyncio.run(
receive(
wallet,
- decrypted_content,
- "",
+ tokenObj,
)
)
except Exception as e:
+ logger.error(e)
pass
# determine timestamp of last check so we don't scan all historical DMs
last_check = await get_nostr_last_check_timestamp(db=wallet.db)
+ logger.debug(f"Last check: {last_check}")
if last_check:
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(
target=client.get_dm,
args=(client.public_key, get_token_callback, {"since": last_check}),
diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py
index 91c8b2e..93065dc 100644
--- a/cashu/wallet/wallet.py
+++ b/cashu/wallet/wallet.py
@@ -5,6 +5,7 @@ import math
import secrets as scrts
import time
import uuid
+from datetime import datetime, timedelta
from itertools import groupby
from typing import Dict, List, Optional, Tuple, Union
@@ -30,6 +31,9 @@ from ..core.base import (
PostMintResponseLegacy,
PostSplitRequest,
Proof,
+ Secret,
+ SecretKind,
+ Tags,
TokenV2,
TokenV2Mint,
TokenV3,
@@ -41,6 +45,7 @@ from ..core.crypto import b_dhke
from ..core.crypto.secp import PrivateKey, PublicKey
from ..core.db import Database
from ..core.helpers import calculate_number_of_blank_outputs, sum_proofs
+from ..core.p2pk import sign_p2pk_sign
from ..core.script import (
step0_carol_checksig_redeemscrip,
step0_carol_privkey,
@@ -49,10 +54,12 @@ from ..core.script import (
)
from ..core.settings import settings
from ..core.split import amount_split
+from ..nostr.nostr.client.client import NostrClient
from ..tor.tor import TorProxy
from ..wallet.crud import (
get_keyset,
get_proofs,
+ get_unused_locks,
invalidate_proof,
secret_used,
store_keyset,
@@ -259,20 +266,6 @@ class LedgerAPI:
raise Exception(f"secret already used: {s}")
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
"""
@@ -438,14 +431,14 @@ class LedgerAPI:
@async_set_requests
async def split(
- self, proofs, amount, scnd_secret: Optional[str] = None
+ self, proofs, amount, secret_lock: Optional[Secret] = None
) -> Tuple[List[Proof], List[Proof]]:
"""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).
- 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."""
logger.debug("Calling split. POST /split")
total = sum_proofs(proofs)
@@ -454,18 +447,18 @@ class LedgerAPI:
scnd_outputs = amount_split(scnd_amt)
amounts = frst_outputs + scnd_outputs
- if scnd_secret is None:
+ if secret_lock is None:
secrets = [self._generate_secret() for _ in range(len(amounts))]
else:
- scnd_secrets = self.generate_secrets(scnd_secret, len(scnd_outputs))
- logger.debug(f"Creating proofs with custom secrets: {scnd_secrets}")
- assert len(scnd_secrets) == len(
+ secret_locks = [secret_lock.serialize() for i in range(len(scnd_outputs))]
+ logger.debug(f"Creating proofs with custom secrets: {secret_locks}")
+ assert len(secret_locks) == len(
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)
secrets = [
self._generate_secret() for s in range(len(frst_outputs))
- ] + scnd_secrets
+ ] + secret_locks
assert len(secrets) == len(
amounts
@@ -477,7 +470,7 @@ class LedgerAPI:
# construct payload
def _splitrequest_include_fields(proofs):
"""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 {
"amount": ...,
"outputs": ...,
@@ -572,6 +565,8 @@ class LedgerAPI:
class Wallet(LedgerAPI):
"""Minimal wallet wrapper."""
+ private_key: Optional[PrivateKey] = None
+
def __init__(self, url: str, db: str, name: str = "no_name"):
super().__init__(url)
self.db = Database("wallet", db)
@@ -579,6 +574,17 @@ class Wallet(LedgerAPI):
self.name = name
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 ----------
async def load_mint(self, keyset_id: str = ""):
@@ -648,27 +654,67 @@ class Wallet(LedgerAPI):
self.proofs += 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(
self,
proofs: List[Proof],
- scnd_script: Optional[str] = None,
- scnd_siganture: Optional[str] = None,
):
- if scnd_script and scnd_siganture:
- 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)
+ proofs = await self.add_witnesses_to_proofs(proofs)
return await self.split(proofs, sum_proofs(proofs))
async def split(
self,
proofs: List[Proof],
amount: int,
- scnd_secret: Optional[str] = None,
+ secret_lock: Optional[Secret] = None,
):
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:
raise Exception("received no splits.")
@@ -957,7 +1003,7 @@ class Wallet(LedgerAPI):
self,
proofs: List[Proof],
amount: int,
- scnd_secret: Optional[str] = None,
+ secret_lock: Optional[Secret] = None,
set_reserved: bool = False,
):
"""
@@ -966,25 +1012,26 @@ class Wallet(LedgerAPI):
Args:
proofs (List[Proof]): Proofs to split
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
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.
"""
- if scnd_secret:
- logger.debug(f"Spending conditions: {scnd_secret}")
+ if secret_lock:
+ logger.debug(f"Spending conditions: {secret_lock}")
spendable_proofs = await self._select_proofs_to_send(proofs, amount)
keep_proofs, send_proofs = await self.split(
- spendable_proofs, amount, scnd_secret
+ spendable_proofs, amount, secret_lock
)
if set_reserved:
await self.set_reserved(send_proofs, reserved=True)
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()
txin_redeemScript = step0_carol_checksig_redeemscrip(alice_privkey.pub)
txin_p2sh_address = step1_carol_create_p2sh_address(txin_redeemScript)
@@ -997,7 +1044,61 @@ class Wallet(LedgerAPI):
address=str(txin_p2sh_address),
)
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 ----------
diff --git a/setup.py b/setup.py
index 6a2a070..7025cc3 100644
--- a/setup.py
+++ b/setup.py
@@ -13,7 +13,7 @@ entry_points = {"console_scripts": ["cashu = cashu.wallet.cli.cli:cli"]}
setuptools.setup(
name="cashu",
- version="0.12.1",
+ version="0.12.2",
description="Ecash wallet and mint for Bitcoin Lightning",
long_description=long_description,
long_description_content_type="text/markdown",
diff --git a/tests/conftest.py b/tests/conftest.py
index 25cf777..6f61c89 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,5 +1,6 @@
import multiprocessing
import os
+import secrets
import shutil
import time
from pathlib import Path
@@ -58,6 +59,7 @@ def mint():
settings.port = 3337
settings.mint_url = "http://localhost:3337"
settings.port = settings.mint_listen_port
+ settings.nostr_private_key = secrets.token_hex(32) # wrong private key
config = uvicorn.Config(
"cashu.mint.app:app",
port=settings.mint_listen_port,
diff --git a/tests/test_wallet.py b/tests/test_wallet.py
index 68c051e..34c7a6b 100644
--- a/tests/test_wallet.py
+++ b/tests/test_wallet.py
@@ -1,10 +1,12 @@
-import time
+import asyncio
+import secrets
from typing import List
import pytest
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.migrations import migrate_databases
from cashu.core.settings import settings
@@ -20,9 +22,10 @@ async def assert_err(f, msg):
try:
await f
except Exception as exc:
- assert exc.args[0] == msg, Exception(
- f"Expected error: {msg}, got: {exc.args[0]}"
- )
+ 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):
@@ -242,43 +245,113 @@ async def test_split_invalid_amount(wallet1: Wallet):
@pytest.mark.asyncio
-async def test_split_with_secret(wallet1: Wallet):
+async def test_create_p2pk_pubkey(wallet1: Wallet):
await wallet1.mint(64)
- secret = f"asdasd_{time.time()}"
- w1_frst_proofs, w1_scnd_proofs = await wallet1.split(
- wallet1.proofs, 32, scnd_secret=secret
- )
- # check if index prefix is in secret
- assert w1_scnd_proofs[0].secret == "0:" + secret
+ pubkey = await wallet1.create_p2pk_pubkey()
+ PublicKey(bytes.fromhex(pubkey), raw=True)
@pytest.mark.asyncio
-async def test_redeem_without_secret(wallet1: Wallet):
+async def test_p2pk(wallet1: Wallet, wallet2: Wallet):
await wallet1.mint(64)
- # strip away the secrets
- w1_scnd_proofs_manipulated = wallet1.proofs.copy()
- for p in w1_scnd_proofs_manipulated:
- p.secret = ""
- await assert_err(
- wallet1.redeem(w1_scnd_proofs_manipulated),
- "Mint Error: no secret in proof.",
+ pubkey_wallet2 = await wallet2.create_p2pk_pubkey()
+ # p2pk test
+ secret_lock = await wallet1.create_p2pk_lock(pubkey_wallet2) # sender side
+ _, send_proofs = await wallet1.split_to_send(
+ wallet1.proofs, 8, secret_lock=secret_lock
)
+ await wallet2.redeem(send_proofs)
@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)
- # 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)
-
- assert send_proofs[0].secret.startswith("P2SH:")
-
- frst_proofs, scnd_proofs = await wallet2.redeem(
- send_proofs, scnd_script=p2shscript.script, scnd_siganture=p2shscript.signature
+ pubkey_wallet2 = await wallet2.create_p2pk_pubkey() # receiver side
+ # sender side
+ secret_lock = await wallet1.create_p2pk_lock(pubkey_wallet2) # 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.")
+
+
+@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(scnd_proofs) == 1
assert sum_proofs(scnd_proofs) == 8
@@ -286,40 +359,11 @@ async def no_test_p2sh(wallet1: Wallet, wallet2: Wallet):
@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)
- # 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_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
+ 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