Skip to content
Open
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
22 changes: 22 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ members = [
"crates/utils/sov-rest-utils",
"crates/utils/rest-api-load-testing",
"crates/utils/nearly-linear",
"crates/utils/sov-gas-tools",
"crates/utils/sov-zkvm-utils",
"crates/utils/sov-build",
"crates/utils/sov-rpc-eth-types",
Expand Down Expand Up @@ -94,6 +95,7 @@ members = [
"examples/demo-rollup/tests/prover/sov-aggregated-proof",
# Bench
"crates/bench/sp1-microbenches",
"crates/bench/native-gas-microbenches",
# Adapters
# TODO: https://github.com/Sovereign-Labs/sovereign-sdk-wip/issues/498
# "crates/adapters/avail",
Expand Down Expand Up @@ -134,6 +136,7 @@ float_arithmetic = "deny" # Can cause subtle bugs due to different
#doc_markdown = "warn"

[workspace.dependencies]
sov-gas-tools = { path = "crates/utils/sov-gas-tools", default-features = false, version = "0.3" }
demo-stf = { path = "examples/demo-rollup/stf", default-features = false, version = "0.3" }
demo-stf-declaration = { path = "examples/demo-rollup/stf/stf-declaration", default-features = false, version = "0.3" }
sov-soak-testing-lib = { path = "crates/utils/sov-soak-testing-lib", default-features = false }
Expand Down
24 changes: 24 additions & 0 deletions crates/bench/native-gas-microbenches/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[package]
name = "native-gas-microbenches"
description = "Native (non-zkVM) wall-clock microbenches for calibrating gas constants"
authors = { workspace = true }
edition = { workspace = true }
homepage = { workspace = true }
license = { workspace = true }
repository = { workspace = true }
version = { workspace = true }
publish = false

[lints]
workspace = true

[dev-dependencies]
criterion = "0.5.1"
sov-gas-tools = { workspace = true }
sov-modules-api = { workspace = true, features = ["native"] }
sov-mock-da = { workspace = true, features = ["native"] }
sov-mock-zkvm = { workspace = true, features = ["native"] }

[[bench]]
name = "sha256"
harness = false
56 changes: 56 additions & 0 deletions crates/bench/native-gas-microbenches/benches/sha256.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//! Native calibration of sha256 gas constants.
//!
//! Mirrors the SP1 `sha256` microbench's call site (`MeteredHasher::digest`) and
//! input sweep, but measures native wall-clock instead of prover gas. Fits
//! `ns = bias + per_byte * size`:
//! - bias -> GAS_TO_CHARGE_HASH_UPDATE
//! - per_byte -> GAS_TO_CHARGE_PER_BYTE_HASH_UPDATE

use std::hint::black_box;

use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
use sov_gas_tools::report::report_size_sweep;
use sov_mock_da::MockDaSpec;
use sov_mock_zkvm::MockZkvm;
use sov_modules_api::default_spec::DefaultSpec;
use sov_modules_api::execution_mode::Native;
use sov_modules_api::{CryptoSpec, MeteredHasher, Spec, UnlimitedGasMeter};

type MicrobenchSpec = DefaultSpec<MockDaSpec, MockZkvm, MockZkvm, Native>;
type Hasher = <<MicrobenchSpec as Spec>::CryptoSpec as CryptoSpec>::Hasher;

const GROUP: &str = "sha256";
// Mirror the SP1 sha256 sweep so native and ZK calibrations are comparable.
const SIZES: &[u64] = &[0, 1, 32, 64, 128, 256, 512, 1024, 4096, 16384, 65536];

fn bench_sha256(c: &mut Criterion) {
let mut group = c.benchmark_group(GROUP);
for &size in SIZES {
let buf: Vec<u8> = (0..size).map(|i| (i as u8).wrapping_mul(0xAB)).collect();
group.bench_with_input(BenchmarkId::from_parameter(size), &buf, |b, buf| {
let mut meter = UnlimitedGasMeter::<MicrobenchSpec>::default();
b.iter(|| {
black_box(
MeteredHasher::<UnlimitedGasMeter<MicrobenchSpec>, Hasher>::digest(
black_box(buf.as_slice()),
&mut meter,
)
.expect("UnlimitedGasMeter never errors"),
)
});
});
}
group.finish();

if let Err(e) = report_size_sweep(
GROUP,
SIZES,
"GAS_TO_CHARGE_HASH_UPDATE",
"GAS_TO_CHARGE_PER_BYTE_HASH_UPDATE",
) {
eprintln!("warn: could not compute fit / constants suggestion: {e}");
}
}

criterion_group!(benches, bench_sha256);
criterion_main!(benches);
1 change: 1 addition & 0 deletions crates/bench/native-gas-microbenches/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
//! Native (non-zkVM) wall-clock microbenches for calibrating gas constants.
1 change: 1 addition & 0 deletions crates/bench/sp1-microbenches/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ path = "src/main.rs"
[dependencies]
anyhow = { workspace = true }
clap = { workspace = true }
sov-gas-tools = { workspace = true }
serde = { workspace = true, features = ["derive"] }
sov-rollup-interface = { workspace = true, features = ["native"] }
sov-sp1-adapter = { workspace = true, features = ["native"] }
Expand Down
2 changes: 1 addition & 1 deletion crates/bench/sp1-microbenches/src/cmd/ed25519.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use sov_rollup_interface::crypto::PrivateKey;
use sov_sp1_adapter::crypto::private_key::SP1PrivateKey;
use sp1_sdk::blocking::{Prover, ProverClient, SP1Stdin};

use crate::fit::fit_prover_gas_per_byte;
use crate::fit_prover_gas_per_byte;
use crate::{load_guest_elf, BenchResult};

const GUEST_ELF_PATH: &str = concat!(
Expand Down
2 changes: 1 addition & 1 deletion crates/bench/sp1-microbenches/src/cmd/sha256.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use anyhow::Context;
use clap::Args;
use sp1_sdk::blocking::{Prover, ProverClient, SP1Stdin};

use crate::fit::fit_prover_gas_per_byte;
use crate::fit_prover_gas_per_byte;
use crate::{load_guest_elf, BenchResult};

const GUEST_ELF_PATH: &str = concat!(
Expand Down
10 changes: 9 additions & 1 deletion crates/bench/sp1-microbenches/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
#![allow(clippy::float_arithmetic)]

pub mod cmd;
pub mod fit;

use anyhow::Context;
use serde::{Deserialize, Serialize};
use sp1_sdk::blocking::Elf;

pub use sov_gas_tools::fit::LinearFit;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BenchResult {
pub input_size: u32,
Expand All @@ -29,6 +30,13 @@ impl BenchResult {
}
}

/// Fit `prover_gas_per_call = bias + per_byte * input_size` over the bench results.
pub fn fit_prover_gas_per_byte(results: &[BenchResult]) -> anyhow::Result<LinearFit> {
let input_sizes: Vec<f64> = results.iter().map(|r| r.input_size as f64).collect();
let prover_gas: Vec<f64> = results.iter().map(|r| r.per_iter_prover_gas()).collect();
Ok(sov_gas_tools::fit::fit_linear(&input_sizes, &prover_gas)?)
}

pub fn load_guest_elf(path: &str) -> anyhow::Result<Elf> {
let bytes = std::fs::read(path)
.with_context(|| format!("guest ELF not found at {path}; did the build script run?"))?;
Expand Down
19 changes: 19 additions & 0 deletions crates/utils/sov-gas-tools/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[package]
name = "sov-gas-tools"
description = "Utilities for calibrating and pricing gas constants: OLS cost-model fitting and native-microbench reporting helpers."
authors = { workspace = true }
edition = { workspace = true }
homepage = { workspace = true }
license = { workspace = true }
repository = { workspace = true }
version = { workspace = true }
publish = false

[lints]
workspace = true

[dependencies]
anyhow = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
Original file line number Diff line number Diff line change
@@ -1,52 +1,60 @@
//! Ordinary least squares fit of the cost model `cost = bias + per_byte * input_size`.
//! Ordinary-least-squares fit of the gas cost model `cost = bias + per_byte * size`.
//!
//! Host-only post-processing of benchmark results — never compiled into the zkVM guest, so
//! the workspace `clippy::float_arithmetic` deny (which exists to prevent native/zkVM
//! divergence) doesn't apply here.

#![allow(clippy::float_arithmetic)]

use crate::BenchResult;
//! Independent of how `cost` was measured: the SP1 microbenches feed prover gas,
//! the native microbenches feed wall-clock ns, and downstream benches feed
//! whatever their gas basis is. Returns the per-call fixed overhead (`bias`, the
//! intercept) and per-byte marginal cost (`per_byte`, the slope), plus
//! `r_squared` / `max_residual` so callers can judge fit quality.

/// Why a [`fit_linear`] call could not produce a fit.
#[derive(Debug, thiserror::Error)]
pub enum FitError {
/// The two input slices had different lengths, so points can't be paired up.
#[error("inputs ({inputs}) and measurements ({measurements}) must have equal length")]
LengthMismatch {
/// Number of input (x) values supplied.
inputs: usize,
/// Number of measurement (y) values supplied.
measurements: usize,
},
/// Fewer than two points were supplied; a line needs at least two.
#[error("need at least 2 data points to fit a line, got {0}")]
TooFewPoints(usize),
/// All input (x) values are identical, so the slope is undefined.
#[error("cannot fit: all input values are identical")]
IdenticalInputs,
}

#[derive(Debug, Clone)]
pub struct LinearFit {
/// Intercept of the fitted line: estimated per-call fixed overhead (cost at input_size = 0).
/// Intercept of the fitted line: estimated per-call fixed overhead (cost at size = 0).
pub bias: f64,
/// Slope of the fitted line: estimated marginal cost per byte of input.
pub per_byte: f64,
/// Coefficient of determination; 1.0 means the line explains the data perfectly, 0.0 means it explains none of the variation.
/// Coefficient of determination; 1.0 means the line explains the data perfectly, 0.0 means none of the variation.
pub r_squared: f64,
/// Largest absolute deviation between any measured point and the fitted line, in the cost's units.
pub max_residual: f64,
}

/// Fit `prover_gas_per_call = bias + per_byte * input_size` over the bench results.
pub fn fit_prover_gas_per_byte(results: &[BenchResult]) -> anyhow::Result<LinearFit> {
let input_sizes: Vec<f64> = results.iter().map(|r| r.input_size as f64).collect();
let prover_gas: Vec<f64> = results.iter().map(|r| r.per_iter_prover_gas()).collect();
fit_linear(&input_sizes, &prover_gas)
}

/// Given a list of `(input_size, measured_cost)` points, find the straight line that best
/// describes their relationship.
///
/// We use this to extract the two numbers our gas model needs: a per-call fixed overhead
/// (the line's intercept, surfaced as `bias`) and a per-byte marginal cost (the line's slope,
/// surfaced as `per_byte`). Together they let us charge gas with one formula:
/// `cost = bias + per_byte × size`.
/// Given a list of `(input_size, measured_cost)` points, find the straight line
/// that best describes their relationship in the ordinary-least-squares sense
/// (minimise the sum of squared vertical distances), and return its intercept
/// (`bias`), slope (`per_byte`), and fit-quality metrics.
///
/// "Best" means ordinary least squares: pick the line that minimises the sum of the squared
/// vertical distances from each data point to the line.
///
/// Implemented ourselves rather than pulled from `linregress` or `linfa`: single-regressor OLS
/// is a few lines of closed-form arithmetic, we don't need confidence intervals or p-values,
/// and avoiding the dependency keeps `ndarray` and friends out of the build graph.
fn fit_linear(inputs: &[f64], measurements: &[f64]) -> anyhow::Result<LinearFit> {
/// Implemented directly rather than via `linregress`/`linfa`: single-regressor
/// OLS is a few lines of closed-form arithmetic, we don't need confidence
/// intervals or p-values, and avoiding the dependency keeps `ndarray` and
/// friends out of the build graph.
pub fn fit_linear(inputs: &[f64], measurements: &[f64]) -> Result<LinearFit, FitError> {
if inputs.len() != measurements.len() {
return Err(FitError::LengthMismatch {
inputs: inputs.len(),
measurements: measurements.len(),
});
}
if inputs.len() < 2 {
anyhow::bail!(
"need at least 2 data points to fit a line, got {}",
inputs.len()
);
return Err(FitError::TooFewPoints(inputs.len()));
}

let sample_count = inputs.len() as f64;
Expand All @@ -57,7 +65,7 @@ fn fit_linear(inputs: &[f64], measurements: &[f64]) -> anyhow::Result<LinearFit>

let slope_denominator = sample_count * sum_x_squared - sum_x * sum_x;
if slope_denominator.abs() < f64::EPSILON {
anyhow::bail!("cannot fit: all input values are identical");
return Err(FitError::IdenticalInputs);
}
let slope = (sample_count * sum_xy - sum_x * sum_y) / slope_denominator;
let intercept = (sum_y - slope * sum_x) / sample_count;
Expand Down Expand Up @@ -148,4 +156,9 @@ mod tests {
// Zero variance in x — slope is undefined; must bail rather than NaN out.
assert!(fit_linear(&[3.0, 3.0, 3.0], &[1.0, 2.0, 3.0]).is_err());
}

#[test]
fn errors_on_mismatched_lengths() {
assert!(fit_linear(&[1.0, 2.0], &[1.0]).is_err());
}
}
16 changes: 16 additions & 0 deletions crates/utils/sov-gas-tools/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//! Tools for calibrating and pricing gas constants from microbenchmarks.
//!
//! - [`fit`] — ordinary-least-squares fit of the cost model
//! `cost = bias + per_byte * size`, shared by the SP1 (prover-gas) and native
//! (wall-clock) microbenches and reusable by downstream rollup benches.
//! - [`report`] — helpers for the native wall-clock microbenches: reading
//! criterion estimates, the `1 gas = 0.01 ns` conversion, and printing
//! suggested `constants.toml` values.
//!
//! Host-only post-processing — never compiled into a zkVM guest, so the workspace
//! `clippy::float_arithmetic` deny (which guards against native/zkVM divergence)
//! doesn't apply.
#![allow(clippy::float_arithmetic)]

pub mod fit;
pub mod report;
Loading
Loading