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
24 changes: 24 additions & 0 deletions contracts/src/EndianConversions.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

library EndianConversions {
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);
}
}
38 changes: 38 additions & 0 deletions contracts/src/RotateLib.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import { EndianConversions } from "./EndianConversions.sol";

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 toPublicInputs(RotateInput memory args, bytes32 finalizedHeaderRoot) internal pure returns (uint256[65] memory) {
uint256[65] memory inputs;

inputs[0] = uint256(EndianConversions.toLittleEndian(uint256(args.syncCommitteePoseidon)));

uint256 syncCommitteeSSZNumeric = uint256(args.syncCommitteeSSZ);
for (uint256 i = 0; i < 32; i++) {
inputs[32 - i] = syncCommitteeSSZNumeric % 2 ** 8;
syncCommitteeSSZNumeric = syncCommitteeSSZNumeric / 2 ** 8;
}

uint256 finalizedHeaderRootNumeric = uint256(finalizedHeaderRoot);
for (uint256 j = 0; j < 32; j++) {
inputs[64 - j] = finalizedHeaderRootNumeric % 2 ** 8;
finalizedHeaderRootNumeric = finalizedHeaderRootNumeric / 2 ** 8;
}

return inputs;
}
}
31 changes: 5 additions & 26 deletions contracts/src/SyncStepLib.sol
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "forge-std/console.sol";

import { EndianConversions } from "./EndianConversions.sol";

library SyncStepLib {
struct SyncStepInput {
Expand All @@ -13,26 +12,6 @@ library SyncStepLib {
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()
Expand All @@ -42,14 +21,14 @@ library SyncStepLib {
*/
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),
EndianConversions.toLittleEndian64(args.attestedSlot),
EndianConversions.toLittleEndian64(args.finalizedSlot),
EndianConversions.toLittleEndian64(args.participation),
args.finalizedHeaderRoot,
args.executionPayloadRoot,
keysPoseidonCommitment
));
uint256 commitment = uint256(toLittleEndian(uint256(h)));
uint256 commitment = uint256(EndianConversions.toLittleEndian(uint256(h)));
return commitment & ((uint256(1) << 253) - 1); // truncated to 253 bits
}
}
23 changes: 23 additions & 0 deletions contracts/test/RotateExternal.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

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

/**
* @title RotateExternal
* @dev This contract exists solely for the purpose of exposing the RotateLib 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 toPublicInputs(RotateLib.RotateInput calldata args, bytes32 finalizedHeaderRoot) public pure returns (uint256[] memory) {
uint256[65] memory commitment = args.toPublicInputs(finalizedHeaderRoot);
// copy all elements into a dynamic array. We need to do this because ethers-rs has a bug that can't support uint256[65] return types
uint256[] memory result = new uint256[](65);
for (uint256 i = 0; i < commitment.length; i++) {
result[i] = commitment[i];
}
return result;
}
}
2 changes: 1 addition & 1 deletion contracts/test/SyncStepExternal.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ pragma solidity 0.8.19;
import { SyncStepLib } from "../src/SyncStepLib.sol";

/**
* @title SyncStepLibTest
* @title SyncStepExternal
* @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
*/
Expand Down
11 changes: 8 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,19 @@ 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.reverse();
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 +163,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
115 changes: 109 additions & 6 deletions lightclient-circuits/tests/step.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
#![feature(generic_const_exprs)]

use ark_std::{end_timer, start_timer};
use eth_types::Minimal;
use ethereum_consensus_types::presets::minimal::{LightClientBootstrap, LightClientUpdateCapella};
use ethereum_consensus_types::signing::{compute_domain, DomainType};
use ethereum_consensus_types::{ForkData, Root};
use halo2_base::gates::builder::CircuitBuilderStage;
use halo2_proofs::dev::MockProver;
use halo2curves::bls12_381;
use halo2curves::bn256::{self, Fr};
use itertools::Itertools;
use light_client_verifier::ZiplineUpdateWitnessCapella;
Expand Down Expand Up @@ -544,14 +547,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 =
poseidon_committee_commitment_from_uncompressed(&witness.pubkeys_uncompressed)?;

let anvil_instance = Anvil::new().spawn();
let ethclient: Arc<SignerMiddleware<Provider<Http>, _>> = make_client(&anvil_instance);
Expand All @@ -568,6 +572,48 @@ 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_public_inputs(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!(result_decoded.len(), instance[0].len());
assert_eq!(vec![result_decoded], instance);
Ok(())
}

abigen!(
SyncStepExternal,
"../contracts/out/SyncStepExternal.sol/SyncStepExternal.json"
Expand Down Expand Up @@ -603,11 +649,57 @@ 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
where
[(); Spec::SYNC_COMMITTEE_SIZE]:,
{
fn from(args: CommitteeRotationArgs<Spec, Fr>) -> Self {
let poseidon_commitment_le = poseidon_committee_commitment_from_compressed(
&args
.pubkeys_compressed
.iter()
.cloned()
.map(|mut b| {
b.reverse();
b
})
.collect_vec(),
)
.unwrap();

let mut pk_vector: Vector<Vector<u8, 48>, { Spec::SYNC_COMMITTEE_SIZE }> = 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 poseidon_committee_commitment_from_uncompressed(
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 All @@ -622,6 +714,17 @@ mod solidity_tests {
Ok(poseidon_commitment.to_bytes_le().try_into().unwrap())
}

fn poseidon_committee_commitment_from_compressed(
pubkeys_compressed: &Vec<Vec<u8>>,
) -> anyhow::Result<[u8; 32]> {
let pubkeys_x = pubkeys_compressed.iter().cloned().map(|mut bytes| {
bytes[47] &= 0b00011111;
bls12_381::Fq::from_bytes_le(&bytes)
});
let poseidon_commitment = fq_array_poseidon_native::<bn256::Fr>(pubkeys_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<SignerMiddleware<Provider<Http>, LocalWallet>> {
let provider = Provider::<Http>::try_from(anvil.endpoint())
Expand Down
Loading