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

Tighten next bottom up checkpoint height #824

Merged
merged 2 commits into from
Mar 20, 2024
Merged
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 contracts/src/errors/IPCErrors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ error FailedRemoveIncompleteQuorum();
error GatewayCannotBeZero();
error InvalidActorAddress();
error InvalidCheckpointEpoch();
error CannotSubmitFutureCheckpoint();
error InvalidBatchEpoch();
error InvalidCheckpointSource();
error InvalidBatchSource();
Expand Down
3 changes: 0 additions & 3 deletions contracts/src/gateway/router/CheckpointingFacet.sol
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,6 @@ contract CheckpointingFacet is GatewayActorModifiers {
bytes32 membershipRootHash,
uint256 membershipWeight
) external systemActorOnly {
if (checkpoint.blockHeight % s.bottomUpCheckPeriod != 0) {
raulk marked this conversation as resolved.
Show resolved Hide resolved
revert InvalidCheckpointEpoch();
}
if (LibGateway.bottomUpCheckpointExists(checkpoint.blockHeight)) {
revert CheckpointAlreadyExists();
}
Expand Down
60 changes: 33 additions & 27 deletions contracts/src/subnet/SubnetActorCheckpointingFacet.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.23;

import {InvalidBatchEpoch, MaxMsgsPerBatchExceeded, InvalidSignatureErr, BottomUpCheckpointAlreadySubmitted, InvalidCheckpointEpoch} from "../errors/IPCErrors.sol";
import {InvalidBatchEpoch, MaxMsgsPerBatchExceeded, InvalidSignatureErr, BottomUpCheckpointAlreadySubmitted, CannotSubmitFutureCheckpoint, InvalidCheckpointEpoch} from "../errors/IPCErrors.sol";
import {IGateway} from "../interfaces/IGateway.sol";
import {BottomUpCheckpoint, BottomUpMsgBatch, BottomUpMsgBatchInfo} from "../structs/CrossNet.sol";
import {Validator, ValidatorSet} from "../structs/Subnet.sol";
Expand All @@ -12,6 +12,7 @@ import {LibValidatorSet, LibStaking} from "../lib/LibStaking.sol";
import {EnumerableSet} from "openzeppelin-contracts/utils/structs/EnumerableSet.sol";
import {LibSubnetActor} from "../lib/LibSubnetActor.sol";
import {Pausable} from "../lib/LibPausable.sol";
import {LibGateway} from "../lib/LibGateway.sol";

contract SubnetActorCheckpointingFacet is SubnetActorModifiers, ReentrancyGuard, Pausable {
using EnumerableSet for EnumerableSet.AddressSet;
Expand All @@ -32,28 +33,20 @@ contract SubnetActorCheckpointingFacet is SubnetActorModifiers, ReentrancyGuard,

bytes32 checkpointHash = keccak256(abi.encode(checkpoint));

if (checkpoint.blockHeight <= s.lastBottomUpCheckpointHeight) {
revert BottomUpCheckpointAlreadySubmitted();
}

// When a bottom up msg batch becomes full, a bottom up checkpoint will be
// created before the next checkpoint period is reached.
if (checkpoint.blockHeight <= s.lastBottomUpCheckpointHeight + s.bottomUpCheckPeriod) {
// validate signatures and quorum threshold, revert if validation fails
validateActiveQuorumSignatures({signatories: signatories, hash: checkpointHash, signatures: signatures});
// validate signatures and quorum threshold, revert if validation fails
validateActiveQuorumSignatures({signatories: signatories, hash: checkpointHash, signatures: signatures});
Comment on lines +36 to +37
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the future, #791 and this PR are closely interrelated. The former is adding checks that this PR is now deleting. It would've been simpler to push these changes to the previous PR, to prevent reviewers from reviewing stale logic.


// If the checkpoint height is the next expected height then this is a new checkpoint which must be executed
// in the Gateway Actor, the checkpoint and the relayer must be stored, last bottom-up checkpoint updated.
s.committedCheckpoints[checkpoint.blockHeight] = checkpoint;
// If the checkpoint height is the next expected height then this is a new checkpoint which must be executed
// in the Gateway Actor, the checkpoint and the relayer must be stored, last bottom-up checkpoint updated.
s.committedCheckpoints[checkpoint.blockHeight] = checkpoint;

s.lastBottomUpCheckpointHeight = checkpoint.blockHeight;
s.lastBottomUpCheckpointHeight = checkpoint.blockHeight;

// Commit in gateway to distribute rewards
IGateway(s.ipcGatewayAddr).commitCheckpoint(checkpoint);
// Commit in gateway to distribute rewards
IGateway(s.ipcGatewayAddr).commitCheckpoint(checkpoint);

// confirming the changes in membership in the child
LibStaking.confirmChange(checkpoint.nextConfigurationNumber);
}
// confirming the changes in membership in the child
LibStaking.confirmChange(checkpoint.nextConfigurationNumber);
}

/// @notice Checks whether the signatures are valid for the provided signatories and hash within the current validator set.
Expand Down Expand Up @@ -96,17 +89,30 @@ contract SubnetActorCheckpointingFacet is SubnetActorModifiers, ReentrancyGuard,
revert MaxMsgsPerBatchExceeded();
}

