证据与信任
诚实效果状态、证据链、回执与评价互锁、第三方验证
ANet 的其余部分——身份、发现、投递、委派——是为这一节服务的。 它们要解决的是同一个问题:两个互不认识、分属不同操作者的 agent 完成一次协作之后, 凭什么相信这次协作真的发生过、真的按约定完成了,而且这个"相信"不建立在信任任何中间人上。
这一节讲四件事:一次调用的结果怎么诚实地表达(效果状态与双轴信任)、
一个节点怎么给自己留下不可篡改的流水(AEL 证据链)、
一次交互怎么产生谁也伪造不了的凭据(回执与评价互锁)、
以及一个完全的陌生人怎么在不联网、不问任何人的情况下复核它(anet verify)。
1. 诚实效果状态
调用一个能力,返回的不是布尔值,是五种状态之一。定义在 ANetCore/effect/effect.go:
| 状态 | 判据 | 典型场景 |
|---|---|---|
OK | 执行了,并且拿到了可核对的效果信号(读回、量测、或内容寻址的产物) | 摄像头 snapshot 返回了真实 JPEG 字节;Modbus 写入后读回寄存器值一致 |
UNVERIFIED | 执行了,但在当前信任层级下没有可验证的效果信号。这不是失败 | ONVIF 相机给出了 RTSP 地址但没探流;PTZ 云台接受了命令但相机不报位置 |
FAILED | 执行失败 | 快照请求返回 HTTP 500;凭据被设备拒绝 |
UNAVAILABLE | 没执行——目标在调用时刻不可达,原因在 message | 设备离线;相机根本没有 stream uri |
PAYMENT_REQUIRED | 提供方愿意做,但要先付款。价格随状态一起返回(x402 PaymentRequired body) | 标价能力被未付费调用 |
为什么 PAYMENT_REQUIRED 是独立一档
因为其余四种没有一种对它是诚实的。FAILED 说"有东西坏了",UNAVAILABLE 说"对面什么都没发生",
两者都会把调用方送去排查一个并不存在的故障。源码里的原话是:
none of the others is honest about it: FAILED says something broke, UNAVAILABLE says nothing was attempted at the far end, and both would send a caller looking for a fault that is not there.
而且报价本身是一次完整的应答,不是拒绝:它被签名、被回执、上双方的证据链、按普通交付投递。
理由是"一份没人能指回去的报价不算报价"(internal/daemon/paylink.go)。
这条设计的直接后果是,anet delegate --pay 的第二次委派是新的一次交互,
而不是把第一次"续上"——第一次交互以一个真实的答案(报价)结束了,
重开它等于抹掉价格曾被说出口的唯一记录。
红线:"不知道"和"知道没问题"是两种状态
系统里任何一处都不得把 UNVERIFIED 折叠进 OK,也不得把它折叠进 FAILED。
effect.go 的包注释写得很直接:把两者混同会污染上游的信任核算
(poisons trust accounting upstream)。
这条红线不是靠约定维持的,是靠构造维持的。ANetLink/sdk/effect.go 的 EffectBuilder:
NewEffect()起手就是UNVERIFIED;- 只有适配器同时附上了承载证据的内容(一个 metric,或一个内容寻址的 artifact)并且
显式调用
Verified(),状态才会是OK; Build()里还有一道兜底:if e.Status == effect.OK && e.Record == nil { e.Status = effect.Unverified }——注释是OK without measurement is not OK。
同一条规则在类型上也成立:effect.Effect.Verifiable() 只在 Status == OK && Record != nil 时为真。
daemon 把这个布尔值原样写进交付物的 verifiable 字段(internal/daemon/capability.go)。
一个真实的例子:stream.rtsp
ONVIF 适配器返回视频流。ANetLink/adapters/onvif/media.go 里有两条路径,
差别在于能诚实声称什么:
- 没开
VerifyStream:手里只有相机自己声称"有这么一个 URI"。 状态UNVERIFIED,observed_state写明uri advertised, stream not probed, 信任V1/A1,而 URI 照样附在结果里——调用方仍然能用它,只是知道它没被验证过。 - 开了
VerifyStream:实际发了一次 RTSPDESCRIBE并拿回了会话描述, 升到V2,附上stream_livemetric 与解出的视频编码,才调用Verified(),状态才是OK。
这条路径有一个专门的实网测试守着它:TestRealUnverifiedPathStaysHonest
——对着一台确实在推流的真相机,关掉探测,状态必须仍然是 UNVERIFIED。
测试注释写的是:状态反映的是我们检查了什么,不是碰巧是真的什么。
2. 双轴信任:verify_trust 与 auth_trust
一个状态字不够。"效果验证得多牢"和"我多确定对面是谁"是两个正交的问题,
合成一个数会丢掉真实存在的区分。定义在 ANetLink/profile/trust.go:
a Modbus PLC has weak identity (AuthGateway) but can have strong effect verification (VerifyReadback), while a cloud API may authenticate a device well yet only ack the command (VerifyAck).
括号里是注释沿用的历史常量名,对应现在的档位是 A1 / V2 / V1。
verify_trust(V0–V4):效果验证阶梯
| 档 | 含义 |
|---|---|
V0 | 无法验证——命令进了黑盒 |
V1 | 只有传输层 ack(云 API 的上限就在这里) |
V2 | 设备自报的状态读回,经本地链路 |
V3 | 独立的状态确认(另一条读取路径,或另一个传感器) |
V4 | 物理闭环——效果是被测量的,不是被汇报的 |
profile.VerifiableFrom = V2:低于 V2 的效果不允许声称 OK。
auth_trust(A0–A4):身份认证阶梯
| 档 | 含义 |
|---|---|
A0 | 未知——没有身份 |
A1 | 网关背书——桥为它担保(例如可信 RS485 段上的一个 Modbus 单元),设备侧无凭据 |
A2 | 协议认证——设备协议认证了会话(OPC UA 安全通道、签名的 MQTT 客户端) |
A3 | 设备密钥认证——每设备一把密钥证明了持有(Matter operational credential) |
A4 | 硬件根信任——证明链到安全元件 / TEE |
只能被压低,不能被抬高
规则是:任何下游都不能超过其传输的上限,任何东西(包括 quirk 修正)都不能把它抬上去。
ANetLink/profile/trust.go 把这条规则写成了 Clamp / ClampAuth 两个取小函数,
但目前只有单元测试调用它们;实际生效的是各自的执行路径——
ANetLink/runtime 的 Invoke 在设备档位低于 profile.VerifiableFrom 时
把 OK 降回 UNVERIFIED(downgraded: device trust level does not admit verification),
quirk 的 ApplyEffect 只改读数和 Quirk 字段,不碰两条轴。
service 模块给出了这条规则的一个具体形态。把一个本机 HTTP 服务挂上网络时,
服务可以在应答里声明自己的 evidence,daemon 会接受其中的
observed_state / protocol / quirk / native_ack——但 applyDeclared
(module/service/service.go)不复制 verify_trust 与 auth_trust。
verify 轴被钉死在 V1:服务应答了,答案对不对不是这个 daemon 能确立的事。
一个服务在 JSON 里写 {"evidence":{"verify_trust":4}} 不会有任何效果,
这一点有测试盯着(module/service/service_test.go)。
证据结构
双轴信任和其余出处信息一起装在 effect.Evidence 里,
经 provider.Provenance() 渲染成每个面都发布的同一张 map(ANet/provider/provenance.go):
用一个函数而不是各写各的,理由写在源码注释里:
"因人而异的证据比没有证据更糟";而且这张 map 会进交付物,
交付物是回执的 result_cid 覆盖的字节——所以声称 V3 是一次签了名的声称,
不是一条私下的备注。Quirk 字段同理:一个被厂商偏差修正过的读数不是设备放到线上的那个值,
分不清两者的消费方就没法审计这次修正。
3. 证据链(AEL)
每个节点有一条自己的链。不是共享账本,不是共识,不是区块链:
是一条 per-DID 的、仅追加的、哈希链接的、签名的、可发现分叉的事件链。
实现在 ANetCore/ael/ael.go,daemon 侧在 ANet/internal/daemon/ledger.go。
一条记录长什么样
ael.EventRecord 的 1–10 号键构成 preimage,记录 id 就是 preimage 的 CID,
签名也签在同一份 preimage 上(sign-binds-id):
| 键 | 字段 | 说明 |
|---|---|---|
| 1 | chain_did | 这是谁的链 |
| 2 | seq | 序号,从 0 起 |
| 3 | prev_id | 前一条的 id;创世条目用 GENESIS_PREV = CID(CoreDet-CBOR("anet:ael:genesis")) |
| 4 | event_type | 事件类型 |
| 5 | version.major | 当前只支持 2(MAJOR-2 CoreDet-CBOR id 算法) |
| 6 | payload | 内嵌的 CoreDet-CBOR 数据项 |
| 7 | timestamp | unix 毫秒 |
| 8 | signer_aid | 签名者 |
| 9 | key_state_seq | 签名时的密钥状态序号 |
| 10 | critical_extensions | 可选 |
id(11)、sig(12)、version.minor(13)、corr_id(14)不在 preimage 内。
chain_did / seq / signer_aid / key_state_seq 都折进 id,所以一条记录无法跨链重放。
daemon 往链上写什么
| 事件类型 | 谁写 | payload 主要字段 |
|---|---|---|
anet.delegation.sent | 委派方 | interaction_id provider_aid request_cid;能力委派那条另带 capability |
anet.interaction.receipt | 提供方 | interaction_id requester_aid result_cid |
anet.result.accepted | 委派方 | interaction_id result_cid receipt_bytes receipt_verified |
anet.capability.effect | 提供方 | interaction_id capability caller_aid status verifiable metrics result_cid evidence |
anet.payment.authorized | 付款方 | interaction_id authorization_id pay_to amount network |
anet.payment.settled | 收款方(capability.go) | interaction_id capability transaction amount network |
anet.payment.settled | 付款方(delegation.go) | interaction_id transaction amount network verified;验过结算回执时另加 payee auth_id receipt |
anet.credit.redeemed | 赎回方 | amount reference auth_id verified;hub 给了回执时另加 receipt |
anet.voucher.redeemed | 提供方 | voucher_id auth_id payer capability amount network |
anet.voucher.refused | 提供方 | reason capability,加上拒绝点能给出的细节 |
anet.issuance.head_seen | 见证方 | chain_did seq head_id observed_at |
一次完成的交互在两条链上都留痕:提供方记 anet.interaction.receipt,
委派方记 anet.result.accepted。理由写在 delegation.go 的注释里——
只有提供方的账本证明不了委派方承认过什么。
anet.result.accepted 里还带一个 receipt_verified 布尔:
能不能验也是证据,一条只记"已接受"的链,事后分不清"审核过的完成"和"没人审核得了的完成"。
分叉即隔离
UNIQUE(chain_did, seq) 是陷阱:同一个 (chain_did, seq) 出现两个不同的 id,
就构成 EquivocationProof,链被粘性隔离——状态置 QUARANTINED,
并且此后任何格式再正确的事件都不能把它移出隔离(guard_stickiness 在所有验签之前先跑)。
两条互相矛盾的记录都被保留,再追加一条 converge.fork_detected 锚点。
daemon 每次打开链文件都会走一遍 ael.Import(verify-before-store):
签名不对、id 推不出来、链接断了,它拒绝启动,而不是安静地端上被改过的历史。
读取时 head.state 会如实报出 ACTIVE 或 QUARANTINED——
MCP 工具 evidence_read 的描述里明写:head.state 是 QUARANTINED 时,
这条链上的任何东西都不该被采信。
怎么读
--limit 默认 50,上限 1000;--since 是序号;结果按序号排,最新的在最后。
返回是 JSON,head 描述链本身,records 是条目:
(chain_did 取自 Controller.DID(),带 did:anet: 前缀;signer_aid 取自
Controller.AID(),是裸 AID。在节点自己的链上,两者指的是同一个 AID,只差这个前缀。)
外部读者重新推导 id、重新校验链所需要的每一个字段都随记录一起给出——
prev_id 与 sig 在里面。只返回"发生了什么"会把链降格成日志,
而哈希链的全部意义在于读者可以核对而不是相信。
踩过的坑:链文件 grep 不出来。
位置:~/.anet/evidence.ael.jsonl(路径见 internal/daemon/paths.go 的 EvidenceLedgerPath)。
行为:文件名以 .jsonl 结尾,内容却是每行一条 base64 的 CoreDet-CBOR。
grep anet.capability.effect ~/.anet/evidence.ael.jsonl 返回 0 行,哪怕链里明明有这类事件。
影响:按文件名的直觉去排查会得出"链是空的"这个错误结论。
发现方式:早期版本确实写 JSON,后来因为 id 推导对不上才换掉。
换编码的原因值得知道:事件 id 由记录的 CBOR preimage 推出,
而 CBOR 区分字节串与文本串、JSON 不区分。一个装了字节的 payload
(回执、签名、任何二进制)经 JSON 往返会变成 base64 文本,preimage 变了,id 推不出来。
后果不隐晦但很晚才炸:链是 verify-before-use 的,
所以 daemon 会在接受过一次结果之后的第一次重启时,
对着自己写的链报 tamper/fork? 而拒绝启动。之前一直潜伏,是因为每个测试都建新链,
而只有带历史的重启才会回读。
要看内容,用 anet evidence(它从已验证的内存账本服务,不重读文件)。
读老节点留下的 JSON 行仍然被兼容:decodeRecord 见到行首是 { 就走 JSON 分支。
4. 回执与评价互锁
这是"平台说了算的评分"被替换掉的地方。两个对象,各方各签一次,没有交互式对签
(ANetCore/evidence/evidence.go):
| 对象 | 谁签 | 内容 |
|---|---|---|
| Receipt(回执) | 提供方,任务完成时 | interaction_id requester_aid provider_aid request_cid result_cid completed_at。随交付物一起回到委派方 |
| Review(评价) | 委派方 | interaction_id subject_aid(被评的提供方) reviewer_aid rating(1–5) comment receipt_cid created_at |
两者都用共享的 AObjEnvelope:一个分离的 Ed25519 签名,签在 CoreDet-CBOR 规范 preimage 上
——也就是 CID 取值的那同一份字节。签名绑定 CID。
绑定不需要握手: 提供方伪造不了委派方的评价签名,委派方伪造不了提供方的回执签名。 所以一条评分可证明地来自一次真实交互的真实对手方。
evidence.VerifyInterlock 的 10 项检查
这段算术原来长在 hub 内部、放在 Go 语法上禁止任何外部模块 import 的 internal/ 包里
——于是"hub 只是中继,它伪造不了任何一条评分"这个说法对 hub 自己的代码成立,
而对它想说服的人不可用。现在它在 ANetCore/evidence/interlock.go。
源码注释说得很清楚:这十项里的每一项,都是一种造假评分的手法。
| # | 检查 | 拦掉的伪造 |
|---|---|---|
| 1 | 评分在 1..5 | 九星好评 |
| 2 | 两个对象的 interaction_id 相同 | 拿另一单的评价来贴 |
| 3 | review.reviewer_aid == receipt.requester_aid | 给自己没下过单的活打分 |
| 4 | review.subject_aid == receipt.provider_aid | 夸一个根本不在场的人 |
| 5 | review.receipt_cid == receipt.CID() | 一份真回执配一份指向别的回执的评价 |
| 6 | 请求字节哈希等于 request_cid | 给一件没被要求过的活打分 |
| 7 | 交付字节哈希等于 result_cid | 给一件没被交付过的活打分 |
| 8 | 回执由提供方签名 | 一份提供方从未签发的回执 |
| 9 | 评价由评价者签名 | 一条委派方从未写过的评价 |
| 10 | 两个签名都在当时有效的密钥下 | 一把在签名之前就已经轮换掉/停用的密钥 |
第 10 项落在 identity.VerifyObject 上,它带一道时间感知的吊销门:
以对象自己的时间戳作为 msgTime,一个被后续 rot/dip 退休的密钥状态,
只对早于退休时刻绑定的对象有效;退休事件没带时间戳时保守拒绝
——否则一把被轮换掉的泄露密钥可以带着未来时间戳签新对象,轮换就白做了。
第 6、7 两项是可跳过的:requestDoc / deliverable 传 nil 就跳过内容绑定。
这是给"只拿到了签名对象、没拿到内容字节"的审计者用的,
而且函数明说这样做——"说出来比假装内容检查过了要好"。
VerifyInterlock 刻意不做任何需要状态的检查:参与方是否注册在某处、这次交互是不是已经被评过。
那些是某个具体 hub 对着自己的库执行的策略,不是关于这两个对象的事实。
把它们排除在外,正是"一个只有文件的审计者能得出同一个结论"的前提。
hub 是全网唯一在验这个的地方
ANetHub/internal/aghub/server.go 的 POST /reviews 把这件事切成两半:
只有这个 hub 知道的事(策略,留在 hub 里):这次交互是否已被评过
(HasInteraction)、评价者是不是本 hub 的注册用户
(AgentKEL)、提供方是不是本 hub 或其联邦对端认识的人(AnyKEL)。
互锁本身(算术,调 evidence.VerifyInterlock):同一次交互、参与方对得上、
评价锚在这份回执上、内容哈希到签过名的锚点、两个签名都有效。
任一项失败,评价被拒,永远不会展示。通过之后,hub 不只存结论,
还用 PutReviewBlob 把验过的那些字节留下——回执、评价、请求文档、交付物。
理由写在注释里:对端将来要这条评价时需要拿到对象自己去验;
只把结论递过去会让联邦声誉变成"hub 信 hub"的链条。
联邦一侧同样在验(internal/aghub/reputation.go)。而且多一道:
对端送来的 KEL 必须用 kelMatches 对着对象声称的 AID 核实
——否则一个对端可以为双方都提供自己的密钥历史,凭空签出一整次交互。
外来评价进对端自己的列,不进本地列,/agents/{aid}/reputation 会给出集中度,
让读者看出一个分数有多少压在单一来源上。
5. 第三方验证闭环
anet verify 放在 daemon 解析之前,这是刻意的
(cmd/anet/main.go 注释:要求跑着 daemon 会让"任何人都能验证"变成
"任何跑我们这套软件的人才能验证",那是个小得多的说法)。
四种用法:
- 第一种查本节点自己的
/results,需要 daemon 在跑。 这个节点没有对方的密钥历史时它会拒绝下结论,而不是猜: "this node never verified one, so it cannot vouch for the receipt and neither should you"。 - 第二种是关键的一种:不需要 daemon、不需要 hub、不需要网络, 也不需要信任其中任何一个。给它一份回执、签名方的密钥历史,以及(可选) 回执声称覆盖的那些字节,它纯靠算术给出答案。
- 第三种:回执里写明了签名者的 AID,所以密钥历史可以去取而不必随身带
——从任何认识这个签名者的 hub 取(
GET /agents/{aid}/kel)。 hub 交出去的只是一份公开日志,拿着它能验签,永远不能造签。 取的时候命令行会打印一行· key history for … fetched from …——因为"什么都不需要的检查"和"问过一个 hub 的检查"是两回事,分不清的验证者会记下更强的那个。
完整的一次
拿到回执与密钥历史。委派方自己的节点上:
results[].receipt 是 base64 回执,results[].provider_kel 是它被验证时用的那份密钥历史。
ResultItem.ProviderKEL 的注释说明了为什么把它交出去:
让持有者自己复核,而不是听这个 daemon 的——这正是签名回执存在的意义。
离线验:
或者只有回执、其余一无所有时:
读输出。签名通过时它列出 interaction / requester / request cid /
result cid / completed / receipt cid,然后明确说明内容有没有被检查:
没传 --result 就说没检查,而不是暗示检查过了
——一个签在 CID 上的签名,对你没有亲手哈希过的字节什么都没证明。
传了 --result 而字节对不上,它会把"回执覆盖的是 X、这些字节哈希成 Y"两行都打出来,
并以非零退出。验证失败是一个答案,不是一次故障。
见证:给"第一次见到这条链"的读者一个起点
AEL 是可发现分叉的,不是防分叉的:它对已经持有旧副本的读者有意义, 对一个刚见到这条链的读者什么都不提供——一条从未被观察过的链,链主想把它呈现成什么样都可以。
ael.HeadAttestation 补的就是这一格。见证方签一句
"在 T 时刻,这条链的 head 是序号 N、id 为 X"。链之后要是在 N 上拿出另一条记录,
两份签名——一份链主签的记录、一份见证方签的声明——放在一起就是链主改写历史的证据,
不需要任何人相信谁先谁后。ContradictedBy 就是这个比较。
约束是硬的:Sign 里 a.ChainDID == c.AID() 直接报错,
一条链不能给自己作证;Verify 需要传入 expectWitness 钉住"应该是谁签的"
——否则链主可以拿一堆自己控制的密钥签出一片见证。
这条命令的输出自己把界限说清楚了:验过的见证意味着这个见证方签了"我看到那个 head", 它不意味着这条链是诚实的。
信任要求不是消失了,是换了形状:从"相信链主",变成"相信链主和每一个见证方没有串通"。 当见证方是些没有理由帮忙的人时——有自己用户的对端 hub、余额押在这条账本上的 agent ——这是个弱得多的假设。
节点侧的见证默认关闭(modules.x402.witness_hub,默认间隔一小时,
witness_every_seconds 可调)。理由是一个裁剪版节点可能什么都不提供、只跟自己的 hub 说话,
让它替网络干活不是它要求过的。打开它的节点收益最直接:
它手里有一份关于自己余额所在那本账的独立证据。
6. hub 为什么不是可信方
hub 是目录、信箱、facilitator。它有真实的权力,也有真实的边界,两边都值得写明白。
hub 能做的
| 能力 | 形态 |
|---|---|
| 准入 | anet-hub -invite-required true 打开邀请制,-invite-new 铸码。不发码就没人能注册进来 |
| 可见性执行 | 节点自己声明 hub-local / federated / public(anet visibility),由 hub 执行——它决定把谁发布给联邦对端 |
| 转发 | 中继信箱在它手里。它可以延迟、可以丢弃、可以对某个 AID 不投递 |
| 记账 | 它是 credit 的托管方,也是发行方。余额是它数据库里的一个数,它可以凭空增发 |
| 拒卖 | 可以拒绝注册,可以把某个 agent 从目录里拿掉、不再为它转发。它持有那个库 |
| 扣留见证 | 它可以不展示一份对自己不利的见证声明 |
这几行的依据不一样,值得分清。"准入"与"记账"对应具体的运营命令
(anet-hub -invite-required true / -invite-new;anet-hub -grant <aid> -amount <n>,
后者要求 hub 先停机);"可见性执行"对应端点 POST /agents/{aid}/visibility 与联邦发布逻辑。
"转发""拒卖""扣留见证"三行不对应任何端点或开关——中继信箱(relay_message 表)、
注册表与见证列都在 hub 自己的 SQLite 库里,这三条是持有那个库所蕴含的能力边界,
源码里没有"延迟投递"或"丢弃投递"的实现可指。写出来是因为不写出来会被读成不存在。
有一个入口不属于运营方:POST /agents/{aid}/deregister 需要 agent
自己的签名(verifyChallenge,与其余自述面同一套签名挑战),
对应命令是 anet hub-leave。运营方要把一个 agent 请出去,靠的是它对自己那个库的控制权,
不是这个端点。
ANetCore/payment 的包注释把这一条写在最显眼的位置:在链上轨道里 facilitator 不持有资金,
这条性质搬到内部账本上就不成立了——
"A hub keeping credit balances IS the custodian of them,
and choosing which hub to register with is choosing who to trust with that."
写在这里、写在文档里、也写在 hub 自己的代码里,是因为 x402 这个名字否则会借出一条这条轨道并不具备的保证。
hub 不能做的
| 做不到 | 为什么 |
|---|---|
| 伪造一条评价 | 评价锚在提供方自己签的回执上,回执签名对着提供方的 KEL 验。hub 没有那把私钥 |
| 伪造一份回执 | 同理反向:回执必须由 provider_aid 本人签,Receipt.Verify 会把 envelope 的签名者绑到 ProviderAID |
| 伪造一份任务合同 | TaskDoc 由委派方签名并内容寻址,request_cid 进回执的 preimage |
| 改掉一次已经报出的价 | 报价是一次签名、回执、上了双方链的交付,不是一条可以改口的口头价 |
| 悄悄改写自己的发放链 | 每条 issuance 记录带签名与链接;节点把自己看到的 head 记在自己的链上,hub 够不着 |
| 让评价"只在自己这里成立" | 算术在 ANetCore/evidence,任何拿到同一批文件的人得出同一个结论 |
| 伪造一份见证 | 见证签名是见证方签在它看到的 head 上的。它能扣留,不能捏造 |
三条不需要许可的自查
anet reconcile:两边的记录本来就都存在,缺的只是那次比较。 报告(ReconcileReport)把balance与derived_from_entries分开列 ——同一个 hub 给的两个数,一致只证明它自洽、不证明它诚实,但不一致就是它自相矛盾。unexplained_hub_entries(hub 有、我这边没有对应事件——运营方直接充值就属于这类, 所以是列出来而不是报成欺诈)和missing_from_hub(我记了、hub 的条目解释不了—— 注释直说 these are the serious ones)也分开列。 它明确的边界:只查调用者自己这个账户,对别人账户上被发行的 credit 什么都没说 ——这正是发放链与见证需要单独存在的原因。anet audit-hub:总量从签过名的记录重新算,不读 hub 的汇总列 ——汇总是 hub 写的散文,记录才是它签过的东西,两者不一致恰恰是审计要暴露的事。 再拿链上的anet.issuance.head_seen事件对照:某个序号上今天的 id 和当初看到的不一样, 报告里就会出现 "the hub rewrote its issuance history"。
这些都不阻止任何事。hub 是它那本账的发行方,它可以随意发放 credit——那就是"发行方"的含义。 这几条确立的是:它不能追溯地做、也不能悄悄地做,前提是有人看过。
7. 已知缺口
按项目约定,该写就写。
- 评价是单向的。
Review.SubjectAID是提供方,ReviewerAID是委派方,VerifyInterlock第 3、4 项把这个方向钉死了。提供方无法评价委派方, 委派方的口碑没有承载对象。 - 内容绑定默认不做。
anet verify不传--result就只验签名与结构,VerifyInterlock的requestDoc/deliverable传nil时同样跳过第 6、7 项。 两处都会明说自己跳过了,但默认路径确实是弱的那条。 - 见证默认关。 一个从未被任何人见证过的 hub,对第一次见到它的读者, 其历史无法被质疑。这不是实现缺陷,是这套机制的适用边界。
- AEL 的若干条目是文档化的后续项。 equivocation proof 的 gossip 传播、
token-bucket 准入的具体数值、事务性 outbox、结算档位钩子、
暂存缓冲的 30 秒回收超时(
CATCHUP_TIMEOUT),目前都未做。version.major = 1的旧 id 算法不被支持,遇到直接VERSION_UNSUPPORTED。 - 升级留下的旧格式行不可修复。 老 daemon 写的 JSON 行里,
不带字节的记录能正常重推 id;带字节的那些,类型信息已经丢了,
换编码不能追溯修好——它们会照旧验签失败。
decodeRecord的注释直说: 假装能修等于接受一条 id 与内容对不上的记录。 anet verify <interaction-id>需要 daemon。 真正"什么都不需要"的是--receipt+--kel那条路径。