import { describe, expect, test } from 'bun:test'; import { AddressIndex, compareAddresses, computeIndexRoot, emptyRootHash, verifyAbsenceProof, verifyInclusionProof, } from '../src/index.js'; describe('AddressIndex', () => { test('empty index has emptyRootHash root', () => { const idx = new AddressIndex(); expect(idx.rootHash()).toEqual(emptyRootHash()); }); test('upsert keeps entries lexicographically sorted', () => { const idx = new AddressIndex(); idx.upsert({ address: 'charlie', latestLeafIndex: 1, bundleHash: new Uint8Array(32).fill(3), deleted: false, }); idx.upsert({ address: 'alice', latestLeafIndex: 0, bundleHash: new Uint8Array(32).fill(1), deleted: false, }); idx.upsert({ address: 'bob', latestLeafIndex: 2, bundleHash: new Uint8Array(32).fill(2), deleted: false, }); const snap = idx.snapshot(); expect(snap.map((e) => e.address)).toEqual(['alice', 'bob', 'charlie']); }); test('compareAddresses is byte-lex', () => { expect(compareAddresses('alice', 'bob') < 0).toBe(true); expect(compareAddresses('bob', 'alice') > 0).toBe(true); expect(compareAddresses('alice', 'alice')).toBe(0); expect(compareAddresses('alice', 'aliceb')).toBeLessThan(0); }); test('inclusion proof verifies against rootHash', () => { const idx = new AddressIndex(); for (const a of ['alice', 'bob', 'charlie', 'dave', 'eve']) { idx.upsert({ address: a, latestLeafIndex: 0, bundleHash: new Uint8Array(32).fill(a.charCodeAt(0)), deleted: false, }); } const proof = idx.inclusionProof('charlie'); expect(proof).not.toBeNull(); expect(verifyInclusionProof(proof!, idx.rootHash())).toBe(true); }); test('inclusion proof fails against tampered root', () => { const idx = new AddressIndex(); idx.upsert({ address: 'alice', latestLeafIndex: 0, bundleHash: new Uint8Array(32).fill(1), deleted: false, }); const proof = idx.inclusionProof('alice')!; const tampered = new Uint8Array(idx.rootHash()); tampered[0] ^= 0xff; expect(verifyInclusionProof(proof, tampered)).toBe(false); }); test('absence proof: query between two entries', () => { const idx = new AddressIndex(); idx.upsert({ address: 'alice', latestLeafIndex: 0, bundleHash: new Uint8Array(32).fill(1), deleted: false, }); idx.upsert({ address: 'charlie', latestLeafIndex: 1, bundleHash: new Uint8Array(32).fill(3), deleted: false, }); const absence = idx.absenceProof('bob'); expect(absence).not.toBeNull(); expect(verifyAbsenceProof(absence!, idx.rootHash())).toBe(true); }); test('absence proof: query before first entry', () => { const idx = new AddressIndex(); idx.upsert({ address: 'm', latestLeafIndex: 0, bundleHash: new Uint8Array(32).fill(1), deleted: false, }); idx.upsert({ address: 'z', latestLeafIndex: 1, bundleHash: new Uint8Array(32).fill(2), deleted: false, }); const absence = idx.absenceProof('a'); expect(absence!.prev).toBeNull(); expect(absence!.next).not.toBeNull(); expect(verifyAbsenceProof(absence!, idx.rootHash())).toBe(true); }); test('absence proof: query after last entry', () => { const idx = new AddressIndex(); idx.upsert({ address: 'a', latestLeafIndex: 0, bundleHash: new Uint8Array(32).fill(1), deleted: false, }); idx.upsert({ address: 'm', latestLeafIndex: 1, bundleHash: new Uint8Array(32).fill(2), deleted: false, }); const absence = idx.absenceProof('z'); expect(absence!.prev).not.toBeNull(); expect(absence!.next).toBeNull(); expect(verifyAbsenceProof(absence!, idx.rootHash())).toBe(true); }); test('absence proof: empty tree', () => { const idx = new AddressIndex(); const absence = idx.absenceProof('alice'); expect(absence).not.toBeNull(); expect(absence!.treeSize).toBe(0); expect(verifyAbsenceProof(absence!, idx.rootHash())).toBe(true); }); test('absence proof returns null for existing address', () => { const idx = new AddressIndex(); idx.upsert({ address: 'alice', latestLeafIndex: 0, bundleHash: new Uint8Array(32).fill(1), deleted: false, }); expect(idx.absenceProof('alice')).toBeNull(); }); test('absence proof can be forged-detected: claim adjacent but not adjacent', () => { const idx = new AddressIndex(); idx.upsert({ address: 'alice', latestLeafIndex: 0, bundleHash: new Uint8Array(32).fill(1), deleted: false, }); idx.upsert({ address: 'bob', latestLeafIndex: 1, bundleHash: new Uint8Array(32).fill(2), deleted: false, }); idx.upsert({ address: 'charlie', latestLeafIndex: 2, bundleHash: new Uint8Array(32).fill(3), deleted: false, }); const absence = idx.absenceProof('aaron')!; // Tamper: replace prev with non-adjacent neighbor (charlie) const charlieProof = idx.inclusionProof('charlie')!; const forged = { ...absence, prev: { position: charlieProof.position, entry: charlieProof.entry, auditPath: charlieProof.auditPath, }, }; expect(verifyAbsenceProof(forged, idx.rootHash())).toBe(false); }); test('tombstone marks entry deleted', () => { const idx = new AddressIndex(); idx.upsert({ address: 'alice', latestLeafIndex: 0, bundleHash: new Uint8Array(32).fill(1), deleted: false, }); idx.tombstone('alice', 5); const e = idx.get('alice')!; expect(e.deleted).toBe(true); expect(e.latestLeafIndex).toBe(5); expect(e.bundleHash.length).toBe(0); }); test('computeIndexRoot equals AddressIndex.rootHash for the same sorted snapshot', () => { const idx = new AddressIndex(); for (const a of ['carol', 'alice', 'bob']) { idx.upsert({ address: a, latestLeafIndex: 0, bundleHash: new Uint8Array(32).fill(a.charCodeAt(0)), deleted: false, }); } expect(idx.rootHash()).toEqual(computeIndexRoot(idx.snapshot())); }); });