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