TS port slice 6: Nostr event protocol layer with rust-nostr cross-fixtures

- NIP-01 event serialization/id computation, BIP-340 signing and
  verification via @noble/curves, SeedPass event builders (manifest 30070,
  chunk 30071, delta 30072 with d/e tags), and REQ/EVENT/EOSE/OK/NOTICE/
  CLOSED client-relay framing. Transport-agnostic; the WebSocket adapter
  lands with the interface milestones.
- Cross-implementation fixtures generated through the Python side's
  nostr_sdk (rust-nostr) with pinned created_at: TS ids match theirs
  byte-for-byte, their signatures verify in TS, and TS-signed events
  reproduce identical ids. The generator caches signatures across runs
  (BIP-340 aux randomness) so fixture output stays byte-deterministic and
  the CI drift job holds.
- 143 core parity tests (Node + jsdom) + 16 CLI tests green.

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Fable 5 committed 2026-08-13 21:34:40 -04:00
1 parent e91d5d0be2
commit 774c992153
8 files changed
+518 -3

No files matched your search

+3 -2
View File
@@ -35,6 +35,7 @@ until a later milestone.
| Deterministic conflict merge | P0 | green | `sync_merge.json` | Full `merge_index_payloads` port: ts precedence, canonical-hash tie-breaks (Python-compatible ensure_ascii canonical JSON), equal-ts field union incl. the `custom_fields: null` quirk, tombstone lifecycle, retention cap, `_sync_meta`. |
| Tombstones / delta replay | P0 | green | `delta_replay.json` | Encrypted V3 deltas decrypted and merged in order with `source_tag = sha256(payload)[:16]`, final state matches Python. |
| index0 content merge (`_system.index0`) | P2 | partial | — | Empty-skeleton normalization matches Python; merging populated index0 event logs throws loudly instead of silently dropping data. Full port belongs to the atlas milestone. |
| Nostr event layer (NIP-01 ids, BIP-340 sign/verify, framing) | P0 | green | `nostr_events.json` | Event ids match rust-nostr (Python nostr_sdk) fixtures byte-for-byte; fixture signatures verify in TS; TS-signed events reproduce the same ids and verify. REQ/EVENT/EOSE/OK/NOTICE/CLOSED framing round-trips. Fixture signatures are cached across generator runs (BIP-340 aux randomness). WebSocket relay transport: todo (interface milestones). |
| Portable backup import/export | P0 | green | `portable_backup.json` | format_version 1, seed-only and plaintext modes; TS imports the Python export, re-exports byte-identically under pinned nonce/timestamp, verifies canonical checksum, rejects tampering/unknown versions. |
| Entry creation (provision) | P0 | green | `entries_index.json` | TS `add*Entry` ops rebuild the Python `EntryManager` fixture index byte-for-byte (pinned clock): field shapes, id allocation, independent TOTP derivation-index allocation, ISO timestamp format. Not ported: index0 event emission, modify/archive ops. |
| Entry secret retrieval | P0 | green | `entry_secrets.json` | Reveal values match Python retrieval for password (v2), TOTP-at-time, nostr entry nsec (BIP-85 app 39 — NOT the sync client's 1237), seed and managed-account mnemonics. |
@@ -44,8 +45,8 @@ until a later milestone.
| Environment | Status |
|---|---|
| Node 22 (vitest) | green — 129/129 core + 16 CLI |
| jsdom browser-like env | green — 129/129 core |
| Node 22 (vitest) | green — 143/143 core + 16 CLI |
| jsdom browser-like env | green — 143/143 core |
| Real Chromium/Firefox (vitest browser mode) | todo — lands with Milestone 6 web app CI |
## Dependency notes (to expand into docs/typescript_dependency_review.md)
+18
View File
@@ -106,4 +106,22 @@ export {
type ChunkMeta,
type Manifest,
} from "./sync/snapshot.js";
export {
computeEventId,
serializeEventForId,
signEvent,
verifyEvent,
signerPublicKeyHex,
buildChunkEvent,
buildManifestEvent,
buildDeltaEvent,
reqMessage,
eventMessage,
closeMessage,
parseRelayMessage,
type NostrEvent,
type UnsignedEvent,
type Filter,
type RelayMessage,
} from "./sync/events.js";
export * from "./util/bytes.js";
+207
View File
@@ -0,0 +1,207 @@
/**
* Nostr event protocol layer: NIP-01 event ids, BIP-340 signing/verify, and
* client-relay message framing. Transport-agnostic — the WebSocket relay
* adapter builds on this but lives with the interface milestones.
*
* Cross-implementation fixtures come from the Python side's rust-nostr
* (nostr_sdk); TS-computed ids must match theirs exactly.
*/
import { schnorr } from "@noble/curves/secp256k1.js";
import { sha256 } from "@noble/hashes/sha2.js";
import { bytesToHex, hexToBytes, utf8 } from "../util/bytes.js";
import { KIND_DELTA, KIND_MANIFEST, KIND_SNAPSHOT_CHUNK } from "./snapshot.js";
export interface NostrEvent {
id: string;
pubkey: string;
created_at: number;
kind: number;
tags: string[][];
content: string;
sig: string;
}
export type UnsignedEvent = Omit<NostrEvent, "id" | "sig" | "pubkey">;
/**
* NIP-01 string escaping for the id serialization: exactly \n \" \\ \r \t
* \b \f are escaped; every other character (including other control chars
* and non-ASCII) is included verbatim as UTF-8.
*/
function nip01EscapeString(s: string): string {
let out = '"';
for (const ch of s) {
switch (ch) {
case "\n": out += "\\n"; break;
case '"': out += '\\"'; break;
case "\\": out += "\\\\"; break;
case "\r": out += "\\r"; break;
case "\t": out += "\\t"; break;
case "\b": out += "\\b"; break;
case "\f": out += "\\f"; break;
default: out += ch;
}
}
return out + '"';
}
/** The exact byte sequence NIP-01 hashes: [0,pubkey,created_at,kind,tags,content]. */
export function serializeEventForId(
pubkey: string,
createdAt: number,
kind: number,
tags: string[][],
content: string,
): string {
const tagsJson =
"[" +
tags.map((tag) => "[" + tag.map(nip01EscapeString).join(",") + "]").join(",") +
"]";
return `[0,${nip01EscapeString(pubkey)},${createdAt},${kind},${tagsJson},${nip01EscapeString(content)}]`;
}
export function computeEventId(
pubkey: string,
createdAt: number,
kind: number,
tags: string[][],
content: string,
): string {
return bytesToHex(sha256(utf8(serializeEventForId(pubkey, createdAt, kind, tags, content))));
}
export function signerPublicKeyHex(privateKeyHex: string): string {
return bytesToHex(schnorr.getPublicKey(hexToBytes(privateKeyHex)));
}
/** Sign an event with a secp256k1 private key (x-only pubkey, BIP-340 sig). */
export function signEvent(privateKeyHex: string, unsigned: UnsignedEvent): NostrEvent {
const priv = hexToBytes(privateKeyHex);
const pubkey = bytesToHex(schnorr.getPublicKey(priv));
const id = computeEventId(
pubkey,
unsigned.created_at,
unsigned.kind,
unsigned.tags,
unsigned.content,
);
const sig = bytesToHex(schnorr.sign(hexToBytes(id), priv));
return { id, pubkey, ...unsigned, sig };
}
/** Verify an event's id derivation and BIP-340 signature. */
export function verifyEvent(event: NostrEvent): boolean {
const expectedId = computeEventId(
event.pubkey,
event.created_at,
event.kind,
event.tags,
event.content,
);
if (expectedId !== event.id) return false;
try {
return schnorr.verify(hexToBytes(event.sig), hexToBytes(event.id), hexToBytes(event.pubkey));
} catch {
return false;
}
}
// --- SeedPass event builders (parity: nostr/snapshot.py EventBuilder use) ---
export function buildChunkEvent(
chunkId: string,
contentB64: string,
createdAt: number,
): UnsignedEvent {
return {
kind: KIND_SNAPSHOT_CHUNK,
tags: [["d", chunkId]],
content: contentB64,
created_at: createdAt,
};
}
export function buildManifestEvent(
manifestId: string,
manifestJson: string,
createdAt: number,
): UnsignedEvent {
return {
kind: KIND_MANIFEST,
tags: [["d", manifestId]],
content: manifestJson,
created_at: createdAt,
};
}
export function buildDeltaEvent(
manifestEventId: string,
contentB64: string,
createdAt: number,
): UnsignedEvent {
return {
kind: KIND_DELTA,
tags: [["e", manifestEventId]],
content: contentB64,
created_at: createdAt,
};
}
// --- NIP-01 client/relay message framing ---
export interface Filter {
ids?: string[];
authors?: string[];
kinds?: number[];
"#d"?: string[];
"#e"?: string[];
since?: number;
until?: number;
limit?: number;
}
export function reqMessage(subscriptionId: string, ...filters: Filter[]): string {
return JSON.stringify(["REQ", subscriptionId, ...filters]);
}
export function eventMessage(event: NostrEvent): string {
return JSON.stringify(["EVENT", event]);
}
export function closeMessage(subscriptionId: string): string {
return JSON.stringify(["CLOSE", subscriptionId]);
}
export type RelayMessage =
| { type: "EVENT"; subscriptionId: string; event: NostrEvent }
| { type: "EOSE"; subscriptionId: string }
| { type: "OK"; eventId: string; accepted: boolean; message: string }
| { type: "NOTICE"; message: string }
| { type: "CLOSED"; subscriptionId: string; message: string };
export function parseRelayMessage(raw: string): RelayMessage {
const arr = JSON.parse(raw) as unknown[];
if (!Array.isArray(arr) || typeof arr[0] !== "string") {
throw new Error("malformed relay message");
}
switch (arr[0]) {
case "EVENT":
return { type: "EVENT", subscriptionId: String(arr[1]), event: arr[2] as NostrEvent };
case "EOSE":
return { type: "EOSE", subscriptionId: String(arr[1]) };
case "OK":
return {
type: "OK",
eventId: String(arr[1]),
accepted: Boolean(arr[2]),
message: String(arr[3] ?? ""),
};
case "NOTICE":
return { type: "NOTICE", message: String(arr[1] ?? "") };
case "CLOSED":
return { type: "CLOSED", subscriptionId: String(arr[1]), message: String(arr[2] ?? "") };
default:
throw new Error(`unknown relay message type: ${arr[0]}`);
}
}
+135
View File
@@ -0,0 +1,135 @@
/**
* Nostr event layer parity: NIP-01 id computation must match rust-nostr
* (via the Python-side nostr_sdk fixtures), signatures must verify, and
* TS-built/signed events must be accepted by TS verification.
*/
import { describe, expect, it } from "vitest";
import { nostrEvents, nostrSnapshot, deltaReplay, nostrKeyCases } from "@seedpass/test-vectors";
import {
computeEventId,
signEvent,
verifyEvent,
signerPublicKeyHex,
buildChunkEvent,
buildManifestEvent,
buildDeltaEvent,
reqMessage,
parseRelayMessage,
eventMessage,
type NostrEvent,
} from "@seedpass/core";
describe("NIP-01 event ids (cross-implementation)", () => {
it.each(nostrEvents.events)("$name id matches rust-nostr", ({ event }) => {
expect(
computeEventId(event.pubkey, event.created_at, event.kind, event.tags, event.content),
).toBe(event.id);
});
it("signer identity matches the app-1237 fixture key", () => {
const fixtureKey = nostrKeyCases.find(
(c) => c.mnemonic_id === "abandon12" && c.account_index === 0,
)!;
expect(nostrEvents.signer_private_key_hex).toBe(fixtureKey.private_key_hex);
expect(signerPublicKeyHex(fixtureKey.private_key_hex)).toBe(
nostrEvents.signer_public_key_hex,
);
});
});
describe("BIP-340 signature verification", () => {
it.each(nostrEvents.events)("$name verifies", ({ event }) => {
expect(verifyEvent(event as NostrEvent)).toBe(true);
});
it("rejects a tampered event", () => {
const event = { ...(nostrEvents.events[0]!.event as NostrEvent) };
event.content = event.content + "x";
expect(verifyEvent(event)).toBe(false);
});
});
describe("event builders match the Python event shapes", () => {
const byName = (name: string) =>
nostrEvents.events.find((e) => e.name === name)!.event as NostrEvent;
it("chunk event", () => {
const py = byName("chunk-0");
const unsigned = buildChunkEvent(
nostrSnapshot.chunk_metas[0]!.id,
nostrSnapshot.chunks_b64[0]!,
py.created_at,
);
expect(computeEventId(py.pubkey, unsigned.created_at, unsigned.kind, unsigned.tags, unsigned.content)).toBe(py.id);
});
it("manifest event", () => {
const py = byName("manifest");
const unsigned = buildManifestEvent(
nostrSnapshot.manifest_id,
nostrSnapshot.manifest_json,
py.created_at,
);
expect(computeEventId(py.pubkey, unsigned.created_at, unsigned.kind, unsigned.tags, unsigned.content)).toBe(py.id);
});
it("delta event", () => {
const py = byName("delta-1");
const unsigned = buildDeltaEvent(
nostrSnapshot.manifest_id,
deltaReplay.delta_payloads_b64[0]!,
py.created_at,
);
expect(computeEventId(py.pubkey, unsigned.created_at, unsigned.kind, unsigned.tags, unsigned.content)).toBe(py.id);
});
it("TS signing produces the same ids and a valid signature", () => {
const py = byName("manifest");
const signed = signEvent(
nostrEvents.signer_private_key_hex,
buildManifestEvent(nostrSnapshot.manifest_id, nostrSnapshot.manifest_json, py.created_at),
);
expect(signed.id).toBe(py.id);
expect(signed.pubkey).toBe(py.pubkey);
expect(verifyEvent(signed)).toBe(true);
});
});
describe("NIP-01 message framing", () => {
it("REQ round-trips filters", () => {
const raw = reqMessage("sub1", {
authors: [nostrEvents.signer_public_key_hex],
kinds: [30070],
"#d": [nostrSnapshot.manifest_id],
limit: 1,
});
const arr = JSON.parse(raw);
expect(arr[0]).toBe("REQ");
expect(arr[2].kinds).toEqual([30070]);
});
it("parses EVENT/EOSE/OK/NOTICE/CLOSED", () => {
const ev = nostrEvents.events[0]!.event;
expect(parseRelayMessage(JSON.stringify(["EVENT", "sub1", ev]))).toEqual({
type: "EVENT",
subscriptionId: "sub1",
event: ev,
});
expect(parseRelayMessage('["EOSE","sub1"]')).toEqual({ type: "EOSE", subscriptionId: "sub1" });
expect(parseRelayMessage(`["OK","${ev.id}",true,""]`)).toEqual({
type: "OK",
eventId: ev.id,
accepted: true,
message: "",
});
expect(parseRelayMessage('["NOTICE","hi"]')).toEqual({ type: "NOTICE", message: "hi" });
expect(parseRelayMessage('["CLOSED","sub1","reason"]')).toEqual({
type: "CLOSED",
subscriptionId: "sub1",
message: "reason",
});
const msg = eventMessage(ev as NostrEvent);
expect(JSON.parse(msg)[1].id).toBe(ev.id);
});
});
@@ -11,6 +11,7 @@
"kdf_metadata.json",
"legacy_payloads.json",
"managed_seeds.json",
"nostr_events.json",
"nostr_keys.json",
"nostr_snapshot.json",
"password_kdf.json",
@@ -23,6 +24,6 @@
],
"fixture_version": 1,
"generator": "scripts/generate_ts_port_fixtures.py",
"python_commit": "ab569fe900213937a21f0616057d3d5c46015e3c",
"python_commit": "e91d5d0be2355c4be78b8c455ddf87b66d795036",
"redaction_policy": "All fixtures derive from public BIP-39 test mnemonics (abandon.../legal.../zoo...). Secret-shaped outputs are generated test material, never from a live profile."
}
@@ -0,0 +1,60 @@
{
"description": "Signed Nostr events from the Python-side nostr_sdk (rust-nostr) with pinned created_at. Ids are NIP-01-deterministic; signatures are cached across generator runs (BIP-340 aux randomness).",
"events": [
{
"event": {
"content": "H4sIAAAAAAACAwHjDxzwVjN8yABUBOsovylao1dfY22Y+jyucdrP9HwxZksfJP00vjARSES7X9CGz/sCp7ZcIm3Ok3+n478l25p90hmLwZTTO02lv3C3leTqTt680X7TtqbHdEB1RAxkan+TkA18n4UCOw7z3C17AT0qoV3Drvgz5qLhyvTrrMOPxqa32mIUgkVpYAu75KBhS4PMR0FNE/KdEpTvR/De1B+Xk3gb66IfpEsqq5TArksXd7955thFWJVvm1m1amJyUEwk95JSThwVq5SENnISGG7RH7s3z/Ah4hoQCzM/IrN1+tt2Sqg5BHSpemsg2vMSwYlobqggf8P95yYfiIBZ6mkWtA8OfMtm7dYkQDvxOpciy4XDX1BtV9rkFgLnSmptePwMhuHoCVsnpkq0PKttBHFA0eieup6wvI9AODZ60Vi/Saol1oyjNQwmvqiM8JqPwkbr2bEZLfOIMddPAVtktQJV0BtBRAdmU4UwkfufUOSHYYK8fxvJefJU/sq5H/AN2iUsUXaSnJaYnj+Dr5pV/ouYeq0tUCt2p/Shrf3+YZQlovnTkGCLGcLaBqXTPW2uAbf+G5HAgpr1MZwEFhIOWjt8lGQPIHFqYCtC6IcFiOn95oZYnqofGrUpvERLXjLO+8KFd902Kl2ymC6MWmTAR68ab5WR1TzrhFBBp1lOc1sOuTLiXb9wnS0T/JquX1QRplZLnxHP2gzDZsRMVzTY5WgSNvjye5c3qQWvftGs7iIjjTdUt20zSwonFErRCl7AXVkJmiUmTwt4por69NYBLDFMUZAFv82n+jC1Mk5Ixh23SYMXn9ptXM7PdHKDr1r7UNrnRBM33Zzh752A3QzfAlYIorD4usWFgz+lG8Xw97Ef/QVCaRsge3TUlat/MARldvbCCWSB7RiFmWbzyQVIP8MHXoTX0fSdvtSRBRbQUFv2ImotG3E8vgPObiv65LtmPJf6HkhosGX0VIKLgGJj4Mzi63BN0Fu/+/FEHIomX+tlQAuU/HNsWmGIogvqylKT/Bd7AyYaUw9TDg6hlbnoZDpALziNL4k=",
"created_at": 1700000000,
"id": "28e4ad167400ee19929818b50d2d72cce084613b08e5761e6af1d0e1c3af95aa",
"kind": 30071,
"pubkey": "257f22c3049cf52100bdf17d6bce85b7c9d648850e4e3c1965733dd176b646e0",
"sig": "58c7b0a53c12f5912a879289fb4244d72956522e037d3e31820fa2aecc8e64e9992f05653e4cc30f59a491b26b553ba82ff82443d1c2d5531a7ee77ce29f5447",
"tags": [
[
"d",
"seedpass-chunk-0000"
]
]
},
"name": "chunk-0"
},
{
"event": {
"content": "{\"ver\": 1, \"algo\": \"gzip\", \"chunks\": [{\"id\": \"seedpass-chunk-0000\", \"size\": 800, \"hash\": \"c57f7c84461a7db48c478ce8f62a4a19be933a592436f2bedeb85bd54ca3ed48\", \"event_id\": null}, {\"id\": \"seedpass-chunk-0001\", \"size\": 800, \"hash\": \"a8dbca0d027ec5af9f517bb543ce3e7f2e511b76ca8fd6656d0054fde796815b\", \"event_id\": null}, {\"id\": \"seedpass-chunk-0002\", \"size\": 800, \"hash\": \"7672fd2923ba8ba868282176385d45400d4787fae28a9430df3c3df9253b24ef\", \"event_id\": null}, {\"id\": \"seedpass-chunk-0003\", \"size\": 800, \"hash\": \"9fdc9b622219d3e2874e4eee96ba134d682a7f720478ba770462c5ade273510c\", \"event_id\": null}, {\"id\": \"seedpass-chunk-0004\", \"size\": 800, \"hash\": \"dbf3068bf08dec2be41992e3163a5ded6bd7b10dd7f6dbc6ffd9fd0bc992f70a\", \"event_id\": null}, {\"id\": \"seedpass-chunk-0005\", \"size\": 90, \"hash\": \"76fd3739a3e002e1921a47e3c0e3b947c2c3f45304deda86745279a5ef24d6c2\", \"event_id\": null}], \"delta_since\": 1700000000, \"nonce\": \"PAqvfNIUFpEog1t93a3M4A==\", \"index0\": null}",
"created_at": 1700000000,
"id": "f6b1362c95101cf8ec58bce9af8c7ae67f41ebf29a52a8c5239953f0b550cf78",
"kind": 30070,
"pubkey": "257f22c3049cf52100bdf17d6bce85b7c9d648850e4e3c1965733dd176b646e0",
"sig": "ee8def6729fbd1619f19091fea9d653c5b7539f9a7c2662a9678b8c66903ab0c1ed9af6ba3593858162aa24f8db25ab78118159ab113ac1f183ecbe85d7e10e0",
"tags": [
[
"d",
"168836eb089b4d0b57f92b3b14eaeb9e875ee9dcd8f59c80f6d027e08f273c12"
]
]
},
"name": "manifest"
},
{
"event": {
"content": "VjN8dk5XXH5X36Xdmu5olZDxZJTpOTZDjskABP6osJd8Rftxc7YoBfXa/Yh4XHpySBsf70Vnm0Q0m1xQBOoeShxXtCgUbQ48x8pY3NnY3suMDjlqou0ti5vuMVknTRPwgyBnFAo1DUeGmZSidFK0/z8EfxqanEklB9e9yUtqOiN04UmwK8tnr0u0n2pJd82oppv3PNPZs4FFFFmiD1d+ZVlOBEyuziEmTjuhP6axqflqEv4Y2nox82w+XvBbFhkCO2lVO9Q++81rhE8OKJ9q9iuUURD+U83UjhLTdNavcu+jk2r4AyUMQh9rK43IabeEK5cFJmFpLWNOinzouOvQJQ9yGPvccYmarBIza8BsiJLINnX1oNNRk8TG3NQ4i8bKdeXBaATuBeLPDVhF2GKICGzy3sQN7CbvB5BjPBKD4+om2eVU8MJW2AKCz5WTMADF",
"created_at": 1700000000,
"id": "640d785f97902b72f152294b5497054bb159ff2ba66b7b4b89d0d8946596b286",
"kind": 30072,
"pubkey": "257f22c3049cf52100bdf17d6bce85b7c9d648850e4e3c1965733dd176b646e0",
"sig": "871a1b745e2e0ce7f7fd341666543f7961a1029ddfa8f45f46ddc78fe26011f312b6235dee97890c0d9bc20b9fe33cb4fd6391376c17e353bb2aca1d89b8c4f4",
"tags": [
[
"e",
"168836eb089b4d0b57f92b3b14eaeb9e875ee9dcd8f59c80f6d027e08f273c12"
]
]
},
"name": "delta-1"
}
],
"fixture_version": 1,
"mnemonic_id": "abandon12",
"signer_private_key_hex": "0ac7be6c7b08bf16ead8778494cf7256c566fb68bb09c0285b056d8a568516fc",
"signer_public_key_hex": "257f22c3049cf52100bdf17d6bce85b7c9d648850e4e3c1965733dd176b646e0"
}
+18
View File
@@ -27,6 +27,7 @@ import syncMergeJson from "../fixtures/sync_merge.json";
import deltaReplayJson from "../fixtures/delta_replay.json";
import portableBackupJson from "../fixtures/portable_backup.json";
import entrySecretsJson from "../fixtures/entry_secrets.json";
import nostrEventsJson from "../fixtures/nostr_events.json";
export interface Bip39Case {
id: string;
@@ -213,6 +214,23 @@ export const entrySecrets = entrySecretsJson as {
managed_entry_8_mnemonic: string;
};
export interface SignedNostrEvent {
id: string;
pubkey: string;
created_at: number;
kind: number;
tags: string[][];
content: string;
sig: string;
}
export const nostrEvents = nostrEventsJson as {
mnemonic_id: string;
signer_private_key_hex: string;
signer_public_key_hex: string;
events: { name: string; event: SignedNostrEvent }[];
};
export const legacyPayloads = legacyPayloadsJson as {
mnemonic_id: string;
plaintext_utf8: string;
+75
View File
@@ -906,6 +906,78 @@ def gen_entry_secrets() -> dict:
}
def gen_nostr_events(snapshot: dict, delta_replay: dict) -> dict:
"""Signed Nostr events for the snapshot/chunk/delta model.
Event ids are deterministic (NIP-01 serialization); BIP-340 signatures
use random aux data and are NOT. To keep the generator byte-deterministic
we reuse the previously committed signature whenever the event id is
unchanged — a fresh signature is only produced for new/changed events.
"""
import json as json_mod
from nostr_sdk import EventBuilder, EventId, Keys, Kind, Tag, Timestamp
# Sync client identity: BIP-85 app 1237, account index 0 (nostr_keys.json)
bip85 = _bip85(MNEMONICS[PRIMARY])
entropy = bip85.derive_entropy(index=0, entropy_bytes=32, app_no=NOSTR_KEY_APP_ID)
keys = Keys.parse(entropy.hex())
existing_sigs: dict[str, str] = {}
existing_path = FIXTURES_DIR / "nostr_events.json"
if existing_path.exists():
try:
for ev in json_mod.loads(existing_path.read_text())["events"]:
existing_sigs[ev["event"]["id"]] = ev["event"]["sig"]
except Exception:
pass
def build(name: str, kind: int, content: str, tags: list) -> dict:
builder = (
EventBuilder(Kind(kind), content)
.tags(tags)
.custom_created_at(Timestamp.from_secs(FIXED_UNIX))
)
event = json_mod.loads(builder.sign_with_keys(keys).as_json())
cached_sig = existing_sigs.get(event["id"])
if cached_sig:
event["sig"] = cached_sig
return {"name": name, "event": event}
events = [
build(
"chunk-0",
30071,
snapshot["chunks_b64"][0],
[Tag.identifier(snapshot["chunk_metas"][0]["id"])],
),
build(
"manifest",
30070,
snapshot["manifest_json"],
[Tag.identifier(snapshot["manifest_id"])],
),
build(
"delta-1",
30072,
delta_replay["delta_payloads_b64"][0],
[Tag.event(EventId.parse(snapshot["manifest_id"]))],
),
]
return {
"description": (
"Signed Nostr events from the Python-side nostr_sdk (rust-nostr) "
"with pinned created_at. Ids are NIP-01-deterministic; signatures "
"are cached across generator runs (BIP-340 aux randomness)."
),
"mnemonic_id": PRIMARY,
"signer_private_key_hex": entropy.hex(),
"signer_public_key_hex": keys.public_key().to_hex(),
"events": events,
}
def gen_kdf_metadata() -> dict:
return {
"description": (
@@ -959,6 +1031,9 @@ def main() -> None:
files["entry_secrets.json"] = gen_entry_secrets()
files["sync_merge.json"] = gen_sync_merge()
files["delta_replay.json"] = gen_delta_replay()
files["nostr_events.json"] = gen_nostr_events(
files["nostr_snapshot.json"], files["delta_replay.json"]
)
for name, data in files.items():
data["fixture_version"] = FIXTURE_VERSION