Skip to content

NUT-00: Notation, Utilization, and Terminology

mandatory


This document details the notation and models used throughout the specification and lays the groundwork for understanding the basic cryptography used in the Cashu protocol.

  • Sending user: Alice
  • Receiving user: Carol
  • Mint: Bob

Blind Diffie-Hellmann key exchange (BDHKE)

Variables

  • G elliptic curve generator point

Bob (mint)

  • k private key of mint (one for each amount)
  • K public key corresponding to k
  • C_ blind signature (on B_)

Alice (user)

  • x UTF-8-encoded random string (secret message), corresponds to point Y = hash_to_curve(x) on curve
  • r blinding factor
  • B_ blinded message
  • C unblinded signature

hash_to_curve(x: bytes) -> curve point Y

Deterministically maps a message to a public key point on the secp256k1 curve, utilizing a domain separator to ensure uniqueness.

Y = PublicKey('02' || SHA256(msg_hash || counter)) where msg_hash is SHA256(DOMAIN_SEPARATOR || x)

  • Y derived public key
  • DOMAIN_SEPARATOR constant byte string b"Secp256k1_HashToCurve_Cashu_"
  • x message to hash
  • counter uint32 counter(byte order little endian) incremented from 0 until a point is found that lies on the curve

Protocol

  • Mint Bob publishes public key K = kG
  • Alice picks secret x and computes Y = hash_to_curve(x)
  • Alice sends to Bob: B_ = Y + rG with r being a random blinding factor (blinding)
  • Bob sends back to Alice blinded key: C_ = kB_ (these two steps are the DH key exchange) (signing)
  • Alice can calculate the unblinded key as C_ - rK = kY + krG - krG = kY = C (unblinding)
  • Alice can take the pair (x, C) as a token and can send it to Carol.
  • Carol can send (x, C) to Bob who then checks that k*hash_to_curve(x) == C (verification), and if so treats it as a valid spend of a token, adding x to the list of spent secrets.

0.1 - Models

BlindedMessage

An encrypted ("blinded") secret and an amount is sent from Alice to Bob for minting tokens or for swapping tokens. A BlindedMessage is also called an output.

{
  "amount": int,
  "id": hex_str,
  "B_": hex_str
}

amount is the value for the requested BlindSignature, id is the requested keyset ID from which we expect a signature, and B_ is the blinded secret message generated by Alice. An array [BlindedMessage] is also referred to as BlindedMessages.

BlindSignature

A BlindSignature is sent from Bob to Alice after minting tokens or after swapping tokens. A BlindSignature is also called a promise.

{
  "amount": int,
  "id": hex_str,
  "C_": hex_str
}

amount is the value of the blinded token, id is the keyset id of the mint keys that signed the token, and C_ is the blinded signature on the secret message B_ sent in the previous step.

Proof

A Proof is also called an input and is generated by Alice from a BlindSignature it received. An array [Proof] is called Proofs. Alice sends Proofs to Bob for melting tokens. Serialized Proofs can also be sent from Alice to Carol. Upon receiving the token, Carol deserializes it and requests a swap from Bob to receive new Proofs.

{
  "amount": int,
  "id": hex_str,
  "secret": str,
  "C": hex_str,
}

amount is the amount of the Proof, secret is the secret message and is a utf-8 encoded string (the use of a 64 character hex string generated from 32 random bytes is recommended to prevent fingerprinting), C is the unblinded signature on secret (hex string), id is the keyset id of the mint public keys that signed the token (hex string).

0.2 - Protocol

Errors

In case of an error, mints respond with the HTTP status code 400 and include the following data in their response:

{
  "detail": "oops",
  "code": 1337
}

Here, detail is the error message, and code is the error code. Error codes are to be defined in the documents concerning the use of a certain API endpoint.

0.3 - Methods

Serialization of tokens

Tokens can be serialized to send them between users Alice and Carol. Serialized tokens have a Cashu token prefix, a versioning flag, and the token. Optionally, a URI prefix for making tokens clickable on the web.

We use the following format for token serialization:

cashu[version][token]

