Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
274 changes: 252 additions & 22 deletions .github/workflows/bench.yml

Large diffs are not rendered by default.

22 changes: 22 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,28 @@ lto = true
codegen-units = 1
debug = true

# Fast-compile variant used by the bench CI for in-workspace bench
# targets — specifically `cargo test -p precompiles --profile bench-fast`.
# `tests/instances/eth_runner/` is excluded from this workspace (see
# `workspace.exclude` above), so its `cargo run --manifest-path …` uses
# the duplicate `[profile.bench-fast]` defined in
# `tests/instances/eth_runner/Cargo.toml`; keep the two in sync.
# Runtime perf of these driver binaries doesn't affect measurements
# (cycle counts come from the RISC-V simulator), so disabling fat LTO
# and parallelizing codegen cuts "Run benchmarks" compile time by a
# large factor. The RISC-V proving binary is yet another workspace at
# `zksync_os/Cargo.toml` and uses its own `[profile.release]` — also
# unaffected.
# NOTE: the literal string `bench-fast` is used as a `grep -q` fallback
# target by `.github/workflows/bench.yml` — if this profile name is
# changed, update the workflow too.
[profile.bench-fast]
inherits = "release"
opt-level = 3
lto = false
codegen-units = 16
debug = false

[patch.crates-io]
#zksync_os_evm_errors = { path = "../zksync-os-interface/crates/evm-errors" }
#zksync_os_interface = { path = "../zksync-os-interface/crates/interface" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,9 @@ where
&initial_state_commitment
);