// if the bottom up messages' length is max, we consider that epoch valid
if (checkpoint.msgs.length == s.maxMsgsPerBottomUpBatch) {
uint256 lastBottomUpCheckpointHeight = s.lastBottomUpCheckpointHeight;
uint256 bottomUpCheckPeriod = s.bottomUpCheckPeriod;

// cannot submit past bottom up checkpoint
if (checkpoint.blockHeight <= lastBottomUpCheckpointHeight) {
revert BottomUpCheckpointAlreadySubmitted();
}

uint256 nextCheckpointHeight = LibGateway.getNextEpoch(lastBottomUpCheckpointHeight, bottomUpCheckPeriod);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok so if I understand this right, then in case we had an early submission due to reaching msg size limit then we still continue to do checkpoints at multiples of bottomUpCheckPeriod after the early submission?

So for example if both bottomUpCheckPeriod and maxMsgsPerBottomUpBatch is 10, and we had the following checkpoints and msg sizes:

chk 10 and 2 msg
chk 20 and 3 msg
chk 25 and 10msg -> here we had 10 msg already at height 25 causing early submission
chk 30 and 2 msg
...

I s this correctly understood? I may have this wrong though as the screenshot that raul shares does not show that this is the case :S

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought the above screenshot is aligned with what you described? Just the above screenshot has checkpoint period 60. Am I missing sth?


if (checkpoint.blockHeight > nextCheckpointHeight) {
revert CannotSubmitFutureCheckpoint();
}

// the expected bottom up checkpoint height, valid height
if (checkpoint.blockHeight == nextCheckpointHeight) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not line up with my understanding Fendermint, which is also not being updated in this PR. So I think this will PR will break checkpointing.

} else if height.value() % gateway.bottom_up_check_period(state)? == 0 {

I believe Fendermint produces checkpoints every bottomUpCheckPeriod from genesis. If an early checkpoint was produced due to reaching the msg batch limit, Fendermint will still produce the next checkpoint at the original schedule, but this contract change will expect it at exactly bottomUpCheckPeriod epochs after the last checkpoint.


Test screenshot you shared with me also confirms my understanding is correct:

image

Did you verify that the checkpoint at 1500 was accepted by the subnet actor at the parent?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the confusion comes from this line:

uint256 nextCheckpointHeight = LibGateway.getNextEpoch(lastBottomUpCheckpointHeight, bottomUpCheckPeriod);

This function basically deduces the next checkpoint height, see test cases added.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logic seems correct, apologies for the false alarm! I'd suggest improving the naming of the method at least (LibGateway.getNextEpoch() is missing information).

return;
}

