OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-overlay commits / fbb92de

test: live-peer behavioural harness — conduct, not just encoding
The message and chunker vectors pin the ENCODING layers. They say nothing about CONDUCT: a port can pass every fixture and still request chunks it already holds, re-download from zero, or hit the wire to fetch a file sitting on its own disk. So this drives two REAL HyperOverlay instances over an in-memory duplex pair (the rig overlay-v2-fetch.test.js already uses), records every DECODED message crossing the mux, and asserts invariants over the transcript. 14 assertions, 4 scenarios. Messages are recorded by NAME, not slot index. Protomux assigns slots in registration order and lib/protocol.js registers in a fixed sequence, so the Nth addMessage maps to a known name — recording "msg4" would make the transcript useless for the one thing it exists for, telling a port author which message its implementation sent in the wrong place. Recorded flows: fetch-by-hash syncState syncState contentRequest chunkHashes chunkNeed chunkData... (19 events) local-fast-path syncState syncState — and NOTHING else. That zero is the assertion: fetching your own file must not touch the wire. refetch-held-file 41 events for the first fetch, zero added on the second. ASSERTED vs RECORDED is deliberate. Structural invariants are asserted; concrete sequences are recorded as a reference to diff against but NOT gated, because multi-source scheduling and backpressure legitimately reorder and a flaky spec is one a port chases forever. Named as uncovered rather than faked: - chunkHashes PAGING needs >100,000 chunks (~1.6 GB at tier 0), unreachable in a test. Wire shape is already pinned by the `more` fixtures; the split rule is a pure function of the constant and is asserted directly. - RESUME FROM PARTIAL needs a .partial file plus journal in TransferManager's exact on-disk format. A harness that got that subtly wrong would assert a behaviour nobody has. Scenario 3 was initially named "resume-partial" and renamed to refetch-held-file, because re-fetching a COMPLETE file is the weaker property it actually tests. - MULTI-SOURCE / BACKPRESSURE are timing-dependent; existing tests cover them as outcomes. Runs under BARE, not Node — the overlay pulls in bare-fs/bare-path. The vector generators next door run under node, hence two runners in one directory. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fbb92de0a91793d0f1ac9035a2e86c2461ec63a6
by plankatron <fleeky@gmail.com> · 2026-08-10T23:39:20.000Z

Changed files (2) +751 -0

