Skip to content

Commit 00ed962

Browse files
feat: two phase bottom-up checkpoint batch execution (#1330)
Co-authored-by: cryptoAtwill <108330426+cryptoAtwill@users.noreply.github.com>
1 parent 464d5b3 commit 00ed962

38 files changed

Lines changed: 2259 additions & 1045 deletions

contracts/.storage-layouts/GatewayActorModifiers.json

Lines changed: 216 additions & 188 deletions
Large diffs are not rendered by default.

contracts/.storage-layouts/GatewayDiamond.json

Lines changed: 215 additions & 187 deletions
Large diffs are not rendered by default.

contracts/.storage-layouts/SubnetActorDiamond.json

Lines changed: 153 additions & 245 deletions
Large diffs are not rendered by default.

contracts/.storage-layouts/SubnetActorModifiers.json

Lines changed: 154 additions & 246 deletions
Large diffs are not rendered by default.

contracts/contracts/errors/IPCErrors.sol

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ error GatewayCannotBeZero();
2222
error InvalidActorAddress();
2323
error InvalidCheckpointEpoch();
2424
error CannotSubmitFutureCheckpoint();
25+
error BatchMsgAlreadyExecuted();
26+
error MissingBatchCommitment();
27+
error DuplicatedCheckpointHeight(uint64 height);
28+
error InvalidInclusionProof();
2529
error InvalidBatchEpoch();
2630
error InvalidCheckpointSource();
2731
error InvalidBatchSource();

contracts/contracts/gateway/router/CheckpointingFacet.sol

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
pragma solidity ^0.8.23;
33

44
import {GatewayActorModifiers} from "../../lib/LibGatewayActorStorage.sol";
5-
import {BottomUpCheckpoint} from "../../structs/CrossNet.sol";
5+
import {BottomUpCheckpoint, IpcEnvelope} from "../../structs/CrossNet.sol";
6+
import {BottomUpBatchRecorded} from "../../structs/BottomUpBatch.sol";
67
import {LibGateway} from "../../lib/LibGateway.sol";
78
import {LibQuorum} from "../../lib/LibQuorum.sol";
89
import {Subnet} from "../../structs/Subnet.sol";
@@ -45,20 +46,31 @@ contract CheckpointingFacet is GatewayActorModifiers {
4546

4647
LibGateway.checkMsgLength(checkpoint.msgs);
4748

48-
execBottomUpMsgs(checkpoint.msgs, subnet);
49-
5049
emit CheckpointCommitted({subnet: checkpoint.subnetID.getAddress(), subnetHeight: checkpoint.blockHeight});
5150
}
5251

52+
/// @notice submit a verified batch of committed cross-net messages for execution.
53+
/// @param msgs The batch of messages to be executed.
54+
function execBottomUpMsgBatch(IpcEnvelope[] calldata msgs) external {
55+
(bool subnetExists, Subnet storage subnet) = LibGateway.getSubnet(msg.sender);
56+
if (!subnetExists) {
57+
revert SubnetNotFound();
58+
}
59+
60+
_execBottomUpMsgBatch(msgs, subnet);
61+
}
62+
5363
/// @notice creates a new bottom-up checkpoint
5464
/// @param checkpoint - a bottom-up checkpoint
5565
/// @param membershipRootHash - a root hash of the Merkle tree built from the validator public keys and their weight
5666
/// @param membershipWeight - the total weight of the membership
67+
/// @param msgs - the full messages batch
5768
/// @param activity - the full activity rollup
5869
function createBottomUpCheckpoint(
5970
BottomUpCheckpoint calldata checkpoint,
6071
bytes32 membershipRootHash,
6172
uint256 membershipWeight,
73+
IpcEnvelope[] calldata msgs,
6274
FullActivityRollup calldata activity
6375
) external systemActorOnly {
6476
if (LibGateway.bottomUpCheckpointExists(checkpoint.blockHeight)) {
@@ -76,6 +88,7 @@ contract CheckpointingFacet is GatewayActorModifiers {
7688

7789
LibGateway.storeBottomUpCheckpoint(checkpoint);
7890

91+
emit BottomUpBatchRecorded(uint64(checkpoint.blockHeight), msgs);
7992
emit ActivityRollupRecorded(uint64(checkpoint.blockHeight), activity);
8093
}
8194

@@ -109,9 +122,7 @@ contract CheckpointingFacet is GatewayActorModifiers {
109122
});
110123
}
111124

112-
/// @notice submit a batch of cross-net messages for execution.
113-
/// @param msgs The batch of bottom-up cross-network messages to be executed.
114-
function execBottomUpMsgs(IpcEnvelope[] calldata msgs, Subnet storage subnet) internal {
125+
function _execBottomUpMsgBatch(IpcEnvelope[] calldata msgs, Subnet storage subnet) internal {
115126
uint256 totalValue;
116127
uint256 crossMsgLength = msgs.length;
117128

contracts/contracts/interfaces/IGateway.sol

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ interface IGateway {
2626
/// @notice commitCheckpoint propagates the commitment of a checkpoint from a child
2727
function commitCheckpoint(BottomUpCheckpoint calldata bottomUpCheckpoint) external;
2828

29+
/// @notice submit a verified batch of committed cross-net messages for execution.
30+
function execBottomUpMsgBatch(IpcEnvelope[] calldata msgs) external;
31+
2932
/// @notice fund locks the received funds —denominated in the native coin— and moves the value down the hierarchy,
3033
/// crediting the funds to the specified address in the destination network.
3134
///

contracts/contracts/lib/LibActivity.sol

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -99,23 +99,20 @@ library LibActivity {
9999

100100
SubnetKey subnetKey = SubnetKey.wrap(subnet.toHash());
101101

102-
uint256 size = s.tracker[subnetKey].pendingHeights.length();
102+
ConsensusTracker storage tracker = s.tracker[subnetKey];
103+
bytes32[] memory heights = tracker.pendingHeights.values();
104+
uint256 size = heights.length;
105+
103106
result = new ListPendingReturnEntry[](size);
104107

105-
// Ok to not optimize with unchecked increments, since we expect this to be used off-chain only, for introspection.
106108
for (uint256 i = 0; i < size; i++) {
107-
ConsensusTracker storage tracker = s.tracker[subnetKey];
108-
bytes32[] memory heights = tracker.pendingHeights.values();
109-
110-
for (uint256 j = 0; j < heights.length; j++) {
111-
uint64 height = uint64(uint256(heights[j]) << 192 >> 192);
112-
ConsensusPendingAtHeight storage pending = tracker.pending[height];
113-
result[i] = ListPendingReturnEntry({
114-
height: height,
115-
summary: pending.summary,
116-
claimed: pending.claimed.values()
117-
});
118-
}
109+
uint64 height = uint64(uint256(heights[i]));
110+
ConsensusPendingAtHeight storage pending = tracker.pending[height];
111+
result[i] = ListPendingReturnEntry({
112+
height: height,
113+
summary: pending.summary,
114+
claimed: pending.claimed.values()
115+
});
119116
}
120117

121118
return result;
@@ -151,7 +148,16 @@ library LibActivity {
151148
// Prune state for this height if all validators have claimed.
152149
if (pending.claimed.length() == pending.summary.stats.totalActiveValidators) {
153150
ConsensusTracker storage tracker = s.tracker[subnetKey];
151+
154152
tracker.pendingHeights.remove(bytes32(uint256(checkpointHeight)));
153+
154+
// Clear nested set before deleting the struct.
155+
ConsensusPendingAtHeight storage pending = tracker.pending[checkpointHeight];
156+
uint256 len = pending.claimed.length();
157+
for (uint256 i = 0; i < len; i++) {
158+
address addr = pending.claimed.at(0);
159+
pending.claimed.remove(addr);
160+
}
155161
delete tracker.pending[checkpointHeight];
156162
}
157163
}
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
// SPDX-License-Identifier: MIT OR Apache-2.0
2+
pragma solidity ^0.8.23;
3+
4+
import {IValidatorRewarder} from "../interfaces/IValidatorRewarder.sol";
5+
import {Consensus, CompressedActivityRollup} from "../structs/Activity.sol";
6+
import {BottomUpBatch} from "../structs/BottomUpBatch.sol";
7+
import {IpcEnvelope} from "../structs/CrossNet.sol";
8+
import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
9+
import {MerkleProof} from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
10+
import {SubnetID} from "../structs/Subnet.sol";
11+
import {SubnetIDHelper} from "../lib/SubnetIDHelper.sol";
12+
import {InvalidInclusionProof, BatchMsgAlreadyExecuted, MissingBatchCommitment, DuplicatedCheckpointHeight} from "../errors/IPCErrors.sol";
13+
import {BottomUpBatch} from "../structs/BottomUpBatch.sol";
14+
15+
/// Library to handle bottom up batch 2-phase execution.
16+
library LibBottomUpBatch {
17+
bytes32 private constant NAMESPACE = keccak256("bottomupbatch");
18+
19+
using EnumerableSet for EnumerableSet.Bytes32Set;
20+
21+
/// @notice Represents a pending bottom-up batch commitment awaiting full execution at a specific checkpoint height.
22+
struct PendingBatch {
23+
/// @notice The pending batch commitment.
24+
BottomUpBatch.Commitment commitment;
25+
/// @notice Set of message leaf hashes that have already been executed for this batch.
26+
EnumerableSet.Bytes32Set executed;
27+
}
28+
29+
/// @notice Storage structure used by the SubnetActor to manage bottom-up message batches and their execution status.
30+
struct BottomUpBatchStorage {
31+
/// @notice Set of checkpoint heights with batches that are still pending execution.
32+
EnumerableSet.Bytes32Set pendingHeights;
33+
/// @notice Mapping of checkpoint height to its pending batch data.
34+
mapping(uint256 => PendingBatch) pending;
35+
}
36+
37+
function ensureValidProof(
38+
BottomUpBatch.MerkleHash[] memory proof,
39+
BottomUpBatch.MerkleHash root,
40+
BottomUpBatch.MerkleHash leaf
41+
) internal pure {
42+
bytes32[] memory proofBytes = new bytes32[](proof.length);
43+
for (uint256 i = 0; i < proof.length; i++) {
44+
proofBytes[i] = BottomUpBatch.MerkleHash.unwrap(proof[i]);
45+
}
46+
bool valid = MerkleProof.verify({
47+
proof: proofBytes,
48+
root: BottomUpBatch.MerkleHash.unwrap(root),
49+
leaf: BottomUpBatch.MerkleHash.unwrap(leaf)
50+
});
51+
if (!valid) {
52+
revert InvalidInclusionProof();
53+
}
54+
}
55+
56+
function recordBottomUpBatchCommitment(
57+
uint64 checkpointHeight,
58+
BottomUpBatch.Commitment calldata commitment
59+
) internal {
60+
BottomUpBatchStorage storage s = bottomUpBatchStorage();
61+
62+
bool added = s.pendingHeights.add(bytes32(uint256(checkpointHeight)));
63+
if (!added) {
64+
revert DuplicatedCheckpointHeight(checkpointHeight);
65+
}
66+
67+
PendingBatch storage pending = s.pending[checkpointHeight];
68+
pending.commitment = commitment;
69+
}
70+
71+
function processBottomUpBatchMsg(
72+
uint256 checkpointHeight,
73+
IpcEnvelope calldata ipcMsg,
74+
BottomUpBatch.MerkleHash[] calldata proof
75+
) internal {
76+
BottomUpBatchStorage storage s = bottomUpBatchStorage();
77+
78+
// Find the pending batch.
79+
PendingBatch storage pending = s.pending[checkpointHeight];
80+
BottomUpBatch.MerkleHash root = pending.commitment.msgsRoot;
81+
if (BottomUpBatch.MerkleHash.unwrap(root) == bytes32(0)) {
82+
revert MissingBatchCommitment();
83+
}
84+
85+
// Check the validity of the proof.
86+
BottomUpBatch.MerkleHash leaf = makeLeaf(ipcMsg);
87+
ensureValidProof(
88+
proof,
89+
root,
90+
leaf
91+
);
92+
93+
bool added = pending.executed.add(BottomUpBatch.MerkleHash.unwrap(leaf));
94+
if (!added) {
95+
revert BatchMsgAlreadyExecuted();
96+
}
97+
98+
// Prune state for this height if all msgs were executed.
99+
if (pending.executed.length() == pending.commitment.totalNumMsgs) {
100+
s.pendingHeights.remove(bytes32(uint256(checkpointHeight)));
101+
102+
// Clear nested set before deleting the struct.
103+
PendingBatch storage pending = s.pending[checkpointHeight];
104+
uint256 len = pending.executed.length();
105+
for (uint256 i = 0; i < len; i++) {
106+
bytes32 leaf = pending.executed.at(0);
107+
pending.executed.remove(leaf);
108+
}
109+
delete s.pending[checkpointHeight];
110+
}
111+
}
112+
113+
/// Return type for the list pending commitments view method.
114+
struct ListPendingCommitmentsEntry {
115+
uint64 height;
116+
BottomUpBatch.Commitment commitment;
117+
bytes32[] executed;
118+
}
119+
120+
/// A view accessor to query the pending commitments for a given subnet.
121+
function listPendingCommitments() internal view returns (ListPendingCommitmentsEntry[] memory result) {
122+
BottomUpBatchStorage storage s = bottomUpBatchStorage();
123+
124+
bytes32[] memory heights = s.pendingHeights.values();
125+
uint256 size = heights.length;
126+
127+
result = new ListPendingCommitmentsEntry[](size);
128+
129+
for (uint256 i = 0; i < size; i++) {
130+
uint64 height = uint64(uint256(heights[i]));
131+
PendingBatch storage pending = s.pending[height];
132+
result[i] = ListPendingCommitmentsEntry({
133+
height: height,
134+
commitment: pending.commitment,
135+
executed: pending.executed.values()
136+
});
137+
}
138+
}
139+
140+
141+
function makeLeaf(IpcEnvelope memory _msg) public pure returns (BottomUpBatch.MerkleHash) {
142+
// solhint-disable-next-line func-named-parameters
143+
bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(
144+
_msg.kind,
145+
_msg.localNonce,
146+
_msg.originalNonce,
147+
_msg.value,
148+
_msg.to.subnetId.root,
149+
_msg.to.subnetId.route,
150+
_msg.to.rawAddress.addrType,
151+
_msg.to.rawAddress.payload,
152+
_msg.from.subnetId.root,
153+
_msg.from.subnetId.route,
154+
_msg.from.rawAddress.addrType,
155+
_msg.from.rawAddress.payload,
156+
_msg.message
157+
))));
158+
return BottomUpBatch.MerkleHash.wrap(leaf);
159+
}
160+
161+
function bottomUpBatchStorage() internal pure returns (BottomUpBatchStorage storage ds) {
162+
bytes32 position = NAMESPACE;
163+
assembly {
164+
ds.slot := position
165+
}
166+
return ds;
167+
}
168+
}

contracts/contracts/lib/LibGateway.sol

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import {GatewayActorStorage, LibGatewayActorStorage} from "../lib/LibGatewayActo
66
import {BURNT_FUNDS_ACTOR} from "../constants/Constants.sol";
77
import {SubnetID, Subnet, AssetKind, Asset} from "../structs/Subnet.sol";
88
import {SubnetActorGetterFacet} from "../subnet/SubnetActorGetterFacet.sol";
9-
import {CallMsg, IpcMsgKind, IpcEnvelope, OutcomeType, BottomUpMsgBatch, BottomUpMsgBatch, BottomUpCheckpoint, ParentFinality} from "../structs/CrossNet.sol";
9+
import {CallMsg, IpcMsgKind, IpcEnvelope, OutcomeType, BottomUpMsgBatch, BottomUpCheckpoint, ParentFinality} from "../structs/CrossNet.sol";
10+
import {BottomUpBatch} from "../structs/BottomUpBatch.sol";
1011
import {Membership} from "../structs/Subnet.sol";
1112
import {CrossMsgHelper} from "../lib/CrossMsgHelper.sol";
1213
import {FilAddress} from "fevmate/contracts/utils/FilAddress.sol";
@@ -94,17 +95,7 @@ library LibGateway {
9495
b.nextConfigurationNumber = checkpoint.nextConfigurationNumber;
9596
b.blockHeight = checkpoint.blockHeight;
9697
b.activity = checkpoint.activity;
97-
98-
uint256 msgLength = checkpoint.msgs.length;
99-
for (uint256 i; i < msgLength; ) {
100-
// We need to push because initializing an array with a static
101-
// length will cause a copy from memory to storage, making
102-
// the compiler unhappy.
103-
b.msgs.push(checkpoint.msgs[i]);
104-
unchecked {
105-
++i;
106-
}
107-
}
98+
b.msgs = checkpoint.msgs;
10899
}
109100

110101
/// @notice stores bottom-up batch
@@ -557,12 +548,12 @@ library LibGateway {
557548
}
558549
}
559550

560-
/// @notice Checks the length of a message batch, ensuring it is in (0, maxMsgsPerBottomUpBatch).
561-
/// @param msgs The batch of messages to check.
562-
function checkMsgLength(IpcEnvelope[] calldata msgs) internal view {
551+
/// @notice Checks the length of a message batch commitment, ensuring it is in (0, maxMsgsPerBottomUpBatch).
552+
/// @param commitment The batch commitment to check.
553+
function checkMsgLength(BottomUpBatch.Commitment calldata commitment) internal view {
563554
GatewayActorStorage storage s = LibGatewayActorStorage.appStorage();
564555

565-
if (msgs.length > s.maxMsgsPerBottomUpBatch) {
556+
if (commitment.totalNumMsgs > s.maxMsgsPerBottomUpBatch) {
566557
revert MaxMsgsPerBatchExceeded();
567558
}
568559
}

0 commit comments

Comments
 (0)