Skip to content

Commit b8c57e7

Browse files
Merge branch 'noble-assets:main' into malte/noble-bls-ism
2 parents 382d8e8 + 69984fc commit b8c57e7

15 files changed

Lines changed: 382 additions & 279 deletions

File tree

.env.example

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
ETH_BEACON_RPC=
22
ETH_EXECUTION_RPC=
3-
NETWORK_PRIVATE_KEY=
4-
LIGHT_CLIENT_CONTRACT=
5-
API_KEY=
3+
RPC_URL= # RPC_URL of the network where the light client will be deployed, e.g. Noble end point. Will also be used by Foundry scripts to deploy the contracts.
4+
PRIVATE_KEY= # Used for foundry scripts, to deploy contracts. This is the private key of the deployer
5+
VERIFIER_URL= # Used for foundry scripts, to verify contracts. This is the URL of the block explorer API, e.g. the Blockscout API endpoint
6+
NETWORK_PRIVATE_KEY= # Private key for SP1 network prover authentication
7+
LIGHT_CLIENT_CONTRACT= # Contract address of light client deployed on the counterparty chain
8+
API_KEY= # API Key for the GRPC prover service, used for authentication
69
CHAIN_ID = # Optional, defaults to 1 (Ethereum Mainnet)
710
PORT = # Optional, defaults to 50051

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ resolver = "3"
44

55
[workspace.package]
66
authors = ["NASD Inc."]
7-
license = "BUSL-1.1"
7+
license = "Apache-2.0"
88
version = "0.1.0"
99
edition = "2024"
1010
repository = "https://github.com/noble-assets/ism-evm"
@@ -46,7 +46,7 @@ prost = "0.14.1"
4646
tracing = "0.1.44"
4747
tracing-subscriber = "0.3.22"
4848

49-
[patch.crates-io] # General
49+
[patch.crates-io]
5050
sha2-v0-9-9 = { git = "https://github.com/sp1-patches/RustCrypto-hashes", package = "sha2", tag = "patch-sha2-0.9.9-sp1-4.0.0" }
5151
sha3-v0-10-8 = { git = "https://github.com/sp1-patches/RustCrypto-hashes", package = "sha3", tag = "patch-sha3-0.10.8-sp1-4.0.0" }
5252
tiny-keccak = { git = "https://github.com/sp1-patches/tiny-keccak", tag = "patch-2.0.2-sp1-4.0.0" }