cashu is the Cashu token prefix. [version] is a single base64_urlsafe character to denote the token format version.

URI tags

To make Cashu tokens clickable on the web, we use the URI scheme cashu:. An example of a serialized token with URI tag is

cashu:cashuAeyJwcm9vZn...

V3 tokens

V3 tokens are deprecated and the use of the more space-efficient V4 tokens is encouraged.

Version

This token format has the [version] value A.

Format

V3 tokens are base64-encoded JSON objects. The token format supports tokens from multiple mints. The JSON is serialized with a base64_urlsafe (base64 encoding with / replaced by _ and + by -). base64_urlsafe strings may have padding characters (usually =) at the end which can be omitted. Clients need to be able to decode both cases.

cashuA[base64_token_json]

[base64_token_json] is the token JSON serialized in base64_urlsafe. [base64_token_json] should be cleared of any whitespace before serializing.

Token format

The deserialized base64_token_json is

{
  "token": [
    {
      "mint": str,
      "proofs": Proofs
    },
    ...
  ],
  "unit": str <optional>,
  "memo": str <optional>
}

mint is the mint URL. The mint URL must be stripped of any trailing slashes (/). Proofs is an array of Proof objects. The next two elements are only for displaying the receiving user appropriate information: unit is the currency unit of the token keysets (see Keysets for supported units), and memo is an optional text memo from the sender.

Example

Below is a TokenV3 JSON before base64_urlsafe serialization.

{
  "token": [
    {
      "mint": "https://8333.space:3338",
      "proofs": [
        {
          "amount": 2,
          "id": "009a1f293253e41e",
          "secret": "407915bc212be61a77e3e6d2aeb4c727980bda51cd06a6afc29e2861768a7837",
          "C": "02bc9097997d81afb2cc7346b5e4345a9346bd2a506eb7958598a72f0cf85163ea"
        },
        {
          "amount": 8,
          "id": "009a1f293253e41e",
          "secret": "fe15109314e61d7756b0f8ee0f23a624acaa3f4e042f61433c728c7057b931be",
          "C": "029e8e5050b890a7d6c0968db16bc1d5d5fa040ea1de284f6ec69d61299f671059"
        }
      ]
    }
  ],
  "unit": "sat",
  "memo": "Thank you."
}

When serialized, this becomes:

cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91LiJ9

V4 tokens

V4 tokens are a space-efficient way of serializing tokens using the CBOR binary format. All keys are single characters and hex strings are encoded in binary. V4 tokens can only hold proofs from a single mint.

Version

This token format has the [version] value B.

Format

Wallets serialize tokens in a base64_urlsafe format (base64 encoding with / replaced by _ and + by -). base64_urlsafe strings may have padding characters (usually =) at the end which can be omitted. Clients need to be able to decode both cases.

cashuB[base64_token_cbor]
Token format

For readability, the structure of a TokenV4 object is shown below in its equivalent JSON form.

{
  "m": str, // mint URL
  "u": str, // currency unit
  "d": str <optional>, // optional memo
  "t": [
    {
      "i": bytes, // keyset ID (short or long form)
      "p": [ // proofs with this keyset ID
        {
          "a": int, // amount
          "s": str, // secret
          "c": bytes, // signature
          "d": { <optional> // optional DLEQ proof
            "e": bytes,
            "s": bytes,
            "r": bytes
          },
          "w": str <optional> // optional witness
        },
        ...
      ]
    },
    ...
  ],
}

m is the mint URL. The mint URL MUST be normalized by stripping any trailing slashes (/). u is the currency unit of the token keysets. Supported units are defined in Keysets. d is an optional, human-readable memo provided by the sender.

Within the t (token) array, i denotes the keyset ID associated with the proofs contained in p which are grouped by i. p is an array of Proof objects with the original keyset ID field id omitted. All proofs in the corresponding p array MUST belong to the same keyset ID.

Unless otherwise stated, fields of type bytes represent byte strings in the CBOR encoding. In the original JSON representation of Proof objects, these values are encoded as hexadecimal strings. Implementations MUST convert between hex strings and raw byte arrays when translating between JSON and CBOR representations.

