NUT-11: Pay to Public Key (P2PK)¶
optional
depends on: NUT-10, NUT-08
This NUT describes Pay-to-Public-Key (P2PK) which is one kind of spending condition based on NUT-10's well-known Secret
. Using P2PK, we can lock ecash tokens to a receiver's ECC public key and require a Schnorr signature with the corresponding private key to unlock the ecash. The spending condition is enforced by the mint.
Caution: If the mint does not support this type of spending condition, proofs may be treated as a regular anyone-can-spend tokens. Applications need to make sure to check whether the mint supports a specific kind of spending condition by checking the mint's NUT-06 info endpoint.
Pay-to-Pubkey¶
NUT-10 Secret kind: P2PK
If for a Proof
, Proof.secret
is a Secret
of kind P2PK
, the proof must be unlocked by providing a witness Proof.witness
and one or more valid signatures in the array Proof.witness.signatures
.
In the basic case, when spending a locked token, the mint requires one valid Schnorr signature in Proof.witness.signatures
on Proof.secret
by the public key in Proof.Secret.data
.
To give a concrete example of the basic case, to mint a locked token we first create a P2PK Secret
that reads:
[
"P2PK",
{
"nonce": "859d4935c4907062a6297cf4e663e2835d90d97ecdd510745d32f6816323a41f",
"data": "0249098aa8b9d2fbec49ff8598feb17b592b986e62319a4fa488a3dc36387157a7",
"tags": [["sigflag", "SIG_INPUTS"]]
}
]
Here, Secret.data
is the public key of the recipient of the locked ecash. We serialize this Secret
to a string in Proof.secret
and get a blind signature by the mint that is stored in Proof.C
(see NUT-03]).
The recipient who owns the private key of the public key Secret.data
can spend this proof by providing a signature on the serialized Proof.secret
string that is then added to Proof.witness.signatures
:
{
"amount": 1,
"secret": "[\"P2PK\",{\"nonce\":\"859d4935c4907062a6297cf4e663e2835d90d97ecdd510745d32f6816323a41f\",\"data\":\"0249098aa8b9d2fbec49ff8598feb17b592b986e62319a4fa488a3dc36387157a7\",\"tags\":[[\"sigflag\",\"SIG_INPUTS\"]]}]",
"C": "02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904",
"id": "009a1f293253e41e",
"witness": "{\"signatures\":[\"60f3c9b766770b46caac1d27e1ae6b77c8866ebaeba0b9489fe6a15a837eaa6fcd6eaa825499c72ac342983983fd3ba3a8a41f56677cc99ffd73da68b59e1383\"]}"
}
Signature scheme¶
To spend a token locked with P2PK
, the spender needs to include signatures in the spent proofs. We use libsecp256k1
's serialized 64 byte Schnorr signatures on the SHA256 hash of the message to sign. The message to sign is the field Proof.secret
in the inputs. If indicated by Secret.tags.sigflag
in the inputs, outputs might also require signatures on the message BlindedMessage.B_
.
An ecash spending operation like swap and melt can have multiple inputs and outputs. If we have more than one locked input, we either provide signatures in each input individually (for SIG_INPUTS
) or only in the first input for the entire transaction (for SIG_ALL
). The inputs are the Proofs
provided in the inputs
field and the outputs are the BlindedMessages
in the outputs
field in the request body (see PostMeltRequest
in NUT-05 and PostSwapRequest
in NUT-03).
Tags¶
More complex spending conditions can be defined in the tags in Secret.tags
. All tags are optional. Tags are arrays with two or more strings being ["key", "value1", "value2", ...]
. We denote a specific tag in a proof
Supported tags are:
sigflag: <str_enum[SIG_FLAG]>
sets the signature flagpubkeys: <hex_str>
are additional public keys (together with the one in thedata
field of the secret) that can provide signatures (allows multiple entries)n_sigs: <int>
specifies the minimum number of public keys providing valid signatureslocktime: <int>
is the Unix timestamp of when the lock expiresrefund: <hex_str>
are optional refund public keys that can exclusively spend afterlocktime
(allows multiple entries)n_sigs_refund: <int>
specifies the minimum number of refund public keys providing valid signatures
[!NOTE]
The tag serialization type is
[<str>, <str>, ...]
but some tag values areint
. Wallets and mints must cast types appropriately for de/serialization.
Signature flags¶
Signature flags are defined in the tag Secret.tags['sigflag']
. Currently, there are two signature flags.
SIG_INPUTS
requires valid signatures on all inputs independently. It is the default signature flag and will be applied if thesigflag
tag is absent.SIG_ALL
requires valid signatures on all inputs and on all outputs of a transaction.
If any one input has the signature flag SIG_ALL
, then all inputs are required to have the same kind, the flag SIG_ALL
and the same Secret.data
and Secret.tags
, otherwise an error is returned.
SIG_INPUTS
is only enforced if no input is SIG_ALL
.
Signature flag SIG_INPUTS
¶
SIG_INPUTS
means that each Proof
(input) requires it's own signature. The signature is provided in the Proof.witness
field of each input separately. The format of the witness is defined later on.
Signed inputs¶
A Proof
(an input) with a signature P2PKWitness.signatures
on secret
is the JSON (see NUT-00):
{
"amount": <int>,
"secret": <str>,
"C": <hex_str>,
"id": <str>,
"witness": <P2PKWitness | str> // Signatures on "secret"
}
The secret
field is signed as a string.
Witness format¶
Signatures are stored in P2PKWitness
objects and are provided in either each Proof.witness
of all inputs separately (for SIG_INPUTS
) or only in the first input of the transaction (for SIG_ALL
). P2PKWitness
is a serialized JSON string of the form
{
"signatures": <Array[<hex_str>]>
}
The signatures
are an array of signatures in hex and correspond to the signatures by one or more signing public keys.
Signature flag SIG_ALL
¶
SIG_ALL
is enforced only if the following conditions are met:
- If one input has the signature flag
SIG_ALL
, all other inputs MUST have the sameSecret.data
andSecret.tags
, and by extension, also beSIG_ALL
. - If one or more inputs differ from this, an error is returned.
If this condition is met, the SIG_ALL
flag is enforced and only the first input of a transaction requires a witness that covers all other inputs and outputs of the transaction. All signatures by the signing public keys MUST be provided in the Proof.witness
of the first input of the transaction.
Message aggregation for SIG_ALL
¶
The message to be signed depends on the type of transaction containing an input with signature flag SIG_ALL
.
Aggregation for swap
¶
A swap contains inputs
and outputs
(see NUT-03). To provide a valid signature, the owner (or owners) of the signing public keys must concatenate the secret
fields of all Proofs
(inputs), and the B_
fields of all BlindedMessages
(outputs, see NUT-00) to a single message string in the order they appear in the transaction. This string concatenated is then hashed and signed (see Signature scheme).
If a swap transaction has n
inputs and m
outputs, the message to sign becomes:
msg = secret_0 || ... || secret_n || B_0 || ... || B_m
Here, ||
denotes string concatenation. The B_
of each output is a hex string.
Aggregation for melt
¶
For a melt transaction, the message to sign is composed of all the inputs, the quote ID being paid, and the NUT-08 blank outputs
.
If a melt transaction has n
inputs, m
blank outputs, and a quote ID quote_id
, the message to sign becomes:
msg = secret_0 || ... || secret_n || B_0 || ... || B_m || quote_id
Here, ||
denotes string concatenation. The B_
of each output is a hex string.
Multisig¶
Cashu offers two levels of multi-signature protection: Locktime MultiSig
and Refund MultiSig
, which are activated depending on the status of the proof's locktime
tag.
Locktime MultiSig¶
[!NOTE] Locktime Multisig conditions only apply if the
locktime
tag is not present, or is a timestamp in the future.
If the pubkeys
tag is present, the Proof
is spendable only if a valid signature is given by at least ONE of the public keys contained in the Secret.data
field or the pubkeys
tag.
If the n_sigs
tag is a positive integer, the mint will require at least n_sigs
of those public keys to provide a valid signature.
If the number of public keys with valid signatures is greater or equal to the number specified in n_sigs
, the transaction is valid. The signatures are provided in an array of strings in the P2PKWitness
object.
Expressed as an "n-of-m" scheme, n = n_sigs
is the number of required signatures and m = 1 (data field) + len(pubkeys tag)
is the number of public keys that could sign.
[!CAUTION]
Because Schnorr signatures are non-deterministic, we expect a minimum number of unique public keys with valid signatures instead of expecting a minimum number of signatures.
Locktime¶
If the tag locktime
is the unix time and the mint's local clock is greater than locktime
, the Proof
becomes spendable by anyone, except if the following condition is also true.
[!NOTE] A
Proof
is considered spendable by anyone if it only requires asecret
and a valid signatureC
to be spent (which is the default case).
Refund MultiSig¶
If the locktime
tag is in the past and the refund
tag is present, the Proof
is spendable only if a valid signature is given by at least ONE of the refund
pubkeys.
If the n_sigs_refund
tag is present, the mint will require at leastn_sigs_refund
of the refund
pubkeys to provide a valid signature.
[!CAUTION]
Because Schnorr signatures are non-deterministic, we expect a minimum number of unique public keys with valid signatures instead of expecting a minimum number of signatures.
Complex Example¶
This is an example Secret
that locks a Proof
with a Pay-to-Pubkey (P2PK) condition that requires 2-of-3 signatures from the public keys in the data
field and the pubkeys
tag. If the timelock
has passed, the Proof
becomes spendable with a single signature from ONE of the two public keys in the refund
tag. The signature flag sigflag
indicates that signatures are necessary on the inputs
and the outputs
of the transaction this Proof
is spent by.
[
"P2PK",
{
"nonce": "da62796403af76c80cd6ce9153ed3746",
"data": "033281c37677ea273eb7183b783067f5244933ef78d8c3f15b1a77cb246099c26e",
"tags": [
["sigflag", "SIG_ALL"],
["n_sigs", "2"],
["locktime", "1689418329"],
[
"refund",
"033281c37677ea273eb7183b783067f5244933ef78d8c3f15b1a77cb246099c26e",
"02e2aeb97f47690e3c418592a5bcda77282d1339a3017f5558928c2441b7731d50"
],
[
"pubkeys",
"02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904",
"023192200a0cfd3867e48eb63b03ff599c7e46c8f4e41146b2d281173ca6c50c54"
]
]
}
]
Use cases¶
The following use cases are unlocked using P2PK:
- Publicly post locked ecash that can only be redeemed by the intended receiver
- Final offline-receiver payments that can't be double-spent when combined with an offline signature check mechanism like DLEQ proofs
- Receiver of locked ecash can defer and batch multiple mint round trips for receiving proofs (requires DLEQ)
- Ecash that is owned by multiple people via the multisignature abilities
- Atomic swaps when used in combination with the locktime feature
Mint info setting¶
The NUT-06 MintMethodSetting
indicates support for this feature:
{
"11": {
"supported": true
}
}