-
-
Notifications
You must be signed in to change notification settings - Fork 68
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
[Wallet/mint] P2PK with timelocks #270
Conversation
Codecov ReportPatch coverage:
Additional details and impacted files@@ Coverage Diff @@
## main #270 +/- ##
==========================================
+ Coverage 56.27% 56.59% +0.32%
==========================================
Files 42 43 +1
Lines 3421 3541 +120
==========================================
+ Hits 1925 2004 +79
- Misses 1496 1537 +41
☔ View full report in Codecov by Sentry. |
cashu/wallet/helpers.py
Outdated
elif all([p.secret.startswith("P2PK:") for p in proofs]): | ||
p2pk_signatures = await wallet.sign_p2pk_with_privatekey(proofs) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Receiver: aggregates all signatures for each 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These comments are all wrong and from a previous implementation, please ignore.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2PK mint validation
# 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed to wallet-generated seed in #131
Pay to Public Key (P2PK) allows the sender to lock tokens to a recipient public key. The lock is enforced by the mint and unlocked using a signature of the intended recipient.
New Secret format
For a normal ecash operation,
Alice
generates a randomsecret
that the mint blind-signs. If anyone later presentssecret
and the signatureC
to the mint, the ecash transaction is valid. Naturally, we want to express more complex spending conditions for transactions. That's where P2PK and P2SH spending conditions come in.To lock tokens to a spending condition,
Alice
needs to generate a special type ofsecret
. If the mint encounters asecret
with a well-known format corresponding to P2PK or P2SH locks, it will demand other conditions to be met in order to validate the transaction.Secret
The well-known
secret
is as follows:kind
is the kind of lock used, currently:P2PK
orP2SH
data
is the lock (pubkey for P2PK, address for P2SH)nonce
is random data (useful if multiple proofs use the samedata
lock)timelock
is the unix time at which the lock will expire (and everyone who knowssecret
can redeem the token)tags
are additional data that are committed toHere is one example
secret
:["P2PK", {"data": "03e7f4164a8db54e6b6c67bb9c9885d5aec7e24075af8928411478fb9d74aec4ca", "nonce": "dbab884a7b3e427066eb4e0da1776984", "timelock": 1688247220}]
In the following, we explain how P2PK works and defer documenting P2SH to a later point in time.
Locking P2PK tokens
Alice
wants to sendCarol
tokens that only she can unlock. For this,Carol
provides her public key<carol_pub>
.Alice
sends aPOST /split
request to the mint providingproofs
as inputs and (a subset of)outputs
as the tokens she wants to send toAlice
.For those tokens she wants to send to
Carol
, she choses the well-knownsecret
format above. She chooses the kindP2PK
, inserts<carol_pub>
into thedata
field, and chooses a timelock of 24 hours by settingstimelock
to the current unix timestamp + 24 hours. She proceeds to unblind the signature as she would normally. The resulting tokens are locked by P2PK and onlyCarol
can redeem them in the next 24 hours.Unlocking P2PK tokens
Spending conditions are enforced by the mint. For
Carol
to unlock proofs in the nextPOST /split
request she needs to provide proof that she can indeed unlock the proof (the witness). Note that the witness is revealed to the mint only during the spending of the token, not while locking (since it is blind-signed).There are two conditions for unlocking a P2PK token that are enforced by the mint.
Timelock
If the current unix time is later than the timelock specified in the secret, i.e.,
now > timelock_int
, the timelock spending path is triggered. There are currently two options to continune.secret.tags
contains norefund
tag, the token is spendable by anyone who knows the secret.secret.tags
contains a tag["refund", <refund_pubkey>
], then the proof can be spent if the owner of<refund_pubkey>
provided their signature inproof.p2pksig
. This will typically be the sender's pubkey, in this case<alice_pub>
.Signature
To redeem the token using the signature path,
Carol
provides a Schnorr signature onsha256(secret)
of eachProof
she wants to redeem. To do that, Carol redeems the token as usual using the endpointPOST /split -d <PostSplitRequest>
but she includes a signaturep2pksig
onsha256(secret)
in every singleProof
inside thePostSplitRequest
.The
Proof
object is extended by a fieldp2pksig
that is the signature onsha256(secret)
by<carol_pub>
. Thep2pksig
is unique to eachProof
since at least thenonce
field insecret
should be unique. AProof
is now of the formSignature verification
When a token is redeemed (i.e. spent by
Carol
), the mint encounterssecret
and recognizes its well-known format. The mint now demands P2PK validation. For that, the mint first checks whether the time lock is in the past which would validate the spend attempt. If not, the mint parses thedata
field fromsecret
which is<carol_pub>
. The mint then verifies the signaturep2pksig
onsha256(secret)
is by<carol_pub>
.Implementation details
Nutshell uses
settings.nostr_private_key
to generate the receiving lock (carol_pub
) and uses it to signsha256(secret)
.Note: In the current implement, this only works if the nostr private key is stored in hex format. I did not add deserialization of different formats to this part but it's already implemented in the rest of the nostr features.
Note: the recipient could also be required to sign the entire transaction at once including the outputs but that would require more heavy lifting on the mint side. Please provide feedback.
Usage
To use this feature in your Nutshell wallet
NOSTR_PRIVATE_KEY
(hex or nsec) to.env
cashu lock
cashu send <amount> --lock P2PK:<pubkey>
cashu receive <token>
Changes
Mint
Wallet:
cashu receive
(P2SH and P2PK)cashu lock
now shows P2PK key per default andcashu lock --p2sh
creates P2SH lockscashu lock
.cashu receive <token> --lock <address>
is nowcashu receive <token>
)TIMELOCK_DELTA_SECONDS
Todo
secret
Secret
object that can be serialized instead of this ugly:
-seperated string