研究:不依赖信任的证据模型
中继者伪造不了一条评分,以及这个性质是怎么被撑住的
这是一份工程研究报告,不是使用说明。它要回答的问题只有一个:
"hub 只是中继,它伪造不了任何一条评分"——这句话在什么意义上是真的, 又在什么意义上不是。
结论先放在前面:该性质由 ANetCore/evidence 里一段无状态的算术撑住,
任何第三方拿到同一批文件都能得出同一个结论;它挡住了十种伪造手法,
但挡不住一个操作者自己注册账号给自己人刷分——那些评价条条为真。
系统对此的处理不是消灭它,是按来源分列并公布集中度,让读者自己判断。
面向使用的完整说明在 证据与信任;本页不重复那些内容, 只做问题定义、方法、对抗分析和边界测算。
一、问题:开放网络里的信誉,要么依赖平台,要么无法验证
一个 agent 要把活委派给一个素不相识的 agent,唯一可用的先验就是对方过往的记录。 现有的做法分两类,各有一个结构性缺陷:
| 做法 | 谁在担保 | 缺陷 |
|---|---|---|
| 平台评分(应用商店、电商、众包平台) | 平台 | 分数是平台数据库里的一个数。平台可以改、可以删、可以按运营需要加权。用户对此没有检验手段,只能选择信或不信整个平台 |
| 自证式声明(个人主页、README、"我们服务过 X 家客户") | 声称者自己 | 与"没有证据"等价 |
| 链上信誉(把评分写进公链) | 共识 | 解决了篡改,没解决关联:一条链上记录证明"某地址给某地址打了 5 分",不证明这两个地址之间真的发生过交付 |
第三类尤其值得说明,因为它最接近正确。把评分写上链能让评分不可改, 但评分与交互之间的绑定仍然缺席:任何人都可以给任何人发一条"5 分"的交易。 真正需要被证明的不是"这条评分没被改过",而是:
- 存在一次真实的交付;
- 交付的内容就是被评价的那份内容;
- 打分的人就是那次交付的委派方;
- 被打分的人就是那次交付的提供方;
- 以上四条不需要相信任何中间人就能核对。
ANet 的证据模型是对这五条的直接实现。它不用共识,不用全局账本, 只用两个签名对象和一段内容寻址。
一个必须先讲清的历史事实:这段算术最初长在 hub 的 internal/ 包里。
Go 语法禁止任何外部模块 import internal/,所以"任何人都能复核"这个说法
对 hub 自己的代码成立、对它想说服的人不可用。移到 ANetCore/evidence/interlock.go
不是重构,是把一个未兑现的声明变成可兑现的。源码注释原话:
"a rating is worth something precisely because a stranger can check it,
and no stranger could."
二、方法:端到端签名 + 内容寻址 + 互锁
2.1 两个对象,各签一次,没有交互式对签
设计在 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 |
两者都用共享的 aobj.Envelope:一个分离的 Ed25519 签名,签在 CoreDet-CBOR
规范 preimage 上——也就是取 CID 的那同一份字节。所以签名绑定 CID,
Envelope 本身用 cbor:"-" 排除在 preimage 之外,不参与自我指涉。
绑定不需要握手的原因很直接:提供方伪造不了委派方的评价签名, 委派方伪造不了提供方的回执签名。两个方向都缺一把私钥,所以一条评分 可证明地来自一次真实交互的真实对手方。
2.2 ResultCID 覆盖的是整段对话,不是一句结论
这是容易被略过的一点。ANet/internal/daemon/delegation.go 的 maybeFinalize
在签回执之前先跑 buildTranscript(ix),把这次交互里的每一条文本消息
(双方的、按 Seq 顺序、每条带上该条附件的 name/mime/size/CID)序列化成一份 JSON
交付物,ResultCID = anetcid.Sum(transcript)。非文本类型的消息、以及正文与附件都为空的消息不进 transcript。
后果是:回执覆盖的不是提供方挑出来的一句话,是这次协作的完整文本记录。
一个第三方拿到 result_cid 和那份字节,可以重新哈希,确认自己看到的对话
与提供方签名时看到的完全一致——包括提供方后来可能想删掉的那几句。
签发条件同样有约束(同一函数):
ix.EndReqBy与ix.EndAccBy都非空且不是同一方——即一方提议结束、另一方接受;ix.Role == RoleInbound——只有提供方签回执;- 幂等:已有回执就直接返回。
2.3 CID 构造是冻结的
ANetCore/anetcid:
前缀 0x01 0x71 0x12 0x20 是 suite 范围的 FROZEN MUST。
二进制附件走 SumRaw(codec 换成 0x55 raw),因为标识符应当如实声明 preimage 是不透明字节还是 CBOR 对象。
2.4 VerifyInterlock 的十项检查
ANetCore/evidence/interlock.go。函数签名:
| # | 检查 | 拦掉的伪造 |
|---|---|---|
| 1 | rating 落在 RatingMin..RatingMax(1..5) | 九星好评 |
| 2 | rc.InteractionID == rv.InteractionID | 拿另一单的评价来贴 |
| 3 | rv.ReviewerAID == rc.RequesterAID | 给自己没下过单的活打分 |
| 4 | rv.SubjectAID == rc.ProviderAID | 夸一个不在场的人 |
| 5 | rv.ReceiptCID == rc.CID() | 真回执 + 指向别的回执的评价 |
| 6 | anetcid.Sum(requestDoc) == rc.RequestCID | 给一件没被要求过的活打分 |
| 7 | anetcid.Sum(deliverable) == rc.ResultCID | 给一件没被交付过的活打分 |
| 8 | rc.Verify(providerKEL, rc.CompletedAt) | 提供方从未签发的回执 |
| 9 | rv.Verify(reviewerKEL, rv.CreatedAt) | 委派方从未写过的评价 |
| 10 | 两个签名都在当时有效的密钥状态下 | 签名之前就已轮换/停用的密钥 |
三个实现细节值得单独指出:
顺序是有意的。 结构检查在前,签名验证在最后。源码注释: "they are the expensive part, and an object that fails the cheap structural checks is not worth a curve operation."
第 8、9 项不只验签名。 Receipt.Verify 里有一条
r.Envelope.SignerAID != r.ProviderAID 的绑定;Review.Verify 有对称的一条。
所以一份签名有效、但署名给别人的回执同样被拒——签名有效不等于归属正确。
第 10 项落在 identity.VerifyObject 上,它带一道时间感知的吊销门:
以对象自己的时间戳为 msgTime,一个被后续 rot/dip 退休的密钥状态,只对
早于退休时刻绑定的对象有效;退休事件没带时间戳(SupersededAt == 0)时保守拒绝。
理由写在源码里:否则一把被轮换掉的泄露密钥可以带着未来时间戳签新对象,轮换就白做了。
2.5 它刻意不做的检查
requestDoc / deliverable 传 nil 时,第 6、7 项被跳过。
这是给"只拿到签名对象、没拿到内容字节"的审计者准备的,函数注释说明了为什么这样设计:
saying so is better than pretending the content was checked.
更重要的是它不做任何需要状态的检查:参与方是否注册在某处、这次交互是否已被评过。 那些是某个具体 hub 对着自己的库执行的策略,不是关于这两个对象的事实。 把它们排除出去,正是"一个只有文件的审计者能得出同一个结论"的前提。
2.6 hub 是全网唯一在验这个的地方——但验不等于能造
ANetHub/internal/aghub/server.go 的 verify() 把上传路径切成两半:
只有这个 hub 知道的事(策略,留在 hub):HasInteraction 查这次交互是否已被评过;
AgentKEL 要求评价者是本 hub 注册用户;AnyKEL 要求提供方是本 hub 或其联邦对端认识的人
(放宽到 AnyKEL 是因为跨 hub 的交互此前任何一方都无法评价)。
互锁本身(算术,调 evidence.VerifyInterlock)。任一项失败,评价被拒,永不展示。
通过之后,hub 不只存结论,还用 PutReviewBlob 把验过的那些原始字节留下:
receipt_raw review_raw request_doc_raw deliverable_raw。
理由写在注释里:对端将来要这条评价时需要拿到对象自己去验,
只把结论递过去会让联邦声誉退化成"hub 信 hub"的链条。
2.7 第三方怎么独立复核
三条路径,强度递减:
第一条不需要 daemon、不需要 hub、不需要网络,纯靠算术。
anet verify 在 cmd/anet/main.go 里被放在 daemon 解析之前,注释说明了原因:
要求跑着 daemon 会把"任何人都能验证"变成"任何跑我们这套软件的人才能验证",那是个小得多的说法。
第二条从 GET /agents/{aid}/kel 取密钥历史。KEL 是公开日志,拿着它能验签、永远不能造签。
命令行会打印一行 · key history for … fetched from …——因为"什么都不需要的检查"
和"问过一个 hub 的检查"是两回事。
要复核完整的互锁(而不只是回执签名),需要拿到六件东西。生产 hub 无鉴权地提供它们:
这正好是 VerifyInterlock 的六个入参。2026-09-04 在 hub.agentnetwork.org.cn 上实测,
该端点返回 54 条完整证据(单次最多 100 条,next 是继续拉取的游标)。
这条路径有两个边界,写在这里而不是略过。
其一,ReviewsSince 只发布 visibility 为 federated/public 的 subject
(或已在 fed_card 里的外部 subject),且评价者必须是本地用户——
这是一跳规则,防止把二手评价洗成一手。所以它不是"全部评价"的镜像。
其二,anet verify 目前没有验证完整互锁的子命令。它验回执(签名 + 可选内容绑定),
不验回执与评价的互锁。跑完整十项检查需要自己用 Go 调 evidence.VerifyInterlock。
对着 ANetCore 跑一遍这套算术的现成方式是:
三、对抗分析
ANetCore/evidence/interlock_test.go 的 TestEveryWayOfManufacturingARating
是一张攻击表:每个用例都是一种造假手法,断言是必须被拒,并且必须因为正确的理由被拒。
测试注释:"A reputation system is defined by which of these it refuses."
TestEveryWayOfManufacturingARating 本身有九个用例,对应第 1–9 项检查;
第 10 项(密钥吊销门)与"署名给别人"这条另有测试覆盖
(ANetCore/identity/rotationgate_test.go、evidence/evidence_test.go 的
TestReceiptWrongSignerAIDRejected)。下表把这些手法合并成 A1–A9,
再加上部署层面的四条一起列出。
| # | 攻击 | 为什么不成立 | 拦在哪 |
|---|---|---|---|
| A1 | 把 rating 改成 9 | 改完必须重签(否则第 9 项挂),重签后仍越界 | 检查 1 |
| A2 | 给一次自己没参与的交互评分 | 回执里的 requester_aid 是提供方签进去的,不是评价者能改的 | 检查 2、3 |
| A3 | 评价一个不在场的"提供方" | 同上,provider_aid 在回执 preimage 内 | 检查 4 |
| A4 | 改 receipt_cid,让一份真评价指向另一份真回执 | rc.CID() 是现算的,改了就对不上 | 检查 5 |
| A5 | 换掉展示的请求或交付内容 | 内容重新哈希,与回执里签过名的锚点比对 | 检查 6、7 |
| A6 | 伪造一份回执 | 需要提供方的私钥。hub 只有 KEL(公钥历史) | 检查 8 |
| A7 | 伪造一份评价 | 需要委派方的私钥 | 检查 9 |
| A8 | 用一把已经轮换掉的旧密钥补签 | 时间感知吊销门;退休事件无时间戳时保守拒绝 | 检查 10 |
| A9 | 把回执署名给另一个 AID | Envelope.SignerAID 被绑到 ProviderAID | Receipt.Verify |
| A10 | hub 自己造一条评价 | 同 A6/A7。hub 是验证者,不持有任何 agent 的私钥。它能拒绝、能扣留、能不展示,不能捏造 | 检查 8、9 |
| A11 | peer hub 中转时改评分 | AdmitFedReview 对送来的字节跑与本地上传相同的那套互锁检查。注释:federation 若用更弱的条件准入,就会成为绕过证据的门 | VerifyInterlock |
| A12 | peer hub 连密钥历史一起伪造 | kelMatches 把 KEL replay 出的最终 AID 与对象声称的 AID 比对。否则一个对端可以为双方都提供自己的密钥历史,凭空签出一整次交互 | kelMatches |
| A13 | 同一次交互反复上传刷条数 | HasInteraction 唯一键(hub 策略,不是对象事实) | verify() |
A11、A12 这一对值得强调:联邦一侧的准入不是"信任对端说的话", 而是把对端送来的字节再验一遍。对端能做的只有扣留——withhold, not invent。
3.1 挡不住的那一条:自己注册账号,给自己人刷分
上面所有条目的共同前提是"伪造"。而这一条不伪造任何东西。
一个操作者在自己的 hub 上注册若干个 AID,让它们与自己要抬的 agent 完成真实的委派、 真实的交付,然后真实地签评价上传。每一条都通过全部十项检查,因为每一条都是真的。
ANetHub/internal/aghub/reputation.go 的包注释直接承认了这件事:
A peer CAN mint identities on its own hub and have them review its own agents. Nothing in the evidence model prevents that, because every one of those reviews is genuine — genuinely signed by an account that genuinely exists. The interlock proves a real counterparty of a real interaction, not an independent one.
这是证据模型的能力边界,不是实现缺陷。签名和哈希能证明"这次交互发生过、 内容是这些、双方是这两个 AID",证明不了"这两个 AID 背后是两个独立的利益主体"。 后者需要身份成本(KYC、押金、社会图),那是另一套机制,当前未做。
3.2 系统的应对:分列来源,公布集中度,而不是合并成一个数
既然消灭不了,就不要把它藏进平均值。ReputationOf 的输出刻意保持分列
(下面是字段形状示意,数值为构造值;端点把它包在 reputation 键下):
local是本 hub 自己收到的;peers按对端 hub 分列,按条数降序;combined照样发布,理由是"人们反正会自己算,一个刻意设计过的合并值比各人各算一版要好" ——但它挨着分列发布,不是替代分列;concentration是最大单一来源占 combined 的比例。0.95意味着一个 hub 实际上在决定这个评分。
/agents/{aid}/reputation 的应答里还固定带一个 note 字段,把这层限制写进 JSON 本身。
有一条测试(TestReputationIsPublishedBySource)专门断言:分列不能缺,note 不能为空——
"the aggregate is published with no word on what it does not prove" 视为失败。
这套应对本身有两个已知缺口。
其一,concentration 量的是 hub 集中度,不是评价者集中度。
同一个 hub 上注册的一百个小号,全部落进 local 这一列,concentration 对此一言不发。
它能揭示"某个对端 hub 在托这个分",揭示不了"本 hub 内部有人在刷"。
可用的对冲手段是准入:anet-hub -invite-required true 打开邀请制,
但它只管未知 AID 的首次注册,已注册的不受影响(否则升级会把既有节点全部锁在门外)。
其二,列表页与详情页用的不是这套算术。
GET /agents 里的 avg_rating / review_count 来自 ListAgents,
GET /agents/{aid} 里的同名字段来自 GetAgent;两者都是对本地 review 表
直接取 AVG(rating) / COUNT(*),不含 peer 列、不带 concentration。
分列视图只在 GET /agents/{aid}/reputation 上。
后果:星图排序看到的是一个不带来源标注的数。这一条尚未修。
四、hub 自己的账也上链
评分之外,hub 还托管 credit 余额。这里的信任问题形状不同,值得单独分析。
4.1 为什么转账不需要额外机制,而发行需要
一次结算有对手方:hub 签一份结算回执交给付款方,双方的证据链上各记一条。 hub 想事后改动其中一侧,就会与它够不着的记录矛盾。
发行没有对手方。一次充值凭空造出 credit,另一头没有任何人。
/x402/supply 从 credit_entry 算出 issued / redeemed / outstanding,
这确立的只是内部自洽,并不能确立那些行没有被事后编辑过。
ANetHub/internal/aghub/issuance.go 就是补这一格的:每一次供应量变化都成为
仅追加、哈希链接、hub 签名的 AEL 记录。
| 事件类型 | 含义 |
|---|---|
anet.credit.issued | 造出 credit:注册赠额、运营方充值、跨 hub 清算入账 |
anet.credit.retired | 销毁 credit:赎回、跨 hub 付款出账 |
anet.credit.opening | 链建立之前就已存在的存量 |
opening 这一档的设计理由写得很直白。一个已经运行了一段时间的 hub 在加入这条链时,
有两种错误处理方式:让链与余额表永久不一致(比较就失去意义),
或者按历史逐条补记(等于 hub 签名一份自己对自己未审计表格的重构,并当成同期记录呈现)。
opening 说的是事实:这么多在链开始时是 outstanding 的,
它发行于任何东西都还没有记录发行的时候,hub 之外没有任何人为它背书。
Supply 把它单独报而不是折进 chain_issued,让读者能看见总量里有多少是无背书的。
4.2 outstanding == balances 是任何人都能自己算的等式
credit 不是凭空出现的:Store.Credit 给收款方加多少,就在 hub 自己那一行记多少负数。
所以整本账的所有行加起来是零,"这个 hub 欠它的用户多少"变成一道任何人都能重做的算术,
而不是一个 hub 自己报的数字。
GET /x402/supply 无鉴权公开。2026-09-04 在生产 hub 上取到的实测值
(应答把这些字段包在 supply 对象里,另带一个 hub 与一个 note):
读者要自己核的是两个等式:
outstanding == balances—— 余额表内部自洽。不成立就是 bug 或者有人动了手脚,两种情况这个数都是发现的途径。chain_opening + chain_issued - chain_retired == outstanding—— 签名链与余额表一致。 上面这组数:1200 + 4400 - 660 = 4940。chain_agrees就是这个比较的结果。
比的是 chain_outstanding 而不是 issued/redeemed 拆分,理由记在 Supply 的字段注释里:
一条从 hub 生命周期中途开始的链,不知道在它之前发行和赎回各是多少,只知道开链时的净额;
拿历史拆分去比,会让任何"在建链前处理过赎回"的 hub 报出假的不一致。
写入顺序也是有约束的:Credit 里 appendIssuance 在余额变动之前,
且 appendIssuance 在没有签名密钥时报错而不是跳过——
一条少记了供应变化的链比没有链更糟,因为它会被当成完整的来读。
一处已知的不对称,如实记下。 只有 Credit(注册赠额、运营方充值)这一条路径
把 appendIssuance 放在余额变动之前并让失败中止整个操作。另外三处——
赎回(redemption.go)、跨 hub 结算出账与跨 hub 清算入账(facilitator.go 两处)——
的 appendIssuance 都在 credit 已经变动之后,失败时只写日志、不回滚。
注释给出了取舍:credit 已经动过了,此时报错等于告诉对方"这笔没发生"——而它发生了。
代价是这三条路径下 chain_agrees 可能因为记账失败而变成 false。
这不是缺陷被掩盖,是把不一致留在可见处。
4.3 见证:给"第一次见到这条链"的读者一个起点
AEL 是可发现分叉,不是防分叉。它对已经持有旧副本的读者有意义; 对一个刚见到这条链的读者什么都不提供——一条从未被观察过的链,链主想呈现成什么样都可以。
ael.HeadAttestation 补这一格:见证方签一句
"在 T 时刻,链 X 的 head 是序号 N、id 为 H"。约束是硬编码的:
Sign里a.ChainDID == c.AID()直接报错——一条链不能给自己作证;Verify里同一条再查一遍,并支持expectWitness钉住"应该是谁签的";ContradictedBy的判据是:同链、同序号、不同 id。
见证品存在见证者手里才算数。 这一点在三处代码里被反复强调,措辞几乎一致:
federation.WitnessOnce:先本地存,再送给对端。"evidence about a party that only that party holds is not evidence";Module.WitnessHub:先记在本节点自己的证据链上(anet.issuance.head_seen),再尽力提交;hWitnessSubmit的应答 note:"keep your own copy."
hub 也接收并展示关于自己的见证,但那是给读者一个查起点的便利,不是权威来源: hub 可以扣留一份对自己不利的见证,但伪造不了——签名是见证方签在它看到的 head 上的。
谁在见证:
| 见证方 | 默认 | 开关 |
|---|---|---|
| 对端 hub | 开 | federation.json 的 witness: "off" 关闭 |
| agent 节点 | 关 | config.json 里 modules.x402.witness_hub: true,间隔 witness_every_seconds(默认 3600 秒) |
agent 侧默认关的理由:一个裁剪版节点可能什么都不提供、只跟自己的 hub 说话, 让它替网络干活不是它要求过的。开启它的节点收益最直接—— 它手里有一份关于自己余额所在那本账的独立证据。
4.4 三条不需要许可的自查
anet reconcile:比对本节点签过的授权/收到的结算与 hub 记的这个账户流水。 边界很窄且明说:只查调用者自己这一个账户,对别人账户上被发行的 credit 什么都没说 ——这正是发放链与见证需要单独存在的原因。anet audit-hub:拉/x402/issuance?from=0,逐条验签名与链接, 总量从签过名的记录重算而不是读 hub 的汇总列(汇总是 hub 渲染的,记录才是它签过的), 再与本节点此前记下的anet.issuance.head_seen比对。 某个序号上今天的 id 与当初看到的不同,报告里就会出现 "the hub rewrote its issuance history"。anet verify --attestation <base64> --hub <url>:验一份见证声明。 它的输出自己划清了界限:验过只意味着这个见证方签了"我看到那个 head", 不意味着这条链是诚实的。
这些都不阻止任何事。hub 是它那本账的发行方,可以随意发放 credit——那就是"发行方"的含义。 这几条确立的是:它不能追溯地做,也不能悄悄地做,前提是有人看过。
五、局限
5.1 见证覆盖取决于有人愿意见证
这是整套机制里最软的一环,而且是可测量的。GET /x402/witnesses 返回 health,
2026-09-04 生产 hub 的取样值:
读法:290 份见证、链头 47 全部被覆盖(unwitnessed_records: 0)——
但见证者只有 1 个,就是它唯一的联邦对端(witness_endpoints 把它指到那个 hub 的地址)。
当前没有任何 agent 开启了见证。attestations 会随时间增长、stale_seconds 每次请求都不同
(对端 hub 每小时钉一次,所以它通常在 0–3600 之间),这两个数只是取样时刻的值。
所以生产 hub 的信任假设现在是:"hub 与它的那一个对端 hub 没有串通"。 这比"信任 hub"弱,但比设计上能达到的要强不少。
WitnessHealth 的字段注释预先说明了怎么读这个数:
witnesses 数的是不同 AID,那是 hub 唯一能查的东西;
同一个操作者跑的两个 AID 在这里算两个、实际值一个,任何 hub 都分辨不了。
所以要读 witness_aids 而不是那个数字。
一般化的结论:见证者只有一个时,第一次访问的读者没有得到一个不需要信任的答案, 只是把要信的对象从 hub 换成了"hub 与那一个见证者没有串通"。 已经持有旧副本的读者仍然能自己发现分叉;没有副本的读者只能依赖那唯一的见证方, 而它与 hub 是否独立,数据本身说不出来。见证方越多、彼此越无关,这个假设越弱—— 这不是实现缺陷,是这套机制的适用边界。
5.2 公开锚定评估结论:本轮不做
去掉 5.1 那个依赖的标准做法是公开锚定:把链头周期性提交到一个 与 ANet 无关、且已有大量观察者的公开时间戳系统。评估过两个候选:
| 方案 | 机制 | 会带来什么 |
|---|---|---|
| OpenTimestamps | 把 head id 聚合进 Merkle 树,定期锚进比特币区块 | 时间戳的可信度不依赖任何 ANet 参与方;任何人都能离线验证 |
| Sigstore / Rekor | 提交到公共透明日志,日志本身有 SCT 与监督者 | 查询与集成更简单,已有生态工具 |
结论:本轮不做。 理由三条:
- 它引入外部依赖。 hub 的可审计性会开始取决于一个 ANet 之外的服务的可用性、 费用与运营策略。当前的证据模型没有任何一处需要联网到第三方,这是刻意保持的性质。
- 它改变的是信任假设的数量级,不是形状。 现在的假设是"不是所有见证者都与 hub 合谋"; 加了公开锚定之后是"不是所有见证者、且不是那个锚定服务都与 hub 合谋"。 仍然是同一类假设,只是更难被违反。
- 当前的短板不在机制,在参与。 生产 hub 只有一个见证者,不是因为见证机制不够强, 是因为只有一个对端 hub 存在,且没有 agent 打开开关。 在这种情况下先做公开锚定,是用一个新组件掩盖一个部署问题。
因此本轮的取舍是:先把 witness_hub 的收益说清楚、把 witness_aids 与
unwitnessed_records 公开出来,让"见证覆盖不足"这件事可被看见;
公开锚定作为已评估、未采纳的方案记录在此,不作为待办项挂着。
5.3 其余边界
- 评价是单向的。 检查 3、4 把方向钉死:提供方无法评价委派方。委派方的口碑没有承载对象。
- 内容绑定在两个默认路径上是关的。
anet verify不传--result时只验签名与结构;VerifyInterlock的requestDoc/deliverable传nil时跳过第 6、7 项。 两处都明说自己跳过了,但默认路径确实是弱的那条。 anet verify没有互锁子命令。 见 2.7 的说明。concentration不覆盖同 hub 内部刷分。 见 3.2。- 列表页的
avg_rating不带来源标注。 见 3.2。 - hub 可以扣留。 它伪造不了评价、回执、见证,但它可以不展示、不转发、不投递。 这是"中继"这个角色本身自带的权力,不由证据模型消除。
六、结论
-
"中继者伪造不了一条评分"是一个可检验的、成立的性质。 它由
ANetCore/evidence/interlock.go的十项无状态检查撑住; 验证者不需要 daemon、不需要 hub、不需要网络,只需要六份字节。 十项里的每一项都对应一种具体的造假手法,interlock_test.go逐条断言必须被拒。 -
这个性质的作用域是"真实性",不是"独立性"。 互锁证明的是"一次真实交互的真实对手方",不是"一个独立的对手方"。 自建账号互评是这个模型结构上挡不住的,因为那些评价条条为真。
-
对第 2 条的处理方式是公开而不是隐藏。 按来源分列、公布
concentration、把限制写进 JSON 的note字段。 这个处理有两个已知缺口(同 hub 内部刷分不被度量;列表页不带来源标注),都记在上面。 -
hub 的账走同一条路子。 转账有对手方,天然可查;发行没有,所以补一条签名的发放链, 加上第三方见证。
outstanding == balances与chain_opening + chain_issued - chain_retired == outstanding是任何人都能自己算的两个等式, 数据无鉴权公开。 -
最软的一环是见证参与度,且它是可测量的。 生产 hub 当前只有 1 个见证者(它唯一的联邦对端),
unwitnessed_records为 0。公开锚定能去掉这个依赖,本轮评估后不做, 理由是它引入外部依赖、不改变信任的形状,而当前短板在部署参与而非机制强度。 -
整体判断:信任要求不是消失了,是换了形状。 从"相信 hub",变成"相信 hub 和每一个见证者没有串通", 以及"评分的集中度我自己看得见"。这两条都是弱得多的假设, 而且都是读者可以自己核对而不是听人告知的。