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: implement checkpoint fraud proofs #4277

Merged
merged 32 commits into from
Aug 12, 2024
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
999a960
Implement fraud proofs
yorhodes Jun 25, 2023
06457c4
Add premature tests
yorhodes Jun 25, 2023
d29432d
Add fraudulent message ID tests
yorhodes Jun 25, 2023
69878aa
Add fraudulent root tests
yorhodes Jun 25, 2023
65eebf0
Adjust names and modifiers
yorhodes Jun 25, 2023
b6c0dd0
Revert unrelated changes
yorhodes Jun 25, 2023
150b13b
Cover non local checkpoints
yorhodes Jun 28, 2023
f6388b8
Merge branch 'main' into fraud-proofs
yorhodes Jun 28, 2023
6a6fb66
Fix prettier
yorhodes Jun 28, 2023
1444384
Add natspecs
yorhodes Jun 28, 2023
ae7cf71
Add more natspecs
yorhodes Jun 28, 2023
9f52367
Merge branch 'main' into fraud-proofs
yorhodes Jun 29, 2023
7deec02
Merge branch 'main' into fraud-proofs
yorhodes Jun 30, 2023
0fd43f0
Do not use modifiers for coverage
yorhodes Jun 30, 2023
5dd9b71
Add isLocal checkpoint slashing condition
yorhodes Jul 2, 2023
712492e
Make fraud proof interface non local
yorhodes Jul 2, 2023
856e220
Make fraud proof interfaces external
yorhodes Jul 2, 2023
ce0b60b
Remove unused signer fn
yorhodes Jul 10, 2023
3d4ceb3
Merge branch 'main' into fraud-proofs
yorhodes Aug 6, 2024
30a5d75
Update fraud proofs for v3
yorhodes Aug 6, 2024
712b435
Fuzz test msg count and body params
yorhodes Aug 6, 2024
1c08229
Test via inserting raw leaf content
yorhodes Aug 6, 2024
57007b1
Pass first test fixture
yorhodes Aug 7, 2024
ba15c2d
Pass all fixtures
yorhodes Aug 7, 2024
4b223b7
Add changeset
yorhodes Aug 7, 2024
e5490ce
Read file once
yorhodes Aug 7, 2024
9a34318
Store result of fraud proofs for checkpoint digests
yorhodes Aug 7, 2024
6fc5009
Revert storage caching which has race conditions
yorhodes Aug 8, 2024
e543465
Helper for checkpoint merkle tree address
yorhodes Aug 8, 2024
1e20e6a
Fix edge case of index = 0
yorhodes Aug 9, 2024
7cc9e68
Add tests for invalid merkle proofs
yorhodes Aug 9, 2024
d24ba38
Add more premature tests
yorhodes Aug 9, 2024
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
2 changes: 1 addition & 1 deletion .changeset/fast-schools-battle.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
"@hyperlane-xyz/cli": patch
'@hyperlane-xyz/cli': patch
---

Require at least 1 chain selection in warp init
5 changes: 5 additions & 0 deletions .changeset/two-tigers-sniff.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hyperlane-xyz/core': minor
---

Implement checkpoint fraud proofs for use in slashing
141 changes: 141 additions & 0 deletions solidity/contracts/CheckpointFraudProofs.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity >=0.8.0;

import {Address} from "@openzeppelin/contracts/utils/Address.sol";

import {TypeCasts} from "./libs/TypeCasts.sol";
import {Checkpoint, CheckpointLib} from "./libs/CheckpointLib.sol";
import {MerkleLib, TREE_DEPTH} from "./libs/Merkle.sol";
import {MerkleTreeHook} from "./hooks/MerkleTreeHook.sol";
import {IMailbox} from "./interfaces/IMailbox.sol";

