Skip to content

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 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.secret contains 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 flag
  • pubkeys: <hex_str> are additional public keys (together with the one in the data field of the secret) that can provide signatures (allows multiple entries)
  • n_sigs: <int> specifies the minimum number of Locktime Multisig public keys providing valid signatures
  • locktime: <int> is the Unix timestamp of when the lock expires
  • refund: <hex_str> are additional public keys that can provide signatures after locktime (allows multiple entries)
  • n_sigs_refund: <int> specifies the minimum number of Refund Multisig public keys providing valid signatures after locktime expires

[!NOTE]

The tag serialization type is [<str>, <str>, ...] but some tag values are int. 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_INPUTS requires valid signatures on all inputs independently. It is the default signature flag and will be applied if the sigflag 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 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 same Secret.data and Secret.tags, and by extension, also be SIG_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-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-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 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, 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 Proof is considered spendable by anyone if it only requires a secret and a valid unblinded signature C to 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 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 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"
      ]
    ]
  }
]

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
  }
}