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(contracts): verify xsubmission merke proofs #199

Merged
merged 6 commits into from
Jan 30, 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
4 changes: 2 additions & 2 deletions contracts/bindings/omniportal.go

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions contracts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"test:gen:xsubs": "ts-node test/ts/script/genxsubs/main.ts"
},
"devDependencies": {
"@openzeppelin/contracts": "^5.0.1",
"@openzeppelin/merkle-tree": "^1.0.5",
"@types/node": "^20.11.7",
"ds-test": "https://github.com/dapphub/ds-test",
Expand Down
7 changes: 7 additions & 0 deletions contracts/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion contracts/src/OmniPortal.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
pragma solidity 0.8.23;

import { IOmniPortal } from "./interfaces/IOmniPortal.sol";
import { XBlockMerkleProof } from "./libraries/XBlockMerkleProof.sol";
import { XTypes } from "./libraries/XTypes.sol";

contract OmniPortal is IOmniPortal {
Expand Down Expand Up @@ -41,7 +42,10 @@ contract OmniPortal is IOmniPortal {
function xsubmit(XTypes.Submission calldata xsub) external {
// TODO: verify a quorum of validators have signed off on the attestation root.

// TODO: verify block header and msgs are included in the attestation merkle root
require(
XBlockMerkleProof.verify(xsub.attestationRoot, xsub.blockHeader, xsub.msgs, xsub.proof, xsub.proofFlags),
"OmniPortal: invalid proof"
);

for (uint256 i = 0; i < xsub.msgs.length; i++) {
_exec(xsub.msgs[i]);
Expand Down
53 changes: 53 additions & 0 deletions contracts/src/libraries/XBlockMerkleProof.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity 0.8.23;

import { MerkleProof } from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
import { XTypes } from "./XTypes.sol";

/**
* @title XBlockMerkleProof
* @dev Library for verifying XBlock merkle proofs
*/
library XBlockMerkleProof {
/**
* @dev Verifies a multi merkle proof for the provided block header and messages, against the provided root.
* Msgs order must match the order used to construct the merkle proof.
* @param root The root of the xblock merkle tree, generally XSubmission.attestationRoot.
* @param blockHeader The xblock header.
* @param msgs The xmsgs to verify.
* @param proof The merkle proof.
* @param proofFlags The merkle proof flags.
* @return True if the proof is valid.
*/
function verify(
bytes32 root,
XTypes.BlockHeader calldata blockHeader,
XTypes.Msg[] calldata msgs,
bytes32[] calldata proof,
bool[] calldata proofFlags
) internal pure returns (bool) {
return MerkleProof.multiProofVerify(proof, proofFlags, root, _leaves(blockHeader, msgs));
}

/// @dev Convert block header and msgs to leaf hashes
function _leaves(XTypes.BlockHeader calldata blockHeader, XTypes.Msg[] calldata msgs)
private
pure
returns (bytes32[] memory)
{
bytes32[] memory leaves = new bytes32[](msgs.length + 1);

leaves[0] = _leafHash(abi.encode(blockHeader));
for (uint256 i = 0; i < msgs.length; i++) {
leaves[i + 1] = _leafHash(abi.encode(msgs[i]));
}

return leaves;
}

/// @dev Double hash leaves, as recommended by OpenZeppelin, to prevent second preimage attacks
/// Leaves must be double hashed in tree / proof construction
function _leafHash(bytes memory leaf) private pure returns (bytes32) {
return keccak256(bytes.concat(keccak256(leaf)));
}
}
22 changes: 2 additions & 20 deletions contracts/test/OmniPortal_exec.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,7 @@ contract OmniPortal_exec_Test is Base {

assertEq(counter.count(), count + 1);
assertEq(portal.inXStreamOffset(xmsg.sourceChainId), offset + 1);

Vm.Log[] memory logs = vm.getRecordedLogs();

_assertReceiptEmitted(
logs[0],
xmsg.sourceChainId,
offset,
relayer,
true // success
);
assertReceipt(vm.getRecordedLogs()[0], xmsg);
}

/// @dev Test that exec of an XMsg that reverts succeeds, and emits the correct XReceipt
Expand All @@ -50,16 +41,7 @@ contract OmniPortal_exec_Test is Base {

assertEq(counter.count(), count);
assertEq(portal.inXStreamOffset(xmsg.sourceChainId), offset + 1);

Vm.Log[] memory logs = vm.getRecordedLogs();

_assertReceiptEmitted(
logs[0],
xmsg.sourceChainId,
offset,
relayer,
false // failure
);
assertReceipt(vm.getRecordedLogs()[0], xmsg);
}

/// @dev Test that exec of an XMsg with the wrong destChainId reverts
Expand Down
197 changes: 50 additions & 147 deletions contracts/test/OmniPortal_xsubmit.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,182 +10,85 @@ import { Vm } from "forge-std/Vm.sol";
* @dev Tests of OmniPortal.xsubmit
*/
contract OmniPortal_xsubmit_Test is Base {
/// @dev Test that an XSubmission with a single XMsg succeeds
/// Check that the correct XReceipt's are emitter, and stream offset is incremented.
function test_xsubmit_xmsgSingle_succeeds() public {
XTypes.Msg[] memory xmsgs = new XTypes.Msg[](1);
function test_xsubmit_xblock1_succeeds() public {
XTypes.Submission memory xsub = readXSubmission("xblock1", portal.chainId());

xmsgs[0] = _inbound_increment(0);

XTypes.Submission memory submission = _xsub(xmsgs);

uint256 count = counter.count();
uint64 sourceChainId = xmsgs[0].sourceChainId;
uint64 offset = portal.inXStreamOffset(sourceChainId);
uint64 sourceChainId = xsub.msgs[0].sourceChainId;
uint64 expectedOffset = xsub.msgs[0].streamOffset + uint64(xsub.msgs.length);

vm.prank(relayer);
vm.recordLogs();
portal.xsubmit(submission);

assertEq(counter.count(), count + 1);
assertEq(portal.inXStreamOffset(sourceChainId), offset + 1);

Vm.Log[] memory logs = vm.getRecordedLogs();
expectCalls(xsub.msgs);
portal.xsubmit(xsub);

assertEq(logs.length, xmsgs.length);

_assertReceiptEmitted(
logs[0],
sourceChainId,
offset,
relayer,
true // success
);
assertEq(portal.inXStreamOffset(sourceChainId), expectedOffset);
assertReceipts(vm.getRecordedLogs(), xsub.msgs);
}

/// @dev Test that an XSubmission with a batch of XMsgs succeeds.
/// Check that the correct XReceipt's are emitter, and stream offset is incremented.
function test_xsubmit_xmsgBatch_succeeds() public {
XTypes.Msg[] memory xmsgs = new XTypes.Msg[](4);

xmsgs[0] = _inbound_increment(0);
xmsgs[1] = _inbound_increment(1);
xmsgs[2] = _inbound_increment(2);
xmsgs[3] = _inbound_increment(3);
function test_xsubmit_xblock2_succeeds() public {
// need to submit xblock1 first, to set the streamOffset
XTypes.Submission memory xsub1 = readXSubmission("xblock1", portal.chainId());
portal.xsubmit(xsub1);

XTypes.Submission memory submission = _xsub(xmsgs);
XTypes.Submission memory xsub2 = readXSubmission("xblock2", portal.chainId());

uint256 count = counter.count();
uint64 sourceChainId = xmsgs[0].sourceChainId;
uint64 offset = portal.inXStreamOffset(sourceChainId);
uint64 sourceChainId = xsub2.msgs[0].sourceChainId;
uint64 expectedOffset = xsub2.msgs[0].streamOffset + uint64(xsub2.msgs.length);

vm.prank(relayer);
vm.recordLogs();
portal.xsubmit(submission);

assertEq(counter.count(), count + 4);
assertEq(portal.inXStreamOffset(sourceChainId), offset + 4);
expectCalls(xsub2.msgs);
portal.xsubmit(xsub2);

Vm.Log[] memory logs = vm.getRecordedLogs();

assertEq(logs.length, xmsgs.length);

for (uint256 i = 0; i < xmsgs.length; i++) {
_assertReceiptEmitted(
logs[i],
sourceChainId,
offset + uint64(i),
relayer,
true // success
);
}
assertEq(portal.inXStreamOffset(sourceChainId), expectedOffset);
assertReceipts(vm.getRecordedLogs(), xsub2.msgs);
}

/// @dev Test that an XSubmission with a batch of XMsgs, in which one reverts, succeeds.
/// Check that the correct XReceipt's are emitter, and stream offset is incremented.
function test_xsubmit_xmsgBatchWithRevert_succeeds() public {
XTypes.Msg[] memory xmsgs = new XTypes.Msg[](4);

xmsgs[0] = _inbound_increment(0);
xmsgs[1] = _inbound_increment(1);
xmsgs[2] = _inbound_revert(2);
xmsgs[3] = _inbound_increment(3);
function test_xsubmit_xblock1_chainB_succeeds() public {
XTypes.Submission memory xsub = readXSubmission("xblock1", chainBId);

XTypes.Submission memory submission = _xsub(xmsgs);

uint256 count = counter.count();
uint64 sourceChainId = xmsgs[0].sourceChainId;
uint64 offset = portal.inXStreamOffset(sourceChainId);
uint64 sourceChainId = xsub.msgs[0].sourceChainId;
uint64 expectedOffset = xsub.msgs[0].streamOffset + uint64(xsub.msgs.length);

vm.prank(relayer);
vm.recordLogs();
portal.xsubmit(submission);

assertEq(counter.count(), count + 3); // only 3, because one msg was a revert
assertEq(portal.inXStreamOffset(sourceChainId), offset + 4);

Vm.Log[] memory logs = vm.getRecordedLogs();

assertEq(logs.length, xmsgs.length);

_assertReceiptEmitted(
logs[0],
sourceChainId,
offset,
relayer,
true // success
);

_assertReceiptEmitted(
logs[1],
sourceChainId,
offset + 1,
relayer,
true // success
);

// this one fails
_assertReceiptEmitted(
logs[2],
sourceChainId,
offset + 2,
relayer,
false // failure
);

_assertReceiptEmitted(
logs[3],
sourceChainId,
offset + 3,
relayer,
true // success
);
}

/// @dev Test that an XSubmission with a batch of XMsgs with an XMsg behind the current offset reverts
function test_xsubmit_xmsgBatchOneBehindOffset_reverts() public {
XTypes.Msg[] memory xmsgs = new XTypes.Msg[](4);
expectCalls(xsub.msgs);
chainBPortal.xsubmit(xsub);

xmsgs[0] = _inbound_increment(0);
xmsgs[1] = _inbound_increment(1);
xmsgs[2] = _inbound_increment(2);
xmsgs[3] = _inbound_increment(2); // intentionally behind offset

XTypes.Submission memory submission = _xsub(xmsgs);

vm.expectRevert("OmniPortal: wrong streamOffset");
portal.xsubmit(submission);
assertEq(chainBPortal.inXStreamOffset(sourceChainId), expectedOffset);
assertReceipts(vm.getRecordedLogs(), xsub.msgs);
}

/// @dev Test that an XSubmission with a batch of XMsgs with an XMsg ahead the current offset reverts
function test_xsubmit_xmsgBatchOneAheadOffset_reverts() public {
XTypes.Msg[] memory xmsgs = new XTypes.Msg[](4);
function test_xsubmit_xblock2_chainB_succeeds() public {
// need to submit xblock1 first, to set the streamOffset
XTypes.Submission memory xsub1 = readXSubmission("xblock1", chainBId);
chainBPortal.xsubmit(xsub1);

xmsgs[0] = _inbound_increment(0);
xmsgs[1] = _inbound_increment(1);
xmsgs[2] = _inbound_increment(2);
xmsgs[3] = _inbound_increment(4); // intentionally ahead offset
XTypes.Submission memory xsub2 = readXSubmission("xblock2", chainBId);

XTypes.Submission memory submission = _xsub(xmsgs);
uint64 sourceChainId = xsub2.msgs[0].sourceChainId;
uint64 expectedOffset = xsub2.msgs[0].streamOffset + uint64(xsub2.msgs.length);

vm.expectRevert("OmniPortal: wrong streamOffset");
portal.xsubmit(submission);
}
vm.prank(relayer);
vm.recordLogs();
expectCalls(xsub2.msgs);
chainBPortal.xsubmit(xsub2);

/// @dev Test that an XSubmission with a batch of XMsgs in which one has the wrong destChainId reverts
function test_xsubmit_xmsgBatchWrongChainId_reverts() public {
XTypes.Msg[] memory xmsgs = new XTypes.Msg[](4);
assertEq(chainBPortal.inXStreamOffset(sourceChainId), expectedOffset);
assertReceipts(vm.getRecordedLogs(), xsub2.msgs);
}

xmsgs[0] = _inbound_increment(0);
xmsgs[1] = _inbound_increment(1);
xmsgs[2] = _inbound_increment(2);
xmsgs[3] = _inbound_increment(3);
function test_xsubmit_wrongChainId_reverts() public {
XTypes.Submission memory xsub = readXSubmission("xblock1", portal.chainId());

xmsgs[1].destChainId = xmsgs[0].destChainId + 1; // intentionally wrong chainId
vm.expectRevert("OmniPortal: wrong destChainId");
chainBPortal.xsubmit(xsub);
}

XTypes.Submission memory submission = _xsub(xmsgs);
function test_xsubmit_wrongStreamOffset_reverts() public {
XTypes.Submission memory xsub = readXSubmission("xblock2", portal.chainId());

vm.expectRevert("OmniPortal: wrong destChainId");
portal.xsubmit(submission);
vm.expectRevert("OmniPortal: wrong streamOffset");
portal.xsubmit(xsub);
}
}
Loading
Loading