Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stamps #295

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ mypy:
poetry run mypy cashu --ignore-missing

flake8:
poetry run flake8 cashu
poetry run flake8 cashu tests

format: isort black

Expand Down
16 changes: 16 additions & 0 deletions cashu/core/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,22 @@ class PostRestoreResponse(BaseModel):
promises: List[BlindedSignature] = []


# ------- API: STAMP -------


class StampSignature(BaseModel):
e: str
s: str


class PostStampRequest(BaseModel):
proofs: List[Proof]


class PostStampResponse(BaseModel):
sigs: List[StampSignature]


# ------- KEYSETS -------


Expand Down
47 changes: 46 additions & 1 deletion cashu/core/crypto/b_dhke.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"""

import hashlib
from typing import Optional
from typing import Optional, Tuple

from secp256k1 import PrivateKey, PublicKey

Expand Down Expand Up @@ -74,6 +74,51 @@ def verify(a: PrivateKey, C: PublicKey, secret_msg: str) -> bool:
return C == Y.mult(a) # type: ignore


# stamps


def hash_e(*args) -> bytes:
e_ = ""
for pk in args:
assert isinstance(pk, PublicKey), "object is not of type PublicKey"
e_ += pk.serialize(compressed=False).hex()
e = hashlib.sha256(e_.encode("utf-8")).digest()
return e


def stamp_step1_bob(
secret_msg: str, C: PublicKey, a: PrivateKey, p_bytes: bytes = b""
) -> Tuple[PrivateKey, PrivateKey]:
if p_bytes:
# deterministic p for testing
p = PrivateKey(privkey=p_bytes, raw=True)
else:
# normally, we generate a random p
p = PrivateKey()
assert p.pubkey
R1: PublicKey = p.pubkey # R1 = pG
Y: PublicKey = hash_to_curve(secret_msg.encode("utf-8"))
R2: PublicKey = Y.mult(p) # type: ignore # R2 = pY
print(R1.serialize().hex(), R2.serialize().hex())
e = hash_e(R1, R2, Y, C)
s = p.tweak_add(a.tweak_mul(e)) # s = p + ea
spk = PrivateKey(s, raw=True)
epk = PrivateKey(e, raw=True)
return epk, spk


def stamp_step2_alice_verify(
secret_msg: str, C: PublicKey, s: PrivateKey, e: PrivateKey, A: PublicKey
) -> bool:
Y: PublicKey = hash_to_curve(secret_msg.encode("utf-8"))
assert s.pubkey
R1: PublicKey = s.pubkey - A.mult(e) # type: ignore # R1 = sG - eA
R2: PublicKey = Y.mult(s) - C.mult(e) # type: ignore # R2 = sY - eC
print(R1.serialize().hex(), R2.serialize().hex())
e_bytes = e.private_key
return e_bytes == hash_e(R1, R2, Y, C)


# Below is a test of a simple positive and negative case

# # Alice's keys
Expand Down
18 changes: 18 additions & 0 deletions cashu/mint/ledger.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
Secret,
SecretKind,
SigFlags,
StampSignature,
)
from ..core.crypto import b_dhke
from ..core.crypto.keys import derive_pubkey, random_hash
Expand Down Expand Up @@ -1118,3 +1119,20 @@ async def restore(
return_outputs.append(output)
logger.trace(f"promise found: {promise}")
return return_outputs, promises

async def stamp(self, proofs: List[Proof]) -> List[StampSignature]:
signatures: List[StampSignature] = []
if not all([self._verify_proof_bdhke(p) for p in proofs]):
raise Exception("Some proofs are invalid")
for proof in proofs:
assert proof.id
private_key_amount = self.keysets.keysets[proof.id].private_keys[
proof.amount
]
e, s = b_dhke.stamp_step1_bob(
secret_msg=proof.secret,
C=PublicKey(bytes.fromhex(proof.C), raw=True),
a=private_key_amount,
)
signatures.append(StampSignature(e=e.serialize(), s=s.serialize()))
return signatures
15 changes: 15 additions & 0 deletions cashu/mint/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
PostSplitRequest,
PostSplitResponse,
PostSplitResponse_Deprecated,
PostStampRequest,
PostStampResponse,
)
from ..core.errors import CashuError
from ..core.settings import settings
Expand Down Expand Up @@ -282,3 +284,16 @@ async def restore(payload: PostMintRequest) -> PostRestoreResponse:
assert payload.outputs, Exception("no outputs provided.")
outputs, promises = await ledger.restore(payload.outputs)
return PostRestoreResponse(outputs=outputs, promises=promises)


@router.post(
"/stamp",
name="Stamp",
summary="Request signatures on proofs",
response_model=PostStampResponse,
response_description=("List of signatures on proofs."),
)
async def stamp(payload: PostStampRequest) -> PostStampResponse:
assert payload.proofs, Exception("no proofs provided")
signatures = await ledger.stamp(payload.proofs)
return PostStampResponse(sigs=signatures)
29 changes: 29 additions & 0 deletions cashu/wallet/wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
PostMintResponse,
PostRestoreResponse,
PostSplitRequest,
PostStampRequest,
PostStampResponse,
Proof,
Secret,
SecretKind,
Expand Down Expand Up @@ -608,6 +610,30 @@ async def restore_promises(
returnObj = PostRestoreResponse.parse_obj(reponse_dict)
return returnObj.outputs, returnObj.promises

@async_set_requests
async def get_proofs_stamps(self, proofs: List[Proof]):
"""
Cheks whether the secrets in proofs are already spent or not and returns a list of booleans.
"""
payload = PostStampRequest(proofs=proofs)

def _get_proofs_stamps_include_fields(proofs):
"""strips away fields from the model that aren't necessary for this endpoint"""
proofs_include = {"id", "amount", "secret", "C"}
return {
"proofs": {i: proofs_include for i in range(len(proofs))},
}

