Skip to content

Commit 2a591c1

Browse files
swiftsync: hint generation + reconstruction — end-to-end on real testnet4
hint.js: generateHints walks a range, builds the final UTXO set, and records the block-local output indices that stay unspent (index counts all outputs so a verifier aligns; OP_RETURN gets an index but is never unspent). reconstructUtxo is the assumevalid verifier — rebuilds the UTXO set from blocks + the hint bitmap alone, no input processing. Proven end-to-end on 5,000 real testnet4 blocks (113,300 outputs): generate hint → Elias-Fano hintsfile → decode → reconstruct == the real UTXO set (4,574 coins). Hintsfile is 13,897 bytes = 0.981 bits/output, matching the BIP's concise- representation claim. The full assumevalid SwiftSync loop now runs on real blocks. Remaining: the full-chain run (real hintsfile + accumulator oracle test over all of testnet4), which gates on the validate-sync finishing.
1 parent 6ec6902 commit 2a591c1

5 files changed

Lines changed: 85 additions & 2 deletions

File tree

‎packages/swiftsync/DESIGN.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,8 @@ method we use against Bitcoin Core. No external reference needed.
6363
- `validate.js` — drive the engine over blocks → add/spend ops. **Done** (verified on 5,000 real testnet4 blocks).
6464
- `hintsfile.js` — Elias-Fano hintsfile encode/decode (the interop artifact). **Done** (matches the BIP `elias_fano.json` vectors byte-for-byte).
6565
- `undo.js` — spent-coin data for the full version: CompressAmount + reconstructable script + height code + Coin record. **Done** (amount/script match the BIP vectors byte-for-byte).
66-
- (todo) hint generation — produce the per-block unspent-index sets from a validated chain (needs the finished sync).
67-
- (todo) oracle test — run validate-sync + SwiftSync over testnet4; assert equal digests.
66+
- `hint.js` — `generateHints` (per-block unspent-index sets from a chain) + `reconstructUtxo` (the assumevalid verifier). **Done** (end-to-end on 5,000 real testnet4 blocks: reconstruction == real UTXO set; hintsfile 0.98 bits/output).
67+
- (todo) full-chain run — generate the real testnet4 hintsfile + commitment (needs the finished sync) and the accumulator oracle test over the whole chain.
6868

6969
## Decisions
7070

‎packages/swiftsync/hint.js‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Hint generation + reconstruction.
2+
//
3+
// generateHints: walk a contiguous range, build the UTXO set at the end, then for
4+
// each block list the block-local output indices that remain unspent. The index
5+
// counts ALL outputs in transaction order (so a verifier walking outputs aligns);
6+
// OP_RETURN outputs get an index but are never in the UTXO set, so never in a
7+
// hint. These per-block index lists are what hintsfile.js Elias-Fano encodes.
8+
//
9+
// reconstructUtxo: the assumevalid verifier side — rebuild the UTXO set purely
10+
// from the blocks + the hint bitmap (no input processing). With the matching
11+
// accumulator check (validate.js), this is SwiftSync: fast set construction from
12+
// hints, verified against a trusted commitment.
13+
//
14+
// Engine-agnostic: caller injects txidOf(tx).
15+
16+
const NULL_TXID = '00'.repeat(32);
17+
const isOpReturn = (spk) => typeof spk === 'string' && spk.startsWith('6a');
18+
19+
export function generateHints(blocks, { txidOf }) {
20+
const utxo = new Set();
21+
for (const block of blocks) for (const tx of block.transactions) {
22+
const txid = txidOf(tx);
23+
for (let v = 0; v < tx.outputs.length; v++) if (!isOpReturn(tx.outputs[v].scriptPubKey)) utxo.add(txid + ':' + v);
24+
for (const inp of tx.inputs) if (inp.prevout.txid !== NULL_TXID) utxo.delete(inp.prevout.txid + ':' + inp.prevout.vout);
25+
}
26+
const blockHints = [];
27+
for (const block of blocks) {
28+
const idx = []; let i = 0;
29+
for (const tx of block.transactions) {
30+
const txid = txidOf(tx);
31+
for (let v = 0; v < tx.outputs.length; v++) { if (utxo.has(txid + ':' + v)) idx.push(i); i++; }
32+
}
33+
blockHints.push(idx);
34+
}
35+
return { height: blocks.length, blockHints };
36+
}
37+
38+
export function reconstructUtxo(blocks, blockHints, { txidOf }) {
39+
const utxo = new Set();
40+
for (let b = 0; b < blocks.length; b++) {
41+
const want = new Set(blockHints[b]); let i = 0;
42+
for (const tx of blocks[b].transactions) {
43+
const txid = txidOf(tx);
44+
for (let v = 0; v < tx.outputs.length; v++) { if (want.has(i)) utxo.add(txid + ':' + v); i++; }
45+
}
46+
}
47+
return utxo;
48+
}