// the max batch size not reached, we only support checkpoint period submission.
uint256 lastBottomUpCheckpointHeight = s.lastBottomUpCheckpointHeight;
if (checkpoint.blockHeight != lastBottomUpCheckpointHeight + s.bottomUpCheckPeriod) {
if (checkpoint.blockHeight != lastBottomUpCheckpointHeight) {
revert InvalidCheckpointEpoch();
}
// if the bottom up messages' length is max, we consider that epoch valid, allow early submission
if (checkpoint.msgs.length == s.maxMsgsPerBottomUpBatch) {
return;
}

revert InvalidCheckpointEpoch();
}
}
18 changes: 0 additions & 18 deletions contracts/test/integration/GatewayDiamond.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -1124,24 +1124,6 @@ contract GatewayActorDiamondTest is Test, IntegrationTestBase {
);
vm.stopPrank();

// failed to create a checkpoint with the height not multiple to checkpoint period
checkpoint = BottomUpCheckpoint({
subnetID: gatewayDiamond.getter().getNetworkName(),
blockHeight: d + d / 2,
blockHash: keccak256("block2"),
nextConfigurationNumber: 2,
msgs: new IpcEnvelope[](0)
});

vm.startPrank(FilAddress.SYSTEM_ACTOR);
vm.expectRevert(InvalidCheckpointEpoch.selector);
gatewayDiamond.checkpointer().createBottomUpCheckpoint(
checkpoint,
membershipRoot,
weights[0] + weights[1] + weights[2]
);
vm.stopPrank();

