Skip to content

Commit

Permalink
preconf router-based forced tx inclusion
Browse files Browse the repository at this point in the history
  • Loading branch information
cyberhorsey committed Jan 22, 2025
1 parent 12de2cf commit 729520b
Show file tree
Hide file tree
Showing 5 changed files with 194 additions and 12 deletions.
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;
}

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) {
uint256 multiplier = pendingForcedTxHashes == 0 ? 1 : pendingForcedTxHashes;
return baseStakeAmount * multiplier;
}

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

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
)
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;
}


// 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

0 comments on commit 729520b

Please sign in to comment.