/** * 覆盖网络线 · 序㉔「内容分发(块级内容寻址)」单测。 * * ⚠️ 本文件**不在** `package.json` 的 `test` 脚本文件列表里(交接单 §3.1 禁止改那张列表) * ⇒ 单跑:`node --test test/overlay-content.test.mjs` * (`npm test` 的基线与"本文件新增用例数"分开报,见交接单 §8 的 ④)。 * * 判据对应关系(交接单 §1): * - **E2** 只拿到一部分也能开始共享 ⇒ 「切分与部分持有」组 * - **E3** 版本更新只传变化块 ⇒ 「局部性」组 * - **E4** 客户端校验哈希 ⇒ 「校验」组 * - **E5** 分组隔离 ⇒ 「分组」组 * - **E6** 内容源优先级可观测 ⇒ 「优先级链」组 * * 纪律(本线反复踩过的坑): * - ⛔ **只写日志不算计数** ⇒ 每个判别器断言的是**数字递增**,不是"调用没抛错"; * - ⛔ **不许放宽判据凑绿** ⇒ 断言用**精确等值**(`deepStrictEqual` / `strictEqual`), * 不用 `>=` 这类可以让实现变差仍然通过的写法(除非判据本身就要求"至少")。 */ import { test } from 'node:test' import assert from 'node:assert/strict' import { createHash } from 'node:crypto' import { mkdtempSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { DEFAULT_BLOCK_SIZE, BLOCK_ID_HEX_LEN, blockIdOf, chunkify, contentIdOf, isBlockId, planOf, reassemble, } from '../lib/net/relay/content/chunker.js' import { ContentStore, DEFAULT_MAX_BYTES } from '../lib/net/relay/content/store.js' import { ContentSourceChain, SOURCE_TIERS, DEFAULT_TIER_ORDER, emptySourceCounters, } from '../lib/net/relay/content/source.js' import { ContentPeerGroup, groupKeyOf, sameGroup, PEER_COUNTER_KEYS } from '../lib/net/relay/content/peer.js' /** 造一段可复现的伪随机内容(⛔ 不用 `Math.random` —— 用例必须可复现)。 */ function makeBytes(size, seed = 1) { const b = Buffer.alloc(size) let x = seed >>> 0 for (let i = 0; i < size; i += 1) { // xorshift32:确定性、够散、零依赖 x ^= x << 13 x >>>= 0 x ^= x >>> 17 x ^= x << 5 x >>>= 0 b[i] = x & 0xff } return b } // ───────────────────────────────────────────────────────────────────────────── // 组 1:切分(S1 的核心不变量) // ───────────────────────────────────────────────────────────────────────────── test('chunker: 同一份内容切两次 ⇒ 块 id 序列完全一致(确定性)', () => { const buf = makeBytes(3 * DEFAULT_BLOCK_SIZE + 12345) const a = chunkify(buf) const b = chunkify(buf) assert.deepStrictEqual( a.chunks.map((c) => c.id), b.chunks.map((c) => c.id), '同一份字节两次切分必须给出逐位相同的块 id 序列', ) assert.strictEqual(a.contentId, b.contentId) assert.strictEqual(a.size, buf.length) }) test('chunker: 块 id 只由字节决定(改 1 字节 ⇒ 只有 1 块变化)', () => { const size = 3 * DEFAULT_BLOCK_SIZE const before = makeBytes(size) const after = Buffer.from(before) // 改第 2 块中间的一个字节(偏移显然落在第二块内) const flipAt = DEFAULT_BLOCK_SIZE + 100 after[flipAt] = after[flipAt] ^ 0xff const p0 = planOf(before) const p1 = planOf(after) assert.strictEqual(p0.ids.length, p1.ids.length, '块数不变') const changed = [] for (let i = 0; i < p0.ids.length; i += 1) { if (p0.ids[i] !== p1.ids[i]) changed.push(i) } assert.deepStrictEqual(changed, [1], `改 1 字节应只影响第 2 块(index=1),实际变了 ${JSON.stringify(changed)}`) // 其余块 id 必须逐位相同 ⇒ 对端持有的那几块**不用重传**(E3) assert.strictEqual(p0.ids[0], p1.ids[0]) assert.strictEqual(p0.ids[2], p1.ids[2]) }) test('chunker: 块 id 不掺序号/长度(同内容不同位置 ⇒ 同 id ⇒ 可去重)', () => { const block = makeBytes(1024, 42) const dup = Buffer.concat([block, makeBytes(1024, 43), block]) // 同一段内容出现两次 const r = chunkify(dup, 1024) assert.strictEqual(r.chunks.length, 3) assert.strictEqual(r.chunks[0].id, r.chunks[2].id, '同内容必须同 id —— id 不得掺入序号') assert.deepStrictEqual(r.ids, [r.chunks[0].id, r.chunks[1].id], '去重后的 id 列表只保留首次出现序') }) test('chunker: id 形状与长度(小写 hex,恰 32 位)', () => { const id = blockIdOf(Buffer.from('hello')) assert.ok(isBlockId(id), `${id} 应为合法块 id`) assert.strictEqual(id.length, BLOCK_ID_HEX_LEN) assert.ok(!isBlockId(id.toUpperCase()), '大写 hex 不是合法 id(口径收紧 = 避免"看着像但实际上不同")') assert.ok(!isBlockId('zz'), '乱码不是合法 id') assert.strictEqual(contentIdOf(Buffer.from('hello')), createHash('sha256').update(Buffer.from('hello')).digest('hex').slice(0, BLOCK_ID_HEX_LEN)) }) test('chunker: id 算法**逐字节钉死**(= sha256(内容) 前 32 hex,⛔ 不得掺任何东西)', () => { // 🔑 为什么必须有这条:id 算法一旦掺入"序号 / 长度 / 来源 / 时间", // 同内容会得到不同 id ⇒ **去重与共享同时静默失效**,而"两次切分一致"这类 // 自洽性断言**照样全绿**(先红后绿实测:只改 id 算法时旧用例一条都不红 ⇒ 覆盖缺口)。 // 故这里用**外部复算**(node:crypto 直接算)做绝对锚,而不依赖实现自洽。 const cases = [ Buffer.alloc(0), Buffer.from('hello'), Buffer.from([0x00]), Buffer.from([0xff, 0x00, 0xff]), makeBytes(1024, 1), makeBytes(1024, 2), makeBytes(2048, 1), ] for (const buf of cases) { const expected = createHash('sha256').update(buf).digest('hex').slice(0, BLOCK_ID_HEX_LEN) assert.strictEqual( blockIdOf(buf), expected, `id 必须恰为 sha256(内容) 的前 ${BLOCK_ID_HEX_LEN} 位 hex(内容 ${buf.length}B)`, ) } // 长度相同、内容不同 ⇒ id 必须不同(防"掺长度"这类退化) assert.notStrictEqual(blockIdOf(makeBytes(1024, 1)), blockIdOf(makeBytes(1024, 2))) // 长度不同但**前缀相同** ⇒ id 也必须不同(防"截断 / 掺长度"这类退化) const prefix = makeBytes(2048, 9) assert.notStrictEqual(blockIdOf(prefix), blockIdOf(prefix.subarray(0, 1024))) // 整份内容 id 同理钉死 const whole = makeBytes(4096, 3) assert.strictEqual( contentIdOf(whole), createHash('sha256').update(whole).digest('hex').slice(0, BLOCK_ID_HEX_LEN), ) }) test('chunker: 空内容与不足一块的边界', () => { const empty = chunkify(Buffer.alloc(0)) assert.strictEqual(empty.chunks.length, 0) assert.strictEqual(empty.size, 0) const small = chunkify(Buffer.from('abc'), 1024) assert.strictEqual(small.chunks.length, 1) assert.strictEqual(small.chunks[0].offset, 0) assert.strictEqual(small.chunks[0].bytes.length, 3) }) test('chunker: 非法块大小必须抛错(⛔ 不许静默取默认)', () => { assert.throws(() => chunkify(Buffer.from('x'), 0), /正整数/) assert.throws(() => chunkify(Buffer.from('x'), -1), /正整数/) assert.throws(() => chunkify(Buffer.from('x'), 1.5), /正整数/) }) test('chunker: subarray 视图陷阱 —— 块字节必须已复制(不随源 buffer 漂移)', () => { const src = makeBytes(2 * DEFAULT_BLOCK_SIZE) const r = chunkify(src) const idBefore = r.chunks[0].id const saved = Buffer.from(r.chunks[0].bytes) // 原地改源 buffer:已切出的块**不应**受影响 src[0] = src[0] ^ 0xff assert.ok(r.chunks[0].bytes.equals(saved), '切出的块必须与源 buffer 脱钩') assert.strictEqual(blockIdOf(r.chunks[0].bytes), idBefore) }) // ───────────────────────────────────────────────────────────────────────────── // 组 2:校验(E4 —— 篡改块必须被丢弃,⛔ 不落盘) // ───────────────────────────────────────────────────────────────────────────── test('E4: store.put 拒绝内容与声明 id 不符的块,并计数', () => { const store = new ContentStore({ maxBytes: 4 * 1024 * 1024 }) const good = makeBytes(1000, 7) const id = blockIdOf(good) store.put(id, good) assert.strictEqual(store.get(id)?.equals(good), true) const tampered = Buffer.from(good) tampered[10] = tampered[10] ^ 0x01 const before = store.counters() assert.throws(() => store.put(id, tampered), /块校验失败|丢弃/) const after = store.counters() assert.strictEqual(after.putRejected, before.putRejected + 1, 'E4 写侧:被拒次数必须 +1(⛔ 只写日志不算)') assert.strictEqual(after.puts, before.puts, '被拒的块不得计入成功入库') // ⛔ 不落盘:仓库里那一份仍然是好的 assert.strictEqual(store.get(id)?.equals(good), true, '原块必须完好(篡改块没覆盖它)') }) test('E4: store.get 读出损坏块 ⇒ 丢弃 + corruptReads 递增(不返回坏数据)', () => { const dir = mkdtempSync(join(tmpdir(), 'dshs-content-')) try { const store = new ContentStore({ maxBytes: 4 * 1024 * 1024, dir }) const bytes = makeBytes(2048, 11) const id = blockIdOf(bytes) store.put(id, bytes) // 绕过 store 直接在盘上改坏(模拟"磁盘写坏 / 进程外改动")—— 内存里有副本,先建个空 store 走盘路径 const store2 = new ContentStore({ maxBytes: 4 * 1024 * 1024, dir }) const bad = Buffer.from(bytes) bad[3] = bad[3] ^ 0xff writeFileSync(join(dir, id), bad) const c0 = store2.counters() const got = store2.get(id) const c1 = store2.counters() assert.strictEqual(got, undefined, '损坏块必须返回 undefined') assert.strictEqual(c1.corruptReads, c0.corruptReads + 1, 'E4 读侧:损坏读必须 +1(⛔ 不许静默当"没有")') } finally { rmSync(dir, { recursive: true, force: true }) } }) test('E4: reassemble 在缺块 / 篡改块时抛错并点名 index', () => { const buf = makeBytes(2 * 1024 + 10, 5) const r = chunkify(buf, 1024) const plan = r.ids const parts = new Map() for (const c of r.chunks) parts.set(c.id, c.bytes) assert.ok(reassemble(plan, parts).equals(buf), '完好块集必须能重组回原内容') // 缺一块 const missing = new Map(parts) missing.delete(plan[1]) assert.throws(() => reassemble(plan, missing), /缺少块 index=1/) // 篡改一块(id 对不上) const tampered = new Map(parts) const t = Buffer.from(parts.get(plan[1])) t[0] = t[0] ^ 0xff tampered.set(plan[1], t) assert.throws(() => reassemble(plan, tampered), /块校验失败 index=1/) }) // ───────────────────────────────────────────────────────────────────────────── // 组 3:内容寻址存储(S1 —— 去重 / LRU / 计数) // ───────────────────────────────────────────────────────────────────────────── test('store: 同内容重复入库 ⇒ 去重(容量不重复计,命中仍计数)', () => { const store = new ContentStore({ maxBytes: 1024 * 1024 }) const bytes = makeBytes(4096, 3) const id = blockIdOf(bytes) store.put(id, bytes) const usedAfterFirst = store.bytes store.put(id, bytes) store.put(id, Buffer.from(bytes)) assert.strictEqual(store.size, 1, '同 id 只应有一份') assert.strictEqual(store.bytes, usedAfterFirst, '去重 ⇒ 容量不得重复累加') assert.strictEqual(store.counters().puts, 1, '只有第一次算新入库') }) test('store: 超上限 ⇒ LRU 淘汰,且 evicted 计数与容量约束都被断言', () => { const blockSize = 1024 const store = new ContentStore({ maxBytes: 3 * blockSize }) const ids = [] for (let i = 0; i < 5; i += 1) { const b = makeBytes(blockSize, 100 + i) const id = blockIdOf(b) ids.push(id) store.put(id, b) } assert.ok(store.bytes <= 3 * blockSize, `容量必须被约束:${store.bytes} <= ${3 * blockSize}`) assert.ok(store.counters().evicted >= 2, `应发生 ≥2 次淘汰,实际 ${store.counters().evicted}`) assert.strictEqual(store.get(ids[4]) === undefined, false, '最近写入的块必须还在') assert.strictEqual(store.get(ids[0]), undefined, '最早的块应已被淘汰') }) test('store: 单块超上限 ⇒ 明确拒绝并计数(⛔ 不是静默丢)', () => { const store = new ContentStore({ maxBytes: 10_000, maxBlockBytes: 500 }) const big = makeBytes(600, 1) const id = blockIdOf(big) const c0 = store.counters() assert.throws(() => store.put(id, big), /超过单块上限/) assert.strictEqual(store.counters().oversizeRejected, c0.oversizeRejected + 1) }) test('store: 命中/未命中计数可断言(E1 的直接来源)', () => { const store = new ContentStore({ maxBytes: 1024 * 1024 }) const bytes = makeBytes(2048, 9) const id = blockIdOf(bytes) const c0 = store.counters() assert.strictEqual(store.get(id), undefined) assert.strictEqual(store.counters().misses, c0.misses + 1, '未命中必须 +1') store.put(id, bytes) const c1 = store.counters() assert.notStrictEqual(store.get(id), undefined) assert.strictEqual(store.counters().hits, c1.hits + 1, '命中必须 +1(这就是"省下一次回源"的机器判据)') }) test('store: 非法 id 与非法构造参数必须拒绝', () => { assert.throws(() => new ContentStore({ maxBytes: 0 }), /正数/) assert.throws(() => new ContentStore({ maxBytes: -5 }), /正数/) const store = new ContentStore({ maxBytes: 1024 }) const c0 = store.counters() assert.throws(() => store.put('not-a-valid-id', Buffer.from('x')), /非法块 id/) assert.strictEqual(store.counters().putRejected, c0.putRejected + 1) assert.strictEqual(store.get('not-a-valid-id'), undefined) assert.strictEqual(store.has('not-a-valid-id'), false) }) test('store: 默认上限 = DEFAULT_MAX_BYTES 且 > 0(P5 预算的固化点)', () => { assert.strictEqual(DEFAULT_MAX_BYTES, 64 * 1024 * 1024) const store = new ContentStore() assert.strictEqual(store.size, 0) assert.strictEqual(store.bytes, 0) }) // ───────────────────────────────────────────────────────────────────────────── // 组 4:内容源优先级链(E6 —— 判别器必须落计数) // ───────────────────────────────────────────────────────────────────────────── test('E6: 五档链路顺序固定,且每次取块点名来源档位(计数递增)', async () => { assert.deepStrictEqual( DEFAULT_TIER_ORDER, ['local', 'peer', 'edge', 'region', 'origin'], '内容源优先级:本地 → 同局域网 peer → 同区域边缘缓存 → 区域分发点 → 公网源', ) assert.deepStrictEqual(SOURCE_TIERS, DEFAULT_TIER_ORDER) const hits = [] const chain = new ContentSourceChain({ // 四档都返回内容(夹具):链必须**按顺序**命中第一档可用者 fetchers: { local: async () => undefined, // 本地没有 peer: async () => ({ tier: 'peer', bytes: Buffer.from('from-peer') }), edge: async () => ({ tier: 'edge', bytes: Buffer.from('from-edge') }), region: async () => ({ tier: 'region', bytes: Buffer.from('from-region') }), origin: async () => ({ tier: 'origin', bytes: Buffer.from('from-origin') }), }, onHit: (tier) => hits.push(tier), }) const r = await chain.fetch('a'.repeat(BLOCK_ID_HEX_LEN)) assert.strictEqual(r?.bytes.toString(), 'from-peer', '本地空 ⇒ 命中 peer(顺序:local → peer)') assert.deepStrictEqual(hits, ['peer'], '命中档位必须被点名') const c = chain.counters() assert.strictEqual(c.local, 0) assert.strictEqual(c.peer, 1, 'E6:peer 命中计数必须为 1(⛔ 只写日志 = 不合格)') assert.strictEqual(c.edge, 0, '⛔ 不许越过 peer 直接打 edge') }) test('E6: 逐档递减 —— 去掉某一档后必须落到下一档,且计数逐档递增', async () => { const make = (available) => new ContentSourceChain({ fetchers: Object.fromEntries( DEFAULT_TIER_ORDER.map((tier) => [ tier, async () => (available.includes(tier) ? { tier, bytes: Buffer.from(`from-${tier}`) } : undefined), ]), ), }) // 只 origin 有 const c1 = make(['origin']) const r1 = await c1.fetch('b'.repeat(BLOCK_ID_HEX_LEN)) assert.strictEqual(r1?.tier, 'origin') assert.deepStrictEqual( DEFAULT_TIER_ORDER.map((t) => c1.counters()[t]), [0, 0, 0, 0, 1], '只有 origin 命中 ⇒ 计数必须是 [0,0,0,0,1]', ) // edge 也有 ⇒ 必须停在 edge(⛔ 不许越过更靠前的档) const c2 = make(['edge', 'origin']) const r2 = await c2.fetch('c'.repeat(BLOCK_ID_HEX_LEN)) assert.strictEqual(r2?.tier, 'edge') assert.strictEqual(c2.counters().origin, 0, '⛔ 越过 edge 去打 origin = 优先级链失效') // local 也有 ⇒ 必须停在 local(最短路径) const c3 = make(DEFAULT_TIER_ORDER) const r3 = await c3.fetch('d'.repeat(BLOCK_ID_HEX_LEN)) assert.strictEqual(r3?.tier, 'local') assert.deepStrictEqual( DEFAULT_TIER_ORDER.map((t) => c3.counters()[t]), [1, 0, 0, 0, 0], 'local 命中 ⇒ 后面四档计数必须全 0', ) }) test('E6: 全档皆无 ⇒ 返回 undefined 且**每一档都留下未命中痕迹**(⛔ 不许静默返空)', async () => { const tried = [] const chain = new ContentSourceChain({ fetchers: Object.fromEntries(DEFAULT_TIER_ORDER.map((tier) => [tier, async () => undefined])), onMiss: (tier) => tried.push(tier), }) const r = await chain.fetch('e'.repeat(BLOCK_ID_HEX_LEN)) assert.strictEqual(r, undefined) assert.deepStrictEqual(tried, DEFAULT_TIER_ORDER, '"没有"必须留下**逐档**痕迹,否则就是本线反复踩的静默失效') const total = Object.values(chain.counters()).reduce((a, b) => a + b, 0) assert.strictEqual(total, 0, '未命中不得计入任何档位的命中计数') assert.strictEqual(chain.misses(), DEFAULT_TIER_ORDER.length, '未命中合计 = 档数') }) test('E6: fetcher 抛错必须计数并**继续往下一档**(⛔ 不许整体失败、不许静默吞)', async () => { const chain = new ContentSourceChain({ fetchers: { local: async () => { throw new Error('local boom') }, peer: async () => undefined, edge: async () => ({ tier: 'edge', bytes: Buffer.from('ok') }), region: async () => undefined, origin: async () => undefined, }, }) const r = await chain.fetch('f'.repeat(BLOCK_ID_HEX_LEN)) assert.strictEqual(r?.bytes.toString(), 'ok', '前面的档抛错后必须继续尝试后面的档') assert.strictEqual(chain.errors().local, 1, '抛错必须单独计数(与"没有"可区分)') }) test('source: emptySourceCounters 覆盖五档且形状完整', () => { const c = emptySourceCounters() for (const tier of DEFAULT_TIER_ORDER) { assert.strictEqual(typeof c[tier], 'number', `档位 ${tier} 必须有计数`) assert.strictEqual(c[tier], 0) } }) // ───────────────────────────────────────────────────────────────────────────── // 组 5:分组隔离(E5 —— 跨组不穿透) // ───────────────────────────────────────────────────────────────────────────── test('E5: 分组键由 (网络, 组) 决定;跨组/跨网一律不同组', () => { assert.strictEqual(groupKeyOf('u:1', 'lan-a'), 'u:1|lan-a') assert.notStrictEqual(groupKeyOf('u:1', 'lan-a'), groupKeyOf('u:1', 'lan-b'), '同网不同组必须不同') assert.notStrictEqual(groupKeyOf('u:1', 'lan-a'), groupKeyOf('u:2', 'lan-a'), '跨网不得同组') assert.strictEqual(sameGroup('u:1', 'lan-a', 'u:1', 'lan-a'), true) assert.strictEqual(sameGroup('u:1', 'lan-a', 'u:1', 'lan-b'), false) assert.strictEqual(sameGroup('u:1', 'lan-a', 'u:2', 'lan-a'), false) }) test('E5: 本地组内取块命中;跨组一律拒绝且计入 crossGroupDenied(⛔ 必须是显式拒绝)', () => { const group = new ContentPeerGroup({ network: 'u:1', group: 'lan-a', log: () => {} }) const bytes = makeBytes(4096, 21) const id = blockIdOf(bytes) // 同组 peer 供块 group.addPeer({ name: 'u:1/p1', network: 'u:1', group: 'lan-a', holds: [id] }) // 跨组 peer 也持有同一块(用来证明"不穿透") group.addPeer({ name: 'u:1/p2', network: 'u:1', group: 'lan-b', holds: [id] }) group.addPeer({ name: 'u:2/p3', network: 'u:2', group: 'lan-a', holds: [id] }) const inGroup = group.candidates(id) assert.deepStrictEqual( inGroup.map((p) => p.name), ['u:1/p1'], 'E5:只有同 (网, 组) 的 peer 能成为候选', ) const c0 = group.counters() const denied = group.markDenied('u:1/p2', id) assert.strictEqual(denied, true, '跨组请求必须被显式拒绝') const c1 = group.counters() assert.strictEqual(c1.crossGroupDenied, c0.crossGroupDenied + 1, 'E5:跨组拒绝必须计数(⛔ 静默返空 = 假绿)') assert.strictEqual(group.markDenied('u:2/p3', id), true) assert.strictEqual(group.counters().crossGroupDenied, 2) // 同组请求不被拒绝 assert.strictEqual(group.markDenied('u:1/p1', id), false, '同组不得被拒') assert.strictEqual(group.counters().crossGroupDenied, 2, '同组不得计入跨组拒绝') }) test('E5: PEER_COUNTER_KEYS 齐全(判别器存在性可断言)', () => { const group = new ContentPeerGroup({ network: 'ops', group: 'lan-x', log: () => {} }) const c = group.counters() for (const k of PEER_COUNTER_KEYS) { assert.strictEqual(typeof c[k], 'number', `peer 判别器 ${k} 必须是数字(OBS 会断言它)`) } assert.ok(PEER_COUNTER_KEYS.includes('crossGroupDenied')) assert.ok(PEER_COUNTER_KEYS.includes('peerHits')) assert.ok(PEER_COUNTER_KEYS.includes('peerMisses')) }) test('E5: 分组不影响同组内多 peer 的可用性(E2 的支撑)', () => { const group = new ContentPeerGroup({ network: 'u:1', group: 'lan-a', log: () => {} }) const a = makeBytes(2048, 31) const b = makeBytes(2048, 32) const ida = blockIdOf(a) const idb = blockIdOf(b) group.addPeer({ name: 'u:1/pA', network: 'u:1', group: 'lan-a', holds: [ida] }) group.addPeer({ name: 'u:1/pB', network: 'u:1', group: 'lan-a', holds: [idb] }) // 只拿到一部分也能开始共享:两人各持一块,请求任一块都有人能供 assert.deepStrictEqual(group.candidates(ida).map((p) => p.name), ['u:1/pA']) assert.deepStrictEqual(group.candidates(idb).map((p) => p.name), ['u:1/pB']) }) // ───────────────────────────────────────────────────────────────────────────── // 组 6:装配级不回归(本单的模块集合必须自洽) // ───────────────────────────────────────────────────────────────────────────── test('端到端(进程内):切分 → 入库 → 计划重组 → 校验闭环', async () => { const store = new ContentStore({ maxBytes: 8 * 1024 * 1024 }) const buf = makeBytes(4 * 1024 * 1024 + 777, 77) const r = chunkify(buf) for (const c of r.chunks) store.put(c.id, c.bytes) // 只拿到 30% 也要能列出计划(E2) const plan = planOf(buf) assert.strictEqual(plan.ids.length, r.chunks.length) // 从 store 取回并重组 const parts = new Map() for (const id of plan.ids) { const got = store.get(id) assert.notStrictEqual(got, undefined) parts.set(id, got) } assert.ok(reassemble(plan.ids, parts).equals(buf), '重组必须逐字节等于原内容') assert.strictEqual(store.counters().hits, plan.ids.length, `命中数应等于块数 ${plan.ids.length}`) }) // ───────────────────────────────────────────────────────────────────────────── // 组 7:序㉘ · 单 B「组密钥加密」—— F1–F6(交接单_组密钥加密_20260918 §5) // ⚠️ 断言一律**精确等值**(⛔ 不用 `>=` 让实现变差还能过,除非判据本身就要求"至少") // ⚠️ 本组**刻意不进** `package.json` 的 test 列表(那张列表是硬编码的)⇒ 单跑本文件 // ───────────────────────────────────────────────────────────────────────────── import { readFileSync } from 'node:fs' import { ContentRuntime } from '../lib/net/relay/content/runtime.js' import { generateAuthorityKey, signPayloadWith } from '../lib/net/relay/identity.js' import { ContentCipher, CONTENT_CRYPTO_COUNTER_KEYS, CONTENT_CIPHER_VERSION, IV_LEN, KEY_LEN, TAG_LEN, groupKeyCredentialPayload, keyIdOf, loadGroupKeyFile, verifyGroupKeyCredential, // 🆕 序㊻ · C(域分离) BLOCK_ID_DOMAIN_TAG, deriveBlockIdKey, } from '../lib/net/relay/content/crypto.js' /** 造一个确定性的组密钥(⛔ 不用随机 —— 用例必须可复现)。 */ function makeKey(seed = 9) { return makeBytes(KEY_LEN, seed) } test('F1 确定性:同组 + 同明文两次 ⇒ 密文逐字节相同("按哈希共享块"的前提)', () => { const cipher = new ContentCipher({ groupKey: 'ops|relay', epoch: 1, key: makeKey(1) }) const plain = Buffer.from('dshs-content-block-0123456789', 'utf8') const a = cipher.encryptBlock(plain) const b = cipher.encryptBlock(plain) assert.ok(a.equals(b), '同明文两次必须逐字节相同(否则块 id 每次都变 ⇒ 去重与 peer 命中全废)') assert.strictEqual(a.length, plain.length + IV_LEN + TAG_LEN, '密文 = iv(12) + tag(16) + 明文') assert.strictEqual(cipher.decodeBlock(a).toString('utf8'), 'dshs-content-block-0123456789') }) test('F1-b 换 epoch / 换密钥 ⇒ 密文必不同;且旧密文用新密钥解 ⇒ 认证失败', () => { const k = makeKey(1) const c1 = new ContentCipher({ groupKey: 'ops|relay', epoch: 1, key: k }) const c2 = new ContentCipher({ groupKey: 'ops|relay', epoch: 2, key: k }) const c3 = new ContentCipher({ groupKey: 'ops|relay', epoch: 1, key: makeKey(2) }) const plain = Buffer.from('same-plaintext', 'utf8') const b1 = c1.encryptBlock(plain) assert.ok(!b1.equals(c2.encryptBlock(plain)), '换 epoch ⇒ AAD 与密钥双变 ⇒ 密文必须不同') assert.ok(!b1.equals(c3.encryptBlock(plain)), '换密钥 ⇒ 密文必须不同') assert.strictEqual(c2.decodeBlock(b1), undefined, '跨 epoch 必须**在认证阶段**被拒') assert.strictEqual(c3.decodeBlock(b1), undefined, '跨密钥必须被拒') assert.strictEqual(c2.counters().decryptRejected, 1) assert.strictEqual(c3.counters().decryptRejected, 1) }) test('F1-c 块 id 挂密文(β′):同组两次独立"切块+加密" ⇒ id 序列逐字一致;换组 ⇒ 全变', () => { const buf = makeBytes(3 * 1024 * 1024 + 13, 41) const A = new ContentCipher({ groupKey: 'ops|grpA', epoch: 1, key: makeKey(3) }) const B = new ContentCipher({ groupKey: 'ops|grpB', epoch: 1, key: makeKey(4) }) const encA = { encode: (p) => A.encryptBlock(p) } const r1 = chunkify(buf, 1024 * 1024, encA) const r2 = chunkify(buf, 1024 * 1024, encA) assert.deepStrictEqual(r1.chunks.map((c) => c.id), r2.chunks.map((c) => c.id), '组内 id 必须稳定') const rb = chunkify(buf, 1024 * 1024, { encode: (p) => B.encryptBlock(p) }) assert.notDeepStrictEqual( r1.chunks.map((c) => c.id), rb.chunks.map((c) => c.id), '换组 ⇒ 全部 id 改变(= 轮换 ⇒ 全量回源 的直接证据)', ) // 明文哈希(α)与密文哈希(β′)**必须不同**(否则等于没换口径) assert.notDeepStrictEqual(r1.chunks.map((c) => c.id), chunkify(buf, 1024 * 1024).chunks.map((c) => c.id)) // 密文口径下"计划"与"切分"必须同源(否则先查 peer 会查错 id) assert.deepStrictEqual(planOf(buf, 1024 * 1024, encA).ids, r1.chunks.map((c) => c.id)) }) test('F1-d 缺省(不传 transforms)⇒ 与序㉔ 逐字一致(零回归口径)', () => { const buf = makeBytes(1024 * 1024 + 5, 5) const a = chunkify(buf) const b = chunkify(buf, DEFAULT_BLOCK_SIZE, {}) assert.deepStrictEqual(a.chunks.map((c) => c.id), b.chunks.map((c) => c.id)) assert.strictEqual(a.contentId, b.contentId) assert.strictEqual(a.contentId, contentIdOf(buf), '缺省 contentId 仍是明文哈希') }) test('F2 共享不退化:put → 链上取回两次 ⇒ local 命中递增且解密成功', async () => { const cipher = new ContentCipher({ groupKey: 'ops|relay', epoch: 1, key: makeKey(6) }) const rt = new ContentRuntime({ network: 'ops', group: 'relay', cipher, storeMaxBytes: 16 * 1024 * 1024 }) const buf = makeBytes(2 * 1024 * 1024 + 9, 61) const put = rt.putContent(buf) assert.strictEqual(put.plan.length, 3, '2 MiB + 9 B ⇒ 3 块') const once = await rt.fetchContent(put.plan) const twice = await rt.fetchContent(put.plan) assert.ok(once.equals(buf), '第一次取回必须是原明文') assert.ok(twice.equals(buf), '第二次取回必须是原明文') const c = rt.snapshot() assert.strictEqual(c.source.local, 6, '两次取回 × 3 块 ⇒ local 命中 6(⛔ 不许退化成回源)') assert.strictEqual(c.source.origin, 0) assert.strictEqual(c.store.hits, 6) assert.strictEqual(c.crypto.decrypts, 6) assert.strictEqual(c.crypto.decryptRejected, 0) // ⚠️ 存储里放的是**密文**:库里的字节 ≠ 明文 const firstId = put.plan[0] assert.ok(!rt.store.get(firstId).equals(buf.subarray(0, 1024 * 1024)), '库里的块必须是密文') // 16 字节的标记在密文里必须找不到 assert.ok(rt.store.get(firstId).length > 1024 * 1024, '密文比明文长 iv+tag') }) test('F2-b 去重仍成立(E1 口径):同一份内容重复 put ⇒ cache 不翻倍', () => { const cipher = new ContentCipher({ groupKey: 'ops|relay', epoch: 1, key: makeKey(7) }) const rt = new ContentRuntime({ network: 'ops', group: 'relay', cipher }) const buf = makeBytes(1024 * 1024 * 2, 71) rt.putContent(buf) const blocksAfter1 = rt.snapshot().storeBlocks assert.strictEqual(blocksAfter1, 2) rt.putContent(buf) assert.strictEqual(rt.snapshot().storeBlocks, blocksAfter1, '同内容重复入库必须去重(否则 E1 直接崩)') assert.strictEqual(rt.snapshot().store.puts, blocksAfter1, '重复 put 不得重复计 puts') }) test('F3 跨组不可解:组 B 用自己的密钥解组 A 的密文 ⇒ 拒 + decryptRejected +1', () => { const A = new ContentCipher({ groupKey: 'ops|grpA', epoch: 1, key: makeKey(8) }) const B = new ContentCipher({ groupKey: 'ops|grpB', epoch: 1, key: makeKey(9) }) const blob = A.encryptBlock(Buffer.from('只有 A 组能看的内容', 'utf8')) assert.strictEqual(B.decodeBlock(blob), undefined) assert.strictEqual(B.counters().decryptRejected, 1) assert.strictEqual(A.decodeBlock(blob).toString('utf8'), '只有 A 组能看的内容') assert.strictEqual(A.counters().decryptRejected, 0) }) test('F3-b 跨组判定不被放松:未声明 epoch 的 peer 仍按"同组即可用"(序㉔ 语义不变)', () => { const g = new ContentPeerGroup({ network: 'ops', group: 'lan-a', epoch: 1 }) g.addPeer({ name: 'ops/p1', network: 'ops', group: 'lan-a', holds: ['aa'] }) g.addPeer({ name: 'ops/p2', network: 'ops', group: 'lan-b', holds: ['aa'] }) assert.deepStrictEqual(g.candidates('aa').map((p) => p.name), ['ops/p1']) assert.strictEqual(g.counters().crossGroupDenied, 0, '跨组拒绝语义未被本单改动') }) test('F3-c epoch 不一致 ⇒ 不作候选 + 单独计数(⛔ 不混进 crossGroupDenied)', () => { const g = new ContentPeerGroup({ network: 'ops', group: 'lan-a', epoch: 2 }) g.addPeer({ name: 'ops/p1', network: 'ops', group: 'lan-a', holds: ['aa'], epoch: 1 }) g.addPeer({ name: 'ops/p2', network: 'ops', group: 'lan-a', holds: ['aa'], epoch: 2 }) assert.deepStrictEqual(g.candidates('aa').map((p) => p.name), ['ops/p2']) assert.strictEqual(g.counters().epochMismatch, 1) assert.strictEqual(g.counters().crossGroupDenied, 0) }) test('F4 明文不出现(+ 反向证明):加密 ⇒ 扫不到;不加密 ⇒ 必须扫得到', () => { const marker = 'PLAINTEXT-MARKER-7f3a91' const cipher = new ContentCipher({ groupKey: 'ops|relay', epoch: 1, key: makeKey(10) }) const rt = new ContentRuntime({ network: 'ops', group: 'relay', cipher }) const put = rt.putContent(Buffer.from(`head-${marker}-tail`, 'utf8')) for (const id of put.plan) { assert.ok(!rt.store.get(id).includes(Buffer.from(marker, 'utf8')), '密文里不许出现明文标记') } // ⛔ 反向夹具:**关掉加密** ⇒ 同一份内容入库后**必须**扫得到标记(否则"没扫到"毫无意义) const plainRt = new ContentRuntime({ network: 'ops', group: 'relay' }) const put2 = plainRt.putContent(Buffer.from(`head-${marker}-tail`, 'utf8')) assert.ok( plainRt.store.get(put2.plan[0]).includes(Buffer.from(marker, 'utf8')), '未启用加密时必须扫得到(= 判据有效性证明)', ) }) test('F4-b 启动自证:detChecks/detMismatches/plainScans/plainLeaks 四键语义', async () => { const cipher = new ContentCipher({ groupKey: 'ops|relay', epoch: 1, key: makeKey(11) }) const rt = new ContentRuntime({ network: 'ops', group: 'relay', cipher }) const ok = await rt.selfProbe('self-probe-marker') assert.strictEqual(ok, true) const c = rt.snapshot().crypto assert.strictEqual(c.detChecks, 1) assert.strictEqual(c.detMismatches, 0) assert.strictEqual(c.plainScans, 1) assert.strictEqual(c.plainLeaks, 0) assert.strictEqual(c.decrypts, 2, '自证两次解密:crypto 自证一次 + 优先级链上一次') }) test('F4-c 不启用加密 ⇒ crypto 键整体缺席(⛔ 不是补零 ⇒ 探针才能记 SKIP)', async () => { const rt = new ContentRuntime({ network: 'ops', group: 'relay' }) assert.strictEqual('crypto' in rt.snapshot(), false) assert.strictEqual(rt.cryptoEnabled, false) assert.strictEqual(rt.snapshot().peer.epochMismatch, 0) assert.strictEqual(await rt.selfProbe('x'), undefined, '未启用 ⇒ 自证直接短路(不打日志、不计数)') }) test('F6 失败关闭(五种情形各有具名原因,⛔ 绝不静默"以为加密了")', () => { const dir = mkdtempSync(join(tmpdir(), 'cfgx-')) const k = makeKey(12) const good = { version: 1, group: 'relay', epoch: 1, key: k.toString('base64') } // ① no-file assert.strictEqual(loadGroupKeyFile({ file: join(dir, 'nope.json'), group: 'relay' }).reason, 'no-file') // ② bad-perms(🔴 POSIX-only:靠 enforcePerms 显式开启 ⇒ 本机 Windows 也能证明"判据有牙") const p600 = join(dir, 'k600.json') writeFileSync(p600, JSON.stringify(good), { mode: 0o600 }) assert.strictEqual(loadGroupKeyFile({ file: p600, group: 'relay', enforcePerms: true }).reason, 'bad-perms') // ③ group-mismatch assert.strictEqual(loadGroupKeyFile({ file: p600, group: 'local', enforcePerms: false }).reason, 'group-mismatch') // ③-b network 不配也要拦("同名不同网") const pNet = join(dir, 'knet.json') writeFileSync(pNet, JSON.stringify({ ...good, network: 'ops' }), { mode: 0o600 }) assert.strictEqual( loadGroupKeyFile({ file: pNet, group: 'relay', network: 'u:1', enforcePerms: false }).reason, 'group-mismatch', ) // ④ bad-key const pBad = join(dir, 'kbad.json') writeFileSync(pBad, JSON.stringify({ ...good, key: Buffer.alloc(16).toString('base64') }), { mode: 0o600 }) assert.strictEqual(loadGroupKeyFile({ file: pBad, group: 'relay', enforcePerms: false }).reason, 'bad-key') // ⑤ bad-epoch const pEpoch = join(dir, 'kepoch.json') writeFileSync(pEpoch, JSON.stringify({ ...good, epoch: 0 }), { mode: 0o600 }) assert.strictEqual(loadGroupKeyFile({ file: pEpoch, group: 'relay', enforcePerms: false }).reason, 'bad-epoch') // ⑥ 解析错 const pJunk = join(dir, 'kjunk.json') writeFileSync(pJunk, '{ not json', { mode: 0o600 }) assert.strictEqual(loadGroupKeyFile({ file: pJunk, group: 'relay', enforcePerms: false }).reason, 'parse-error') // ⑦ 正例:装载成功且 keyId 与 key 一致;双 epoch 计数对 const both = { ...good, epoch: 2, previous: [{ epoch: 1, key: makeKey(3).toString('base64') }] } writeFileSync(p600, JSON.stringify(both), { mode: 0o600 }) const okv = loadGroupKeyFile({ file: p600, group: 'relay', network: 'ops', enforcePerms: false }) assert.strictEqual(okv.ok, true) assert.strictEqual(okv.keyId, keyIdOf(k)) assert.strictEqual(okv.epoch, 2) assert.strictEqual(okv.epochs, 2) assert.strictEqual(okv.permsChecked, false) // 🔴 权限判定的**平台语义必须分开写**:Windows 没有 POSIX 权限位(实测 mode 恒 666) // ⇒ 强制判定在 Windows 上**必然**报 `bad-perms`(这正是"判据有牙"的证明), // 在 POSIX 上 0600 的文件则必须通过并回报 `permsChecked=true`。 const forced = loadGroupKeyFile({ file: p600, group: 'relay', enforcePerms: true }) if (process.platform === 'win32') { assert.strictEqual(forced.reason, 'bad-perms', 'Windows:无权限位 ⇒ 强制判定必红(判据有牙)') } else { assert.strictEqual(forced.ok, true) assert.strictEqual(forced.permsChecked, true, 'POSIX:0600 ⇒ 通过且已判定') } rmSync(dir, { recursive: true, force: true }) }) test('F6-b 双 epoch 过渡:窗口内两种密文都能解;超窗口 ⇒ epochExpired + 拒', () => { const kOld = makeKey(13) const kNew = makeKey(14) const retired = new Date(Date.now() - 60_000).toISOString() const cipher = new ContentCipher({ groupKey: 'ops|relay', epoch: 2, key: kNew, previous: [{ epoch: 1, key: kOld, retiredAt: retired }], graceMs: 10 * 60 * 1000, }) const old1 = new ContentCipher({ groupKey: 'ops|relay', epoch: 1, key: kOld }) const blob = old1.encryptBlock(Buffer.from('上一代的内容', 'utf8')) assert.strictEqual(cipher.decodeBlock(blob).toString('utf8'), '上一代的内容', '窗口内旧 epoch 必须能解') assert.strictEqual(cipher.counters().epochExpired, 0) // 超窗口(graceMs 小于已退休时长) const expired = new ContentCipher({ groupKey: 'ops|relay', epoch: 2, key: kNew, previous: [{ epoch: 1, key: kOld, retiredAt: retired }], graceMs: 1000, }) assert.strictEqual(expired.decodeBlock(blob), undefined) assert.strictEqual(expired.counters().epochExpired, 1) assert.strictEqual(expired.counters().decryptRejected, 1) // 写入一律用**新** epoch(旧 epoch 只解不写) assert.strictEqual(cipher.epoch, 2) assert.deepStrictEqual(cipher.epochs(), [2, 1]) }) test('S1 组密钥凭据:签名者签发 ⇒ 验签通过;篡改 epoch ⇒ 失败关闭', () => { const signer = generateAuthorityKey() const k = makeKey(15) const doc = { version: CONTENT_CIPHER_VERSION, network: 'ops', group: 'relay', epoch: 1, keyId: keyIdOf(k), issuedAt: new Date(0).toISOString(), } const sig = signPayloadWith(signer.privateKeyPem, groupKeyCredentialPayload(doc)) const okv = verifyGroupKeyCredential(doc, sig, [signer.publicKey]) assert.strictEqual(okv.ok, true) assert.strictEqual(okv.doc.epoch, 1) // 篡改 epoch ⇒ 验签必失败(载荷覆盖全部字段) const bad = verifyGroupKeyCredential({ ...doc, epoch: 2 }, sig, [signer.publicKey]) assert.strictEqual(bad.ok, false) assert.strictEqual(bad.reason, 'signature-mismatch') // ⛔ 不可验 = 不接受(受信签名者为空) assert.strictEqual(verifyGroupKeyCredential(doc, sig, []).reason, 'no-trusted-keys') // 🔴 载荷内**不含密钥本体**(本单红线:中继只广播三元组) const payload = groupKeyCredentialPayload(doc) assert.ok(!payload.includes(k.toString('base64'))) assert.ok(payload.includes('keyId=' + keyIdOf(k))) }) test('口径守卫:探针 OBS-23 的键表必须与 crypto 模块**同源**(防"改了模块没改探针")', () => { const probeSrc = readFileSync(new URL('../scripts/overlay-probe.cjs', import.meta.url), 'utf8') const m = /const CRYPTO_NUM_KEYS = \[([\s\S]*?)\]/.exec(probeSrc) assert.notStrictEqual(m, null, '探针里必须能找到 CRYPTO_NUM_KEYS') const inProbe = [...m[1].matchAll(/'([a-zA-Z]+)'/g)].map((x) => x[1]) assert.deepStrictEqual(inProbe, [...CONTENT_CRYPTO_COUNTER_KEYS], '探针键表与模块键表必须逐字一致') }) test('F5/E1 口径:加密前后"回源份数"不变(同组 N 次取用 ⇒ 全部 local 命中、零回源)', async () => { const cipher = new ContentCipher({ groupKey: 'ops|relay', epoch: 1, key: makeKey(16) }) const rt = new ContentRuntime({ network: 'ops', group: 'relay', cipher, storeMaxBytes: 32 * 1024 * 1024 }) const pack = makeBytes(1024 * 1024 * 2 + 11, 88) let missing = 0 for (let i = 0; i < 4; i += 1) { const put = rt.putContent(pack) for (const id of put.plan) { const got = await rt.source.fetch(id) if (got === undefined) missing += 1 } } assert.strictEqual(missing, 0) assert.strictEqual(rt.snapshot().source.local, 12, '4 轮 × 3 块,全部 local 命中(= E1 的"回源 1 份"口径)') assert.strictEqual(rt.snapshot().store.puts, 3, '同内容只入库 3 个块(去重)') assert.strictEqual(rt.snapshot().crypto.decryptRejected, 0) }) // ───────────────────────────────────────────────────────────────────────────── // 组 G(序㊻ · C 域分离):块 id 的 **per-network keyed hash** // // 🔑 这一组是 `OBS-29` 的**离线等价体**(探针那条判据在同一批函数上跑)。 // 两条腿**必须同时有**(缺一即视为未覆盖): // · **正腿** = 同字节 + **不同 network** ⇒ 块 id **不同**; // · 🔴 **负腿** = **去掉 per-network 维度**(`netKey` 取同值 / 取空)⇒ 同一谓词**必红**。 // ⛔ 只给正腿 = 会漏掉"去了重"(把去重也一起干掉照样全绿); // ⛔ 没有负腿 = "上了个**无效的**域分离"会全绿 —— 本线老病根(装了但没生效 = 静默放行)。 // ───────────────────────────────────────────────────────────────────────────── /** C 的取数夹具:**同一把组密钥** + 不同 network ⇒ 只有 network 维度在变。 */ function keyingFixture(keySeed = 21) { const cipher = new ContentCipher({ groupKey: 'ops|content', epoch: 1, key: makeKey(keySeed) }) const bytes = makeBytes(3 * 1024 * 1024 + 7, 5) // 4 块(⛔ 不用整块数 —— 边界块也一起验) // ⚠️ **同网复用同一个 runtime**(缓存):既省事,也让"同网 ⇒ 同一套 id / 同一份存储"这一 // 语义在夹具层面就成立(`E1` 重取需要"同一个网内重复取用")。 const cache = new Map() const rtOf = (network) => { if (!cache.has(network)) cache.set(network, new ContentRuntime({ network, group: 'content', cipher })) return cache.get(network) } return { cipher, bytes, rtOf } } /** 🔴 正腿的**谓词本体**(放在函数里 ⇒ 正/负两腿断言的是同一个谓词,⛔ 不是两份写法)。 */ const idDiffersAcrossNetworks = (planA, planB) => planA[0] !== planB[0] test('G1 🔴 正腿:同字节 + 不同 network ⇒ 块 id 必须不同(且两侧都不是裸哈希)', () => { const { bytes, rtOf } = keyingFixture() const a = rtOf('ops').planContent(bytes).ids const b = rtOf('u:1').planContent(bytes).ids const bare = planOf(bytes).ids assert.strictEqual(idDiffersAcrossNetworks(a, b), true, '不同 network ⇒ 块 id 必须不同(域分离生效)') assert.notStrictEqual(a[0], bare[0], '⚠️ A 网不得等于裸哈希 —— 否则 = 域分离没生效("装了没生效"的本形)') assert.notStrictEqual(b[0], bare[0], '⚠️ B 网不得等于裸哈希(同上)') assert.strictEqual(a.length, 4, '4 块:正腿必须覆盖多块而不是只比第一块') for (let i = 0; i < a.length; i += 1) { assert.notStrictEqual(a[i], b[i], `第 ${i} 块:跨网必须不同`) } }) test('G1-b 正腿(id 函数直测):blockIdOf / contentIdOf 的 netKey 维度必须起作用', () => { const k = makeKey(21) const b = makeBytes(4096, 5) assert.notStrictEqual(blockIdOf(b, deriveBlockIdKey(k, 'ops')), blockIdOf(b, deriveBlockIdKey(k, 'u:1'))) assert.notStrictEqual(contentIdOf(b, deriveBlockIdKey(k, 'ops')), contentIdOf(b, deriveBlockIdKey(k, 'u:1'))) }) test('G2 🔴 正腿:同 network + 同字节 ⇒ 块 id 必须相同(**去重不得丢**)', () => { const { bytes, rtOf } = keyingFixture() const rt1 = rtOf('ops') const rt2 = rtOf('ops') assert.strictEqual(idDiffersAcrossNetworks(rt1.planContent(bytes).ids, rt2.planContent(bytes).ids), false) assert.deepStrictEqual(rt1.planContent(bytes).ids, rt2.planContent(bytes).ids, '同网同内容 ⇒ 计划逐字一致') // 去重仍然成立:同一份内容入库两次 ⇒ store 只入 4 个**唯一**块(3 整块 + 1 边界块) const put1 = rt1.putContent(bytes) const put2 = rt1.putContent(bytes) assert.deepStrictEqual(put2.plan, put1.plan, '同网重复写 ⇒ 计划 id 逐字一致(去重必须还在)') assert.strictEqual(rt1.snapshot().store.puts, 4, '同内容只入库 4 个块(⛔ 域分离不得把去重干掉)') assert.strictEqual(rt1.snapshot().store.putRejected, 0) }) test('G3 装配面:域分离启用后 store 两处复算 / 链取回 / 重组位三条路都必须过(= 调用点无漏改)', async () => { const { bytes, rtOf } = keyingFixture() const rt = rtOf('ops') const put = rt.putContent(bytes) // ① store 写侧复算(漏改 ⇒ 这里必然抛 / putRejected 涨) assert.strictEqual(rt.snapshot().store.putRejected, 0, 'store 写侧复算必须过') // ② 生产路径:优先级链取回(local 档命中 → 唯一解密点) const got = await rt.fetchContent(put.plan) assert.ok(got !== undefined && got.equals(bytes), '经优先级链取回必须逐字节等于原内容') assert.strictEqual(rt.snapshot().store.corruptReads, 0, 'store 读侧复算必须过') // ③ 工具路径:重组位(`chunker#reassemble` 也要同一个 netKey) const stored = new Map() for (const id of put.plan) stored.set(id, rt.store.get(id)) const back = await rt.reassembleContent(stored, put.plan) assert.ok(back !== undefined && back.equals(bytes), '重组位必须逐字节等于原内容(⛔ 漏 netKey ⇒ 必抛校验失败)') // ④ D7 闸门:**跨网**取回的块(块本身完好)必须被丢弃 —— 域不同 ⇒ id 不等 ⇒ 不算命中 const other = rtOf('u:1') const putOther = other.putContent(bytes) assert.notDeepStrictEqual(putOther.plan, put.plan, '两个域的块 id 必须不同') assert.strictEqual(rt.store.has(putOther.plan[0]), false, '跨域的块 id 不得在本地命中') }) test('G4 回滚路径:缺省 / 空 netKey ⇒ 回落裸 sha256(与序㉔ 逐字一致)', () => { const b = makeBytes(4096, 5) const bare = createHash('sha256').update(b).digest('hex').slice(0, BLOCK_ID_HEX_LEN) assert.strictEqual(blockIdOf(b), bare, '缺省必须恰为裸 sha256 前 32 hex') assert.strictEqual(blockIdOf(b, Buffer.alloc(0)), bare, '⚠️ 空 netKey 必须**等价于**不传(回滚路径)') assert.strictEqual(contentIdOf(b, Buffer.alloc(0)), bare) // ⛔ 反向:非空 netKey **必须**离出裸哈希(否则"回落"与"生效"不可分 ⇒ 判据无牙) assert.notStrictEqual(blockIdOf(b, Buffer.alloc(4, 0xab)), bare, '非空 netKey 必须走 HMAC 分支') // 不启用组密钥的 runtime ⇒ 恒回落裸哈希(= 缺省不启用 / 回滚路径成立) const plain = new ContentRuntime({ network: 'ops', group: 'content' }) assert.strictEqual(plain.netKey, undefined) assert.strictEqual(plain.blockIdKeyId, undefined) assert.deepStrictEqual(plain.planContent(b).ids, planOf(b).ids, '不启用 ⇒ 计划逐字等于裸哈希口径') }) test('G5 🔴 负腿「netKey 取同值」:去掉 per-network 维度 ⇒ 正腿谓词必红(具名 flat-key-collapses)', () => { const { bytes, rtOf } = keyingFixture() // 正腿:两个**不同** network assert.strictEqual( idDiffersAcrossNetworks(rtOf('ops').planContent(bytes).ids, rtOf('u:1').planContent(bytes).ids), true, '正腿基线:不同 network ⇒ 谓词必须为真', ) // 🔴 负腿:把 network 维度**去掉**(两侧取同一个 network ⇒ 同一把域密钥) const flatA = rtOf('ops').planContent(bytes).ids const flatB = rtOf('ops').planContent(bytes).ids assert.strictEqual( idDiffersAcrossNetworks(flatA, flatB), false, '⚠️ flat-key-collapses:域密钥取同值 ⇒ 谓词**必须为假**(判据有牙;若这里为真说明判据根本没看 network)', ) // ⚠️ 且此时**仍然是"带密钥"的路径**(不等于裸哈希)—— 排除了"负腿其实是回落裸哈希"的混淆 assert.notStrictEqual(flatA[0], planOf(bytes).ids[0], '负腿必须走 HMAC 路径(⛔ 不是回落裸哈希)') }) test('G6 🔴 负腿「netKey 取空」:回落裸哈希 ⇒ 同一正腿谓词必红(具名 empty-key-falls-back-to-bare-hash)', () => { const b = makeBytes(4096, 5) const k = deriveBlockIdKey(makeKey(21), 'ops') // 谓词本体在**函数级**再跑一遍(与 G1/G5 同一个谓词形状) const differs = (x, y) => x !== y assert.strictEqual(differs(blockIdOf(b, k), blockIdOf(b, k)), false, '取空(两侧同一把钥)⇒ 必为假') assert.strictEqual(differs(blockIdOf(b), blockIdOf(b)), false, '两侧都不传 ⇒ 必为假') // ⛔ 反向:只把**一侧**取空 ⇒ 谓词为真(说明"取空"确实会改变取值 ⇒ 判据对 netKey 敏感) assert.strictEqual(differs(blockIdOf(b, k), blockIdOf(b)), true, '一侧带钥一侧不带 ⇒ 必须不同') // ⚠️ 取空与生效**必须可分**:否则这条负腿没有判别力 assert.notStrictEqual(blockIdOf(b, k), blockIdOf(b)) }) test('G7 派生:确定性 + 只由 (组密钥, network) 决定 + 换钥必变 + 不等于密钥本体', () => { const k = makeKey(21) assert.strictEqual(deriveBlockIdKey(k, 'ops').length, KEY_LEN, '派生钥必须是 32 B(HMAC-SHA256 输出)') assert.ok(deriveBlockIdKey(k, 'ops').equals(deriveBlockIdKey(k, 'ops')), '确定性:同钥同网 ⇒ 同一把') assert.ok(!deriveBlockIdKey(k, 'ops').equals(deriveBlockIdKey(k, 'u:1')), 'network 维度必须起作用') assert.ok(!deriveBlockIdKey(k, 'ops').equals(deriveBlockIdKey(makeKey(22), 'ops')), '换组密钥 ⇒ 派生钥必变') assert.ok(!deriveBlockIdKey(k, 'ops').equals(k), '⛔ 派生钥不得等于组密钥本体(否则"派生"是假的)') assert.strictEqual(typeof BLOCK_ID_DOMAIN_TAG, 'string') assert.ok(BLOCK_ID_DOMAIN_TAG.length > 0, '域标签必须非空(它进 HMAC ⇒ 换标签 = 全部块 id 换代)') }) test('G8 观测面:未启用 ⇒ 两键整体缺席;启用 ⇒ 只放指纹(⛔ 不许出现域密钥本体)', () => { const plain = new ContentRuntime({ network: 'ops', group: 'content' }) const sp = plain.snapshot() assert.ok(!('blockIdKeyed' in sp), '未启用域分离 ⇒ blockIdKeyed 必须**缺席**(⛔ 不补 false)') assert.ok(!('blockIdKeyId' in sp), '未启用域分离 ⇒ blockIdKeyId 必须缺席') const { rtOf } = keyingFixture() const rt = rtOf('ops') const s = rt.snapshot() assert.strictEqual(s.blockIdKeyed, true) assert.match(String(s.blockIdKeyId), /^[0-9a-f]{16}$/, '指纹必须是 16 hex') assert.strictEqual(s.blockIdKeyId, keyIdOf(rt.netKey), '指纹必须 = keyIdOf(域密钥)') // 🔑 跨机口径一致(47 / 106 必须逐字相同;不同 ⇒ 跨机取块全判校验失败) assert.strictEqual(rtOf('ops').blockIdKeyId, rtOf('ops').blockIdKeyId, '同钥同网 ⇒ 口径指纹必须一致') assert.notStrictEqual(rtOf('ops').blockIdKeyId, rtOf('u:1').blockIdKeyId, '不同网 ⇒ 指纹必须不同') // 🔴 泄漏守卫:整份 /status 文本里不得出现域密钥本体(hex / base64 两种写法都扫) const text = JSON.stringify(s) assert.ok(!text.includes(rt.netKey.toString('hex')), '⛔ /status 不得出现域密钥(hex)') assert.ok(!text.includes(rt.netKey.toString('base64')), '⛔ /status 不得出现域密钥(base64)') }) test('G9 边界:域分离启用后"同内容不同域"的块**共享必然失效**(= 设计代价,⛔ 不是缺陷)', async () => { const { bytes, rtOf } = keyingFixture() const a = rtOf('ops') const b = rtOf('u:1') const putA = a.putContent(bytes) // B 域完全独立:既查不到 A 的块,也解不开 A 的密文(AAD 里就有 groupKey 的网名) assert.strictEqual(b.store.has(putA.plan[0]), false, '跨域不得命中') const gotB = await b.fetchContent(putA.plan) assert.strictEqual(gotB, undefined, '跨域取内容必须**整体失败**(⛔ 不许拼半截 / 不许静默回源代价)') }) test('G10 🔴 `E1` 基线**重取**(C 新 id 口径):同网重复取用 ⇒ 零增量回源;跨网 ⇒ 各 1 份', async () => { // ⚠️ `E1` 的**定义不重估**(回源字节 ≈ 1 份 × 组数)—— 只重取**基线读数**。 const { bytes, rtOf } = keyingFixture() const A = rtOf('ops') const putA = A.putContent(bytes) const hit0 = A.snapshot().source.local for (let i = 0; i < 4; i += 1) { const got = await A.fetchContent(putA.plan) assert.ok(got !== undefined && got.equals(bytes), '第 ' + i + ' 轮取回必须逐字节等于原内容') } const s = A.snapshot() assert.strictEqual(s.source.local - hit0, putA.plan.length * 4, '4 轮 × 4 块 ⇒ 全部 local 命中') assert.strictEqual(s.source.origin, 0, '⚠️ 稳态回源必须 0("1 份"由首次写入承担 —— 新口径下不变)') assert.strictEqual(s.store.puts, putA.dedupIds.length, '同内容只入库**去重后**的块数') assert.strictEqual(s.store.putRejected, 0) // 跨网:B 网必须**各自一份**(id 不同 ⇒ 天然不去重 —— 这正是 C 的域收窄点) const B = rtOf('u:1') assert.strictEqual(B.snapshot().source.local, 0, 'B 网起点零命中(= 各自回源 1 份)') const putB = B.putContent(bytes) assert.notDeepStrictEqual(putB.plan, putA.plan, '两个网的块 id 序列必须不同') const gotB = await B.fetchContent(putB.plan) assert.ok(gotB !== undefined && gotB.equals(bytes), 'B 网自己那一份必须能取回') assert.notStrictEqual(putB.dedupIds[0], putA.dedupIds[0], '跨网不得共享块(= 设计意图,⛔ 不是缺陷)') })