NUT-11: Pay to Public Key (P2PK)
optional
depends on: NUT-10, NUT-08
extended by: NUT-28
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
Proofs (see NUT-00) 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 regular anyone-can-spend Proofs. 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.
Basic Case
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 Proof, 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
Proof 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 Proofs locked with P2PK, the spender
needs to include signatures in the Proofs used as “inputs” for the
spending operation. 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, unless otherwise indicated by the
Secret.tags.sigflag in the inputs, as detailed
below.
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).
[!NOTE]
The field
Proof.secretcontains escaped JSON for transport. The message to sign MUST be constructed using the unescaped secret string, eg: [“P2PK”,{“nonce”:“c7f280eb55c1e856…
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.
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 by its key.
Supported tags are:
sigflag: <str_enum[SIG_FLAG]>sets the signature flagpubkeys: <hex_str>are additional public keys (together with the one in thedatafield of the secret) that can provide signatures (allows multiple entries)n_sigs: <int>specifies the minimum number of Locktime Multisig public keys providing valid signatureslocktime: <int>is the Unix timestamp of when the lock expiresrefund: <hex_str>are additional public keys that can provide signatures afterlocktime(allows multiple entries)n_sigs_refund: <int>specifies the minimum number of Refund Multisig public keys providing valid signatures afterlocktimeexpires
Each of the above tags may appear exactly ONCE in a P2PK secret. If a tag appears more than once, the P2PK secret is malformed and the Proof MUST be rejected as unspendable.
If n_sigs or n_sigs_refund is not a
positive integer, or exceeds the total number of keys in its
pathway, the P2PK secret is malformed and the Proof
MUST be rejected as unspendable.
[!NOTE]
The tag serialization type is
[<str>, <str>, ...]but some tag values areint. Wallets and mints must cast types appropriately for de/serialization.
Signature flag
Signature flags are defined in the tag
Secret.tags['sigflag']. Currently, there are two
signature flags.
SIG_INPUTSrequires valid signatures on all inputs independently. It is the default signature flag and will be applied if thesigflagtag is absent.SIG_ALLrequires 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.
If a P2PK secret has any other signature flag value, the P2PK secret is malformed and the Proof MUST be rejected as unspendable.
Signature flag
SIG_INPUTS
SIG_INPUTS means that each Proof
(input) requires its own signature. The signature is provided in
the Proof.witness field of each input separately. The
format of the witness was defined
earlier 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.
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.dataandSecret.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 and C fields of
all Proofs (inputs) with the amount and
B_ fields of all BlindedMessages
(outputs, see NUT-00) to a single
message string in the order they appear in the transaction. This
concatenated string 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 || C_0 || ... || secret_n || C_n || amount_0 || B_0 || ... || amount_m || B_m
Here, || denotes string concatenation. The
C of each input and B_ of each output
are hex strings and amount is a
UTF-8-encoded 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 || C_0 || ... || secret_n || C_n || amount_0 || B_0 || ... || amount_m || B_m || quote_id
Here, || denotes string concatenation. The
C of each input and B_ of each output
are hex strings and amount is a
UTF-8-encoded string.
Locktime Tag
The locktime tag signals which set of locking
rules the mint should apply. There are three possible states the
locktime tag can represent:
Permanent Lock
If the locktime tag is not present, or is not a
valid unix time, the lock is considered “permanent”.
Locktime
Multisig conditions apply if the pubkeys tag is
present, Basic Case
conditions if not.
Active Lock
If the locktime tag is a valid unix time and the
mint’s local clock is less than or equal to locktime
(now <= locktime), the lock is “active”.
Locktime
Multisig conditions apply if the pubkeys tag is
present, Basic Case
conditions if not.
Expired Lock
If the locktime tag is a valid unix time and the
mint’s local clock is greater than locktime
(now > locktime), the lock has “expired”.
Both Locktime
Multisig and Refund Multisig
conditions apply if the refund tag is present,
otherwise the proof is considered unlocked and spendable without a
witness signature.
[!NOTE] A
Proofis considered spendable by anyone if it only requires asecretand a valid unblinded signatureCto be spent (which is the default case in NUT-00).
Multisig
Cashu offers two multi-signature pathways:
Locktime MultiSig and Refund MultiSig,
which are activated depending on the status of the proof’s
locktime tag.
[!NOTE] Each pathway has a self-contained set of conditions which must be satisfied for that pathway to be valid. You cannot mix/blend conditions between pathways.
Locktime MultiSig
Locktime Multisig extends the Basic Case by allowing
proofs to be locked to multiple public keys. The additional
locking public keys are stored in the pubkeys
tag.
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
than or equal to the number specified in n_sigs (or
1 if n_sigs is not present), 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) + count(pubkeys tag keys) is the
total number of public keys that could sign.
[!CAUTION]
Because Schnorr signatures are non-deterministic (due to auxiliary random data), we expect a minimum number of unique public keys with valid signatures instead of expecting a minimum number of signatures.
Refund MultiSig
Refund Multisig allows proofs to be additionally
spendable by a separate set of public keys once the
locktime has expired. These public keys are stored in
the refund tag, and can include keys previously
listed in data or pubkeys.
Locktime Multisig conditions continue to apply, and the proof can continue to be spent according to Locktime Multisig rules.
In addition, the Proof can be
spent if a valid signature is given by at least
ONE of the public keys contained in the
refund tag.
If the n_sigs_refund tag is a positive integer,
the mint will require at least n_sigs_refund of those
refund public keys to provide a valid signature.
If the number of refund public keys with valid
signatures is greater than or equal to the number specified in
n_sigs_refund (or 1 if
n_sigs_refund is not present), 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_refund
is the number of required signatures and
m = count(refund tag keys) is the total number of
refund keys that could sign.
[!CAUTION]
Because Schnorr signatures are non-deterministic (due to auxiliary random data), 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 locktime has passed, the Proof
continues to be spendable with 2-of-3 signatures from the public
keys in the data field and the pubkeys
tag. But now it ALSO becomes spendable with a
single signature from any ONE of the 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"
]
]
}
]
Public Key Canonicalisation
Public keys MUST use the compressed Secp256k1 public key format.
Each key MUST appear at most ONCE per multi-signature pathway. The same key MAY appear in both pathways.
Keys are compared using their lowercase x-coordinate
(02 or 03 y-parity prefix ignored).
Example: the following keys are considered equivalent because they all have the same lowercase x-coordinate, so can all be Schnorr signed using the same secret key:
02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904 // 02 lowercase
02698C4E2B5F9534CD0687D87513C759790CF829AA5739184A3E3735471FBDA904 // 02 uppercase
02698C4e2B5f9534cD0687d87513C759790cF829aa5739184a3e3735471fbda904 // 02 mixed case
03698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904 // 03 lowercase
03698C4E2B5F9534CD0687D87513C759790CF829AA5739184A3E3735471FBDA904 // 03 uppercase
03698C4e2B5f9534cD0687d87513C759790cF829aa5739184a3e3735471fbda904 // 03 mixed case
Example: key duplicated in main pathway (malformed):
"data": "02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904",
"pubkeys": "03698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904"
Example: key allowed in both pathways:
"data": "02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904",
"refund": "03698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904"
If a pathway contains a duplicate key, the P2PK secret is malformed and the Proof MUST be rejected as unspendable.
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 nuts object
indicates support for this feature:
{
"11": {
"supported": true
}
}