diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f829d5a1..f1876fda 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -37,6 +37,12 @@ jobs: - name: Download consensus spec run: just download-spec-tests + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + + - name: Build contracts + run: just build-contracts + # skip this in favour of compiling in the `Test` action. # - name: Check # run: cargo check --all diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..c65a5965 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "contracts/lib/forge-std"] + path = contracts/lib/forge-std + url = https://github.com/foundry-rs/forge-std diff --git a/contracts/.gitmodules b/contracts/.gitmodules deleted file mode 100644 index 888d42dc..00000000 --- a/contracts/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "lib/forge-std"] - path = lib/forge-std - url = https://github.com/foundry-rs/forge-std diff --git a/contracts/lib/forge-std b/contracts/lib/forge-std index 74cfb77e..1d9650e9 160000 --- a/contracts/lib/forge-std +++ b/contracts/lib/forge-std @@ -1 +1 @@ -Subproject commit 74cfb77e308dd188d2f58864aaf44963ae6b88b1 +Subproject commit 1d9650e951204a0ddce9ff89c32f1997984cef4d diff --git a/contracts/remappings.txt b/contracts/remappings.txt index 1076a425..845bd0af 100644 --- a/contracts/remappings.txt +++ b/contracts/remappings.txt @@ -1,2 +1,2 @@ ds-test/=lib/forge-std/lib/ds-test/src/ -forge-std/=lib/forge-std/src/ \ No newline at end of file +forge-std/=lib/forge-std/src/ diff --git a/contracts/src/Spectre.sol b/contracts/src/Spectre.sol index e6850f9b..37189f38 100644 --- a/contracts/src/Spectre.sol +++ b/contracts/src/Spectre.sol @@ -1,8 +1,11 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; +import { SyncStepLib } from "./SyncStepLib.sol"; + contract Spectre { - + using SyncStepLib for SyncStepLib.SyncStepInput; + address public verifierContract; constructor(address _verifierContract) { diff --git a/contracts/src/SyncStepLib.sol b/contracts/src/SyncStepLib.sol new file mode 100644 index 00000000..9a5f425a --- /dev/null +++ b/contracts/src/SyncStepLib.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "forge-std/console.sol"; + + +library SyncStepLib { + struct SyncStepInput { + uint64 attestedSlot; + uint64 finalizedSlot; + uint64 participation; + bytes32 finalizedHeaderRoot; + bytes32 executionPayloadRoot; + } + + function toLittleEndian64(uint64 v) internal pure returns (bytes8) { + v = ((v & 0xFF00FF00FF00FF00) >> 8) | ((v & 0x00FF00FF00FF00FF) << 8); + v = ((v & 0xFFFF0000FFFF0000) >> 16) | ((v & 0x0000FFFF0000FFFF) << 16); + v = ((v & 0xFFFFFFFF00000000) >> 32) | ((v & 0x00000000FFFFFFFF) << 32); + return bytes8(v); + } + + function toLittleEndian(uint256 v) internal pure returns (bytes32) { + v = ((v & 0xFF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00) >> 8) + | ((v & 0x00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF00FF) << 8); + v = ((v & 0xFFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000) >> 16) + | ((v & 0x0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF0000FFFF) << 16); + v = ((v & 0xFFFFFFFF00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF00000000) >> 32) + | ((v & 0x00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF00000000FFFFFFFF) << 32); + v = ((v & 0xFFFFFFFFFFFFFFFF0000000000000000FFFFFFFFFFFFFFFF0000000000000000) >> 64) + | ((v & 0x0000000000000000FFFFFFFFFFFFFFFF0000000000000000FFFFFFFFFFFFFFFF) << 64); + v = (v >> 128) | (v << 128); + return bytes32(v); + } + + /** + * @notice Compute the public input commitment for the sync step given this input. + * This must always match the prodecure used in lightclient-circuits/src/sync_step_circuit.rs - SyncStepCircuit::instance() + * @param args The arguments for the sync step + * @param keysPoseidonCommitment The commitment to the keys used in the sync step + * @return The public input commitment that can be sent to the verifier contract. + */ + function toInputCommitment(SyncStepInput memory args, bytes32 keysPoseidonCommitment) internal pure returns (uint256) { + bytes32 h = sha256(abi.encodePacked( + toLittleEndian64(args.attestedSlot), + toLittleEndian64(args.finalizedSlot), + toLittleEndian64(args.participation), + args.finalizedHeaderRoot, + args.executionPayloadRoot, + keysPoseidonCommitment + )); + uint256 commitment = uint256(toLittleEndian(uint256(h))); + return commitment & ((uint256(1) << 253) - 1); // truncated to 253 bits + } +} diff --git a/contracts/test/SpectreSyncStep.sol b/contracts/test/SpectreSyncStep.sol deleted file mode 100644 index 3a50e7a0..00000000 --- a/contracts/test/SpectreSyncStep.sol +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity 0.8.19; - -import "forge-std/Script.sol"; -import "forge-std/safeconsole.sol"; -import "forge-std/Test.sol"; -import "../lib/YulDeployer.sol"; -import {Spectre} from "../src/Spectre.sol"; - -contract SpectreSyncStep is Test { - YulDeployer yulDeployer; - address verifierAddress; - bytes proof; - - function setUp() public virtual { - - yulDeployer = new YulDeployer(); - // `mainnet_10_7.v1` is a Yul verifier for a SNARK constraining a chain of up to 1024 block headers - // and Merkle-ization of their block hashes as specified in `updateRecent`. - verifierAddress = address(yulDeployer.deployContract("sync_step_k21")); - proof = vm.parseBytes(vm.readFile("test/data/sync_step_21.calldata")); - } - - function testPostHeader() public { - vm.pauseGasMetering(); - - Spectre spectre = new Spectre(verifierAddress); - vm.resumeGasMetering(); - - spectre.postHeader(proof); - // verifierAddress.call(proof); - } -} diff --git a/contracts/test/SyncStepExternal.sol b/contracts/test/SyncStepExternal.sol new file mode 100644 index 00000000..44af7689 --- /dev/null +++ b/contracts/test/SyncStepExternal.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import { SyncStepLib } from "../src/SyncStepLib.sol"; + +/** +* @title SyncStepLibTest +* @dev This contract exists solely for the purpose of exposing the SyncStepLib functions +* so they can be used in the Rust test suite. It should not be part of a production deployment +*/ +contract SyncStepExternal { + using SyncStepLib for SyncStepLib.SyncStepInput; + + function toInputCommitment(SyncStepLib.SyncStepInput calldata args, bytes32 keysPoseidonCommitment) public pure returns (uint256) { + return args.toInputCommitment(keysPoseidonCommitment); + } +} diff --git a/justfile b/justfile index 1e8cc45e..9ca27712 100644 --- a/justfile +++ b/justfile @@ -25,6 +25,9 @@ gen-step-evm-verifier: gen-rotation-evm-verifier: cargo run -r -- aggregation -c ./lightclient-circuits/config/aggregation.json --app-pk-path ./build/committee_update.pkey --app-config-path ./lightclient-circuits/config/committee_update.json -i ./rotation -o evm-verifier ./contracts/snark-verifiers/committee_update_aggregated.yul +build-contracts: + cd contracts && forge build + # downloads spec tests and copies them to the right locations. download-spec-tests: clean-spec-tests #!/usr/bin/env bash diff --git a/lightclient-circuits/Cargo.toml b/lightclient-circuits/Cargo.toml index 46db4e93..f7810f6c 100644 --- a/lightclient-circuits/Cargo.toml +++ b/lightclient-circuits/Cargo.toml @@ -65,6 +65,9 @@ rstest = "0.18.2" test-utils = { git = "ssh://git@github.com/sygmaprotocol/Zipline.git", rev = "27e8a01" } ethereum-consensus-types = { git = "ssh://git@github.com/sygmaprotocol/Zipline.git", rev = "27e8a01" } light-client-verifier = { git = "ssh://git@github.com/sygmaprotocol/Zipline.git", rev = "27e8a01" } +ethers = "2.0.10" +tokio = { version = "1.32.0", features = ["rt", "macros"] } +anyhow = "1.0.75" [features] default = [] diff --git a/lightclient-circuits/src/lib.rs b/lightclient-circuits/src/lib.rs index 762d4a88..e5c22d70 100644 --- a/lightclient-circuits/src/lib.rs +++ b/lightclient-circuits/src/lib.rs @@ -18,7 +18,7 @@ pub mod committee_update_circuit; pub mod sync_step_circuit; pub mod builder; -mod poseidon; +pub mod poseidon; mod ssz_merkle; pub use halo2_base::gates::builder::FlexGateConfigParams; diff --git a/lightclient-circuits/src/sync_step_circuit.rs b/lightclient-circuits/src/sync_step_circuit.rs index d39e578d..cacc6b2d 100644 --- a/lightclient-circuits/src/sync_step_circuit.rs +++ b/lightclient-circuits/src/sync_step_circuit.rs @@ -257,7 +257,6 @@ impl SyncStepCircuit { false, )? .output_bytes; - let pi_commit = truncate_sha256_into_single_elem(thread_pool.main(), range, pi_hash_bytes); Ok(vec![pi_commit]) diff --git a/lightclient-circuits/tests/step.rs b/lightclient-circuits/tests/step.rs index 1e4d8c05..77d2334f 100644 --- a/lightclient-circuits/tests/step.rs +++ b/lightclient-circuits/tests/step.rs @@ -507,3 +507,111 @@ fn test_eth2_spec_evm_verify( println!("deployment_code size: {}", deployment_code.len()); snark_verifier_sdk::evm::evm_verify(deployment_code, instances, proof); } + +mod solidity_tests { + use super::*; + use ethers::{ + contract::abigen, + core::utils::{Anvil, AnvilInstance}, + middleware::SignerMiddleware, + providers::{Http, Provider}, + signers::{LocalWallet, Signer}, + }; + use halo2_base::safe_types::ScalarField; + use halo2curves::group::UncompressedEncoding; + use lightclient_circuits::poseidon::fq_array_poseidon_native; + use std::sync::Arc; + + /// Ensure that the instance encoding implemented in Solidity matches exactly the instance encoding expected by the circuit + #[rstest] + #[tokio::test] + async fn test_instance_commitment_evm_equivalence( + #[files("../consensus-spec-tests/tests/minimal/capella/light_client/sync/pyspec_tests/**")] + #[exclude("deneb*")] + path: PathBuf, + ) -> anyhow::Result<()> { + let (witness, _) = read_test_files_and_gen_witness(path); + let instance = SyncStepCircuit::::instance_commitment(&witness); + let poseidon_commitment_le = extract_poseidon_committee_commitment(&witness)?; + + let anvil_instance = Anvil::new().spawn(); + let ethclient: Arc, _>> = make_client(&anvil_instance); + let contract = SyncStepExternal::deploy(ethclient, ())?.send().await?; + + let result = contract + .to_input_commitment(SyncStepInput::from(witness), poseidon_commitment_le) + .call() + .await?; + let mut result_bytes = [0_u8; 32]; + result.to_little_endian(&mut result_bytes); + + assert_eq!(bn256::Fr::from_bytes(&result_bytes).unwrap(), instance); + Ok(()) + } + + abigen!( + SyncStepExternal, + "../contracts/out/SyncStepExternal.sol/SyncStepExternal.json" + ); + + // SyncStepInput type produced by abigen macro matches the solidity struct type + impl From> for SyncStepInput { + fn from(args: SyncStepArgs) -> Self { + let participation = args + .pariticipation_bits + .iter() + .map(|v| *v as u64) + .sum::(); + + let finalized_header_root: [u8; 32] = args + .finalized_header + .clone() + .hash_tree_root() + .unwrap() + .as_bytes() + .try_into() + .unwrap(); + + let execution_payload_root: [u8; 32] = args.execution_payload_root.try_into().unwrap(); + + SyncStepInput { + attested_slot: args.attested_header.slot, + finalized_slot: args.finalized_header.slot, + participation: participation, + finalized_header_root, + execution_payload_root, + } + } + } + + fn extract_poseidon_committee_commitment( + witness: &SyncStepArgs, + ) -> anyhow::Result<[u8; 32]> { + let pubkey_affines = witness + .pubkeys_uncompressed + .iter() + .cloned() + .map(|bytes| { + halo2curves::bls12_381::G1Affine::from_uncompressed_unchecked( + &bytes.as_slice().try_into().unwrap(), + ) + .unwrap() + }) + .collect_vec(); + let poseidon_commitment = + fq_array_poseidon_native::(pubkey_affines.iter().map(|p| p.x)).unwrap(); + Ok(poseidon_commitment.to_bytes_le().try_into().unwrap()) + } + + /// Return a fresh ethereum chain+client to test against + fn make_client(anvil: &AnvilInstance) -> Arc, LocalWallet>> { + let provider = Provider::::try_from(anvil.endpoint()) + .unwrap() + .interval(std::time::Duration::from_millis(10u64)); + let wallet: LocalWallet = anvil.keys()[0].clone().into(); + + let client: SignerMiddleware, _> = + SignerMiddleware::new(provider, wallet.with_chain_id(anvil.chain_id())); + Arc::new(client) + } +}