diff --git a/packages/protocol/contracts/L1/ITaikoL1.sol b/packages/protocol/contracts/L1/ITaikoL1.sol
index b01e83131e2..6f32ba20170 100644
--- a/packages/protocol/contracts/L1/ITaikoL1.sol
+++ b/packages/protocol/contracts/L1/ITaikoL1.sol
@@ -20,7 +20,7 @@ interface ITaikoL1 {
         returns (TaikoData.BlockMetadata memory meta_, TaikoData.EthDeposit[] memory deposits_);
 
     /// @notice Proposes a Taiko L2 block (version 2)
-    /// @param _params Block parameters, currently an encoded BlockParams object.
+    /// @param _params Block parameters, an encoded BlockParamsV2 object.
     /// @param _txList txList data if calldata is used for DA.
     /// @return meta_ The metadata of the proposed L2 block.
     function proposeBlockV2(
@@ -30,6 +30,17 @@ interface ITaikoL1 {
         external
         returns (TaikoData.BlockMetadataV2 memory meta_);
 
+    /// @notice Proposes a Taiko L2 block (version 2)
+    /// @param _paramsArr A list of encoded BlockParamsV2 objects.
+    /// @param _txListArr A list of txList.
+    /// @return metaArr_ The metadata objects of the proposed L2 blocks.
+    function proposeBlocksV2(
+        bytes[] calldata _paramsArr,
+        bytes[] calldata _txListArr
+    )
+        external
+        returns (TaikoData.BlockMetadataV2[] memory metaArr_);
+
     /// @notice Proves or contests a block transition.
     /// @param _blockId The index of the block to prove. This is also used to
     /// select the right implementation version.
diff --git a/packages/protocol/contracts/L1/TaikoL1.sol b/packages/protocol/contracts/L1/TaikoL1.sol
index 664d18ff31b..a4a03717b2e 100644
--- a/packages/protocol/contracts/L1/TaikoL1.sol
+++ b/packages/protocol/contracts/L1/TaikoL1.sol
@@ -25,6 +25,7 @@ contract TaikoL1 is EssentialContract, ITaikoL1, TaikoEvents {
     uint256[50] private __gap;
 
     error L1_FORK_ERROR();
+    error L1_INVALID_PARAMS();
     error L1_RECEIVE_DISABLED();
 
     modifier whenProvingNotPaused() {
@@ -37,6 +38,11 @@ contract TaikoL1 is EssentialContract, ITaikoL1, TaikoEvents {
         emit StateVariablesUpdated(state.slotB);
     }
 
+    modifier onlyRegisteredProposer() {
+        LibProposing.checkProposerPermission(this);
+        _;
+    }
+
     /// @dev Allows for receiving Ether from Hooks
     receive() external payable {
         if (!inNonReentrant()) revert L1_RECEIVE_DISABLED();
@@ -76,6 +82,7 @@ contract TaikoL1 is EssentialContract, ITaikoL1, TaikoEvents {
     )
         external
         payable
+        onlyRegisteredProposer
         whenNotPaused
         nonReentrant
         emitEventForClient
@@ -96,18 +103,36 @@ contract TaikoL1 is EssentialContract, ITaikoL1, TaikoEvents {
         bytes calldata _txList
     )
         external
+        onlyRegisteredProposer
         whenNotPaused
         nonReentrant
         emitEventForClient
-        returns (TaikoData.BlockMetadataV2 memory meta_)
+        returns (TaikoData.BlockMetadataV2 memory)
     {
-        TaikoData.Config memory config = getConfig();
+        return _proposeBlock(_params, _txList, getConfig());
+    }
 
-        (, meta_,) = LibProposing.proposeBlock(state, config, this, _params, _txList);
-        if (meta_.id < config.ontakeForkHeight) revert L1_FORK_ERROR();
+    /// @inheritdoc ITaikoL1
+    function proposeBlocksV2(
+        bytes[] calldata _paramsArr,
+        bytes[] calldata _txListArr
+    )
+        external
+        onlyRegisteredProposer
+        whenNotPaused
+        nonReentrant
+        emitEventForClient
+        returns (TaikoData.BlockMetadataV2[] memory metaArr_)
+    {
+        if (_paramsArr.length == 0 || _paramsArr.length != _txListArr.length) {
+            revert L1_INVALID_PARAMS();
+        }
 
-        if (LibUtils.shouldVerifyBlocks(config, meta_.id, true) && !state.slotB.provingPaused) {
-            LibVerifying.verifyBlocks(state, config, this, config.maxBlocksToVerify);
+        metaArr_ = new TaikoData.BlockMetadataV2[](_paramsArr.length);
+        TaikoData.Config memory config = getConfig();
+
+        for (uint256 i; i < _paramsArr.length; ++i) {
+            metaArr_[i] = _proposeBlock(_paramsArr[i], _txListArr[i], config);
         }
     }
 
@@ -276,6 +301,22 @@ contract TaikoL1 is EssentialContract, ITaikoL1, TaikoEvents {
          });
     }
 
+    function _proposeBlock(
+        bytes calldata _params,
+        bytes calldata _txList,
+        TaikoData.Config memory _config
+    )
+        internal
+        returns (TaikoData.BlockMetadataV2 memory meta_)
+    {
+        (, meta_,) = LibProposing.proposeBlock(state, _config, this, _params, _txList);
+        if (meta_.id < _config.ontakeForkHeight) revert L1_FORK_ERROR();
+
+        if (LibUtils.shouldVerifyBlocks(_config, meta_.id, true) && !state.slotB.provingPaused) {
+            LibVerifying.verifyBlocks(state, _config, this, _config.maxBlocksToVerify);
+        }
+    }
+
     /// @dev chain_pauser is supposed to be a cold wallet.
     function _authorizePause(
         address,
diff --git a/packages/protocol/contracts/L1/libs/LibProposing.sol b/packages/protocol/contracts/L1/libs/LibProposing.sol
index 6edd7187819..df332d55967 100644
--- a/packages/protocol/contracts/L1/libs/LibProposing.sol
+++ b/packages/protocol/contracts/L1/libs/LibProposing.sol
@@ -17,7 +17,6 @@ library LibProposing {
     struct Local {
         TaikoData.SlotB b;
         TaikoData.BlockParamsV2 params;
-        address proposerAccess;
         ITierProvider tierProvider;
         bytes32 parentMetaHash;
         bool postFork;
@@ -82,15 +81,6 @@ library LibProposing {
     {
         // Checks proposer access.
         Local memory local;
-
-        local.proposerAccess = _resolver.resolve(LibStrings.B_PROPOSER_ACCESS, true);
-        if (
-            local.proposerAccess != address(0)
-                && !IProposerAccess(local.proposerAccess).isProposerEligible(msg.sender)
-        ) {
-            revert L1_INVALID_PROPOSER();
-        }
-
         local.b = _state.slotB;
         local.postFork = local.b.numBlocks >= _config.ontakeForkHeight;
 
@@ -263,4 +253,13 @@ library LibProposing {
             });
         }
     }
+
+    function checkProposerPermission(IAddressResolver _resolver) internal view {
+        address proposerAccess = _resolver.resolve(LibStrings.B_PROPOSER_ACCESS, true);
+        if (proposerAccess == address(0)) return;
+
+        if (!IProposerAccess(proposerAccess).isProposerEligible(msg.sender)) {
+            revert L1_INVALID_PROPOSER();
+        }
+    }
 }
diff --git a/packages/protocol/contracts/L1/libs/LibProving.sol b/packages/protocol/contracts/L1/libs/LibProving.sol
index 345957f440c..b894396814d 100644
--- a/packages/protocol/contracts/L1/libs/LibProving.sol
+++ b/packages/protocol/contracts/L1/libs/LibProving.sol
@@ -135,7 +135,7 @@ library LibProving {
         uint64 _blockId,
         bytes calldata _input
     )
-        internal
+        public
     {
         Local memory local;