NIP-46 extension: batched NIP-44 decryption
NIP-46 extension: batched NIP-44 decryption
Motivation
Reading one gift-wrapped DM (NIP-59) costs two NIP-44 decryptions: the wrap (kind 1059) and the seal (kind 13). Under NIP-46 each decryption is a remote call costing two relay events, request and response.
Syncing N messages on a new device therefore costs 4N events between one signer/client pair, published in a burst. At 500 messages that is 2000 events, which trips relay rate limits: responses are dropped and the sync never completes.
The decryption count is fixed by the encryption scheme. The round-trip count is not.
Method: nip44_decrypt_batch
Request
{
"id": "<random string>",
"method": "nip44_decrypt_batch",
"params": ["<json array of [peer_pubkey, ciphertext] pairs>"]
}params holds exactly one element: a JSON-encoded array of two-element arrays. Each pair is [peer_pubkey, ciphertext], the two arguments nip44_decrypt takes. Keeping the batch in one string leaves params a flat array of strings, as in every other NIP-46 method.
Response
{
"id": "<same as request>",
"result": "<json array of plaintexts>"
}result is a JSON-encoded array with the same length and order as the input. Element i is the plaintext for pair i, or null if that payload could not be decrypted (malformed payload, wrong conversation key, MAC failure). A backlog routinely contains undecryptable events; one must not fail the rest.
Errors
error is a free-form string in NIP-46, which gives a client nothing to branch on. This method constrains it: when a batch fails as a whole, error MUST begin with one of the tokens below, terminated by end-of-string or a colon. Text after the colon has no protocol meaning.
error := token [ ":" SP human-readable-text ]| Token | Meaning | Client MUST |
|---|---|---|
unsupported_method | Signer does not implement batching. | Disable batching for the session; fall back to per-item. |
invalid_params | Batch was malformed. | Surface as a client bug. MUST NOT disable batching. |
batch_too_large | Batch exceeded the size cap. | Re-split and retry. MUST NOT disable batching. |
approval_required | No standing grant covers these items. | Suspend batching, fall back per-item, re-arm per below. |
rejected | User denied the request. | Fail the pending calls. MUST NOT disable batching. |
unsupported_method is the only token that permanently disables batching. Treating invalid_params as unsupported makes a client abandon batching over its own serialization bug; treating rejected as retryable re-sends a denied batch on every attempt.
Undecryptable payloads are null entries, not errors. The batch fails whole only for the five reasons above.
Size limits
The cap is a byte budget, not an item count: the response carries every plaintext inside one kind:24133 event and must fit relay size caps, and items range from a few dozen bytes to ~2 KB.
- Clients MUST bound a batch by the summed byte length of its ciphertexts, and SHOULD assume
32 KiB until told otherwise.
- Signers MUST accept batches totalling up to 32 KiB.
- A signer rejecting an oversized batch MUST answer
batch_too_large: <bytes>, the largest
total it accepts, in decimal. Clients MUST adopt that value for the session, re-split and retry.
- Signers MUST NOT truncate:
resultis positional, so a short array is indistinguishable
from one whose tail failed to decrypt.
Permissions
Batching grants nothing nip44_decrypt does not. A signer MUST NOT treat a batch as a separate permission scope, and MUST NOT decrypt an item it would refuse individually.
Signers SHOULD serve a batch only under an existing standing grant for nip44_decrypt, and answer approval_required otherwise. Prompting per item defeats the purpose; prompting once for fifty items asks the user to approve what they cannot inspect.
approval_required suspends batching, it does not end it. After falling back, a client MUST re-enable batching once an individual nip44_decrypt returns a plaintext — that approval is the condition the batch was waiting for. A client that treats the token as terminal stays degraded for the whole session after the grant has landed.
Discovery
NIP-46 has no capability negotiation, so support is probed. The probe must survive signers that answer an unknown method with silence instead of an error.
- Signers MUST answer an unrecognised method with
unsupported_method. Clients MUST NOT
rely on this.
- A session's first batch MUST carry exactly one item. It is a decrypt the client needed
anyway, so a successful probe costs nothing extra.
- Clients MUST bound the probe with a timeout, SHOULD use 10 seconds, and MUST treat expiry
as unsupported_method.
- After a successful probe a client SHOULD batch for the rest of the session and MUST NOT
re-probe, subject to the re-arm and re-split rules above.
A capability list in NIP-46 would replace the probe.
Out of scope
nip04_decrypt and nip44_encrypt share the shape and could take identical treatment. NIP-04 is deprecated, and the observed rate-limit failures are on the decrypt side. Nothing here blocks adding them.
Reference implementation
Amber (Android), EventNotificationConsumer.handleNip44DecryptBatch, implements the wire format on the NIP-46 relay path: positional result, per-item null, whole-batch failure when an item needs approval. It does not yet emit the error tokens or the byte budget.