diff --git a/contracts/src/RotateLib.sol b/contracts/src/RotateLib.sol new file mode 100644 index 00000000..4eaba733 --- /dev/null +++ b/contracts/src/RotateLib.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + + +library RotateLib { + struct RotateInput { + bytes32 syncCommitteeSSZ; + bytes32 syncCommitteePoseidon; + } + + /** + * @notice Compute the public input commitment for the rotation + * This must always match the method used in lightclient-circuits/src/committee_udate_circuit.rs - CommitteeUpdateCircuit::instance() + * @param args The arguments for the sync step + * @return The public input commitment that can be sent to the verifier contract. + */ + function toInputCommitment(RotateInput memory args, bytes32 finalizedHeaderRoot) internal pure returns (uint256[] memory) { + // TODO: Impment this + return new uint256[](0); + } +} diff --git a/contracts/test/RotateExternal.sol b/contracts/test/RotateExternal.sol new file mode 100644 index 00000000..401970e7 --- /dev/null +++ b/contracts/test/RotateExternal.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.19; + +import { RotateLib } from "../src/RotateLib.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 RotateExternal { + using RotateLib for RotateLib.RotateInput; + + function toInputCommitment(RotateLib.RotateInput calldata args, bytes32 finalizedHeaderRoot) public pure returns (uint256[] memory) { + return args.toInputCommitment(finalizedHeaderRoot); + } +} diff --git a/lightclient-circuits/src/committee_update_circuit.rs b/lightclient-circuits/src/committee_update_circuit.rs index a49908ea..c8132595 100644 --- a/lightclient-circuits/src/committee_update_circuit.rs +++ b/lightclient-circuits/src/committee_update_circuit.rs @@ -159,6 +159,7 @@ impl CommitteeUpdateCircuit { let instance_vec = iter::once(poseidon_commitment) .chain(ssz_root.0.map(|b| bn256::Fr::from(b as u64))) + .chain(finalized_header_root.0.map(|b| bn256::Fr::from(b as u64))) .collect(); vec![instance_vec] diff --git a/lightclient-circuits/tests/step.rs b/lightclient-circuits/tests/step.rs index 2dbc5e07..25b802a5 100644 --- a/lightclient-circuits/tests/step.rs +++ b/lightclient-circuits/tests/step.rs @@ -544,14 +544,14 @@ mod solidity_tests { /// 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( + async fn test_step_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 poseidon_commitment_le = extract_poseidon_committee_commitment(&witness.pubkeys_uncompressed)?; let anvil_instance = Anvil::new().spawn(); let ethclient: Arc, _>> = make_client(&anvil_instance); @@ -568,6 +568,37 @@ mod solidity_tests { Ok(()) } + #[rstest] + #[tokio::test] + async fn test_rotate_public_input_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 = CommitteeUpdateCircuit::::instance(&witness); + let finalized_block_root = witness.finalized_header.clone().hash_tree_root().unwrap().as_bytes().try_into().unwrap(); + + let anvil_instance = Anvil::new().spawn(); + let ethclient: Arc, _>> = make_client(&anvil_instance); + let contract = RotateExternal::deploy(ethclient, ())?.send().await?; + + let result = contract + .to_input_commitment(RotateInput::from(witness), finalized_block_root) + .call() + .await?; + + // convert each of the returned values to a field element + let result_decoded: Vec<_> = result.iter().map(|v| { + let mut b = [0_u8; 32]; + v.to_little_endian(&mut b); + bn256::Fr::from_bytes(&b).unwrap() + }).collect(); + + assert_eq!(vec![result_decoded], instance); + Ok(()) + } + abigen!( SyncStepExternal, "../contracts/out/SyncStepExternal.sol/SyncStepExternal.json" @@ -603,11 +634,38 @@ mod solidity_tests { } } - fn extract_poseidon_committee_commitment( - witness: &SyncStepArgs, + abigen!( + RotateExternal, + "../contracts/out/RotateExternal.sol/RotateExternal.json" + ); + + // CommitteeRotationArgs type produced by abigen macro matches the solidity struct type + impl From> for RotateInput { + fn from(args: CommitteeRotationArgs) -> Self { + let poseidon_commitment_le = extract_poseidon_committee_commitment(&args.pubkeys_compressed).unwrap(); + + let mut pk_vector: Vector, 512> = args + .pubkeys_compressed + .iter() + .cloned() + .map(|v| v.try_into().unwrap()) + .collect_vec() + .try_into() + .unwrap(); + + let sync_committee_ssz = pk_vector.hash_tree_root().unwrap().as_bytes().try_into().unwrap(); + + RotateInput { + sync_committee_ssz, + sync_committee_poseidon: poseidon_commitment_le, + } + } + } + + fn extract_poseidon_committee_commitment( + pubkeys_uncompressed: &Vec> ) -> anyhow::Result<[u8; 32]> { - let pubkey_affines = witness - .pubkeys_uncompressed + let pubkey_affines = pubkeys_uncompressed .iter() .cloned() .map(|bytes| {