contract CheckpointFraudProofs {
using CheckpointLib for Checkpoint;
using Address for address;
using TypeCasts for bytes32;

// merkle tree hook => root => index
mapping(address => mapping(bytes32 => uint32)) public storedCheckpoint;
yorhodes marked this conversation as resolved.
Show resolved Hide resolved

function storedCheckpointContainsMessage(
address merkleTreeHook,
uint32 index,
bytes32 messageId,
bytes32[TREE_DEPTH] calldata proof
) public view returns (bool) {
bytes32 root = MerkleLib.branchRoot(messageId, proof, index);
uint32 storedIndex = storedCheckpoint[merkleTreeHook][root];
return storedIndex >= index;
yorhodes marked this conversation as resolved.
Show resolved Hide resolved
}

modifier onlyMessageInStoredCheckpoint(
Checkpoint calldata checkpoint,
bytes32[TREE_DEPTH] calldata proof,
bytes32 messageId
) {
require(
storedCheckpointContainsMessage(
checkpoint.merkleTreeHook.bytes32ToAddress(),
checkpoint.index,
messageId,
proof
),
"message must be member of stored checkpoint"
);
_;
}

function isLocal(
Checkpoint calldata checkpoint
) public view returns (bool) {
address merkleTreeHook = checkpoint.merkleTreeHook.bytes32ToAddress();
return
merkleTreeHook.isContract() &&
MerkleTreeHook(merkleTreeHook).localDomain() == checkpoint.origin;
}

modifier onlyLocal(Checkpoint calldata checkpoint) {
require(isLocal(checkpoint), "must be local checkpoint");
_;
}

/**
* @notice Stores the latest checkpoint of the provided merkle tree hook
* @param merkleTreeHook Address of the merkle tree hook to store the latest checkpoint of.
* @dev Must be called before proving fraud to circumvent race on message insertion and merkle proof construction.
*/
function storeLatestCheckpoint(
yorhodes marked this conversation as resolved.
Show resolved Hide resolved
address merkleTreeHook
) external returns (bytes32 root, uint32 index) {
(root, index) = MerkleTreeHook(merkleTreeHook).latestCheckpoint();
storedCheckpoint[merkleTreeHook][root] = index;
}
yorhodes marked this conversation as resolved.
Show resolved Hide resolved

/**
* @notice Checks whether the provided checkpoint is premature (fraud).
* @param checkpoint Checkpoint to check.
* @dev Checks whether checkpoint.index is greater than or equal to mailbox count
* @return Whether the provided checkpoint is premature.
*/
function isPremature(
Checkpoint calldata checkpoint
) external view onlyLocal(checkpoint) returns (bool) {
// count is the number of messages in the mailbox (i.e. the latest index + 1)
uint32 count = MerkleTreeHook(
checkpoint.merkleTreeHook.bytes32ToAddress()
).count();

// index >= count is equivalent to index > latest index
return checkpoint.index >= count;
}

/**
* @notice Checks whether the provided checkpoint has a fraudulent message ID.
* @param checkpoint Checkpoint to check.
* @param proof Merkle proof of the actual message ID at checkpoint.index on checkpoint.mailbox
* @param actualMessageId Actual message ID at checkpoint.index on checkpoint.mailbox
* @dev Must produce proof of inclusion for actualMessageID against some stored checkpoint.
* @return Whether the provided checkpoint has a fraudulent message ID.
*/
function isFraudulentMessageId(
yorhodes marked this conversation as resolved.
Show resolved Hide resolved
Checkpoint calldata checkpoint,
bytes32[TREE_DEPTH] calldata proof,
bytes32 actualMessageId
)
external
view
onlyLocal(checkpoint)
onlyMessageInStoredCheckpoint(checkpoint, proof, actualMessageId)
returns (bool)
{
return actualMessageId != checkpoint.messageId;
}

/**
* @notice Checks whether the provided checkpoint has a fraudulent root.
* @param checkpoint Checkpoint to check.
* @param proof Merkle proof of the checkpoint.messageId at checkpoint.index on checkpoint.mailbox
* @dev Must produce proof of inclusion for checkpoint.messageId against some stored checkpoint.
* @return Whether the provided checkpoint has a fraudulent message ID.
*/
function isFraudulentRoot(
Checkpoint calldata checkpoint,
bytes32[TREE_DEPTH] calldata proof
)
external
view
onlyLocal(checkpoint)
onlyMessageInStoredCheckpoint(checkpoint, proof, checkpoint.messageId)
returns (bool)
{
// proof of checkpoint.messageId at checkpoint.index is the list of siblings from the leaf node to some stored root
// once verifying the proof, we can reconstruct the specific root at checkpoint.index by replacing siblings greater
// than the index (right subtrees) with zeroes
bytes32 root = MerkleLib.reconstructRoot(
checkpoint.messageId,
proof,
checkpoint.index
);
return root != checkpoint.root;
}
}
41 changes: 32 additions & 9 deletions solidity/contracts/libs/CheckpointLib.sol
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity >=0.8.0;

// ============ External Imports ============
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";

struct Checkpoint {
uint32 origin;
bytes32 merkleTreeHook;
bytes32 root;
uint32 index;
bytes32 messageId;
}

