mirror of
https://github.com/aljazceru/nutshell.git
synced 2026-01-06 02:14:21 +01:00
[WIP] [NUTs] NUT-06 update: deprecate amount field in /split (#263)
* mint upgraded and still backwards compatible * cleanup * fix tests * fix things * add deprecated message to new struct * fix test * fix typo * readd endpoint that got lost during merge * version bump in pyproject.toml * remove wallet backwards compatibility because it makes no sense * comment for backwards compat * fix comment
This commit is contained in:
@@ -271,11 +271,6 @@ class PostMintRequest(BaseModel):
|
||||
outputs: List[BlindedMessage]
|
||||
|
||||
|
||||
class PostMintResponseLegacy(BaseModel):
|
||||
# NOTE: Backwards compability for < 0.8.0 where we used a simple list and not a key-value dictionary
|
||||
__root__: List[BlindedSignature] = []
|
||||
|
||||
|
||||
class PostMintResponse(BaseModel):
|
||||
promises: List[BlindedSignature] = []
|
||||
|
||||
@@ -305,7 +300,7 @@ class GetMeltResponse(BaseModel):
|
||||
|
||||
class PostSplitRequest(BaseModel):
|
||||
proofs: List[Proof]
|
||||
amount: int
|
||||
amount: Optional[int] = None # deprecated since 0.13.0
|
||||
outputs: List[BlindedMessage]
|
||||
# signature: Optional[str] = None
|
||||
|
||||
@@ -323,8 +318,14 @@ class PostSplitRequest(BaseModel):
|
||||
|
||||
|
||||
class PostSplitResponse(BaseModel):
|
||||
fst: List[BlindedSignature]
|
||||
snd: List[BlindedSignature]
|
||||
promises: List[BlindedSignature]
|
||||
|
||||
|
||||
# deprecated since 0.13.0
|
||||
class PostSplitResponse_Deprecated(BaseModel):
|
||||
fst: List[BlindedSignature] = []
|
||||
snd: List[BlindedSignature] = []
|
||||
deprecated: str = "The amount field is deprecated since 0.13.0"
|
||||
|
||||
|
||||
# ------- API: CHECK -------
|
||||
|
||||
@@ -444,14 +444,6 @@ class Ledger:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _verify_split_amount(self, amount: int) -> None:
|
||||
"""Split amount like output amount can't be negative or too big."""
|
||||
try:
|
||||
self._verify_amount(amount)
|
||||
except:
|
||||
# For better error message
|
||||
raise Exception("invalid split amount: " + str(amount))
|
||||
|
||||
def _verify_amount(self, amount: int) -> int:
|
||||
"""Any amount used should be a positive integer not larger than 2^MAX_ORDER."""
|
||||
valid = (
|
||||
@@ -463,12 +455,18 @@ class Ledger:
|
||||
return amount
|
||||
|
||||
def _verify_equation_balanced(
|
||||
self, proofs: List[Proof], outs: List[BlindedSignature]
|
||||
self,
|
||||
proofs: List[Proof],
|
||||
outs: Union[List[BlindedSignature], List[BlindedMessage]],
|
||||
) -> None:
|
||||
"""Verify that Σoutputs - Σinputs = 0."""
|
||||
"""Verify that Σinputs - Σoutputs = 0.
|
||||
Outputs can be BlindedSignature or BlindedMessage.
|
||||
"""
|
||||
sum_inputs = sum(self._verify_amount(p.amount) for p in proofs)
|
||||
sum_outputs = sum(self._verify_amount(p.amount) for p in outs)
|
||||
assert sum_outputs - sum_inputs == 0
|
||||
assert (
|
||||
sum_outputs - sum_inputs == 0
|
||||
), "inputs do not have same amount as outputs"
|
||||
|
||||
async def _request_lightning_invoice(self, amount: int):
|
||||
"""Generate a Lightning invoice using the funding source backend.
|
||||
@@ -1007,10 +1005,11 @@ class Ledger:
|
||||
|
||||
async def split(
|
||||
self,
|
||||
*,
|
||||
proofs: List[Proof],
|
||||
amount: int,
|
||||
outputs: List[BlindedMessage],
|
||||
keyset: Optional[MintKeyset] = None,
|
||||
amount: Optional[int] = None, # backwards compatibility < 0.13.0
|
||||
):
|
||||
"""Consumes proofs and prepares new promises based on the amount split. Used for splitting tokens
|
||||
Before sending or for redeeming tokens for new ones that have been received by another wallet.
|
||||
@@ -1031,15 +1030,15 @@ class Ledger:
|
||||
|
||||
await self._set_proofs_pending(proofs)
|
||||
|
||||
total = sum_proofs(proofs)
|
||||
total_amount = sum_proofs(proofs)
|
||||
|
||||
try:
|
||||
logger.trace(f"verifying _verify_split_amount")
|
||||
# verify that amount is kosher
|
||||
self._verify_split_amount(amount)
|
||||
self._verify_amount(total_amount)
|
||||
|
||||
# verify overspending attempt
|
||||
if amount > total:
|
||||
raise Exception("split amount is higher than the total sum.")
|
||||
self._verify_equation_balanced(proofs, outputs)
|
||||
|
||||
logger.trace("verifying proofs: _verify_proofs_and_outputs")
|
||||
await self._verify_proofs_and_outputs(proofs, outputs)
|
||||
@@ -1055,20 +1054,32 @@ class Ledger:
|
||||
# delete proofs from pending list
|
||||
await self._unset_proofs_pending(proofs)
|
||||
|
||||
# split outputs according to amount
|
||||
outs_fst = amount_split(total - amount)
|
||||
B_fst = [od for od in outputs[: len(outs_fst)]]
|
||||
B_snd = [od for od in outputs[len(outs_fst) :]]
|
||||
# BEGIN backwards compatibility < 0.13.0
|
||||
if amount is not None:
|
||||
logger.debug(
|
||||
"Split: Client provided `amount` - backwards compatibility response pre 0.13.0"
|
||||
)
|
||||
# split outputs according to amount
|
||||
total = sum_proofs(proofs)
|
||||
if amount > total:
|
||||
raise Exception("split amount is higher than the total sum.")
|
||||
outs_fst = amount_split(total - amount)
|
||||
B_fst = [od for od in outputs[: len(outs_fst)]]
|
||||
B_snd = [od for od in outputs[len(outs_fst) :]]
|
||||
|
||||
# generate promises
|
||||
prom_fst, prom_snd = await self._generate_promises(
|
||||
B_fst, keyset
|
||||
), await self._generate_promises(B_snd, keyset)
|
||||
# generate promises
|
||||
prom_fst = await self._generate_promises(B_fst, keyset)
|
||||
prom_snd = await self._generate_promises(B_snd, keyset)
|
||||
promises = prom_fst + prom_snd
|
||||
# END backwards compatibility < 0.13.0
|
||||
else:
|
||||
promises = await self._generate_promises(outputs, keyset)
|
||||
|
||||
# verify amounts in produced proofs
|
||||
self._verify_equation_balanced(proofs, prom_fst + prom_snd)
|
||||
# verify amounts in produced promises
|
||||
self._verify_equation_balanced(proofs, promises)
|
||||
|
||||
logger.trace(f"split successful")
|
||||
return promises
|
||||
return prom_fst, prom_snd
|
||||
|
||||
async def restore(
|
||||
|
||||
@@ -22,6 +22,7 @@ from ..core.base import (
|
||||
PostRestoreResponse,
|
||||
PostSplitRequest,
|
||||
PostSplitResponse,
|
||||
PostSplitResponse_Deprecated,
|
||||
)
|
||||
from ..core.errors import CashuError
|
||||
from ..core.settings import settings
|
||||
@@ -208,7 +209,7 @@ async def check_fees(payload: CheckFeesRequest) -> CheckFeesResponse:
|
||||
@router.post("/split", name="Split", summary="Split proofs at a specified amount")
|
||||
async def split(
|
||||
payload: PostSplitRequest,
|
||||
) -> Union[CashuError, PostSplitResponse]:
|
||||
) -> Union[CashuError, PostSplitResponse, PostSplitResponse_Deprecated]:
|
||||
"""
|
||||
Requetst a set of tokens with amount "total" to be split into two
|
||||
newly minted sets with amount "split" and "total-split".
|
||||
@@ -219,17 +220,36 @@ async def split(
|
||||
logger.trace(f"> POST /split: {payload}")
|
||||
assert payload.outputs, Exception("no outputs provided.")
|
||||
try:
|
||||
split_return = await ledger.split(
|
||||
payload.proofs, payload.amount, payload.outputs
|
||||
promises = await ledger.split(
|
||||
proofs=payload.proofs, outputs=payload.outputs, amount=payload.amount
|
||||
)
|
||||
except Exception as exc:
|
||||
return CashuError(code=0, error=str(exc))
|
||||
if not split_return:
|
||||
if not promises:
|
||||
return CashuError(code=0, error="there was an error with the split")
|
||||
frst_promises, scnd_promises = split_return
|
||||
resp = PostSplitResponse(fst=frst_promises, snd=scnd_promises)
|
||||
logger.trace(f"< POST /split: {resp}")
|
||||
return resp
|
||||
|
||||
if payload.amount:
|
||||
# BEGIN backwards compatibility < 0.13
|
||||
# old clients expect two lists of promises where the second one's amounts
|
||||
# sum up to `amount`. The first one is the rest.
|
||||
# The returned value `promises` has the form [keep1, keep2, ..., send1, send2, ...]
|
||||
# The sum of the sendx is `amount`. We need to split this into two lists and keep the order of the elements.
|
||||
frst_promises: List[BlindedSignature] = []
|
||||
scnd_promises: List[BlindedSignature] = []
|
||||
scnd_amount = 0
|
||||
for promise in promises[::-1]: # we iterate backwards
|
||||
if scnd_amount < payload.amount:
|
||||
scnd_promises.insert(0, promise) # and insert at the beginning
|
||||
scnd_amount += promise.amount
|
||||
else:
|
||||
frst_promises.insert(0, promise) # and insert at the beginning
|
||||
logger.trace(
|
||||
f"Split into keep: {len(frst_promises)}: {sum([p.amount for p in frst_promises])} sat and send: {len(scnd_promises)}: {sum([p.amount for p in scnd_promises])} sat"
|
||||
)
|
||||
return PostSplitResponse_Deprecated(fst=frst_promises, snd=scnd_promises)
|
||||
# END backwards compatibility < 0.13
|
||||
else:
|
||||
return PostSplitResponse(promises=promises)
|
||||
|
||||
|
||||
@router.post(
|
||||
|
||||
@@ -32,9 +32,9 @@ from ..core.base import (
|
||||
PostMeltRequest,
|
||||
PostMintRequest,
|
||||
PostMintResponse,
|
||||
PostMintResponseLegacy,
|
||||
PostRestoreResponse,
|
||||
PostSplitRequest,
|
||||
PostSplitResponse_Deprecated,
|
||||
Proof,
|
||||
Secret,
|
||||
SecretKind,
|
||||
@@ -487,11 +487,7 @@ class LedgerAPI(object):
|
||||
reponse_dict = resp.json()
|
||||
self.raise_on_error(reponse_dict)
|
||||
logger.trace("Lightning invoice checked. POST /mint")
|
||||
try:
|
||||
# backwards compatibility: parse promises < 0.8.0 with no "promises" field
|
||||
promises = PostMintResponseLegacy.parse_obj(reponse_dict).__root__
|
||||
except:
|
||||
promises = PostMintResponse.parse_obj(reponse_dict).promises
|
||||
promises = PostMintResponse.parse_obj(reponse_dict).promises
|
||||
|
||||
# bump secret counter in database
|
||||
await bump_secret_derivation(
|
||||
@@ -504,27 +500,16 @@ class LedgerAPI(object):
|
||||
self,
|
||||
proofs: List[Proof],
|
||||
outputs: List[BlindedMessage],
|
||||
secrets: List[str],
|
||||
rs: List[PrivateKey],
|
||||
amount: int,
|
||||
secret_lock: Optional[Secret] = None,
|
||||
) -> Tuple[List[BlindedSignature], List[BlindedSignature]]:
|
||||
"""Consume proofs and create new promises based on amount split.
|
||||
|
||||
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 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."""
|
||||
) -> List[BlindedSignature]:
|
||||
"""Consume proofs and create new promises based on amount split."""
|
||||
logger.debug("Calling split. POST /split")
|
||||
split_payload = PostSplitRequest(proofs=proofs, amount=amount, outputs=outputs)
|
||||
split_payload = PostSplitRequest(proofs=proofs, outputs=outputs)
|
||||
|
||||
# construct payload
|
||||
def _splitrequest_include_fields(proofs: List[Proof]):
|
||||
"""strips away fields from the model that aren't necessary for the /split"""
|
||||
proofs_include = {"id", "amount", "secret", "C", "p2shscript", "p2pksigs"}
|
||||
return {
|
||||
"amount": ...,
|
||||
"outputs": ...,
|
||||
"proofs": {i: proofs_include for i in range(len(proofs))},
|
||||
}
|
||||
@@ -537,13 +522,13 @@ class LedgerAPI(object):
|
||||
promises_dict = resp.json()
|
||||
self.raise_on_error(promises_dict)
|
||||
|
||||
promises_fst = [BlindedSignature(**p) for p in promises_dict["fst"]]
|
||||
promises_snd = [BlindedSignature(**p) for p in promises_dict["snd"]]
|
||||
mint_response = PostMintResponse.parse_obj(promises_dict)
|
||||
promises = [BlindedSignature(**p.dict()) for p in mint_response.promises]
|
||||
|
||||
if len(promises_fst) == 0 and len(promises_snd) == 0:
|
||||
if len(promises) == 0:
|
||||
raise Exception("received no splits.")
|
||||
|
||||
return promises_fst, promises_snd
|
||||
return promises
|
||||
|
||||
@async_set_requests
|
||||
async def check_proof_state(self, proofs: List[Proof]):
|
||||
@@ -1065,21 +1050,23 @@ class Wallet(LedgerAPI):
|
||||
amount: int,
|
||||
secret_lock: Optional[Secret] = None,
|
||||
) -> Tuple[List[Proof], List[Proof]]:
|
||||
"""Split proofs into two sets of proofs at a given amount.
|
||||
"""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 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.
|
||||
|
||||
Args:
|
||||
proofs (List[Proof]): Input proofs to split.
|
||||
amount (int): Amount to split at.
|
||||
secret_lock (Optional[Secret], optional): Secret to lock outputs to. Defaults to None.
|
||||
|
||||
Raises:
|
||||
Exception: Raises exception if no proofs have been provided
|
||||
Exception: Raises exception if no proofs are returned after splitting
|
||||
proofs (List[Proof]): _description_
|
||||
amount (int): _description_
|
||||
secret_lock (Optional[Secret], optional): _description_. Defaults to None.
|
||||
|
||||
Returns:
|
||||
Tuple[List[Proof], List[Proof]]: Two sets of proofs after splitting.
|
||||
_type_: _description_
|
||||
"""
|
||||
assert len(proofs) > 0, ValueError("no proofs provided.")
|
||||
assert len(proofs) > 0, "no proofs provided."
|
||||
assert sum_proofs(proofs) >= amount, "amount too large."
|
||||
assert amount > 0, "amount must be positive."
|
||||
|
||||
# potentially add witnesses to unlock provided proofs (if they indicate one)
|
||||
proofs = await self.add_witnesses_to_proofs(proofs)
|
||||
@@ -1125,36 +1112,25 @@ class Wallet(LedgerAPI):
|
||||
outputs = await self.add_witnesses_to_outputs(proofs, outputs)
|
||||
|
||||
# Call /split API
|
||||
promises_fst, promises_snd = await super().split(
|
||||
proofs, outputs, secrets, rs, amount, secret_lock
|
||||
)
|
||||
promises = await super().split(proofs, outputs)
|
||||
|
||||
# Construct proofs from returned promises (i.e., unblind the signatures)
|
||||
frst_proofs = self._construct_proofs(
|
||||
promises_fst,
|
||||
secrets[: len(promises_fst)],
|
||||
rs[: len(promises_fst)],
|
||||
derivation_paths,
|
||||
)
|
||||
scnd_proofs = self._construct_proofs(
|
||||
promises_snd,
|
||||
secrets[len(promises_fst) :],
|
||||
rs[len(promises_fst) :],
|
||||
derivation_paths,
|
||||
)
|
||||
new_proofs = self._construct_proofs(promises, secrets, rs, derivation_paths)
|
||||
|
||||
# remove used proofs from wallet and add new ones
|
||||
used_secrets = [p.secret for p in proofs]
|
||||
self.proofs = list(filter(lambda p: p.secret not in used_secrets, self.proofs))
|
||||
# add new proofs to wallet
|
||||
self.proofs += frst_proofs + scnd_proofs
|
||||
self.proofs += new_proofs
|
||||
# store new proofs in database
|
||||
await self._store_proofs(frst_proofs + scnd_proofs)
|
||||
# invalidate used proofs
|
||||
async with self.db.connect() as conn:
|
||||
for proof in proofs:
|
||||
await invalidate_proof(proof, db=self.db, conn=conn)
|
||||
return frst_proofs, scnd_proofs
|
||||
await self._store_proofs(new_proofs)
|
||||
# invalidate used proofs in database
|
||||
for proof in proofs:
|
||||
await invalidate_proof(proof, db=self.db)
|
||||
|
||||
keep_proofs = new_proofs[: len(frst_outputs)]
|
||||
send_proofs = new_proofs[len(frst_outputs) :]
|
||||
return keep_proofs, send_proofs
|
||||
|
||||
async def pay_lightning(
|
||||
self, proofs: List[Proof], invoice: str, fee_reserve_sat: int
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "cashu"
|
||||
version = "0.12.3"
|
||||
version = "0.13.0"
|
||||
description = "Ecash wallet and mint"
|
||||
authors = ["calle <callebtc@protonmail.com>"]
|
||||
license = "MIT"
|
||||
|
||||
@@ -27,6 +27,13 @@ def test_tokenv3_deserialize_serialize():
|
||||
assert token.serialize() == token_str
|
||||
|
||||
|
||||
def test_tokenv3_deserialize_with_memo():
|
||||
token_str = "cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIkplaFpMVTZuQ3BSZCIsICJhbW91bnQiOiAyLCAic2VjcmV0IjogIjBFN2lDazRkVmxSZjVQRjFnNFpWMnciLCAiQyI6ICIwM2FiNTgwYWQ5NTc3OGVkNTI5NmY4YmVlNjU1ZGJkN2Q2NDJmNWQzMmRlOGUyNDg0NzdlMGI0ZDZhYTg2M2ZjZDUifSwgeyJpZCI6ICJKZWhaTFU2bkNwUmQiLCAiYW1vdW50IjogOCwgInNlY3JldCI6ICJzNklwZXh3SGNxcXVLZDZYbW9qTDJnIiwgIkMiOiAiMDIyZDAwNGY5ZWMxNmE1OGFkOTAxNGMyNTliNmQ2MTRlZDM2ODgyOWYwMmMzODc3M2M0NzIyMWY0OTYxY2UzZjIzIn1dLCAibWludCI6ICJodHRwOi8vbG9jYWxob3N0OjMzMzgifV0sICJtZW1vIjogIlRlc3QgbWVtbyJ9"
|
||||
token = TokenV3.deserialize(token_str)
|
||||
assert token.serialize() == token_str
|
||||
assert token.memo == "Test memo"
|
||||
|
||||
|
||||
def test_calculate_number_of_blank_outputs():
|
||||
# Example from NUT-08 specification.
|
||||
fee_reserve_sat = 1000
|
||||
|
||||
@@ -198,7 +198,8 @@ async def test_split_more_than_balance(wallet1: Wallet):
|
||||
await wallet1.mint(64)
|
||||
await assert_err(
|
||||
wallet1.split(wallet1.proofs, 128),
|
||||
"Mint Error: split amount is higher than the total sum.",
|
||||
# "Mint Error: inputs do not have same amount as outputs",
|
||||
"amount too large.",
|
||||
)
|
||||
assert wallet1.balance == 64
|
||||
|
||||
@@ -274,7 +275,7 @@ async def test_split_invalid_amount(wallet1: Wallet):
|
||||
await wallet1.mint(64)
|
||||
await assert_err(
|
||||
wallet1.split(wallet1.proofs, -1),
|
||||
"Mint Error: invalid split amount: -1",
|
||||
"amount must be positive.",
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user