NUTXX - ECDH-derived Pay-to-Blinded-Key (P2BK)#300
NUTXX - ECDH-derived Pay-to-Blinded-Key (P2BK)#300robwoodgate wants to merge 5 commits intocashubtc:mainfrom
Conversation
d98d9d6 to
58231b0
Compare
d4rp4t
left a comment
There was a problem hiding this comment.
You've added a package.json file, propably by accident
Good catch - thanks @d4rp4t. Fixed |
f665226 to
518522e
Compare
|
@robwoodgate We should derive a Use case example: I've opened a PR here |
We already DO calculate shared secret (Zx) per locking key. The slot index is just for ADDITIONAL uniqueness, so that if the same key (P) is added to both pubkeys and refund, it will be uniquely blinded by the slot index. Only the sender knows the ephemeral secret, so only they can derive Zx per locking key (eP) The receiver(s) only know the ephemeral pubkey (E) and their own secret key (p), so they can only generate the shared secret for their own key (pE). EDIT: I've added some clarifying note blocks, because it's a crucial point that is easy to overlook. |
|
@robwoodgate I think we can avoid the |
Love this idea! That would be the ultimate privacy move because P2BK proofs would be totally indistinguishable from standard P2PK proofs. And making the The only downside is the privacy benefit lol... there would be no way to know if a proof was blinded or not, so you would have to try signing EVERY P2PK proof that doesn't have your pubkey with both your secret key (p) and both your derived secret keys (p') to be sure it's not yours. Overall, I think that's probably a tradeoff worth making for the privacy. And very in line with Bitcoin silent payments. Anyone disagree? |
Thinking about this some more this afternoon.... a possible reason to not do this: if the Mint knows a P2BK (Though around half of all 32 byte string nonces would naturally be valid x-coordinates in any case...) |
|
@robwoodgate around ~ Though this could be easily fixed if newer wallets always use EC public keys as nonces, even for normal p2pk. The |
That would alleviate the discrimination concern for sure. It would also go some way to alleviating the related concern that using the |
|
Discussed off proposal: Two paths to resolution:
I would personally prefer option 2, especially given the Mint can find out who is using silent payments easily through option 1. |
To summarize the dilemma: Option 1: Carry
|
Discussed again off proposal: The general feeling was to go with
Overall, reason 2 (loss of SIG_ALL compatibility) was seen as the main reason to NOT use the nonce as the carrier. |
|
@robwoodgate We could simplify the parity detection on the receiver side if we compared the x-only of the unblinded public key: But this is more of an implementation choice. We should however mention in the NUT that this is possible. |
I don't think we need to mention implementation detail in the NUT. In cashu-ts, the aim was to achieve algorithmic constant time, so both You are correct the original pubkey It's not much of a simplifcation, as the blinded private key still needs to be derived in any case. |
Overall, the parity detection issue is nothing to do with Pubkeys, it depends on whether the receiver secret key So a wallet/Nostr client etc might allow a negative-Y generating sk to be stored, because it is flipped 'on the fly'. We therefore will always need to check both for Schnorr derived pubkeys. |
To me, this sounds like a semplification. You trade in 2 point-scalar multiplications and 1 point addition for 1 point-scalar multiplication and 1 addition. |
I understand now - yes, you can save a point multiply, and the approach is sound. I will revisit the cashu-ts reference implementation though for optimization. |
@lollerfirst - I've now added this as the primary workflow. As we have one in the spec, it may as well be the optimal one! |
|
@lollerfirst - I've aded a comprehensive test vector page which will allow implementors to double-check a concrete example across all slots. |
|
@callebtc @thesimplekid - We now have implementations in review for Cashu-TS and CDK, so this PR is ready for review too. There is one question I have about whether we update NUT-18 to show p2pk_e as a default, LMK if I should add that. |
|
Filenames now set for immediate merge as NUT-28 |
SatsAndSports
left a comment
There was a problem hiding this comment.
Thanks for the last small changes. Looks good
|
|
||
| ECDH allows two parties to create an x-coordinate shared secret (`Zx`) by combining their private key with the public key of the other party: `Zx = x(epG) = x(eP) = x(pE)`. | ||
|
|
||
| For P2BK, the sender creates an ephemeral keypair (private key: `e`, public key: `E`). This protects the privacy of their own long-lived public key. They then calculate the shared secret by combining the ephemeral private key (`e`) and the receiver's long-lived public key (`P`). |
There was a problem hiding this comment.
This protects the privacy of their own long-lived public key.
we didn't define what the long-lived public key is yet (next sentence). we only speak about the receiver's pubkey but the sentence sounds like it protects the sender's pubkey. suggest to just remove the sentence.
There was a problem hiding this comment.
That's exactly right - using an ephemeral keypair does protect the senders pubkey :)
|
|
||
| ECDH allows two parties to create an x-coordinate shared secret (`Zx`) by combining their private key with the public key of the other party: `Zx = x(epG) = x(eP) = x(pE)`. | ||
|
|
||
| For P2BK, the sender creates an ephemeral keypair (private key: `e`, public key: `E`). This protects the privacy of their own long-lived public key. They then calculate the shared secret by combining the ephemeral private key (`e`) and the receiver's long-lived public key (`P`). |
There was a problem hiding this comment.
| For P2BK, the sender creates an ephemeral keypair (private key: `e`, public key: `E`). This protects the privacy of their own long-lived public key. They then calculate the shared secret by combining the ephemeral private key (`e`) and the receiver's long-lived public key (`P`). | |
| For P2BK, the sender creates an ephemeral keypair (private key: `e`, public key: `E`). They then calculate the shared secret by combining the ephemeral private key (`e`) and the receiver's long-lived public key (`P`). |
There was a problem hiding this comment.
The idea here was to highlight to sender that using an ephemeral keypair protects their own (long lived) public key from being linked to the proofs.
We can remove the sentence, but I think it's an important feature of P2BK... it provides privacy for both sender AND receiver.
| - `i_byte` is the single unsigned byte representation of `i`: (`0x00` to `0x0A`) | ||
| - `||` denotes concatenation | ||
|
|
||
| For broader compatibility, `rᵢ` **MUST NOT** be normalised modulo `n` |
There was a problem hiding this comment.
For broader compatibility, `rᵢ` **MUST NOT** be normalised modulo `n`
I know this issue (backwards compat) but why not? Every private key should be normalized.
There was a problem hiding this comment.
I agree. I specced it modulo n in the intial draft, but received feedback that RUST (CDK) would have problems doing modulo n calculations.
So that's why it was re-drafted as "tweak-and-single-retry, abandon ephemeral key if still out of range"
Modulo n is the far simpler implementation, if it can be supported consistently.
That's the backwards compatibility issue. Happy to go either way - just make the call.
There was a problem hiding this comment.
modulo n could technically give us 0 right?
It's so unlikely, I guess we could ignore it
But if we want to avoid that unlikely event, we could do modulo (n-1) and then add one
i.e. r = 1 + sha256(...) % (n-1) , where % is modulo, in order to ensure that we get a number between 1 and n-1 (inclusive). Although hopefully the cryptography library does something like this already
There was a problem hiding this comment.
I believe the usual practice is to do standard modulo n and discard/retry if zero.
| If `rᵢ` is not in the range `1 ≤ rᵢ ≤ n−1`, retry once with an extra `0xff` byte appended to the hash input as follows: | ||
|
|
||
| ``` | ||
| rᵢ = SHA-256( b"Cashu_P2BK_v1" || Zx || i_byte || 0xff ) | ||
| ``` | ||
|
|
||
| If `rᵢ` is still not in the range `1 ≤ rᵢ ≤ n−1`, abort and discard the ephemeral keypair. |
There was a problem hiding this comment.
It would allow us to delete this part
| if (r === 0n || r >= secp256k1.Point.CURVE().n) { | ||
| // Very unlikely to get here! | ||
| r = bytesToNumber( | ||
| sha256(Bytes.concat(P2BK_DST, Zx, iByte, new Uint8Array([0xff]))), | ||
| ); | ||
| if (r === 0n || r >= secp256k1.Point.CURVE().n) { | ||
| // Astronomically unlikely to get here! | ||
| throw new Error("P2BK: tweak derivation failed"); | ||
| } | ||
| } |
There was a problem hiding this comment.
suggest: do not use own normalization, let the crypto lib handle it?
There was a problem hiding this comment.
If RUST can handle the modulo n, it's a thumbs up from me. (@thesimplekid )
c972bc1 to
b66dfc8
Compare
CLOSES #290
REPLACES: #291
Implementations:
Summary:
Defines P2BK as an ECDH-derived blinding scheme instead of one using random scalars.
Each proof now includes a per-proof ephemeral pubkey
p2pk_e, from which both parties can derive the same blinding factor(s) deterministically.ECDH shared secret works because:
Importantly, 3rd parties and the mint CANNOT derive the original locking pubkeys. Only the sender and the receiver have the secret keys required to calculate the ECDH shared secret, which can derive both the original pubkeys and the signing secret.
Proofs can be locked to a well known public key, posted in public without compromising privacy, and spent by the recipient without needing any side-channel communication.
Key points:
p2pk_e(33-byte SEC1 pubkey) per proof (stored aspein token v4 format)rᵢ = SHA-256( b"Cashu_P2BK_v1" || Zx || keyset_id_bytes || i_byte)where Zx is a shared ECDH secret,
keyset_id_bytesis the hex_to_bytes of keyset ID, andi_byteis the P2PK locking key "slot" position.Assumptions
Live Demo: