Skip to content

Commit

Permalink
Step circuit instance encoding solidity (with tests) (#21)
Browse files Browse the repository at this point in the history
* add missing instances in step & rotation circuits

* return all 32 bytes as expected by sha chip

* replace onion hashing with input concat

* off-circuit instance generation for concat input hashing  0c6f3f

* add lib implementing encoding and fix forge building process

* adds tests between rust and solidity implementations driven from rust

* refactor test function

* format the solidity_tests module

* restrict function types to pure

* remove deploy script from this branch

* add changes required from add-missing-instance

* roll back solidity version change

* remove unused deps

* tests passing with version revert and move endian converter helpers to SyncStepLib.sol

* add new just action for building contracts and do this in CI before running tests that use solidity build output

---------

Co-authored-by: Timofey Luin <[email protected]>
  • Loading branch information
willemolding and nulltea authored Oct 10, 2023
1 parent 3c15023 commit 029283d
Show file tree
Hide file tree
Showing 14 changed files with 202 additions and 41 deletions.
6 changes: 6 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "contracts/lib/forge-std"]
path = contracts/lib/forge-std
url = https://github.com/foundry-rs/forge-std
3 changes: 0 additions & 3 deletions contracts/.gitmodules

This file was deleted.

2 changes: 1 addition & 1 deletion contracts/remappings.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
ds-test/=lib/forge-std/lib/ds-test/src/
forge-std/=lib/forge-std/src/
forge-std/=lib/forge-std/src/
5 changes: 4 additions & 1 deletion contracts/src/Spectre.sol
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down
55 changes: 55 additions & 0 deletions contracts/src/SyncStepLib.sol
Original file line number Diff line number Diff line change
@@ -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
}
}
33 changes: 0 additions & 33 deletions contracts/test/SpectreSyncStep.sol

This file was deleted.

17 changes: 17 additions & 0 deletions contracts/test/SyncStepExternal.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 { 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);
}
}
3 changes: 3 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions lightclient-circuits/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ rstest = "0.18.2"
test-utils = { git = "ssh://[email protected]/sygmaprotocol/Zipline.git", rev = "27e8a01" }
ethereum-consensus-types = { git = "ssh://[email protected]/sygmaprotocol/Zipline.git", rev = "27e8a01" }
light-client-verifier = { git = "ssh://[email protected]/sygmaprotocol/Zipline.git", rev = "27e8a01" }
ethers = "2.0.10"
tokio = { version = "1.32.0", features = ["rt", "macros"] }
anyhow = "1.0.75"

[features]
default = []
2 changes: 1 addition & 1 deletion lightclient-circuits/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
1 change: 0 additions & 1 deletion lightclient-circuits/src/sync_step_circuit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,6 @@ impl<S: Spec, F: Field> SyncStepCircuit<S, F> {
false,
)?
.output_bytes;

let pi_commit = truncate_sha256_into_single_elem(thread_pool.main(), range, pi_hash_bytes);

Ok(vec![pi_commit])
Expand Down
108 changes: 108 additions & 0 deletions lightclient-circuits/tests/step.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<Minimal, bn256::Fr>::instance_commitment(&witness);
let poseidon_commitment_le = extract_poseidon_committee_commitment(&witness)?;

let anvil_instance = Anvil::new().spawn();
let ethclient: Arc<SignerMiddleware<Provider<Http>, _>> = 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<Spec: eth_types::Spec> From<SyncStepArgs<Spec>> for SyncStepInput {
fn from(args: SyncStepArgs<Spec>) -> Self {
let participation = args
.pariticipation_bits
.iter()
.map(|v| *v as u64)
.sum::<u64>();

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<Spec: eth_types::Spec>(
witness: &SyncStepArgs<Spec>,
) -> 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::<bn256::Fr>(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<SignerMiddleware<Provider<Http>, LocalWallet>> {
let provider = Provider::<Http>::try_from(anvil.endpoint())
.unwrap()
.interval(std::time::Duration::from_millis(10u64));
let wallet: LocalWallet = anvil.keys()[0].clone().into();

let client: SignerMiddleware<Provider<Http>, _> =
SignerMiddleware::new(provider, wallet.with_chain_id(anvil.chain_id()));
Arc::new(client)
}
}

0 comments on commit 029283d

Please sign in to comment.