Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(protocol): support delayed forced inclusion of txs #18826

Draft
wants to merge 43 commits into
base: pacaya_fork
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
e4a7d10
emit blob hashes in event
dantaik Jan 22, 2025
56a6e93
Update TaikoInbox.sol
dantaik Jan 22, 2025
c38afbb
refactor
dantaik Jan 22, 2025
4bde037
Update TaikoInbox.sol
dantaik Jan 22, 2025
bd2202f
Update Layer1Test.sol
dantaik Jan 22, 2025
e723138
BatchInfo
dantaik Jan 22, 2025
5a5f2b1
more
dantaik Jan 22, 2025
1266339
fix
dantaik Jan 22, 2025
8519a57
fix
dantaik Jan 22, 2025
24d0029
Update TaikoInbox.sol
dantaik Jan 22, 2025
34ea458
forge fmt & update contract layout tables
dantaik Jan 22, 2025
267719e
reorder
dantaik Jan 22, 2025
08356e4
Update TaikoInbox.sol
dantaik Jan 22, 2025
321002d
Update ITaikoInbox.sol
dantaik Jan 22, 2025
19e00ab
more
dantaik Jan 22, 2025
5a59c94
more
dantaik Jan 22, 2025
5b39351
more
dantaik Jan 22, 2025
95df593
Update ITaikoInbox.sol
dantaik Jan 22, 2025
b27e27d
Merge branch 'pacaya_fork' into emit_blob_hashes_in_event
dantaik Jan 23, 2025
40dcfa8
show case an idea
dantaik Jan 23, 2025
84d4493
Update TaikoInboxWithForcedTxInclusion.sol
dantaik Jan 23, 2025
69848fe
split
dantaik Jan 23, 2025
e1290d4
Update TaikoInboxWithForcedTxInclusion.sol
dantaik Jan 23, 2025
6eaa54b
Update TaikoInboxWithForcedTxInclusion.sol
dantaik Jan 23, 2025
f38b07e
Update TaikoInbox.sol
dantaik Jan 23, 2025
fec04d5
forge fmt & update contract layout tables
dantaik Jan 23, 2025
3095e13
more
dantaik Jan 23, 2025
48479b5
support blob hashes as parameters
dantaik Jan 23, 2025
cc6274b
Merge branch 'emit_blob_hashes_in_event' into forced_tx_inclusion
dantaik Jan 23, 2025
e532efe
removed 1 TODO
dantaik Jan 23, 2025
022849d
Update TaikoInboxWithForcedTxInclusion.sol
dantaik Jan 23, 2025
7a8e392
Merge branch 'pacaya_fork' into emit_blob_hashes_in_event
dantaik Jan 23, 2025
da354f5
Merge branch 'emit_blob_hashes_in_event' into forced_tx_inclusion
dantaik Jan 23, 2025
f9885e9
Update ITaikoInbox.sol
dantaik Jan 23, 2025
e5bf3f1
Merge branch 'pacaya_fork' into forced_tx_inclusion
dantaik Jan 24, 2025
40b0db8
feat(protocol): forced inclusion store (#18829)
cyberhorsey Jan 24, 2025
8fc98eb
merge pacaya_fork branch, update resolvers
cyberhorsey Jan 24, 2025
cf15174
add the forced inclusion inbox as a WL proposer
cyberhorsey Jan 24, 2025
b96c0ef
Merge branch 'pacaya_fork' into forced_tx_inclusion
dantaik Jan 25, 2025
c64a03b
fee => feeInGwei
dantaik Jan 27, 2025
f9fa4b4
move new files to a new dir
dantaik Jan 27, 2025
06225d8
Update gen-layouts.sh
dantaik Jan 27, 2025
13113d8
Merge branch 'pacaya_fork' into forced_tx_inclusion
dantaik Jan 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/protocol/contracts/layer1/based/ITaikoInbox.sol
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ interface ITaikoInbox {
error MsgValueNotZero();
error NoBlocksToProve();
error NotFirstProposal();
error NotInboxOperator();
error NotWhitelistedProposer();
error ParentMetaHashMismatch();
error SameTransition();
error SignalNotSent();
Expand Down
6 changes: 3 additions & 3 deletions packages/protocol/contracts/layer1/based/TaikoInbox.sol
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,15 @@ abstract contract TaikoInbox is EssentialContract, ITaikoInbox, ITaiko {
BatchParams memory params = abi.decode(_params, (BatchParams));

{
address operator = resolve(LibStrings.B_INBOX_OPERATOR, true);
if (operator == address(0)) {
address whitelistedProposer = resolve(LibStrings.B_WHITELISTED_PROPOSER, true);
if (whitelistedProposer == 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(msg.sender == whitelistedProposer, NotWhitelistedProposer());
require(params.proposer != address(0), CustomProposerMissing());
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// 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 ForcedInclusionInbox
/// @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 [email protected]
contract ForcedInclusionInbox is EssentialContract {
using LibMath for uint256;

event ForcedInclusionProcessed(IForcedInclusionStore.ForcedInclusion);

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.
/// @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.
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));
(info_, meta_) = inbox.proposeBatch(_params, _txList);

// Process the next forced inclusion.
IForcedInclusionStore store =
IForcedInclusionStore(resolve(LibStrings.B_FORCED_INCLUSION_STORE, false));

IForcedInclusionStore.ForcedInclusion memory inclusion =
store.consumeForcedInclusion(msg.sender);

if (inclusion.createdAt != 0) {
ITaikoInbox.BatchParams memory params;

if (_forcedInclusionParams.length != 0) {
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);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// 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 "./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;

uint256 public immutable inclusionDelay;
uint64 public immutable feeInGwei;

mapping(uint256 id => ForcedInclusion inclusion) public queue; // slot 1
uint64 public head; // slot 2
uint64 public tail;
uint128 private __reserved1;

uint256[48] private __gap;

constructor(
address _resolver,
uint256 _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());

ForcedInclusion memory inclusion = ForcedInclusion({
blobHash: blobHash,
feeInGwei: uint64(msg.value / 1 gwei),
createdAt: uint64(block.timestamp),
blobByteOffset: blobByteOffset,
blobByteSize: blobByteSize
});

queue[tail++] = inclusion;

emit ForcedInclusionStored(inclusion);
}

function consumeForcedInclusion(address _feeRecipient)
external
nonReentrant
onlyFromNamed(LibStrings.B_TAIKO_FORCED_INCLUSION_INBOX)
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];

if (inclusion.createdAt != 0 && block.timestamp >= inclusionDelay + inclusion.createdAt) {
inclusion_ = inclusion;
_feeRecipient.sendEtherAndVerify(inclusion.feeInGwei * 1 gwei);
delete queue[_head];
head = _head + 1;
emit ForcedInclusionConsumed(inclusion);
}
}

// @dev Override this function for easier testing blobs
function _blobHash(uint8 blobIndex) internal view virtual returns (bytes32) {
return blobhash(blobIndex);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "src/layer1/based/TaikoInbox.sol";
import "./ForcedInclusionStore.sol";

/// @title IForcedInclusionInbox
/// @custom:security-contact [email protected]
interface IForcedInclusionInbox {
event ForcedInclusionProcessed(IForcedInclusionStore.ForcedInclusion);

/// @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
returns (ITaikoInbox.BatchInfo memory info_, ITaikoInbox.BatchMetadata memory meta_);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

/// @title IForcedInclusionStore
/// @custom:security-contact [email protected]
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();

/// @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 createdAt;
uint32 blobByteOffset;
uint32 blobByteSize;
}

/// @dev Consume a forced inclusion request.
/// The inclusion request must be marked as processed and the priority fee must be paid to the
/// caller.
/// @return inclusion_ The forced inclusion request.
function consumeForcedInclusion(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;
}
24 changes: 17 additions & 7 deletions packages/protocol/contracts/layer1/preconf/impl/PreconfRouter.sol
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "src/shared/common/EssentialContract.sol";
import "src/shared/libs/LibStrings.sol";
import "src/layer1/based/TaikoInbox.sol";
import "src/layer1/forced-inclusion/IForcedInclusionInbox.sol";
import "../iface/IPreconfRouter.sol";
import "../iface/IPreconfWhitelist.sol";
import "src/layer1/based/ITaikoInbox.sol";
import "src/shared/libs/LibStrings.sol";
import "src/shared/common/EssentialContract.sol";

/// @title PreconfRouter
/// @custom:security-contact [email protected]
Expand All @@ -20,7 +21,7 @@ contract PreconfRouter is EssentialContract, IPreconfRouter {

/// @inheritdoc IPreconfRouter
function proposePreconfedBlocks(
bytes calldata,
bytes calldata _forcedInclusionParams,
bytes calldata _batchParams,
bytes calldata _batchTxList
)
Expand All @@ -32,9 +33,18 @@ contract PreconfRouter is EssentialContract, IPreconfRouter {
IPreconfWhitelist(resolve(LibStrings.B_PRECONF_WHITELIST, false)).getOperatorForEpoch();
require(msg.sender == selectedOperator, NotTheOperator());

// Call the proposeBatch function on the TaikoInbox
address taikoInbox = resolve(LibStrings.B_TAIKO, false);
(, meta_) = ITaikoInbox(taikoInbox).proposeBatch(_batchParams, _batchTxList);
// check if we have a forced inclusion inbox
address forcedInclusionInbox = resolve(LibStrings.B_TAIKO_FORCED_INCLUSION_INBOX, true);
if (forcedInclusionInbox == address(0)) {
// Call the proposeBatch function on the TaikoInbox
address taikoInbox = resolve(LibStrings.B_TAIKO, false);
(, meta_) = ITaikoInbox(taikoInbox).proposeBatch(_batchParams, _batchTxList);
} else {
// Call the proposeBatchWithForcedInclusion function on the ForcedInclusionInbox
(, meta_) = IForcedInclusionInbox(forcedInclusionInbox).proposeBatchWithForcedInclusion(
_forcedInclusionParams, _batchParams, _batchTxList
);
Comment on lines +43 to +46
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that this is still only callable by the whitelisted preconfers right? If so, would say again that forced inclusion != inclusion. Wrote about that here before: #18815 (comment)

Inclusion can only be guaranteed when block proposing is made permissionless again (at least for the forced blocks, but may as well allow any block to be proposed when that happens because why not).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with Brecht's point (above) — in the current implementation, forced inclusion can only occur if there is at least one preconfer willing to propose blocks when it becomes the current preconfer.

To address this, one potential solution is to enable fully permissionless block proposing if no blocks have been proposed within the last N seconds (though some details still need to be ironed out). While this approach is related to forced inclusion, I believe it aligns more closely with the core protocol or preconfirmation design. Even without forced inclusion, this feature would be a valuable addition to the preconfirmation mechanism. Without it, if there are only a few preconfers and they all stop proposing blocks due to profitability issues, the chain could come to a halt.

}

// Verify that the sender had set itself as the proposer
require(meta_.proposer == msg.sender, ProposerIsNotTheSender());
Expand Down
5 changes: 4 additions & 1 deletion packages/protocol/contracts/shared/libs/LibStrings.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ library LibStrings {
bytes32 internal constant B_ERC1155_VAULT = bytes32("erc1155_vault");
bytes32 internal constant B_ERC20_VAULT = bytes32("erc20_vault");
bytes32 internal constant B_ERC721_VAULT = bytes32("erc721_vault");
bytes32 internal constant B_INBOX_OPERATOR = bytes32("inbox_operator");
bytes32 internal constant B_FORCED_INCLUSION_STORE = bytes32("forced_inclusion_store");
bytes32 internal constant B_WHITELISTED_PROPOSER = bytes32("whitelisted_proposer");
bytes32 internal constant B_PRECONF_ROUTER = bytes32("preconf_router");
bytes32 internal constant B_PRECONF_WHITELIST = bytes32("preconf_whitelist");
bytes32 internal constant B_PRECONF_WHITELIST_OWNER = bytes32("preconf_whitelist_owner");
Expand All @@ -25,6 +26,8 @@ library LibStrings {
bytes32 internal constant B_SGX_WATCHDOG = bytes32("sgx_watchdog");
bytes32 internal constant B_SIGNAL_SERVICE = bytes32("signal_service");
bytes32 internal constant B_TAIKO = bytes32("taiko");
bytes32 internal constant B_TAIKO_FORCED_INCLUSION_INBOX =
bytes32("taiko_forced_inclusion_inbox");
bytes32 internal constant B_TAIKO_TOKEN = bytes32("taiko_token");
bytes32 internal constant B_WITHDRAWER = bytes32("withdrawer");
bytes32 internal constant H_SIGNAL_ROOT = keccak256("SIGNAL_ROOT");
Expand Down
2 changes: 2 additions & 0 deletions packages/protocol/script/gen-layouts.sh
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ contracts_layer1=(
"contracts/layer1/team/TokenUnlock.sol:TokenUnlock"
"contracts/layer1/provers/ProverSet.sol:ProverSet"
"contracts/layer1/based/ForkRouter.sol:ForkRouter"
"contracts/layer1/forced-inclusion/ForcedInclusionInbox"
"contracts/layer1/forced-inclusion/ForcedInclusionStore"
)

# Layer 2 contracts
Expand Down
Loading