Files
dsh_shenxian/test/overlay-content.test.mjs
T
admin d2ef362a98 feat(overlay): 覆盖网络线 序㊾ —— 探针观测面改「两台中继并集」(附 序㊽ 源码/文档补提交)
序㊾(本棒):
- scripts/overlay-probe.cjs:OBS-01 / OBS-08 / OBS-09 的数据源由「只读 47 中继」
  改为「按两台中继取并集」,消除 worker 归属漂移时的假红 / 假 SKIP
  · endpoints 以 network:hostId:port 为键合并,online 取「或」、localPort 取在线那一侧
  · used 按 network/hostId 去重计数(不求和,避免凭空放大在册数)
  · localPort 属中继机回环落点 ⇒ 按归属分机探活(106 侧落点由 106 机上探)
  · derived(OBS-11)保持 47 视角;阈值与判据一律未放宽
  · OBS-16 计数约束:对 47 /status 的读取仍为三次、Δ 只取 47 的 counters;
    对端 106 的采样为独立一次,落在第三次采样之后,不进 (status2, status3] 门窗口
  · 新增 --peer-status-fixture(并集的对端那一半)与「并集不可取证」强制留痕
- 交接单《覆盖网络-序45-低熵块治理-测熵与实现》§16 全节(§8 前前缀逐字未变)
- 参数表 §11.16 补记(§10 现算指纹未变,值格未动)

附(前几棒已完成并已部署、但尚未入仓的源码 / 文档):
- src/net/relay/content/*.ts、src/net/relay/index.ts、main.ts:块级寻址 C 域分离
- src/supervisor/orchestrator.ts、src/worker/agent.ts:日志采集与巡检(方案 C)
- test/overlay-content.test.mjs:随附用例(npm test = 200 pass / 0 fail / 1 skipped,Node 22)
- scripts/dshlog.mjs(跨机日志取证)、scripts/overlay-entropy.cjs(熵探针)
- dsh-server-docs/04-调整方案/129、133;INDEX.md / docs-manifest.json / 交接单 README 登记
2026-09-19 05:35:35 +08:00

1049 lines
55 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 覆盖网络线 · 序㉔「内容分发(块级内容寻址)」单测。
*
* ⚠️ 本文件**不在** `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], '跨网不得共享块(= 设计意图,⛔ 不是缺陷)')
})