// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; /// @notice Ordered index of data-only shard contracts holding one payload. /// Once sealed the shard list is immutable and ownership is renounced, /// so the payload can never be swapped, extended or removed. contract Registry { address public owner; bool public isSealed; address[] private _shards; /// @notice Total byte length of the concatenated payload. uint256 public payloadLength; /// @notice keccak256 of the concatenated shard bytecode, set at seal time. bytes32 public payloadHash; error NotOwner(); error AlreadySealed(); error NotSealed(); error EmptyShard(uint256 index); error LengthMismatch(uint256 expected, uint256 actual); event ShardsAdded(uint256 fromIndex, uint256 count); event Sealed(uint256 payloadLength, bytes32 payloadHash); event Attested(address indexed by, bool ok, bytes32 payloadHash); constructor() { owner = msg.sender; } modifier onlyOwner() { if (msg.sender != owner) revert NotOwner(); _; } modifier unsealed() { if (isSealed) revert AlreadySealed(); _; } /// @notice Append shards in payload order. Reverts on any codeless address /// so a mis-ordered or failed deploy cannot be indexed. function addShards(address[] calldata shards_) external onlyOwner unsealed { uint256 from = _shards.length; for (uint256 i = 0; i < shards_.length; ++i) { if (shards_[i].code.length == 0) revert EmptyShard(from + i); _shards.push(shards_[i]); } emit ShardsAdded(from, shards_.length); } /// @notice Freeze the index and renounce ownership. Verifies the on-chain /// byte total matches the caller's claim before committing. function seal(uint256 payloadLength_, bytes32 payloadHash_) external onlyOwner unsealed { uint256 total = measuredLength(); if (total != payloadLength_) revert LengthMismatch(payloadLength_, total); payloadLength = payloadLength_; payloadHash = payloadHash_; isSealed = true; owner = address(0); emit Sealed(payloadLength_, payloadHash_); } /// @notice Sum of every shard's code size, read live from chain state. function measuredLength() public view returns (uint256 total) { address[] memory s = _shards; for (uint256 i = 0; i < s.length; ++i) { total += s[i].code.length; } } /// @notice Re-reads every shard into memory, concatenates and hashes it /// inside the EVM. The chain checking its own claim, no client /// involved. Free as a call; memory expansion dominates the cost. function verify() public view returns (bool) { if (!isSealed) revert NotSealed(); uint256 len = payloadLength; bytes memory buf = new bytes(len); uint256 off; address[] memory s = _shards; for (uint256 i = 0; i < s.length; ++i) { address shard = s[i]; uint256 size = shard.code.length; assembly { extcodecopy(shard, add(add(buf, 0x20), off), 0, size) } off += size; } if (off != len) return false; return keccak256(buf) == payloadHash; } /// @notice Same check as a transaction, so the result is in a receipt /// anyone can point at. Permissionless: attest as often as you like. function attest() external returns (bool ok) { ok = verify(); emit Attested(msg.sender, ok, payloadHash); } function shardCount() external view returns (uint256) { return _shards.length; } function shardAt(uint256 index) external view returns (address) { return _shards[index]; } /// @notice Full ordered shard list, for one-call client bootstrap. function allShards() external view returns (address[] memory) { return _shards; } /// @notice Shard list plus the sizes a client should expect for each. function manifest() external view returns (address[] memory shards_, uint256[] memory sizes) { shards_ = _shards; sizes = new uint256[](shards_.length); for (uint256 i = 0; i < shards_.length; ++i) { sizes[i] = shards_[i].code.length; } } }