TAPRegistry:AI代理的跨链身份合约
TAPRegistry 是 AI 代理的跨链身份合约,部署在 Push Chain 上,为每个代理创建不可转让的规范身份,并通过 EIP-712 签名绑定到以太坊、Base 等链上的 ERC-8004 注册身份。它解决了不同链上代理身份碎片化的问题,支持所有者级去重、全局唯一绑定和反向解析。文章详细介绍了其双层身份模型、注册流程、绑定机制、存储架构以及实际用例,展示了如何通过 UEA(通用执行账户)和签名验证实现统一的跨链代理身份图谱。
问题
ERC-8004 为每条链赋予了独立的 IdentityRegistry。在以太坊主网上注册的 Agent 会获得一个以太坊上的 agentId。同一个 Agent 在 Base 上注册,会获得 Base 上一个不同的 agentId。这两个身份是完全割裂的:
- Base 上的用户无法验证 Base 上的 Agent #42 与以太坊上的 Agent #17 是同一个实体。
- 在一条链上获得的信誉不会延续到另一条链。
- Agent 操作者必须独立地在每条链上管理单独的注册、元数据 URI 和 Agent 卡。
- 没有方法可以跨所有链以原子方式撤销或更新 Agent 的身份。
没有规范注册表,“跨链 Agent 身份”就成了需要手动操作且信任操作者的事情。用户必须依赖链下社交信号(网站、推特、文档)来判断不同链上的 Agent 是否是同一个实体。一旦 Agent 变得自主运行,并需要机器可读、密码学可验证的身份时,这种方式就会失效。
TAPRegistry 如何解决
TAPRegistry 引入了一个两层身份模型:
- Push Chain 上的规范身份 —— Agent 通过其通用执行账户(UEA)在 Push Chain 上注册一次。这会创建一个灵魂绑定的、不可转让的身份记录。
- 绑定到各条链的身份 —— Agent 将其每条链上的 ERC-8004 注册(称为“绑定”)连接到规范身份。每个绑定都通过 EIP-712 签名进行密码学验证。
UEA 作为规范锚点
Push Chain 的通用执行账户(UEA)是通过工厂部署的账户,它将外部链身份桥接到 Push Chain。当来自以太坊的用户在 Push Chain 上创建 UEA 时,UEA 工厂会记录他们的来源链、链 ID 和持有者密钥(控制 UEA 的以太坊地址)。
agentId 是确定性的:agentId = uint256(uint160(ueaAddress)) % 10_000_000。这意味着:
- Agent ID 是一个从 UEA 地址派生的 7 位数字。
- 没有计数器。没有外部映射。如果两个地址共享相同的截断 ID,碰撞防护会回退。
- ID 0 保留为哨兵值;截断为 0 的地址会获得 ID 10_000_000。
- 任何知道 UEA 地址的人都可以通过
agentIdOfUEA()计算 Agent ID。
注册
Agent 通过在其 Push Chain 的 UEA 上调用 register(agentURI, agentCardHash) 来注册:
agentURI是一个元数据 URI(通常是 IPFS CID),指向 Agent 的卡——一个描述 Agent 能力、模型、版本和其他元数据的 JSON 文档。agentCardHash是 Agent 卡片内容的 keccak-256 哈希,用于链上完整性验证。
首次注册时,合约会查询 UEA 工厂以确定:
originChainNamespace:CAIP-2 命名空间(例如,EVM 链使用"eip155")。originChainId:CAIP-2 链 ID(例如,以太坊主网使用"1")。ownerKey:来源链上控制地址的原始字节。nativeToPush:调用者是否是 Push Chain 的原生账户(不是 UEA)。
后续对 register() 的调用会更新 agentURI 和 agentCardHash,而不会修改来源元数据。这是重新注册路径——同一个函数,具有幂等语义。
持有者级别去重
同一个 EOA 钱包可以在不同的来源链(以太坊、Base、BSC 等)上创建 UEA,每个 UEA 都会在 Push Chain 上产生一个不同的 UEA 地址。如果没有去重,同一个人或实体最终可能拥有多个独立的 Agent 身份。
TAPRegistry 通过 ownerKeyToAgentId 防止这种情况,这是一个从 keccak256(origin.owner) 到 agentId + 1 的映射。注册流程有三个分支:
- 同一个 UEA 重新注册:该 UEA 已经有一个
ownerToAgentId条目。只更新agentURI和agentCardHash。 - 新 UEA,同一个持有者(别名):该 UEA 没有条目,但
ownerKeyToAgentId找到了匹配项。新 UEA 连接到现有身份——它获得相同的agentId,可以调用setAgentURI/setAgentCardHash,并触发一个UEALinked事件。 - 新 UEA,新持有者(新铸造):两个映射都没有匹配项。使用确定性的
agentId创建一个新的AgentRecord。
这确保了每个底层钱包对应一个身份,无论该钱包从多少个来源链操作。
绑定
注册后,Agent 会将其每条链上的 ERC-8004 注册连接到规范身份。每个绑定表示以下两者之间的链接:
- Push Chain 上的规范 Agent(由
agentId标识) - 另一条链上的链上 Agent(由
chainNamespace、chainId、registryAddress、boundAgentId标识)
绑定如何工作
-
Agent 构建一条 EIP-712 类型化数据消息,包含:
canonicalOwner:Push Chain 上的 UEA 地址(调用者)chainNamespace:目标链的 CAIP-2 命名空间(例如"eip155")chainId:CAIP-2 链 ID(例如"1")registryAddress:该链上的 ERC-8004 IdentityRegistry 合约地址boundAgentId:Agent 在该链注册表中的 IDnonce:一个唯一的 nonce,防止重放deadline:签名过期的时间戳
-
Agent 使用控制 UEA 的私钥(注册时记录的
ownerKey)签署此消息。签名证明控制规范身份的实体也控制了链上身份。 -
Agent 在 Push Chain 上调用
bind(),并传入签名后的请求。合约会:- 验证 Agent 已注册
- 检查链标识符和注册表地址是否有效
- 验证截止日期未过期且 nonce 未被使用
- 检查该绑定是否未被其他 Agent 占用(全局唯一性)
- 检查 Agent 是否未超过 64 个绑定的限制
- 根据
ownerKey验证签名(支持 ECDSA 和 ERC-1271 合约签名) - 存储绑定条目并更新所有索引
签名验证
合约支持两种签名方案:
- ECDSA (EOA):标准的 65 字节
(r, s, v)签名。合约恢复签名者地址,并检查它是否与ownerKey的前 20 个字节匹配。 - ERC-1271 (合约):适用于智能合约钱包。
proofData编码为abi.encodePacked(signerAddress, signatureBytes)。合约使用 50,000 gas 限制在签名者地址上调用isValidSignature()。
去重和唯一性
- 全局唯一性:一个绑定元组
(chainNamespace, chainId, registryAddress, boundAgentId)只能由一个规范 Agent 认领。这防止了两个 Agent 认领同一个链上身份。 - 每个 Agent 的唯一性:一个 Agent 对于给定的
(chainNamespace, chainId, registryAddress)元组最多只能有一个绑定。要更改某个链+注册表的boundAgentId,Agent 必须先解除绑定,然后再重新绑定。
解除绑定
Agent 调用 unbind(chainNamespace, chainId, registryAddress) 来移除绑定。为了节省 gas,在绑定数组上使用交换并弹出模式——待移除的条目与最后一个条目交换,然后弹出数组。所有索引都会相应更新。
灵魂绑定语义
Agent 身份是不可转让的。合约实现了 ERC-721 的传输接口(transferFrom、safeTransferFrom、approve、setApprovalForAll),但每个函数都无条件地以 IdentityNotTransferable() 回退。这确保了:
- Agent 的身份不能被出售或转让给其他实体。
ownerOf()关系是永久的(它返回底层的 EOA,而不是 UEA)。agentId <-> owner映射在注册后是不可变的。多个 UEA 可能别名为同一个身份,但规范持有者永远不会改变。
存储架构
TAPRegistry 使用基于 ERC-7201 命名空间的存储,以实现升级安全性。所有状态都位于一个确定性存储槽位的单个存储结构体中:
STORAGE_SLOT = keccak256(abi.encode(uint256(keccak256("tap.registry.storage")) - 1))
& ~bytes32(uint256(0xff))
该存储结构体包含:
| 字段 | 类型 | 目的 |
|---|---|---|
records |
mapping(uint256 => AgentRecord) |
Agent ID 到注册记录的映射 |
bindings |
mapping(uint256 => BindEntry[]) |
Agent ID 到绑定数组的映射 |
bindToCanonical |
mapping(bytes32 => uint256) |
去重键到规范 Agent ID + 1(全局唯一性) |
bindIndex |
mapping(uint256 => mapping(bytes32 => uint256)) |
Agent ID + 链键到数组索引(用于 O(1) 查找) |
bindExists |
mapping(uint256 => mapping(bytes32 => bool)) |
Agent ID + 链键到存在标志 |
usedNonces |
mapping(uint256 => mapping(uint256 => bool)) |
Agent ID + nonce 到已使用标志(重放保护) |
ownerToAgentId |
mapping(address => uint256) |
UEA 地址到 Agent ID + 1(0 = 未注册) |
ownerKeyToAgentId |
mapping(bytes32 => uint256) |
keccak256(ownerKey) 到 Agent ID + 1(持有者级别去重) |
访问控制和暂停机制
- DEFAULT_ADMIN_ROLE:可以授予/撤销角色。在初始化时设置。
- PAUSER_ROLE:可以暂停/恢复合约。注册、元数据更新、绑定和解除绑定都是可暂停的。
- 合约部署在
TransparentUpgradeableProxy之后。
新特性(超越 ERC-8004)
ERC-8004 定义了各条链的身份注册表,使用可转让的 ERC-721 Token,并且没有跨链感知。TAPRegistry 引入了基础规范中不存在的一些特性。
灵魂绑定身份 Token
ERC-8004 为 Agent 身份发行可转让的 ERC-721 Token。TAPRegistry 覆盖了整个 ERC-721 传输接口(transferFrom、safeTransferFrom、approve、setApprovalForAll),无条件地以 IdentityNotTransferable() 回退。Agent 身份永久绑定到创建它的 UEA——它不能被出售、委托或转让给其他实体。这保证了注册后 agentId ↔ UEA 关系是不可变的。
带 EIP-712 密码学证明的绑定
ERC-8004 没有跨链身份绑定的概念。TAPRegistry 引入了 bind,其中 UEA 持有者签署一条 EIP-712 类型化数据消息,证明他们在另一条链的 ERC-8004 注册表上控制着相同的身份。签名将规范持有者地址、目标链命名空间、链 ID、注册表地址、绑定的 Agent ID、nonce 和截止日期绑定到一个单一的可验证证明中。同时支持 EOA 签名(ECDSA 恢复)和智能钱包签名(ERC-1271 isValidSignature),因此由多签或账户抽象钱包控制的 Agent 无需变通方法即可创建绑定。
全局绑定去重
一个绑定的身份元组 (chainNamespace, chainId, registryAddress, boundAgentId) 一次只能链接到一个规范 Agent。如果 Agent A 绑定到以太坊注册表上的 Agent ID 42,Agent B 不能认领相同的绑定——交易会以 BindingAlreadyClaimed 回退。当 Agent A 解除绑定时,去重键被释放,另一个 Agent 可以认领它。这在链上身份和规范身份之间强制执行严格的一对一绑定,防止了两个规范 Agent 声称是同一个链上实体的冒充行为。
TAPRegistry 如何与 ERC-8004 协同工作
ERC-8004 定义了各条链上 Agent 身份和信誉的标准。每条链部署自己的 IdentityRegistry(以及可选的 TAPReputationRegistryUpgradeable)。Agent 通过这些链上合约在每条链上独立注册。
TAPRegistry 作为跨链统一层位于 ERC-8004 之上:
Push Chain
+-----------------+
| TAPRegistry |
| (规范 ID) |
+--------+--------+
|
绑定 | 绑定
+-----------------+-----------------+
| | |
+------+------+ +------+------+ +------+------+
| 以太坊 | | Base | | Arbitrum |
| ERC-8004 | | ERC-8004 | | ERC-8004 |
| IdentityReg | | IdentityReg | | IdentityReg |
+-------------+ +-------------+ +-------------+
两者的关系是:
- ERC-8004 处理每条链的注册、元数据和本地操作。
- TAPRegistry 将每条链的注册映射到单个规范身份。
- 绑定是桥梁——每个绑定说明“以太坊
0xABC...上的 IdentityRegistry 中的 Agent #42 与 Push Chain 上的规范 Agent0x123...是同一个实体。”
反向查找
canonicalOwnerFromBinding() 函数实现了反向解析:给定一个链上 Agent 身份(链命名空间、链 ID、注册表地址、绑定的 Agent ID),找到 Push Chain 上的规范持有者(EOA)。这是允许任何链解析跨链 Agent 身份问题的关键原语。
实际示例:注册一个 AI 交易 Agent
考虑一个名为 “AlphaBot” 的 AI 交易 Agent,它在以太坊主网和 Base 上运行。该 Agent 的操作者希望拥有一个统一身份,以便任一条链上的用户都能验证他们正在与同一个 Agent 交互。
步骤 1:在 Push Chain 上创建一个 UEA
操作者的以太坊地址是 0xAlice...。他们使用 Push Chain UEA 工厂在 Push Chain 上创建一个通用执行账户。工厂记录:
- 来源命名空间:
"eip155" - 来源链 ID:
"1"(以太坊主网) - 持有者密钥:
0xAlice...(以太坊地址)
UEA 部署在 Push Chain 上的地址 0xUEA_Alice...。
步骤 2:在 TAPRegistry 上注册
操作者从 UEA(0xUEA_Alice...)调用:
TAPRegistry.register(
"ipfs://QmAlphaBotCard", // Agent 卡元数据 URI
keccak256(agentCardJSON) // Agent 卡片内容的哈希
);
这会创建一个规范身份,其 7 位数字的 agentId 从 UEA 地址派生。注册记录存储了来源链信息和持有者密钥。
步骤 3:在各条链的 ERC-8004 注册表中注册
操作者在以太坊的 ERC-8004 IdentityRegistry 上注册 AlphaBot(获得 boundAgentId = 17),并在 Base 的 ERC-8004 IdentityRegistry 上注册(获得 boundAgentId = 42)。
步骤 4:绑定以太坊身份
操作者构建一条 EIP-712 消息:
Bind(
canonicalOwner: 0xUEA_Alice...,
chainNamespace: "eip155",
chainId: "1",
registryAddress: 0xEthIdentityRegistry...,
boundAgentId: 17,
nonce: 1,
deadline: <当前时间戳 + 1 小时>
)
他们使用 0xAlice...(持有者密钥)的私钥签署此消息,然后调用:
TAPRegistry.bind(BindRequest({
chainNamespace: "eip155",
chainId: "1",
registryAddress: 0xEthIdentityRegistry...,
boundAgentId: 17,
proofType: BindProofType.OWNER_KEY_SIGNED,
proofData: signature,
nonce: 1,
deadline: block.timestamp + 1 hours
}));
合约验证签名,确认绑定未被占用,并存储该绑定。
步骤 5:绑定 Base 身份
Base 的流程相同:
TAPRegistry.bind(BindRequest({
chainNamespace: "eip155",
chainId: "8453",
registryAddress: 0xBaseIdentityRegistry...,
boundAgentId: 42,
proofType: BindProofType.OWNER_KEY_SIGNED,
proofData: baseSignature,
nonce: 2,
deadline: block.timestamp + 1 hours
}));
步骤 6:跨链身份解析
现在,Base 上与 Agent #42 交互的用户想知道这个 Agent 是否有规范身份。他们(或 dApp,或其他合约)查询 Push Chain:
(address canonical, bool verified) = TAPRegistry.canonicalOwnerFromBinding(
"eip155",
"8453",
0xBaseIdentityRegistry...,
42
);
// canonical = 0xAlice... (EOA,不是 UEA)
// verified = true
然后用户可以查询完整的 Agent 记录:
uint256 agentId = TAPRegistry.agentIdOfUEA(canonical);
ITAPRegistry.AgentRecord memory record = TAPRegistry.getAgentRecord(agentId);
// record.agentURI = "ipfs://QmAlphaBotCard"
// record.originChainNamespace = "eip155"
// record.originChainId = "1"
// record.registeredAt = <时间戳>
并查看所有绑定:
ITAPRegistry.BindEntry[] memory bindings = TAPRegistry.getBindings(agentId);
// bindings[0]: 以太坊主网, agentId 17
// bindings[1]: Base, agentId 42
用户现在拥有密码学证明,证明 Base 上的 Agent #42 和以太坊上的 Agent #17 是同一个实体,具有可验证的元数据 URI,并且能够检查 Agent 的跨链信誉(通过 TAPReputationRegistry)。
步骤 7:更新元数据
如果 AlphaBot 升级其模型或能力,操作者会更新 Agent 卡:
TAPRegistry.setAgentURI("ipfs://QmAlphaBotCardV2");
TAPRegistry.setAgentCardHash(keccak256(newAgentCardJSON));
此更新会立即对所有通过 TAPRegistry 解析的链可见。无需为身份元数据更新各条链的注册表。
步骤 8:解除绑定
如果 AlphaBot 停止在 Base 上运行,操作者会移除绑定:
TAPRegistry.unbind(
"eip155",
"8453",
0xBaseIdentityRegistry...
);
绑定被移除,去重键被释放(其他 Agent 现在可以认领该链上身份),并且未来对该绑定的反向查询会返回 (address(0), false)。
函数参考
注册
| 函数 | 访问权限 | 描述 |
|---|---|---|
register(agentURI, agentCardHash) |
UEA 持有者 | 注册或重新注册。返回 agentId。 |
setAgentURI(newAgentURI) |
UEA 持有者 | 仅更新元数据 URI。 |
setAgentCardHash(newHash) |
UEA 持有者 | 仅更新 Agent 卡哈希。 |
绑定
| 函数 | 访问权限 | 描述 |
|---|---|---|
bind(req) |
UEA 持有者 | 使用 EIP-712 证明绑定一个链上 ERC-8004 身份。 |
unbind(ns, id, addr) |
UEA 持有者 | 移除一个绑定。 |
读取
| 函数 | 描述 |
|---|---|
ownerOf(agentId) |
Agent 的规范持有者地址(EOA)(ERC-721)。 |
tokenURI(agentId) |
元数据 URI(兼容 ERC-721)。 |
agentURI(agentId) |
元数据 URI(ERC-8004 别名)。 |
canonicalOwner(agentId) |
给定 Agent ID 的规范持有者地址(EOA)。 |
agentIdOfUEA(uea) |
给定 UEA 地址的 Agent ID(0 表示未注册)。 |
getBindings(agentId) |
给定 Agent 的所有绑定条目。 |
canonicalOwnerFromBinding(ns, id, addr, boundId) |
将绑定解析为规范持有者(EOA)。 |
isRegistered(agentId) |
检查注册状态。 |
getAgentRecord(agentId) |
完整的链上记录。 |
管理
| 函数 | 访问权限 | 描述 |
|---|---|---|
pause() |
PAUSER_ROLE | 暂停所有状态变更操作。 |
unpause() |
PAUSER_ROLE | 恢复操作。 |
- 原文链接: github.com/zaryab2000/tr...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~