contracts/script/Deploy.s.sol

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
pragma solidity 0.8.30;
3+
4+
import {Script, console} from "forge-std/src/Script.sol";
5+
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
6+
import {EthereumLightClient} from "../src/light-client/EthereumLightClient.sol";
7+
import {EvmISM} from "../src/ism/EvmISM.sol";
8+
9+
/**
10+
* @title Deploy
11+
* @notice Deploys the EthereumLightClient and EvmISM behind UUPS proxies.
12+
*
13+
* @dev Required environment variables:
14+
* ETHEREUM_LIGHT_CLIENT_VK - SP1 verification key for the Helios circuit
15+
* HYPERLANE_MERKLE_VK - SP1 verification key for the Merkle tree circuit
16+
* INITIAL_SLOT - Beacon chain slot of the trusted checkpoint
17+
* INITIAL_HEADER - Beacon block header hash at the initial slot
18+
* INITIAL_STATE_ROOT - Execution layer state root at the initial block
19+
* INITIAL_BLOCK_NUMBER - Execution layer block number for the initial slot
20+
* INITIAL_SYNC_COMMITTEE - Sync committee hash for the period containing the initial slot
21+
* OWNER - Address that will own both contracts
22+
*/
23+
contract Deploy is Script {
24+
function run() public {
25+
// --- Read environment variables ---
26+
bytes32 lightClientVk = vm.envBytes32("ETHEREUM_LIGHT_CLIENT_VK");
27+
bytes32 merkleVk = vm.envBytes32("HYPERLANE_MERKLE_VK");
28+
uint256 initialSlot = vm.envUint("INITIAL_SLOT");
29+
bytes32 initialHeader = vm.envBytes32("INITIAL_HEADER");
30+
bytes32 initialStateRoot = vm.envBytes32("INITIAL_STATE_ROOT");
31+
uint256 initialBlockNumber = vm.envUint("INITIAL_BLOCK_NUMBER");
32+
bytes32 initialSyncCommittee = vm.envBytes32("INITIAL_SYNC_COMMITTEE");
33+
address owner = vm.envAddress("OWNER");
34+
35+
uint256 sourceChainId = 1; // Ethereum mainnet
36+
address sp1Verifier = 0x397A5f7f3dBd538f23DE225B51f532c34448dA9B;
37+
38+
// --- Sanity checks ---
39+
require(lightClientVk != bytes32(0), "ETHEREUM_LIGHT_CLIENT_VK not set");
40+
require(merkleVk != bytes32(0), "HYPERLANE_MERKLE_VK not set");
41+
require(initialSlot != 0, "INITIAL_SLOT not set");
42+
require(initialHeader != bytes32(0), "INITIAL_HEADER not set");
43+
require(initialStateRoot != bytes32(0), "INITIAL_STATE_ROOT not set");
44+
require(initialBlockNumber != 0, "INITIAL_BLOCK_NUMBER not set");
45+
require(initialSyncCommittee != bytes32(0), "INITIAL_SYNC_COMMITTEE not set");
46+
require(owner != address(0), "OWNER not set");
47+
48+
console.log("=== Deploying EthereumLightClient ===");
49+
console.log(" Initial slot:", initialSlot);
50+
console.log(" Initial block number:", initialBlockNumber);
51+
console.log(" Owner:", owner);
52+
53+
vm.startBroadcast();
54+
55+
// ============ Light Client ============
56+
57+
EthereumLightClient lcImpl = new EthereumLightClient(sourceChainId);
58+
console.log(" Implementation:", address(lcImpl));
59+
60+
ERC1967Proxy lcProxy = new ERC1967Proxy(
61+
address(lcImpl),
62+
abi.encodeCall(
63+
EthereumLightClient.initialize,
64+
(
65+
lightClientVk,
66+
initialSlot,
67+
initialHeader,
68+
initialStateRoot,
69+
initialBlockNumber,
70+
initialSyncCommittee,
71+
sp1Verifier,
72+
owner
73+
)
74+
)
75+
);
76+
console.log(" Proxy:", address(lcProxy));
77+
78+
// ============ ISM ============
79+
80+
console.log("=== Deploying EvmISM ===");
81+
82+
EvmISM ismImpl = new EvmISM();
83+
console.log(" Implementation:", address(ismImpl));
84+
85+
ERC1967Proxy ismProxy = new ERC1967Proxy(
86+
address(ismImpl), abi.encodeCall(EvmISM.initialize, (merkleVk, sp1Verifier, address(lcProxy)))
87+
);
88+
console.log(" Proxy:", address(ismProxy));
89+
90+
vm.stopBroadcast();
91+
92+
// ============ Post-deploy verification ============
93+
94+
EthereumLightClient lc = EthereumLightClient(address(lcProxy));
95+
require(lc.latestSlot() == initialSlot, "latestSlot mismatch");
96+
require(lc.latestBlockNumber() == initialBlockNumber, "latestBlockNumber mismatch");
97+
require(lc.programVk() == lightClientVk, "lc programVk mismatch");
98+
require(lc.headers(initialSlot) == initialHeader, "header mismatch");
99+
require(lc.stateRoots(initialBlockNumber) == initialStateRoot, "stateRoot mismatch");
100+
101+
EvmISM ism = EvmISM(address(ismProxy));
102+
require(ism.programVk() == merkleVk, "ism programVk mismatch");
103+
require(address(ism.lightClient()) == address(lcProxy), "light client address mismatch");
104+
require(address(ism.verifier()) == sp1Verifier, "ism verifier mismatch");
105+
106+
console.log("=== Deployment verified successfully ===");
107+
console.log(" LightClient proxy:", address(lcProxy));
108+
console.log(" ISM proxy:", address(ismProxy));
109+
}
110+
}

contracts/src/interfaces/IEthereumLightClient.sol

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ pragma solidity >=0.8.20;
33

44
import {IVersioned} from "./IVersioned.sol";
55
import {IVkUpdatable} from "./IVkUpdatable.sol";
6+
import {IEvmLightClient} from "./IEvmLightClient.sol";
67

