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

[Wallet/mint] P2PK with timelocks #270

Merged
merged 20 commits into from
Jul 1, 2023
Merged

[Wallet/mint] P2PK with timelocks #270

merged 20 commits into from
Jul 1, 2023

Conversation

callebtc
Copy link
Collaborator

@callebtc callebtc commented Jun 30, 2023

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 random secret that the mint blind-signs. If anyone later presents secret and the signature C 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 of secret. If the mint encounters a secret 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 <str>, 
  {
    "data": <str>,
    "nonce": <str>,
    "timelock": <int>,
    "tags": [[ "tag_name", "tag_value"],  ... ]
  }
]
  • kind is the kind of lock used, currently: P2PK or P2SH
  • data is the lock (pubkey for P2PK, address for P2SH)
  • nonce is random data (useful if multiple proofs use the same data lock)
  • timelock is the unix time at which the lock will expire (and everyone who knows secret can redeem the token)
  • tags are additional data that are committed to

Here 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 send Carol tokens that only she can unlock. For this, Carol provides her public key <carol_pub>. Alice sends a POST /split request to the mint providing proofs as inputs and (a subset of) outputs as the tokens she wants to send to Alice.

For those tokens she wants to send to Carol, she choses the well-known secret format above. She chooses the kind P2PK, inserts <carol_pub> into the data field, and chooses a timelock of 24 hours by settings timelock 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 only Carol 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 next POST /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.

  • If secret.tags contains no refund tag, the token is spendable by anyone who knows the secret.
  • If secret.tags contains a tag ["refund", <refund_pubkey>], then the proof can be spent if the owner of <refund_pubkey> provided their signature in proof.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 on sha256(secret) of each Proof she wants to redeem. To do that, Carol redeems the token as usual using the endpoint POST /split -d <PostSplitRequest> but she includes a signature p2pksig on sha256(secret) in every single Proof inside the PostSplitRequest.

The Proof object is extended by a field p2pksig that is the signature on sha256(secret) by <carol_pub>. The p2pksig is unique to each Proof since at least the nonce field in secret should be unique. A Proof is now of the form

{
  "amount": int, 
  "secret": str,
  "C": str,
  "id": str,
  "p2pksig": <signature_hex> <-- new
}

Signature verification

When a token is redeemed (i.e. spent by Carol), the mint encounters secret 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 the data field from secret which is <carol_pub>. The mint then verifies the signature p2pksig on sha256(secret) is by <carol_pub>.

Implementation details

Nutshell uses settings.nostr_private_key to generate the receiving lock (carol_pub) and uses it to sign sha256(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

  • add NOSTR_PRIVATE_KEY (hex or nsec) to .env
  • receiver: create a lock with cashu lock
  • sender: send to this lock with cashu send <amount> --lock P2PK:<pubkey>
  • receiver: receive with cashu receive <token>

Changes

Mint

  • Accepts secrets up to 512 characters long
  • Refactor P2SH code
  • Add P2PK
  • Add timelocks check to P2PK and P2SH

Wallet:

  • Remove need to specify lock for cashu receive (P2SH and P2PK)
  • cashu lock now shows P2PK key per default and cashu lock --p2sh creates P2SH locks
  • The recipient can see their public key lock using the command cashu lock.
  • Recipients of either P2SH and P2PK tokens do not have to enter their lock anymore (cashu receive <token> --lock <address> is now cashu receive <token>)
  • Add new setting TIMELOCK_DELTA_SECONDS
  • Fix nostr receive

Todo

  • use Schnorr signatures
  • decide on which private key to use
  • if nostr: parse nsec from settings, not only hex
  • find a better way to serialize the secret
  • then add optional refund pubkey
  • make timelocks optional as well
  • add tests for P2PK
  • add tests for timelocks for P2PK and P2SH
  • write NUT (add test vectors)
  • add Secret object that can be serialized instead of this ugly :-seperated string
  • sign messages only after hashing

@callebtc callebtc changed the title [Wallet/mint] P2PK [Wallet/mint] P2PK with timelocks Jun 30, 2023
@codecov
Copy link

codecov bot commented Jun 30, 2023

Codecov Report

Patch coverage: 56.71% and project coverage change: +0.32 🎉

Comparison is base (4beaf8f) 56.27% compared to head (5f2c61f) 56.59%.

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     
Impacted Files Coverage Δ
setup.py 0.00% <ø> (ø)
cashu/mint/ledger.py 33.16% <12.50%> (-1.75%) ⬇️
cashu/wallet/cli/cli.py 49.51% <15.00%> (+<0.01%) ⬆️
cashu/wallet/nostr.py 42.85% <25.00%> (+0.19%) ⬆️
cashu/core/p2pk.py 36.84% <36.84%> (ø)
cashu/wallet/helpers.py 82.40% <50.00%> (-2.36%) ⬇️
cashu/core/base.py 94.09% <87.09%> (-1.08%) ⬇️
cashu/wallet/wallet.py 82.73% <93.10%> (+0.62%) ⬆️
cashu/core/script.py 46.34% <100.00%> (+3.15%) ⬆️
cashu/core/settings.py 96.38% <100.00%> (+0.04%) ⬆️
... and 1 more

... and 1 file with indirect coverage changes

☔ View full report in Codecov by Sentry.
📢 Do you have feedback about the report comment? Let us know in this issue.

@callebtc callebtc changed the title [Wallet/mint] P2PK with timelocks [WIP] [Wallet/mint] P2PK with timelocks Jun 30, 2023
@callebtc callebtc marked this pull request as draft June 30, 2023 20:52
@callebtc callebtc requested a review from xphade July 1, 2023 00:36
@callebtc callebtc added enhancement New feature or request wallet About the Nutshell wallet mint About the Nutshell mint nuts NUT specs related labels Jul 1, 2023
cashu/wallet/wallet.py Outdated Show resolved Hide resolved
cashu/wallet/wallet.py Outdated Show resolved Hide resolved
cashu/wallet/helpers.py Outdated Show resolved Hide resolved
Comment on lines 135 to 136
elif all([p.secret.startswith("P2PK:") for p in proofs]):
p2pk_signatures = await wallet.sign_p2pk_with_privatekey(proofs)
Copy link
Collaborator Author

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.

cashu/core/p2pk.py Outdated Show resolved Hide resolved
cashu/mint/ledger.py Outdated Show resolved Hide resolved
cashu/mint/ledger.py Outdated Show resolved Hide resolved
@callebtc callebtc marked this pull request as ready for review July 1, 2023 21:41
@callebtc callebtc changed the title [WIP] [Wallet/mint] P2PK with timelocks [Wallet/mint] P2PK with timelocks Jul 1, 2023
@callebtc callebtc merged commit 01d4983 into main Jul 1, 2023
5 checks passed
@callebtc callebtc deleted the p2pk_timelocks branch July 1, 2023 23:56
Comment on lines +283 to +287
# 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
Copy link
Collaborator Author

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.

Comment on lines +259 to +297
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
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2PK mint validation

Comment on lines +577 to +586
# 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
Copy link
Collaborator Author

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request mint About the Nutshell mint nuts NUT specs related wallet About the Nutshell wallet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

1 participant