Skip to content

Simplify when the existing prover wins the auction for the next period #82 #91

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

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
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
44 changes: 32 additions & 12 deletions src/protocol/taiko_alethia/ProverManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ contract ProverManager is IProposerFees, IProverManager {

event Deposit(address indexed user, uint256 amount);
event Withdrawal(address indexed user, uint256 amount);
event ProverOffer(address indexed proposer, uint256 period, uint256 fee, uint256 stake);
event ProverOffer(address indexed proposer, uint256 period, uint256 fee, uint256 stake, bool bondReused);
event ProverEvicted(address indexed prover, address indexed evictor, uint256 periodEnd, uint256 livenessBond);
event ProverExited(address indexed prover, uint256 periodEnd, uint256 provingDeadline);
event NewPeriod(uint256 period);
Expand Down Expand Up @@ -149,10 +149,12 @@ contract ProverManager is IProposerFees, IProverManager {
/// @dev The current best price may be the current prover's fee or the fee of the next bid, depending on whether the
/// period is active or not.
/// An active period is one that doesn't have an `end` timestamp yet.
/// When a period is active, the bid will be for the next period.
function bid(uint256 offeredFee) external {
uint256 currentPeriod = currentPeriodId;
Period storage _currentPeriod = _periods[currentPeriod];
Period storage _nextPeriod = _periods[currentPeriod + 1];
bool bondReused = _currentPeriod.prover == msg.sender;
if (_currentPeriod.end == 0) {
_ensureSufficientUnderbid(_currentPeriod.fee, offeredFee);
_closePeriod(_currentPeriod, successionDelay, provingWindow);
Expand All @@ -161,16 +163,19 @@ contract ProverManager is IProposerFees, IProverManager {
if (_nextProverAddress != address(0)) {
_ensureSufficientUnderbid(_nextPeriod.fee, offeredFee);

// Refund the liveness bond to the losing bid
balances[_nextProverAddress] += _nextPeriod.stake;
// Refund the liveness bond to the previous next bidder,
// but only if it's not the current prover (who may reuse the bond)
if (_nextProverAddress != _currentPeriod.prover) {
balances[_nextProverAddress] += _nextPeriod.stake;
}
}
}

// Record the next period info
uint256 _livenessBond = livenessBond;
_updatePeriod(_nextPeriod, msg.sender, offeredFee, _livenessBond);
_updatePeriod(_nextPeriod, msg.sender, offeredFee, _livenessBond, bondReused);

emit ProverOffer(msg.sender, currentPeriod + 1, offeredFee, _livenessBond);
emit ProverOffer(msg.sender, currentPeriod + 1, offeredFee, _livenessBond, bondReused);
}

/// @inheritdoc IProverManager
Expand Down Expand Up @@ -261,9 +266,13 @@ contract ProverManager is IProposerFees, IProverManager {
Period storage period = _periods[periodId];
require(provenPublication.timestamp > period.end, "Publication must be after period");

uint256 stake = period.stake;
balances[period.prover] += period.pastDeadline ? _calculatePercentage(stake, rewardPercentage) : stake;
period.stake = 0;
// Only release the bond if the prover didn't continue into the next period
Period storage nextPeriod = _periods[periodId + 1];
if (nextPeriod.prover != period.prover) {
uint256 stake = period.stake;
balances[period.prover] += period.pastDeadline ? _calculatePercentage(stake, rewardPercentage) : stake;
period.stake = 0;
}
}

/// @inheritdoc IProposerFees
Expand Down Expand Up @@ -301,7 +310,7 @@ contract ProverManager is IProposerFees, IProverManager {
_closePeriod(period, 0, 0);

Period storage nextPeriod = _periods[periodId + 1];
_updatePeriod(nextPeriod, prover, fee, livenessBond);
_updatePeriod(nextPeriod, prover, fee, livenessBond, false);
}

/// @dev Calculates the percentage of a given numerator scaling up to avoid precision loss
Expand All @@ -317,11 +326,22 @@ contract ProverManager is IProposerFees, IProverManager {
/// @param prover The address of the prover
/// @param fee The fee offered by the prover
/// @param stake The liveness bond to be staked
function _updatePeriod(Period storage period, address prover, uint256 fee, uint256 stake) private {
/// @param bondReused The bond is reused if same prover is continuing from previous period
function _updatePeriod(Period storage period, address prover, uint256 fee, uint256 stake, bool bondReused)
private
{
period.prover = prover;
period.fee = fee;
period.stake = stake; // overwrite previous value. We assume the previous value is zero or already returned
balances[prover] -= stake;

if (bondReused) {
// Carry over existing stake — no deduction needed
period.stake = _periods[currentPeriodId].stake;
} else {
// New prover or fresh start — deduct stake from balance
require(balances[prover] >= stake, "Insufficient balance for bond");
balances[prover] -= stake;
period.stake = stake;
}
}

/// @dev Ensure the offered fee is low enough. It must be at most `maxBidPercentage` of the fee it is outbidding
Expand Down
204 changes: 202 additions & 2 deletions test/ProverManager.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ contract ProverManagerTest is Test {

vm.prank(prover1);
vm.expectEmit();
emit ProverManager.ProverOffer(prover1, 2, maxAllowedFee, LIVENESS_BOND);
emit ProverManager.ProverOffer(prover1, 2, maxAllowedFee, LIVENESS_BOND, false);
proverManager.bid(maxAllowedFee);

// Check that period 2 has been created
Expand Down Expand Up @@ -206,7 +206,7 @@ contract ProverManagerTest is Test {

vm.prank(prover2);
vm.expectEmit();
emit ProverManager.ProverOffer(prover2, 2, secondBidFee, LIVENESS_BOND);
emit ProverManager.ProverOffer(prover2, 2, secondBidFee, LIVENESS_BOND, false);
proverManager.bid(secondBidFee);

// Check that period 2 now has prover2 as the prover
Expand Down Expand Up @@ -284,6 +284,206 @@ contract ProverManagerTest is Test {
proverManager.bid(insufficientlyReducedFee);
}

function test_bid_SameProverReusesBond() public {
// First, have prover1 make a successful bid
_deposit(prover1, DEPOSIT_AMOUNT);

uint256 firstBidFee = _maxAllowedFee(INITIAL_FEE);
vm.prank(prover1);
proverManager.bid(firstBidFee);

// End current period
vm.warp(block.timestamp + EXIT_DELAY + 1);
vm.prank(inbox);
proverManager.payPublicationFee{value: INITIAL_FEE}(proposer, false);

// prover1 bids again with a valid fee (maxAllowedFee under their previous)
uint256 secondBidFee = _maxAllowedFee(firstBidFee);
vm.prank(prover1);
vm.expectEmit();
emit ProverManager.ProverOffer(prover1, 3, secondBidFee, LIVENESS_BOND, true);
proverManager.bid(secondBidFee);

// Check that bond was reused in period 3
ProverManager.Period memory period = proverManager.getPeriod(3);
assertEq(period.prover, prover1, "Prover1 should remain the prover");
assertEq(period.fee, secondBidFee, "Fee should be updated");
assertEq(period.stake, LIVENESS_BOND, "Stake should be zero due to bond reuse");

// Check that prover1's balance was NOT reduced again
uint256 prover1Bal = proverManager.balances(prover1);
assertEq(prover1Bal, DEPOSIT_AMOUNT - LIVENESS_BOND, "Bond should not be deducted again");
}

function test_bid_SameProverReplacesExistingNextBidReusesBond() public {
// Step 1: prover1 becomes current prover in period 2
_deposit(prover1, DEPOSIT_AMOUNT);
uint256 firstFee = _maxAllowedFee(INITIAL_FEE);
vm.prank(prover1);
proverManager.bid(firstFee);

// Advance to period 3
vm.warp(block.timestamp + EXIT_DELAY + 1);
vm.prank(inbox);
proverManager.payPublicationFee{value: INITIAL_FEE}(proposer, false);

// Step 2: prover2 makes a provisional bid for period 3
_deposit(prover2, DEPOSIT_AMOUNT);
uint256 fee2 = _maxAllowedFee(firstFee);
vm.prank(prover2);
vm.expectEmit();
emit ProverManager.ProverOffer(prover2, 3, fee2, LIVENESS_BOND, false);
proverManager.bid(fee2);

assertEq(
proverManager.balances(prover2),
DEPOSIT_AMOUNT - LIVENESS_BOND,
"prover2 balance should be reduced by liveness bond"
);
assertEq(
proverManager.balances(prover1),
DEPOSIT_AMOUNT - LIVENESS_BOND,
"prover1 balance should be reduced by liveness bond. not returned yet."
);

// Step 3: prover1 comes back and re-bids with better fee — bond should be reused
uint256 fee3 = _maxAllowedFee(fee2);
vm.prank(prover1);
vm.expectEmit();
emit ProverManager.ProverOffer(prover1, 3, fee3, LIVENESS_BOND, true);
proverManager.bid(fee3);

// Check: prover1 is now the next prover for period 3
ProverManager.Period memory period = proverManager.getPeriod(3);
assertEq(period.prover, prover1, "Prover1 should now be the next prover");
assertEq(period.fee, fee3, "Fee should match the last bid");
assertEq(period.stake, LIVENESS_BOND, "Stake should match bond reuse value");

// Check: prover2's bond was refunded
assertEq(proverManager.balances(prover2), DEPOSIT_AMOUNT, "Prover2 should have their bond refunded");

// Check: prover1's bond was not deducted a second time
assertEq(
proverManager.balances(prover1),
DEPOSIT_AMOUNT - LIVENESS_BOND,
"Prover1 should not be charged a second bond"
);
}

function test_finalizePastPeriod_ReleasesBondAfterConsecutiveWins() public {
// Step 1: Prover1 bids for period 2
_deposit(prover1, DEPOSIT_AMOUNT);
uint256 firstFee = _maxAllowedFee(INITIAL_FEE);
vm.prank(prover1);
vm.expectEmit();
emit ProverManager.ProverOffer(prover1, 2, firstFee, LIVENESS_BOND, false);
proverManager.bid(firstFee);

// Advance to period 3
vm.warp(vm.getBlockTimestamp() + EXIT_DELAY + 1);
vm.prank(inbox);
proverManager.payPublicationFee{value: INITIAL_FEE}(proposer, false);

// Prover1 bids for period 3
uint256 fee2 = _maxAllowedFee(firstFee);
vm.prank(prover1);
vm.expectEmit();
emit ProverManager.ProverOffer(prover1, 3, fee2, LIVENESS_BOND, true);
proverManager.bid(fee2);

// Advance to period 4
vm.warp(vm.getBlockTimestamp() + EXIT_DELAY + 1);
vm.prank(inbox);
proverManager.payPublicationFee{value: INITIAL_FEE}(proposer, false);

// Prover1 bids again (bond reuse) for period 4
uint256 fee3 = _maxAllowedFee(fee2);
vm.prank(prover1);
vm.expectEmit();
emit ProverManager.ProverOffer(prover1, 4, fee3, LIVENESS_BOND, true);
proverManager.bid(fee3);

assertEq(
proverManager.balances(prover1),
DEPOSIT_AMOUNT - LIVENESS_BOND,
"Prover1 should not be charged a second bond"
);

// Submit publications for period 3
IPublicationFeed.PublicationHeader[] memory headers3 = _insertPublicationsWithFees(2, fee3);
IPublicationFeed.PublicationHeader memory startHeader3 = headers3[0];
IPublicationFeed.PublicationHeader memory endHeader3 = headers3[1];

// Prover2 bids for period 4
_deposit(prover2, DEPOSIT_AMOUNT);
uint256 fee4 = _maxAllowedFee(fee3);
vm.prank(prover2);
vm.expectEmit();
emit ProverManager.ProverOffer(prover2, 4, fee4, LIVENESS_BOND, false);
proverManager.bid(fee4);

assertEq(
proverManager.balances(prover1),
DEPOSIT_AMOUNT - LIVENESS_BOND,
"Prover1 should not be charged a second bond"
);
assertEq(proverManager.balances(prover2), DEPOSIT_AMOUNT - LIVENESS_BOND, "Prover2 should be charged one bond");

// Warp and trigger period 4
vm.warp(vm.getBlockTimestamp() + EXIT_DELAY + 1);
vm.prank(inbox);
proverManager.payPublicationFee{value: INITIAL_FEE}(proposer, false);

// Prove period 3 (last period prover1 was active)
ICheckpointTracker.Checkpoint memory startCheckpoint = ICheckpointTracker.Checkpoint({
publicationId: startHeader3.id - 1,
commitment: keccak256(abi.encode("commitment3_start"))
});
ICheckpointTracker.Checkpoint memory endCheckpoint = ICheckpointTracker.Checkpoint({
publicationId: endHeader3.id,
commitment: keccak256(abi.encode("commitment3_end"))
});

vm.prank(prover1);
proverManager.prove(
startCheckpoint,
endCheckpoint,
startHeader3,
endHeader3,
2,
"0x",
3 // periodId = 3
);

// Insert a publication after period 3 to finalize it
vm.warp(block.timestamp + 1);
IPublicationFeed.PublicationHeader memory afterPeriod3Header = _insertPublication();

ICheckpointTracker.Checkpoint memory checkpointAfter3 = ICheckpointTracker.Checkpoint({
publicationId: afterPeriod3Header.id,
commitment: keccak256(abi.encode("commitment4"))
});
checkpointTracker.setProvenHash(checkpointAfter3);

// Record prover1 balance before finalization
uint256 balanceBefore = proverManager.balances(prover1);
uint256 stake = proverManager.getPeriod(3).stake;

ProverManager.Period memory period4 = proverManager.getPeriod(4);
assertEq(period4.prover, prover2, "Prover2 should now be the next prover");
assertEq(period4.fee, fee4, "Fee should match the last bid");
assertEq(period4.stake, LIVENESS_BOND, "Stake should match bond reuse value");

// Finalize period 3 (last one prover1 was active)
proverManager.finalizePastPeriod(3, afterPeriod3Header);

ProverManager.Period memory periodAfter = proverManager.getPeriod(3);
assertEq(periodAfter.stake, 0, "Stake should be cleared after finalization");

uint256 balanceAfter = proverManager.balances(prover1);
assertEq(balanceAfter, balanceBefore + stake, "Prover1 should be refunded after they stop being prover");
}

/// --------------------------------------------------------------------------
/// evictProver()
/// --------------------------------------------------------------------------
Expand Down
Loading