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

Improve handling of fallback preconfer #101

Merged
merged 1 commit into from
Sep 2, 2024
Merged
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
feat: improve handling of fallback preconfer
AnshuJalan committed Aug 29, 2024
commit 470b57047c7c26a21d92cf3f99aad254e99794f5
4 changes: 2 additions & 2 deletions SmartContracts/src/avs/PreconfConstants.sol
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@ pragma solidity 0.8.25;

library PreconfConstants {
uint256 internal constant SECONDS_IN_SLOT = 12;
uint256 internal constant SECONDS_IN_EPOCH = 384; // 32 slots * 12 seconds
uint256 internal constant TWO_EPOCHS = 768;
uint256 internal constant SECONDS_IN_EPOCH = SECONDS_IN_SLOT * 32;
uint256 internal constant TWO_EPOCHS = 2 * SECONDS_IN_EPOCH;
uint256 internal constant DISPUTE_PERIOD = 2 * SECONDS_IN_EPOCH;
}
259 changes: 172 additions & 87 deletions SmartContracts/src/avs/PreconfTaskManager.sol
Original file line number Diff line number Diff line change
@@ -24,18 +24,20 @@ contract PreconfTaskManager is IPreconfTaskManager, Initializable {
// A ring buffer of upcoming preconfers (who are also the L1 validators)
uint256 internal lookaheadTail;
uint256 internal constant LOOKAHEAD_BUFFER_SIZE = 64;
LookaheadEntry[LOOKAHEAD_BUFFER_SIZE] internal lookahead;
LookaheadBufferEntry[LOOKAHEAD_BUFFER_SIZE] internal lookahead;

// Current and past lookahead posters as a mapping indexed by the timestamp of the epoch
mapping(uint256 epochTimestamp => address poster) internal lookaheadPosters;

// Maps the epoch timestamp to the randomly selected preconfer (if present) for that epoch
mapping(uint256 epochTimestamp => address randomPreconfer) internal randomPreconfers;
// Maps the epoch timestamp to the lookahead poster, correctness status and fallback preconfer
// Note: This may be optimised to re-use existing slots and reduce gas cost.
mapping(uint256 epochTimestamp => LookaheadMetadata) internal lookaheadMetadatas;

// Maps the block height to the associated proposer
// This is required since the stored block in Taiko has this contract as the proposer
mapping(uint256 blockId => address proposer) internal blockIdToProposer;

// Cannot be kept in `PreconfConstants` file because solidity expects array sizes
// to be stored in the main contract file itself.
uint256 internal constant SLOTS_IN_EPOCH = 32;

constructor(
IPreconfServiceManager _serviceManager,
IPreconfRegistry _registry,
@@ -56,8 +58,10 @@ contract PreconfTaskManager is IPreconfTaskManager, Initializable {

/**
* @notice Proposes a new Taiko L2 block.
* @dev This may either be called by a randomly selected preconfer or by a preconfer expected for the current slot
* as per the lookahead. The first caller in every is expected to pass along the lookahead entries for the next epoch.
* @dev The first caller in every epoch is expected to pass along the lookahead entries for the next epoch.
* The function reverts if the lookahead is lagging behind. This is possible if it is
* the first block proposal of the system or no lookahead was posted for the current epoch due to missed proposals.
* In this case, `forcePushLookahead` must be called in order to update the lookahead for the next epoch.
* @param blockParams Block parameters expected by TaikoL1 contract
* @param txList RLP encoded transaction list expected by TaikoL1 contract
* @param lookaheadPointer A pointer to the lookahead entry that may prove that the sender is the preconfer
@@ -70,28 +74,15 @@ contract PreconfTaskManager is IPreconfTaskManager, Initializable {
uint256 lookaheadPointer,
LookaheadSetParam[] calldata lookaheadSetParams
) external payable {
uint256 currentEpochTimestamp = _getEpochTimestamp(block.timestamp);
address randomPreconfer = randomPreconfers[currentEpochTimestamp];
LookaheadBufferEntry memory lookaheadEntry = lookahead[lookaheadPointer % LOOKAHEAD_BUFFER_SIZE];

// Verify that the sender is allowed to propose a block in this slot
uint256 currentEpochTimestamp = _getEpochTimestamp(block.timestamp);
LookaheadMetadata memory lookaheadMetadata = lookaheadMetadatas[currentEpochTimestamp];

if (randomPreconfer != address(0) && msg.sender != randomPreconfer) {
// Revert if the sender is not the randomly selected preconfer for the epoch
// Revert if the lookahead was proven incorrect and the sender is not the fallback preconfer
if (lookaheadMetadata.incorrect && msg.sender != lookaheadMetadata.fallbackPreconfer) {
revert SenderIsNotTheFallbackPreconfer();
} else if (isLookaheadRequired(currentEpochTimestamp) || block.timestamp < lookahead[lookaheadTail].timestamp) {
// A fallback preconfer is selected randomly for the current epoch when
// - Lookahead is empty i.e the epoch has no L1 validators who are opted-in preconfers in the AVS
// - The previous lookahead for the epoch was invalidated
// - It is the first epoch after this contract started offering services

if (msg.sender != getFallbackPreconfer()) {
revert SenderIsNotTheFallbackPreconfer();
} else {
randomPreconfers[currentEpochTimestamp] = msg.sender;
}
} else {
LookaheadEntry memory lookaheadEntry = lookahead[lookaheadPointer % LOOKAHEAD_BUFFER_SIZE];

// The current L1 block's timestamp must be within the range retrieved from the lookahead entry.
// The preconfer is allowed to propose a block in advanced if there are no other entries in the
// lookahead between the present slot and the preconfer's own slot.
@@ -111,7 +102,7 @@ contract PreconfTaskManager is IPreconfTaskManager, Initializable {
// Update the lookahead for the next epoch.
// Only called during the first block proposal of the current epoch.
if (isLookaheadRequired(nextEpochTimestamp)) {
_updateLookahead(currentEpochTimestamp, lookaheadSetParams);
_updateLookahead(nextEpochTimestamp, lookaheadSetParams);
}

// Store the proposer for the block locally
@@ -190,9 +181,11 @@ contract PreconfTaskManager is IPreconfTaskManager, Initializable {
) external {
uint256 epochTimestamp = _getEpochTimestamp(slotTimestamp);

LookaheadMetadata memory lookaheadMetadata = lookaheadMetadatas[epochTimestamp];

// The poster must not already be slashed
if (lookaheadPosters[epochTimestamp] == address(0)) {
revert PosterAlreadySlashedForTheEpoch();
if (lookaheadMetadata.incorrect || lookaheadMetadata.poster == address(0)) {
revert PosterAlreadySlashedOrLookaheadIsEmpty();
}

// Must not have missed dispute period
@@ -206,26 +199,22 @@ contract PreconfTaskManager is IPreconfTaskManager, Initializable {
// We pull the preconfer present at the required slot timestamp in the lookahead.
// If no preconfer is present for a slot, we simply use the 0-address to denote the preconfer.

address preconferInLookahead;
if (randomPreconfers[epochTimestamp] != address(0)) {
// If the epoch had a random preconfer, the lookahead was empty
preconferInLookahead = address(0);
} else {
LookaheadEntry memory lookaheadEntry = lookahead[lookaheadPointer % LOOKAHEAD_BUFFER_SIZE];
LookaheadBufferEntry memory lookaheadEntry = lookahead[lookaheadPointer % LOOKAHEAD_BUFFER_SIZE];

// Validate lookahead pointer
if (slotTimestamp > lookaheadEntry.timestamp || slotTimestamp <= lookaheadEntry.prevTimestamp) {
revert InvalidLookaheadPointer();
}
// Validate lookahead pointer
if (slotTimestamp > lookaheadEntry.timestamp || slotTimestamp <= lookaheadEntry.prevTimestamp) {
revert InvalidLookaheadPointer();
}

if (lookaheadEntry.timestamp == slotTimestamp) {
// The slot was dedicated to a specific preconfer
preconferInLookahead = lookaheadEntry.preconfer;
} else {
// The slot was empty and it was the next preconfer who was expected to preconf in advanced.
// We still use the zero address because technically the slot itself was empty in the lookahead.
preconferInLookahead = address(0);
}
address preconferInLookahead;
if (lookaheadEntry.timestamp == slotTimestamp && !lookaheadEntry.isFallback) {
// The slot was dedicated to a specific preconfer
preconferInLookahead = lookaheadEntry.preconfer;
} else {
// The slot was empty and it was the next preconfer who was expected to preconf in advanced, OR
// the slot was empty and the preconfer was expected to be the fallback preconfer for the epoch.
// We still use the zero address because technically the slot itself was empty in the lookahead.
preconferInLookahead = address(0);
}

// Fetch the preconfer associated with the validator from the registry
@@ -252,22 +241,50 @@ contract PreconfTaskManager is IPreconfTaskManager, Initializable {
revert LookaheadEntryIsCorrect();
}

// Slash the original lookahead poster
address poster = lookaheadPosters[epochTimestamp];
lookaheadPosters[epochTimestamp] = address(0);
preconfServiceManager.slashOperator(poster);
// If it is the current epoch's lookahead being proved incorrect then insert a fallback preconfer
if (block.timestamp < epochTimestamp + PreconfConstants.SECONDS_IN_EPOCH) {
lookaheadMetadatas[epochTimestamp].fallbackPreconfer = getFallbackPreconfer(epochTimestamp);
}

lookaheadMetadatas[epochTimestamp].incorrect = true;
preconfServiceManager.slashOperator(lookaheadMetadata.poster);

emit ProvedIncorrectLookahead(lookaheadMetadata.poster, slotTimestamp, msg.sender);
}

/**
* @notice Forces the lookahead to be set for the next epoch if it is lagging behind
* @dev This is called once when the system starts up to push the first lookahead, and later anytime
* when the lookahead is lagging due to missed proposals.
* @param lookaheadSetParams Collection of timestamps and preconfer addresses to be inserted in the lookahead
*/
function forcePushLookahead(LookaheadSetParam[] calldata lookaheadSetParams) external {
// Sender must be a preconfer
if (preconfRegistry.getPreconferIndex(msg.sender) != 0) {
revert PreconferNotRegistered();
}

// Lookahead must be lagging behind
LookaheadBufferEntry memory lastLookaheadEntry = lookahead[lookaheadTail % LOOKAHEAD_BUFFER_SIZE];
if (lastLookaheadEntry.timestamp >= block.timestamp) {
revert LookaheadIsNotLagging();
}

// Update the lookahead for next epoch
uint256 nextEpochTimestamp = _getEpochTimestamp(block.timestamp) + PreconfConstants.SECONDS_IN_EPOCH;
_updateLookahead(nextEpochTimestamp, lookaheadSetParams);

emit ProvedIncorrectLookahead(poster, slotTimestamp, msg.sender);
// Block the preconfer from withdrawing stake from Eigenlayer during the dispute window
preconfServiceManager.lockStakeUntil(msg.sender, block.timestamp + PreconfConstants.DISPUTE_PERIOD);
}

//=========
// Helpers
//=========

/// @dev Updates the lookahead for the next epoch
/// @dev Updates the lookahead for an epoch
function _updateLookahead(uint256 epochTimestamp, LookaheadSetParam[] calldata lookaheadSetParams) private {
uint256 nextEpochTimestamp = epochTimestamp + PreconfConstants.SECONDS_IN_EPOCH;
uint256 nextEpochEndTimestamp = nextEpochTimestamp + PreconfConstants.SECONDS_IN_EPOCH;
uint256 epochEndTimestamp = epochTimestamp + PreconfConstants.SECONDS_IN_EPOCH;

// The tail of the lookahead is tracked and connected to the first new lookahead entry so
// that when no more preconfers are present in the remaining slots of the current epoch,
@@ -282,37 +299,55 @@ contract PreconfTaskManager is IPreconfTaskManager, Initializable {
uint256 _lookaheadTail = lookaheadTail;
uint256 prevSlotTimestamp = lookahead[_lookaheadTail % LOOKAHEAD_BUFFER_SIZE].timestamp;

for (uint256 i; i < lookaheadSetParams.length; ++i) {
if (lookaheadSetParams.length == 0) {
// If no preconfers are present in the lookahead, we use the fallback preconfer for the entire epoch
address fallbackPreconfer = getFallbackPreconfer(epochTimestamp);
_lookaheadTail += 1;

address preconfer = lookaheadSetParams[i].preconfer;
uint256 slotTimestamp = lookaheadSetParams[i].timestamp;

// Each entry must be registered in the preconf registry
if (preconfRegistry.getPreconferIndex(preconfer) != 0) {
revert PreconferNotRegistered();
}

// Ensure that the timestamps belong to a valid slot in the next epoch
if (
(slotTimestamp - nextEpochTimestamp) % 12 != 0 || slotTimestamp >= nextEpochEndTimestamp
|| slotTimestamp <= prevSlotTimestamp
) {
revert InvalidSlotTimestamp();
}

// Update the lookahead entry
lookahead[_lookaheadTail % LOOKAHEAD_BUFFER_SIZE] = LookaheadEntry({
timestamp: uint48(slotTimestamp),
prevTimestamp: uint48(prevSlotTimestamp),
preconfer: preconfer
// and, insert it in the last slot of the epoch so that it may start preconfing in advanced
lookahead[_lookaheadTail % LOOKAHEAD_BUFFER_SIZE] = LookaheadBufferEntry({
isFallback: true,
timestamp: uint40(epochEndTimestamp - PreconfConstants.SECONDS_IN_SLOT),
prevTimestamp: uint40(prevSlotTimestamp),
preconfer: fallbackPreconfer
});
prevSlotTimestamp = slotTimestamp;
} else {
for (uint256 i; i < lookaheadSetParams.length; ++i) {
_lookaheadTail += 1;

address preconfer = lookaheadSetParams[i].preconfer;
uint256 slotTimestamp = lookaheadSetParams[i].timestamp;

// Each entry must be registered in the preconf registry
if (preconfRegistry.getPreconferIndex(preconfer) != 0) {
revert PreconferNotRegistered();
}

// Ensure that the timestamps belong to a valid slot in the epoch
if (
(slotTimestamp - epochTimestamp) % 12 != 0 || slotTimestamp >= epochEndTimestamp
|| slotTimestamp <= prevSlotTimestamp
) {
revert InvalidSlotTimestamp();
}

// Update the lookahead entry
lookahead[_lookaheadTail % LOOKAHEAD_BUFFER_SIZE] = LookaheadBufferEntry({
isFallback: false,
timestamp: uint40(slotTimestamp),
prevTimestamp: uint40(prevSlotTimestamp),
preconfer: preconfer
});
prevSlotTimestamp = slotTimestamp;
}
}

lookaheadTail = _lookaheadTail;
lookaheadPosters[epochTimestamp] = msg.sender;
lookaheadMetadatas[epochTimestamp].poster = msg.sender;

// We directly use the lookahead set params even in the case of a fallback preconfer to
// assist the nodes in identifying an incorrect lookahead. The contents of this event can be matched against
// the output of `getLookaheadParamsForEpoch` to verify the correctness of the lookahead.
emit LookaheadUpdated(lookaheadSetParams);
}

@@ -351,18 +386,60 @@ contract PreconfTaskManager is IPreconfTaskManager, Initializable {
// Views
//=======

function getFallbackPreconfer() public view returns (address) {
uint256 randomness = block.prevrandao;
/// @dev We use the beacon block root at the first block in the last epoch as randomness to
/// decide on the preconfer for the given epoch
function getFallbackPreconfer(uint256 epochTimestamp) public view returns (address) {
// Start of the last epoch
uint256 lastEpochTimestamp = epochTimestamp - PreconfConstants.SECONDS_IN_EPOCH;
uint256 randomness = uint256(_getBeaconBlockRoot(lastEpochTimestamp));
uint256 preconferIndex = randomness % preconfRegistry.getNextPreconferIndex();

return preconfRegistry.getPreconferAtIndex(preconferIndex);
}

function isLookaheadRequired(uint256 epochTimestamp) public view returns (bool) {
return lookaheadPosters[epochTimestamp] == address(0);
}
/**
* @notice Returns the full 32 slot preconfer lookahead for the epoch
* @dev This function has been added as a helper for the node to get the full 32 slot lookahead without
* the need of deconstructing the contract storage. Due to the fact that we are deconstructing an efficient
* data structure to fill in all the slots, this is very heavy on gas, and onchain calls to it should be avoided.
* @param epochTimestamp The start timestamp of the epoch for which the lookahead is to be generated
*/
function getLookaheadForEpoch(uint256 epochTimestamp) external view returns (address[SLOTS_IN_EPOCH] memory) {
address[SLOTS_IN_EPOCH] memory lookaheadForEpoch;

function getLookahead() external view returns (LookaheadEntry[LOOKAHEAD_BUFFER_SIZE] memory) {
return lookahead;
LookaheadMetadata memory lookaheadMetadata = lookaheadMetadatas[epochTimestamp];

if (lookaheadMetadata.incorrect) {
for (uint256 i; i < SLOTS_IN_EPOCH; ++i) {
lookaheadForEpoch[i] = lookaheadMetadata.fallbackPreconfer;
}
} else {
uint256 _lookaheadTail = lookaheadTail;
uint256 lastSlotTimestamp =
epochTimestamp + PreconfConstants.SECONDS_IN_EPOCH - PreconfConstants.SECONDS_IN_SLOT;

// Find the entry that fills the last slot of the epoch
while (lookahead[_lookaheadTail % LOOKAHEAD_BUFFER_SIZE].prevTimestamp > lastSlotTimestamp) {
_lookaheadTail -= 1;
}

address preconfer = lookahead[_lookaheadTail % LOOKAHEAD_BUFFER_SIZE].preconfer;
uint256 prevTimestamp = lookahead[_lookaheadTail % LOOKAHEAD_BUFFER_SIZE].prevTimestamp;

// Iterate backwards and fill in the slots
for (uint256 i = SLOTS_IN_EPOCH - 1; i >= 0; --i) {
lookaheadForEpoch[i] = preconfer;

lastSlotTimestamp -= PreconfConstants.SECONDS_IN_SLOT;
if (lastSlotTimestamp == prevTimestamp) {
_lookaheadTail -= 1;
preconfer = lookahead[_lookaheadTail % LOOKAHEAD_BUFFER_SIZE].preconfer;
prevTimestamp = lookahead[_lookaheadTail % LOOKAHEAD_BUFFER_SIZE].prevTimestamp;
}
}
}

return lookaheadForEpoch;
}

/**
@@ -373,7 +450,7 @@ contract PreconfTaskManager is IPreconfTaskManager, Initializable {
* in the same sequence as they appear in the epoch. So at index n - 1, we have the validator for slot n in that
* epoch.
*/
function getLookaheadParamsForEpoch(uint256 epochTimestamp, bytes[32] memory validatorBLSPubKeys)
function getLookaheadParamsForEpoch(uint256 epochTimestamp, bytes[SLOTS_IN_EPOCH] memory validatorBLSPubKeys)
external
view
returns (LookaheadSetParam[] memory)
@@ -412,4 +489,12 @@ contract PreconfTaskManager is IPreconfTaskManager, Initializable {

return lookaheadSetParams;
}

function isLookaheadRequired(uint256 epochTimestamp) public view returns (bool) {
return lookaheadMetadatas[epochTimestamp].poster == address(0);
}

function getLookaheadBuffer() external view returns (LookaheadBufferEntry[LOOKAHEAD_BUFFER_SIZE] memory) {
return lookahead;
}
}
39 changes: 31 additions & 8 deletions SmartContracts/src/interfaces/IPreconfTaskManager.sol
Original file line number Diff line number Diff line change
@@ -14,11 +14,13 @@ interface IPreconfTaskManager {
bytes32 txListHash;
}

struct LookaheadEntry {
struct LookaheadBufferEntry {
// True when the preconfer is randomly selected
bool isFallback;
// Timestamp of the slot at which the provided preconfer is the L1 validator
uint48 timestamp;
uint40 timestamp;
// Timestamp of the last slot that had a valid preconfer
uint48 prevTimestamp;
uint40 prevTimestamp;
// Address of the preconfer who is also the L1 validator
// The preconfer will have rights to propose a block in the range (prevTimestamp, timestamp]
address preconfer;
@@ -31,6 +33,16 @@ interface IPreconfTaskManager {
address preconfer;
}

struct LookaheadMetadata {
// True if the lookahead was proved to be incorrect
bool incorrect;
// The poster of the lookahead
address poster;
// Fallback preconfer selected for the epoch in which the lookahead was posted
// This is only set when the lookahead is proved to be incorrect
address fallbackPreconfer;
}

event LookaheadUpdated(LookaheadSetParam[]);
event ProvedIncorrectPreconfirmation(address indexed preconfer, uint256 indexed blockId, address indexed disputer);
event ProvedIncorrectLookahead(address indexed poster, uint256 indexed timestamp, address indexed disputer);
@@ -55,16 +67,18 @@ interface IPreconfTaskManager {
error MetadataMismatch();
/// @dev The expected validator has been slashed on CL
error ExpectedValidatorMustNotBeSlashed();
/// @dev The lookahead poster for the epoch has already been slashed
error PosterAlreadySlashedForTheEpoch();
/// @dev The lookahead poster for the epoch has already been slashed or there is no lookahead for epoch
error PosterAlreadySlashedOrLookaheadIsEmpty();
/// @dev The lookahead preconfer matches the one the actual validator is proposing for
error LookaheadEntryIsCorrect();
/// @dev Cannot force push a lookahead since it is not lagging behind
error LookaheadIsNotLagging();

/// @dev Accepts block proposal by an operator and forwards it to TaikoL1 contract
function newBlockProposal(
bytes calldata blockParams,
bytes calldata txList,
uint256 lookaheadHint,
uint256 lookaheadPointer,
LookaheadSetParam[] calldata lookaheadSetParams
) external payable;

@@ -83,8 +97,14 @@ interface IPreconfTaskManager {
EIP4788.InclusionProof memory validatorInclusionProof
) external;

/// @dev Returns the entire lookahead buffer
function getLookahead() external view returns (LookaheadEntry[64] memory);
/// @dev Forces the lookahead to be set for the next epoch if it is lagging behind
function forcePushLookahead(LookaheadSetParam[] memory lookaheadSetParams) external;

/// @dev Returns the fallback preconfer for the given epoch
function getFallbackPreconfer(uint256 epochTimestamp) external view returns (address);

/// @dev Returns the full 32 slot preconfer lookahead for the epoch
function getLookaheadForEpoch(uint256 epochTimestamp) external view returns (address[32] memory);

/// @dev Return the parameters required for the lookahead to be set for the given epoch
function getLookaheadParamsForEpoch(uint256 epochTimestamp, bytes[32] memory validatorBLSPubKeys)
@@ -95,4 +115,7 @@ interface IPreconfTaskManager {
/// @dev Returns true is a lookahead is not posted for an epoch
/// @dev In the event that a lookahead was posted but later invalidated, this returns false
function isLookaheadRequired(uint256 epochTimestamp) external view returns (bool);

/// @dev Returns the entire lookahead buffer
function getLookaheadBuffer() external view returns (LookaheadBufferEntry[64] memory);
}