NIP-3B — OpenTimestamps over Signed Events

OTS attestations that bind both event.id AND the Schnorr signature, plus an in-event JSON side-car transport. Companion to NIP-03 (which only commits to event.id).

NIP-3B

OpenTimestamps over Signed Events

draft optional

This NIP defines a Bitcoin time-attestation scheme for Nostr events that binds the attestation to the signed event — i.e. the event id together with the Schnorr signature — instead of the canonical event id alone.

It also specifies a non-canonical, in-event JSON transport for the proof (“side-car”), so a single network roundtrip can carry both the signed event and its time-attestation. As a courtesy to legacy consumers it includes a demo profile that re-uses the same transport for NIP-03 proofs.

This NIP is a companion to NIP-03, not a replacement: NIP-03 proofs remain interoperable; clients that wish to keep them MAY still produce or consume kind:1040 events. NIP-3B specifies what to do when you want a proof that actually attests the signature.

Why a new scheme

NIP-03 specifies an OpenTimestamps proof over event.id:

The OpenTimestamps proof MUST prove the referenced e event id as its digest.

event.id is sha256(canonical_serialization) — i.e. the canonical fields [0, pubkey, created_at, kind, tags, content] hashed. Since the signature is not part of that hash, an OTS proof of event.id says nothing about when the signature was produced. An attacker holding a pre-existing canonical event serialization can forge a Schnorr signature over the same id at any later moment; the original NIP-03 proof would still verify, even though the signed event “did not exist” in any meaningful Nostr sense at the attested time.

To bind time-attestation to the signed event itself, NIP-3B uses a leaf digest that commits to both the canonical id and the signature:

leaf = sha256( event.id_bytes || event.sig_bytes )

event.id_bytes is the 32-byte hex-decoded id; event.sig_bytes is the 64-byte hex-decoded Schnorr signature. The OTS proof is built over those 96 bytes hashed.

Because Schnorr signatures over secp256k1 cannot be forged without the private key, an OTS proof of sha256(id || sig) proves that BOTH the id and a valid signature over it existed before the attested Bitcoin block.

Transport

NIP-3B specifies two transports for the same proof. They are interoperable and either may be used; relays / clients SHOULD support both on read.

A. Kind 1041 — “OpenTimestamps Attestation over Signed Event”

A regular event whose content carries the base64-encoded .ots blob:

{
  "kind": 1041,
  "tags": [
    ["e",          "<target-event-id>",   "<relay-url-hint>"],
    ["k",          "<target-event-kind>"],
    ["p",          "<target-event-pubkey>"],
    ["digest_alg", "id+sig"]
  ],
  "content": "<base64-encoded .ots file>"
}

The .ots blob’s leaf digest MUST equal sha256(target.id_bytes || target.sig_bytes).

Tag rules:

  • e (REQUIRED) — the target event id (64 hex chars). The third element is an optional relay-url hint per NIP-01.
  • k (REQUIRED) — the target event’s kind, as a decimal string.
  • p (REQUIRED) — the target event’s pubkey (64 hex chars).
  • digest_alg (RECOMMENDED) — "id+sig". Reserved for future schemes.

The content field MUST be the base64-encoded full bytes of an .ots file. The file SHOULD contain a single Bitcoin attestation; pending calendars SHOULD be elided in published copies (clients that produce calendar-pending proofs SHOULD upgrade and re-publish, or keep them in private storage until upgraded).

This is parallel to NIP-03’s kind:1040, with three differences:

  1. The leaf digest is id || sig instead of id alone.
  2. The digest_alg tag is REQUIRED to distinguish the schemes when both appear (a relay holding both 1040 and 1041 attestations for the same target is normal during migration).
  3. The p tag is REQUIRED so consumers can resolve the target pubkey without looking up the original event.

B. In-event side-car (ots_proof)

When the publisher of an event holds a proof for that same event, the proof MAY ride along inside the event’s JSON as a top-level field:

{
  "id":         "<event-id>",
  "pubkey":     "<pubkey>",
  "created_at": 1234567890,
  "kind":       1,
  "tags":       [],
  "content":    "the post",
  "sig":        "<sig>",
  "ots_proof":  "<base64-encoded .ots file>",
  "ots_alg":    "id+sig"
}

ots_proof and ots_alg are NOT part of the canonical signing input and are NOT inside the tags array. The signature is computed over only [0, pubkey, created_at, kind, tags, content] exactly as in NIP-01.