resp = self.s.post(
self.url + "/stamp",
json=payload.dict(include=_get_proofs_stamps_include_fields(proofs)), # type: ignore
)
self.raise_on_error(resp)

return_dict = resp.json()
stamps = PostStampResponse.parse_obj(return_dict)
return stamps


class Wallet(LedgerAPI):
"""Minimal wallet wrapper."""
Expand Down Expand Up @@ -1185,6 +1211,9 @@ async def pay_lightning(
async def check_proof_state(self, proofs):
return await super().check_proof_state(proofs)

async def get_proofs_stamps(self, proofs):
return await super().get_proofs_stamps(proofs)

# ---------- TOKEN MECHANIS ----------

async def _store_proofs(self, proofs):
Expand Down
76 changes: 75 additions & 1 deletion tests/test_crypto.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
from cashu.core.crypto.b_dhke import hash_to_curve, step1_alice, step2_bob, step3_alice
from cashu.core.crypto.b_dhke import (
hash_to_curve,
stamp_step1_bob,
stamp_step2_alice_verify,
step1_alice,
step2_bob,
step3_alice,
)
from cashu.core.crypto.secp import PrivateKey, PublicKey


Expand Down Expand Up @@ -107,3 +114,70 @@ def test_step3():
C.serialize().hex()
== "03c724d7e6a5443b39ac8acf11f40420adc4f99a02e7cc1b57703d9391f6d129cd"
)


def test_stamp_sign_verify():
secret_msg = "test_message"
r = PrivateKey(
privkey=bytes.fromhex(
"0000000000000000000000000000000000000000000000000000000000000001"
),
raw=True,
)
B_, _ = step1_alice(secret_msg, blinding_factor=r)
a = PrivateKey(
privkey=bytes.fromhex(
"0000000000000000000000000000000000000000000000000000000000000001"
),
raw=True,
)
A = a.pubkey
assert A

C_ = step2_bob(B_, a)
C = step3_alice(C_, r, A)
e, s = stamp_step1_bob(secret_msg=secret_msg, C=C, a=a)
assert stamp_step2_alice_verify(secret_msg=secret_msg, C=C, s=s, e=e, A=A)

# wrong secret
secret_msg_wrong = secret_msg + "wrong"
assert not stamp_step2_alice_verify(secret_msg=secret_msg_wrong, C=C, s=s, e=e, A=A)

# wrong C
C_wrong = PublicKey(
bytes.fromhex(
"02c724d7e6a5443b39ac8acf11f40420adc4f99a02e7cc1b57703d9391f6d129cd"
),
raw=True,
)
assert not stamp_step2_alice_verify(secret_msg=secret_msg, C=C_wrong, s=s, e=e, A=A)

# wrong s
s_wrong = PrivateKey(
privkey=bytes.fromhex(
"0000000000000000000000000000000000000000000000000000000000000001"
),
raw=True,
)
assert not stamp_step2_alice_verify(secret_msg=secret_msg, C=C, s=s_wrong, e=e, A=A)

# wrong e
e_wrong = PrivateKey(
privkey=bytes.fromhex(
"0000000000000000000000000000000000000000000000000000000000000001"
),
raw=True,
)
assert not stamp_step2_alice_verify(secret_msg=secret_msg, C=C, s=s, e=e_wrong, A=A)

# wrong A
a_wrong = PrivateKey(
privkey=bytes.fromhex(
"0000000000000000000000000000000000000000000000000000000000000002"
),
raw=True,
)
assert a_wrong.pubkey
assert not stamp_step2_alice_verify(
secret_msg=secret_msg, C=C, s=s, e=e, A=a_wrong.pubkey
)
8 changes: 8 additions & 0 deletions tests/test_wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -593,3 +593,11 @@ async def test_restore_wallet_after_send_and_self_receive_nonquadratic_value(
assert wallet3.balance == 182
await wallet3.invalidate(wallet3.proofs)
assert wallet3.balance == 64


@pytest.mark.asyncio
async def test_stamp_proofs(wallet1: Wallet):
await wallet1.mint(17)
assert wallet1.balance == 17
resp = await wallet1.get_proofs_stamps(wallet1.proofs)
assert resp.dict()["sigs"]
Loading