Skip to content

Commit

Permalink
feat: implement checkpoint fraud proofs (#4277)
Browse files Browse the repository at this point in the history
### Description

Implements 4 categories of checkpoint fraud proofs for use in future
validator slashing protocol:

1. **premature**: if a checkpoint index is greater than the
corresponding mailbox count, it is fraudulent
2. **non local**: if a checkpoint origin does not match the checkpoint's
mailbox domain, it is fraudulent
3. **message id**: if a checkpoint message ID differs from the actual
message ID (verified by merkle proof) at the checkpoint index, it is
fraudulent
4. **root**: if a checkpoint root differs from the actual root (verified
by merkle proof + root reconstruction) at the checkpoint index, it is
fraudulent

Notably this is implemented independently from signature verification to
allow for multiple checkpoint signing schemes to reuse the same
checkpoint logic.

### Related issues

- Touches
#3799
- See #2431 for
previous discussions

### Backward compatibility

Yes

### Testing

Unit testing with fixtures in `vectors/merkle.json` that are generated
by the rust merkle tree and proof code
  • Loading branch information
yorhodes authored Aug 12, 2024
1 parent d1bf212 commit cb404cb
Show file tree
Hide file tree
Showing 7 changed files with 519 additions and 13 deletions.
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
143 changes: 143 additions & 0 deletions solidity/contracts/CheckpointFraudProofs.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// 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";

struct StoredIndex {
uint32 index;
bool exists;
}

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

mapping(address merkleTree => mapping(bytes32 root => StoredIndex index))
public storedCheckpoints;

function storedCheckpointContainsMessage(
address merkleTree,
uint32 index,
bytes32 messageId,
bytes32[TREE_DEPTH] calldata proof
) public view returns (bool) {
bytes32 root = MerkleLib.branchRoot(messageId, proof, index);
StoredIndex storage storedIndex = storedCheckpoints[merkleTree][root];
return storedIndex.exists && storedIndex.index >= index;
}

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

function isLocal(
Checkpoint calldata checkpoint
) public view returns (bool) {
address merkleTree = checkpoint.merkleTreeAddress();
return
merkleTree.isContract() &&
MerkleTreeHook(merkleTree).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 merkleTree 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(
address merkleTree
) external returns (bytes32 root, uint32 index) {
(root, index) = MerkleTreeHook(merkleTree).latestCheckpoint();
storedCheckpoints[merkleTree][root] = StoredIndex(index, true);
}

/**
* @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
) public view onlyLocal(checkpoint) returns (bool) {
// count is the number of messages in the mailbox (i.e. the latest index + 1)
uint32 count = MerkleTreeHook(checkpoint.merkleTreeAddress()).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.merkleTree
* @param actualMessageId Actual message ID at checkpoint.index on checkpoint.merkleTree
* @dev Must produce proof of inclusion for actualMessageID against some stored checkpoint.
* @return Whether the provided checkpoint has a fraudulent message ID.
*/
function isFraudulentMessageId(
Checkpoint calldata checkpoint,
bytes32[TREE_DEPTH] calldata proof,
bytes32 actualMessageId
)
public
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.merkleTree
* @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
)
public
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;
}
}
50 changes: 41 additions & 9 deletions solidity/contracts/libs/CheckpointLib.sol
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity >=0.8.0;

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

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

library CheckpointLib {
using TypeCasts for bytes32;

/**
* @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 +27,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 +46,47 @@ 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.merkleTree,
checkpoint.root,
checkpoint.index,
checkpoint.messageId
);
}

function merkleTreeAddress(
Checkpoint calldata checkpoint
) internal pure returns (address) {
return checkpoint.merkleTree.bytes32ToAddress();
}

/**
* @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.
* @return _current Calculated merkle root
**/
function reconstructRoot(
bytes32 _item,
bytes32[TREE_DEPTH] memory _branch, // cheaper than calldata indexing
uint256 _index
) 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

0 comments on commit cb404cb

Please sign in to comment.