12<html lang="en">3<head>4<meta charset="utf-8">5<meta name="viewport" content="width=device-width, initial-scale=1">6<title>NIP-77 - Negentropy Syncing — Nostr Instruction Manual</title>7<meta name="description" content="NIP-77: Negentropy syncing. NEG-OPEN, NEG-MSG, NEG-ERR, NEG-CLOSE messages and the full Negentropy V1 wire format: varints, bounds, ID lists, fingerprints.">8<link rel="preload" href="../assets/fonts/michroma-400-latin.woff2" as="font" type="font/woff2" crossorigin>9<link rel="preload" href="../assets/fonts/exo2-var-latin.woff2" as="font" type="font/woff2" crossorigin>10<link rel="preload" href="../assets/fonts/sairacondensed-600-latin.woff2" as="font" type="font/woff2" crossorigin>11<link rel="preload" href="../assets/fonts/sairacondensed-700-latin.woff2" as="font" type="font/woff2" crossorigin>12<link rel="preload" href="../assets/fonts/sairacondensed-500-latin.woff2" as="font" type="font/woff2" crossorigin>13<link rel="preload" href="../assets/fonts/sharetechmono-400-latin.woff2" as="font" type="font/woff2" crossorigin>14<link rel="preload" href="../assets/fonts/exo2-400i-latin.woff2" as="font" type="font/woff2" crossorigin>15<link rel="icon" href="../assets/favicon.svg" type="image/svg+xml">16<link rel="stylesheet" href="../assets/manual.css">17</head>18<body>1920<svg width="0" height="0" style="position:absolute">21 <defs>22 <marker id="arr" markerWidth="9" markerHeight="9" refX="7" refY="4.5" orient="auto">23 <path d="M1,1 L8,4.5 L1,8 Z" fill="#f26a00"/>24 </marker>25 <marker id="arrb" markerWidth="9" markerHeight="9" refX="7" refY="4.5" orient="auto">26 <path d="M1,1 L8,4.5 L1,8 Z" fill="#2e5f9e"/>27 </marker>28 </defs>29</svg>3031<div class="page">32 <span class="staple s1"></span><span class="staple s2"></span>3334 <header class="band">35 <a class="logo" href="../index.html">NOSTR<b>▸</b>PROTOCOL MANUAL</a>36 <span class="booklet-note">Chapter 77 - Relay Ops</span>37 </header>38 <nav class="tabs">39 <a href="../index.html">Contents</a>40 <a href="#overview" class="on">Overview</a>41 <a href="#spec">Spec</a>42 <a href="#examples">Examples</a>43 <a href="#history">History</a>44 </nav>4546 <div class="chapter">47 <div class="nipno" data-n="77">77</div>48 <div class="titles">49 <div class="kicker">Nostr Implementation Possibility - Hex № 77</div>50 <h1>Negentropy Syncing</h1>51 <p class="sub">Compare two event sets over the wire without sending the events. Learn only what is missing on each side, then transfer just that.</p>52 </div>53 </div>5455 <main class="spread">56 <div class="main">5758 <h2 class="bar" id="overview"><span class="num">1</span> Overview</h2>59 <p>Two parties often hold overlapping sets of events. A client re-opens after a week offline. A relay mirrors another relay. Fetching everything again with <code>REQ</code> wastes bandwidth. Even fetching all IDs wastes bandwidth when most IDs match.</p>60 <p>This NIP defines a sync extension for that case. It works for client-to-relay sync and for relay-to-relay sync. When the two sides share events, it uses less bandwidth than transferring the full event set, or even just the full ID list.</p>61 <p>The extension is a Nostr-friendly wrapper around the <a href="https://github.com/hoytech/negentropy">Negentropy</a> protocol. Negentropy applies a technique called <a href="https://logperiodic.com/rbsr.html">Range-Based Set Reconciliation</a>: split the ID space into ranges, compare a short <strong>fingerprint</strong> per range, and recurse only into ranges that differ. Negentropy is a binary protocol, so this wrapper hex-encodes its messages inside normal Nostr JSON arrays. The full Negentropy Protocol V1 specification is part of this NIP (§4 to §6 below).</p>62 <p>The sync itself moves no events. It only tells the client which IDs it has that the other side needs, and which IDs it needs that the other side has. The client then uploads with <code>EVENT</code> and downloads with <code>REQ</code>, as defined in <a href="01.html">NIP-01</a>.</p>6364 <div class="box hint">65 <span class="tag">Hint!</span>66 <p>You do not have to transfer anything. A client that only wants the number of unique events, for example a reaction count, can run the sync, count the IDs, and skip the upload and download entirely.</p>67 </div>6869 <h2 class="bar" id="spec"><span class="num">2</span> Sync Messages</h2>70 <p>We call the two sides the <strong>client</strong> (the initiator) and the <strong>relay</strong>, even when the initiator is another relay. The wrapper adds four message types on top of the <a href="01.html">NIP-01</a> WebSocket connection:</p>7172 <div class="tscroll"><table class="ref">73 <caption>NIP-77 message types</caption>74 <tr><th>Message</th><th>Direction</th><th>Shape</th><th>Purpose</th></tr>75 <tr><td class="btn">NEG-OPEN</td><td>client → relay</td><td><code>["NEG-OPEN", <sub_id>, <filter>, <initial msg, hex>]</code></td><td>Start a sync for one filter.</td></tr>76 <tr><td class="btn">NEG-MSG</td><td>both</td><td><code>["NEG-MSG", <sub_id>, <msg, hex>]</code></td><td>Carry one Negentropy round. Same format in both directions.</td></tr>77 <tr><td class="btn">NEG-ERR</td><td>relay → client</td><td><code>["NEG-ERR", <sub_id>, <reason>]</code></td><td>Refuse or abort a sync. Closes the subscription.</td></tr>78 <tr><td class="btn">NEG-CLOSE</td><td>client → relay</td><td><code>["NEG-CLOSE", <sub_id>]</code></td><td>End a sync and release relay resources.</td></tr>79 </table></div>8081 <h3>NEG-OPEN</h3>82 <div class="crt">83 <div class="titlebar">Initial message - client to relay</div>84<pre>[85 <span class="s">"NEG-OPEN"</span>,86 <span class="c"><subscription ID string></span>,87 <span class="c"><filter></span>,88 <span class="c"><initialMessage, hex-encoded></span>89]</pre>90 </div>91 <ul>92 <li>The subscription ID identifies which sync a later message refers to. It only needs to be unique among the concurrent sync subscriptions on this WebSocket connection. An integer that increments once per <code>NEG-OPEN</code> is fine.</li>93 <li>Sync subscription IDs live in a separate namespace from <code>REQ</code> subscription IDs. The two never collide.</li>94 <li>A <code>NEG-OPEN</code> that reuses a currently open sync subscription ID first closes the existing subscription.</li>95 <li>The filter is a normal <a href="01.html">NIP-01</a> filter. It defines the event set on both sides.</li>96 <li><code>initialMessage</code> is the client's first Negentropy binary message, hex-encoded. See §4.</li>97 </ul>9899 <h3>NEG-ERR</h3>100 <p>If the relay cannot service a request, it returns an error. After a <code>NEG-ERR</code>, the subscription is closed.</p>101 <div class="crt">102 <div class="titlebar">Error message - relay to client</div>103<pre>[104 <span class="s">"NEG-ERR"</span>,105 <span class="c"><subscription ID string></span>,106 <span class="c"><reason code string></span>107]</pre>108 </div>109 <p>Error reasons use the <a href="01.html">NIP-01</a> format: a machine-readable single-word prefix, then <code>:</code>, then a human-readable message. The suggested reasons are:</p>110 <div class="tscroll"><table class="ref">111 <caption>Suggested error reasons</caption>112 <tr><th>Prefix</th><th>Meaning</th><th>Example</th></tr>113 <tr><td class="btn">blocked</td><td>The relay rejects the query. It may refuse queries that touch too many records, or records that are too old. The relay may return its maximum record count as the 4th element of the response.</td><td><code>blocked: this query is too big</code></td></tr>114 <tr><td class="btn">closed</td><td>Sync queries can be stateful. The relay timed out an inactive query to recover memory.</td><td><code>closed: you took too long to respond!</code></td></tr>115 </table></div>116117 <div class="box tech">118 <span class="tag">Tech Note</span>119 <p>The <code>blocked</code> reason says the maximum record count "can optionally be returned as the 4th element in the response". The <code>NEG-ERR</code> array is defined with only three elements, and the spec does not give the 4th element a type or shape. Treat any 4th element as an implementation-defined hint.</p>120 </div>121122 <h3>NEG-MSG</h3>123 <p>After the open, the relay and the client alternate. Each side parses the message it received, builds a reply, and sends it back:</p>124 <div class="crt">125 <div class="titlebar">Subsequent messages - bidirectional</div>126<pre>[127 <span class="s">"NEG-MSG"</span>,128 <span class="c"><subscription ID string></span>,129 <span class="c"><message, hex-encoded></span>130]</pre>131 </div>132 <p><code>message</code> is one Negentropy binary message, hex-encoded. Both directions use the same binary format (§4).</p>133134 <h3>NEG-CLOSE</h3>135 <p>When the client is done, it tells the relay to release its resources:</p>136 <div class="crt">137 <div class="titlebar">Close message - client to relay</div>138<pre>[139 <span class="s">"NEG-CLOSE"</span>,140 <span class="c"><subscription ID string></span>141]</pre>142 </div>143 <p>Disconnecting the WebSocket has the same effect.</p>144145 <h2 class="bar"><span class="num">3</span> The Sync Loop</h2>146 <p>The high-level procedure has five steps:</p>147 <ol>148 <li>The client chooses a filter. It selects its local events that match the filter, or uses a cache. It builds the initial Negentropy message from that set.</li>149 <li>The client sends <code>NEG-OPEN</code> with the filter and the initial message.</li>150 <li>The relay selects its local events that match the filter, or uses a cache.</li>151 <li>The relay builds a response and returns it in a <code>NEG-MSG</code>.</li>152 <li>The client parses the message. It learns IDs it has that the relay needs, and IDs it needs that the relay has. To continue, it sends a new <code>NEG-MSG</code> and waits for step 4 again. To stop, it sends <code>NEG-CLOSE</code> or disconnects.</li>153 </ol>154 <p>The loop only exchanges IDs. To move the events, the client uploads with <code>EVENT</code> and downloads with <code>REQ</code>. It may do this on the same WebSocket connection, in parallel with further <code>NEG-MSG</code> rounds.</p>155156 <figure class="diagram">157 <div class="frame">158 <svg viewBox="0 0 560 330" role="img" aria-label="Sequence diagram: client sends NEG-OPEN, sides alternate NEG-MSG rounds, client transfers events with EVENT and REQ, then sends NEG-CLOSE">159 <line class="d-wire" x1="110" y1="40" x2="110" y2="315" stroke-dasharray="3 4"/>160 <line class="d-wire" x1="450" y1="40" x2="450" y2="315" stroke-dasharray="3 4"/>161 <rect class="d-actor" x="55" y="8" width="110" height="30" rx="6"/>162 <text class="d-actor-label" x="110" y="28" text-anchor="middle">Client</text>163 <rect class="d-actor" x="395" y="8" width="110" height="30" rx="6"/>164 <text class="d-actor-label" x="450" y="28" text-anchor="middle">Relay</text>165 <path class="d-msg" d="M110,62 L445,62"/>166 <text class="d-label" x="280" y="55" text-anchor="middle">["NEG-OPEN", "N1", {filter}, "61…"]</text>167 <path class="d-msg blue" d="M450,95 L115,95"/>168 <text class="d-label" x="280" y="88" text-anchor="middle">["NEG-MSG", "N1", "61…"]</text>169 <path class="d-msg d-flow" d="M110,128 L445,128"/>170 <text class="d-label" x="280" y="121" text-anchor="middle">["NEG-MSG", "N1", "61…"] repeat until done</text>171 <path class="d-msg blue d-flow" d="M450,161 L115,161"/>172 <text class="d-label" x="280" y="154" text-anchor="middle">["NEG-MSG", "N1", "61…"]</text>173 <path class="d-msg" d="M110,205 L445,205"/>174 <text class="d-label" x="280" y="198" text-anchor="middle">["EVENT", {…}] upload IDs the relay needs</text>175 <path class="d-msg" d="M110,238 L445,238"/>176 <text class="d-label" x="280" y="231" text-anchor="middle">["REQ", "dl", {"ids":[…]}] download IDs the client needs</text>177 <path class="d-msg" d="M110,278 L445,278"/>178 <text class="d-label" x="280" y="271" text-anchor="middle">["NEG-CLOSE", "N1"]</text>179 <text class="d-note" x="280" y="305" text-anchor="middle">Transfers may run in parallel with later NEG-MSG rounds.</text>180 <circle r="4" fill="#f26a00">181 <animateMotion dur="2.6s" repeatCount="indefinite" path="M110,128 L445,128"/>182 </circle>183 <circle r="4" fill="#2e5f9e">184 <animateMotion dur="2.6s" begin="1.3s" repeatCount="indefinite" path="M450,161 L115,161"/>185 </circle>186 </svg>187 </div>188 <figcaption>Fig. 77-1 — <b>The sync loop.</b> One <code>NEG-OPEN</code>, alternating <code>NEG-MSG</code> rounds, then plain <code>EVENT</code> and <code>REQ</code> transfers.</figcaption>189 </figure>190191 <h2 class="bar"><span class="num">4</span> Negentropy Wire Format</h2>192 <p>This section and the next two restate the Negentropy Protocol V1 appendix. The appendix calls the two sides <strong>client</strong> and <strong>server</strong>; in Nostr terms the server is the relay. The client sends the first message. The server replies. The client keeps querying until it is satisfied, then terminates. Messages in both directions have the same format.</p>193194 <h3>Preparation: records and ordering</h3>195 <p>Each side has a collection of <strong>records</strong>. A record is a 64-bit numeric timestamp plus a 256-bit ID. For Nostr events, these are <code>created_at</code> and the event <code>id</code>. Before syncing, each side sorts its records by timestamp, ascending. When two timestamps are equal, sort lexically by ID, ascending by first differing byte.</p>196 <div class="box warn">197 <span class="tag">Warning!</span>198 <p>A record may not use the maximum uint64 value (<code>2**64 - 1</code>) as a timestamp. That value is reserved as the special "infinity" timestamp inside the protocol.</p>199 </div>200 <p>The goal: the client learns the set of IDs it has that the server does not, and the set of IDs the server has that it does not.</p>201202 <h3>Varint</h3>203 <p>A <strong>varint</strong> is a variable-sized unsigned integer. It is written as base-128 digits, most significant digit first, with as few digits as possible. Bit eight (the high bit) is set on every byte except the last:</p>204 <div class="crt">205 <div class="titlebar">Grammar - varint</div>206<pre>Varint := <span class="c"><Digit+128></span>* <span class="c"><Digit></span></pre>207 </div>208209 <h3>Id</h3>210 <p>An ID is a fixed byte-string of length 32:</p>211 <div class="crt">212 <div class="titlebar">Grammar - id</div>213<pre>Id := Byte{32}</pre>214 </div>215216 <h3>Message</h3>217 <p>A reconciliation message is one protocol version byte, then an ordered list of ranges:</p>218 <div class="crt">219 <div class="titlebar">Grammar - message</div>220<pre>Message := <span class="c"><protocolVersion (Byte)></span> <span class="c"><Range></span>*</pre>221 </div>222 <ul>223 <li>The current protocol version is 1, encoded as the byte <code>0x61</code>. Version 2 will be <code>0x62</code>, and so forth.</li>224 <li>If a server receives a version it cannot handle, it replies with a single byte: the highest version it supports. The client can then downgrade and retry its message.</li>225 <li>Each range covers a contiguous section of the timestamp/ID space. The first range starts at timestamp 0 and an all-zero ID.</li>226 <li>Ranges are always adjacent. There are no gaps.</li>227 <li>If the last range does not end at infinity, an implicit <code>Skip</code>-to-infinity range is appended. The range list therefore always covers the full timestamp/ID space.</li>228 </ul>229230 <h3>Range</h3>231 <p>A range is an upper bound, a mode, and a payload. The mode selects the payload type:</p>232 <div class="crt">233 <div class="titlebar">Grammar - range and payloads</div>234<pre>Range := <span class="c"><upperBound (Bound)></span> <span class="c"><mode (Varint)></span> <span class="c"><payload (Skip | Fingerprint | IdList)></span>235236Skip := <span class="c">(mode 0 - empty)</span>237Fingerprint := Byte{16} <span class="c">(mode 1)</span>238IdList := <span class="c"><length (Varint)></span> <span class="c"><ids (Id)></span>* <span class="c">(mode 2)</span></pre>239 </div>240 <div class="tscroll"><table class="ref">241 <caption>Range modes</caption>242 <tr><th>Mode</th><th>Payload</th><th>Meaning</th></tr>243 <tr><td class="btn">0</td><td><code>Skip</code> — empty</td><td>The sender does not wish to process this range further.</td></tr>244 <tr><td class="btn">1</td><td><code>Fingerprint</code> — 16 bytes</td><td>A digest (§6) of every ID the sender has inside the range.</td></tr>245 <tr><td class="btn">2</td><td><code>IdList</code> — varint count, then IDs</td><td>The full list of IDs the sender has inside the range.</td></tr>246 </table></div>247248 <p>The message grammar fixes the format, not the strategy. A typical implementation compares fingerprints per range. Equal fingerprints mean the range matches; answer it with <code>Skip</code>. Different fingerprints mean a difference hides inside; split the range into sub-ranges and recurse. Once a range is small, send an <code>IdList</code> so the other side can diff it directly and reply with its own <code>IdList</code> of that range. The IDs each side sees in received lists, minus the IDs it already has, are the IDs it needs; and the reverse gives the IDs the other side needs.</p>249250 <figure class="diagram">251 <div class="frame">252 <svg viewBox="0 0 560 210" role="img" aria-label="Range-based set reconciliation: one fingerprint range over the full timestamp space is split into smaller ranges; matching ranges are skipped, differing ranges become ID lists">253 <text class="d-actor-label" x="10" y="18">Round 1 — one range, fingerprints differ</text>254 <rect x="10" y="28" width="540" height="26" rx="4" fill="#f26a00"/>255 <text class="d-label" x="280" y="45" text-anchor="middle" fill="#fff">[ts 0 … infinity) mode 1 FINGERPRINT — mismatch!</text>256 <path class="d-msg" d="M140,62 L100,92"/>257 <path class="d-msg" d="M280,62 L280,92"/>258 <path class="d-msg" d="M420,62 L460,92"/>259 <text class="d-actor-label" x="10" y="88">Round 2 — split into sub-ranges</text>260 <rect x="10" y="98" width="170" height="26" rx="4" fill="#7d8794"/>261 <text class="d-label" x="95" y="115" text-anchor="middle" fill="#fff">FP equal → SKIP</text>262 <rect x="186" y="98" width="180" height="26" rx="4" fill="#f26a00"/>263 <text class="d-label" x="276" y="115" text-anchor="middle" fill="#fff">FP differs → recurse</text>264 <rect x="372" y="98" width="178" height="26" rx="4" fill="#7d8794"/>265 <text class="d-label" x="461" y="115" text-anchor="middle" fill="#fff">FP equal → SKIP</text>266 <path class="d-msg" d="M240,132 L220,160"/>267 <path class="d-msg" d="M320,132 L340,160"/>268 <text class="d-actor-label" x="10" y="158">Round 3 — small ranges become ID lists</text>269 <rect x="150" y="166" width="125" height="26" rx="4" fill="#2c7a3f"/>270 <text class="d-label" x="212" y="183" text-anchor="middle" fill="#fff">mode 2 IDLIST</text>271 <rect x="285" y="166" width="125" height="26" rx="4" fill="#2c7a3f"/>272 <text class="d-label" x="347" y="183" text-anchor="middle" fill="#fff">mode 2 IDLIST</text>273 <text class="d-note" x="420" y="183">→ diff directly</text>274 </svg>275 </div>276 <figcaption>Fig. 77-2 — <b>Range-based set reconciliation.</b> Fingerprints prune matching ranges. Only differing ranges shrink down to explicit ID lists.</figcaption>277 </figure>278279 <h2 class="bar"><span class="num">5</span> Bounds and Timestamp Encoding</h2>280 <p>Every range has an <em>inclusive</em> lower bound and an <em>exclusive</em> upper bound. Only the upper bound is encoded. The lower bound of a range is the upper bound of the previous range, or timestamp 0 with an all-zero ID for the first range.</p>281 <p>A bound is an encoded timestamp plus a variable-length ID prefix. The prefix disambiguates records that share a timestamp:</p>282 <div class="crt">283 <div class="titlebar">Grammar - bound</div>284<pre>Bound := <span class="c"><encodedTimestamp (Varint)></span> <span class="c"><length (Varint)></span> <span class="c"><idPrefix (Byte)></span>*</pre>285 </div>286 <p>The timestamp is delta-encoded:</p>287 <ul>288 <li>The infinity timestamp is encoded as <code>0</code>.</li>289 <li>Every other timestamp is encoded as <code>1 + offset</code>. The offset is the difference between this timestamp and the previously encoded timestamp in the same message.</li>290 <li>The initial offset base is <code>0</code>. It resets at the beginning of each message.</li>291 <li>Offsets are never negative. Ranges in a message are encoded in ascending order, they never overlap, and an upper bound's timestamp is greater than or equal to its lower bound's timestamp.</li>292 </ul>293 <p>The <code>idPrefix</code> rules:</p>294 <ul>295 <li>Its size is given by <code>length</code>: between 0 and 32 bytes, inclusive.</li>296 <li>Use the shortest prefix that separates the first record of this range from the last record of the previous range.</li>297 <li>If those two records have different timestamps, use length 0. Otherwise use the byte-length of their common ID prefix, plus 1.</li>298 <li>When the prefix is shorter than 32 bytes, the omitted trailing bytes are implicitly zero.</li>299 </ul>300301 <h2 class="bar"><span class="num">6</span> Fingerprint Algorithm</h2>302 <p>The 16-byte fingerprint of a range is computed in four steps:</p>303 <ol>304 <li>Add the element IDs mod 2<sup>256</sup>. Interpret each ID as a 32-byte little-endian unsigned integer.</li>305 <li>Concatenate the sum with the number of elements in the range, encoded as a varint (§4).</li>306 <li>Hash the result with SHA-256.</li>307 <li>Take the first 16 bytes.</li>308 </ol>309310 <figure class="diagram">311 <div class="frame">312 <svg viewBox="0 0 560 120" role="img" aria-label="Fingerprint pipeline: IDs are summed mod 2 to the 256, concatenated with the element count varint, hashed with SHA-256, and truncated to 16 bytes">313 <rect class="d-actor" x="10" y="35" width="120" height="50" rx="8"/>314 <text class="d-actor-label" x="70" y="57" text-anchor="middle">Sum IDs</text>315 <text class="d-note" x="70" y="74" text-anchor="middle">mod 2^256, LE</text>316 <rect class="d-actor" x="180" y="35" width="120" height="50" rx="8"/>317 <text class="d-actor-label" x="240" y="57" text-anchor="middle">Append</text>318 <text class="d-note" x="240" y="74" text-anchor="middle">count as varint</text>319 <rect class="d-actor" x="350" y="35" width="90" height="50" rx="8"/>320 <text class="d-actor-label" x="395" y="57" text-anchor="middle">SHA-256</text>321 <rect class="d-actor" x="480" y="35" width="70" height="50" rx="8"/>322 <text class="d-actor-label" x="515" y="57" text-anchor="middle">Trim</text>323 <text class="d-note" x="515" y="74" text-anchor="middle">first 16 B</text>324 <path class="d-msg d-flow" d="M132,60 L177,60"/>325 <path class="d-msg d-flow" d="M302,60 L347,60"/>326 <path class="d-msg d-flow" d="M442,60 L477,60"/>327 </svg>328 </div>329 <figcaption>Fig. 77-3 — <b>Fingerprint pipeline.</b> Sum, append count, hash, truncate. Equal sets give equal fingerprints.</figcaption>330 </figure>331332 <div class="box tech">333 <span class="tag">Tech Note</span>334 <p>The version-downgrade rule says an incompatible server replies "with a single byte". The wrapper never sends raw bytes, so read this as a <code>NEG-MSG</code> whose hex payload decodes to that one byte. The NIP does not state this mapping explicitly.</p>335 </div>336337 <h2 class="bar" id="examples"><span class="num">7</span> Worked Example</h2>338 <p>First, the bytes. The client opens with the laziest possible message: one range from zero to infinity, answered with a fingerprint of everything it has. The relay's fingerprint differs, so it splits. Its reply names its two newest event IDs explicitly and fingerprints the rest:</p>339 <div class="crt">340 <div class="titlebar">Decoded Negentropy message - relay reply, 92 bytes</div>341<pre><span class="k">61</span> <span class="c">-- protocol version 1 (0x61)</span>342<span class="c">-- Range 1: [ts 0 … ts 1749700000) --</span>343<span class="k">86 c2 a9 9b 21</span> <span class="c">-- bound timestamp: varint(1 + 1749700000), first offset base is 0</span>344<span class="k">00</span> <span class="c">-- bound idPrefix length: 0 (timestamps differ)</span>345<span class="k">01</span> <span class="c">-- mode 1: Fingerprint</span>346<span class="s">3c9d…77a2</span> <span class="c">-- 16-byte digest of all relay IDs below ts 1749700000</span>347<span class="c">-- Range 2: [ts 1749700000 … infinity) --</span>348<span class="k">00</span> <span class="c">-- bound timestamp: 0 = infinity</span>349<span class="k">00</span> <span class="c">-- bound idPrefix length: 0</span>350<span class="k">02</span> <span class="c">-- mode 2: IdList</span>351<span class="k">02</span> <span class="c">-- 2 IDs follow</span>352<span class="s">b1a649eb…</span> <span class="c">-- Id (32 bytes)</span>353<span class="s">d2b699fa…</span> <span class="c">-- Id (32 bytes)</span></pre>354 </div>355 <p>Now the full session. The client is missing <code>d2b699fa…</code> and holds one event the relay lacks. Orange lines go to the relay. Blue lines come from the relay:</p>356 <div class="crt">357 <div class="titlebar">Session transcript - wss://relay.example.com</div>358<pre><span class="c">-- open: filter + initial message (one fingerprint range over everything) --</span>359<span class="send">→ ["NEG-OPEN", "N1", {"kinds":[1], "authors":["22a12a12…"]}, "610000015fe2…9b31"]</span>360<span class="recv">← ["NEG-MSG", "N1", "6186c2a99b2100013c9d…77a200000202b1a649eb…d2b699fa…"]</span>361362<span class="c">-- client answers the fingerprint range, replies with its own IdList for range 2 --</span>363<span class="send">→ ["NEG-MSG", "N1", "6186c2a99b21000000000202b1a649eb…e4c02231…"]</span>364<span class="recv">← ["NEG-MSG", "N1", "61000000"]</span> <span class="c">-- one Skip range to infinity: reconciled</span>365366<span class="c">-- client learned: it needs d2b699fa…, the relay needs e4c02231… --</span>367<span class="send">→ ["EVENT", {"id":"e4c02231…", "kind":1, …}]</span>368<span class="recv">← ["OK", "e4c02231…", true, ""]</span>369<span class="send">→ ["REQ", "dl", {"ids":["d2b699fa…"]}]</span>370<span class="recv">← ["EVENT", "dl", {"id":"d2b699fa…", …}]</span>371<span class="recv">← ["EOSE", "dl"]</span>372373<span class="send">→ ["NEG-CLOSE", "N1"]</span>374375<span class="c">-- a rejected sync on another subscription looks like this --</span>376<span class="recv">← ["NEG-ERR", "N2", "blocked: this query is too big"]</span></pre>377 </div>378379 <h2 class="bar" id="history"><span class="num">8</span> Version History</h2>380 <p>NIP-77 is young and has stayed close to its original text. Notable changes, newest first:</p>381 <ul class="patchnotes">382 <li class="minor"><span class="ver">2026</span><span class="when">2026-06</span>Code-fence cleanup across the repository; message examples relabeled from <code>jsonc</code> to <code>yaml</code>.</li>383 <li><span class="ver">2025</span><span class="when">2025-12</span>Added the <code>relay</code> tag to the status line, marking this NIP as a relay-side feature.</li>384 <li class="minor"><span class="ver">2025</span><span class="when">2025-08</span>Typo fixes.</li>385 <li><span class="ver">Origin</span><span class="when">2025-05</span>fiatjaf_ merged the spec, wrapping hoytech's Negentropy protocol for Nostr and inlining the full V1 wire-format appendix.</li>386 </ul>387388 <div class="credits">389 <h3 class="cardhead">Original Proposal - Credits</h3>390 <table>391 <tr><td>Author</td><td>fiatjaf_ (spec wraps the Negentropy protocol by hoytech)</td></tr>392 <tr><td>First commit</td><td>2025-05-26 — “NIP-77: Negentropy syncing” (<a href="https://github.com/nostr-protocol/nips/pull/1494">PR #1494</a>)</td></tr>393 <tr><td>Status</td><td>draft - optional - relay — a bandwidth optimization; plain <code>REQ</code> always works without it</td></tr>394 <tr><td>Source</td><td><a href="https://github.com/nostr-protocol/nips/blob/master/77.md">77.md on GitHub</a> - <a href="https://github.com/nostr-protocol/nips/commits/master/77.md">full change history</a></td></tr>395 </table>396 </div>397398 <h2 class="bar"><span class="num">9</span> See Also</h2>399 <ul>400 <li><a href="01.html">NIP-01</a> — defines the filter object, the <code>EVENT</code> and <code>REQ</code> messages used for the actual transfer, and the error-prefix format.</li>401 <li><a href="45.html">NIP-45</a> — <code>COUNT</code>: a simpler way to count matching events when you trust the relay's number.</li>402 <li><a href="42.html">NIP-42</a> — client authentication; relays may restrict expensive sync queries to authenticated clients.</li>403 <li><a href="11.html">NIP-11</a> — the relay information document, where a relay can advertise supported NIPs.</li>404 </ul>405 </div>406407 <aside>408 <div class="cartridge">409 <div class="inner">410 <h3 class="cardhead">Spec Card</h3>411 <div class="lights"><span class="led on"></span><span class="led on"></span><span class="led on"></span></div>412 <dl>413 <dt>Status</dt><dd><span class="chip draft">draft</span><span class="chip optional">optional</span><span class="chip">relay</span></dd>414 <dt>Defines</dt><dd>NEG-OPEN - NEG-MSG - NEG-ERR - NEG-CLOSE - Negentropy V1 wire format</dd>415 <dt>Kinds</dt><dd>none — no new event kinds</dd>416 <dt>Requires</dt><dd><a href="01.html">NIP-01</a> filters, EVENT, REQ</dd>417 <dt>Transport</dt><dd>WebSocket - hex-encoded binary in JSON arrays</dd>418 <dt>Crypto</dt><dd>sha256 fingerprints (mod 2^256 ID sums)</dd>419 <dt>First commit</dt><dd>2025-05-26</dd>420 <dt>Last updated</dt><dd>2026-06-13</dd>421 </dl>422 </div>423 </div>424 </aside>425 </main>426427 <nav class="navbtns">428 <a href="75.html">◀ NIP-75: Zap Goals</a>429 <a href="78.html">NIP-78: Arbitrary custom app data ▶</a>430 </nav>431432 <footer class="pagefoot">433 <a href="../index.html">Nostr Protocol Manual</a>434 <span class="pageno">NIP 77 / F4</span>435 </footer>436</div>437438<script src="../assets/webmcp.js" defer></script>439</body>440</html>