Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rotation - Adds input encoding in Solidity and compatibility tests #24

Merged
merged 11 commits into from
Oct 23, 2023
21 changes: 21 additions & 0 deletions contracts/src/RotateLib.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
17 changes: 17 additions & 0 deletions contracts/test/RotateExternal.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

import { RotateLib } from "../src/RotateLib.sol";

/**
* @title SyncStepLibTest
willemolding marked this conversation as resolved.
Show resolved Hide resolved
* @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);
}
}
10 changes: 7 additions & 3 deletions lightclient-circuits/src/committee_update_circuit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,15 +136,18 @@ impl<S: Spec, F: Field> CommitteeUpdateCircuit<S, F> {
Ok(public_inputs)
}

pub fn instance(args: &witness::CommitteeRotationArgs<S, F>) -> Vec<Vec<bn256::Fr>> {
pub fn instance(args: &witness::CommitteeRotationArgs<S, F>) -> Vec<Vec<bn256::Fr>>
where
[(); { S::SYNC_COMMITTEE_SIZE }]:,
{
let pubkeys_x = args.pubkeys_compressed.iter().cloned().map(|mut bytes| {
bytes[47] &= 0b11111000;
bytes[47] &= 0b00011111;
bls12_381::Fq::from_bytes_le(&bytes)
});

let poseidon_commitment = fq_array_poseidon_native::<bn256::Fr>(pubkeys_x).unwrap();

let mut pk_vector: Vector<Vector<u8, 48>, 512> = args
let mut pk_vector: Vector<Vector<u8, 48>, { S::SYNC_COMMITTEE_SIZE }> = args
.pubkeys_compressed
.iter()
.cloned()
Expand All @@ -159,6 +162,7 @@ impl<S: Spec, F: Field> CommitteeUpdateCircuit<S, F> {

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)))
ec2 marked this conversation as resolved.
Show resolved Hide resolved
.collect();

vec![instance_vec]
Expand Down
1 change: 0 additions & 1 deletion lightclient-circuits/src/poseidon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ pub fn fq_array_poseidon_native<F: Field>(
.collect_vec()
})
.collect_vec();

let mut poseidon = PoseidonNative::<F, POSEIDON_SIZE, { POSEIDON_SIZE - 1 }>::new(R_F, R_P);
let mut current_poseidon_hash = None;

Expand Down
98 changes: 90 additions & 8 deletions lightclient-circuits/tests/step.rs
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,11 @@ fn read_test_files_and_gen_witness(
.next_sync_committee
.aggregate_pubkey
.to_bytes()
.to_vec();
.iter()
// Reverse bytes because of endieness
.copied()
.rev()
.collect_vec();

let mut agg_pk: ByteVector<48> = ByteVector(Vector::try_from(agg_pubkeys_compressed).unwrap());

Expand All @@ -374,7 +378,10 @@ fn read_test_files_and_gen_witness(
.pubkeys
.iter()
.cloned()
.map(|pk| pk.to_bytes().to_vec())
.map(|pk| {
// Reverse bytes because of endieness
pk.to_bytes().iter().copied().rev().collect_vec()
})
.collect_vec(),
randomness: crypto::constant_randomness(),
_spec: Default::default(),
Expand Down Expand Up @@ -544,14 +551,15 @@ 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::<Minimal, bn256::Fr>::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<SignerMiddleware<Provider<Http>, _>> = make_client(&anvil_instance);
Expand All @@ -568,6 +576,47 @@ 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::<Minimal, bn256::Fr>::instance(&witness);
ec2 marked this conversation as resolved.
Show resolved Hide resolved
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<SignerMiddleware<Provider<Http>, _>> = 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"
Expand Down Expand Up @@ -603,11 +652,44 @@ mod solidity_tests {
}
}

fn extract_poseidon_committee_commitment<Spec: eth_types::Spec>(
witness: &SyncStepArgs<Spec>,
abigen!(
RotateExternal,
"../contracts/out/RotateExternal.sol/RotateExternal.json"
);

// CommitteeRotationArgs type produced by abigen macro matches the solidity struct type
impl<Spec: eth_types::Spec> From<CommitteeRotationArgs<Spec, Fr>> for RotateInput {
fn from(args: CommitteeRotationArgs<Spec, Fr>) -> Self {
let poseidon_commitment_le =
extract_poseidon_committee_commitment(&args.pubkeys_compressed).unwrap();

let mut pk_vector: Vector<Vector<u8, 48>, 512> = args
ec2 marked this conversation as resolved.
Show resolved Hide resolved
.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<Vec<u8>>,
) -> anyhow::Result<[u8; 32]> {
let pubkey_affines = witness
.pubkeys_uncompressed
let pubkey_affines = pubkeys_uncompressed
.iter()
.cloned()
.map(|bytes| {
Expand Down
Loading