From 0e07c8660d632265d90be5110020fff0e3cdc89d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kyle=20=F0=9F=90=86?= Date: Fri, 19 Jun 2026 12:32:00 -0400 Subject: [PATCH 1/2] Add decode and verify commands (NIP-19, NIP-01) --- src/main.zig | 110 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/src/main.zig b/src/main.zig index 8e17473..70eedaa 100644 --- a/src/main.zig +++ b/src/main.zig @@ -16,6 +16,8 @@ const usage = \\ count [filters..] Count matching events (NIP-45) \\ relay Print the NIP-11 relay information document \\ sync [filters..] NIP-77 reconcile src's events into dst + \\ decode Decode a NIP-19 entity (npub/nsec/note/nevent/naddr/nprofile) + \\ verify [event-json] Verify an event's id and signature (reads stdin if no arg) \\ \\event flags: --sec -c -k --ts --pow --auth \\ -t -p -e -d (all repeatable) (no url = sign only) @@ -59,6 +61,10 @@ pub fn main(init: std.process.Init) !void { try cmdRelay(io, arena, out, rest); } else if (std.mem.eql(u8, cmd, "sync")) { try cmdSync(arena, out, rest); + } else if (std.mem.eql(u8, cmd, "decode")) { + try cmdDecode(arena, out, rest); + } else if (std.mem.eql(u8, cmd, "verify")) { + try cmdVerify(io, arena, out, rest); } else if (std.mem.eql(u8, cmd, "help") or std.mem.eql(u8, cmd, "-h") or std.mem.eql(u8, cmd, "--help")) { try out.writeAll(usage); } else { @@ -105,6 +111,110 @@ fn cmdKey(env_sec: ?[]const u8, out: *Io.Writer, args: []const [:0]const u8) !vo try out.writeAll("usage: noz key | generate>\n"); } +// Decode a NIP-19 bech32 entity (or bare hex pubkey) into its underlying fields. +fn cmdDecode(arena: Allocator, out: *Io.Writer, args: []const [:0]const u8) !void { + const input = if (args.len >= 1) args[0] else { + try out.writeAll("usage: noz decode \n"); + return; + }; + + try nostr.init(); + defer nostr.cleanup(); + + var decoded = nostr.bech32.decodeNostr(arena, input) catch { + try out.print("invalid: cannot decode {s}\n", .{input}); + out.flush() catch {}; + std.process.exit(1); + }; + defer decoded.deinit(arena); + + switch (decoded) { + .pubkey => |pk| try hexField(out, "pubkey", &pk), + .seckey => |sk| try hexField(out, "seckey", &sk), + .event_id => |id| try hexField(out, "id", &id), + .profile => |p| { + try hexField(out, "pubkey", &p.pubkey); + for (p.relays) |r| try field(out, "relay", r); + }, + .event => |e| { + try hexField(out, "id", &e.id); + if (e.author) |a| try hexField(out, "author", &a); + if (e.kind) |k| try out.print("{s:<8}{d}\n", .{ "kind", k }); + for (e.relays) |r| try field(out, "relay", r); + }, + .addr => |a| { + try out.print("{s:<8}{d}\n", .{ "kind", a.kind }); + try hexField(out, "pubkey", &a.pubkey); + try field(out, "ident", a.identifier); + for (a.relays) |r| try field(out, "relay", r); + }, + .offer => |o| { + try hexField(out, "pubkey", &o.pubkey); + try field(out, "relay", o.relay); + try field(out, "offer", o.offer_id); + }, + .debit => |d| { + try hexField(out, "pubkey", &d.pubkey); + try field(out, "relay", d.relay); + if (d.pointer) |p| try field(out, "pointer", p); + }, + .manage => |m| { + try hexField(out, "pubkey", &m.pubkey); + try field(out, "relay", m.relay); + if (m.pointer) |p| try field(out, "pointer", p); + }, + } +} + +fn field(out: *Io.Writer, label: []const u8, value: []const u8) !void { + try out.print("{s:<8}{s}\n", .{ label, value }); +} + +fn hexField(out: *Io.Writer, label: []const u8, bytes: *const [32]u8) !void { + var buf: [64]u8 = undefined; + nostr.hex.encode(bytes, &buf); + try field(out, label, &buf); +} + +// Verify an event's id and Schnorr signature, read from an arg or stdin. +// Exits non-zero on any validation failure. +fn cmdVerify(io: Io, arena: Allocator, out: *Io.Writer, args: []const [:0]const u8) !void { + const raw = if (args.len >= 1) args[0] else blk: { + var in_buf: [4096]u8 = undefined; + var in_r = Io.File.stdin().reader(io, &in_buf); + break :blk in_r.interface.allocRemaining(arena, .limited(1 << 20)) catch { + try out.writeAll("usage: noz verify (or pipe the event on stdin)\n"); + return; + }; + }; + const json = std.mem.trim(u8, raw, " \t\r\n"); + if (json.len == 0) { + try out.writeAll("usage: noz verify (or pipe the event on stdin)\n"); + return; + } + + try nostr.init(); + defer nostr.cleanup(); + + var event = nostr.Event.parse(json) catch fail(out, "invalid: malformed event"); + defer event.deinit(); + + event.validate() catch |e| fail(out, switch (e) { + error.IdMismatch => "invalid: id does not match content", + error.SigMismatch => "invalid: signature verification failed", + error.FutureEvent => "invalid: created_at is too far in the future", + else => "invalid: event failed validation", + }); + + try out.writeAll("valid\n"); +} + +fn fail(out: *Io.Writer, msg: []const u8) noreturn { + out.print("{s}\n", .{msg}) catch {}; + out.flush() catch {}; + std.process.exit(1); +} + fn cmdEvent(env_sec: ?[]const u8, arena: Allocator, out: *Io.Writer, args: []const [:0]const u8) !void { var sec: ?[]const u8 = null; var content: []const u8 = ""; From b54a1cdc79f3b8bef7086c7792d1484ff0d4825c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kyle=20=F0=9F=90=86?= Date: Fri, 19 Jun 2026 12:34:46 -0400 Subject: [PATCH 2/2] Document decode and verify commands in README --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6db6284..44d9fd8 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,8 @@ [libnostr-z](https://github.com/privkeyio/libnostr-z). A focused tool for the things you actually script: signing and publishing -events, querying relays, counting, reading relay info, and syncing. +events, querying relays, counting, reading relay info, syncing, decoding +NIP-19 entities, and verifying events. ## Build @@ -26,6 +27,8 @@ noz req ... subscribe and print matching events (to EOSE) noz count ... count matching events (NIP-45) noz relay print the NIP-11 relay info doc noz sync ... NIP-77 reconcile src's events into dst +noz decode decode a NIP-19 entity (npub/nsec/note/nevent/naddr/nprofile) +noz verify [event-json] verify an event's id and signature (reads stdin if no arg) ``` ## License