(bool ok, uint256 e, ) = gatewayDiamond.getter().getCurrentBottomUpCheckpoint();
require(ok, "checkpoint not exist");
require(e == d, "out height incorrect");
Expand Down
176 changes: 174 additions & 2 deletions contracts/test/integration/SubnetActorDiamond.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -714,13 +714,13 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase {

// skip the current checkpoint, should fail
checkpointWithIncorrectHeight.blockHeight = saDiamond.getter().bottomUpCheckPeriod() + 1;
vm.expectRevert(InvalidCheckpointEpoch.selector);
vm.expectRevert(CannotSubmitFutureCheckpoint.selector);
vm.prank(validators[0]);
saDiamond.checkpointer().submitCheckpoint(checkpointWithIncorrectHeight, validators, signatures);

// skip the curent checkpoint but submit at the next bottom up checkpoint, should fail
checkpointWithIncorrectHeight.blockHeight = saDiamond.getter().bottomUpCheckPeriod() * 2;
vm.expectRevert(InvalidCheckpointEpoch.selector);
vm.expectRevert(CannotSubmitFutureCheckpoint.selector);
vm.prank(validators[0]);
saDiamond.checkpointer().submitCheckpoint(checkpointWithIncorrectHeight, validators, signatures);

Expand Down Expand Up @@ -839,6 +839,163 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase {
saDiamond.checkpointer().submitCheckpoint(checkpoint, validators, signatures);
}

function testSubnetActorDiamond_submitCheckpoint_mixAndMatch() public {
(uint256[] memory keys, address[] memory validators, ) = TestUtils.getThreeValidators(vm);
bytes[] memory pubKeys = new bytes[](3);
bytes[] memory signatures = new bytes[](3);

for (uint256 i = 0; i < 3; i++) {
vm.deal(validators[i], 10 gwei);
pubKeys[i] = TestUtils.deriveValidatorPubKeyBytes(keys[i]);
vm.prank(validators[i]);
saDiamond.manager().join{value: 10}(pubKeys[i]);
}

vm.deal(address(saDiamond), 100 ether);
vm.prank(address(saDiamond));
gatewayDiamond.manager().register{value: DEFAULT_MIN_VALIDATOR_STAKE + 3 * DEFAULT_CROSS_MSG_FEE}(
3 * DEFAULT_CROSS_MSG_FEE
);

SubnetID memory localSubnetID = saDiamond.getter().getParent().createSubnetId(address(saDiamond));

IpcEnvelope[] memory msgs = new IpcEnvelope[](MAX_MSGS_PER_BATCH);
for (uint256 i = 0; i < MAX_MSGS_PER_BATCH; i++) {
IpcEnvelope memory crossMsg = TestUtils.newXnetCallMsg(
IPCAddress({subnetId: localSubnetID, rawAddress: FvmAddressHelper.from(address(saDiamond))}),
IPCAddress({
subnetId: saDiamond.getter().getParent(),
rawAddress: FvmAddressHelper.from(address(saDiamond))
}),
1,
uint64(i)
);
msgs[i] = crossMsg;
}

vm.prank(validators[0]);

// submit a full msg batch, even though next expected height is bottomUpCheckPeriod()
BottomUpCheckpoint memory checkpoint = BottomUpCheckpoint({
subnetID: localSubnetID,
blockHeight: 1,
blockHash: keccak256("block1"),
nextConfigurationNumber: 0,
msgs: msgs
});
submitCheckpointInternal(checkpoint, validators, signatures, keys);
require(saDiamond.getter().lastBottomUpCheckpointHeight() == 1, " checkpoint height incorrect");

// submit a full msg batch, allow early submission,
// even though next expected height is bottomUpCheckPeriod()
checkpoint = BottomUpCheckpoint({
subnetID: localSubnetID,
blockHeight: 3,
blockHash: keccak256("block2"),
nextConfigurationNumber: 0,
msgs: msgs
});
submitCheckpointInternal(checkpoint, validators, signatures, keys);
require(saDiamond.getter().lastBottomUpCheckpointHeight() == 3, " checkpoint height incorrect");

// should not allow submission of past checkpoints already confirmed, last bottom up checkpoint height is 3
checkpoint = BottomUpCheckpoint({
subnetID: localSubnetID,
blockHeight: 2,
blockHash: keccak256("block1"),
nextConfigurationNumber: 0,
msgs: msgs
});
vm.expectRevert(BottomUpCheckpointAlreadySubmitted.selector);
submitCheckpointInternal(checkpoint, validators, signatures, keys);

// submit future checkpoint, should reject
checkpoint = BottomUpCheckpoint({
subnetID: localSubnetID,
blockHeight: saDiamond.getter().bottomUpCheckPeriod() + 1,
blockHash: keccak256("block2"),
nextConfigurationNumber: 0,
msgs: msgs
});
vm.expectRevert(CannotSubmitFutureCheckpoint.selector);
submitCheckpointInternal(checkpoint, validators, signatures, keys);

checkpoint = BottomUpCheckpoint({
subnetID: localSubnetID,
blockHeight: saDiamond.getter().bottomUpCheckPeriod(),
blockHash: keccak256("block2"),
nextConfigurationNumber: 0,
msgs: new IpcEnvelope[](0)
});
submitCheckpointInternal(checkpoint, validators, signatures, keys);
require(
saDiamond.getter().lastBottomUpCheckpointHeight() == saDiamond.getter().bottomUpCheckPeriod(),
" checkpoint height incorrect"
);

checkpoint = BottomUpCheckpoint({
subnetID: localSubnetID,
blockHeight: saDiamond.getter().bottomUpCheckPeriod() + 1,
blockHash: keccak256("block2"),
nextConfigurationNumber: 0,
msgs: msgs
});
submitCheckpointInternal(checkpoint, validators, signatures, keys);
require(
saDiamond.getter().lastBottomUpCheckpointHeight() == saDiamond.getter().bottomUpCheckPeriod() + 1,
" checkpoint height incorrect"
);

checkpoint = BottomUpCheckpoint({
subnetID: localSubnetID,
blockHeight: saDiamond.getter().bottomUpCheckPeriod() + 2,
blockHash: keccak256("block2"),
nextConfigurationNumber: 0,
msgs: msgs
});
submitCheckpointInternal(checkpoint, validators, signatures, keys);
require(
saDiamond.getter().lastBottomUpCheckpointHeight() == saDiamond.getter().bottomUpCheckPeriod() + 2,
" checkpoint height incorrect"
);

checkpoint = BottomUpCheckpoint({
subnetID: localSubnetID,
blockHeight: saDiamond.getter().bottomUpCheckPeriod() + 3,
blockHash: keccak256("block2"),
nextConfigurationNumber: 0,
msgs: new IpcEnvelope[](0)
});
vm.expectRevert(InvalidCheckpointEpoch.selector);
submitCheckpointInternal(checkpoint, validators, signatures, keys);

checkpoint = BottomUpCheckpoint({
subnetID: localSubnetID,
blockHeight: saDiamond.getter().bottomUpCheckPeriod() * 2,
blockHash: keccak256("block2"),
nextConfigurationNumber: 0,
msgs: new IpcEnvelope[](0)
});
submitCheckpointInternal(checkpoint, validators, signatures, keys);
require(
saDiamond.getter().lastBottomUpCheckpointHeight() == saDiamond.getter().bottomUpCheckPeriod() * 2,
" checkpoint height incorrect"
);

checkpoint = BottomUpCheckpoint({
subnetID: localSubnetID,
blockHeight: saDiamond.getter().bottomUpCheckPeriod() * 3,
blockHash: keccak256("block2"),
nextConfigurationNumber: 0,
msgs: new IpcEnvelope[](0)
});
submitCheckpointInternal(checkpoint, validators, signatures, keys);
require(
saDiamond.getter().lastBottomUpCheckpointHeight() == saDiamond.getter().bottomUpCheckPeriod() * 3,
" checkpoint height incorrect"
);
}

function testSubnetActorDiamond_submitCheckpointWithReward() public {
(uint256[] memory keys, address[] memory validators, ) = TestUtils.getThreeValidators(vm);
bytes[] memory pubKeys = new bytes[](3);
Expand Down Expand Up @@ -1626,4 +1783,19 @@ contract SubnetActorDiamondTest is Test, IntegrationTestBase {
saDiamond.pauser().unpause();
require(!saDiamond.pauser().paused(), "paused");
}

function submitCheckpointInternal(
BottomUpCheckpoint memory checkpoint,
address[] memory validators,
bytes[] memory signatures,
uint256[] memory keys
) internal {
bytes32 hash = keccak256(abi.encode(checkpoint));
for (uint256 i = 0; i < 3; i++) {
(uint8 v, bytes32 r, bytes32 s) = vm.sign(keys[i], hash);
signatures[i] = abi.encodePacked(r, s, v);
}

saDiamond.checkpointer().submitCheckpoint(checkpoint, validators, signatures);
}
}
25 changes: 25 additions & 0 deletions contracts/test/unit/LibGateway.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -426,4 +426,29 @@ contract LibGatewayTest is Test {

t.applyMsg(childSubnet, crossMsg);
}

function test_nextCheckpointEpoch() public pure {
uint64 checkpointPeriod = 10;

require(LibGateway.getNextEpoch(0, checkpointPeriod) == checkpointPeriod, "next epoch not correct");
require(LibGateway.getNextEpoch(1, checkpointPeriod) == checkpointPeriod, "next epoch not correct");
require(LibGateway.getNextEpoch(10, checkpointPeriod) == checkpointPeriod * 2, "next epoch not correct");
require(LibGateway.getNextEpoch(15, checkpointPeriod) == checkpointPeriod * 2, "next epoch not correct");

checkpointPeriod = 17;

require(LibGateway.getNextEpoch(0, checkpointPeriod) == checkpointPeriod, "next epoch not correct");
require(
LibGateway.getNextEpoch(checkpointPeriod - 1, checkpointPeriod) == checkpointPeriod,
"next epoch not correct"
);
require(
LibGateway.getNextEpoch(checkpointPeriod, checkpointPeriod) == checkpointPeriod * 2,
"next epoch not correct"
);
require(
LibGateway.getNextEpoch(checkpointPeriod + 1, checkpointPeriod) == checkpointPeriod * 2,
"next epoch not correct"
);
}
}
Loading