The .ots file’s leaf digest MUST equal sha256(id || sig) of the same event. ots_alg MUST be "id+sig" for the scheme defined in this NIP; the field is reserved so future schemes can carry the proof in the same slot without ambiguity.

The side-car MAY be stripped by any relay or client that does not wish to carry it. Stripping leaves the canonical signed event intact; the signature continues to verify; only the time-attestation is lost. This makes the side-car opportunistic by design: a publisher SHOULD also produce and broadcast a kind:1041 event so the proof can be obtained out of band when the side-car is not available.

A relay or client that wishes to retain side-cars across writes SHOULD emit them on outbound EVENT frames whenever the consumer has signalled support — for example via a connection-time hint (?ots_sidecar=1 is a reasonable convention). When no such signal is present, the relay SHOULD emit canonical events unchanged so unaware Nostr clients keep working.

Verification

A NIP-3B proof is valid when ALL of the following hold:

  1. The signed event verifies under NIP-01 (canonical id + valid Schnorr signature).
  2. The proof’s .ots file deserialises and walks to a Bitcoin block header against the verifier’s local chain (no external trust required; any standards-compliant OpenTimestamps verifier suffices).
  3. The leaf digest the proof commits to equals sha256(event.id_bytes || event.sig_bytes) for the kind-1041 form, or sha256(id || sig) of the carrying event for the side-car form.

Each consumer SHOULD verify independently with its own miniots / OTS verifier and its own Bitcoin headers store. Consumers MUST NOT rely on a relay’s accept-rules as a substitute for verification.

The earliest Bitcoin block height across a verifying proof’s attestations is the canonical “this signed event existed by block N” claim. When more than one valid NIP-3B proof exists for the same event, consumers SHOULD take the earliest block height as the canonical attested time.

Demo profile: NIP-03 over the side-car transport (oldnostrots)

To exercise the in-event side-car transport with the existing fleet of NIP-03 kind:1040 attestations, this NIP optionally specifies a second, non-canonical field on the event JSON:

{
  "id": "...", "pubkey": "...", ..., "sig": "...",
  "oldnostrots": "<base64-encoded .ots file>"
}

oldnostrots carries an OTS proof whose leaf digest commits to event.id ALONE (the legacy NIP-03 scheme). A consumer that already holds a kind:1040 event referencing some target event MAY repackage the proof into the side-car format by emitting the canonical target event with the oldnostrots field added.

This is explicitly a demo of the transport, not of the digest scheme. A proof in oldnostrots does not bind the signature and inherits the NIP-03 weakness described above. It exists so:

  • Existing NIP-03 attestations can be transported alongside their signed events without introducing kind:1041 events that would re-attest under a stronger scheme they cannot satisfy.
  • Implementers can validate the in-JSON side-car transport end to end — publishing, relay storage, on-the-wire reattach, downstream verification — using the existing corpus of Bitcoin-confirmed NIP-03 proofs.

A consumer MAY treat oldnostrots as a “this event id existed by block N” hint, but MUST NOT treat it as a binding time-attestation of the signature. New deployments SHOULD always use ots_proof (id+sig) and not oldnostrots.

ots_proof and oldnostrots MAY appear on the same event. When they do, each is independently verifiable; verifiers SHOULD prefer ots_proof.

Redundant-attestation cleanup

Because OTS proofs accumulate calendar-pending stubs and may carry multiple Bitcoin attestations, an in-event side-car blob can be considerably larger than the minimal proof needed to verify the attested time. A side-car holder MAY prune the proof by:

  1. Re-running the OTS proof through any pruner that retains the earliest verifying Bitcoin attestation and discards pending calendars and later attestations.
  2. Replacing the ots_proof (or oldnostrots) value with the pruned bytes.

The event.id, pubkey, tags, content, and sig MUST remain unchanged. The pruned proof MUST still verify against the same leaf digest. Consumers receiving a pruned proof for an event whose unpruned form they previously held SHOULD prefer the smaller blob (the pruned form is strictly more compact while semantically equivalent).

Comparison with NIP-03

Property NIP-03 (kind:1040) NIP-3B (kind:1041) NIP-3B (side-car)
Leaf digest binds event id yes yes yes
Leaf digest binds Schnorr signature no yes yes
Separate Nostr event for the proof yes yes no
Carried inside the target event JSON n/a n/a yes
Removable per relay / per client yes (own event) yes (own event) yes (just strip)
Verification needs external lookup yes (target event) yes (target event) no

