30817:nip-46-extension-batched-nip-44-decryption
NIP-46 extension: batched NIP-44 decryption
- signed by
- npub1f27g7...yk9swu
- published
- 2026-08-10
- event
- 1452ebb4be...5dc5
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.
Discussion
Connect a key to comment.