aboutsummaryrefslogtreecommitdiffstats
path: root/tools/negentropy-test.js
diff options
context:
space:
mode:
Diffstat (limited to 'tools/negentropy-test.js')
-rw-r--r--tools/negentropy-test.js64
1 files changed, 64 insertions, 0 deletions
diff --git a/tools/negentropy-test.js b/tools/negentropy-test.js
new file mode 100644
index 0000000..28a1f30
--- /dev/null
+++ b/tools/negentropy-test.js
@@ -0,0 +1,64 @@
+// The negentropy client (NIP-77, src/negentropy.js) on the wire as strfry
+// and the reference implementation speak it: the opening message for a
+// fixed set is byte-identical to the reference's (checked against
+// hoytech/negentropy's JS on 2026-10-16: seven pairings of ours and theirs
+// as client and relay agreed on every have/need), and a reconciliation
+// against our own relay side finds exactly what each side lacks — in one
+// round for a small difference, in several when the relay's frame is
+// small — and ends with nothing to say once the sets match.
+// Run: bun tools/negentropy-test.js
+import assert from 'node:assert/strict';
+import { createHash } from 'node:crypto';
+import { NegSet, Negentropy } from '../src/negentropy.js';
+const id = (n) => createHash('sha256').update('item-' + n).digest('hex');
+const sha = (s) => createHash('sha256').update(s).digest('hex');
+
+// the fixed sets the reference was run on
+const A = Array.from({ length: 120 }, (_, i) => [1000 + Math.floor(i / 3), id(i)]);
+const B = [...A.filter((_, i) => i % 17 !== 0), ...Array.from({ length: 9 }, (_, i) => [1000 + i * 4, id(1000 + i)])];
+const first = await new Negentropy(new NegSet(A)).initiate();
+assert.equal(first.length, 634, 'the opening message for the fixed set is 317 bytes');
+assert.equal(first.slice(0, 2), '61', 'protocol version 1');
+assert.equal(sha(first), 'c926c013652f6e2f727abff15c0838aebd7f613fb4749ee691952dfc8be8ff81', 'the opening message is the reference implementation\'s, byte for byte');
+
+async function talk(client, relay) {
+ let msg = await client.initiate(), rounds = 0;
+ const have = [], need = [];
+ while (msg) {
+ rounds++;
+ const r = await relay.reconcile(msg);
+ assert(r.next, 'the relay side always answers');
+ const c = await client.reconcile(r.next);
+ have.push(...c.have); need.push(...c.need); msg = c.next;
+ }
+ return { rounds, have: have.sort(), need: need.sort() };
+}
+{
+ const r = await talk(new Negentropy(new NegSet(A)), new Negentropy(new NegSet(B)));
+ assert.equal(r.rounds, 1, 'a small difference reconciles in one round');
+ assert.deepEqual(r.have, A.filter((_, i) => i % 17 === 0).map((x) => x[1]).sort(), 'have: the ids the relay lacks');
+ assert.deepEqual(r.need, Array.from({ length: 9 }, (_, i) => id(1000 + i)).sort(), 'need: the ids we lack');
+}
+{
+ const r = await talk(new Negentropy(new NegSet(A)), new Negentropy(new NegSet(A)));
+ assert.equal(r.rounds, 1); assert.deepEqual(r.have, []); assert.deepEqual(r.need, []);
+ const r2 = await talk(new Negentropy(new NegSet([])), new Negentropy(new NegSet(B)));
+ assert.equal(r2.need.length, B.length, 'holding nothing, we need everything');
+ const r3 = await talk(new Negentropy(new NegSet(B)), new Negentropy(new NegSet([])));
+ assert.equal(r3.have.length, B.length, 'the relay holding nothing lacks everything');
+}
+{
+ // a big set and a relay with a small frame: many rounds, the same answer
+ const big = Array.from({ length: 3000 }, (_, i) => [1000 + i, id(i)]);
+ const theirs = [...big.filter((_, i) => i % 101 !== 0), ...Array.from({ length: 25 }, (_, i) => [1000 + i * 7, id(5000 + i)])];
+ const r = await talk(new Negentropy(new NegSet(big)), new Negentropy(new NegSet(theirs), { frameSizeLimit: 4096 }));
+ assert(r.rounds > 1, 'a small frame takes several rounds: ' + r.rounds);
+ assert.equal(r.have.length, 30); assert.equal(r.need.length, 25);
+}
+{
+ // the set drops duplicates and bad ids, and sorts by time then id
+ const s = new NegSet([[5, id(1)], [5, id(1)], [3, 'nope'], [4, id(2)], [5, id(0)]]);
+ assert.equal(s.size, 3);
+ assert.deepEqual(s.items.map((x) => x.ts), [4, 5, 5]);
+}
+console.log('✓ negentropy: the reference\'s bytes, one round for a small difference, several for a small frame, nothing to say when equal');