Contract Name:
LightClient
Contract Source Code:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.16;
import {IFunctionGateway} from "succinctx/interfaces/IFunctionGateway.sol";
import {OutputReader} from "./OutputReader.sol";
contract LightClient {
bytes32 public immutable GENESIS_VALIDATORS_ROOT;
uint256 public immutable GENESIS_TIME;
uint256 public immutable SECONDS_PER_SLOT;
uint256 public immutable SLOTS_PER_PERIOD;
uint32 public immutable SOURCE_CHAIN_ID;
uint16 public immutable FINALITY_THRESHOLD;
bytes32 public immutable STEP_FUNCTION_ID;
bytes32 public immutable ROTATE_FUNCTION_ID;
address public immutable FUNCTION_GATEWAY_ADDRESS;
uint256 internal constant MIN_SYNC_COMMITTEE_PARTICIPANTS = 10;
uint256 internal constant SYNC_COMMITTEE_SIZE = 512;
uint256 internal constant FINALIZED_ROOT_INDEX = 105;
uint256 internal constant NEXT_SYNC_COMMITTEE_INDEX = 55;
uint256 internal constant EXECUTION_STATE_ROOT_INDEX = 402;
/// @notice The latest slot the light client has a finalized header for.
uint256 public head = 0;
/// @notice Maps from a slot to a beacon block header root.
mapping(uint256 => bytes32) public headers;
/// @notice Maps from a slot to the timestamp of when the headers mapping was updated with slot as a key
mapping(uint256 => uint256) public timestamps;
/// @notice Maps from a slot to the current finalized ethereum1 execution state root.
mapping(uint256 => bytes32) public executionStateRoots;
/// @notice Maps from a period to the poseidon commitment for the sync committee.
mapping(uint256 => bytes32) public syncCommitteePoseidons;
event HeadUpdate(uint256 indexed slot, bytes32 indexed root);
event SyncCommitteeUpdate(uint256 indexed period, bytes32 indexed root);
error SyncCommitteeNotSet(uint256 period);
error HeaderRootNotSet(uint256 slot);
error SlotBehindHead(uint64 slot);
error NotEnoughParticipation(uint16 participation);
error SyncCommitteeAlreadySet(uint256 period);
error HeaderRootAlreadySet(uint256 slot);
error StateRootAlreadySet(uint256 slot);
constructor(
bytes32 genesisValidatorsRoot,
uint256 genesisTime,
uint256 secondsPerSlot,
uint256 slotsPerPeriod,
uint256 syncCommitteePeriod,
bytes32 syncCommitteePoseidon,
uint32 sourceChainId,
uint16 finalityThreshold,
bytes32 stepFunctionId,
bytes32 rotateFunctionId,
address gatewayAddress
) {
GENESIS_VALIDATORS_ROOT = genesisValidatorsRoot;
GENESIS_TIME = genesisTime;
SECONDS_PER_SLOT = secondsPerSlot;
SLOTS_PER_PERIOD = slotsPerPeriod;
SOURCE_CHAIN_ID = sourceChainId;
FINALITY_THRESHOLD = finalityThreshold;
STEP_FUNCTION_ID = stepFunctionId;
ROTATE_FUNCTION_ID = rotateFunctionId;
FUNCTION_GATEWAY_ADDRESS = gatewayAddress;
setSyncCommitteePoseidon(syncCommitteePeriod, syncCommitteePoseidon);
}
/// @notice Through the FunctionGateway, request for a step proof to be generated with the given attested slot number as the input.
function requestStep(uint256 attestedSlot) external payable {
IFunctionGateway(FUNCTION_GATEWAY_ADDRESS).requestCall{value: msg.value}(
STEP_FUNCTION_ID,
abi.encodePacked(
syncCommitteePoseidons[getSyncCommitteePeriod(attestedSlot)], uint64(attestedSlot)
),
address(this),
abi.encodeWithSelector(this.step.selector, attestedSlot),
1000000
);
}
/// @notice Through the FunctionGateway, request for a rotate proof to be generated with the given finalized slot number as the input.
function requestRotate(uint256 finalizedSlot) external payable {
IFunctionGateway(FUNCTION_GATEWAY_ADDRESS).requestCall{value: msg.value}(
ROTATE_FUNCTION_ID,
abi.encodePacked(headers[finalizedSlot]),
address(this),
abi.encodeWithSelector(this.rotate.selector, finalizedSlot),
1000000
);
}
/// @notice Process a step proof that has been verified in the FunctionGateway, then move the head forward and store the new roots.
function step(uint256 attestedSlot) external {
uint256 period = getSyncCommitteePeriod(attestedSlot);
bytes32 syncCommitteePoseidon = syncCommitteePoseidons[period];
if (syncCommitteePoseidon == bytes32(0)) {
revert SyncCommitteeNotSet(period);
}
// Input: [uint256 syncCommitteePoseidon, uint64 attestedSlot]
// Output: [bytes32 finalizedHeaderRoot, bytes32 executionStateRoot, uint64 finalizedSlot, uint16 participation]
bytes memory output = IFunctionGateway(FUNCTION_GATEWAY_ADDRESS).verifiedCall(
STEP_FUNCTION_ID, abi.encodePacked(syncCommitteePoseidon, uint64(attestedSlot))
);
bytes32 finalizedHeaderRoot = bytes32(OutputReader.readUint256(output, 0));
bytes32 executionStateRoot = bytes32(OutputReader.readUint256(output, 32));
uint64 finalizedSlot = OutputReader.readUint64(output, 64);
uint16 participation = OutputReader.readUint16(output, 72);
if (participation < FINALITY_THRESHOLD) {
revert NotEnoughParticipation(participation);
}
if (finalizedSlot <= head) {
revert SlotBehindHead(finalizedSlot);
}
setSlotRoots(uint256(finalizedSlot), finalizedHeaderRoot, executionStateRoot);
}
/// @notice Process a rotate proof that has been verified in the FunctionGateway, then store the next sync committee poseidon.
function rotate(uint256 finalizedSlot) external {
bytes32 finalizedHeaderRoot = headers[finalizedSlot];
if (finalizedHeaderRoot == bytes32(0)) {
revert HeaderRootNotSet(finalizedSlot);
}
// Input: [bytes32 finalizedHeaderRoot]
// Output: [bytes32 syncCommitteePoseidon]
bytes memory output = IFunctionGateway(FUNCTION_GATEWAY_ADDRESS).verifiedCall(
ROTATE_FUNCTION_ID, abi.encodePacked(finalizedHeaderRoot)
);
bytes32 syncCommitteePoseidon = bytes32(OutputReader.readUint256(output, 0));
uint256 period = getSyncCommitteePeriod(finalizedSlot);
uint256 nextPeriod = period + 1;
setSyncCommitteePoseidon(nextPeriod, syncCommitteePoseidon);
}
/// @notice Gets the sync committee period from a slot.
function getSyncCommitteePeriod(uint256 slot) internal view returns (uint256) {
return slot / SLOTS_PER_PERIOD;
}
/// @notice Gets the current slot for the chain the light client is reflecting.
function getCurrentSlot() internal view returns (uint256) {
return (block.timestamp - GENESIS_TIME) / SECONDS_PER_SLOT;
}
/// @notice Sets the current slot for the chain the light client is reflecting.
/// @dev Checks if roots exists for the slot already. If there is, check for a conflict between
/// the given roots and the existing roots. If there is an existing header but no
/// conflict, do nothing. This avoids timestamp renewal DoS attacks.
function setSlotRoots(uint256 slot, bytes32 finalizedHeaderRoot, bytes32 executionStateRoot)
internal
{
if (headers[slot] != bytes32(0)) {
revert HeaderRootAlreadySet(slot);
}
if (executionStateRoots[slot] != bytes32(0)) {
revert StateRootAlreadySet(slot);
}
head = slot;
headers[slot] = finalizedHeaderRoot;
executionStateRoots[slot] = executionStateRoot;
timestamps[slot] = block.timestamp;
emit HeadUpdate(slot, finalizedHeaderRoot);
}
/// @notice Sets the sync committee poseidon for a given period.
function setSyncCommitteePoseidon(uint256 period, bytes32 poseidon) internal {
if (syncCommitteePoseidons[period] != bytes32(0)) {
revert SyncCommitteeAlreadySet(period);
}
syncCommitteePoseidons[period] = poseidon;
emit SyncCommitteeUpdate(period, poseidon);
}
}
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;
interface IFunctionGatewayEvents {
event RequestCallback(
uint32 indexed nonce,
bytes32 indexed functionId,
bytes input,
bytes context,
address callbackAddress,
bytes4 callbackSelector,
uint32 callbackGasLimit,
uint256 feeAmount
);
event RequestCall(
bytes32 indexed functionId,
bytes input,
address entryAddress,
bytes entryCalldata,
uint32 entryGasLimit,
address sender,
uint256 feeAmount
);
event RequestFulfilled(
uint32 indexed nonce, bytes32 indexed functionId, bytes32 inputHash, bytes32 outputHash
);
event Call(bytes32 indexed functionId, bytes32 inputHash, bytes32 outputHash);
}
interface IFunctionGatewayErrors {
error InvalidRequest(uint32 nonce, bytes32 expectedRequestHash, bytes32 requestHash);
error CallbackFailed(bytes4 callbackSelector, bytes output, bytes context);
error InvalidCall(bytes32 functionId, bytes input);
error CallFailed(address callbackAddress, bytes callbackData);
error InvalidProof(address verifier, bytes32 inputHash, bytes32 outputHash, bytes proof);
}
interface IFunctionGateway is IFunctionGatewayEvents, IFunctionGatewayErrors {
function requestCallback(
bytes32 _functionId,
bytes memory _input,
bytes memory _context,
bytes4 _callbackSelector,
uint32 _callbackGasLimit
) external payable returns (bytes32);
function requestCall(
bytes32 _functionId,
bytes memory _input,
address _entryAddress,
bytes memory _entryData,
uint32 _entryGasLimit
) external payable;
function verifiedCall(bytes32 _functionId, bytes memory _input)
external
view
returns (bytes memory);
}
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.16;
library OutputReader {
function readUint256(bytes memory _output, uint256 _offset) internal pure returns (uint256) {
uint256 value;
assembly {
value := mload(add(add(_output, 0x20), _offset))
}
return value;
}
function readUint128(bytes memory _output, uint256 _offset) internal pure returns (uint128) {
uint128 value;
assembly {
value := mload(add(add(_output, 0x10), _offset))
}
return value;
}
function readUint64(bytes memory _output, uint256 _offset) internal pure returns (uint64) {
uint64 value;
assembly {
value := mload(add(add(_output, 0x08), _offset))
}
return value;
}
function readUint32(bytes memory _output, uint256 _offset) internal pure returns (uint32) {
uint32 value;
assembly {
value := mload(add(add(_output, 0x04), _offset))
}
return value;
}
function readUint16(bytes memory _output, uint256 _offset) internal pure returns (uint16) {
uint16 value;
assembly {
value := mload(add(add(_output, 0x02), _offset))
}
return value;
}
}