library CheckpointLib {
/**
* @notice Returns the digest validators are expected to sign when signing checkpoints.
* @param _origin The origin domain of the checkpoint.
* @param _originmerkleTreeHook The address of the origin merkle tree hook as bytes32.
* @param _merkleTreeHook The address of the origin merkle tree hook as bytes32.
* @param _checkpointRoot The root of the checkpoint.
* @param _checkpointIndex The index of the checkpoint.
* @param _messageId The message ID of the checkpoint.
Expand All @@ -17,12 +24,12 @@ library CheckpointLib {
*/
function digest(
uint32 _origin,
bytes32 _originmerkleTreeHook,
bytes32 _merkleTreeHook,
bytes32 _checkpointRoot,
uint32 _checkpointIndex,
bytes32 _messageId
) internal pure returns (bytes32) {
bytes32 _domainHash = domainHash(_origin, _originmerkleTreeHook);
bytes32 _domainHash = domainHash(_origin, _merkleTreeHook);
return
ECDSA.toEthSignedMessageHash(
keccak256(
Expand All @@ -36,25 +43,41 @@ library CheckpointLib {
);
}

/**
* @notice Returns the digest validators are expected to sign when signing checkpoints.
* @param checkpoint The checkpoint (struct) to hash.
* @return The digest of the checkpoint.
*/
function digest(
Checkpoint calldata checkpoint
) internal pure returns (bytes32) {
return
digest(
checkpoint.origin,
checkpoint.merkleTreeHook,
checkpoint.root,
checkpoint.index,
checkpoint.messageId
);
}

/**
* @notice Returns the domain hash that validators are expected to use
* when signing checkpoints.
* @param _origin The origin domain of the checkpoint.
* @param _originmerkleTreeHook The address of the origin merkle tree as bytes32.
* @param _merkleTreeHook The address of the origin merkle tree as bytes32.
* @return The domain hash.
*/
function domainHash(
uint32 _origin,
bytes32 _originmerkleTreeHook
bytes32 _merkleTreeHook
) internal pure returns (bytes32) {
// Including the origin merkle tree address in the signature allows the slashing
// protocol to enroll multiple trees. Otherwise, a valid signature for
// tree A would be indistinguishable from a fraudulent signature for tree B.
// The slashing protocol should slash if validators sign attestations for
// anything other than a whitelisted tree.
return
keccak256(
abi.encodePacked(_origin, _originmerkleTreeHook, "HYPERLANE")
);
keccak256(abi.encodePacked(_origin, _merkleTreeHook, "HYPERLANE"));
}
}
36 changes: 33 additions & 3 deletions solidity/contracts/libs/Merkle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ pragma solidity >=0.6.11;

// work based on eth2 deposit contract, which is used under CC0-1.0

uint256 constant TREE_DEPTH = 32;
uint256 constant MAX_LEAVES = 2 ** TREE_DEPTH - 1;

/**
* @title MerkleLib
* @author Celo Labs Inc.
* @notice An incremental merkle tree modeled on the eth2 deposit contract.
**/
library MerkleLib {
uint256 internal constant TREE_DEPTH = 32;
uint256 internal constant MAX_LEAVES = 2 ** TREE_DEPTH - 1;

/**
* @notice Struct representing incremental merkle tree. Contains current
* branch and the number of inserted leaves in the tree.
Expand Down Expand Up @@ -140,6 +140,36 @@ library MerkleLib {
}
}

/**
* @notice Calculates and returns the merkle root as if the index is
* the topmost leaf in the tree.
* @param _item Merkle leaf
* @param _branch Merkle proof
* @param _index Index of `_item` in tree
* @dev Replaces siblings greater than the index (right subtrees) with zeroes.
yorhodes marked this conversation as resolved.
Show resolved Hide resolved
* @return _current Calculated merkle root
**/
function reconstructRoot(
bytes32 _item,
bytes32[TREE_DEPTH] memory _branch, // cheaper than calldata indexing
uint256 _index
yorhodes marked this conversation as resolved.
Show resolved Hide resolved
) internal pure returns (bytes32 _current) {
_current = _item;

bytes32[TREE_DEPTH] memory _zeroes = zeroHashes();

for (uint256 i = 0; i < TREE_DEPTH; i++) {
uint256 _ithBit = (_index >> i) & 0x01;
// cheaper than calldata indexing _branch[i*32:(i+1)*32];
if (_ithBit == 1) {
_current = keccak256(abi.encodePacked(_branch[i], _current));
} else {
// remove right subtree from proof
_current = keccak256(abi.encodePacked(_current, _zeroes[i]));
}
}
}

// keccak256 zero hashes
bytes32 internal constant Z_0 =
hex"0000000000000000000000000000000000000000000000000000000000000000";
Expand Down
1 change: 1 addition & 0 deletions solidity/foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ optimizer = true
optimizer_runs = 999_999
fs_permissions = [
{ access = "read", path = "./script/avs/"},
{ access = "read", path = "../vectors" },
{ access = "write", path = "./fixtures" }
]
ignored_warnings_from = [
Expand Down
Loading
Loading