Optional fields MAY be omitted if not present. Receivers MUST ignore unknown fields to preserve forward compatibility.

Short keyset ID

To reduce the size of the i field and the overall Token encoding, wallets MAY use the short keyset ID representation (s_id).

The short keyset ID is defined as the first 8 bytes of the full 33-byte keyset ID:

  • Byte form: s_id = id_bytes[:8]
  • Hex form: s_id = id_hex_str[:16]

Wallets receiving a Token MUST support both short and full keyset ID representations. When a short keyset ID is encountered, the wallet MUST resolve it to the corresponding full keyset ID before processing the contained Proof objects.

If a short keyset ID resolves to more than one known full keyset ID, the identifier is considered ambiguous. In this case, the wallet MUST fail token parsing and return an error.

The mint is unaware of the s_id. All API endpoints exposed by the mint use the full keyset ID.

Example

Below is a TokenV4 JSON before CBOR and base64_urlsafe serialization.

{
    "t": [
        {
            "i": h'00ffd48b8f5ecf80',
            "p": [
                {
                    "a": 1,
                    "s": "acc12435e7b8484c3cf1850149218af90f716a52bf4a5ed347e48ecc13f77388",
                    "c": h'0244538319de485d55bed3b29a642bee5879375ab9e7a620e11e48ba482421f3cf',
                },
            ],
        },
        {
            "i": h'00ad268c4d1f5826',
            "p": [
                {
                    "a": 2,
                    "s": "1323d3d4707a58ad2e23ada4e9f1f49f5a5b4ac7b708eb0d61f738f48307e8ee",
                    "c": h'023456aa110d84b4ac747aebd82c3b005aca50bf457ebd5737a4414fac3ae7d94d',
                },
                {
                    "a": 1,
                    "s": "56bcbcbb7cc6406b3fa5d57d2174f4eff8b4402b176926d3a57d3c3dcbb59d57",
                    "c": h'0273129c5719e599379a974a626363c333c56cafc0e6d01abe46d5808280789c63',
                },
            ],
        },
    ],
    "m": "http://localhost:3338",
    "u": "sat",
}

The h'' values are bytes but displayed as hex strings here.

We serialize this JSON using CBOR which can be seen here. The resulting bytes are then serialized to a string using base64_urlsafe and the prefix cashuB is added. This leaves us with the following serialized TokenV4:

cashuBo2F0gqJhaUgA_9SLj17PgGFwgaNhYQFhc3hAYWNjMTI0MzVlN2I4NDg0YzNjZjE4NTAxNDkyMThhZjkwZjcxNmE1MmJmNGE1ZWQzNDdlNDhlY2MxM2Y3NzM4OGFjWCECRFODGd5IXVW-07KaZCvuWHk3WrnnpiDhHki6SCQh88-iYWlIAK0mjE0fWCZhcIKjYWECYXN4QDEzMjNkM2Q0NzA3YTU4YWQyZTIzYWRhNGU5ZjFmNDlmNWE1YjRhYzdiNzA4ZWIwZDYxZjczOGY0ODMwN2U4ZWVhY1ghAjRWqhENhLSsdHrr2Cw7AFrKUL9Ffr1XN6RBT6w659lNo2FhAWFzeEA1NmJjYmNiYjdjYzY0MDZiM2ZhNWQ1N2QyMTc0ZjRlZmY4YjQ0MDJiMTc2OTI2ZDNhNTdkM2MzZGNiYjU5ZDU3YWNYIQJzEpxXGeWZN5qXSmJjY8MzxWyvwObQGr5G1YCCgHicY2FtdWh0dHA6Ly9sb2NhbGhvc3Q6MzMzOGF1Y3NhdA

Binary Token

Token can be transmitted in a binary format when applicable (for example when transmitting via NFC). For this the serialised token is prepended with a prefix and a version byte.

utf8("craw") || utf8(<token_version>) || <serialised_token>
  • Binary Encoding V4: utf8("craw") || utf8("B") || cbor(token_v4_object)