登记合约与上链
登记合约只做一件事:收到指纹,发出一条事件。法读取的是事件的格式。
接口
event Anchored(address indexed author, bytes32 indexed hash);
function anchor(bytes32 hash) external;
function anchorMany(bytes32[] calldata hashes) external;anchor 发出一条事件;anchorMany 为数组中的每个元素各发一条,空数组发出零条。事件中的 author 是调用者地址。合约零存储、零所有者、零升级入口,对所有调用者一视同仁。源码在 base/zikaron-core/contracts/src/ZikaronRegistry.sol,许可为 CC0-1.0。
已知部署
应用内置四处部署,同一份钉定的构建,代码哈希相同;以太坊主网、Arbitrum One、OP Mainnet 三处还是同一个部署者、同一份部署字节、同一个地址:
| 网络 | 链号 | 登记合约 | 起始区块 |
|---|---|---|---|
| 以太坊主网(默认) | 1 | 0x36Ea8A857a5FE813429d4D9947000C644A88809A | 26087229 |
| Sepolia 测试网 | 11155111 | 0xC29410B882c4C3b77e33659d2f06ac563e7B08a3 | 11715660 |
| Arbitrum One | 42161 | 0x36Ea8A857a5FE813429d4D9947000C644A88809A | 511445184 |
| OP Mainnet | 10 | 0x36Ea8A857a5FE813429d4D9947000C644A88809A | 157735914 |
起始区块是扫描的起点,早于这个合约上的第一条上链记录。每个身份记着自己用哪一处,两种角色同用;「默认」指首次启动向导与「新建身份」里先选中的那一项。预置节点与选取标准见发布 · 合约与网络。
两种上链形式
- 登记形式
- 交易成功,且回执中有一条由登记合约发出的日志:恰有三个主题、数据为空;主题 0 为
Anchored(address,bytes32)的 keccak256,主题 1 为交易发送者,主题 2 为指纹。同一指纹还须出现在交易的调用数据中,起始偏移为 32 的整数倍,或 4 加 32 的整数倍。 - 自转账形式
- 一笔发给自己的交易,调用数据长度为 32 的正整数倍,每 32 字节为一个指纹。只依赖交易本身,适用于任何 EVM 链。
两种形式效力相同。核验方在自己的扫描范围中写明认可哪些登记合约地址、哪些发送者、哪些自转账交易。
对发送者的三项要求
- 发送者是由交易签名恢复出的地址。经中继代发的交易,发送者为中继地址;由账户代码验证的交易,在法中发送者一项为空。
- 发送地址在上链所在区块前后均为普通地址。带有代码或 EIP-7702 委托的地址发出的上链记录,一律作废。
- 交易签名中写明链号。
这三项保证一条上链记录始终可以读作「这把密钥本人所为」。
代码哈希
合约的运行时字节码长 246 字节,其 keccak256 为:
它由固定的编译设置产生:solc 0.8.28,EVM 版本 cancun,开启优化(200 轮),开启 via_ir,字节码去除元数据。源码或设置的任何改动都会改变哈希。自行重算:
cd base/zikaron-core/contracts
forge build
cast keccak $(forge inspect ZikaronRegistry deployedBytecode)自行部署
任何人都可以在任一已完成上海升级的 EVM 链上部署相同的字节。部署后核对链上代码哈希与上表一致,再在应用的「设置 › 网络 › 编辑节点」里为这个身份填入链 ID、登记合约、起始区块与节点;只用来核验别人材料的,加进「只读网络」。
forge create src/ZikaronRegistry.sol:ZikaronRegistry \
--rpc-url <节点> --private-key <部署用的密钥> --broadcast
cast keccak $(cast code <新地址> --rpc-url <节点>)部署请用专门的密钥;身份密钥只用于签署账本和发送上链交易。
这份固定构建使用 PUSH0 指令,适用于已完成上海升级的链。更早的链须另行构建,代码哈希也随之改变。法按核验方声明的合约地址读取日志,因此另一份构建只关乎部署的便利。应用对登记合约的代码哈希核两处:读只读网络之前,逐个节点问它的链号与登记合约处的代码:给出钉定这一份的节点才计入,网络只在这些节点上读;在那里答不出代码的节点(尚未同步到合约部署的那一块)不计入;有一个节点给出别的代码,整条网络这一次即不用;所有节点在那里都没有代码时判作指纹不符;保存主网络的链设置时,内置表之外的登记合约也先问节点它的代码,是钉定的那一版才保存,否则提示「登记合约 … 不是钉定构建,链设置没有保存」;节点还空着时照存,读数记作「未核:无节点」,配好节点时再核一次,不符即报出。所以应用里的只读网络与主网络,用的都是钉定的那一份代码。
费用参考
| 调用 | 合约执行消耗的 gas | 整笔交易的估算(以太坊主网) |
|---|---|---|
anchor | 2,352 | 约 23,600 |
anchorMany,1 条 | 2,776 | 约 24,050 |
anchorMany,10 条 | 17,746 | 约 42,900 |
anchorMany,50 条 | 约 127,300 | |
anchorMany,83 条 | 约 198,000 | |
anchorMany,100 条 | 167,661 | 约 233,800 |
第二栏是在 forge 里测得的合约执行部分;第三栏是节点对整笔交易的估算,含 21,000 的基础费用与调用数据费用,随哈希的字节略有出入(哈希里每多一个零字节,约少 12 gas)。条数越多,单条分摊越少。
应用与命令行按同一条规则定 gas:几个节点估数不同时取最大的,交易所带的额度是它的 1.5 倍(向上取整),最多 200,000;估数本身超过 200,000 即拒,整批留在队列里(应用里提示无法估算;命令行直发时答 E_GAS_REFUSED,发数据目录的队列时答 E_SEND_REFUSED,token 为 GAS_REFUSED)。所以在以太坊主网上,一笔交易稳妥装 83 条;84 条的估数贴着 200,000,过不过要看哈希里的零字节。出价上限是基础费的两倍加小费,小费取最近 20 块成交小费中位数的中位数(取中位数大于零的块,块块为零时取零),最高 1 gwei;读不到基础费时按保底 3 gwei 与 1 gwei 计,确认卡上注明「节点未给基础费,上限按保底值」。费率只读答的是这条链的节点。发前的余额检查在各节点都已到的最低一块上逐个节点问、彼此比对,有节点答出才算数。
链上已有即不发。应用的发送器、经桌面与经凭据的 anchor、以及命令行直发的 anchor --form registry,签名之前都先读这个登记合约的 Anchored 日志(从合约部署块到链头),问这把钥是否已锚过所给的哈希;有一条即一笔不发(应用提示「这一批里有存证已在链上,这一次一笔也没有发。」,命令行答 E_ON_CHAIN)。各节点一致读到链上已有的条目随后移出待上链队列,下一次只发其余的;只一处节点说有,不移。
卡住的一批。一批交易在节点池里迟迟不入块、而链上现价已高过它发出时的上限时,可以加价重发:以同一个 nonce 重签,两项费率各至少比原来高 10%(节点只在这样时才收替换),原来的与新的都继续盯,先入块的作数。这一批离开了节点池而 nonce 仍空着时,桌面在账本状态行给「按现价重发」由用户按;经凭据的 anchor --ledger 在「自动发送」时在原 nonce 上照原样重签再发;nonce 已被别的交易用掉时,这一批作废,条目退回队列。一批至多重发 3 次。同一个 nonce 至多一笔能入块,所以一批只付一次费。
经其他合约上链
任何合约只要发出同样格式的日志,都可充当登记合约。以下三个条件同时成立,该日志才算上链记录:交易由账本密钥亲自发出;指纹出现在该交易自身的调用数据中,且位置符合上述规定;日志由被调用的合约自身发出。合约在内部转而调用登记合约时,日志记录的是那个合约的地址,对任何账本而言都只是一条普通日志。
核验方只应把读过源码、代码固定的合约地址写入扫描范围。应用对只读网络强制这一条:登记合约的代码哈希不等于钉定的那一个,就不读它。
自含上链证明
法 §9.7 定义了自含的上链证明,记录包的 proofs/ 目录用于携带。凭这样一份证明,核验方只需两个可信的区块哈希即可验证一条上链记录:区块头,交易与回执各自的默克尔证明,以及发送地址在前后两个区块中的账户证明。节点清理历史数据后,此类证明依然有效。