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): router based forced tx inclusion #18824

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,24 @@ import "src/layer1/based/ITaikoInbox.sol";
/// @title IPreconfRouter
/// @custom:security-contact [email protected]
interface IPreconfRouter {

struct ForcedTx {
bytes txList;
uint256 timestamp;
bool included;
uint256 stakeAmount;
Copy link
Contributor

Choose a reason for hiding this comment

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

how about priorityFee

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeah that's maybe a better name

}

error ForcedTxListAlreadyIncluded();
error ForcedTxListAlreadyStored();
error ForcedTxListHashNotFound();
error InsufficientStakeAmount();
error NotTheOperator();
error ProposerIsNotTheSender();


event ForcedTxStored(bytes indexed txHash, uint256 timestamp);

/// @notice Proposes a batch of blocks that have been preconfed.
/// @dev This function only accepts batches from an operator selected to preconf in a particular
/// slot or epoch and routes that batch to the TaikoInbox.
Expand All @@ -19,8 +34,13 @@ interface IPreconfRouter {
function proposePreconfedBlocks(
bytes calldata _params,
bytes calldata _batchParams,
bytes calldata _batchTxList
bytes calldata _batchTxList,
bool force
)
external
returns (ITaikoInbox.BatchMetadata memory meta_);

function updateBaseStakeAmount(uint256 _newBaseStakeAmount) external;

function storeForcedTx(bytes calldata _txList) payable external;
}
77 changes: 72 additions & 5 deletions packages/protocol/contracts/layer1/preconf/impl/PreconfRouter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,95 @@ pragma solidity ^0.8.24;
import "../iface/IPreconfRouter.sol";
import "../iface/IPreconfWhitelist.sol";
import "src/layer1/based/ITaikoInbox.sol";
import "src/shared/libs/LibAddress.sol";
import "src/shared/libs/LibStrings.sol";
import "src/shared/common/EssentialContract.sol";

/// @title PreconfRouter
/// @custom:security-contact [email protected]
contract PreconfRouter is EssentialContract, IPreconfRouter {
uint256[50] private __gap;
mapping(bytes32 => ForcedTx) public forcedTxLists;

constructor(address _resolver) EssentialContract(_resolver) { }
uint256 public pendingForcedTxHashes;

uint256 public inclusionWindow;

uint256 public baseStakeAmount;

uint256[46] private __gap;

constructor(address _resolver, uint256 _inclusionWindow, uint256 _baseStakeAmount) EssentialContract(_resolver) {
inclusionWindow = _inclusionWindow;
baseStakeAmount = _baseStakeAmount;
}

function init(address _owner) external initializer {
__Essential_init(_owner);
}

function updateBaseStakeAmount(uint256 _newBaseStakeAmount) external onlyOwner {
baseStakeAmount = _newBaseStakeAmount;
}

function getRequiredStakeAmount() public view returns (uint256) {
return (2 ** pendingForcedTxHashes).max(4096) * baseStakeAmount;
return baseStakeAmount * multiplier;
}

function storeForcedTx(bytes calldata _txList) payable external {
uint256 requiredStake = getRequiredStakeAmount();
require(msg.value >= requiredStake, InsufficientStakeAmount());
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
require(msg.value >= requiredStake, InsufficientStakeAmount());
require(msg.value == requiredStake, InsufficientStakeAmount());

Copy link
Contributor

Choose a reason for hiding this comment

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

Otherwise, please send back the overpaid fee.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

will add


bytes32 txListHash = keccak256(_txList);
require(forcedTxLists[txListHash].timestamp == 0, ForcedTxListAlreadyStored());

forcedTxLists[txListHash] = ForcedTx({
txList: _txList,
timestamp: block.timestamp,
included: false,
stakeAmount: msg.value
});

pendingForcedTxHashes++;

emit ForcedTxStored(_txList, block.timestamp);
}

/// @inheritdoc IPreconfRouter
function proposePreconfedBlocks(
bytes calldata,
bytes calldata _batchParams,
bytes calldata _batchTxList
bytes calldata _batchTxList,
bool force
Copy link
Contributor

Choose a reason for hiding this comment

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

the preconfer can always use "false" for force, so we are not forcing?

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe we can try to query if there is a forced txs request due, if so, force will be automatically true.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

force is there so that a non preconfer can call this function with force = true, and then it will check the forced block inclusion. That way, the user doesn't rely on a preconfer to include it, nor does it block preconfer from proposing.

Copy link
Contributor

@dantaik dantaik Jan 23, 2025

Choose a reason for hiding this comment

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

But this will ruin the preconfirmation - the preconfer may already shard some shared blocks before they are proposed on L1, now suddenly a user kicks in with a block which will reorg the chain.

)
external
returns (ITaikoInbox.BatchMetadata memory meta_)
{
// Sender must be the selected operator for the epoch
bytes32 forcedTxListHash = keccak256(_batchTxList);

// Sender must be the selected operator for the epoch, or able to propose a forced txList
// after the inclusion window has expired
address selectedOperator =
IPreconfWhitelist(resolve(LibStrings.B_PRECONF_WHITELIST, false)).getOperatorForEpoch();
require(msg.sender == selectedOperator, NotTheOperator());

if(force) {
require(msg.sender == selectedOperator || canProposeFallback(forcedTxListHash), NotTheOperator());
} else {
require(msg.sender == selectedOperator, NotTheOperator());
}

if (force) {
require(forcedTxLists[forcedTxListHash].timestamp != 0, ForcedTxListHashNotFound());
require(!forcedTxLists[forcedTxListHash].included, ForcedTxListAlreadyIncluded());

// Pay out the stake to the proposer
LibAddress.sendEtherAndVerify(msg.sender, forcedTxLists[forcedTxListHash].stakeAmount);

pendingForcedTxHashes--;

forcedTxLists[forcedTxListHash].included = true;
Copy link
Contributor

Choose a reason for hiding this comment

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

forcedTxLists[forcedTxListHash].txList is not used. I'm not sure we can simply replace _batchTxList with forcedTxLists[forcedTxListHash].txList, you will also need to construct a batchParams as:

BatchParams memory params;
params.blocks = new BlockParams[](1);
params.blocks[0].numTransactions = some_constant.

Copy link
Contributor

Choose a reason for hiding this comment

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

Basically you need to call ITaikoInbox(taikoInbox).proposeBatch twice, one for the forced batch, the other one for the normal batch. The first batch always have 1 block with MAX_FORCED_TRANSACTIONS txs.

Copy link
Contributor Author

@cyberhorsey cyberhorsey Jan 23, 2025

Choose a reason for hiding this comment

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

The idea if that you would call this with its own batch, and _batchTxList is forcedTxLists[forcedTxListHash].txList. It is gas inefficient and will likely never be used, since we don't plan on censoring any user's transactions, but it keeps it very simple.

This function is etiher called by the preconfing gateway, with it's own batch, and is profitable because of the priorityFee reward, or by the user themselves, and either way the input _batchTxList is indeed the forcedTxList, because forcedTxListHash is a hash of _batchTxList.

Does this propose a problem?

}


// Call the proposeBatch function on the TaikoInbox
address taikoInbox = resolve(LibStrings.B_TAIKO, false);
Expand All @@ -39,4 +101,9 @@ contract PreconfRouter is EssentialContract, IPreconfRouter {
// Verify that the sender had set itself as the proposer
require(meta_.proposer == msg.sender, ProposerIsNotTheSender());
}

function canProposeFallback(bytes32 _forcedTxHash) public view returns (bool) {
return block.timestamp > forcedTxLists[_forcedTxHash].timestamp + inclusionWindow &&
!forcedTxLists[_forcedTxHash].included;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,10 @@ contract DeployPreconfContracts is BaseScript {
address(new PreconfWhitelist(sharedResolver)),
abi.encodeCall(PreconfWhitelist.init, (contractOwner))
);

// Deploy PreconfRouter
deploy(
LibStrings.B_PRECONF_ROUTER,
address(new PreconfRouter(sharedResolver)),
address(new PreconfRouter(sharedResolver, vm.envUint("INCLUSION_WINDOW"), vm.envUint("BASE_STAKE_AMOUNT"))),
abi.encodeCall(PreconfRouter.init, (contractOwner))
);
}
Expand Down
97 changes: 94 additions & 3 deletions packages/protocol/test/layer1/preconf/router/RouterTest.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pragma solidity ^0.8.24;
import "./RouterTestBase.sol";
import "../mocks/MockBeaconBlockRoot.sol";
import "src/layer1/based/ITaikoInbox.sol";
import "src/layer1/preconf/iface/IPreconfRouter.sol";

contract RouterTest is RouterTestBase {
function test_proposePreconfedBlocks() external {
Expand Down Expand Up @@ -54,7 +55,7 @@ contract RouterTest is RouterTestBase {
// Prank as Carol (selected operator) and propose blocks
vm.prank(Carol);
ITaikoInbox.BatchMetadata memory meta =
router.proposePreconfedBlocks("", abi.encode(params), "");
router.proposePreconfedBlocks("", abi.encode(params), "", false);

// Assert the proposer was set correctly in the metadata
assertEq(meta.proposer, Carol);
Expand Down Expand Up @@ -88,7 +89,7 @@ contract RouterTest is RouterTestBase {
// Prank as David (not the selected operator) and propose blocks
vm.prank(David);
vm.expectRevert(IPreconfRouter.NotTheOperator.selector);
router.proposePreconfedBlocks("", "", "");
router.proposePreconfedBlocks("", "", "", false);
}

function test_proposePreconfedBlocks_proposerNotSender() external {
Expand Down Expand Up @@ -139,6 +140,96 @@ contract RouterTest is RouterTestBase {
// Prank as Carol (selected operator) and propose blocks
vm.prank(Carol);
vm.expectRevert(IPreconfRouter.ProposerIsNotTheSender.selector);
router.proposePreconfedBlocks("", abi.encode(params), "");
router.proposePreconfedBlocks("", abi.encode(params), "", false);
}

function test_canProposeFallback_notExpired() external {
// Store a forced transaction
vm.deal(address(this), 1 ether);
router.storeForcedTx{value: 0.1 ether}(testTxList);

// Ensure fallback cannot be proposed before the inclusion window expires
assertFalse(router.canProposeFallback(testTxListHash));
}

function test_canProposeFallback_expired() external {
// Store a forced transaction
vm.deal(address(this), 1 ether);
router.storeForcedTx{value: 0.1 ether}(testTxList);

// Warp time beyond the inclusion window
vm.warp(block.timestamp + router.inclusionWindow() + 1);

// Ensure fallback can now be proposed
assertTrue(router.canProposeFallback(testTxListHash));
}

function test_storeForcedTx_success() external {
// Ensure the initial balance is sufficient
vm.deal(address(this), 10 ether);

// Store forced transaction with the correct stake
uint256 requiredStake = router.baseStakeAmount();
router.storeForcedTx{value: requiredStake}(testTxList);

// Validate stored transaction data
(bytes memory storedTxList, uint256 timestamp, bool included, uint256 stakeAmount) =
router.forcedTxLists(testTxListHash);

assertEq(storedTxList, testTxList);
assertEq(timestamp, block.timestamp);
assertEq(included, false);
assertEq(stakeAmount, requiredStake);

// Ensure the pendingForcedTxHashes count is incremented
assertEq(router.pendingForcedTxHashes(), 1);
}

function test_storeForcedTx_insufficientStake() external {
vm.deal(address(this), 10 ether);

uint256 incorrectStake = router.getRequiredStakeAmount() - 1;
console2.log(router.getRequiredStakeAmount());
vm.expectRevert(IPreconfRouter.InsufficientStakeAmount.selector);
router.storeForcedTx{value: incorrectStake}(testTxList);
}


function test_storeForcedTx_dynamicStakeIncrease() external {
vm.deal(address(this), 10 ether);

uint256 baseStake = router.baseStakeAmount();

for (uint256 i = 0; i < 3; i++) {
uint256 expectedStake = baseStake * (i + 1);
bytes memory newTx = abi.encodePacked(testTxList, i);
bytes32 newTxHash = keccak256(newTx);

router.storeForcedTx{value: expectedStake}(newTx);

(, , , uint256 stakeAmount) = router.forcedTxLists(newTxHash);
assertEq(stakeAmount, expectedStake);
}

assertEq(router.pendingForcedTxHashes(), 3);
}

function test_storeForcedTx_duplicate() external {
vm.deal(address(this), 10 ether);

router.storeForcedTx{value: router.getRequiredStakeAmount()}(testTxList);

uint256 requiredStake = router.getRequiredStakeAmount();
vm.expectRevert(IPreconfRouter.ForcedTxListAlreadyStored.selector);
router.storeForcedTx{value: requiredStake}(testTxList);
}

function test_storeForcedTx_pendingTxCount() external {
vm.deal(address(this), 10 ether);

router.storeForcedTx{value: router.baseStakeAmount()}(testTxList);

assertEq(router.pendingForcedTxHashes(), 1);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,15 @@ abstract contract RouterTestBase is Layer1Test {
PreconfWhitelist internal whitelist;
address internal routerOwner;
address internal whitelistOwner;
bytes internal testTxList = "test transaction list";
bytes32 internal testTxListHash;
uint256 internal initialTimestamp;

function setUpOnEthereum() internal virtual override {
routerOwner = Alice;
whitelistOwner = Alice;
testTxListHash = keccak256(testTxList);
initialTimestamp = block.timestamp;

vm.chainId(1);

Expand All @@ -37,7 +42,7 @@ abstract contract RouterTestBase is Layer1Test {
router = PreconfRouter(
deploy({
name: "preconf_router",
impl: address(new PreconfRouter(address(resolver))),
impl: address(new PreconfRouter(address(resolver), 60, 0.01 ether)),
data: abi.encodeCall(PreconfRouter.init, (routerOwner))
})
);
Expand Down
Loading