NIP-03 and NIP-3B are not in conflict; both kinds may coexist on the same relay for the same target event. Consumers SHOULD prefer NIP-3B when both schemes claim the same target.

Migration

For a target event already carrying a NIP-03 attestation, the publisher MAY:

  • Continue to publish NIP-03 kind:1040 events.
  • ADDITIONALLY publish a NIP-3B kind:1041 event whose .ots proves sha256(id || sig) of the same target.
  • ADDITIONALLY include ots_proof (NIP-3B) and/or oldnostrots (the NIP-03 demo profile) on the target event when re-broadcasting it.

There is no requirement to retract NIP-03 attestations. They remain valid as id-only timestamps; they simply do not provide the stronger signature-binding guarantee NIP-3B does.

Examples

Kind 1041 attesting a kind 1 note

{
  "kind": 1041,
  "id": "53a3a26844ac13ceb677c73bf3463650bd9681c080eb4bee574d252ff33be298",
  "pubkey": "6d7124d023574010cd5d3aea61b262a2b96e9dff41889102d2c34a058a859ec2",
  "created_at": 1778100210,
  "tags": [
    ["e", "7e0c8cbe361180827fe9ee2f4b7658218ded36838e9009216ebd793e92532e29",
          "ws://127.0.0.1:4849"],
    ["k", "1"],
    ["p", "6d7124d023574010cd5d3aea61b262a2b96e9dff41889102d2c34a058a859ec2"],
    ["digest_alg", "id+sig"]
  ],
  "content": "AE9wZW5UaW1lc3RhbXBzAABQcm9vZgC/...",
  "sig": "..."
}

The content is the base64-encoded .ots. Its leaf digest equals sha256("7e0c8cbe...".bytes || "<sig of 7e0c...>".bytes).

Kind 1 with ots_proof side-car

{
  "id":         "97369303c3119d0f06fe0b11043ccd24db6a4910c8c1c44f8c332ce7a9735210",
  "pubkey":     "6d7124d023574010cd5d3aea61b262a2b96e9dff41889102d2c34a058a859ec2",
  "created_at": 1778115277,
  "kind":       1,
  "tags":       [],
  "content":    "OTSuite end-to-end via supervisor",
  "sig":        "...",
  "ots_proof":  "AE9wZW5UaW1lc3RhbXBzAABQcm9vZgC/...",
  "ots_alg":    "id+sig"
}

The signature is over [0, pubkey, created_at, kind, tags, content] per NIP-01; ots_proof and ots_alg are outside that scope. The .ots file’s leaf digest equals sha256(id || sig).

Kind 1 with the demo oldnostrots field (legacy id-only)

{
  "id":          "430bf2069503bae5af8bc0514d73a8aa0ed6d8fd616e2048599a82b88e818530",
  "pubkey":      "...",
  "created_at":  1778097096,
  "kind":        1,
  "tags":        [],
  "content":     "...",
  "sig":         "...",
  "oldnostrots": "AE9wZW5UaW1lc3RhbXBzAABQcm9vZgC/..."
}

The proof commits to event.id alone — see the warning in the demo section above. New deployments SHOULD use ots_proof instead.

Implementation notes

  • A relay that strips unknown top-level fields will discard side-cars; the signed event remains intact. Side-cars are opportunistic.
  • A relay that wishes to preserve side-cars across writes SHOULD store the proof bytes by event id in its own storage and reattach them on outbound EVENT frames when the subscriber has signalled support. A simple HTTP side-channel (e.g. GET /sidecar/<event-id> returning the raw .ots) is also a reasonable transport for consumers that don’t want to opt in on the WS layer.
  • The proof in the side-car SHOULD be identical to the proof in the matching kind:1041 event when both exist. A consumer that finds two proofs for the same target SHOULD reject any inconsistency (different leaf digests, different earliest Bitcoin heights) and prefer the verified one.
  • For verification, consumers SHOULD use a local Bitcoin headers store rather than trusting a third party. Each verifying component in a pipeline SHOULD perform its own check.

References

  • NIP-01 Basic protocol flow description
  • NIP-03 OpenTimestamps Attestations for Events
  • OpenTimestamps Bitcoin-anchored time attestations

Write a comment
No comments yet.