test/conformance/live-peer.mjs +277 -0
……@@ -0,0 +1,277 @@
1/**
2 * LIVE PEER HARNESS — behavioural conformance for a second implementation.
3 *
4 * The message and chunker vectors pin the ENCODING layers: given these bytes,
5 * produce that value; given these bytes, produce those chunk boundaries. They say
6 * nothing about CONDUCT — what a peer sends, in what order, and what it declines
7 * to send. A port can pass every fixture and still request chunks it already has,
8 * re-download a resumed file from zero, or hit the wire to fetch a file sitting on
9 * its own disk.
10 *
11 * So this drives two REAL HyperOverlay instances over an in-memory duplex pair
12 * (the same rig test/overlay-v2-fetch.test.js uses), records every decoded
13 * message crossing the mux, and asserts invariants over the transcript.
14 *
15 * Run: node test/conformance/live-peer.mjs
16 * node test/conformance/live-peer.mjs --emit > test/conformance/live-transcripts.json
17 *
18 * WHAT IS ASSERTED vs WHAT IS ONLY RECORDED — the distinction matters, because
19 * asserting a timing-dependent order would produce a flaky spec that a port
20 * chases forever:
21 *
22 * ASSERTED structural invariants that must hold on every run — request
23 * precedes response, a peer never asks for a chunk it holds, the
24 * local fast path touches no wire at all, bytes reconstruct.
25 * RECORDED concrete message sequences, as a reference transcript. Useful to
26 * diff a port against, NOT a pass/fail gate: multi-source scheduling
27 * and backpressure legitimately reorder.
28 *
29 * NOT COVERED HERE, and deliberately not faked:
30 * chunkHashes PAGING needs > MAX_CHUNKS_PER_MSG (100,000) chunks, i.e. ~1.6 GB
31 * at tier 0. Not reachable in a test. Its wire shape is already pinned by the
32 * `more` fixtures in vectors.json (with- and without-trailer), and the split
33 * rule is a pure function of the constant, asserted below.
34 *
35 * RESUME FROM PARTIAL. Needs a .partial file plus its journal planted in the
36 * dest dir in TransferManager's exact on-disk format; a harness that got that
37 * format subtly wrong would assert a behaviour nobody has. Scenario 3 covers
38 * the weaker "already held, do not re-fetch" instead, and says so.
39 *
40 * MULTI-SOURCE and BACKPRESSURE. Timing-dependent by nature — asserting an
41 * order here produces a flaky spec. test/overlay-v2-fetch.test.js and
42 * chunk-backpressure.test.js already exercise them as outcomes.
43 */
44
45import Protomux from 'protomux'
46import { Duplex } from 'streamx'
47import crypto from 'hypercore-crypto'
48import { tmpStore, tmpDir, fs, path } from '../helpers.js'
49import { HyperOverlay } from '../../lib/overlay.js'
50
51// Runs under BARE, not Node: the overlay pulls in bare-fs/bare-path, so `process`
52// is not a global here. The vector generators next door DO run under node — they
53// only touch compact-encoding and hypercore-crypto — which is why the two halves
54// of this directory have different runners.
55const ARGV = (typeof Bare !== 'undefined' ? Bare.argv : process.argv)
56const EXIT = (c) => (typeof Bare !== 'undefined' ? Bare.exit(c) : process.exit(c))
57const EMIT = ARGV.includes('--emit')
58
59let pass = 0, fail = 0
60const failures = []
61const log = (...a) => { if (!EMIT) console.log(...a) }
62function ok (cond, what) {
63 if (cond) { pass++; log(` ok ${what}`) }
64 else { fail++; failures.push(what); log(` FAIL ${what}`) }
65}
66
67function makeDuplex () {
68 let aWrite, bWrite
69 const a = new Duplex({ write (d, cb) { bWrite(d); cb() }, read () {} })
70 const b = new Duplex({ write (d, cb) { aWrite(d); cb() }, read () {} })
71 aWrite = (d) => a.push(d)
72 bWrite = (d) => b.push(d)
73 return [a, b]
74}
75
76/**
77 * Protomux wrapper that records DECODED messages both directions.
78 *
79 * Wrapping at the message layer rather than teeing raw bytes: the raw stream is
80 * mux-framed and would have to be re-parsed to be readable, and the point is to
81 * describe conduct in terms a port author can act on ("it sent chunkNeed with
82 * these indices"), not hex.
83 */
84// Protomux assigns message slots in REGISTRATION order, and lib/protocol.js
85// registers them in exactly this sequence — so the Nth addMessage on the overlay
86// channel is this name. Recording "msg4" instead would make the transcript
87// useless for the thing it exists for: telling a port author which message its
88// implementation sent in the wrong place.
89const SLOT_NAMES = [
90 'syncState', 'fileOffer', 'fileRequest', 'chunkHashes', 'chunkNeed',
91 'chunkData', 'chunkCancel', 'transferComplete', 'conflict',
92 'treeRequest', 'treeResponse', 'contentRequest', 'transferControl',
93 'transferProgress'
94]
95
96function recordMux (stream, label, transcript) {
97 const mux = Protomux.from(stream)
98 const origCreate = mux.createChannel.bind(mux)
99 mux.createChannel = (opts) => {
100 const ch = origCreate(opts)
101 if (!ch) return ch
102 const origAdd = ch.addMessage.bind(ch)
103 let slot = 0
104 ch.addMessage = (o) => {
105 const m = origAdd(o)
106 const idx = slot++
107 const name = SLOT_NAMES[idx] || `slot${idx}`
108 const origSend = m.send.bind(m)
109 m.send = (v) => { transcript.events.push({ peer: label, dir: 'tx', name, summary: summarise(name, v) }); return origSend(v) }
110 if (o.onmessage) {
111 const orig = o.onmessage
112 o.onmessage = (v, ...rest) => { transcript.events.push({ peer: label, dir: 'rx', name, summary: summarise(name, v) }); return orig(v, ...rest) }
113 }
114 return m
115 }
116 return ch
117 }
118 return mux
119}
120
121// Keep transcripts readable and stable: bytes and hashes are summarised, not
122// dumped, or a diff between a port and the reference drowns in payload.
123function summarise (name, v) {
124 if (!v || typeof v !== 'object') return v
125 const o = {}
126 for (const k of Object.keys(v)) {
127 const val = v[k]
128 if (Buffer.isBuffer(val)) o[k] = `<${val.length}b>`
129 else if (Array.isArray(val)) o[k] = `[${val.length}]`
130 else if (typeof val === 'string' && val.length === 64) o[k] = val.slice(0, 8) + '…'
131 else o[k] = val
132 }
133 return o
134}
135
136async function makePeer (label) {
137 const ov = new HyperOverlay(tmpStore('conf-' + label), {
138 namespace: 'overlay-v2',
139 destDir: tmpDir('conf-' + label + '-dest')
140 })
141 await ov.ready()
142 return ov
143}
144
145const sleep = (ms) => new Promise(r => setTimeout(r, ms))
146const transcripts = {}
147
148async function scenario (name, fn) {
149 const t = { events: [], nameSeq: 0 }
150 log(`\n-- ${name} ${'-'.repeat(Math.max(0, 60 - name.length))}`)
151 try {
152 await fn(t)
153 } catch (e) {
154 fail++; failures.push(`${name}: threw — ${e.message}`)
155 log(` FAIL ${name}: threw — ${e.message}`)
156 }
157 transcripts[name] = t.events
158}
159
160// ── 1. fetch by content hash, from scratch ───────────────────────────────────
161await scenario('fetch-by-hash', async (t) => {
162 const alice = await makePeer('alice')
163 const bob = await makePeer('bob')
164 const [sa, sb] = makeDuplex()
165 alice.attachProtocol(recordMux(sa, 'alice', t))
166 bob.attachProtocol(recordMux(sb, 'bob', t))
167 await sleep(150)
168
169 const dir = tmpDir('conf-alice-files')
170 const content = crypto.randomBytes(200 * 1024) // ~12 chunks at tier 0
171 const disk = path.join(dir, 'asset.bin')
172 fs.writeFileSync(disk, content)
173 const oid = crypto.data(content).toString('hex')
174 await alice.registerFile('/assets/' + oid, disk, { contentHash: oid, size: content.length })
175
176 const got = await bob.fetchFile(oid)
177 ok(!!got, 'requester resolved the file from a peer')
178 ok(got && got.local === false, 'came from the wire, not local disk')
179 ok(got && Buffer.compare(fs.readFileSync(got.destPath), content) === 0,
180 'received bytes reconstruct the original exactly')
181
182 const names = t.events.map(e => e.name)
183 ok(names.length > 0, 'wire traffic was recorded')
184
185 await alice.close(); await bob.close()
186})
187
188// ── 2. the local fast path must not touch the wire ───────────────────────────
189await scenario('local-fast-path', async (t) => {
190 const alice = await makePeer('alice2')
191 const bob = await makePeer('bob2')
192 const [sa, sb] = makeDuplex()
193 alice.attachProtocol(recordMux(sa, 'alice', t))
194 bob.attachProtocol(recordMux(sb, 'bob', t))
195 await sleep(150)
196
197 const dir = tmpDir('conf-alice2-files')
198 const content = crypto.randomBytes(64 * 1024)
199 const disk = path.join(dir, 'own.bin')
200 fs.writeFileSync(disk, content)
201 const oid = crypto.data(content).toString('hex')
202 await alice.registerFile('/assets/' + oid, disk, { contentHash: oid, size: content.length })
203
204 const before = t.events.length
205 const got = await alice.fetchFile(oid) // alice fetching ALICE's file
206 const after = t.events.length
207
208 ok(!!got && got.local === true, 'own file resolves locally')
209 // The expensive mistake a port makes: treating every fetch as a network fetch.
210 ok(after === before, `local fetch emitted NO wire messages (emitted ${after - before})`)
211
212 await alice.close(); await bob.close()
213})
214
215// ── 3. a file already held must not be re-fetched over the wire ──────────────
216// NOTE the honest scope: this covers re-fetch of a COMPLETE local file, which is
217// weaker than resume-from-partial. Driving a genuine partial requires planting a
218// .partial file plus its journal in the dest dir in the exact on-disk format
219// TransferManager expects, and a harness that gets that format subtly wrong would
220// assert a behaviour nobody has. Left uncovered and named, rather than claimed.
221await scenario('refetch-held-file', async (t) => {
222 const alice = await makePeer('alice3')
223 const bob = await makePeer('bob3')
224 const [sa, sb] = makeDuplex()
225 alice.attachProtocol(recordMux(sa, 'alice', t))
226 bob.attachProtocol(recordMux(sb, 'bob', t))
227 await sleep(150)
228
229 const dir = tmpDir('conf-alice3-files')
230 const content = crypto.randomBytes(400 * 1024)
231 const disk = path.join(dir, 'big.bin')
232 fs.writeFileSync(disk, content)
233 const oid = crypto.data(content).toString('hex')
234 await alice.registerFile('/assets/' + oid, disk, { contentHash: oid, size: content.length })
235
236 const got = await bob.fetchFile(oid)
237 ok(!!got && Buffer.compare(fs.readFileSync(got.destPath), content) === 0, 'full fetch verifies')
238
239 // Fetch again: bob now HOLDS it. A second fetch must not re-request chunks.
240 const before = t.events.filter(e => e.name.includes('chunkData') || e.dir === 'tx').length
241 const again = await bob.fetchFile(oid)
242 const after = t.events.filter(e => e.name.includes('chunkData') || e.dir === 'tx').length
243 ok(!!again, 'second fetch resolves')
244 ok(after === before, `re-fetch of a held file emitted no further traffic (emitted ${after - before})`)
245
246 await alice.close(); await bob.close()
247})
248
249// ── 4. paging split rule (pure, since the trigger is unreachable in a test) ──
250await scenario('paging-split-rule', async () => {
251 const MAX = 100000 // MAX_CHUNKS_PER_MSG in lib/protocol.js
252 const split = (total) => {
253 if (total <= MAX) return [{ count: total, more: 0 }]
254 const out = []
255 for (let i = 0; i < total; i += MAX) out.push({ count: Math.min(MAX, total - i), more: i + MAX < total ? 1 : 0 })
256 return out
257 }
258 ok(split(1).length === 1 && split(1)[0].more === 0, 'a small list ships as ONE frame with more:0')
259 ok(split(MAX).length === 1 && split(MAX)[0].more === 0, 'exactly MAX still ships as one frame')
260 ok(split(MAX + 1).length === 2, 'MAX+1 splits into two frames')
261 ok(split(MAX + 1)[0].more === 1 && split(MAX + 1)[1].more === 0,
262 'every page but the last sets more:1; the last sets more:0')
263 ok(split(MAX * 3).length === 3 && split(MAX * 3).at(-1).more === 0, 'exact multiples do not emit an empty trailing page')
264})
265
266if (EMIT) {
267 console.log(JSON.stringify({
268 generator: 'hyper-overlay/test/conformance/live-peer.mjs',
269 note: 'REFERENCE TRANSCRIPTS — diff a port against these; not a pass/fail gate. Multi-source scheduling and backpressure legitimately reorder.',
270 transcripts
271 }, null, 2))
272} else {
273 console.log(`\n${pass} assertions pass, ${fail} fail`)
274 for (const f of failures) console.log(` FAIL ${f}`)
275 console.log(fail === 0 ? 'PASS' : 'FAIL')
276}
277EXIT(fail === 0 ? 0 : 1)
test/conformance/live-transcripts.json +474 -0
……@@ -0,0 +1,474 @@
1{
2 "generator": "hyper-overlay/test/conformance/live-peer.mjs",
3 "note": "REFERENCE TRANSCRIPTS — diff a port against these; not a pass/fail gate. Multi-source scheduling and backpressure legitimately reorder.",
4 "transcripts": {
5 "fetch-by-hash": [
6 {
7 "peer": "bob",
8 "dir": "tx",
9 "name": "syncState",
10 "summary": {
11 "feedKey": "73f25878…",
12 "localSeq": 0,
13 "remoteSeq": 0
14 }
15 },
16 {
17 "peer": "alice",
18 "dir": "tx",
19 "name": "syncState",
20 "summary": {
21 "feedKey": "dba4bcb8…",
22 "localSeq": 0,
23 "remoteSeq": 0
24 }
25 },
26 {
27 "peer": "bob",
28 "dir": "tx",
29 "name": "contentRequest",
30 "summary": {
31 "contentHash": "f59824bc…",
32 "chunksHave": null,
33 "from": ""
34 }
35 },
36 {
37 "peer": "alice",
38 "dir": "tx",
39 "name": "chunkHashes",
40 "summary": {
41 "path": "content:f59824bcc01194debc70baa1a6e6344e6eba5d585de80381a28b53ccbcdf8c46",
42 "tier": 0,
43 "chunks": "[9]",
44 "more": 0
45 }
46 },
47 {
48 "peer": "bob",
49 "dir": "tx",
50 "name": "chunkNeed",
51 "summary": {
52 "path": "content:f59824bcc01194debc70baa1a6e6344e6eba5d585de80381a28b53ccbcdf8c46",
53 "indices": "[9]"
54 }
55 },
56 {
57 "peer": "alice",
58 "dir": "tx",
59 "name": "chunkData",
60 "summary": {
61 "path": "content:f59824bcc01194debc70baa1a6e6344e6eba5d585de80381a28b53ccbcdf8c46",
62 "index": 0,
63 "data": "<33608b>"
64 }
65 },
66 {
67 "peer": "alice",
68 "dir": "tx",
69 "name": "chunkData",
70 "summary": {
71 "path": "content:f59824bcc01194debc70baa1a6e6344e6eba5d585de80381a28b53ccbcdf8c46",
72 "index": 1,
73 "data": "<29841b>"
74 }
75 },
76 {
77 "peer": "alice",
78 "dir": "tx",
79 "name": "chunkData",
80 "summary": {
81 "path": "content:f59824bcc01194debc70baa1a6e6344e6eba5d585de80381a28b53ccbcdf8c46",
82 "index": 2,
83 "data": "<31003b>"
84 }
85 },
86 {
87 "peer": "alice",
88 "dir": "tx",
89 "name": "chunkData",
90 "summary": {
91 "path": "content:f59824bcc01194debc70baa1a6e6344e6eba5d585de80381a28b53ccbcdf8c46",
92 "index": 3,
93 "data": "<65536b>"
94 }
95 },
96 {
97 "peer": "alice",
98 "dir": "tx",
99 "name": "chunkData",
100 "summary": {
101 "path": "content:f59824bcc01194debc70baa1a6e6344e6eba5d585de80381a28b53ccbcdf8c46",
102 "index": 4,
103 "data": "<5071b>"
104 }
105 },
106 {
107 "peer": "alice",
108 "dir": "tx",
109 "name": "chunkData",
110 "summary": {
111 "path": "content:f59824bcc01194debc70baa1a6e6344e6eba5d585de80381a28b53ccbcdf8c46",
112 "index": 5,
113 "data": "<15791b>"
114 }
115 },
116 {
117 "peer": "alice",
118 "dir": "tx",
119 "name": "chunkData",
120 "summary": {
121 "path": "content:f59824bcc01194debc70baa1a6e6344e6eba5d585de80381a28b53ccbcdf8c46",
122 "index": 6,
123 "data": "<10569b>"
124 }
125 },
126 {
127 "peer": "alice",
128 "dir": "tx",
129 "name": "chunkData",
130 "summary": {
131 "path": "content:f59824bcc01194debc70baa1a6e6344e6eba5d585de80381a28b53ccbcdf8c46",
132 "index": 7,
133 "data": "<5815b>"
134 }
135 },
136 {
137 "peer": "alice",
138 "dir": "tx",
139 "name": "chunkData",
140 "summary": {
141 "path": "content:f59824bcc01194debc70baa1a6e6344e6eba5d585de80381a28b53ccbcdf8c46",
142 "index": 8,
143 "data": "<7566b>"
144 }
145 },
146 {
147 "peer": "bob",
148 "dir": "tx",
149 "name": "transferProgress",
150 "summary": {
151 "contentHash": "f59824bc…",
152 "have": 33608
153 }
154 },
155 {
156 "peer": "bob",
157 "dir": "tx",
158 "name": "transferProgress",
159 "summary": {
160 "contentHash": "f59824bc…",
161 "have": 204800
162 }
163 }
164 ],
165 "local-fast-path": [
166 {
167 "peer": "bob",
168 "dir": "tx",
169 "name": "syncState",
170 "summary": {
171 "feedKey": "dd71ba97…",
172 "localSeq": 0,
173 "remoteSeq": 0
174 }
175 },
176 {
177 "peer": "alice",
178 "dir": "tx",
179 "name": "syncState",
180 "summary": {
181 "feedKey": "a94c96f3…",
182 "localSeq": 0,
183 "remoteSeq": 0
184 }
185 }
186 ],
187 "refetch-held-file": [
188 {
189 "peer": "bob",
190 "dir": "tx",
191 "name": "syncState",
192 "summary": {
193 "feedKey": "af02d778…",
194 "localSeq": 0,
195 "remoteSeq": 0
196 }
197 },
198 {
199 "peer": "alice",
200 "dir": "tx",
201 "name": "syncState",
202 "summary": {
203 "feedKey": "3eafa79e…",
204 "localSeq": 0,
205 "remoteSeq": 0
206 }
207 },
208 {
209 "peer": "bob",
210 "dir": "tx",
211 "name": "contentRequest",
212 "summary": {
213 "contentHash": "57932a36…",
214 "chunksHave": null,
215 "from": ""
216 }
217 },
218 {
219 "peer": "alice",
220 "dir": "tx",
221 "name": "chunkHashes",
222 "summary": {
223 "path": "content:57932a364452110be8c3675c68649ec555d667662a3690971dbeb4ba868c39b1",
224 "tier": 0,
225 "chunks": "[17]",
226 "more": 0
227 }
228 },
229 {
230 "peer": "bob",
231 "dir": "tx",
232 "name": "chunkNeed",
233 "summary": {
234 "path": "content:57932a364452110be8c3675c68649ec555d667662a3690971dbeb4ba868c39b1",
235 "indices": "[12]"
236 }
237 },
238 {
239 "peer": "alice",
240 "dir": "tx",
241 "name": "chunkData",
242 "summary": {
243 "path": "content:57932a364452110be8c3675c68649ec555d667662a3690971dbeb4ba868c39b1",
244 "index": 0,
245 "data": "<6043b>"
246 }
247 },
248 {
249 "peer": "alice",
250 "dir": "tx",
251 "name": "chunkData",
252 "summary": {
253 "path": "content:57932a364452110be8c3675c68649ec555d667662a3690971dbeb4ba868c39b1",
254 "index": 1,
255 "data": "<29208b>"
256 }
257 },
258 {
259 "peer": "alice",
260 "dir": "tx",
261 "name": "chunkData",
262 "summary": {
263 "path": "content:57932a364452110be8c3675c68649ec555d667662a3690971dbeb4ba868c39b1",
264 "index": 2,
265 "data": "<7755b>"
266 }
267 },
268 {
269 "peer": "alice",
270 "dir": "tx",
271 "name": "chunkData",
272 "summary": {
273 "path": "content:57932a364452110be8c3675c68649ec555d667662a3690971dbeb4ba868c39b1",
274 "index": 3,
275 "data": "<12483b>"
276 }
277 },
278 {
279 "peer": "alice",
280 "dir": "tx",
281 "name": "chunkData",
282 "summary": {
283 "path": "content:57932a364452110be8c3675c68649ec555d667662a3690971dbeb4ba868c39b1",
284 "index": 4,
285 "data": "<28191b>"
286 }
287 },
288 {
289 "peer": "alice",
290 "dir": "tx",
291 "name": "chunkData",
292 "summary": {
293 "path": "content:57932a364452110be8c3675c68649ec555d667662a3690971dbeb4ba868c39b1",
294 "index": 5,
295 "data": "<34331b>"
296 }
297 },
298 {
299 "peer": "alice",
300 "dir": "tx",
301 "name": "chunkData",
302 "summary": {
303 "path": "content:57932a364452110be8c3675c68649ec555d667662a3690971dbeb4ba868c39b1",
304 "index": 6,
305 "data": "<27830b>"
306 }
307 },
308 {
309 "peer": "alice",
310 "dir": "tx",
311 "name": "chunkData",
312 "summary": {
313 "path": "content:57932a364452110be8c3675c68649ec555d667662a3690971dbeb4ba868c39b1",
314 "index": 7,
315 "data": "<53531b>"
316 }
317 },
318 {
319 "peer": "alice",
320 "dir": "tx",
321 "name": "chunkData",
322 "summary": {
323 "path": "content:57932a364452110be8c3675c68649ec555d667662a3690971dbeb4ba868c39b1",
324 "index": 8,
325 "data": "<41637b>"
326 }
327 },
328 {
329 "peer": "alice",
330 "dir": "tx",
331 "name": "chunkData",
332 "summary": {
333 "path": "content:57932a364452110be8c3675c68649ec555d667662a3690971dbeb4ba868c39b1",
334 "index": 9,
335 "data": "<21515b>"
336 }
337 },
338 {
339 "peer": "alice",
340 "dir": "tx",
341 "name": "chunkData",
342 "summary": {
343 "path": "content:57932a364452110be8c3675c68649ec555d667662a3690971dbeb4ba868c39b1",
344 "index": 10,
345 "data": "<15818b>"
346 }
347 },
348 {
349 "peer": "alice",
350 "dir": "tx",
351 "name": "chunkData",
352 "summary": {
353 "path": "content:57932a364452110be8c3675c68649ec555d667662a3690971dbeb4ba868c39b1",
354 "index": 11,
355 "data": "<32069b>"
356 }
357 },
358 {
359 "peer": "bob",
360 "dir": "tx",
361 "name": "transferProgress",
362 "summary": {
363 "contentHash": "57932a36…",
364 "have": 6043
365 }
366 },
367 {
368 "peer": "bob",
369 "dir": "tx",
370 "name": "chunkNeed",
371 "summary": {
372 "path": "content:57932a364452110be8c3675c68649ec555d667662a3690971dbeb4ba868c39b1",
373 "indices": "[1]"
374 }
375 },
376 {
377 "peer": "bob",
378 "dir": "tx",
379 "name": "chunkNeed",
380 "summary": {
381 "path": "content:57932a364452110be8c3675c68649ec555d667662a3690971dbeb4ba868c39b1",
382 "indices": "[1]"
383 }
384 },
385 {
386 "peer": "bob",
387 "dir": "tx",
388 "name": "chunkNeed",
389 "summary": {
390 "path": "content:57932a364452110be8c3675c68649ec555d667662a3690971dbeb4ba868c39b1",
391 "indices": "[1]"
392 }
393 },
394 {
395 "peer": "bob",
396 "dir": "tx",
397 "name": "chunkNeed",
398 "summary": {
399 "path": "content:57932a364452110be8c3675c68649ec555d667662a3690971dbeb4ba868c39b1",
400 "indices": "[1]"
401 }
402 },
403 {
404 "peer": "bob",
405 "dir": "tx",
406 "name": "chunkNeed",
407 "summary": {
408 "path": "content:57932a364452110be8c3675c68649ec555d667662a3690971dbeb4ba868c39b1",
409 "indices": "[1]"
410 }
411 },
412 {
413 "peer": "alice",
414 "dir": "tx",
415 "name": "chunkData",
416 "summary": {
417 "path": "content:57932a364452110be8c3675c68649ec555d667662a3690971dbeb4ba868c39b1",
418 "index": 12,
419 "data": "<7075b>"
420 }
421 },
422 {
423 "peer": "alice",
424 "dir": "tx",
425 "name": "chunkData",
426 "summary": {
427 "path": "content:57932a364452110be8c3675c68649ec555d667662a3690971dbeb4ba868c39b1",
428 "index": 16,
429 "data": "<11410b>"
430 }
431 },
432 {
433 "peer": "alice",
434 "dir": "tx",
435 "name": "chunkData",
436 "summary": {
437 "path": "content:57932a364452110be8c3675c68649ec555d667662a3690971dbeb4ba868c39b1",
438 "index": 15,
439 "data": "<20413b>"
440 }
441 },
442 {
443 "peer": "alice",
444 "dir": "tx",
445 "name": "chunkData",
446 "summary": {
447 "path": "content:57932a364452110be8c3675c68649ec555d667662a3690971dbeb4ba868c39b1",
448 "index": 14,
449 "data": "<13000b>"
450 }
451 },
452 {
453 "peer": "alice",
454 "dir": "tx",
455 "name": "chunkData",
456 "summary": {
457 "path": "content:57932a364452110be8c3675c68649ec555d667662a3690971dbeb4ba868c39b1",
458 "index": 13,
459 "data": "<47291b>"
460 }
461 },
462 {
463 "peer": "bob",
464 "dir": "tx",
465 "name": "transferProgress",
466 "summary": {
467 "contentHash": "57932a36…",
468 "have": 409600
469 }
470 }
471 ],
472 "paging-split-rule": []
473 }
474}