From 60bda909e894274b1f9156726d23dd97efde940e Mon Sep 17 00:00:00 2001 From: mpetrun5 Date: Thu, 18 Jan 2024 14:13:26 +0100 Subject: [PATCH 1/4] Implement SpectreProxy --- src/contracts/interfaces/ISpectre.sol | 48 +++++++++++ src/contracts/proxies/SpectreProxy.sol | 114 +++++++++++++++++++++++++ 2 files changed, 162 insertions(+) create mode 100644 src/contracts/interfaces/ISpectre.sol create mode 100644 src/contracts/proxies/SpectreProxy.sol diff --git a/src/contracts/interfaces/ISpectre.sol b/src/contracts/interfaces/ISpectre.sol new file mode 100644 index 00000000..e5a1f0d7 --- /dev/null +++ b/src/contracts/interfaces/ISpectre.sol @@ -0,0 +1,48 @@ +// The Licensed Work is (c) 2023 Sygma +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity 0.8.11; + + +/** + @title Interface for Spectre (https://github.com/ChainSafe/Spectre/blob/main/contracts/src/Spectre.sol) contract + @author ChainSafe Systems. + */ +interface ISpectre { + struct SyncStepInput { + uint64 attestedSlot; + uint64 finalizedSlot; + uint64 participation; + bytes32 finalizedHeaderRoot; + bytes32 executionPayloadRoot; + uint256[12] accumulator; + } + struct RotateInput { + bytes32 syncCommitteeSSZ; + uint256 syncCommitteePoseidon; + uint256[12] accumulator; + } + + /// @notice Verify that a sync committee has attested to a block that finalizes the given header root and execution payload + /// @param input The input to the sync step. Defines the slot and attestation to verify + /// @param proof The proof for the sync step + function step(SyncStepInput calldata input, bytes calldata proof) external; + + + /// @notice Use the current sync committee to verify the transition to a new sync committee + /// @param rotateInput The input to the sync step. + /// @param rotateProof The proof for the rotation + /// @param stepInput The input to the sync step. + /// @param stepProof The proof for the sync step + function rotate( + RotateInput calldata rotateInput, + bytes calldata rotateProof, + SyncStepInput calldata stepInput, + bytes calldata stepProof + ) external; + + /// @notice Fetches the execution payload root for the given slot + /// @param slot Beacon chain slot for requesting execution payload root + /// @return Execution payload root for given slot + function executionPayloadRoots(uint256 slot) external view returns (bytes32); + +} diff --git a/src/contracts/proxies/SpectreProxy.sol b/src/contracts/proxies/SpectreProxy.sol new file mode 100644 index 00000000..10ec47bf --- /dev/null +++ b/src/contracts/proxies/SpectreProxy.sol @@ -0,0 +1,114 @@ +// The Licensed Work is (c) 2023 Sygma +// SPDX-License-Identifier: LGPL-3.0-only + +pragma solidity 0.8.11; + +import "../interfaces/ISpectre.sol"; +import "../utils/AccessControl.sol"; + +/** + @title Proxies calls to Spectre https://github.com/ChainSafe/Spectre/blob/main/contracts/src/Spectre.sol + to enable multiple domain support + @author ChainSafe Systems. + */ +contract SpectreProxy is AccessControl { + + uint public constant STATE_ROOT_INDEX = 18; + + // source domainID => slot => state root + mapping(uint8 => mapping(uint256 => bytes32)) stateRoots; + + // source domainID => spectre contract address + mapping(uint8 => address) _spectreContracts; + + event CommitteeRotated(uint8 sourceDomainID, uint256 slot); + event StateRootSubmitted(uint8 sourceDomainID, uint256 slot, bytes32 stateRoot); + + modifier onlyAdmin() { + _onlyAdmin(); + _; + } + + function _onlyAdmin() private view { + require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()), "sender doesn't have admin role"); + } + + /** + @notice Admin function that sets the spectre address for a domain + @param sourceDomainID Domain ID of the source chain + @param spectreAddress Address of the contract + */ + function adminSetSpectreAddress(uint8 sourceDomainID, address spectreAddress) external onlyAdmin { + _spectreContracts[sourceDomainID] = spectreAddress; + } + + /** + @notice Proxy for the Spectre rotate function that supports multiple domains + @param sourceDomainID DomainID of the network for which the proof is submitted + @param rotateInput The input to the sync step + @param rotateProof The proof for the rotation + @param stepInput The input to the sync step + @param stepProof The proof for the sync step + */ + function rotate( + uint8 sourceDomainID, + ISpectre.RotateInput calldata rotateInput, + bytes calldata rotateProof, + ISpectre.SyncStepInput calldata stepInput, + bytes calldata stepProof + ) external { + address spectreAddress = _spectreContracts[sourceDomainID]; + require(spectreAddress != address(0), "no spectre address found"); + + ISpectre spectre = ISpectre(spectreAddress); + spectre.rotate(rotateInput, rotateProof, stepInput, stepProof); + + emit CommitteeRotated(sourceDomainID, stepInput.attestedSlot); + } + + /** + @notice Proxy for the Spectre step function that proves and stores the execution state root + @param input The input to the sync step. Defines the slot and attestation to verify + @param stepProof The proof for the sync step + @param stateRoot The execution state root for the step slot + @param stateRootProof Indexed merkle proof for the state root + */ + function step( + uint8 sourceDomainID, + ISpectre.SyncStepInput calldata input, + bytes calldata stepProof, + bytes32 stateRoot, + bytes[] calldata stateRootProof + ) external { + address spectreAddress = _spectreContracts[sourceDomainID]; + require(spectreAddress != address(0), "no spectre address found"); + + ISpectre spectre = ISpectre(spectreAddress); + spectre.step(input, stepProof); + + bytes32 executionRoot = spectre.executionPayloadRoots(input.finalizedSlot); + require( + verifyMerkleBranch(stateRoot, executionRoot, stateRootProof, STATE_ROOT_INDEX), + "invalid merkle proof" + ); + + stateRoots[sourceDomainID][input.finalizedSlot] = stateRoot; + emit StateRootSubmitted(sourceDomainID, input.finalizedSlot, stateRoot); + + } + + function verifyMerkleBranch(bytes32 leaf, bytes32 root, bytes[] calldata proof, uint256 index) internal pure returns (bool) { + bytes32 value = leaf; + + for (uint256 i = 0; i < proof.length; i++) { + if ((index / (2**i)) % 2 == 1) { + value = sha256(abi.encodePacked(proof[i], value)); + } else { + value = sha256(abi.encodePacked(value, proof[i])); + } + } + + return value == root; + } + +} From 96ec230b0f854cbb4862047ad467d6265e15c21b Mon Sep 17 00:00:00 2001 From: mpetrun5 Date: Thu, 18 Jan 2024 16:00:30 +0100 Subject: [PATCH 2/4] Add tests --- src/contracts/TestContracts.sol | 30 ++++ src/contracts/interfaces/ISpectre.sol | 3 +- src/contracts/proxies/SpectreProxy.sol | 34 +++- test/proxies/spectre/spectreProxy.test.ts | 187 ++++++++++++++++++++++ 4 files changed, 246 insertions(+), 8 deletions(-) create mode 100644 test/proxies/spectre/spectreProxy.test.ts diff --git a/src/contracts/TestContracts.sol b/src/contracts/TestContracts.sol index e00f6650..27ca3287 100755 --- a/src/contracts/TestContracts.sol +++ b/src/contracts/TestContracts.sol @@ -214,3 +214,33 @@ contract TestDeposit { emit TestExecute(depositor, num, addresses[1], message); } } + +contract TestSpectre { + struct SyncStepInput { + uint64 attestedSlot; + uint64 finalizedSlot; + uint64 participation; + bytes32 finalizedHeaderRoot; + bytes32 executionPayloadRoot; + uint256[12] accumulator; + } + + struct RotateInput { + bytes32 syncCommitteeSSZ; + uint256 syncCommitteePoseidon; + uint256[12] accumulator; + } + + mapping(uint256 => bytes32) public executionPayloadRoots; + + function step(SyncStepInput calldata input, bytes calldata proof) external { + executionPayloadRoots[input.finalizedSlot] = input.executionPayloadRoot; + } + + function rotate( + RotateInput calldata rotateInput, + bytes calldata rotateProof, + SyncStepInput calldata stepInput, + bytes calldata stepProof + ) external {} +} diff --git a/src/contracts/interfaces/ISpectre.sol b/src/contracts/interfaces/ISpectre.sol index e5a1f0d7..6252a7d4 100644 --- a/src/contracts/interfaces/ISpectre.sol +++ b/src/contracts/interfaces/ISpectre.sol @@ -22,7 +22,8 @@ interface ISpectre { uint256[12] accumulator; } - /// @notice Verify that a sync committee has attested to a block that finalizes the given header root and execution payload + /// @notice Verify that a sync committee has attested to a block that finalizes + /// the given header root and execution payload /// @param input The input to the sync step. Defines the slot and attestation to verify /// @param proof The proof for the sync step function step(SyncStepInput calldata input, bytes calldata proof) external; diff --git a/src/contracts/proxies/SpectreProxy.sol b/src/contracts/proxies/SpectreProxy.sol index 10ec47bf..d1759316 100644 --- a/src/contracts/proxies/SpectreProxy.sol +++ b/src/contracts/proxies/SpectreProxy.sol @@ -13,13 +13,13 @@ import "../utils/AccessControl.sol"; */ contract SpectreProxy is AccessControl { - uint public constant STATE_ROOT_INDEX = 18; + uint8 public constant STATE_ROOT_INDEX = 18; // source domainID => slot => state root - mapping(uint8 => mapping(uint256 => bytes32)) stateRoots; + mapping(uint8 => mapping(uint256 => bytes32)) public stateRoots; // source domainID => spectre contract address - mapping(uint8 => address) _spectreContracts; + mapping(uint8 => address) public spectreContracts; event CommitteeRotated(uint8 sourceDomainID, uint256 slot); event StateRootSubmitted(uint8 sourceDomainID, uint256 slot, bytes32 stateRoot); @@ -33,13 +33,28 @@ contract SpectreProxy is AccessControl { require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()), "sender doesn't have admin role"); } + /** + @notice Initializes spectre proxy and sets initial + spectre addresses. + @param domainIDS List of to be domain IDs. + @param spectreAddresses List of spectre addresses. + */ + constructor(uint8[] memory domainIDS, address[] memory spectreAddresses) { + require(domainIDS.length == spectreAddresses.length, "array length should be equal"); + for (uint8 i = 0; i < domainIDS.length; i++) { + spectreContracts[domainIDS[i]] = spectreAddresses[i]; + } + + _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); + } + /** @notice Admin function that sets the spectre address for a domain @param sourceDomainID Domain ID of the source chain @param spectreAddress Address of the contract */ function adminSetSpectreAddress(uint8 sourceDomainID, address spectreAddress) external onlyAdmin { - _spectreContracts[sourceDomainID] = spectreAddress; + spectreContracts[sourceDomainID] = spectreAddress; } /** @@ -57,7 +72,7 @@ contract SpectreProxy is AccessControl { ISpectre.SyncStepInput calldata stepInput, bytes calldata stepProof ) external { - address spectreAddress = _spectreContracts[sourceDomainID]; + address spectreAddress = spectreContracts[sourceDomainID]; require(spectreAddress != address(0), "no spectre address found"); ISpectre spectre = ISpectre(spectreAddress); @@ -80,7 +95,7 @@ contract SpectreProxy is AccessControl { bytes32 stateRoot, bytes[] calldata stateRootProof ) external { - address spectreAddress = _spectreContracts[sourceDomainID]; + address spectreAddress = spectreContracts[sourceDomainID]; require(spectreAddress != address(0), "no spectre address found"); ISpectre spectre = ISpectre(spectreAddress); @@ -97,7 +112,12 @@ contract SpectreProxy is AccessControl { } - function verifyMerkleBranch(bytes32 leaf, bytes32 root, bytes[] calldata proof, uint256 index) internal pure returns (bool) { + function verifyMerkleBranch( + bytes32 leaf, + bytes32 root, + bytes[] calldata proof, + uint8 index + ) internal pure returns (bool) { bytes32 value = leaf; for (uint256 i = 0; i < proof.length; i++) { diff --git a/test/proxies/spectre/spectreProxy.test.ts b/test/proxies/spectre/spectreProxy.test.ts new file mode 100644 index 00000000..e77bf42d --- /dev/null +++ b/test/proxies/spectre/spectreProxy.test.ts @@ -0,0 +1,187 @@ +import { assert, expect } from "chai"; +import { ethers } from "hardhat"; +import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; +import {} from "../../helpers"; +import type { ISpectre, SpectreProxy } from "../../../typechain-types"; + +describe("", () => { + const originDomainID = 1; + + const invalidOriginDomainID = 4; + const validDomainID = 3; + + const rotateProof = + "0xcc69885fda6bcc1a4ace058b4a62bf5e179ea78fd58a1ccd71c22cc9b688792f"; + const stepProof = + "0xcc69885fda6bcc1a4ace058b4a62bf5e179ea78fd58a1ccd71c22cc9b688792f"; + + const validStateRoot = + "0x8c0c3244e0ca8c0e5416a3407787c71b29225723e0f887396ce018f8f38f20d5"; + const validStateRootProof = [ + "0x0c2e45ec77206f3b0cac1da903c4bc05cf177da367c428c1ba3cab0f654f4f78", + "0xdf581c183b1083cf6be31fde9f6073dfacfc252f8b514577f2ca03955b921552", + "0x59dac95a8278295a3a05d809156f69b45007af3f3df94bcabe4bbbdd9cce5c5a", + "0x4dc9cd52dff9694aed19a73da85386b77d641c81bcb7015dbf7daeec5614f010", + ]; + const invalidStateRoot = + "0xcc69885fda6bcc1a4ace058b4a62bf5e179ea78fd58a1ccd71c22cc9b688792f"; + const invalidStateRootProof = [ + "0x0c2e45ec77206f3b0cac1da903c4bc05cf177da367c428c1ba3cab0f654f4f78", + "0xdf581c183b1083cf6be31fde9f6073dfacfc252f8b514577f2ca03955b921552", + "0x59dac95a8278295a3a05d809156f69b45007af3f3df94bcabe4bbbdd9cce5c5a", + ]; + + const rotateInput: ISpectre.RotateInputStruct = { + syncCommitteePoseidon: "256", + syncCommitteeSSZ: + "0xcc69885fda6bcc1a4ace058b4a62bf5e179ea78fd58a1ccd71c22cc9b688792f", + accumulator: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], + }; + const stepInput: ISpectre.SyncStepInputStruct = { + finalizedHeaderRoot: + "0xcc69885fda6bcc1a4ace058b4a62bf5e179ea78fd58a1ccd71c22cc9b688792f", + finalizedSlot: 100, + attestedSlot: 101, + participation: 8, + executionPayloadRoot: + "0x9109d68183cb2c2b4d8d769a4263195c153ece0d2bc797f44b8f6cec4814439c", + accumulator: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], + }; + + const constructorDomains = [2, 3]; + const invalidSpectreAddress = "0x9Da9DbbB87db6e9862C79651CBae0D468fa88c71"; + const constructorAddresses = [invalidSpectreAddress]; + + let spectreAddress: string; + + let spectreProxyInstance: SpectreProxy; + let nonAdminAccount: HardhatEthersSigner; + + beforeEach(async () => { + [, nonAdminAccount] = await ethers.getSigners(); + const SpectreProxyContract = + await ethers.getContractFactory("SpectreProxy"); + const SpectreContract = await ethers.getContractFactory("TestSpectre"); + const spectreInstance = await SpectreContract.deploy(); + spectreAddress = await spectreInstance.getAddress(); + constructorAddresses[1] = spectreAddress; + spectreProxyInstance = await SpectreProxyContract.deploy( + constructorDomains, + constructorAddresses, + ); + }); + + it("constructor should set intial addresses", async () => { + assert.equal( + await spectreProxyInstance.spectreContracts(constructorDomains[0]), + constructorAddresses[0], + ); + assert.equal( + await spectreProxyInstance.spectreContracts(constructorDomains[1]), + spectreAddress, + ); + }); + + it("should require admin role to set spectre address", async () => { + await expect( + spectreProxyInstance + .connect(nonAdminAccount) + .adminSetSpectreAddress(originDomainID, spectreAddress), + ).to.be.revertedWith("sender doesn't have admin role"); + }); + + it("should set spectre address with an admin role", async () => { + await spectreProxyInstance.adminSetSpectreAddress( + originDomainID, + spectreAddress, + ); + + assert.equal( + await spectreProxyInstance.spectreContracts(originDomainID), + spectreAddress, + ); + }); + + it("should revert if spectre address not set in rotate", async () => { + await expect( + spectreProxyInstance.rotate( + invalidOriginDomainID, + rotateInput, + rotateProof, + stepInput, + stepProof, + ), + ).to.be.revertedWith("no spectre address found"); + }); + + it("should emit event even if rotate successful", async () => { + const rotateTx = await spectreProxyInstance.rotate( + validDomainID, + rotateInput, + rotateProof, + stepInput, + stepProof, + ); + + await expect(rotateTx) + .to.emit(spectreProxyInstance, "CommitteeRotated") + .withArgs(validDomainID, stepInput.attestedSlot); + }); + + it("should revert if spectre address not set in step", async () => { + await expect( + spectreProxyInstance.step( + invalidOriginDomainID, + stepInput, + stepProof, + validStateRoot, + validStateRootProof, + ), + ).to.be.revertedWith("no spectre address found"); + }); + + it("should revert if step proof not valid", async () => { + await expect( + spectreProxyInstance.step( + validDomainID, + stepInput, + stepProof, + validStateRoot, + invalidStateRootProof, + ), + ).to.be.revertedWith("invalid merkle proof"); + }); + + it("should revert if step state root not valid", async () => { + await expect( + spectreProxyInstance.step( + validDomainID, + stepInput, + stepProof, + invalidStateRoot, + validStateRootProof, + ), + ).to.be.revertedWith("invalid merkle proof"); + }); + + it("should emit event and store state root if step valid", async () => { + const stepTx = await spectreProxyInstance.step( + validDomainID, + stepInput, + stepProof, + validStateRoot, + validStateRootProof, + ); + + assert.equal( + await spectreProxyInstance.stateRoots( + validDomainID, + stepInput.finalizedSlot, + ), + validStateRoot, + ); + await expect(stepTx) + .to.emit(spectreProxyInstance, "StateRootSubmitted") + .withArgs(validDomainID, stepInput.finalizedSlot, validStateRoot); + }); +}); From 796ecc78c4ff94ccbb5fb98ad0697451069e1677 Mon Sep 17 00:00:00 2001 From: mpetrun5 Date: Thu, 18 Jan 2024 16:04:31 +0100 Subject: [PATCH 3/4] Cleanup tests --- test/proxies/spectre/spectreProxy.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/proxies/spectre/spectreProxy.test.ts b/test/proxies/spectre/spectreProxy.test.ts index e77bf42d..9aabdf19 100644 --- a/test/proxies/spectre/spectreProxy.test.ts +++ b/test/proxies/spectre/spectreProxy.test.ts @@ -1,10 +1,9 @@ import { assert, expect } from "chai"; import { ethers } from "hardhat"; import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; -import {} from "../../helpers"; import type { ISpectre, SpectreProxy } from "../../../typechain-types"; -describe("", () => { +describe("Spectre Proxy", () => { const originDomainID = 1; const invalidOriginDomainID = 4; From 4a51662825276f0b95687cefdf3f54124e6703dc Mon Sep 17 00:00:00 2001 From: mpetrun5 Date: Wed, 31 Jan 2024 11:29:42 +0100 Subject: [PATCH 4/4] Add getStateRoot function --- src/contracts/proxies/SpectreProxy.sol | 13 ++++++++++++- test/proxies/spectre/spectreProxy.test.ts | 7 +++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/contracts/proxies/SpectreProxy.sol b/src/contracts/proxies/SpectreProxy.sol index d1759316..aae7c188 100644 --- a/src/contracts/proxies/SpectreProxy.sol +++ b/src/contracts/proxies/SpectreProxy.sol @@ -80,7 +80,7 @@ contract SpectreProxy is AccessControl { emit CommitteeRotated(sourceDomainID, stepInput.attestedSlot); } - + /** @notice Proxy for the Spectre step function that proves and stores the execution state root @param input The input to the sync step. Defines the slot and attestation to verify @@ -112,6 +112,17 @@ contract SpectreProxy is AccessControl { } + /** + @notice Returns a state root. + @param sourceDomainID ID of chain state root originated from. + @param slot slot number of the state root. + @return State root for the given domain ID and slot. + */ + function getStateRoot(uint8 sourceDomainID, uint256 slot) public view returns (bytes32) { + return stateRoots[sourceDomainID][slot]; + } + + function verifyMerkleBranch( bytes32 leaf, bytes32 root, diff --git a/test/proxies/spectre/spectreProxy.test.ts b/test/proxies/spectre/spectreProxy.test.ts index 9aabdf19..14ff24c1 100644 --- a/test/proxies/spectre/spectreProxy.test.ts +++ b/test/proxies/spectre/spectreProxy.test.ts @@ -179,6 +179,13 @@ describe("Spectre Proxy", () => { ), validStateRoot, ); + assert.equal( + await spectreProxyInstance.getStateRoot( + validDomainID, + stepInput.finalizedSlot, + ), + validStateRoot, + ); await expect(stepTx) .to.emit(spectreProxyInstance, "StateRootSubmitted") .withArgs(validDomainID, stepInput.finalizedSlot, validStateRoot);