Skip to content

Commit

Permalink
Rotation - Adds input encoding in Solidity and compatibility tests (#24)
Browse files Browse the repository at this point in the history
* adds tests for encoding input. Failing to encode public keys

* Fix clearing of bits

* Fix Endieness of pubkeys

* adds using spec for committee size and correctly handling compressed and uncompressed keys

* add draft of encoding. Something weird is going on though

* rotate input encoding tests passing

* refactor endian conversions to own lib

* fix endieness

* ok fixed it for real

* rename rotation input encoding function to toPublicInputs

---------

Co-authored-by: ec2 <[email protected]>
  • Loading branch information
willemolding and ec2 authored Oct 23, 2023
1 parent 2f4bbc7 commit bfab59a
Show file tree
Hide file tree
Showing 8 changed files with 208 additions and 37 deletions.
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)))
.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);
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

0 comments on commit bfab59a

Please sign in to comment.