diff --git a/packages/protocol/contracts/layer1/based/ITaikoInbox.sol b/packages/protocol/contracts/layer1/based/ITaikoInbox.sol new file mode 100644 index 00000000000..046d5e66989 --- /dev/null +++ b/packages/protocol/contracts/layer1/based/ITaikoInbox.sol @@ -0,0 +1,403 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "src/shared/based/LibSharedData.sol"; + +/// @title TaikoInbox +/// @notice Acts as the inbox for the Taiko Alethia protocol, a simplified version of the +/// original Taiko-Based Contestable Rollup (BCR). The tier-based proof system and +/// contestation mechanisms have been removed. +/// +/// Key assumptions of this protocol: +/// - Block proposals and proofs are asynchronous. Proofs are not available at proposal time, +/// unlike Taiko Gwyneth, which assumes synchronous composability. +/// - Proofs are presumed error-free and thoroughly validated, with proof type management +/// delegated to IVerifier contracts. +/// +/// @dev Registered in the address resolver as "taiko". +/// @custom:security-contact security@taiko.xyz +interface ITaikoInbox { + struct BlockParams { + // the max number of transactions in this block. Note that if there are not enough + // transactions in calldata or blobs, the block will contains as many transactions as + // possible. + uint16 numTransactions; + // For the first block in a batch, the block timestamp is the batch params' `timestamp` + // plus this time shift value; + // For all other blocks in the same batch, the block timestamp is its parent block's + // timestamp plus this time shift value. + uint8 timeShift; + } + + struct BlobParams { + // The hashes of the blob. Note that if this array is not empty. `firstBlobIndex` and + // `numBlobs` must be 0. + bytes32[] blobHashes; + // The index of the first blob in this batch. + uint8 firstBlobIndex; + // The number of blobs in this batch. Blobs are initially concatenated and subsequently + // decompressed via Zlib. + uint8 numBlobs; + // The byte offset of the blob in the batch. + uint32 byteOffset; + // The byte size of the blob. + uint32 byteSize; + } + + struct BatchParams { + address proposer; + address coinbase; + bytes32 parentMetaHash; + uint64 anchorBlockId; + bytes32 anchorInput; + uint64 lastBlockTimestamp; + bool revertIfNotFirstProposal; + bytes32[] signalSlots; + // Specifies the number of blocks to be generated from this batch. + BlobParams blobParams; + BlockParams[] blocks; + } + + /// @dev This struct holds batch information essential for constructing blocks offchain, but it + /// does not include data necessary for batch proving. + struct BatchInfo { + bytes32 txsHash; + // Data to build L2 blocks + BlockParams[] blocks; + bytes32[] blobHashes; + bytes32 extraData; + address coinbase; + uint64 proposedIn; // Used by node/client + uint32 blobByteOffset; + uint32 blobByteSize; + uint32 gasLimit; + uint64 lastBlockId; + uint64 lastBlockTimestamp; + // Data for the L2 anchor transaction, shared by all blocks in the batch + uint64 anchorBlockId; + // corresponds to the `_anchorStateRoot` parameter in the anchor transaction. + // The batch's validity proof shall verify the integrity of these two values. + bytes32 anchorBlockHash; + bytes32 anchorInput; + LibSharedData.BaseFeeConfig baseFeeConfig; + bytes32[] signalSlots; + } + + /// @dev This struct holds batch metadata essential for proving the batch. + struct BatchMetadata { + bytes32 infoHash; + address proposer; + uint64 batchId; + uint64 proposedAt; // Used by node/client + } + + /// @notice Struct representing transition to be proven. + struct Transition { + bytes32 parentHash; + bytes32 blockHash; + bytes32 stateRoot; + } + + // @notice Struct representing transition storage + /// @notice 4 slots used. + struct TransitionState { + bytes32 parentHash; + bytes32 blockHash; + bytes32 stateRoot; + address prover; + bool inProvingWindow; + uint48 createdAt; + } + + /// @notice 3 slots used. + struct Batch { + bytes32 metaHash; // slot 1 + uint64 lastBlockId; // slot 2 + uint96 reserved3; + uint96 livenessBond; + uint64 batchId; // slot 3 + uint64 lastBlockTimestamp; + uint64 anchorBlockId; + uint24 nextTransitionId; + uint8 reserved4; + // The ID of the transaction that is used to verify this batch. However, if this batch is + // not verified as the last one in a transaction, verifiedTransitionId will remain zero. + uint24 verifiedTransitionId; + } + + /// @notice Forge is only able to run coverage in case the contracts by default capable of + /// compiling without any optimization (neither optimizer runs, no compiling --via-ir flag). + struct Stats1 { + uint64 genesisHeight; + uint64 __reserved2; + uint64 lastSyncedBatchId; + uint64 lastSyncedAt; + } + + struct Stats2 { + uint64 numBatches; + uint64 lastVerifiedBatchId; + bool paused; + uint56 lastProposedIn; + uint64 lastUnpausedAt; + } + + struct ForkHeights { + uint64 ontake; + uint64 pacaya; + } + + /// @notice Struct holding Taiko configuration parameters. See {TaikoConfig}. + struct Config { + /// @notice The chain ID of the network where Taiko contracts are deployed. + uint64 chainId; + /// @notice The maximum number of unverified batches the protocol supports. + uint64 maxUnverifiedBatches; + /// @notice Size of the batch ring buffer, allowing extra space for proposals. + uint64 batchRingBufferSize; + /// @notice The maximum number of verifications allowed when a batch is proposed or proved. + uint64 maxBatchesToVerify; + /// @notice The maximum gas limit allowed for a block. + uint32 blockMaxGasLimit; + /// @notice The amount of Taiko token as a prover liveness bond per batch. + uint96 livenessBondBase; + /// @notice The amount of Taiko token as a prover liveness bond per block. + uint96 livenessBondPerBlock; + /// @notice The number of batches between two L2-to-L1 state root sync. + uint8 stateRootSyncInternal; + /// @notice The max differences of the anchor height and the current block number. + uint64 maxAnchorHeightOffset; + /// @notice Base fee configuration + LibSharedData.BaseFeeConfig baseFeeConfig; + /// @notice The proving window in seconds. + uint16 provingWindow; + /// @notice The time required for a transition to be used for verifying a batch. + uint24 cooldownWindow; + /// @notice The maximum number of signals to be received by TaikoL2. + uint8 maxSignalsToReceive; + /// @notice The maximum number of blocks per batch. + uint16 maxBlocksPerBatch; + /// @notice Historical heights of the forks. + ForkHeights forkHeights; + } + + /// @notice Struct holding the state variables for the {Taiko} contract. + struct State { + // Ring buffer for proposed batches and a some recent verified batches. + mapping(uint256 batchId_mod_batchRingBufferSize => Batch batch) batches; + // Indexing to transition ids (ring buffer not possible) + mapping(uint256 batchId => mapping(bytes32 parentHash => uint24 transitionId)) transitionIds; + // Ring buffer for transitions + mapping( + uint256 batchId_mod_batchRingBufferSize + => mapping(uint24 transitionId => TransitionState ts) + ) transitions; + bytes32 __reserve1; // slot 4 - was used as a ring buffer for Ether deposits + Stats1 stats1; // slot 5 + Stats2 stats2; // slot 6 + mapping(address account => uint256 bond) bondBalance; + uint256[43] __gap; + } + + /// @notice Emitted when tokens are deposited into a user's bond balance. + /// @param user The address of the user who deposited the tokens. + /// @param amount The amount of tokens deposited. + event BondDeposited(address indexed user, uint256 amount); + + /// @notice Emitted when tokens are withdrawn from a user's bond balance. + /// @param user The address of the user who withdrew the tokens. + /// @param amount The amount of tokens withdrawn. + event BondWithdrawn(address indexed user, uint256 amount); + + /// @notice Emitted when a token is credited back to a user's bond balance. + /// @param user The address of the user whose bond balance is credited. + /// @param amount The amount of tokens credited. + event BondCredited(address indexed user, uint256 amount); + + /// @notice Emitted when a token is debited from a user's bond balance. + /// @param user The address of the user whose bond balance is debited. + /// @param amount The amount of tokens debited. + event BondDebited(address indexed user, uint256 amount); + + /// @notice Emitted when a batch is synced. + /// @param stats1 The Stats1 data structure. + event Stats1Updated(Stats1 stats1); + + /// @notice Emitted when some state variable values changed. + /// @param stats2 The Stats2 data structure. + event Stats2Updated(Stats2 stats2); + + /// @notice Emitted when a batch is proposed. + /// @param info The info of the proposed batch. + /// @param meta The metadata of the proposed batch. + /// @param txList The tx list in calldata. + event BatchProposed(BatchInfo info, BatchMetadata meta, bytes txList); + + /// @notice Emitted when multiple transitions are proved. + /// @param verifier The address of the verifier. + /// @param transitions The transitions data. + event BatchesProved(address verifier, uint64[] batchIds, Transition[] transitions); + + /// @notice Emitted when a transition is overwritten by a conflicting one with the same parent + /// hash but different block hash or state root. + /// @param batchId The batch ID. + /// @param oldTran The old transition overwritten. + /// @param newTran The new transition. + event ConflictingProof(uint64 batchId, TransitionState oldTran, Transition newTran); + + /// @notice Emitted when a batch is verified. + /// @param batchId The ID of the verified batch. + /// @param blockHash The hash of the verified batch. + event BatchesVerified(uint64 batchId, bytes32 blockHash); + + /// @notice Emitted when a transition is written to the state by the owner. + /// @param batchId The ID of the batch containing the transition. + /// @param tid The ID of the transition within the batch. + /// @param ts The transition state written. + event TransitionWritten(uint64 batchId, uint24 tid, TransitionState ts); + + error AnchorBlockIdSmallerThanParent(); + error AnchorBlockIdTooLarge(); + error AnchorBlockIdTooSmall(); + error ArraySizesMismatch(); + error BatchNotFound(); + error BatchVerified(); + error BlobNotFound(); + error BlockNotFound(); + error BlobNotSpecified(); + error ContractPaused(); + error CustomProposerMissing(); + error CustomProposerNotAllowed(); + error EtherNotPaidAsBond(); + error ForkNotActivated(); + error InsufficientBond(); + error InvalidBlobParams(); + error InvalidGenesisBlockHash(); + error InvalidParams(); + error InvalidTransitionBlockHash(); + error InvalidTransitionParentHash(); + error InvalidTransitionStateRoot(); + error MetaHashMismatch(); + error MsgValueNotZero(); + error NoBlocksToProve(); + error NotFirstProposal(); + error NotInboxOperator(); + error ParentMetaHashMismatch(); + error SameTransition(); + error SignalNotSent(); + error TimestampSmallerThanParent(); + error TimestampTooLarge(); + error TimestampTooSmall(); + error TooManyBatches(); + error TooManyBlocks(); + error TooManySignals(); + error TransitionNotFound(); + error ZeroAnchorBlockHash(); + + /// @notice Proposes a batch of blocks. + /// @param _params ABI-encoded BlockParams. + /// @param _txList The transaction list in calldata. If the txList is empty, blob will be used + /// for data availability. + /// @return info_ The info of the proposed batch. + /// @return meta_ The metadata of the proposed batch. + function proposeBatch( + bytes calldata _params, + bytes calldata _txList + ) + external + returns (BatchInfo memory info_, BatchMetadata memory meta_); + + /// @notice Proves state transitions for multiple batches with a single aggregated proof. + /// @param _params ABI-encoded parameter containing: + /// - metas: Array of metadata for each batch being proved. + /// - transitions: Array of batch transitions to be proved. + /// @param _proof The aggregated cryptographic proof proving the batches transitions. + function proveBatches(bytes calldata _params, bytes calldata _proof) external; + + /// @notice Deposits TAIKO tokens into the contract to be used as liveness bond. + /// @param _amount The amount of TAIKO tokens to deposit. + function depositBond(uint256 _amount) external payable; + + /// @notice Withdraws a specified amount of TAIKO tokens from the contract. + /// @param _amount The amount of TAIKO tokens to withdraw. + function withdrawBond(uint256 _amount) external; + + /// @notice Returns the TAIKO token balance of a specific user. + /// @param _user The address of the user. + /// @return The TAIKO token balance of the user. + function bondBalanceOf(address _user) external view returns (uint256); + + /// @notice Retrieves the Bond token address. If Ether is used as bond, this function returns + /// address(0). + /// @return The Bond token address. + function bondToken() external view returns (address); + + /// @notice Retrieves the first set of protocol statistics. + /// @return Stats1 structure containing the statistics. + function getStats1() external view returns (Stats1 memory); + + /// @notice Retrieves the second set of protocol statistics. + /// @return Stats2 structure containing the statistics. + function getStats2() external view returns (Stats2 memory); + + /// @notice Retrieves data about a specific batch. + /// @param _batchId The ID of the batch to retrieve. + /// @return batch_ The batch data. + function getBatch(uint64 _batchId) external view returns (Batch memory batch_); + + /// @notice Retrieves a specific transition by batch ID and transition ID. This function may + /// revert if the transition is not found. + /// @param _batchId The batch ID. + /// @param _tid The transition ID. + /// @return The specified transition state. + function getTransitionById( + uint64 _batchId, + uint24 _tid + ) + external + view + returns (ITaikoInbox.TransitionState memory); + + /// @notice Retrieves a specific transition by batch ID and parent Hash. This function may + /// revert if the transition is not found. + /// @param _batchId The batch ID. + /// @param _parentHash The parent hash. + /// @return The specified transition state. + function getTransitionByParentHash( + uint64 _batchId, + bytes32 _parentHash + ) + external + view + returns (ITaikoInbox.TransitionState memory); + + /// @notice Retrieves the transition used for the last verified batch. + /// @return batchId_ The batch ID of the last verified transition. + /// @return blockId_ The block ID of the last verified block. + /// @return ts_ The last verified transition. + function getLastVerifiedTransition() + external + view + returns (uint64 batchId_, uint64 blockId_, TransitionState memory ts_); + + /// @notice Retrieves the transition used for the last synced batch. + /// @return batchId_ The batch ID of the last synced transition. + /// @return blockId_ The block ID of the last synced block. + /// @return ts_ The last synced transition. + function getLastSyncedTransition() + external + view + returns (uint64 batchId_, uint64 blockId_, TransitionState memory ts_); + + /// @notice Retrieves the transition used for verifying a batch. + /// @param _batchId The batch ID. + /// @return The transition used for verifying the batch. + function getBatchVerifyingTransition(uint64 _batchId) + external + view + returns (TransitionState memory); + + /// @notice Retrieves the current protocol configuration. + /// @return The current configuration. + function pacayaConfig() external view returns (Config memory); +} diff --git a/packages/protocol/contracts/layer1/based/TaikoInbox.sol b/packages/protocol/contracts/layer1/based/TaikoInbox.sol new file mode 100644 index 00000000000..895d84a7b85 --- /dev/null +++ b/packages/protocol/contracts/layer1/based/TaikoInbox.sol @@ -0,0 +1,823 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "src/shared/common/EssentialContract.sol"; +import "src/shared/based/ITaiko.sol"; +import "src/shared/libs/LibAddress.sol"; +import "src/shared/libs/LibMath.sol"; +import "src/shared/libs/LibNetwork.sol"; +import "src/shared/libs/LibStrings.sol"; +import "src/shared/signal/ISignalService.sol"; +import "src/layer1/verifiers/IVerifier.sol"; +import "./ITaikoInbox.sol"; + +/// @title TaikoInbox +/// @notice Acts as the inbox for the Taiko Alethia protocol, a simplified version of the +/// original Taiko-Based Contestable Rollup (BCR). The tier-based proof system and +/// contestation mechanisms have been removed. +/// +/// Key assumptions of this protocol: +/// - Block proposals and proofs are asynchronous. Proofs are not available at proposal time, +/// unlike Taiko Gwyneth, which assumes synchronous composability. +/// - Proofs are presumed error-free and thoroughly validated, with proof type management +/// delegated to IVerifier contracts. +/// +/// @dev Registered in the address resolver as "taiko". +/// @custom:security-contact security@taiko.xyz +abstract contract TaikoInbox is EssentialContract, ITaikoInbox, ITaiko { + using LibMath for uint256; + using SafeERC20 for IERC20; + + State public state; // storage layout much match Ontake fork + uint256[50] private __gap; + + // External functions ------------------------------------------------------------------------ + + constructor(address _resolver) EssentialContract(_resolver) { } + + function init(address _owner, bytes32 _genesisBlockHash) external initializer { + __Taiko_init(_owner, _genesisBlockHash); + } + + /// @notice Proposes a batch of blocks. + /// @param _params ABI-encoded BlockParams. + /// @param _txList The transaction list in calldata. If the txList is empty, blob will be used + /// for data availability. + /// @return info_ The info of the proposed batch. + /// @return meta_ The metadata of the proposed batch. + function proposeBatch( + bytes calldata _params, + bytes calldata _txList + ) + public + nonReentrant + returns (BatchInfo memory info_, BatchMetadata memory meta_) + { + Stats2 memory stats2 = state.stats2; + require(stats2.numBatches >= pacayaConfig().forkHeights.pacaya, ForkNotActivated()); + + Config memory config = pacayaConfig(); + + unchecked { + require( + stats2.numBatches <= stats2.lastVerifiedBatchId + config.maxUnverifiedBatches, + TooManyBatches() + ); + + BatchParams memory params = abi.decode(_params, (BatchParams)); + + { + address operator = resolve(LibStrings.B_INBOX_OPERATOR, true); + if (operator == address(0)) { + require(params.proposer == address(0), CustomProposerNotAllowed()); + params.proposer = msg.sender; + + // blob hashes are only accepted if the caller is trusted. + require(params.blobParams.blobHashes.length == 0, InvalidBlobParams()); + } else { + require(msg.sender == operator, NotInboxOperator()); + require(params.proposer != address(0), CustomProposerMissing()); + } + + if (params.coinbase == address(0)) { + params.coinbase = params.proposer; + } + + if (params.revertIfNotFirstProposal) { + require(state.stats2.lastProposedIn != block.number, NotFirstProposal()); + } + } + + bool calldataUsed = _txList.length != 0; + + if (!calldataUsed) { + if (params.blobParams.blobHashes.length == 0) { + require(params.blobParams.numBlobs != 0, BlobNotSpecified()); + } else { + require(params.blobParams.numBlobs == 0, InvalidBlobParams()); + require(params.blobParams.firstBlobIndex == 0, InvalidBlobParams()); + } + } + + // Keep track of last batch's information. + Batch storage lastBatch = + state.batches[(stats2.numBatches - 1) % config.batchRingBufferSize]; + + (uint64 anchorBlockId, uint64 lastBlockTimestamp) = _validateBatchParams( + params, + config.maxAnchorHeightOffset, + config.maxSignalsToReceive, + config.maxBlocksPerBatch, + lastBatch + ); + + // This section constructs the metadata for the proposed batch, which is crucial for + // nodes/clients to process the batch. The metadata itself is not stored on-chain; + // instead, only its hash is kept. + // The metadata must be supplied as calldata prior to proving the batch, enabling the + // computation and verification of its integrity through the comparison of the metahash. + // + // Note that `difficulty` has been removed from the metadata. The client and prover must + // use + // the following approach to calculate a block's difficulty: + // `keccak256(abi.encode("TAIKO_DIFFICULTY", block.number))` + info_ = BatchInfo({ + txsHash: bytes32(0), // to be initialised later + // + // Data to build L2 blocks + blocks: params.blocks, + blobHashes: new bytes32[](0), // to be initialised later + extraData: bytes32(uint256(config.baseFeeConfig.sharingPctg)), + coinbase: params.coinbase, + proposedIn: uint64(block.number), + blobByteOffset: params.blobParams.byteOffset, + blobByteSize: params.blobParams.byteSize, + gasLimit: config.blockMaxGasLimit, + lastBlockId: 0, // to be initialised later + lastBlockTimestamp: lastBlockTimestamp, + // + // Data for the L2 anchor transaction, shared by all blocks in the batch + anchorBlockId: anchorBlockId, + anchorBlockHash: blockhash(anchorBlockId), + anchorInput: params.anchorInput, + baseFeeConfig: config.baseFeeConfig, + signalSlots: params.signalSlots + }); + + require(info_.anchorBlockHash != 0, ZeroAnchorBlockHash()); + + info_.lastBlockId = stats2.numBatches == config.forkHeights.pacaya + ? stats2.numBatches + uint64(params.blocks.length) - 1 + : lastBatch.lastBlockId + uint64(params.blocks.length); + + (info_.txsHash, info_.blobHashes) = + _calculateTxsHash(keccak256(_txList), params.blobParams); + + meta_ = BatchMetadata({ + infoHash: keccak256(abi.encode(info_)), + proposer: params.proposer, + batchId: stats2.numBatches, + proposedAt: uint64(block.timestamp) + }); + + Batch storage batch = state.batches[stats2.numBatches % config.batchRingBufferSize]; + + // SSTORE #1 + batch.metaHash = keccak256(abi.encode(meta_)); + + // SSTORE #2 {{ + batch.batchId = stats2.numBatches; + batch.lastBlockTimestamp = lastBlockTimestamp; + batch.anchorBlockId = anchorBlockId; + batch.nextTransitionId = 1; + batch.verifiedTransitionId = 0; + batch.reserved4 = 0; + // SSTORE }} + + uint96 livenessBond = + config.livenessBondBase + config.livenessBondPerBlock * uint96(params.blocks.length); + _debitBond(params.proposer, livenessBond); + + // SSTORE #3 {{ + batch.lastBlockId = info_.lastBlockId; + batch.reserved3 = 0; + batch.livenessBond = livenessBond; + // SSTORE }} + + stats2.numBatches += 1; + stats2.lastProposedIn = uint56(block.number); + + emit BatchProposed(info_, meta_, _txList); + } // end-of-unchecked + + _verifyBatches(config, stats2, 1); + } + + /// @notice Proves multiple batches with a single aggregated proof. + /// @param _params ABI-encoded parameter containing: + /// - metas: Array of metadata for each batch being proved. + /// - transitions: Array of batch transitions to be proved. + /// @param _proof The aggregated cryptographic proof proving the batches transitions. + function proveBatches(bytes calldata _params, bytes calldata _proof) external nonReentrant { + (BatchMetadata[] memory metas, Transition[] memory trans) = + abi.decode(_params, (BatchMetadata[], Transition[])); + + require(metas.length != 0, NoBlocksToProve()); + require(metas.length == trans.length, ArraySizesMismatch()); + + Stats2 memory stats2 = state.stats2; + require(!stats2.paused, ContractPaused()); + + Config memory config = pacayaConfig(); + IVerifier.Context[] memory ctxs = new IVerifier.Context[](metas.length); + + bool hasConflictingProof; + for (uint256 i; i < metas.length; ++i) { + BatchMetadata memory meta = metas[i]; + + require(meta.batchId >= pacayaConfig().forkHeights.pacaya, ForkNotActivated()); + + require(meta.batchId > stats2.lastVerifiedBatchId, BatchNotFound()); + require(meta.batchId < stats2.numBatches, BatchNotFound()); + + Transition memory tran = trans[i]; + require(tran.parentHash != 0, InvalidTransitionParentHash()); + require(tran.blockHash != 0, InvalidTransitionBlockHash()); + require(tran.stateRoot != 0, InvalidTransitionStateRoot()); + + ctxs[i].batchId = meta.batchId; + ctxs[i].metaHash = keccak256(abi.encode(meta)); + ctxs[i].transition = tran; + + // Verify the batch's metadata. + uint256 slot = meta.batchId % config.batchRingBufferSize; + Batch storage batch = state.batches[slot]; + require(ctxs[i].metaHash == batch.metaHash, MetaHashMismatch()); + + // Finds out if this transition is overwriting an existing one (with the same parent + // hash) or is a new one. + uint24 tid; + uint24 nextTransitionId = batch.nextTransitionId; + if (nextTransitionId > 1) { + // This batch has at least one transition. + if (state.transitions[slot][1].parentHash == tran.parentHash) { + // Overwrite the first transition. + tid = 1; + } else if (nextTransitionId > 2) { + // Retrieve the transition ID using the parent hash from the mapping. If the ID + // is 0, it indicates a new transition; otherwise, it's an overwrite of an + // existing transition. + tid = state.transitionIds[meta.batchId][tran.parentHash]; + } + } + + if (tid == 0) { + // This transition is new, we need to use the next available ID. + unchecked { + tid = batch.nextTransitionId++; + } + } else { + TransitionState memory _ts = state.transitions[slot][tid]; + + bool isSameTransition = _ts.blockHash == tran.blockHash + && (_ts.stateRoot == 0 || _ts.stateRoot == tran.stateRoot); + require(!isSameTransition, SameTransition()); + + hasConflictingProof = true; + emit ConflictingProof(meta.batchId, _ts, tran); + } + + TransitionState storage ts = state.transitions[slot][tid]; + + ts.blockHash = tran.blockHash; + ts.stateRoot = + meta.batchId % config.stateRootSyncInternal == 0 ? tran.stateRoot : bytes32(0); + + bool inProvingWindow; + unchecked { + inProvingWindow = block.timestamp + <= uint256(meta.proposedAt).max(stats2.lastUnpausedAt) + config.provingWindow; + } + + ts.inProvingWindow = inProvingWindow; + ts.prover = inProvingWindow ? meta.proposer : msg.sender; + ts.createdAt = uint48(block.timestamp); + + if (tid == 1) { + ts.parentHash = tran.parentHash; + } else { + state.transitionIds[meta.batchId][tran.parentHash] = tid; + } + } + + address verifier = resolve(LibStrings.B_PROOF_VERIFIER, false); + IVerifier(verifier).verifyProof(ctxs, _proof); + + // Emit the event + { + uint64[] memory batchIds = new uint64[](metas.length); + for (uint256 i; i < metas.length; ++i) { + batchIds[i] = metas[i].batchId; + } + + emit BatchesProved(verifier, batchIds, trans); + } + + if (hasConflictingProof) { + _pause(); + emit Paused(verifier); + } else { + _verifyBatches(config, stats2, metas.length); + } + } + + /// @notice Verify batches by providing the length of the batches to verify. + /// @dev This function is necessary to upgrade from this fork to the next one. + /// @param _length Specifis how many batches to verify. The max number of batches to verify is + /// `pacayaConfig().maxBatchesToVerify * _length`. + function verifyBatches(uint64 _length) + external + nonZeroValue(_length) + nonReentrant + whenNotPaused + { + _verifyBatches(pacayaConfig(), state.stats2, _length); + } + + /// @notice Manually write a transition for a batch. + /// @dev This function is supposed to be used by the owner to force prove a transition for a + /// block that has not been verified. + function writeTransition( + uint64 _batchId, + bytes32 _parentHash, + bytes32 _blockHash, + bytes32 _stateRoot, + address _prover, + bool _inProvingWindow + ) + external + onlyOwner + { + require(_blockHash != 0 && _parentHash != 0 && _stateRoot != 0, InvalidParams()); + require(_batchId > state.stats2.lastVerifiedBatchId, BatchVerified()); + + Config memory config = pacayaConfig(); + uint256 slot = _batchId % config.batchRingBufferSize; + Batch storage batch = state.batches[slot]; + require(batch.batchId == _batchId, BatchNotFound()); + + uint24 tid = state.transitionIds[_batchId][_parentHash]; + if (tid == 0) { + tid = batch.nextTransitionId++; + } + + TransitionState storage ts = state.transitions[slot][tid]; + ts.stateRoot = _batchId % config.stateRootSyncInternal == 0 ? _stateRoot : bytes32(0); + ts.blockHash = _blockHash; + ts.prover = _prover; + ts.inProvingWindow = _inProvingWindow; + ts.createdAt = uint48(block.timestamp); + + if (tid == 1) { + ts.parentHash = _parentHash; + } else { + state.transitionIds[_batchId][_parentHash] = tid; + } + + emit TransitionWritten( + _batchId, + tid, + TransitionState( + _parentHash, + _blockHash, + _stateRoot, + _prover, + _inProvingWindow, + uint48(block.timestamp) + ) + ); + } + + /// @inheritdoc ITaikoInbox + function depositBond(uint256 _amount) external payable whenNotPaused { + state.bondBalance[msg.sender] += _handleDeposit(msg.sender, _amount); + } + + /// @inheritdoc ITaikoInbox + function withdrawBond(uint256 _amount) external whenNotPaused { + uint256 balance = state.bondBalance[msg.sender]; + require(balance >= _amount, InsufficientBond()); + + emit BondWithdrawn(msg.sender, _amount); + + state.bondBalance[msg.sender] -= _amount; + + address bond = bondToken(); + if (bond != address(0)) { + IERC20(bond).safeTransfer(msg.sender, _amount); + } else { + LibAddress.sendEtherAndVerify(msg.sender, _amount); + } + } + + /// @inheritdoc ITaikoInbox + function getStats1() external view returns (Stats1 memory) { + return state.stats1; + } + + /// @inheritdoc ITaikoInbox + function getStats2() external view returns (Stats2 memory) { + return state.stats2; + } + + /// @inheritdoc ITaikoInbox + function getTransitionById( + uint64 _batchId, + uint24 _tid + ) + external + view + returns (TransitionState memory) + { + Config memory config = pacayaConfig(); + uint256 slot = _batchId % config.batchRingBufferSize; + Batch storage batch = state.batches[slot]; + require(batch.batchId == _batchId, BatchNotFound()); + require(_tid != 0 && _tid < batch.nextTransitionId, TransitionNotFound()); + return state.transitions[slot][_tid]; + } + + /// @inheritdoc ITaikoInbox + function getTransitionByParentHash( + uint64 _batchId, + bytes32 _parentHash + ) + external + view + returns (TransitionState memory) + { + Config memory config = pacayaConfig(); + uint256 slot = _batchId % config.batchRingBufferSize; + Batch storage batch = state.batches[slot]; + require(batch.batchId == _batchId, BatchNotFound()); + + uint24 tid = state.transitionIds[_batchId][_parentHash]; + require(tid != 0 && tid < batch.nextTransitionId, TransitionNotFound()); + return state.transitions[slot][tid]; + } + + /// @inheritdoc ITaikoInbox + function getLastVerifiedTransition() + external + view + returns (uint64 batchId_, uint64 blockId_, TransitionState memory ts_) + { + batchId_ = state.stats2.lastVerifiedBatchId; + blockId_ = getBatch(batchId_).lastBlockId; + ts_ = getBatchVerifyingTransition(batchId_); + } + + /// @inheritdoc ITaikoInbox + function getLastSyncedTransition() + external + view + returns (uint64 batchId_, uint64 blockId_, TransitionState memory ts_) + { + batchId_ = state.stats1.lastSyncedBatchId; + blockId_ = getBatch(batchId_).lastBlockId; + ts_ = getBatchVerifyingTransition(batchId_); + } + + /// @inheritdoc ITaikoInbox + function bondBalanceOf(address _user) external view returns (uint256) { + return state.bondBalance[_user]; + } + + /// @notice Determines the operational layer of the contract, whether it is on Layer 1 (L1) or + /// Layer 2 (L2). + /// @return True if the contract is operating on L1, false if on L2. + function isOnL1() external pure override returns (bool) { + return true; + } + + // Public functions ------------------------------------------------------------------------- + + /// @inheritdoc EssentialContract + function paused() public view override returns (bool) { + return state.stats2.paused; + } + + /// @inheritdoc ITaikoInbox + function bondToken() public view returns (address) { + return resolve(LibStrings.B_BOND_TOKEN, true); + } + + /// @inheritdoc ITaikoInbox + function getBatch(uint64 _batchId) public view returns (Batch memory batch_) { + Config memory config = pacayaConfig(); + + batch_ = state.batches[_batchId % config.batchRingBufferSize]; + require(batch_.batchId == _batchId, BatchNotFound()); + } + + /// @inheritdoc ITaikoInbox + function getBatchVerifyingTransition(uint64 _batchId) + public + view + returns (TransitionState memory ts_) + { + Config memory config = pacayaConfig(); + + uint64 slot = _batchId % config.batchRingBufferSize; + Batch storage batch = state.batches[slot]; + require(batch.batchId == _batchId, BatchNotFound()); + + if (batch.verifiedTransitionId != 0) { + ts_ = state.transitions[slot][batch.verifiedTransitionId]; + } + } + + /// @inheritdoc ITaikoInbox + function pacayaConfig() public view virtual returns (Config memory); + + // Internal functions ---------------------------------------------------------------------- + + function __Taiko_init(address _owner, bytes32 _genesisBlockHash) internal onlyInitializing { + __Essential_init(_owner); + + require(_genesisBlockHash != 0, InvalidGenesisBlockHash()); + state.transitions[0][1].blockHash = _genesisBlockHash; + + Batch storage batch = state.batches[0]; + batch.metaHash = bytes32(uint256(1)); + batch.lastBlockTimestamp = uint64(block.timestamp); + batch.anchorBlockId = uint64(block.number); + batch.nextTransitionId = 2; + batch.verifiedTransitionId = 1; + + state.stats1.genesisHeight = uint64(block.number); + + state.stats2.lastProposedIn = uint56(block.number); + state.stats2.numBatches = 1; + + emit BatchesVerified(0, _genesisBlockHash); + } + + function _unpause() internal override { + state.stats2.lastUnpausedAt = uint64(block.timestamp); + state.stats2.paused = false; + } + + function _pause() internal override { + state.stats2.paused = true; + } + + function _calculateTxsHash( + bytes32 _txListHash, + BlobParams memory _blobParams + ) + internal + view + virtual + returns (bytes32 hash_, bytes32[] memory blobHashes_) + { + unchecked { + if (_blobParams.blobHashes.length != 0) { + blobHashes_ = _blobParams.blobHashes; + } else { + blobHashes_ = new bytes32[](_blobParams.numBlobs); + for (uint256 i; i < _blobParams.numBlobs; ++i) { + blobHashes_[i] = blobhash(_blobParams.firstBlobIndex + i); + } + } + + for (uint256 i; i < blobHashes_.length; ++i) { + require(blobHashes_[i] != 0, BlobNotFound()); + } + hash_ = keccak256(abi.encode(_txListHash, blobHashes_)); + } + } + + // Private functions ----------------------------------------------------------------------- + + function _verifyBatches( + Config memory _config, + Stats2 memory _stats2, + uint256 _length + ) + private + { + uint64 batchId = _stats2.lastVerifiedBatchId; + + bool canVerifyBlocks; + unchecked { + uint64 pacayaForkHeight = pacayaConfig().forkHeights.pacaya; + canVerifyBlocks = pacayaForkHeight == 0 || batchId >= pacayaForkHeight - 1; + } + + if (canVerifyBlocks) { + uint256 slot = batchId % _config.batchRingBufferSize; + Batch storage batch = state.batches[slot]; + uint24 tid = batch.verifiedTransitionId; + bytes32 blockHash = state.transitions[slot][tid].blockHash; + + SyncBlock memory synced; + + uint256 stopBatchId; + unchecked { + stopBatchId = ( + _config.maxBatchesToVerify * _length + _stats2.lastVerifiedBatchId + 1 + ).min(_stats2.numBatches); + } + + for (++batchId; batchId < stopBatchId; ++batchId) { + slot = batchId % _config.batchRingBufferSize; + batch = state.batches[slot]; + uint24 nextTransitionId = batch.nextTransitionId; + + if (paused()) break; + if (nextTransitionId <= 1) break; + + TransitionState storage ts = state.transitions[slot][1]; + if (ts.parentHash == blockHash) { + tid = 1; + } else if (nextTransitionId > 2) { + uint24 _tid = state.transitionIds[batchId][blockHash]; + if (_tid == 0) break; + tid = _tid; + ts = state.transitions[slot][tid]; + } else { + break; + } + + unchecked { + if (ts.createdAt + _config.cooldownWindow > block.timestamp) { + break; + } + } + + blockHash = ts.blockHash; + + uint96 bondToReturn = + ts.inProvingWindow ? batch.livenessBond : batch.livenessBond / 2; + _creditBond(ts.prover, bondToReturn); + + if (batchId % _config.stateRootSyncInternal == 0) { + synced.batchId = batchId; + synced.blockId = batch.lastBlockId; + synced.tid = tid; + synced.stateRoot = ts.stateRoot; + } + + for (uint24 i = 2; i < nextTransitionId; ++i) { + ts = state.transitions[slot][i]; + delete state.transitionIds[batchId][ts.parentHash]; + } + } + + unchecked { + --batchId; + } + + if (_stats2.lastVerifiedBatchId != batchId) { + _stats2.lastVerifiedBatchId = batchId; + + batch = state.batches[_stats2.lastVerifiedBatchId % _config.batchRingBufferSize]; + batch.verifiedTransitionId = tid; + emit BatchesVerified(_stats2.lastVerifiedBatchId, blockHash); + + if (synced.batchId != 0) { + if (synced.batchId != _stats2.lastVerifiedBatchId) { + // We write the synced batch's verifiedTransitionId to storage + batch = state.batches[synced.batchId % _config.batchRingBufferSize]; + batch.verifiedTransitionId = synced.tid; + } + + Stats1 memory stats1 = state.stats1; + stats1.lastSyncedBatchId = batch.batchId; + stats1.lastSyncedAt = uint64(block.timestamp); + state.stats1 = stats1; + + emit Stats1Updated(stats1); + + // Ask signal service to write cross chain signal + ISignalService(resolve(LibStrings.B_SIGNAL_SERVICE, false)).syncChainData( + _config.chainId, LibStrings.H_STATE_ROOT, synced.blockId, synced.stateRoot + ); + } + } + } + + state.stats2 = _stats2; + emit Stats2Updated(_stats2); + } + + function _debitBond(address _user, uint256 _amount) private { + if (_amount == 0) return; + + uint256 balance = state.bondBalance[_user]; + if (balance >= _amount) { + unchecked { + state.bondBalance[_user] = balance - _amount; + } + } else { + uint256 amountDeposited = _handleDeposit(_user, _amount); + require(amountDeposited == _amount, InsufficientBond()); + } + emit BondDebited(_user, _amount); + } + + function _creditBond(address _user, uint256 _amount) private { + if (_amount == 0) return; + unchecked { + state.bondBalance[_user] += _amount; + } + emit BondCredited(_user, _amount); + } + + function _handleDeposit( + address _user, + uint256 _amount + ) + private + returns (uint256 amountDeposited_) + { + address bond = bondToken(); + + if (bond != address(0)) { + require(msg.value == 0, MsgValueNotZero()); + + uint256 balance = IERC20(bond).balanceOf(address(this)); + IERC20(bond).safeTransferFrom(_user, address(this), _amount); + amountDeposited_ = IERC20(bond).balanceOf(address(this)) - balance; + } else { + require(msg.value == _amount, EtherNotPaidAsBond()); + amountDeposited_ = _amount; + } + emit BondDeposited(_user, amountDeposited_); + } + + function _validateBatchParams( + BatchParams memory _params, + uint64 _maxAnchorHeightOffset, + uint8 _maxSignalsToReceive, + uint16 _maxBlocksPerBatch, + Batch memory _lastBatch + ) + private + view + returns (uint64 anchorBlockId_, uint64 lastBlockTimestamp_) + { + unchecked { + if (_params.anchorBlockId == 0) { + anchorBlockId_ = uint64(block.number - 1); + } else { + require( + _params.anchorBlockId + _maxAnchorHeightOffset >= block.number, + AnchorBlockIdTooSmall() + ); + require(_params.anchorBlockId < block.number, AnchorBlockIdTooLarge()); + require( + _params.anchorBlockId >= _lastBatch.anchorBlockId, + AnchorBlockIdSmallerThanParent() + ); + anchorBlockId_ = _params.anchorBlockId; + } + + lastBlockTimestamp_ = _params.lastBlockTimestamp == 0 + ? uint64(block.timestamp) + : _params.lastBlockTimestamp; + + require(lastBlockTimestamp_ <= block.timestamp, TimestampTooLarge()); + + uint64 totalShift; + for (uint256 i; i < _params.blocks.length; ++i) { + totalShift += _params.blocks[i].timeShift; + } + + require(lastBlockTimestamp_ >= totalShift, TimestampTooSmall()); + + uint64 firstBlockTimestamp = lastBlockTimestamp_ - totalShift; + + require( + firstBlockTimestamp + _maxAnchorHeightOffset * LibNetwork.ETHEREUM_BLOCK_TIME + >= block.timestamp, + TimestampTooSmall() + ); + + require( + firstBlockTimestamp >= _lastBatch.lastBlockTimestamp, TimestampSmallerThanParent() + ); + + // make sure the batch builds on the expected latest chain state. + require( + _params.parentMetaHash == 0 || _params.parentMetaHash == _lastBatch.metaHash, + ParentMetaHashMismatch() + ); + } + + if (_params.signalSlots.length != 0) { + require(_params.signalSlots.length <= _maxSignalsToReceive, TooManySignals()); + + ISignalService signalService = + ISignalService(resolve(LibStrings.B_SIGNAL_SERVICE, false)); + + for (uint256 i; i < _params.signalSlots.length; ++i) { + require(signalService.isSignalSent(_params.signalSlots[i]), SignalNotSent()); + } + } + + require(_params.blocks.length != 0, BlockNotFound()); + require(_params.blocks.length <= _maxBlocksPerBatch, TooManyBlocks()); + } + + // Memory-only structs ---------------------------------------------------------------------- + + struct SyncBlock { + uint64 batchId; + uint64 blockId; + uint24 tid; + bytes32 stateRoot; + } +} diff --git a/packages/protocol/contracts/layer1/forced-inclusion/ForcedInclusionStore.sol b/packages/protocol/contracts/layer1/forced-inclusion/ForcedInclusionStore.sol new file mode 100644 index 00000000000..0f865b5235b --- /dev/null +++ b/packages/protocol/contracts/layer1/forced-inclusion/ForcedInclusionStore.sol @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "src/shared/common/EssentialContract.sol"; +import "src/shared/libs/LibMath.sol"; +import "src/shared/libs/LibAddress.sol"; +import "src/shared/libs/LibStrings.sol"; +import "src/layer1/based/ITaikoInbox.sol"; +import "./IForcedInclusionStore.sol"; + +/// @title ForcedInclusionStore +/// @dev A contract for storing and managing forced inclusion requests. Forced inclusions allow +/// users to pay a fee +/// to ensure their transactions are included in a block. The contract maintains a FIFO queue +/// of inclusion requests. +/// @custom:security-contact +contract ForcedInclusionStore is EssentialContract, IForcedInclusionStore { + using LibAddress for address; + using LibMath for uint256; + + uint256 private constant SECONDS_PER_BLOCK = 12; + + uint8 public immutable inclusionDelay; + uint64 public immutable feeInGwei; + + mapping(uint256 id => ForcedInclusion inclusion) public queue; // slot 1 + uint64 public head; // slot 2 + uint64 public tail; + uint64 public lastProcessedAtBatchId; + uint64 private __reserved1; + + uint256[48] private __gap; + + constructor( + address _resolver, + uint8 _inclusionDelay, + uint64 _feeInGwei + ) + EssentialContract(_resolver) + { + require(_inclusionDelay != 0 && _inclusionDelay % SECONDS_PER_BLOCK == 0, InvalidParams()); + require(_feeInGwei != 0, InvalidParams()); + + inclusionDelay = _inclusionDelay; + feeInGwei = _feeInGwei; + } + + function init(address _owner) external initializer { + __Essential_init(_owner); + } + + function storeForcedInclusion( + uint8 blobIndex, + uint32 blobByteOffset, + uint32 blobByteSize + ) + external + payable + nonReentrant + { + bytes32 blobHash = _blobHash(blobIndex); + require(blobHash != bytes32(0), BlobNotFound()); + require(msg.value == feeInGwei * 1 gwei, IncorrectFee()); + + ITaikoInbox inbox = ITaikoInbox(resolve(LibStrings.B_TAIKO, false)); + + ForcedInclusion memory inclusion = ForcedInclusion({ + blobHash: blobHash, + feeInGwei: uint64(msg.value / 1 gwei), + createdAtBatchId: inbox.getStats2().numBatches, + blobByteOffset: blobByteOffset, + blobByteSize: blobByteSize + }); + + queue[tail++] = inclusion; + + emit ForcedInclusionStored(inclusion); + } + + function consumeOldestForcedInclusion(address _feeRecipient) + external + nonReentrant + onlyFromNamed(LibStrings.B_TAIKO_WRAPPER) + returns (ForcedInclusion memory inclusion_) + { + // we only need to check the first one, since it will be the oldest. + uint64 _head = head; + ForcedInclusion storage inclusion = queue[_head]; + require(inclusion.createdAtBatchId != 0, NoForcedInclusionFound()); + + ITaikoInbox inbox = ITaikoInbox(resolve(LibStrings.B_TAIKO, false)); + + inclusion_ = inclusion; + delete queue[_head]; + + unchecked { + lastProcessedAtBatchId = inbox.getStats2().numBatches; + head = _head + 1; + } + + emit ForcedInclusionConsumed(inclusion_); + _feeRecipient.sendEtherAndVerify(inclusion_.feeInGwei * 1 gwei); + } + + function getForcedInclusion(uint256 index) external view returns (ForcedInclusion memory) { + return queue[index]; + } + + function getOldestForcedInclusionDeadline() public view returns (uint256) { + unchecked { + ForcedInclusion storage inclusion = queue[head]; + return inclusion.createdAtBatchId == 0 + ? type(uint64).max + : uint256(lastProcessedAtBatchId).max(inclusion.createdAtBatchId) + inclusionDelay; + } + } + + function isOldestForcedInclusionDue() external view returns (bool) { + ITaikoInbox inbox = ITaikoInbox(resolve(LibStrings.B_TAIKO, false)); + return inbox.getStats2().numBatches >= getOldestForcedInclusionDeadline(); + } + + // @dev Override this function for easier testing blobs + function _blobHash(uint8 blobIndex) internal view virtual returns (bytes32) { + return blobhash(blobIndex); + } +} diff --git a/packages/protocol/contracts/layer1/forced-inclusion/IForcedInclusionStore.sol b/packages/protocol/contracts/layer1/forced-inclusion/IForcedInclusionStore.sol new file mode 100644 index 00000000000..dae28021a52 --- /dev/null +++ b/packages/protocol/contracts/layer1/forced-inclusion/IForcedInclusionStore.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +/// @title IForcedInclusionStore +/// @custom:security-contact security@taiko.xyz +interface IForcedInclusionStore { + /// @dev Error thrown when a blob is not found. + error BlobNotFound(); + /// @dev Error thrown when the parameters are invalid. + error InvalidParams(); + /// @dev Error thrown when the fee is incorrect. + error IncorrectFee(); + + error NoForcedInclusionFound(); + + /// @dev Event emitted when a forced inclusion is stored. + event ForcedInclusionStored(ForcedInclusion forcedInclusion); + /// @dev Event emitted when a forced inclusion is consumed. + event ForcedInclusionConsumed(ForcedInclusion forcedInclusion); + + struct ForcedInclusion { + bytes32 blobHash; + uint64 feeInGwei; + uint64 createdAtBatchId; + uint32 blobByteOffset; + uint32 blobByteSize; + } + + /// @dev Retrieve a forced inclusion request by its index. + /// @param index The index of the forced inclusion request in the queue. + /// @return The forced inclusion request at the specified index. + function getForcedInclusion(uint256 index) external view returns (ForcedInclusion memory); + + /// @dev Get the deadline for the oldest forced inclusion. + /// @return The deadline for the oldest forced inclusion. + function getOldestForcedInclusionDeadline() external view returns (uint256); + + /// @dev Check if the oldest forced inclusion is due. + /// @return True if the oldest forced inclusion is due, false otherwise. + function isOldestForcedInclusionDue() external view returns (bool); + + /// @dev Consume a forced inclusion request. + /// The inclusion request must be marked as processed and the priority fee must be paid to the + /// caller. + /// @param _feeRecipient The address to receive the priority fee. + /// @return inclusion_ The forced inclusion request. + function consumeOldestForcedInclusion(address _feeRecipient) + external + returns (ForcedInclusion memory); + + /// @dev Store a forced inclusion request. + /// The priority fee must be paid to the contract. + /// @param blobIndex The index of the blob that contains the transaction data. + /// @param blobByteOffset The byte offset in the blob + /// @param blobByteSize The size of the blob in bytes + function storeForcedInclusion( + uint8 blobIndex, + uint32 blobByteOffset, + uint32 blobByteSize + ) + external + payable; +} diff --git a/packages/protocol/contracts/layer1/forced-inclusion/TaikoWrapper.sol b/packages/protocol/contracts/layer1/forced-inclusion/TaikoWrapper.sol new file mode 100644 index 00000000000..6f5b02d4010 --- /dev/null +++ b/packages/protocol/contracts/layer1/forced-inclusion/TaikoWrapper.sol @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "src/shared/common/EssentialContract.sol"; +import "src/shared/based/ITaiko.sol"; +import "src/shared/libs/LibMath.sol"; +import "src/shared/libs/LibNetwork.sol"; +import "src/shared/libs/LibStrings.sol"; +import "src/shared/signal/ISignalService.sol"; +import "src/layer1/verifiers/IVerifier.sol"; +import "src/layer1/based/TaikoInbox.sol"; +import "./ForcedInclusionStore.sol"; + +/// @title TaikoWrapper +/// @dev This contract is part of a delayed inbox implementation to enforce the inclusion of +/// transactions. +/// The current design is a simplified and can be improved with the following ideas: +/// 1. **Fee-Based Request Prioritization**: +/// - Proposers can selectively fulfill pending requests based on transaction fees. +/// - Requests not yet due can be processed earlier if fees are attractive, incentivizing timely +/// execution. +/// +/// 2. **Rate Limit Control**: +/// - A rate-limiting mechanism ensures a minimum interval of 12*N seconds between request +/// fulfillments. +/// - Prevents proposers from being overwhelmed during high request volume, ensuring system +/// stability. +/// +/// 3. **Calldata and Blob Support**: +/// - Supports both calldata and blobs in the transaction list. +/// +/// 4. **Gas-Efficient Request Storage**: +/// - Avoids storing full request data in contract storage. +/// - Saves only the request hash and its timestamp. +/// - Leverages Ethereum events to store request details off-chain. +/// - Proposers can reconstruct requests as needed, minimizing on-chain storage and gas +/// consumption. +/// +/// @custom:security-contact security@taiko.xyz + +contract TaikoWrapper is EssentialContract { + using LibMath for uint256; + + /// @dev Event emitted when a forced inclusion is processed. + event ForcedInclusionProcessed(IForcedInclusionStore.ForcedInclusion); + /// @dev Error thrown when the oldest forced inclusion is due. + + error OldestForcedInclusionDue(); + + uint16 public constant MAX_FORCED_TXS_PER_FORCED_INCLUSION = 512; + + uint256[50] private __gap; + + constructor(address _resolver) EssentialContract(_resolver) { } + + function init(address _owner) external initializer { + __Essential_init(_owner); + } + + /// @notice Proposes a batch of blocks with forced inclusion. + /// @param _forcedInclusionParams An optional ABI-encoded BlockParams for the forced inclusion + /// batch. + /// @param _params ABI-encoded BlockParams. + /// @param _txList The transaction list in calldata. If the txList is empty, blob will be used + /// for data availability. + /// @return info_ The info of the proposed batch. + /// @return meta_ The metadata of the proposed batch. + function proposeBatchWithForcedInclusion( + bytes calldata _forcedInclusionParams, + bytes calldata _params, + bytes calldata _txList + ) + external + nonReentrant + returns (ITaikoInbox.BatchInfo memory info_, ITaikoInbox.BatchMetadata memory meta_) + { + ITaikoInbox inbox = ITaikoInbox(resolve(LibStrings.B_TAIKO, false)); + + IForcedInclusionStore store = + IForcedInclusionStore(resolve(LibStrings.B_FORCED_INCLUSION_STORE, false)); + + if (_forcedInclusionParams.length == 0) { + require(!store.isOldestForcedInclusionDue(), OldestForcedInclusionDue()); + } else { + IForcedInclusionStore.ForcedInclusion memory inclusion = + store.consumeOldestForcedInclusion(msg.sender); + + ITaikoInbox.BatchParams memory params = + abi.decode(_forcedInclusionParams, (ITaikoInbox.BatchParams)); + + // Overwrite the batch params to have only 1 block and up to + // MAX_FORCED_TXS_PER_FORCED_INCLUSION transactions + if (params.blocks.length == 0) { + params.blocks = new ITaikoInbox.BlockParams[](1); + } + + if (params.blocks[0].numTransactions < MAX_FORCED_TXS_PER_FORCED_INCLUSION) { + params.blocks[0].numTransactions = MAX_FORCED_TXS_PER_FORCED_INCLUSION; + } + + params.blobParams.blobHashes = new bytes32[](1); + params.blobParams.blobHashes[0] = inclusion.blobHash; + params.blobParams.byteOffset = inclusion.blobByteOffset; + params.blobParams.byteSize = inclusion.blobByteSize; + + inbox.proposeBatch(abi.encode(params), ""); + emit ForcedInclusionProcessed(inclusion); + } + + (info_, meta_) = inbox.proposeBatch(_params, _txList); + } +} diff --git a/packages/protocol/contracts/layer1/fork-router/ForkRouter.sol b/packages/protocol/contracts/layer1/fork-router/ForkRouter.sol new file mode 100644 index 00000000000..410e38d8b8e --- /dev/null +++ b/packages/protocol/contracts/layer1/fork-router/ForkRouter.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; + +/// @title ForkRouter +/// @custom:security-contact security@taiko.xyz +/// @notice This contract routes calls to the current fork. +/// +/// +--> newFork +/// PROXY -> FORK_ROUTER--| +/// +--> oldFork +contract ForkRouter is UUPSUpgradeable, Ownable2StepUpgradeable { + address public immutable oldFork; + address public immutable newFork; + + error InvalidParams(); + error ZeroForkAddress(); + + constructor(address _oldFork, address _newFork) { + require(_newFork != address(0) && _newFork != _oldFork, InvalidParams()); + + oldFork = _oldFork; + newFork = _newFork; + + _disableInitializers(); + } + + fallback() external payable virtual { + _fallback(); + } + + receive() external payable virtual { + _fallback(); + } + + /// @notice Returns true if a function should be routed to the old fork + /// @dev This function should be overridden by the implementation contract + function shouldRouteToOldFork(bytes4) public pure virtual returns (bool) { + return false; + } + + function _fallback() internal virtual { + address fork = shouldRouteToOldFork(msg.sig) ? oldFork : newFork; + require(fork != address(0), ZeroForkAddress()); + + assembly { + calldatacopy(0, 0, calldatasize()) + let result := delegatecall(gas(), fork, 0, calldatasize(), 0, 0) + returndatacopy(0, 0, returndatasize()) + + switch result + case 0 { revert(0, returndatasize()) } + default { return(0, returndatasize()) } + } + } + + function _authorizeUpgrade(address) internal virtual override onlyOwner { } +} diff --git a/packages/protocol/contracts/layer1/fork-router/PacayaForkRouter.sol b/packages/protocol/contracts/layer1/fork-router/PacayaForkRouter.sol new file mode 100644 index 00000000000..47ab7245bec --- /dev/null +++ b/packages/protocol/contracts/layer1/fork-router/PacayaForkRouter.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "./ForkRouter.sol"; + +/// @title IOntakeFork +/// @dev Derived from TaikoL1.sol in the Taiko Ontake fork +/// https://github.com/taikoxyz/taiko-mono/releases/tag/protocol-v1.11.0 +/// @custom:security-contact security@taiko.xyz +interface IOntakeFork { + function proposeBlockV2(bytes calldata, bytes calldata) external; + function proposeBlocksV2(bytes[] calldata, bytes[] calldata) external; + function proveBlock(uint64, bytes calldata) external; + function proveBlocks(uint64[] calldata, bytes[] calldata, bytes calldata) external; + function verifyBlocks(uint64) external; + function getVerifiedBlockProver(uint64) external view; + function getLastVerifiedBlock() external view; + function getBlockV2(uint64) external view; + function getTransition(uint64, uint32) external view; + function getTransition(uint64, bytes32) external; + function getTransitions(uint64[] calldata, bytes32[] calldata) external; + function lastProposedIn() external view; + function getStateVariables() external view; + function getConfig() external pure; + function resolve(uint64, bytes32, bool) external view; + function resolve(bytes32, bool) external view; +} + +/// @title PacayaForkRouter +/// @notice This contract routes calls to the current fork. +/// @custom:security-contact security@taiko.xyz +contract PacayaForkRouter is ForkRouter { + constructor(address _oldFork, address _newFork) ForkRouter(_oldFork, _newFork) { } + + function shouldRouteToOldFork(bytes4 _selector) public pure override returns (bool) { + if ( + _selector == IOntakeFork.proposeBlockV2.selector + || _selector == IOntakeFork.proposeBlocksV2.selector + || _selector == IOntakeFork.proveBlock.selector + || _selector == IOntakeFork.proveBlocks.selector + || _selector == IOntakeFork.verifyBlocks.selector + || _selector == IOntakeFork.getVerifiedBlockProver.selector + || _selector == IOntakeFork.getLastVerifiedBlock.selector + || _selector == IOntakeFork.getBlockV2.selector + || _selector == bytes4(keccak256("getTransition(uint64,uint32)")) + || _selector == bytes4(keccak256("getTransition(uint64,bytes32)")) + || _selector == IOntakeFork.getTransitions.selector + || _selector == IOntakeFork.lastProposedIn.selector + || _selector == IOntakeFork.getStateVariables.selector + || _selector == IOntakeFork.getConfig.selector + || _selector == bytes4(keccak256("resolve(uint64,bytes32,bool)")) + || _selector == bytes4(keccak256("resolve(bytes32,bool)")) + ) return true; + + return false; + } +} diff --git a/packages/protocol/contracts/layer2/based/TaikoAnchor.sol b/packages/protocol/contracts/layer2/based/TaikoAnchor.sol new file mode 100644 index 00000000000..4433632a870 --- /dev/null +++ b/packages/protocol/contracts/layer2/based/TaikoAnchor.sol @@ -0,0 +1,390 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import "src/shared/common/EssentialContract.sol"; +import "src/shared/based/ITaiko.sol"; +import "src/shared/libs/LibStrings.sol"; +import "src/shared/libs/LibAddress.sol"; +import "src/shared/libs/LibMath.sol"; +import "src/shared/signal/ISignalService.sol"; +import "./LibEIP1559.sol"; +import "./LibL2Config.sol"; +import "./IBlockHashProvider.sol"; +import "./TaikoAnchorDeprecated.sol"; + +/// @title TaikoAnchor +/// @notice Taiko L2 is a smart contract that handles cross-layer message +/// verification and manages EIP-1559 gas pricing for Layer 2 (L2) operations. +/// It is used to anchor the latest L1 block details to L2 for cross-layer +/// communication, manage EIP-1559 parameters for gas pricing, and store +/// verified L1 block information. +/// @custom:security-contact security@taiko.xyz +contract TaikoAnchor is EssentialContract, IBlockHashProvider, TaikoAnchorDeprecated { + using LibAddress for address; + using LibMath for uint256; + using SafeERC20 for IERC20; + + /// @notice Golden touch address is the only address that can do the anchor transaction. + address public constant GOLDEN_TOUCH_ADDRESS = 0x0000777735367b36bC9B61C50022d9D0700dB4Ec; + + uint64 public immutable pacayaForkHeight; + + /// @notice Mapping from L2 block numbers to their block hashes. All L2 block hashes will + /// be saved in this mapping. + mapping(uint256 blockId => bytes32 blockHash) private _blockhashes; + + /// @notice A hash to check the integrity of public inputs. + /// @dev Slot 2. + bytes32 public publicInputHash; + + /// @notice The gas excess value used to calculate the base fee. + /// @dev Slot 3. + uint64 public parentGasExcess; + + /// @notice The last synced L1 block height. + uint64 public lastSyncedBlock; + + /// @notice The last L2 block's timestamp. + uint64 public parentTimestamp; + + /// @notice The last L2 block's gas target. + uint64 public parentGasTarget; + + /// @notice The L1's chain ID. + uint64 public l1ChainId; + + /// @notice The arbitrary bytes32 input chosen by the block proposer. + bytes32 public anchorInput; + + uint256[45] private __gap; + + /// @notice Emitted when the latest L1 block details are anchored to L2. + /// @param parentHash The hash of the parent block. + /// @param parentGasExcess The gas excess value used to calculate the base fee. + event Anchored(bytes32 parentHash, uint64 parentGasExcess); + + /// @notice Emitted when the gas target has been updated. + /// @param oldGasTarget The previous gas target. + /// @param newGasTarget The new gas target. + /// @param oldGasExcess The previous gas excess. + /// @param newGasExcess The new gas excess. + /// @param basefee The base fee in this block. + event EIP1559Update( + uint64 oldGasTarget, + uint64 newGasTarget, + uint64 oldGasExcess, + uint64 newGasExcess, + uint256 basefee + ); + + error L2_BASEFEE_MISMATCH(); + error L2_FORK_ERROR(); + error L2_INVALID_L1_CHAIN_ID(); + error L2_INVALID_L2_CHAIN_ID(); + error L2_INVALID_PARAM(); + error L2_INVALID_SENDER(); + error L2_PUBLIC_INPUT_HASH_MISMATCH(); + error L2_TOO_LATE(); + + modifier onlyGoldenTouch() { + require(msg.sender == GOLDEN_TOUCH_ADDRESS, L2_INVALID_SENDER()); + _; + } + + constructor(address _resolver, uint64 _pacayaForkHeight) EssentialContract(_resolver) { + pacayaForkHeight = _pacayaForkHeight; + } + + /// @notice Initializes the contract. + /// @param _owner The owner of this contract. msg.sender will be used if this value is zero. + /// @param _l1ChainId The ID of the base layer. + /// @param _initialGasExcess The initial parentGasExcess. + function init( + address _owner, + uint64 _l1ChainId, + uint64 _initialGasExcess + ) + external + initializer + { + __Essential_init(_owner); + + require(_l1ChainId != 0, L2_INVALID_L1_CHAIN_ID()); + require(_l1ChainId != block.chainid, L2_INVALID_L1_CHAIN_ID()); + require(block.chainid > 1, L2_INVALID_L2_CHAIN_ID()); + require(block.chainid <= type(uint64).max, L2_INVALID_L2_CHAIN_ID()); + + if (block.number == 0) { + // This is the case in real L2 genesis + } else if (block.number == 1) { + // This is the case in tests + uint256 parentHeight = block.number - 1; + _blockhashes[parentHeight] = blockhash(parentHeight); + } else { + revert L2_TOO_LATE(); + } + + l1ChainId = _l1ChainId; + parentGasExcess = _initialGasExcess; + (publicInputHash,) = _calcPublicInputHash(block.number); + } + + /// @notice Anchors the latest L1 block details to L2 for cross-layer + /// message verification. + /// @dev This function can be called freely as the golden touch private key is publicly known, + /// but the Taiko node guarantees the first transaction of each block is always this anchor + /// transaction, and any subsequent calls will revert with L2_PUBLIC_INPUT_HASH_MISMATCH. + /// @param _anchorBlockId The `anchorBlockId` value in this block's metadata. + /// @param _anchorStateRoot The state root for the L1 block with id equals `_anchorBlockId`. + /// @param _anchorInput An arbitrary bytes32 input chosen by the block proposer. + /// @param _parentGasUsed The gas used in the parent block. + /// @param _baseFeeConfig The base fee configuration. + /// @param _signalSlots The signal slots to mark as received. + function anchorV3( + uint64 _anchorBlockId, + bytes32 _anchorStateRoot, + bytes32 _anchorInput, + uint32 _parentGasUsed, + LibSharedData.BaseFeeConfig calldata _baseFeeConfig, + bytes32[] calldata _signalSlots + ) + external + nonZeroBytes32(_anchorStateRoot) + nonZeroValue(_anchorBlockId) + nonZeroValue(_baseFeeConfig.gasIssuancePerSecond) + nonZeroValue(_baseFeeConfig.adjustmentQuotient) + onlyGoldenTouch + nonReentrant + { + require(block.number >= pacayaForkHeight, L2_FORK_ERROR()); + + anchorInput = _anchorInput; + + uint256 parentId = block.number - 1; + _verifyAndUpdatePublicInputHash(parentId); + _verifyBaseFeeAndUpdateGasExcess(_parentGasUsed, _baseFeeConfig); + _syncChainData(_anchorBlockId, _anchorStateRoot); + _updateParentHashAndTimestamp(parentId); + + ISignalService(resolve(LibStrings.B_SIGNAL_SERVICE, false)).receiveSignals(_signalSlots); + } + + /// @notice Anchors the latest L1 block details to L2 for cross-layer + /// message verification. + /// @dev This function can be called freely as the golden touch private key is publicly known, + /// but the Taiko node guarantees the first transaction of each block is always this anchor + /// transaction, and any subsequent calls will revert with L2_PUBLIC_INPUT_HASH_MISMATCH. + /// @param _anchorBlockId The `anchorBlockId` value in this block's metadata. + /// @param _anchorStateRoot The state root for the L1 block with id equals `_anchorBlockId`. + /// @param _parentGasUsed The gas used in the parent block. + /// @param _baseFeeConfig The base fee configuration. + function anchorV2( + uint64 _anchorBlockId, + bytes32 _anchorStateRoot, + uint32 _parentGasUsed, + LibSharedData.BaseFeeConfig calldata _baseFeeConfig + ) + external + nonZeroBytes32(_anchorStateRoot) + nonZeroValue(_anchorBlockId) + nonZeroValue(_baseFeeConfig.gasIssuancePerSecond) + nonZeroValue(_baseFeeConfig.adjustmentQuotient) + onlyGoldenTouch + nonReentrant + { + require(block.number < pacayaForkHeight, L2_FORK_ERROR()); + + uint256 parentId = block.number - 1; + _verifyAndUpdatePublicInputHash(parentId); + _verifyBaseFeeAndUpdateGasExcess(_parentGasUsed, _baseFeeConfig); + _syncChainData(_anchorBlockId, _anchorStateRoot); + _updateParentHashAndTimestamp(parentId); + } + + /// @notice Withdraw token or Ether from this address. + /// Note: This contract receives a portion of L2 base fees, while the remainder is directed to + /// L2 block's coinbase address. + /// @param _token Token address or address(0) if Ether. + /// @param _to Withdraw to address. + function withdraw( + address _token, + address _to + ) + external + nonZeroAddr(_to) + whenNotPaused + onlyFromOwnerOrNamed(LibStrings.B_WITHDRAWER) + nonReentrant + { + if (_token == address(0)) { + _to.sendEtherAndVerify(address(this).balance); + } else { + IERC20(_token).safeTransfer(_to, IERC20(_token).balanceOf(address(this))); + } + } + + /// @notice Calculates the base fee and gas excess using EIP-1559 configuration for the given + /// parameters. + /// @param _parentGasUsed Gas used in the parent block. + /// @param _baseFeeConfig Configuration parameters for base fee calculation. + /// @return basefee_ The calculated EIP-1559 base fee per gas. + /// @return newGasTarget_ The new gas target value. + /// @return newGasExcess_ The new gas excess value. + function getBasefeeV2( + uint32 _parentGasUsed, + uint64 _blockTimestamp, + LibSharedData.BaseFeeConfig calldata _baseFeeConfig + ) + public + view + returns (uint256 basefee_, uint64 newGasTarget_, uint64 newGasExcess_) + { + // uint32 * uint8 will never overflow + uint64 newGasTarget = + uint64(_baseFeeConfig.gasIssuancePerSecond) * _baseFeeConfig.adjustmentQuotient; + + (newGasTarget_, newGasExcess_) = + LibEIP1559.adjustExcess(parentGasTarget, newGasTarget, parentGasExcess); + + uint64 gasIssuance = + (_blockTimestamp - parentTimestamp) * _baseFeeConfig.gasIssuancePerSecond; + + if ( + _baseFeeConfig.maxGasIssuancePerBlock != 0 + && gasIssuance > _baseFeeConfig.maxGasIssuancePerBlock + ) { + gasIssuance = _baseFeeConfig.maxGasIssuancePerBlock; + } + + (basefee_, newGasExcess_) = LibEIP1559.calc1559BaseFee( + newGasTarget_, newGasExcess_, gasIssuance, _parentGasUsed, _baseFeeConfig.minGasExcess + ); + } + + /// @inheritdoc IBlockHashProvider + function getBlockHash(uint256 _blockId) public view returns (bytes32) { + if (_blockId >= block.number) return 0; + if (_blockId + 256 >= block.number) return blockhash(_blockId); + return _blockhashes[_blockId]; + } + + /// @notice Determines the operational layer of the contract, whether it is on Layer 1 (L1) or + /// Layer 2 (L2). + /// @return True if the contract is operating on L1, false if on L2. + function isOnL1() external pure returns (bool) { + return false; + } + + /// @notice Tells if we need to validate basefee (for simulation). + /// @return Returns true to skip checking basefee mismatch. + function skipFeeCheck() public pure virtual returns (bool) { + return false; + } + + /// @dev Synchronizes chain data with the given anchor block ID and state root. + /// @param _anchorBlockId The ID of the anchor block. + /// @param _anchorStateRoot The state root of the anchor block. + function _syncChainData(uint64 _anchorBlockId, bytes32 _anchorStateRoot) private { + /// @dev If the anchor block ID is less than or equal to the last synced block, return + /// early. + if (_anchorBlockId <= lastSyncedBlock) return; + + /// @dev Store the L1's state root as a signal to the local signal service to + /// allow for multi-hop bridging. + ISignalService(resolve(LibStrings.B_SIGNAL_SERVICE, false)).syncChainData( + l1ChainId, LibStrings.H_STATE_ROOT, _anchorBlockId, _anchorStateRoot + ); + + /// @dev Update the last synced block to the current anchor block ID. + lastSyncedBlock = _anchorBlockId; + } + + /// @dev Updates the parent block hash and timestamp. + /// @param _parentId The ID of the parent block. + function _updateParentHashAndTimestamp(uint256 _parentId) private { + // Get the block hash of the parent block. + bytes32 parentHash = blockhash(_parentId); + + // Store the parent block hash in the _blockhashes mapping. + _blockhashes[_parentId] = parentHash; + + // Update the parent timestamp to the current block timestamp. + parentTimestamp = uint64(block.timestamp); + + // Emit an event to signal that the parent hash and gas excess have been anchored. + emit Anchored(parentHash, parentGasExcess); + } + + /// @dev Verifies the current ancestor block hash and updates it with a new aggregated hash. + /// @param _parentId The ID of the parent block. + function _verifyAndUpdatePublicInputHash(uint256 _parentId) private { + // Calculate the current and new ancestor hashes based on the parent block ID. + (bytes32 currPublicInputHash_, bytes32 newPublicInputHash_) = + _calcPublicInputHash(_parentId); + + // Ensure the current ancestor block hash matches the expected value. + require(publicInputHash == currPublicInputHash_, L2_PUBLIC_INPUT_HASH_MISMATCH()); + + // Update the ancestor block hash to the new calculated value. + publicInputHash = newPublicInputHash_; + } + + /// @dev Verifies that the base fee per gas is correct and updates the gas excess. + /// @param _parentGasUsed The gas used by the parent block. + /// @param _baseFeeConfig The configuration parameters for calculating the base fee. + function _verifyBaseFeeAndUpdateGasExcess( + uint32 _parentGasUsed, + LibSharedData.BaseFeeConfig calldata _baseFeeConfig + ) + private + { + (uint256 basefee, uint64 newGasTarget, uint64 newGasExcess) = + getBasefeeV2(_parentGasUsed, uint64(block.timestamp), _baseFeeConfig); + + require(block.basefee == basefee || skipFeeCheck(), L2_BASEFEE_MISMATCH()); + + emit EIP1559Update(parentGasTarget, newGasTarget, parentGasExcess, newGasExcess, basefee); + + parentGasTarget = newGasTarget; + parentGasExcess = newGasExcess; + } + + /// @dev Calculates the aggregated ancestor block hash for the given block ID. + /// @dev This function computes two public input hashes: one for the previous state and one for + /// the new state. + /// It uses a ring buffer to store the previous 255 block hashes and the current chain ID. + /// @param _blockId The ID of the block for which the public input hash is calculated. + /// @return currPublicInputHash_ The public input hash for the previous state. + /// @return newPublicInputHash_ The public input hash for the new state. + function _calcPublicInputHash(uint256 _blockId) + private + view + returns (bytes32 currPublicInputHash_, bytes32 newPublicInputHash_) + { + bytes32[256] memory inputs; + + // Unchecked is safe because it cannot overflow. + unchecked { + // Put the previous 255 blockhashes (excluding the parent's) into a + // ring buffer. + for (uint256 i; i < 255 && _blockId >= i + 1; ++i) { + uint256 j = _blockId - i - 1; + inputs[j % 255] = blockhash(j); + } + } + + inputs[255] = bytes32(block.chainid); + + assembly { + currPublicInputHash_ := keccak256(inputs, 8192 /*mul(256, 32)*/ ) + } + + inputs[_blockId % 255] = blockhash(_blockId); + assembly { + newPublicInputHash_ := keccak256(inputs, 8192 /*mul(256, 32)*/ ) + } + } +}