Skip to content

NUT-11: Pay to Public Key (P2PK)

optional

depends on: NUT-10


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 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 input or output, we provide signatures in each Proof and BlindedMessage individually. 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 Proof.tags. All tags are optional. Tags are arrays with two or more strings being ["key", "value1", "value2", ...].

Supported tags are:

  • sigflag: <str> determines whether outputs have to be signed as well
  • n_sigs: <int> specifies the minimum number of valid signatures expected
  • pubkeys: <hex_str> are additional public keys that can provide signatures (allows multiple entries)
  • locktime: <int> is the Unix timestamp of when the lock expires
  • refund: <hex_str> are optional refund public keys that can exclusively spend after locktime (allows multiple entries)

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 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. It is the default signature flag and will be applied even if the sigflag tag is absent.
  • SIG_ALL requires valid signatures on all inputs and on all outputs.

The signature flag SIG_ALL is enforced if at least one of the Proofs have the flag SIG_ALL. Otherwise, SIG_INPUTS is enforced.

Signature

Signatures must be provided in the field Proof.witness.signatures for each Proof which is an input. If the signature flag SIG_ALL is enforced, signatures must also be provided for every output in its field BlindedMessage.witness.signatures.

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 of each input is signed as a string.

Signed outputs

A BlindedMessage (an output) with a signature P2PKWitness.signatures on B_ is the JSON (see NUT-00):

{
  "amount": <int>,
  "B_": <hex_str>,
  "witness": <P2PKWitness | str> // Signatures on "B_"
}

The B_ of each output is signed as bytes which comes from the original hex string.

Witness format

P2PKWitness is a serialized JSON string of the form

{
  "signatures": <Array[<hex_str>]>
}

The signatures are an array of signatures in hex. The witness for a spent proof can be obtained with a Proof state check (see NUT-07).

Multisig

If the tag n_sigs is a positive integer, the mint will also consider signatures from public keys specified in the pubkeys tag additional to the public key in Secret.data. If the number of valid signatures is greater or equal to the number specified in n_sigs, the transaction is valid.

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.

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 a secret and a valid signature C to be spent (which is the default case).

Refund public keys

If the locktime is in the past and a tag refund is present, the Proof is spendable only if a valid signature by one of the the refund pubkeys is provided in Proof.witness.signatures and, depending on the signature flag, in BlindedMessage.witness.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 the public key in the refund tag. The signature flag sigflag indicates that signatures are necessary on the inputs and the outputs of a transaction.

[
  "P2PK",
  {
    "nonce": "da62796403af76c80cd6ce9153ed3746",
    "data": "033281c37677ea273eb7183b783067f5244933ef78d8c3f15b1a77cb246099c26e",
    "tags": [
      ["sigflag", "SIG_ALL"],
      ["n_sigs", "2"],
      ["locktime", "1689418329"],
      [
        "refund",
        "033281c37677ea273eb7183b783067f5244933ef78d8c3f15b1a77cb246099c26e"
      ],
      [
        "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
  }
}