‎packages/swiftsync/index.js‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
export { Accumulator } from './accumulator.js';
55
export { applyBlocks } from './validate.js';
6+
export { generateHints, reconstructUtxo } from './hint.js';
67
export { eliasFanoEncode, eliasFanoDecode, encodeHintsfile, decodeHintsfile } from './hintsfile.js';
78
export { compressAmount, decompressAmount, compressScript, expandScript, encodeHeightCode, decodeHeightCode, encodeCoin as encodeSpentCoin, decodeCoin as decodeSpentCoin } from './undo.js';
89

‎packages/swiftsync/package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
".": "./index.js",
99
"./accumulator": "./accumulator.js",
1010
"./validate": "./validate.js",
11+
"./hint": "./hint.js",
1112
"./hintsfile": "./hintsfile.js",
1213
"./undo": "./undo.js"
1314
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Hint generation + reconstruction: the generator marks block-local output
2+
// indices that stay unspent; the verifier rebuilds the exact same UTXO set from
3+
// the bitmap alone. (Verified separately end-to-end on 5,000 real testnet4 blocks.)
4+
5+
import { test } from 'node:test';
6+
import assert from 'node:assert/strict';
7+
import { generateHints, reconstructUtxo } from '../hint.js';
8+
import { encodeHintsfile, decodeHintsfile } from '../hintsfile.js';
9+
10+
const A = 'aa'.repeat(32), B = 'bb'.repeat(32), NULL = '00'.repeat(32);
11+
const txidOf = (tx) => tx.id;
12+
13+
test('generate + reconstruct: indices count all outputs, OP_RETURN never unspent', () => {
14+
const blocks = [
15+
{ transactions: [
16+
{ id: A, inputs: [{ prevout: { txid: NULL, vout: 0xffffffff } }], outputs: [{ scriptPubKey: '51' }, { scriptPubKey: '52' }] }, // A:0, A:1
17+
] },
18+
{ transactions: [
19+
{ id: B, inputs: [{ prevout: { txid: A, vout: 0 } }], // spend A:0
20+
outputs: [{ scriptPubKey: '51' }, { scriptPubKey: '6a00' }] }, // B:0 spendable, B:1 OP_RETURN
21+
] },
22+
];
23+
// unspent at tip: A:1 (block 0 index 1), B:0 (block 1 index 0)
24+
const { blockHints } = generateHints(blocks, { txidOf });
25+
assert.deepEqual(blockHints, [[1], [0]]);
26+
27+
// verifier rebuilds the same UTXO set from the bitmap alone
28+
const utxo = reconstructUtxo(blocks, blockHints, { txidOf });
29+
assert.deepEqual([...utxo].sort(), [A + ':1', B + ':0'].sort());
30+
31+
// and it survives the Elias-Fano hintsfile round-trip
32+
assert.deepEqual(decodeHintsfile(encodeHintsfile({ height: 2, blockHints })).blockHints, blockHints);
33+
});

0 commit comments

Comments
 (0)