30817:nip-xxxx-ipfs-archive-attestations
NIP-XXXX — IPFS Archive Attestations
- signed by
- npub19yw8t...hv322j
- published
- 2026-06-07
- event
- 4b1628e866...f09d
draft optional
Status: proposed draft. The kind number is a placeholder pending assignment via the nostr-protocol/nips repository. The reference implementation (Lodestar) uses 7503, an unused number in the regular range chosen only to avoid collisions — the digits carry no meaning. What is deliberate is the range: see Why a regular kind.
Abstract
This NIP defines a regular event kind that attests to a content-addressed archive of a Nostr identity's events and referenced media. The attestation is a signed commitment to a deterministic bundle (its manifest hash and IPFS root CID), making the archive verifiable — authentic, intact, and vouched-for by the pubkey — independently of any relay or media host, and offline.
Motivation
A pubkey's events are scattered across relays that can disappear, and its media lives on hosts that rot. A plain backup of those events and blobs is useful but untrusted: a recipient can't tell whether it has been altered or truncated. Two properties of Nostr let us do better:
- Every event is self-verifying (
id = sha256(serialization), signed). An archive can preserve this so each event proves its own authenticity. - The author holds a key. The author can therefore sign a commitment to the whole bundle, vouching for a specific snapshot at a known time.
This NIP standardizes that commitment so any client can produce and verify such archives interoperably.
Terminology
- Bundle — a directory of files:
manifest.json,events.jsonl, andmedia/<sha256>blobs. It is content-addressed with UnixFS to a single root CID. - Manifest —
manifest.json, a canonical JSON document committing to the exact event set, media set, and crawl coverage. - Attestation — the event defined here, committing to the manifest hash and root CID.
- Canonical JSON — JSON with object keys sorted lexicographically at every
depth, no insignificant whitespace, and
undefined/absent members omitted. Array element order is significant and fixed by the producer.
The attestation event
A regular event. Authored (signed) by the pubkey being archived.
{
"kind": 7503,
"pubkey": "<hex>",
"created_at": 1730000000,
"content": "{\"events\":1240,\"media\":318,\"blobs\":270,\"generator\":\"lodestar/1\"}",
"tags": [
["manifest", "<sha256-hex of canonical manifest.json>"],
["cid", "<UnixFS root CID, CIDv1, base32 'bafy…'>"],
["event_count", "1240"],
["media_count", "318"],
["blob_count", "270"],
["oldest", "1609459200"],
["newest", "1729999000"],
["version", "1"],
["alt", "Archive attestation: 1240 events, 270 media blobs."]
]
}
Tags
| tag | value | required |
|---|---|---|
manifest |
sha256 (hex) of the canonical manifest.json |
yes |
cid |
UnixFS root CID of the bundle | yes |
version |
archive format version | yes |
event_count |
number of events committed to | recommended |
media_count |
number of media references recorded | recommended |
blob_count |
number of media blobs embedded (bytes captured) | recommended |
oldest |
unix seconds of the oldest archived event | optional |
newest |
unix seconds of the newest archived event | optional |
alt |
NIP-31 human-readable description | yes |
The manifest and cid tags are the only values required for verification; the
counts and oldest/newest are conveniences (they appear in the manifest too).
content is an informational JSON summary and is not part of the verifiable
commitment.
A pubkey MAY publish many attestations over time; each is a permanent snapshot.
Clients showing "the latest archive" SHOULD filter
{ kinds: [7503], authors: [pubkey] } and take the greatest created_at.
Why a regular kind
Regular events (1000–9999) are stored permanently by relays and are never
replaced, which is exactly right for an append-only history of archive snapshots.
An addressable kind (30000–39999) with a d tag would make "my latest archive"
a single queryable coordinate, but each new attestation would overwrite the
previous one — destroying the snapshot history. We therefore use a regular kind
and query the set by authors + kinds.
The bundle
manifest.json (canonical JSON)
{
"version": "1",
"type": "lodestar-archive",
"generator": "lodestar/1",
"pubkey": "<hex>",
"npub": "npub1…",
"createdAt": 1730000000,
"eventCount": 1240,
"eventIds": ["00ab…", "00cd…"], // ALL event ids, sorted ascending
"kindCounts": { "1": 1100, "0": 1, "1063": 80 },
"oldest": 1609459200,
"newest": 1729999000,
"media": [ /* MediaItem records, sorted by (eventId, url, source) */ ],
"mediaBlobs": ["1f3a…", "9b22…"], // sha256 of every embedded blob, sorted
"coverage": { /* CoverageManifest, see below */ }
}
eventIds and mediaBlobs are the corpus commitments. Because an event's id is
the sha256 of its serialization, the sorted id list pins the event set exactly:
no event can be added, removed, or altered without changing the list, and the
list is reproduced from events.jsonl.
A MediaItem records one reference and its capture outcome:
{
"url": "https://…",
"source": "imeta" | "nip94" | "blossom-path" | "content-url" | "profile-picture",
"eventId": "<hex>",
"expectedHash": "<sha256 if the event declared one>",
"actualHash": "<sha256 of fetched bytes, if captured>",
"mimeType": "image/jpeg",
"size": 48213,
"status": "verified" | "captured-unverified" | "mismatch" | "cors-blocked" | "not-found" | "error",
"verified": true | false | null, // null = no declared hash to check against
"servedFrom": "https://…"
}
verified: null is distinct from false: it means integrity was unprovable
(the reference carried no signed hash), not that a check failed.
events.jsonl
Every archived event, one canonical-JSON event per line, sorted by event id. Each line is an unmodified, signed NIP-01 event.
media/<sha256>
Each successfully captured blob, named by the sha256 of its bytes (its content address). Blobs are deduplicated: identical bytes referenced by multiple events are stored once.
Coverage
{
"pubkey": "<hex>",
"crawledAt": 1730000000,
"relaysQueried": ["wss://…"],
"relays": [
{
"url": "wss://…",
"status": "ok" | "error" | "partial" | "aborted",
"eventCount": 1200,
"pages": 4,
"oldest": 1609459200,
"newest": 1729999000,
"reachedEnd": true,
"hitWindowCap": false,
"kindCounts": { "1": 1100 },
"error": "optional message"
}
],
"totalUnique": 1240,
"kindCounts": { "1": 1100 },
"complete": false, // ALWAYS false — see Limitations
"notes": ["…caveats…"]
}
Content addressing
The bundle is encoded as a UnixFS directory over manifest.json,
events.jsonl, and the media/ blobs, using the unixfs-v1-2025 import
profile: CIDv1, raw leaves, sha2-256, 1 MiB fixed-size chunks, 1024 links per
file node, 256-fanout HAMT directory sharding. The serialized form is a CAR
whose single root is the cid value; importing the CAR into any IPFS node
reproduces that CID. Re-deriving the CID from the loose files (rather than
importing the CAR) reproduces it only under the same profile.
The attestation deliberately commits to the bundle but is not contained in
it — a signature cannot sign over bytes that include itself. The attestation is
distributed alongside the bundle (e.g. as a sidecar attestation.json, or
simply published to relays).
Verification
Given the bundle, the attestation, and the expected pubkey:
- Attestation — verify the attestation's signature and that
pubkeyequals the archive pubkey andkindis this kind. - Manifest — recompute sha256 over the exact
manifest.jsonbytes; it MUST equal themanifesttag. - CID — re-pack the bundle files (or import the CAR) under the
unixfs-v1-2025profile; the root CID MUST equal thecidtag. - Events — for each line in
events.jsonl, verifyid == sha256(serialization)and the signature; the set of ids MUST equalmanifest.eventIds; every event'spubkeyMUST equal the archive pubkey. - Media — each
media/<sha256>blob's bytes MUST hash to its filename; the set MUST equalmanifest.mediaBlobs.
An archive is verified only if every step passes. A verifier MUST NOT report success when a step was skipped (e.g. the CID was not recomputed); it should distinguish "verified" from "verified except CID."
Relationship to other NIPs
- NIP-01 — events are stored and verified verbatim; ids and signatures are the base of authenticity.
- NIP-65 — the crawl SHOULD read the author's relay list (kind 10002) to choose relays; the coverage manifest records which were used.
- NIP-92 / NIP-94 / NIP-96 / Blossom — sources for media references and their
declared sha256 (
x/ox, or the hash embedded in a Blossom URL path), enabling per-blob verification. - NIP-09 — deletion requests (kind 5) are archived as ordinary events; archival is content-preservation and does not apply deletions. Clients MAY record deletion status separately.
- NIP-31 — the
alttag describes the event for clients that don't implement this kind.
Limitations
- Completeness is unprovable. A crawl cannot prove it observed every event a
pubkey ever signed (pruned, offline, or unknown relays; silent caps).
coverage.completeis therefore permanentlyfalse, andcoveragerecords exactly what was reached. An attestation proves authenticity of its contents, never exhaustiveness. - Received messages. An author-only crawl captures sent events; it does not capture DMs received (NIP-04) or gift wraps addressed to the pubkey (NIP-59), which are authored elsewhere.
- Replaceable history. Relays usually retain only the latest version of replaceable/addressable events, so historical versions are typically unavailable.
- Unverifiable media. References with no signed hash (bare content URLs,
profile pictures) can be captured but not cryptographically tied to the event
(
verified: null).
Security considerations
- Signatures prove authorship, not truthfulness; an attestation vouches only that this pubkey assembled this snapshot at this time.
- Verification requires a secure context (Web Crypto sha256) and MUST recompute hashes from raw bytes rather than trusting the manifest's self-reported values.
- A verifier should treat
verified: nullmedia as "present but unattested," never as "intact."
Open questions
- Should the CID profile be pinned by this NIP or negotiated per-archive (a
profilefield in the manifest)? - An optional
prevtag linking to the previous attestation id, to form an explicit snapshot chain? - An optional pinning/availability hint (gateway URL, pinning-service receipt) distinct from the CID commitment?
- Multi-key / delegated archives (NIP-26-style) — out of scope for v1.
Cited links
Discussion
Connect a key to comment.