Skip to content

Commit

Permalink
feat(ethexe): support late commitments validation (#4426)
Browse files Browse the repository at this point in the history
  • Loading branch information
grishasobol authored Jan 17, 2025
1 parent cf6bb6f commit 1656dd0
Show file tree
Hide file tree
Showing 11 changed files with 261 additions and 19 deletions.
1 change: 1 addition & 0 deletions ethexe/contracts/script/Deployment.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ contract DeploymentScript is Script {
address(wrappedVara),
1 days,
2 hours,
5 minutes,
validatorsArray
)
)
Expand Down
17 changes: 15 additions & 2 deletions ethexe/contracts/src/Router.sol
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ contract Router is IRouter, OwnableUpgradeable, ReentrancyGuardTransient {
address _wrappedVara,
uint256 _eraDuration,
uint256 _electionDuration,
uint256 _validationDelay,
address[] calldata _validators
) public initializer {
__Ownable_init(_owner);
Expand All @@ -36,6 +37,9 @@ contract Router is IRouter, OwnableUpgradeable, ReentrancyGuardTransient {
require(block.timestamp > 0, "current timestamp must be greater than 0");
require(_electionDuration > 0, "election duration must be greater than 0");
require(_eraDuration > _electionDuration, "era duration must be greater than election duration");
// _validationDelay must be small enough,
// in order to restrict old era validators to make commitments, which can damage the system.
require(_validationDelay < (_eraDuration - _electionDuration) / 10, "validation delay is too big");

_setStorageSlot("router.storage.RouterV1");
Storage storage router = _router();
Expand All @@ -44,7 +48,7 @@ contract Router is IRouter, OwnableUpgradeable, ReentrancyGuardTransient {
router.implAddresses = Gear.AddressBook(_mirror, _mirrorProxy, _wrappedVara);
router.validationSettings.signingThresholdPercentage = Gear.SIGNING_THRESHOLD_PERCENTAGE;
router.computeSettings = Gear.defaultComputationSettings();
router.timelines = Gear.Timelines(_eraDuration, _electionDuration);
router.timelines = Gear.Timelines(_eraDuration, _electionDuration, _validationDelay);

// Set validators for the era 0.
_resetValidators(router.validationSettings.validators0, _validators, block.timestamp);
Expand Down Expand Up @@ -310,15 +314,24 @@ contract Router is IRouter, OwnableUpgradeable, ReentrancyGuardTransient {
Storage storage router = _router();
require(router.genesisBlock.hash != bytes32(0), "router genesis is zero; call `lookupGenesisHash()` first");

require(_blockCommitments.length > 0, "no block commitments to commit");

bytes memory blockCommitmentsHashes;
uint256 maxTimestamp = 0;

for (uint256 i = 0; i < _blockCommitments.length; i++) {
Gear.BlockCommitment calldata blockCommitment = _blockCommitments[i];
blockCommitmentsHashes = bytes.concat(blockCommitmentsHashes, _commitBlock(router, blockCommitment));
if (blockCommitment.timestamp > maxTimestamp) {
maxTimestamp = blockCommitment.timestamp;
}
}

// NOTE: Use maxTimestamp to validate signatures for all block commitments.
// This means that if at least one commitment is for block from current era,
// then all commitments should be checked with current era validators.
require(
Gear.validateSignatures(router, keccak256(blockCommitmentsHashes), _signatures),
Gear.validateSignaturesAt(router, keccak256(blockCommitmentsHashes), _signatures, maxTimestamp),
"signatures verification failed"
);
}
Expand Down
68 changes: 56 additions & 12 deletions ethexe/contracts/src/libraries/Gear.sol
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ library Gear {
struct Timelines {
uint256 era;
uint256 election;
uint256 validationDelay;
}

struct ValidationSettings {
Expand Down Expand Up @@ -191,7 +192,34 @@ library Gear {
view
returns (bool)
{
Validators storage validators = currentEraValidators(router);
return validateSignaturesAt(router, _dataHash, _signatures, block.timestamp);
}

/// @dev Validates signatures of the given data hash at the given timestamp.
function validateSignaturesAt(
IRouter.Storage storage router,
bytes32 _dataHash,
bytes[] calldata _signatures,
uint256 ts
) internal view returns (bool) {
uint256 eraStarted = eraStartedAt(router, block.timestamp);
if (ts < eraStarted && block.timestamp < eraStarted + router.timelines.validationDelay) {
require(ts >= router.genesisBlock.timestamp, "cannot validate before genesis");
require(ts + router.timelines.era >= eraStarted, "timestamp is older than previous era");

// Validation must be done using validators from previous era,
// because `ts` is in the past and we are in the validation delay period.
} else {
require(ts <= block.timestamp, "timestamp cannot be in the future");

if (ts < eraStarted) {
ts = eraStarted;
}

// Validation must be done using current era validators.
}

Validators storage validators = validatorsAt(router, ts);

uint256 threshold =
validatorsThreshold(validators.list.length, router.validationSettings.signingThresholdPercentage);
Expand All @@ -215,25 +243,33 @@ library Gear {
}

function currentEraValidators(IRouter.Storage storage router) internal view returns (Validators storage) {
if (currentEraValidatorsStoredInValidators1(router)) {
return router.validationSettings.validators1;
} else {
return router.validationSettings.validators0;
}
return validatorsAt(router, block.timestamp);
}

/// @dev Returns previous era validators, if there is no previous era,
/// then returns free validators slot, which must be zeroed.
function previousEraValidators(IRouter.Storage storage router) internal view returns (Validators storage) {
if (currentEraValidatorsStoredInValidators1(router)) {
if (validatorsStoredInSlot1At(router, block.timestamp)) {
return router.validationSettings.validators0;
} else {
return router.validationSettings.validators1;
}
}

/// @dev Returns whether current era validators are stored in `router.validationSettings.validators1`.
/// @dev Returns validators at the given timestamp.
/// @param ts Timestamp for which to get the validators.
function validatorsAt(IRouter.Storage storage router, uint256 ts) internal view returns (Validators storage) {
if (validatorsStoredInSlot1At(router, ts)) {
return router.validationSettings.validators1;
} else {
return router.validationSettings.validators0;
}
}

/// @dev Returns whether validators at `ts` are stored in `router.validationSettings.validators1`.
/// `false` means that current era validators are stored in `router.validationSettings.validators0`.
function currentEraValidatorsStoredInValidators1(IRouter.Storage storage router) internal view returns (bool) {
uint256 ts = block.timestamp;
/// @param ts Timestamp for which to check the validators slot.
function validatorsStoredInSlot1At(IRouter.Storage storage router, uint256 ts) internal view returns (bool) {
uint256 ts0 = router.validationSettings.validators0.useFromTimestamp;
uint256 ts1 = router.validationSettings.validators1.useFromTimestamp;

Expand All @@ -244,8 +280,8 @@ library Gear {
bool tsGE0 = ts0 <= ts;
bool tsGE1 = ts1 <= ts;

// Both eras are in the future - impossible case because of implementation.
require(tsGE0 || tsGE1, "could not identify validators for current timestamp");
// Both eras are in the future - not supported by this function.
require(tsGE0 || tsGE1, "could not identify validators for the given timestamp");

// Two impossible cases, because of math rules:
// 1) ts1Greater && !tsGE0 && tsGE1
Expand All @@ -266,4 +302,12 @@ library Gear {
function valueClaimBytes(ValueClaim memory claim) internal pure returns (bytes memory) {
return abi.encodePacked(claim.messageId, claim.destination, claim.value);
}

function eraIndexAt(IRouter.Storage storage router, uint256 ts) internal view returns (uint256) {
return (ts - router.genesisBlock.timestamp) / router.timelines.era;
}

function eraStartedAt(IRouter.Storage storage router, uint256 ts) internal view returns (uint256) {
return router.genesisBlock.timestamp + eraIndexAt(router, ts) * router.timelines.era;
}
}
2 changes: 2 additions & 0 deletions ethexe/contracts/test/Base.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ contract Base is POCBaseTest {
address public admin;
uint48 public eraDuration;
uint48 public electionDuration;
uint256 public validationDelay;
uint256 public blockDuration;
uint256 public maxValidators;

Expand Down Expand Up @@ -119,6 +120,7 @@ contract Base is POCBaseTest {
wrappedVaraAddress,
uint256(eraDuration),
uint256(electionDuration),
uint256(validationDelay),
_validators
)
)
Expand Down
181 changes: 181 additions & 0 deletions ethexe/contracts/test/Router.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ contract RouterTest is Base {
electionDuration = 100;
blockDuration = 12;
maxValidators = 3;
validationDelay = 60;

setUpWrappedVara();

Expand Down Expand Up @@ -116,6 +117,186 @@ contract RouterTest is Base {
commitValidators(wrongValidatorPrivateKeys, commitment);
}

function test_lateCommitments() public {
address[] memory _validators = new address[](3);
uint256[] memory _validatorPrivateKeys = new uint256[](3);
for (uint256 i = 0; i < 3; i++) {
(address addr, uint256 key) = makeAddrAndKey(vm.toString(i));
_validators[i] = addr;
_validatorPrivateKeys[i] = key;
}

Gear.ValidatorsCommitment memory _commitment = Gear.ValidatorsCommitment(_validators, 1);

vm.warp(router.genesisTimestamp() + eraDuration - electionDuration);
commitValidators(_commitment);

// Go to the next era, setting block hash to the last 1 blocks of the era
vm.warp(router.genesisTimestamp() + eraDuration - uint48(blockDuration));
rollBlocks(1);

uint256 _eraStartNumber = vm.getBlockNumber();
uint48 _eraStartTimestamp = uint48(vm.getBlockTimestamp());

Gear.BlockCommitment memory _blockCommitment = Gear.BlockCommitment({
hash: blockHash(_eraStartNumber - 1),
timestamp: _eraStartTimestamp - uint48(blockDuration),
previousCommittedBlock: router.latestCommittedBlockHash(),
predecessorBlock: blockHash(_eraStartNumber - 1),
transitions: new Gear.StateTransition[](0)
});

// Try to commit block from the previous era using new validators
vm.expectRevert();
commitBlock(_validatorPrivateKeys, _blockCommitment);

// Now try to commit block from the previous era using old validators
commitBlock(validatorsPrivateKeys, _blockCommitment);

rollBlocks(1);
_blockCommitment = Gear.BlockCommitment({
hash: blockHash(_eraStartNumber),
timestamp: _eraStartTimestamp,
previousCommittedBlock: router.latestCommittedBlockHash(),
predecessorBlock: blockHash(_eraStartNumber),
transitions: new Gear.StateTransition[](0)
});

// Try to commit block from the new era using old validators
vm.expectRevert();
commitBlock(validatorsPrivateKeys, _blockCommitment);

// Now try to commit block from the new era using new validators
commitBlock(_validatorPrivateKeys, _blockCommitment);
}

function test_lateCommitmentsAfterDelay() public {
address[] memory _validators = new address[](3);
uint256[] memory _validatorPrivateKeys = new uint256[](3);
for (uint256 i = 0; i < 3; i++) {
(address addr, uint256 key) = makeAddrAndKey(vm.toString(i));
_validators[i] = addr;
_validatorPrivateKeys[i] = key;
}

Gear.ValidatorsCommitment memory _commitment = Gear.ValidatorsCommitment(_validators, 1);

vm.warp(router.genesisTimestamp() + eraDuration - electionDuration);
commitValidators(_commitment);

// Go to the next era, setting block hash to the last 5 blocks of the era and first 5 blocks of the new era
vm.warp(router.genesisTimestamp() + eraDuration - 5 * uint48(blockDuration));
rollBlocks(10);

Gear.BlockCommitment memory _blockCommitment = Gear.BlockCommitment({
hash: blockHash(vm.getBlockNumber() - 6),
timestamp: uint48(vm.getBlockTimestamp() - 6 * blockDuration),
previousCommittedBlock: router.latestCommittedBlockHash(),
predecessorBlock: blockHash(vm.getBlockNumber() - 1),
transitions: new Gear.StateTransition[](0)
});

// Try to commit block from the previous era using old validators
// Must be failed because the validation delay is already passed
vm.expectRevert();
commitBlock(validatorsPrivateKeys, _blockCommitment);

// Now try to commit block from the previous era using new validators
// Must be successful because the validation delay is already passed
commitBlock(_validatorPrivateKeys, _blockCommitment);
}

function test_manyLateCommitments() public {
address[] memory _validators = new address[](3);
uint256[] memory _validatorPrivateKeys = new uint256[](3);
for (uint256 i = 0; i < 3; i++) {
(address addr, uint256 key) = makeAddrAndKey(vm.toString(i));
_validators[i] = addr;
_validatorPrivateKeys[i] = key;
}

Gear.ValidatorsCommitment memory _commitment = Gear.ValidatorsCommitment(_validators, 1);

vm.warp(router.genesisTimestamp() + eraDuration - electionDuration);
commitValidators(_commitment);

// Go to the next era, setting block hash to the last 4 blocks of the era
vm.warp(router.genesisTimestamp() + eraDuration - 4 * uint48(blockDuration));
rollBlocks(4);

uint256 _eraStartNumber = vm.getBlockNumber();
uint48 _eraStartTimestamp = uint48(vm.getBlockTimestamp());

// Try to commit blocks: [n - 4] <- [n - 3] <- [n]
// Where [n] is a start of the new era
Gear.BlockCommitment[] memory _commitments = new Gear.BlockCommitment[](3);
_commitments[0] = Gear.BlockCommitment({
hash: blockHash(_eraStartNumber - 4),
timestamp: _eraStartTimestamp - 4 * uint48(blockDuration),
previousCommittedBlock: router.latestCommittedBlockHash(),
predecessorBlock: blockHash(_eraStartNumber),
transitions: new Gear.StateTransition[](0)
});
_commitments[1] = Gear.BlockCommitment({
hash: blockHash(_eraStartNumber - 3),
timestamp: _eraStartTimestamp - 3 * uint48(blockDuration),
previousCommittedBlock: _commitments[0].hash,
predecessorBlock: blockHash(_eraStartNumber),
transitions: new Gear.StateTransition[](0)
});
_commitments[2] = Gear.BlockCommitment({
hash: blockHash(_eraStartNumber),
timestamp: _eraStartTimestamp,
previousCommittedBlock: _commitments[1].hash,
predecessorBlock: blockHash(_eraStartNumber),
transitions: new Gear.StateTransition[](0)
});

// Roll to next block to be possible to make commitment for the era start block
rollBlocks(1);

// Validation must fail because the last block is from new era, so must be committed by new validators
vm.expectRevert();
commitBlocks(validatorsPrivateKeys, _commitments);

// Now try to commit [n - 4] <- [n - 3] <- [n - 2]
_commitments[2] = Gear.BlockCommitment({
hash: blockHash(_eraStartNumber - 2),
timestamp: _eraStartTimestamp - 2 * uint48(blockDuration),
previousCommittedBlock: _commitments[1].hash,
predecessorBlock: blockHash(_eraStartNumber),
transitions: new Gear.StateTransition[](0)
});
// Must be successful, because all blocks are from the previous era
commitBlocks(validatorsPrivateKeys, _commitments);

// Now try to commit [n - 1] <- [n] <- [n + 1] using new validators
rollBlocks(1);
_commitments[0] = Gear.BlockCommitment({
hash: blockHash(_eraStartNumber - 1),
timestamp: _eraStartTimestamp - uint48(blockDuration),
previousCommittedBlock: router.latestCommittedBlockHash(),
predecessorBlock: blockHash(_eraStartNumber),
transitions: new Gear.StateTransition[](0)
});
_commitments[1] = Gear.BlockCommitment({
hash: blockHash(_eraStartNumber),
timestamp: _eraStartTimestamp,
previousCommittedBlock: _commitments[0].hash,
predecessorBlock: blockHash(_eraStartNumber),
transitions: new Gear.StateTransition[](0)
});
_commitments[2] = Gear.BlockCommitment({
hash: blockHash(_eraStartNumber + 1),
timestamp: _eraStartTimestamp + uint48(blockDuration),
previousCommittedBlock: _commitments[1].hash,
predecessorBlock: blockHash(_eraStartNumber),
transitions: new Gear.StateTransition[](0)
});
// Must be successful, because the newest blocks are from the new era
commitBlocks(_validatorPrivateKeys, _commitments);
}

/* helper functions */

function commitValidators(Gear.ValidatorsCommitment memory commitment) private {
Expand Down
2 changes: 1 addition & 1 deletion ethexe/ethereum/Mirror.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion ethexe/ethereum/MirrorProxy.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion ethexe/ethereum/Router.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion ethexe/ethereum/TransparentUpgradeableProxy.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion ethexe/ethereum/WrappedVara.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions ethexe/ethereum/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ impl Ethereum {
_wrappedVara: wvara_address,
_eraDuration: U256::from(24 * 60 * 60),
_electionDuration: U256::from(2 * 60 * 60),
_validationDelay: U256::from(60),
_validators: validators,
}
.abi_encode(),
Expand Down

0 comments on commit 1656dd0

Please sign in to comment.