78
/**
89
* @title IEthereumLightClient
@@ -11,7 +12,7 @@ import {IVkUpdatable} from "./IVkUpdatable.sol";
1112
* proofs. It maintains a chain of finalized headers verified through sync committee signatures and enables
1213
* trustless cross-chain verification of Ethereum state.
1314
*/
14-
interface IEthereumLightClient is IVersioned, IVkUpdatable {
15+
interface IEthereumLightClient is IEvmLightClient, IVersioned, IVkUpdatable {
1516
/// @notice Thrown when an invalid address (e.g., zero address) is provided.
1617
error InvalidAddress();
1718

@@ -101,13 +102,6 @@ interface IEthereumLightClient is IVersioned, IVkUpdatable {
101102
*/
102103
function headers(uint256 slot) external view returns (bytes32);
103104

104-
/**
105-
* @notice Returns the execution state root for a given block number.
106-
* @param blockNumber The execution layer block number.
107-
* @return The state root hash, or zero if not set.
108-
*/
109-
function stateRoots(uint256 blockNumber) external view returns (bytes32);
110-
111105
/**
112106
* @notice Returns the sync committee hash for a given period.
113107
* @param period The sync committee period index.
@@ -121,18 +115,6 @@ interface IEthereumLightClient is IVersioned, IVkUpdatable {
121115
*/
122116
function latestSlot() external view returns (uint256);
123117

124-
/**
125-
* @notice Returns the execution state root for the latest finalized block.
126-
* @return The state root hash that can be used for verifying storage proofs.
127-
*/
128-
function latestStateRoot() external view returns (bytes32);
129-
130-
/**
131-
* @notice Returns the latest finalized execution block number.
132-
* @return The most recent execution block number with a verified state root.
133-
*/
134-
function latestBlockNumber() external view returns (uint256);
135-
136118
/**
137119
* @notice Calculates the sync committee period for a given slot.
138120
* @dev Each period spans 8192 slots (~27 hours). The sync committee rotates at period boundaries.
Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@ import {IVersioned} from "./IVersioned.sol";
77
import {IVkUpdatable} from "./IVkUpdatable.sol";
88

99
/**
10-
* @title IEthereumISM
10+
* @title IEvmISM
1111
* @notice Interface for a Hyperlane Interchain Security Module that verifies messages using SP1 ZK proofs
12-
* of Ethereum's Merkle tree hook roots.
12+
* of Merkle tree hook roots from EVM chains.
1313
* @dev This ISM verifies messages in two steps:
14-
* 1. Accept SP1 proofs of Ethereum's Merkle tree hook roots via update()
14+
* 1. Accept SP1 proofs of the source chain's Merkle tree hook roots via update()
1515
* 2. Validate Hyperlane messages against these roots via verify()
1616
*/
17-
interface IEthereumISM is IInterchainSecurityModule, IVersioned, IVkUpdatable {
17+
interface IEvmISM is IInterchainSecurityModule, IVersioned, IVkUpdatable {
1818
/// @notice Thrown when an invalid address (e.g., zero address) is provided.
1919
error InvalidAddress();
2020

@@ -24,17 +24,17 @@ interface IEthereumISM is IInterchainSecurityModule, IVersioned, IVkUpdatable {
2424
/**
2525
* @notice Emitted when a new Merkle tree root is verified and added to the valid roots set.
2626
* @param root The Merkle tree hook root that was verified.
27-
* @param blockNumber The Ethereum block number at which the root was proven.
28-
* @param stateRoot The Ethereum execution layer state root at the given block.
27+
* @param blockNumber The source chain block number at which the root was proven.
28+
* @param stateRoot The source chain execution state root at the given block.
2929
*/
3030
event Updated(bytes32 root, uint64 blockNumber, bytes32 stateRoot);
3131

3232
/**
3333
* @notice Output structure from the Merkle tree verification circuit.
34-
* @dev This struct contains the data needed to validate a Merkle tree root against Ethereum state.
34+
* @dev This struct contains the data needed to validate a Merkle tree root against the source chain's state.
3535
* @param root The Merkle tree hook root from the Hyperlane contract.
36-
* @param stateRoot The Ethereum execution layer state root at the given block.
37-
* @param blockNumber The Ethereum block number where the Merkle root was proven.
36+
* @param stateRoot The source chain execution state root at the given block.
37+
* @param blockNumber The source chain block number where the Merkle root was proven.
3838
*/
3939
struct CircuitOutput {
4040
bytes32 root;
@@ -56,7 +56,7 @@ interface IEthereumISM is IInterchainSecurityModule, IVersioned, IVkUpdatable {
5656

5757
/**
5858
* @notice Updates the set of valid Merkle roots by verifying an SP1 proof.
59-
* @dev Verifies a ZK proof that attests to a valid Merkle tree hook root from Ethereum,
59+
* @dev Verifies a ZK proof that attests to a valid Merkle tree hook root from the source EVM chain,
6060
* validates the state root against the light client, and adds the root to the valid set.
6161
* @param proof The SP1 proof bytes generated by the Merkle tree circuit.
6262
* @param publicValues The ABI-encoded CircuitOutput containing the proof data.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
pragma solidity >=0.8.20;
3+
4+
/**
5+
* @title IEvmLightClient
6+
* @notice Minimal interface for light clients that provide verified EVM execution state roots.
7+
* @dev Any chain-specific light client (Ethereum, Noble, etc.) should implement this.
8+
*/
9+
interface IEvmLightClient {
10+
/**
11+
* @notice Returns the execution state root for a given block number.
12+
* @param blockNumber The execution layer block number.
13+
* @return The state root hash, or zero if not set.
14+
*/
15+
function stateRoots(uint256 blockNumber) external view returns (bytes32);
16+
17+
/**
18+
* @notice Returns the execution state root for the latest finalized block.
19+
* @return The state root hash that can be used for verifying storage proofs.
20+
*/
21+
function latestStateRoot() external view returns (bytes32);
22+
23+
/**
24+
* @notice Returns the latest finalized execution block number.
25+
* @return The most recent execution block number with a verified state root.
26+
*/
27+
function latestBlockNumber() external view returns (uint256);
28+
}
Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,30 @@ import {OwnableUpgradeable} from "@openzeppelin-contracts-upgradeable/access/Own
55
import {UUPSUpgradeable} from "@openzeppelin-contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
66
import {PausableUpgradeable} from "@openzeppelin-contracts-upgradeable/utils/PausableUpgradeable.sol";
77
import {ISP1Verifier} from "succinctlabs-sp1-contracts/src/ISP1Verifier.sol";
8-
import {IEthereumISM} from "../interfaces/IEthereumISM.sol";
9-
import {IEthereumLightClient} from "../interfaces/IEthereumLightClient.sol";
8+
import {IEvmISM} from "../interfaces/IEvmISM.sol";
9+
import {IEvmLightClient} from "../interfaces/IEvmLightClient.sol";
1010
import {IInterchainSecurityModule} from "hyperlane/interfaces/IInterchainSecurityModule.sol";
1111
import {Message} from "hyperlane/libs/Message.sol";
1212
import {MerkleLib} from "hyperlane/libs/Merkle.sol";
1313

1414
/**
15-
* @title EthereumISM
15+
* @title EvmISM
1616
* @author NASD Inc.
1717
* @notice Hyperlane Interchain Security Module that uses SP1 zk proofs to verify Hyperlane
18-
* Merkle tree hook roots from the Ethereum blockchain
19-
* @dev This EthereumISM verifies messages by:
20-
* 1. Accepting SP1 proofs of Ethereum's Merkle tree hook roots via update()
18+
* Merkle tree hook roots from an EVM source chain.
19+
* @dev This ISM verifies messages by:
20+
* 1. Accepting SP1 proofs of the source chain's Merkle tree hook roots via update()
2121
* 2. Validating Hyperlane messages against these roots via verify(). This will be called by the Hyperlane mailbox.
2222
*/
23-
contract EthereumISM is OwnableUpgradeable, UUPSUpgradeable, PausableUpgradeable, IEthereumISM {
23+
contract EvmISM is OwnableUpgradeable, UUPSUpgradeable, PausableUpgradeable, IEvmISM {
2424
/// @notice The verification key for the EVM Hyperlane Merkle Tree SP1 program circuit
2525
bytes32 public programVk;
2626

2727
/// @notice The SP1 verifier contract used to verify the proofs
2828
ISP1Verifier public verifier;
2929

30-
/// @notice The Ethereum light client contract for validating app hashes
31-
IEthereumLightClient public ethereumLightClient;
30+
/// @notice The light client contract for validating source chain state roots
31+
IEvmLightClient public lightClient;
3232

3333
/// @notice Mapping of verified Merkle tree roots that can be used for message verification
3434
/// @dev A root is added when an SP1 proof is successfully verified via update()
@@ -43,25 +43,22 @@ contract EthereumISM is OwnableUpgradeable, UUPSUpgradeable, PausableUpgradeable
4343
}
4444

4545
/**
46-
* @notice Initializes the EthereumISM contract with required addresses and verification key
46+
* @notice Initializes the EvmISM contract with required addresses and verification key
4747
* @dev Can only be called once due to initializer modifier
4848
* @param _programVk The SP1 program verification key for the Merkle tree circuit
4949
* @param verifierAddress Address of the SP1 verifier contract
50-
* @param ethereumLightClientAddress Address of the Ethereum light client contract
50+
* @param lightClientAddress Address of the EVM light client contract
5151
*/
52-
function initialize(bytes32 _programVk, address verifierAddress, address ethereumLightClientAddress)
53-
public
54-
initializer
55-
{
52+
function initialize(bytes32 _programVk, address verifierAddress, address lightClientAddress) public initializer {
5653
__Ownable_init(msg.sender);
5754
__Pausable_init();
5855

5956
programVk = _programVk;
6057
require(verifierAddress != address(0), InvalidAddress());
6158
verifier = ISP1Verifier(verifierAddress);
6259

63-
require(ethereumLightClientAddress != address(0), InvalidAddress());
64-
ethereumLightClient = IEthereumLightClient(ethereumLightClientAddress);
60+
require(lightClientAddress != address(0), InvalidAddress());
61+
lightClient = IEvmLightClient(lightClientAddress);
6562
}
6663

6764
/**
@@ -71,14 +68,14 @@ contract EthereumISM is OwnableUpgradeable, UUPSUpgradeable, PausableUpgradeable
7168
*/
7269
function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
7370

74-
/// @inheritdoc IEthereumISM
71+
/// @inheritdoc IEvmISM
7572
function update(bytes calldata proof, bytes calldata publicValues) external override whenNotPaused {
7673
verifier.verifyProof(programVk, publicValues, proof);
7774

7875
CircuitOutput memory output = abi.decode(publicValues, (CircuitOutput));
7976

80-
// Validate that the state root matches the Ethereum light client's state at this block number
81-
bytes32 expectedStateRoot = ethereumLightClient.stateRoots(output.blockNumber);
77+
// Validate that the state root matches the light client's state at this block number
78+
bytes32 expectedStateRoot = lightClient.stateRoots(output.blockNumber);
8279
require(expectedStateRoot != bytes32(0) && output.stateRoot == expectedStateRoot, InvalidStateRoot());
8380

8481
// Mark this Merkle hook root as valid for message verification
@@ -87,7 +84,7 @@ contract EthereumISM is OwnableUpgradeable, UUPSUpgradeable, PausableUpgradeable
8784
emit Updated(output.root, output.blockNumber, output.stateRoot);
8885
}
8986

90-
/// @inheritdoc IEthereumISM
87+
/// @inheritdoc IEvmISM
9188
function verify(bytes calldata _metadata, bytes calldata _message)
9289
external
9390
view
@@ -143,7 +140,7 @@ contract EthereumISM is OwnableUpgradeable, UUPSUpgradeable, PausableUpgradeable
143140
}
144141

145142
/**
146-
* @notice Returns the version of the EthereumISM contract
143+
* @notice Returns the version of the EvmISM contract
147144
* @return A string representing the semantic version
148145
*/
149146
function version() external pure override returns (string memory) {

contracts/src/light-client/EthereumLightClient.sol

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,10 @@ contract EthereumLightClient is OwnableUpgradeable, UUPSUpgradeable, PausableUpg
199199
*/
200200
function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
201201

202-
/// @inheritdoc IEthereumLightClient
202+
/**
203+
* @notice Returns the execution state root for the latest finalized block.
204+
* @return The state root hash that can be used for verifying storage proofs.
205+
*/
203206
function latestStateRoot() public view returns (bytes32) {
204207
return stateRoots[latestBlockNumber];
205208
}

0 commit comments

Comments
 (0)