// // 3. Verify/apply reads and writes
// 3. Verify/apply reads and writes — state-tree merkle commit.
let mut updated_state_commitment = initial_state_commitment;
cycle_marker::wrap!("verify_and_apply_batch", {
cycle_marker::wrap!("state_commitment_update", {
io.update_commitment(
Some(&mut updated_state_commitment),
&mut logger,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,8 @@ where
// Events
result_keeper.events(io.events_iterator());

// // 3. Verify/apply reads and writes
cycle_marker::wrap!("verify_and_apply_batch", {
// 3. Verify/apply reads and writes
cycle_marker::wrap!("state_commitment_update", {
io.update_commitment(None, &mut logger, result_keeper);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,17 +96,20 @@ where
da_commitment_scheme
);
}
write_pubdata(
batch_data
.da_commitment_generator
.as_mut()
.unwrap()
.as_mut(),
result_keeper,
block_hash,
metadata.block_timestamp(),
&mut io,
);
// See `post_tx_op_proving_singleblock_batch.rs` for the rationale.
cycle_marker::wrap!("da_commitment", {
write_pubdata(
batch_data
.da_commitment_generator
.as_mut()
.unwrap()
.as_mut(),
result_keeper,
block_hash,
metadata.block_timestamp(),
&mut io,
);
});

io.logs_storage
.apply_to_array_vec(&mut batch_data.logs_storage);
Expand Down Expand Up @@ -142,8 +145,8 @@ where
last_block_timestamp,
};

// 3. Verify/apply reads and writes
cycle_marker::wrap!("verify_and_apply_batch", {
// 3. Verify/apply reads and writes — state-tree merkle commit.
cycle_marker::wrap!("state_commitment_update", {
IOTeardown::<_>::update_commitment(
&mut io,
Some(&mut state_commitment),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,22 @@ where
let mut da_commitment_generator =
da_commitment_generator_from_scheme(io.da_commitment_scheme.unwrap(), A::default())
.unwrap();
write_pubdata(
da_commitment_generator.as_mut(),
result_keeper,
block_hash,
metadata.block_timestamp(),
&mut io,
);
// For keccak DA (`BlobsAndPubdataKeccak256`), `write_pubdata` streams
// bytes through `Keccak256CommitmentGenerator`, which absorbs them
// into the keccak state — this is where the bulk of keccak
// delegations fire on the DA-commit path. For blob DA
// (`BlobsZKsyncOS`) the same call just appends to a buffer (no
// hashing yet); the actual blob KZG work happens in `.finalize()`
// below and is already captured by the `blob_versioned_hash` marker.
cycle_marker::wrap!("da_commitment", {
write_pubdata(
da_commitment_generator.as_mut(),
result_keeper,
block_hash,
metadata.block_timestamp(),
&mut io,
);
});

let (multichain_root, settlement_layer_chain_id) = read_batch_context_inputs(&mut io);

Expand Down Expand Up @@ -152,8 +161,10 @@ where
chain_state_commitment_before
);

// update state commitment
cycle_marker::wrap!("verify_and_apply_batch", {
// update state commitment — this is the state-tree merkle commit
// (Blake-heavy). Distinct from `da_commitment` (keccak/blob over
// pubdata) and `blob_versioned_hash` (KZG per blob).
cycle_marker::wrap!("state_commitment_update", {
IOTeardown::<_>::update_commitment(
&mut io,
Some(&mut state_commitment),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,15 +73,20 @@ where
result_keeper.logs(io.logs_storage.messages_ref_iter());
result_keeper.events(io.events_storage.events_ref_iter());

write_pubdata(
&mut NopCommitmentGenerator,
result_keeper,
block_hash,
metadata.block_timestamp(),
&mut io,
);
// Sequencing-mode post-op uses NopCommitmentGenerator (no DA work),
// but we still mark `da_commitment` for parity with the proving
// paths so the bench label set is consistent across STFs.
cycle_marker::wrap!("da_commitment", {
write_pubdata(
&mut NopCommitmentGenerator,
result_keeper,
block_hash,
metadata.block_timestamp(),
&mut io,
);
});

cycle_marker::wrap!("verify_and_apply_batch", {
cycle_marker::wrap!("state_commitment_update", {
io.update_commitment(None, &mut logger, result_keeper);
});
Ok(())
Expand Down
153 changes: 144 additions & 9 deletions basic_system/src/system_functions/bls12_381/pairing.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use super::*;
use alloc::vec::Vec;
use crypto::ark_ec::AffineRepr;
use crypto::{ark_ec::pairing::Pairing, bls12_381::curves::Bls12_381};
use zk_ee::{
out_of_return_memory,
Expand Down Expand Up @@ -78,25 +79,159 @@ fn bls12_381_pairing_as_system_function_inner<
.try_into()
.unwrap(),
)?;
// e(O, Q) = e(P, O) = 1 in the target field, so degenerate pairs do not
// affect the multi-pairing product. Skip them after subgroup validation
// to save the per-pair Miller-loop precomputation that dominates the
// cost on Pectra degenerate inputs.
if g1.is_zero() || g2.is_zero() {
continue;
}
g1_points.push(g1);
g2_points.push(g2);
}

let pairing_result = <Bls12_381 as Pairing>::multi_pairing(g1_points, g2_points);
output
.try_extend([0u8; 31])
.map_err(|_| out_of_return_memory!())?;

use crypto::ark_ff::Field;
if pairing_result.0 == <Bls12_381 as Pairing>::TargetField::ONE {
output
.try_extend([1u8])
.map_err(|_| out_of_return_memory!())?;
let success = if g1_points.is_empty() {
// Empty product equals the identity in the target field.
true
} else {
output
.try_extend([0u8])
.map_err(|_| out_of_return_memory!())?;
}
let pairing_result = <Bls12_381 as Pairing>::multi_pairing(g1_points, g2_points);
pairing_result.0 == <Bls12_381 as Pairing>::TargetField::ONE
};

output
.try_extend([success as u8])
.map_err(|_| out_of_return_memory!())?;

Ok(())
}

#[cfg(test)]
mod tests {
use super::*;
use core::ops::Neg;
use crypto::bls12_381::eip2537::{serialize_g1_bytes, serialize_g2_bytes};
use zk_ee::reference_implementations::{BaseResources, DecreasingNative};
use zk_ee::system::Resource;

fn encode_g1(point: G1Affine) -> [u8; G1_SERIALIZATION_LEN] {
let mut buf = [0u8; G1_SERIALIZATION_LEN];
serialize_g1_bytes(point, &mut buf);
buf
}

fn encode_g2(point: G2Affine) -> [u8; G2_SERIALIZATION_LEN] {
let mut buf = [0u8; G2_SERIALIZATION_LEN];
serialize_g2_bytes(point, &mut buf);
buf
}

fn encode_pair(g1: G1Affine, g2: G2Affine) -> [u8; BLS12_381_PAIR_LEN] {
let mut buf = [0u8; BLS12_381_PAIR_LEN];
buf[..G1_SERIALIZATION_LEN].copy_from_slice(&encode_g1(g1));
buf[G1_SERIALIZATION_LEN..].copy_from_slice(&encode_g2(g2));
buf
}

fn run(input: &[u8]) -> Vec<u8> {
let allocator = std::alloc::Global;
let mut resource = <BaseResources<DecreasingNative> as Resource>::FORMAL_INFINITE;
let mut dst: Vec<u8> = Vec::new();
Bls12381PairingCheckPrecompile::execute(input, &mut dst, &mut resource, allocator)
.expect("precompile should succeed on well-formed input");
dst
}

fn expect_check(input: &[u8], expected_true: bool) {
let dst = run(input);
let mut expected = [0u8; 32];
expected[31] = expected_true as u8;
assert_eq!(dst.as_slice(), &expected[..]);
}

#[test]
fn single_pair_both_infinity_returns_true() {
let input = [0u8; BLS12_381_PAIR_LEN];
expect_check(&input, true);
}

#[test]
fn single_pair_g1_infinity_returns_true() {
let mut input = [0u8; BLS12_381_PAIR_LEN];
input[G1_SERIALIZATION_LEN..].copy_from_slice(&encode_g2(G2Affine::generator()));
expect_check(&input, true);
}

#[test]
fn single_pair_g2_infinity_returns_true() {
let mut input = [0u8; BLS12_381_PAIR_LEN];
input[..G1_SERIALIZATION_LEN].copy_from_slice(&encode_g1(G1Affine::generator()));
expect_check(&input, true);
}

#[test]
fn many_infinity_pairs_return_true() {
let input = vec![0u8; 7 * BLS12_381_PAIR_LEN];
expect_check(&input, true);
}

#[test]
fn nontrivial_pair_returns_false_and_infinity_does_not_mask_it() {
// e(G1, G2) is the BLS12-381 generator pairing, which is not 1.
let nontrivial = encode_pair(G1Affine::generator(), G2Affine::generator());
expect_check(&nontrivial, false);

// Appending degenerate pairs must not flip the result to true.
let mut with_inf = nontrivial.to_vec();
with_inf.extend_from_slice(&[0u8; BLS12_381_PAIR_LEN]);
expect_check(&with_inf, false);

let mut prefixed = vec![0u8; BLS12_381_PAIR_LEN];
prefixed.extend_from_slice(&nontrivial);
expect_check(&prefixed, false);
}

#[test]
fn balanced_pair_returns_true_with_or_without_infinity_padding() {
// e(G1, G2) * e(-G1, G2) = e(G1, G2) * e(G1, G2)^{-1} = 1
let g1 = G1Affine::generator();
let g2 = G2Affine::generator();
let balanced_a = encode_pair(g1, g2);
let balanced_b = encode_pair(g1.neg(), g2);

let mut balanced = balanced_a.to_vec();
balanced.extend_from_slice(&balanced_b);
expect_check(&balanced, true);

// Interleaving degenerate pairs must keep the result true.
let mut interleaved = vec![0u8; BLS12_381_PAIR_LEN];
interleaved.extend_from_slice(&balanced_a);
interleaved.extend_from_slice(&[0u8; BLS12_381_PAIR_LEN]);
interleaved.extend_from_slice(&balanced_b);
interleaved.extend_from_slice(&[0u8; BLS12_381_PAIR_LEN]);
expect_check(&interleaved, true);
}

#[test]
fn malformed_nonzero_encoding_is_still_rejected() {
// A G1 input where the y-coordinate is forced to zero with a non-zero x
// is not on the curve and must not be accepted as the point at infinity.
// This guards against any future refactor that filters before parsing.
let mut input = [0u8; BLS12_381_PAIR_LEN];
// x = 1 in big-endian, padded to 48 bytes then to the 64-byte slot.
input[G1_SERIALIZATION_LEN - 1] = 1;
// y stays zero. G2 stays at infinity (irrelevant once G1 parse fails).
let allocator = std::alloc::Global;
let mut resource = <BaseResources<DecreasingNative> as Resource>::FORMAL_INFINITE;
let mut dst: Vec<u8> = Vec::new();
let err =
Bls12381PairingCheckPrecompile::execute(&input, &mut dst, &mut resource, allocator)
.expect_err("invalid G1 encoding must be rejected");
// Sanity: we got an error rather than silently treating it as infinity.
let _ = err;
}
}
Loading
Loading