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

feat(ethexe): support late commitments validation #4426

Open
wants to merge 19 commits into
base: master
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
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
Loading