From 3303b128a7fcbada9916a2e055bd33789c78df4e Mon Sep 17 00:00:00 2001 From: AnshuJalan Date: Thu, 20 Jun 2024 15:26:24 +0530 Subject: [PATCH] feat: add block proposals and lookahead posting --- SmartContracts/src/avs/PreconfTaskManager.sol | 229 ++++++++++++++++++ .../src/interfaces/IPreconfTaskManager.sol | 53 +++- .../src/interfaces/taiko/ITaikoL1.sol | 32 +++ 3 files changed, 303 insertions(+), 11 deletions(-) create mode 100644 SmartContracts/src/avs/PreconfTaskManager.sol create mode 100644 SmartContracts/src/interfaces/taiko/ITaikoL1.sol diff --git a/SmartContracts/src/avs/PreconfTaskManager.sol b/SmartContracts/src/avs/PreconfTaskManager.sol new file mode 100644 index 0000000..6b6b001 --- /dev/null +++ b/SmartContracts/src/avs/PreconfTaskManager.sol @@ -0,0 +1,229 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.25; + +import {ITaikoL1} from "../interfaces/taiko/ITaikoL1.sol"; +import {IPreconfTaskManager} from "../interfaces/IPreconfTaskManager.sol"; +import {IPreconfServiceManager} from "../interfaces/IPreconfServiceManager.sol"; +import {IRegistryCoordinator} from "eigenlayer-middleware/interfaces/IRegistryCoordinator.sol"; +import {IIndexRegistry} from "eigenlayer-middleware/interfaces/IIndexRegistry.sol"; + +contract PreconfTaskManager is IPreconfTaskManager { + IPreconfServiceManager internal immutable preconfServiceManager; + IRegistryCoordinator internal immutable registryCoordinator; + IIndexRegistry internal immutable indexRegistry; + ITaikoL1 internal immutable taikoL1; + + // Dec-01-2020 12:00:23 PM +UTC + uint256 internal constant BEACON_GENESIS_TIMESTAMP = 1606824023; + // 12 seconds for each slot, with 32 slots in each epoch + uint256 internal constant SECONDS_IN_EPOCH = 384; + // Span time within which a preconfirmation or posted lookahead may be disputed + uint256 internal constant DISPUTE_PERIOD = 2 * SECONDS_IN_EPOCH; + + // A ring buffer of upcoming preconfers (who are also the L1 validators) + uint256 internal lookaheadTail; + uint256 internal constant LOOKAHEAD_BUFFER_SIZE = 64; + IPreconfTaskManager.LookaheadEntry[LOOKAHEAD_BUFFER_SIZE] internal lookahead; + + // Todo: Make the below two data structure more efficient by reusing slots + + // Current and past lookahead posters as a mapping indexed by the timestamp of the epoch + mapping(uint256 epochTimestamp => address poster) internal lookaheadPosters; + // Maps the epoch timestamp to the randomly selected preconfer (if present) for that epoch + mapping(uint256 epochTimestamp => address randomPreconfer) internal randomPreconfers; + + // A ring buffer of proposed (and preconfed) L2 blocks + uint256 nextBlockId; + uint256 internal constant PROPOSED_BLOCK_BUFFER_SIZE = 256; + mapping(uint256 blockId => IPreconfTaskManager.ProposedBlock proposedBlock) proposedBlocks; + + constructor( + IPreconfServiceManager _serviceManager, + IRegistryCoordinator _registryCoordinator, + IIndexRegistry _indexRegistry, + ITaikoL1 _taikoL1 + ) { + preconfServiceManager = _serviceManager; + registryCoordinator = _registryCoordinator; + indexRegistry = _indexRegistry; + taikoL1 = _taikoL1; + + nextBlockId = 1; + } + + /** + * @notice Proposes a new Taiko L2 block. + * @dev This may either be called by a randomly selected preconfer or by a preconfer expected for the current slot + * as per the lookahead. The first caller in every is expected to pass along the lookahead entries for the next epoch. + * @param blockParams Block parameters expected by TaikoL1 contract + * @param txList RLP encoded transaction list expected by TaikoL1 contract + * @param lookaheadPointer A pointer to the lookahead entry that may prove that the sender is the preconfer + * for the slot. + * @param lookaheadSetParams Collection of timestamps and preconfer addresses to be inserted in the lookahead + */ + function newBlockProposal( + bytes calldata blockParams, + bytes calldata txList, + uint256 lookaheadPointer, + IPreconfTaskManager.LookaheadSetParam[] calldata lookaheadSetParams + ) external { + uint256 currentEpochTimestamp = _getEpochTimestamp(); + address randomPreconfer = randomPreconfers[currentEpochTimestamp]; + + // Verify that the sender is a valid preconfer for the slot and has the right to propose an L2 block + if (randomPreconfer != address(0) && msg.sender != randomPreconfer) { + // Revert if the sender is not the randomly selected preconfer for the epoch + revert IPreconfTaskManager.SenderIsNotTheFallbackPreconfer(); + } else if (isLookaheadRequired(currentEpochTimestamp)) { + // The *current* epoch may require a lookahead in the following situations: + // - It is the first epoch after this contract started offering services + // - The epoch has no L1 validators who are opted-in preconfers in the AVS + // - The previous lookahead for the epoch was invalidated/ + // + // In all the above cases, we expect a preconfer to be randomly chosen as fallaback + if (msg.sender != getFallbackPreconfer()) { + revert IPreconfTaskManager.SenderIsNotTheFallbackPreconfer(); + } else { + randomPreconfers[currentEpochTimestamp] = msg.sender; + } + } else { + IPreconfTaskManager.LookaheadEntry memory lookaheadEntry = + lookahead[lookaheadPointer % LOOKAHEAD_BUFFER_SIZE]; + + // The current L1 block's timestamp must be within the range retrieved from the lookahead entry. + // The preconfer is allowed to propose a block in advanced if there are no other entries in the + // lookahead between the present slot and the preconfer's own slot. + // + // ------[Last slot with an entry]---[X]---[X]----[X]----[Preconfer]------- + // ------[ prevTimestamp ]---[ ]---[ ]----[ ]----[timestamp]------- + // + if (block.timestamp <= lookaheadEntry.prevTimestamp || block.timestamp > lookaheadEntry.timestamp) { + revert IPreconfTaskManager.InvalidLookaheadPointer(); + } else if (msg.sender != lookaheadEntry.preconfer) { + revert IPreconfTaskManager.SenderIsNotThePreconfer(); + } + } + + uint256 nextEpochTimestamp = currentEpochTimestamp + SECONDS_IN_EPOCH; + + // Update the lookahead for the next epoch. + // Only called during the first block proposal of the current epoch. + if (isLookaheadRequired(nextEpochTimestamp)) { + _updateLookahead(currentEpochTimestamp, lookaheadSetParams); + } + + uint256 _nextBlockId = nextBlockId; + + // Store the hash of the transaction list and the proposer of the proposed block. + // The hash is later used to verify transaction inclusion/ordering in a preconfirmation. + proposedBlocks[_nextBlockId % PROPOSED_BLOCK_BUFFER_SIZE] = + IPreconfTaskManager.ProposedBlock({proposer: msg.sender, txListHash: keccak256(txList)}); + + nextBlockId = _nextBlockId + 1; + + // Block the preconfer from withdrawing stake from Eigenlayer during the dispute window + preconfServiceManager.lockStakeUntil(msg.sender, block.timestamp + DISPUTE_PERIOD); + + // Forward the block to Taiko's L1 contract + taikoL1.proposeBlock(blockParams, txList); + } + + function proveIncorrectPreconfirmation(PreconfirmationHeader memory header, bytes memory signature) external {} + + function proveIncorrectLookahead( + uint256 offset, + bytes32[] memory expectedValidator, + uint256 expectedValidatorIndex, + bytes32[] memory expectedValidatorProof, + bytes32[] memory actualValidator, + uint256 actualValidatorIndex, + bytes32[] memory actualValidatorProof, + bytes32 validatorsRoot, + uint256 nr_validators, + bytes32[] memory beaconStateProof, + bytes32 beaconStateRoot, + bytes32[] memory beaconBlockProof + ) external {} + + //========= + // Helpers + //========= + + function _updateLookahead( + uint256 epochTimestamp, + IPreconfTaskManager.LookaheadSetParam[] calldata lookaheadSetParams + ) private { + uint256 nextEpochTimestamp = epochTimestamp + SECONDS_IN_EPOCH; + uint256 nextEpochEndTimestamp = nextEpochTimestamp + SECONDS_IN_EPOCH; + + // The tail of the lookahead is tracked and connected to the first new lookahead entry so + // that when no more preconfers are present in the remaining slots of the current epoch, + // the next epoch's preconfer may start preconfing in advanced. + // + // --[]--[]--[p1]--[]--[]---|---[]--[]--[P2]--[]--[] + // 1 2 3 4 5 6 7 8 9 10 + // Epoch 1 Epoch 2 + // + // Here, P2 may start preconfing and proposing blocks from slot 4 itself + // + uint256 _lookaheadTail = lookaheadTail; + uint256 prevSlotTimestamp = lookahead[_lookaheadTail % LOOKAHEAD_BUFFER_SIZE].timestamp; + + for (uint256 i; i < lookaheadSetParams.length; ++i) { + _lookaheadTail += 1; + + address preconfer = lookaheadSetParams[i].preconfer; + uint256 slotTimestamp = lookaheadSetParams[i].timestamp; + + // Each entry must be a registered AVS operator + if (registryCoordinator.getOperatorStatus(preconfer) != IRegistryCoordinator.OperatorStatus.REGISTERED) { + revert IPreconfTaskManager.SenderNotRegisteredInAVS(); + } + + // Ensure that the timestamps belong to a valid slot in the next epoch + if ((slotTimestamp - nextEpochTimestamp) % 12 != 0 || slotTimestamp >= nextEpochEndTimestamp) { + revert IPreconfTaskManager.InvalidSlotTimestamp(); + } + + // Update the lookahead entry + lookahead[_lookaheadTail % LOOKAHEAD_BUFFER_SIZE] = IPreconfTaskManager.LookaheadEntry({ + timestamp: uint48(slotTimestamp), + prevTimestamp: uint48(prevSlotTimestamp), + preconfer: preconfer + }); + prevSlotTimestamp = slotTimestamp; + } + + lookaheadTail = _lookaheadTail; + lookaheadPosters[epochTimestamp] = msg.sender; + + emit LookaheadUpdated(lookaheadSetParams); + } + + /** + * @notice Computes the timestamp at which the ongoing epoch started. + */ + function _getEpochTimestamp() private view returns (uint256) { + uint256 timePassedSinceGenesis = block.timestamp - BEACON_GENESIS_TIMESTAMP; + uint256 timeToCurrentEpochFromGenesis = (timePassedSinceGenesis / SECONDS_IN_EPOCH) * SECONDS_IN_EPOCH; + return BEACON_GENESIS_TIMESTAMP + timeToCurrentEpochFromGenesis; + } + + //======= + // Views + //======= + + function getFallbackPreconfer() public view returns (address) { + uint256 randomness = block.prevrandao; + // For the POC we only have one quorum at the 0th id. If there are multiple quorums in productions + // we may also need to randomise the quorum selection. + uint8 quorumNumber = 0; + uint32 operatorIndex = uint32(randomness % indexRegistry.totalOperatorsForQuorum(quorumNumber)); + bytes32 operatorId = bytes32(indexRegistry.getLatestOperatorUpdate(quorumNumber, operatorIndex).operatorId); + return registryCoordinator.getOperatorFromId(operatorId); + } + + function isLookaheadRequired(uint256 epochTimestamp) public view returns (bool) { + return lookaheadPosters[epochTimestamp] == address(0); + } +} diff --git a/SmartContracts/src/interfaces/IPreconfTaskManager.sol b/SmartContracts/src/interfaces/IPreconfTaskManager.sol index a050977..29ea0b5 100644 --- a/SmartContracts/src/interfaces/IPreconfTaskManager.sol +++ b/SmartContracts/src/interfaces/IPreconfTaskManager.sol @@ -2,15 +2,11 @@ pragma solidity 0.8.25; interface IPreconfTaskManager { - event LookaheadUpdated(bytes lookahead); - event ProvedIncorrectPreconfirmation(address indexed preconfer, uint256 indexed blockId, address indexed disputer); - event ProvedIncorrectLookahead(address indexed poster, uint256 indexed slot, address indexed disputer); - - struct LookaheadEntry { - // The timestamp of the slot - uint256 timestamp; - // The id of the AVS operator who is also the L1 validator for the slot - uint256 validatorId; + struct ProposedBlock { + // Proposer of the L2 block + address proposer; + // Keccak hash of the RLP transaction list of the block + bytes32 txListHash; } struct PreconfirmationHeader { @@ -22,11 +18,46 @@ interface IPreconfTaskManager { bytes32 txListHash; } + struct LookaheadEntry { + // Timestamp of the slot at which the provided preconfer is the L1 validator + uint48 timestamp; + // Timestamp of the last slot that had a valid preconfer + uint48 prevTimestamp; + // Address of the preconfer who is also the L1 validator + // The preconfer will have rights to propose a block in the range (prevTimestamp, timestamp] + address preconfer; + } + + struct LookaheadSetParam { + // The timestamp of the slot + uint256 timestamp; + // The AVS operator who is also the L1 validator for the slot and will preconf L2 transactions + address preconfer; + } + + event LookaheadUpdated(LookaheadSetParam[]); + event ProvedIncorrectPreconfirmation(address indexed preconfer, uint256 indexed blockId, address indexed disputer); + event ProvedIncorrectLookahead(address indexed poster, uint256 indexed slot, address indexed disputer); + + /// @dev The block proposer is not the randomly chosen fallback preconfer for the current slot/timestamp + error SenderIsNotTheFallbackPreconfer(); + /// @dev The current timestamp does not fall in the range provided by the lookahead pointer + error InvalidLookaheadPointer(); + /// @dev The block proposer is not the assigned preconfer for the current slot/timestamp + error SenderIsNotThePreconfer(); + /// @dev The block proposer has not set themselves as first preconfer in the lookahead they are updating + error SenderMustBeTheFirstEntryInLookaheadSetParams(); + /// @dev The preconfer in the lookahead set params is not registered to the AVS + error SenderNotRegisteredInAVS(); + /// @dev The timestamp in the lookahead is not of a valid future slot in the present epoch + error InvalidSlotTimestamp(); + /// @dev Accepts block proposal by an operator and forwards it to TaikoL1 contract function newBlockProposal( bytes calldata blockParams, bytes calldata txList, - LookaheadEntry[] calldata lookaheadEntries + uint256 lookaheadHint, + LookaheadSetParam[] calldata lookaheadSetParams ) external; /// @dev Slashes a preconfer if the txn and ordering in a signed preconf does not match the actual block @@ -48,5 +79,5 @@ interface IPreconfTaskManager { bytes32[] memory beaconBlockProof ) external; - function isLookaheadRequired() external view returns (bool); + function isLookaheadRequired(uint256 epochTimestamp) external view returns (bool); } diff --git a/SmartContracts/src/interfaces/taiko/ITaikoL1.sol b/SmartContracts/src/interfaces/taiko/ITaikoL1.sol new file mode 100644 index 0000000..fb00764 --- /dev/null +++ b/SmartContracts/src/interfaces/taiko/ITaikoL1.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.25; + +interface ITaikoL1 { + struct BlockMetadata { + bytes32 l1Hash; + bytes32 difficulty; + bytes32 blobHash; + bytes32 extraData; + bytes32 depositsHash; + address coinbase; + uint64 id; + uint32 gasLimit; + uint64 timestamp; + uint64 l1Height; + uint16 minTier; + bool blobUsed; + bytes32 parentMetaHash; + address sender; + } + + struct EthDeposit { + address recipient; + uint96 amount; + uint64 id; + } + + function proposeBlock(bytes calldata _params, bytes calldata _txList) + external + payable + returns (BlockMetadata memory meta_, EthDeposit[] memory deposits_); +}