Skip to content

Commit

Permalink
acceptor term tests
Browse files Browse the repository at this point in the history
  • Loading branch information
apenzk committed Feb 24, 2025
1 parent 13f8eb6 commit bfe6458
Show file tree
Hide file tree
Showing 4 changed files with 193 additions and 70 deletions.
69 changes: 50 additions & 19 deletions protocol-units/settlement/mcr/contracts/src/settlement/MCR.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {MCRStorage} from "./MCRStorage.sol";
import {BaseSettlement} from "./settlement/BaseSettlement.sol";
import {IMCR} from "./interfaces/IMCR.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "forge-std/console.sol";

contract MCR is Initializable, BaseSettlement, MCRStorage, IMCR {

Expand All @@ -17,13 +18,31 @@ contract MCR is Initializable, BaseSettlement, MCRStorage, IMCR {
// Trusted attesters admin
bytes32 public constant TRUSTED_ATTESTER = keccak256("TRUSTED_ATTESTER");

/// @notice Error thrown when acceptor term is greater than or equal to epoch duration
error AcceptorTermTooLong();

/// @notice Sets the acceptor term duration, must be less than epoch duration
/// @param _acceptorTerm New acceptor term duration in time units
function setAcceptorTerm(uint256 _acceptorTerm) public onlyRole(COMMITMENT_ADMIN) {
// Get epoch duration from staking contract
uint256 epochDuration = stakingContract.getEpochDuration(address(this));

// TODO we need to handle that the epochDuration cannot be set lower than the acceptorTerm. This needs to happen in the staking contract.
// Ensure acceptor term is less than epoch duration
if (_acceptorTerm >= epochDuration) {
revert AcceptorTermTooLong();
}

acceptorTerm = _acceptorTerm;
}

function initialize(
IMovementStaking _stakingContract,
uint256 _lastPostconfirmedSuperBlockHeight,
uint256 _leadingSuperBlockTolerance,
uint256 _epochDuration,
uint256 _epochDuration, // in time units
address[] memory _custodians,
uint256 _acceptorTerm
uint256 _acceptorTerm // in time units
) public initializer {
__BaseSettlement_init_unchained();
stakingContract = _stakingContract;
Expand Down Expand Up @@ -288,7 +307,7 @@ contract MCR is Initializable, BaseSettlement, MCRStorage, IMCR {
// TODO: we probably have to apply this check somewhere else as (volunteer) attesters can only postconfirm and rollover an epoch in which they are staked.
if (currentAcceptorIsLive()) {
// TODO: for now everyone can postconfirm, but change this later
// if (attester != getCurrentAcceptor()) revert("NotAcceptorAndAcceptorIsLive");
// if (attester != getAcceptor()) revert("NotAcceptorAndAcceptorIsLive");
}

// keep ticking through postconfirmations and rollovers as long as the acceptor is permitted to do
Expand All @@ -301,25 +320,38 @@ contract MCR is Initializable, BaseSettlement, MCRStorage, IMCR {

function currentAcceptorIsLive() public pure returns (bool) {
// TODO check if current acceptor has been live sufficiently recently
// use getL1BlockStartOfCurrentAcceptorTerm, and the mappings
// use getAcceptorStartTime, and the mappings
return true; // dummy implementation
}

/// @notice Gets the L1 block height at which the current acceptor's term started
function getL1BlockStartOfCurrentAcceptorTerm() public view returns (uint256) {
uint256 currentL1BlockHeight = block.number;
uint256 startL1BlockHeight = currentL1BlockHeight - currentL1BlockHeight % acceptorTerm - 1; // -1 because we do not want to consider the current block.
if (startL1BlockHeight < 0) { // ensure its not below 0
startL1BlockHeight = 0;
/// @notice Gets the start time at which the current acceptor's term started
function getAcceptorStartTime() public view returns (uint256) {
uint256 currentTime = block.timestamp;
// Find start of current term by rounding down to nearest term boundary
uint256 startTime = currentTime - (currentTime % acceptorTerm);
if (startTime > currentTime) { // ensure we don't get future time
startTime = currentTime;
}
return startL1BlockHeight;
}

/// @notice Determines the current acceptor using L1 block hash as a source of randomness
function getCurrentAcceptor() public view returns (address) {
// TODO: acceptor should swap more frequently than every epoch.
// use the blockhash of the first L1 block of the current acceptor's term as the source of randomness
bytes32 randomness = blockhash(getL1BlockStartOfCurrentAcceptorTerm());
return startTime;
}

// TODO we need to think of a solution that is not dependent on the block time as finding the correct block is expensive. We need something simple and cheap.
// TODO For example think in terms of L1 block heights, rather than timestamps both for epochs and acceptor terms.
/// @notice Gets the L1 block number that is closest to but not exceeding the given timestamp
function getClosestL1BlockToTime(uint256 targetTime) public view returns (uint256) {
// TODO: implement this
return 0; // dummy implementation
}

/// @notice Determines the acceptor in the accepting epoch using L1 block hash as a source of randomness
function getAcceptor() public view returns (address) {
// Get block number closest to acceptor start time
uint256 startBlock = getClosestL1BlockToTime(getAcceptorStartTime());
// TODO we should use the hash of the block, not the block number
console.log("Start block:", startBlock);
// Use that block's hash as randomness source
bytes32 randomness = blockhash(startBlock);
console.logBytes32(randomness);
// map the randomness to the attesters
// TODO: make this weighted by stake
address[] memory attesters = stakingContract.getStakedAttestersForAcceptingEpoch(address(this));
Expand Down Expand Up @@ -347,7 +379,6 @@ contract MCR is Initializable, BaseSettlement, MCRStorage, IMCR {
superBlockEpoch = previousSuperBlockEpoch;
}


// if the accepting epoch is far behind the superBlockEpoch (which is determined by commitments measured in L1 block time), then the protocol was not live for a while
// We keep rolling over the epoch (i.e. update stakes) until we catch up with the superBlockEpoch
// TODO: acceptors should be separately rewarded for rollover functions and postconfirmation. Consider to separate this out.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -633,4 +633,8 @@ contract MovementStaking is
) public view returns (uint256) {
return computeAllStake(domain, getAcceptingEpoch(domain));
}

function getEpochDuration(address domain) external view returns (uint256) {
return epochDurationByDomain[domain];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -99,4 +99,9 @@ interface IMovementStaking {

function getStakedAttestersForAcceptingEpoch(address domain) external view returns (address[] memory);
function computeAllStakeForAcceptingEpoch(address attester) external view returns (uint256);

/// @notice Gets the epoch duration for a given domain
/// @param domain The domain to get the epoch duration for
/// @return The epoch duration in seconds
function getEpochDuration(address domain) external view returns (uint256);
}
185 changes: 134 additions & 51 deletions protocol-units/settlement/mcr/contracts/test/settlement/MCR.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ contract MCRTest is Test, IMCR {
string public moveSignature = "initialize(address)";
string public stakingSignature = "initialize(address)";
string public mcrSignature = "initialize(address,uint256,uint256,uint256,address[],uint256)";
uint256 epochDuration = 10 seconds;
uint256 epochDuration = 3600 seconds;
uint256 acceptorTerm = 360 seconds;
bytes32 honestCommitmentTemplate = keccak256(abi.encodePacked(uint256(1), uint256(2), uint256(3)));
bytes32 honestBlockIdTemplate = keccak256(abi.encodePacked(uint256(1), uint256(2), uint256(3)));
bytes32 dishonestCommitmentTemplate = keccak256(abi.encodePacked(uint256(3), uint256(2), uint256(1)));
Expand Down Expand Up @@ -84,9 +85,9 @@ contract MCRTest is Test, IMCR {
stakingProxy, // _stakingContract, address of staking contract
0, // _lastPostconfirmedSuperBlockHeight, start from genesis
5, // _leadingSuperBlockTolerance, max blocks ahead of last confirmed
epochDuration, // _epochDuration, time window for block confirmation
epochDuration, // _epochDuration, how long an epoch lasts, constant stakes in that time
custodians, // _custodians, array with moveProxy address
120 seconds // _acceptorTerm, how long an acceptor serves
acceptorTerm // _acceptorTerm, how long an acceptor serves
);
TransparentUpgradeableProxy mcrProxy = new TransparentUpgradeableProxy(
address(mcrImplementation),
Expand Down Expand Up @@ -224,54 +225,6 @@ contract MCRTest is Test, IMCR {
mcr.initialize(staking, 0, 5, 10 seconds, custodians,120 seconds);
}

// A acceptor that is in place for acceptorTerm time should be replaced by a new acceptor after their term ended.
function testAcceptorRewards() public {
// Setup, with carol having no stake
(address alice, address bob, ) = setupGenesisWithThreeAttesters(50, 50, 0);

// TODO why do we need to whitelist the address?
staking.whitelistAddress(alice);
staking.whitelistAddress(bob);

// make superBlock commitments
MCRStorage.SuperBlockCommitment memory initCommitment = newHonestCommitment(1);
vm.prank(alice);
mcr.submitSuperBlockCommitment(initCommitment);
vm.prank(bob);
mcr.submitSuperBlockCommitment(initCommitment);

// check that alice is the current acceptor
// TODO: getCurrentAcceptor does not yet work.
// assertEq(mcr.getCurrentAcceptor(), alice);
console.log("WARNING: Test not correct yet, as getCurrentAcceptor does not work");

// TODO : here we should check that the reward goes only to alice
// alice can confirm the block comittment and get a reward
// TODO check that bob did not get the reward
vm.prank(bob);
mcr.postconfirmSuperBlocksAndRollover();
assertEq(mcr.getLastPostconfirmedSuperBlockHeight(), 1);

// Alice tries to postconfirm
// TODO: Alice should still get the reward
vm.prank(alice);
mcr.postconfirmSuperBlocksAndRollover();
assertEq(mcr.getLastPostconfirmedSuperBlockHeight(), 1);

// make second superblock commitment
MCRStorage.SuperBlockCommitment memory secondCommitment = newHonestCommitment(2);
vm.prank(alice);
mcr.submitSuperBlockCommitment(secondCommitment);
vm.prank(bob);
mcr.submitSuperBlockCommitment(secondCommitment);

// alice can confirm the block comittment and get a reward
vm.prank(alice);
mcr.postconfirmSuperBlocksAndRollover();
assertEq(mcr.getLastPostconfirmedSuperBlockHeight(), 2);
}


/// @notice Test that an attester cannot submit multiple commitments for the same height
function testAttesterCannotCommitTwice() public {
// three well-funded signers
Expand Down Expand Up @@ -651,4 +604,134 @@ contract MCRTest is Test, IMCR {
assertEq(mcr.getLastPostconfirmedSuperBlockHeight(), 0);
}

// ----------------------------------------------------------------
// -------- Acceptor tests --------------------------------------
// ----------------------------------------------------------------

/// @notice Test that getAcceptorStartTime correctly calculates term start times
function testAcceptorStartTime() public {
// Test at time 0
console.log("Current L1Block time:", block.timestamp);
assertEq(mcr.getAcceptorStartTime(), 0, "Acceptor term should start at time 0");

// Test at half an acceptor term
vm.warp(acceptorTerm/2);
assertEq(mcr.getAcceptorStartTime(), 0, "Acceptor term should start at time 0");

// Test at an acceptor term boundary
vm.warp(acceptorTerm);
assertEq(mcr.getAcceptorStartTime(), acceptorTerm, "Acceptor term should start at time acceptorTerm");

// Test at 1.5 acceptor terms
vm.warp(3 * acceptorTerm / 2);
assertEq(mcr.getAcceptorStartTime(), acceptorTerm, "Acceptor term should start at time acceptorTerm");
}

/// @notice Test setting acceptor term with validation
function testSetAcceptorTerm() public {
// Get initial epoch duration
console.log("Epoch duration expected:", epochDuration);
console.log("Epoch duration:", staking.getEpochDuration(address(mcr)));
assertEq(epochDuration, staking.getEpochDuration(address(mcr)));

// Try setting acceptor term to epoch duration (should fail)
vm.expectRevert(MCR.AcceptorTermTooLong.selector);
mcr.setAcceptorTerm(epochDuration);

// Try setting acceptor term to greater than epoch duration (should fail)
vm.expectRevert(MCR.AcceptorTermTooLong.selector);
mcr.setAcceptorTerm(epochDuration + 1);

// Set to valid term (half of epoch duration)
uint256 newTerm = epochDuration / 2;
mcr.setAcceptorTerm(newTerm);

// Verify term was updated
assertEq(mcr.acceptorTerm(), newTerm, "Acceptor term should be updated");
}

/// @notice Test that getAcceptor correctly selects an acceptor based on block hash
function testGetAcceptor() public {
// Setup with three attesters with equal stakes
(address alice, address bob, address carol) = setupGenesisWithThreeAttesters(1, 1, 1);
console.log("Alice:", alice);
console.log("Bob:", bob);
console.log("Carol:", carol);

// Get initial acceptor
address initialAcceptor = mcr.getAcceptor();
console.log("Initial acceptor:", initialAcceptor);
assertTrue(
initialAcceptor == alice || initialAcceptor == bob || initialAcceptor == carol,
"Acceptor must be one of the staked attesters"
);
console.log("Initial acceptor:", initialAcceptor);

// Verify acceptor stays the same within their term
vm.roll(block.number + 1);
console.log("Rolled to block:", block.number);
assertEq(mcr.getAcceptor(), initialAcceptor, "Acceptor should not change within term");
console.log("Same-term acceptor verification passed");

// Move past acceptor term (120 seconds, set in setUp)
vm.warp(acceptorTerm);
console.log("Warped to timestamp:", block.timestamp);

// Get new acceptor
address newAcceptor = mcr.getAcceptor();
console.log("New acceptor:", newAcceptor);
assertTrue(
newAcceptor == alice || newAcceptor == bob || newAcceptor == carol,
"New acceptor must be one of the staked attesters"
);
console.log("New acceptor:", newAcceptor);
}

// An acceptor that is in place for acceptorTerm time should be replaced by a new acceptor after their term ended.
function testAcceptorRewards() public {
// Setup, with carol having no stake
(address alice, address bob, ) = setupGenesisWithThreeAttesters(50, 50, 0);

// TODO why do we need to whitelist the address?
staking.whitelistAddress(alice);
staking.whitelistAddress(bob);

// make superBlock commitments
MCRStorage.SuperBlockCommitment memory initCommitment = newHonestCommitment(1);
vm.prank(alice);
mcr.submitSuperBlockCommitment(initCommitment);
vm.prank(bob);
mcr.submitSuperBlockCommitment(initCommitment);

// check that alice is the current acceptor
// TODO: getAcceptor does not yet work.
// assertEq(mcr.getAcceptor(), alice);
console.log("WARNING: Test not correct yet, as getAcceptor does not work");

// TODO : here we should check that the reward goes only to alice
// alice can confirm the block comittment and get a reward
// TODO check that bob did not get the reward
vm.prank(bob);
mcr.postconfirmSuperBlocksAndRollover();
assertEq(mcr.getLastPostconfirmedSuperBlockHeight(), 1);

// Alice tries to postconfirm
// TODO: Alice should still get the reward
vm.prank(alice);
mcr.postconfirmSuperBlocksAndRollover();
assertEq(mcr.getLastPostconfirmedSuperBlockHeight(), 1);

// make second superblock commitment
MCRStorage.SuperBlockCommitment memory secondCommitment = newHonestCommitment(2);
vm.prank(alice);
mcr.submitSuperBlockCommitment(secondCommitment);
vm.prank(bob);
mcr.submitSuperBlockCommitment(secondCommitment);

// alice can confirm the block comittment and get a reward
vm.prank(alice);
mcr.postconfirmSuperBlocksAndRollover();
assertEq(mcr.getLastPostconfirmedSuperBlockHeight(), 2);
}

}

0 comments on commit bfe6458

Please sign in to comment.