Skip to content
Open
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
236 changes: 201 additions & 35 deletions bindings/bindings/l1sequencer.go

Large diffs are not rendered by default.

129 changes: 104 additions & 25 deletions contracts/contracts/l1/L1Sequencer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,55 +4,134 @@ pragma solidity =0.8.24;
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";

/// @title L1Sequencer
/// @notice L1 contract for managing the sequencer address.
/// The sequencer address can be updated by the owner (multisig recommended).
/// @notice L1 contract for managing sequencer address with history tracking.
/// Supports querying which sequencer was active at any given L2 block height.
contract L1Sequencer is OwnableUpgradeable {
// ============ Types ============

struct HistoryRecord {
uint64 startL2Block;
address sequencerAddr;
}

// ============ Storage ============

/// @notice Current sequencer address
address public sequencer;
/// @notice Ordered array of sequencer records (by startL2Block ascending).
/// sequencerHistory[0] is the first sequencer after PBFT → single-sequencer upgrade.
HistoryRecord[] public sequencerHistory;

/// @notice The L2 block height at which single-sequencer mode activates.
/// Set by initializeHistory(). Nodes read this to know when to switch consensus.
uint64 public activeHeight;

// ============ Events ============

/// @notice Emitted when sequencer is updated
event SequencerUpdated(address indexed oldSequencer, address indexed newSequencer);
event SequencerUpdated(
address indexed oldSequencer,
address indexed newSequencer,
uint64 startL2Block
);

// ============ Initializer ============

/// @notice Initialize the contract
/// @param _owner Contract owner (multisig recommended)
/// @param _initialSequencer Initial sequencer address (can be address(0) to set later)
function initialize(address _owner, address _initialSequencer) external initializer {
function initialize(address _owner) external initializer {
require(_owner != address(0), "invalid owner");

__Ownable_init();
_transferOwnership(_owner);

// Set initial sequencer if provided
if (_initialSequencer != address(0)) {
sequencer = _initialSequencer;
emit SequencerUpdated(address(0), _initialSequencer);
}
}

// ============ Admin Functions ============

/// @notice Update sequencer address (takes effect immediately)
/// @param newSequencer New sequencer address
function updateSequencer(address newSequencer) external onlyOwner {
require(newSequencer != address(0), "invalid sequencer");
require(newSequencer != sequencer, "same sequencer");
/// @notice Initialize sequencer history (called once before the L2 upgrade).
/// @param firstSequencer The first sequencer address after the upgrade.
/// @param upgradeL2Block The L2 block height where single-sequencer mode activates.
function initializeHistory(
address firstSequencer,
uint64 upgradeL2Block
) external onlyOwner {
require(sequencerHistory.length == 0, "already initialized");
require(firstSequencer != address(0), "invalid address");

sequencerHistory.push(HistoryRecord({
startL2Block: upgradeL2Block,
sequencerAddr: firstSequencer
}));
activeHeight = upgradeL2Block;

emit SequencerUpdated(address(0), firstSequencer, upgradeL2Block);
}

/// @notice Register a sequencer change at a future L2 block height.
/// The new sequencer is NOT active until startL2Block is reached.
/// @param newSequencer New sequencer address.
/// @param startL2Block L2 block height when the new sequencer takes over.
/// Must be strictly greater than the last record.
function updateSequencer(
address newSequencer,
uint64 startL2Block
) external onlyOwner {
require(newSequencer != address(0), "invalid address");
require(sequencerHistory.length > 0, "not initialized");
require(
startL2Block > sequencerHistory[sequencerHistory.length - 1].startL2Block,
"startL2Block must be greater than last record"
);

address oldSequencer = sequencer;
sequencer = newSequencer;
address oldSequencer = sequencerHistory[sequencerHistory.length - 1].sequencerAddr;

emit SequencerUpdated(oldSequencer, newSequencer);
sequencerHistory.push(HistoryRecord({
startL2Block: startL2Block,
sequencerAddr: newSequencer
}));

emit SequencerUpdated(oldSequencer, newSequencer, startL2Block);
}

// ============ View Functions ============

/// @notice Get current sequencer address
/// @notice Get the sequencer that was active at a given L2 block height.
/// @dev Binary search: O(log n).
function getSequencerAt(uint64 l2Height) external view returns (address) {
uint256 len = sequencerHistory.length;
require(len > 0, "no sequencer configured");

uint256 low = 0;
uint256 high = len - 1;
uint256 result = 0;

while (low <= high) {
uint256 mid = (low + high) / 2;
if (sequencerHistory[mid].startL2Block <= l2Height) {
result = mid;
if (mid == high) break;
low = mid + 1;
} else {
if (mid == 0) break;
high = mid - 1;
}
}

require(sequencerHistory[result].startL2Block <= l2Height, "no sequencer at height");
return sequencerHistory[result].sequencerAddr;
}

/// @notice Get the latest registered sequencer address (backward compat).
/// @dev If the latest record's startL2Block hasn't been reached yet,
/// this address is scheduled but not yet active.
function getSequencer() external view returns (address) {
return sequencer;
require(sequencerHistory.length > 0, "no sequencer configured");
return sequencerHistory[sequencerHistory.length - 1].sequencerAddr;
}

/// @notice Get the full sequencer history (for L2 node bulk sync at startup).
function getSequencerHistory() external view returns (HistoryRecord[] memory) {
return sequencerHistory;
}

/// @notice Get the number of sequencer history records.
function getSequencerHistoryLength() external view returns (uint256) {
return sequencerHistory.length;
}
}
Loading
Loading