diff --git a/.github/workflows/perf.yml b/.github/workflows/perf.yml new file mode 100644 index 00000000..8aba65f9 --- /dev/null +++ b/.github/workflows/perf.yml @@ -0,0 +1,81 @@ +name: Perf vs Charon + +on: + workflow_dispatch: + inputs: + tier: + description: "Benchmark tiers to run (1, 2, 12, or all; tier 3 needs relay connectivity)" + required: false + default: "12" + quick: + description: "Quick mode (reduced sample counts)" + type: boolean + required: false + default: false + schedule: + # Nightly at 03:17 UTC, tiers 1+2 only. + - cron: "17 3 * * *" + +env: + CHARON_VERSION: v1.7.1 + +permissions: + contents: read + +jobs: + perf: + runs-on: ubuntu-latest + timeout-minutes: 90 + steps: + - uses: actions/checkout@v6 + + - name: Checkout charon source + uses: actions/checkout@v6 + with: + repository: ObolNetwork/charon + ref: ${{ env.CHARON_VERSION }} + path: charon + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 # stable + # rust-toolchain.toml pins the actual toolchain; the action just + # bootstraps rustup. + + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 + + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version-file: charon/go.mod + cache-dependency-path: perf/go-bench/go.sum + + - name: Run benchmarks + env: + TIER: ${{ inputs.tier || '12' }} + QUICK: ${{ inputs.quick && '1' || '' }} + run: | + # Gate on regressions once a blessed baseline is committed. + BASELINE="" + if [[ -f perf/baseline.json ]]; then + BASELINE="perf/baseline.json" + fi + ./perf/run.sh --tier "$TIER" ${QUICK:+--quick} \ + ${BASELINE:+--baseline "$BASELINE"} + + - name: Report to job summary + if: always() + run: | + if [[ -f perf/out/report.md ]]; then + cat perf/out/report.md >> "$GITHUB_STEP_SUMMARY" + fi + + - name: Upload results + if: always() + uses: actions/upload-artifact@v4 + with: + name: perf-results + path: | + perf/out/report.md + perf/out/results.json + perf/out/go-bench.txt + if-no-files-found: warn diff --git a/.gitignore b/.gitignore index 304cfad0..b2a8e603 100644 --- a/.gitignore +++ b/.gitignore @@ -32,4 +32,6 @@ test-infra/sszfixtures/sszfixtures .claude/worktrees/ .claude/scheduled_tasks.lock -test-cluster \ No newline at end of file +test-cluster +# Performance harness artifacts +/perf/out/ diff --git a/Cargo.lock b/Cargo.lock index c65d45f6..4209e653 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5307,6 +5307,7 @@ dependencies = [ "cancellation", "chrono", "clap", + "criterion", "crossbeam", "dyn-clone", "dyn-eq", @@ -5347,6 +5348,7 @@ name = "pluto-crypto" version = "1.7.1" dependencies = [ "blst", + "criterion", "hex", "pluto-eth2api", "rand 0.8.6", @@ -5365,6 +5367,7 @@ dependencies = [ "bon", "chrono", "clap", + "criterion", "either", "futures", "hex", @@ -5501,6 +5504,7 @@ name = "pluto-frost" version = "1.7.1" dependencies = [ "blst", + "criterion", "hex", "rand 0.8.6", "rand_core 0.6.4", diff --git a/Cargo.toml b/Cargo.toml index 56dce23d..c4410b88 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -176,6 +176,12 @@ panicking_overflow_checks = "deny" redundant_test_prefix = "deny" unwrap_used = "deny" +# Thin LTO + a single codegen unit measured ~6-12% faster on pure-Rust hot +# paths (SSZ, k1util) with no effect on blst-bound ones; see perf/FINDINGS.md. +[profile.release] +lto = "thin" +codegen-units = 1 + # Optimize crypto dependencies in test builds for faster tests [profile.test.package.scrypt] opt-level = 3 diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 297c259b..6eb6a8d6 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -6,6 +6,14 @@ repository.workspace = true license.workspace = true publish.workspace = true +[lib] +bench = false + +[features] +# Exposes internal modules and in-memory harnesses to the criterion benches. +# Never enable in production builds. +bench-util = [] + [dependencies] alloy.workspace = true backon.workspace = true @@ -44,6 +52,7 @@ url.workspace = true [dev-dependencies] anyhow.workspace = true clap.workspace = true +criterion.workspace = true rand.workspace = true prost.workspace = true prost-types.workspace = true @@ -58,6 +67,21 @@ tokio = { workspace = true, features = ["test-util"] } wiremock.workspace = true tower = { workspace = true, features = ["util"] } +[[bench]] +name = "ssz_codec" +harness = false +path = "benches/ssz_codec.rs" + +[[bench]] +name = "corepb" +harness = false +path = "benches/corepb.rs" + +[[bench]] +name = "qbft" +harness = false +path = "benches/qbft.rs" + [build-dependencies] pluto-build-proto.workspace = true built.workspace = true diff --git a/crates/core/benches/corepb.rs b/crates/core/benches/corepb.rs new file mode 100644 index 00000000..2feb28ac --- /dev/null +++ b/crates/core/benches/corepb.rs @@ -0,0 +1,45 @@ +//! Benchmarks for protobuf (de)serialization of consensus wire messages. +//! +//! Benchmark ids follow the cross-language pair naming used by +//! `perf/pairs.json`; the Go counterparts live in `perf/go-bench/`. The +//! fixture is a round-3 QBFTConsensusMsg with four justifications and a 1KiB +//! value, generated by the Go side. +#![allow(missing_docs)] + +use std::{hint::black_box, time::Duration}; + +use criterion::{Criterion, criterion_group, criterion_main}; +use pluto_core::corepb::v1::consensus::QbftConsensusMsg; +use prost::Message; + +const QBFT_MSG_PB: &[u8] = include_bytes!("../../../perf/fixtures/qbft_consensus_msg.pb"); + +fn bench_qbft_msg(c: &mut Criterion) { + let msg = QbftConsensusMsg::decode(QBFT_MSG_PB).expect("bench setup: decode fixture"); + assert_eq!( + msg.encode_to_vec(), + QBFT_MSG_PB, + "qbft msg fixture round-trip mismatch" + ); + + c.bench_function("tier1/proto/qbft_marshal", |b| { + b.iter(|| black_box(&msg).encode_to_vec()) + }); + + c.bench_function("tier1/proto/qbft_unmarshal", |b| { + b.iter(|| QbftConsensusMsg::decode(black_box(QBFT_MSG_PB)).expect("decode should succeed")) + }); +} + +fn config() -> Criterion { + Criterion::default() + .sample_size(50) + .measurement_time(Duration::from_secs(3)) +} + +criterion_group! { + name = benches; + config = config(); + targets = bench_qbft_msg, +} +criterion_main!(benches); diff --git a/crates/core/benches/qbft.rs b/crates/core/benches/qbft.rs new file mode 100644 index 00000000..376d531f --- /dev/null +++ b/crates/core/benches/qbft.rs @@ -0,0 +1,218 @@ +//! Benchmarks a full in-memory QBFT consensus instance: N processes on OS +//! threads wired with crossbeam channels, happy path, measured from spawn to +//! all processes deciding. +//! +//! Benchmark ids follow the cross-language pair naming used by +//! `perf/pairs.json`; the Go counterpart lives in +//! `perf/go-bench/qbft_bench_test.go` and uses the same topology (i64 values, +//! never-firing round timers, blocking fan-out broadcast, value 42). +#![allow(missing_docs)] +// Bench-only arithmetic on small fixed node counts cannot overflow. +#![allow(clippy::arithmetic_side_effects)] + +use std::{any::Any, sync::Arc, thread, time::Duration}; + +use cancellation::CancellationTokenSource; +use criterion::{Criterion, criterion_group, criterion_main}; +use crossbeam::channel as mpmc; +use pluto_core::qbft::{ + self, BroadcastRequest, Definition, MessageType, Msg, QbftError, QbftLogger, QbftTypes, + SomeMsg, Timer, Transport, +}; + +const INSTANCE: i64 = 1; +const VALUE: i64 = 42; +const FIFO_LIMIT: i64 = 100; + +struct BenchQbft; + +impl QbftTypes for BenchQbft { + type Compare = i64; + type Instance = i64; + type Value = i64; +} + +#[derive(Clone, Debug)] +struct BenchMsg { + msg_type: MessageType, + instance: i64, + source: i64, + round: i64, + value: i64, + prepared_round: i64, + prepared_value: i64, + justify: Vec>, +} + +impl SomeMsg for BenchMsg { + fn type_(&self) -> MessageType { + self.msg_type + } + + fn instance(&self) -> i64 { + self.instance + } + + fn source(&self) -> i64 { + self.source + } + + fn round(&self) -> i64 { + self.round + } + + fn value(&self) -> i64 { + self.value + } + + fn value_source(&self) -> Result { + Ok(self.value) + } + + fn prepared_round(&self) -> i64 { + self.prepared_round + } + + fn prepared_value(&self) -> i64 { + self.prepared_value + } + + fn justification(&self) -> Vec> { + self.justify.clone() + } + + fn as_any(&self) -> &dyn Any { + self + } +} + +/// Runs one happy-path consensus instance with `nodes` processes and returns +/// the elapsed time from spawn to every process deciding. Teardown (cancel + +/// join, dominated by the 50ms cancellation poll interval) is excluded. +fn run_consensus(nodes: i64) -> Duration { + let cts = CancellationTokenSource::new(); + let nodes_usize = usize::try_from(nodes).expect("node count fits usize"); + let (decided_tx, decided_rx) = mpmc::bounded::(nodes_usize); + + let mut senders = Vec::with_capacity(nodes_usize); + let mut receivers = Vec::with_capacity(nodes_usize); + for _ in 0..nodes { + let (tx, rx) = mpmc::bounded::>(1024); + senders.push(tx); + receivers.push(rx); + } + let senders = Arc::new(senders); + + let definition = Arc::new(Definition:: { + is_leader: Box::new(move |req| { + (*req.instance + req.round).rem_euclid(nodes) == req.process + }), + new_timer: Box::new(|_round| Timer { + receive: mpmc::never(), + stop: Box::new(|| {}), + }), + compare: Arc::new(|req| { + req.return_err + .send(Ok(())) + .expect("compare status channel open"); + }), + decide: { + let decided_tx = decided_tx.clone(); + Box::new(move |req| { + let _ = decided_tx.send(*req.value); + }) + }, + logger: QbftLogger { + upon_rule: Box::new(|_| {}), + round_change: Box::new(|_| {}), + unjust: Box::new(|_| {}), + }, + nodes, + fifo_limit: FIFO_LIMIT, + }); + + let mut decide_elapsed = Duration::ZERO; + let start = std::time::Instant::now(); + + thread::scope(|s| { + for process in 1..=nodes { + let receiver = receivers[usize::try_from(process).expect("fits") - 1].clone(); + let senders = Arc::clone(&senders); + let token = cts.token().clone(); + let definition = Arc::clone(&definition); + + let transport = Transport:: { + broadcast: Box::new(move |req: BroadcastRequest<'_, BenchQbft>| { + let msg: Msg = Arc::new(BenchMsg { + msg_type: req.type_, + instance: *req.instance, + source: req.source, + round: req.round, + value: *req.value, + prepared_round: req.prepared_round, + prepared_value: *req.prepared_value, + justify: req.justification.cloned().unwrap_or_default(), + }); + + for tx in senders.iter() { + // Best effort: receivers may be gone after decide. + let _ = tx.send(Arc::clone(&msg)); + } + + Ok(()) + }), + receive: receiver, + }; + + let (value_tx, value_rx) = mpmc::bounded::(1); + value_tx.send(VALUE).expect("populate input value"); + let (source_tx, source_rx) = mpmc::bounded::(1); + source_tx.send(VALUE).expect("populate input value source"); + + s.spawn(move || { + // Returns ContextCanceled after the bench cancels below. + let _ = qbft::run( + &token, + &definition, + &transport, + &INSTANCE, + process, + value_rx, + source_rx, + ); + }); + } + + for _ in 0..nodes { + decided_rx + .recv_timeout(Duration::from_secs(30)) + .expect("all processes decide"); + } + + decide_elapsed = start.elapsed(); + cts.cancel(); + }); + + decide_elapsed +} + +fn bench_decide(c: &mut Criterion) { + for (name, nodes) in [("4of4", 4i64), ("7of10", 10)] { + c.bench_function(&format!("tier2/qbft/decide_{name}"), |b| { + b.iter_custom(|iters| (0..iters).map(|_| run_consensus(nodes)).sum()) + }); + } +} + +fn config() -> Criterion { + Criterion::default() + .sample_size(20) + .measurement_time(Duration::from_secs(5)) +} + +criterion_group! { + name = benches; + config = config(); + targets = bench_decide, +} +criterion_main!(benches); diff --git a/crates/core/benches/ssz_codec.rs b/crates/core/benches/ssz_codec.rs new file mode 100644 index 00000000..a1578235 --- /dev/null +++ b/crates/core/benches/ssz_codec.rs @@ -0,0 +1,77 @@ +//! Benchmarks for SSZ encode/decode/hash of duty payloads. +//! +//! Benchmark ids follow the cross-language pair naming used by +//! `perf/pairs.json`; the Go counterparts live in `perf/go-bench/`. Fixtures +//! are shared binary files generated by the Go side +//! (`WRITE_FIXTURES=1 go test -run TestGenFixtures .` in `perf/go-bench/`); +//! each bench asserts a re-encode round-trip before timing so a workload +//! mismatch fails loudly instead of comparing different work. +#![allow(missing_docs)] + +use std::time::Duration; + +use criterion::{Criterion, criterion_group, criterion_main}; +use pluto_core::ssz_codec::{ + decode_phase0_attestation, decode_versioned_signed_proposal, encode_phase0_attestation, + encode_versioned_signed_proposal, +}; +use std::hint::black_box; +use tree_hash::TreeHash; + +const ATTESTATION_SSZ: &[u8] = include_bytes!("../../../perf/fixtures/phase0_attestation.ssz"); +const PROPOSAL_SSZ: &[u8] = include_bytes!("../../../perf/fixtures/deneb_signed_proposal.ssz"); + +fn bench_attestation(c: &mut Criterion) { + let att = decode_phase0_attestation(ATTESTATION_SSZ).expect("bench setup: decode attestation"); + let encoded = encode_phase0_attestation(&att).expect("bench setup: encode attestation"); + assert_eq!( + encoded, ATTESTATION_SSZ, + "attestation fixture round-trip mismatch" + ); + + c.bench_function("tier1/ssz/att_encode", |b| { + b.iter(|| encode_phase0_attestation(black_box(&att)).expect("encode should succeed")) + }); + + c.bench_function("tier1/ssz/att_decode", |b| { + b.iter(|| { + decode_phase0_attestation(black_box(ATTESTATION_SSZ)).expect("decode should succeed") + }) + }); + + c.bench_function("tier1/ssz/att_hash_root", |b| { + b.iter(|| black_box(&att).tree_hash_root()) + }); +} + +fn bench_proposal(c: &mut Criterion) { + let proposal = + decode_versioned_signed_proposal(PROPOSAL_SSZ).expect("bench setup: decode proposal"); + let encoded = + encode_versioned_signed_proposal(&proposal).expect("bench setup: encode proposal"); + assert_eq!( + encoded, PROPOSAL_SSZ, + "proposal fixture round-trip mismatch" + ); + + c.bench_function("tier1/ssz/proposal_encode", |b| { + b.iter(|| encode_versioned_signed_proposal(black_box(&proposal)).expect("encode")) + }); + + c.bench_function("tier1/ssz/proposal_decode", |b| { + b.iter(|| decode_versioned_signed_proposal(black_box(PROPOSAL_SSZ)).expect("decode")) + }); +} + +fn config() -> Criterion { + Criterion::default() + .sample_size(30) + .measurement_time(Duration::from_secs(3)) +} + +criterion_group! { + name = benches; + config = config(); + targets = bench_attestation, bench_proposal, +} +criterion_main!(benches); diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 2e07940d..b8850056 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -67,7 +67,12 @@ mod parsigex_codec; // SSZ codec operates on compile-time-constant byte sizes and offsets. // Arithmetic is bounded and casts from `usize` to `u32` are safe because all // sizes are well below `u32::MAX`. +/// SSZ codec for duty payloads (public only for the criterion benches). #[allow(clippy::arithmetic_side_effects, clippy::cast_possible_truncation)] +#[cfg(feature = "bench-util")] +pub mod ssz_codec; +#[allow(clippy::arithmetic_side_effects, clippy::cast_possible_truncation)] +#[cfg(not(feature = "bench-util"))] pub(crate) mod ssz_codec; pub use parsigex_codec::ParSigExCodecError; diff --git a/crates/core/src/ssz_codec.rs b/crates/core/src/ssz_codec.rs index 9678782f..3be354a5 100644 --- a/crates/core/src/ssz_codec.rs +++ b/crates/core/src/ssz_codec.rs @@ -74,6 +74,20 @@ fn require(bytes: &[u8], need: usize) -> Result<(), SszCodecError> { } } +/// Encodes an SSZ value into a `Vec` pre-sized to its exact serialized length, +/// avoiding the incremental reallocation of `as_ssz_bytes`. +fn ssz_to_vec(value: &T) -> Vec { + let mut buf = Vec::with_capacity(value.ssz_bytes_len()); + value.ssz_append(&mut buf); + buf +} + +/// Appends an SSZ value to `buf`, reserving its exact serialized length first. +fn append_ssz(buf: &mut Vec, value: &T) { + buf.reserve(value.ssz_bytes_len()); + value.ssz_append(buf); +} + // =========================================================================== // Non-versioned SSZ-capable types // =========================================================================== @@ -83,7 +97,7 @@ fn require(bytes: &[u8], need: usize) -> Result<(), SszCodecError> { /// Encodes a `phase0::Attestation` to SSZ binary. pub fn encode_phase0_attestation(att: &phase0::Attestation) -> Result, SszCodecError> { - Ok(att.as_ssz_bytes()) + Ok(ssz_to_vec(att)) } /// Decodes a `phase0::Attestation` from SSZ binary. @@ -95,7 +109,7 @@ pub fn decode_phase0_attestation(bytes: &[u8]) -> Result Result, SszCodecError> { - Ok(sap.as_ssz_bytes()) + Ok(ssz_to_vec(sap)) } /// Decodes a `phase0::SignedAggregateAndProof` from SSZ binary. @@ -109,7 +123,7 @@ pub fn decode_phase0_signed_aggregate_and_proof( pub fn encode_sync_committee_message( msg: &altair::SyncCommitteeMessage, ) -> Result, SszCodecError> { - Ok(msg.as_ssz_bytes()) + Ok(ssz_to_vec(msg)) } /// Decodes an `altair::SyncCommitteeMessage` from SSZ binary. @@ -124,7 +138,7 @@ pub fn decode_sync_committee_message( pub fn encode_contribution_and_proof( cap: &altair::ContributionAndProof, ) -> Result, SszCodecError> { - Ok(cap.as_ssz_bytes()) + Ok(ssz_to_vec(cap)) } /// Decodes an `altair::ContributionAndProof` from SSZ binary. @@ -139,7 +153,7 @@ pub fn decode_contribution_and_proof( pub fn encode_signed_contribution_and_proof( scp: &altair::SignedContributionAndProof, ) -> Result, SszCodecError> { - Ok(scp.as_ssz_bytes()) + Ok(ssz_to_vec(scp)) } /// Decodes an `altair::SignedContributionAndProof` from SSZ binary. @@ -211,23 +225,22 @@ pub fn encode_versioned_attestation( va: &versioned::VersionedAttestation, ) -> Result, SszCodecError> { let version = encode_version(va.version)?; - let inner = encode_attestation_payload(va.attestation.as_ref())?; - if let Some(val_idx) = va.validator_index { - let mut buf = - Vec::with_capacity(VERSIONED_ATTESTATION_VAL_IDX_HEADER as usize + inner.len()); + let mut buf = if let Some(val_idx) = va.validator_index { + let mut buf = Vec::with_capacity(VERSIONED_ATTESTATION_VAL_IDX_HEADER as usize); buf.extend_from_slice(&version); buf.extend_from_slice(&encode_u64(val_idx)); buf.extend_from_slice(&encode_u32(VERSIONED_ATTESTATION_VAL_IDX_HEADER)); - buf.extend_from_slice(&inner); - Ok(buf) + buf } else { - let mut buf = Vec::with_capacity(VERSIONED_SIGNED_AGGREGATE_HEADER as usize + inner.len()); + let mut buf = Vec::with_capacity(VERSIONED_SIGNED_AGGREGATE_HEADER as usize); buf.extend_from_slice(&version); buf.extend_from_slice(&encode_u32(VERSIONED_SIGNED_AGGREGATE_HEADER)); - buf.extend_from_slice(&inner); - Ok(buf) - } + buf + }; + + append_attestation_payload(&mut buf, va.attestation.as_ref())?; + Ok(buf) } /// Decodes a `VersionedAttestation` from SSZ binary with Charon versioned @@ -287,9 +300,10 @@ fn decode_versioned_attestation_no_val_idx( }) } -fn encode_attestation_payload( +fn append_attestation_payload( + buf: &mut Vec, attestation: Option<&AttestationPayload>, -) -> Result, SszCodecError> { +) -> Result<(), SszCodecError> { match attestation { Some( AttestationPayload::Phase0(att) @@ -297,14 +311,18 @@ fn encode_attestation_payload( | AttestationPayload::Bellatrix(att) | AttestationPayload::Capella(att) | AttestationPayload::Deneb(att), - ) => Ok(att.as_ssz_bytes()), + ) => append_ssz(buf, att), Some(AttestationPayload::Electra(att) | AttestationPayload::Fulu(att)) => { - Ok(att.as_ssz_bytes()) + append_ssz(buf, att) + } + None => { + return Err(SszCodecError::Decode( + "missing attestation payload".to_string(), + )); } - None => Err(SszCodecError::Decode( - "missing attestation payload".to_string(), - )), } + + Ok(()) } fn decode_attestation_payload( @@ -350,21 +368,22 @@ pub fn encode_versioned_signed_aggregate_and_proof( va: &versioned::VersionedSignedAggregateAndProof, ) -> Result, SszCodecError> { let version = encode_version(va.version)?; - let inner = match &va.aggregate_and_proof { + + let mut buf = Vec::with_capacity(VERSIONED_SIGNED_AGGREGATE_HEADER as usize); + buf.extend_from_slice(&version); + buf.extend_from_slice(&encode_u32(VERSIONED_SIGNED_AGGREGATE_HEADER)); + + match &va.aggregate_and_proof { SignedAggregateAndProofPayload::Phase0(p) | SignedAggregateAndProofPayload::Altair(p) | SignedAggregateAndProofPayload::Bellatrix(p) | SignedAggregateAndProofPayload::Capella(p) - | SignedAggregateAndProofPayload::Deneb(p) => p.as_ssz_bytes(), + | SignedAggregateAndProofPayload::Deneb(p) => append_ssz(&mut buf, p), SignedAggregateAndProofPayload::Electra(p) | SignedAggregateAndProofPayload::Fulu(p) => { - p.as_ssz_bytes() + append_ssz(&mut buf, p) } - }; + } - let mut buf = Vec::with_capacity(VERSIONED_SIGNED_AGGREGATE_HEADER as usize + inner.len()); - buf.extend_from_slice(&version); - buf.extend_from_slice(&encode_u32(VERSIONED_SIGNED_AGGREGATE_HEADER)); - buf.extend_from_slice(&inner); Ok(buf) } @@ -429,13 +448,12 @@ pub fn encode_versioned_signed_proposal( ) -> Result, SszCodecError> { let version = encode_version(vp.version)?; let blinded: u8 = u8::from(vp.blinded); - let inner = encode_proposal_block(&vp.block)?; - let mut buf = Vec::with_capacity(VERSIONED_SIGNED_PROPOSAL_HEADER as usize + inner.len()); + let mut buf = Vec::with_capacity(VERSIONED_SIGNED_PROPOSAL_HEADER as usize); buf.extend_from_slice(&version); buf.push(blinded); buf.extend_from_slice(&encode_u32(VERSIONED_SIGNED_PROPOSAL_HEADER)); - buf.extend_from_slice(&inner); + append_proposal_block(&mut buf, &vp.block); Ok(buf) } @@ -465,22 +483,22 @@ pub fn decode_versioned_signed_proposal( }) } -fn encode_proposal_block(block: &versioned::SignedProposalBlock) -> Result, SszCodecError> { +fn append_proposal_block(buf: &mut Vec, block: &versioned::SignedProposalBlock) { use versioned::SignedProposalBlock; - Ok(match block { - SignedProposalBlock::Phase0(b) => b.as_ssz_bytes(), - SignedProposalBlock::Altair(b) => b.as_ssz_bytes(), - SignedProposalBlock::Bellatrix(b) => b.as_ssz_bytes(), - SignedProposalBlock::BellatrixBlinded(b) => b.as_ssz_bytes(), - SignedProposalBlock::Capella(b) => b.as_ssz_bytes(), - SignedProposalBlock::CapellaBlinded(b) => b.as_ssz_bytes(), - SignedProposalBlock::Deneb(b) => b.as_ssz_bytes(), - SignedProposalBlock::DenebBlinded(b) => b.as_ssz_bytes(), - SignedProposalBlock::Electra(b) => b.as_ssz_bytes(), - SignedProposalBlock::ElectraBlinded(b) => b.as_ssz_bytes(), - SignedProposalBlock::Fulu(b) => b.as_ssz_bytes(), - SignedProposalBlock::FuluBlinded(b) => b.as_ssz_bytes(), - }) + match block { + SignedProposalBlock::Phase0(b) => append_ssz(buf, b), + SignedProposalBlock::Altair(b) => append_ssz(buf, b), + SignedProposalBlock::Bellatrix(b) => append_ssz(buf, b), + SignedProposalBlock::BellatrixBlinded(b) => append_ssz(buf, b), + SignedProposalBlock::Capella(b) => append_ssz(buf, b), + SignedProposalBlock::CapellaBlinded(b) => append_ssz(buf, b), + SignedProposalBlock::Deneb(b) => append_ssz(buf, b), + SignedProposalBlock::DenebBlinded(b) => append_ssz(buf, b), + SignedProposalBlock::Electra(b) => append_ssz(buf, b), + SignedProposalBlock::ElectraBlinded(b) => append_ssz(buf, b), + SignedProposalBlock::Fulu(b) => append_ssz(buf, b), + SignedProposalBlock::FuluBlinded(b) => append_ssz(buf, b), + } } /// Decodes a bare per-fork full (non-blinded) signed proposal block body from @@ -612,53 +630,76 @@ struct ElectraBlockContents { blobs: Vec, } -/// Encodes the SSZ body of an unsigned proposal block (no versioned header), +/// Encode-side view of [`DenebBlockContents`] that borrows its fields, so +/// serializing a proposal does not deep-clone the block and blobs. Field +/// order must stay identical to the owned struct or the wire format silently +/// diverges. +#[derive(ssz_derive::Encode)] +struct DenebBlockContentsRef<'a> { + block: &'a deneb::BeaconBlock, + kzg_proofs: &'a Vec, + blobs: &'a Vec, +} + +/// Encode-side view of [`ElectraBlockContents`] that borrows its fields. +#[derive(ssz_derive::Encode)] +struct ElectraBlockContentsRef<'a> { + block: &'a electra::BeaconBlock, + kzg_proofs: &'a Vec, + blobs: &'a Vec, +} + +/// Appends the SSZ body of an unsigned proposal block (no versioned header), /// selecting the per-fork layout from the [`UnsignedProposalBlock`] variant. -fn encode_unsigned_proposal_block( - block: &crate::signeddata::ProposalBlock, -) -> Result, SszCodecError> { +fn append_unsigned_proposal_block(buf: &mut Vec, block: &crate::signeddata::ProposalBlock) { use crate::signeddata::ProposalBlock; - Ok(match block { - ProposalBlock::Phase0(b) => b.as_ssz_bytes(), - ProposalBlock::Altair(b) => b.as_ssz_bytes(), - ProposalBlock::Bellatrix(b) => b.as_ssz_bytes(), - ProposalBlock::BellatrixBlinded(b) => b.as_ssz_bytes(), - ProposalBlock::Capella(b) => b.as_ssz_bytes(), - ProposalBlock::CapellaBlinded(b) => b.as_ssz_bytes(), + match block { + ProposalBlock::Phase0(b) => append_ssz(buf, b), + ProposalBlock::Altair(b) => append_ssz(buf, b), + ProposalBlock::Bellatrix(b) => append_ssz(buf, b), + ProposalBlock::BellatrixBlinded(b) => append_ssz(buf, b), + ProposalBlock::Capella(b) => append_ssz(buf, b), + ProposalBlock::CapellaBlinded(b) => append_ssz(buf, b), ProposalBlock::Deneb { block, kzg_proofs, blobs, - } => DenebBlockContents { - block: (**block).clone(), - kzg_proofs: kzg_proofs.clone(), - blobs: blobs.clone(), - } - .as_ssz_bytes(), - ProposalBlock::DenebBlinded(b) => b.as_ssz_bytes(), + } => append_ssz( + buf, + &DenebBlockContentsRef { + block, + kzg_proofs, + blobs, + }, + ), + ProposalBlock::DenebBlinded(b) => append_ssz(buf, b), ProposalBlock::Electra { block, kzg_proofs, blobs, - } => ElectraBlockContents { - block: (**block).clone(), - kzg_proofs: kzg_proofs.clone(), - blobs: blobs.clone(), - } - .as_ssz_bytes(), - ProposalBlock::ElectraBlinded(b) => b.as_ssz_bytes(), + } => append_ssz( + buf, + &ElectraBlockContentsRef { + block, + kzg_proofs, + blobs, + }, + ), + ProposalBlock::ElectraBlinded(b) => append_ssz(buf, b), ProposalBlock::Fulu { block, kzg_proofs, blobs, - } => ElectraBlockContents { - block: (**block).clone(), - kzg_proofs: kzg_proofs.clone(), - blobs: blobs.clone(), - } - .as_ssz_bytes(), - ProposalBlock::FuluBlinded(b) => b.as_ssz_bytes(), - }) + } => append_ssz( + buf, + &ElectraBlockContentsRef { + block, + kzg_proofs, + blobs, + }, + ), + ProposalBlock::FuluBlinded(b) => append_ssz(buf, b), + } } /// Decodes the SSZ body of an unsigned proposal block (no versioned header), @@ -740,13 +781,12 @@ pub fn encode_versioned_proposal( ) -> Result, SszCodecError> { let version = encode_version(vp.version())?; let blinded: u8 = u8::from(vp.is_blinded()); - let inner = encode_unsigned_proposal_block(&vp.block)?; - let mut buf = Vec::with_capacity(VERSIONED_SIGNED_PROPOSAL_HEADER as usize + inner.len()); + let mut buf = Vec::with_capacity(VERSIONED_SIGNED_PROPOSAL_HEADER as usize); buf.extend_from_slice(&version); buf.push(blinded); buf.extend_from_slice(&encode_u32(VERSIONED_SIGNED_PROPOSAL_HEADER)); - buf.extend_from_slice(&inner); + append_unsigned_proposal_block(&mut buf, &vp.block); Ok(buf) } @@ -788,12 +828,11 @@ pub fn encode_versioned_aggregated_attestation( va: &crate::signeddata::VersionedAggregatedAttestation, ) -> Result, SszCodecError> { let version = encode_version(va.0.version)?; - let inner = encode_attestation_payload(va.0.attestation.as_ref())?; - let mut buf = Vec::with_capacity(VERSIONED_SIGNED_AGGREGATE_HEADER as usize + inner.len()); + let mut buf = Vec::with_capacity(VERSIONED_SIGNED_AGGREGATE_HEADER as usize); buf.extend_from_slice(&version); buf.extend_from_slice(&encode_u32(VERSIONED_SIGNED_AGGREGATE_HEADER)); - buf.extend_from_slice(&inner); + append_attestation_payload(&mut buf, va.0.attestation.as_ref())?; Ok(buf) } @@ -832,7 +871,7 @@ pub fn decode_versioned_aggregated_attestation( pub fn encode_sync_contribution( sc: &crate::signeddata::SyncContribution, ) -> Result, SszCodecError> { - Ok(sc.0.as_ssz_bytes()) + Ok(ssz_to_vec(&sc.0)) } /// Decodes an unsigned diff --git a/crates/crypto/Cargo.toml b/crates/crypto/Cargo.toml index f9929e9b..17238381 100644 --- a/crates/crypto/Cargo.toml +++ b/crates/crypto/Cargo.toml @@ -6,6 +6,9 @@ repository.workspace = true license.workspace = true publish.workspace = true +[lib] +bench = false + [dependencies] blst.workspace = true hex.workspace = true @@ -32,4 +35,10 @@ panicking_overflow_checks = "deny" unwrap_used = "deny" [dev-dependencies] +criterion.workspace = true test-case.workspace = true + +[[bench]] +name = "tbls" +harness = false +path = "benches/tbls.rs" diff --git a/crates/crypto/benches/tbls.rs b/crates/crypto/benches/tbls.rs new file mode 100644 index 00000000..4fa3e02f --- /dev/null +++ b/crates/crypto/benches/tbls.rs @@ -0,0 +1,165 @@ +//! Benchmarks for the BLS threshold-signature implementation. +//! +//! Benchmark ids follow the cross-language pair naming used by +//! `perf/pairs.json`; the Go counterparts live in `perf/go-bench/`. +#![allow(missing_docs)] + +use std::{collections::HashMap, hint::black_box, time::Duration}; + +use criterion::{Criterion, criterion_group, criterion_main}; +use pluto_crypto::{ + blst_impl::BlstImpl, + tbls::Tbls, + types::{Index, PrivateKey, PublicKey, Signature}, +}; +use rand::rngs::OsRng; + +/// Must stay identical to the message in `perf/go-bench/tbls_bench_test.go` +/// so both sides time the same workload. +const MSG: [u8; 32] = [0u8; 32]; + +const SPLIT_CASES: [(&str, Index, Index); 2] = [("3of4", 4, 3), ("7of10", 10, 7)]; + +fn new_secret() -> PrivateKey { + BlstImpl + .generate_secret_key(OsRng) + .expect("bench setup: keygen") +} + +/// Returns `threshold` partial signatures over [`MSG`] from a fresh +/// threshold-split key, keyed by 1-indexed share ID. +fn partial_signatures(total: Index, threshold: Index) -> HashMap { + let tbls = BlstImpl; + let shares = tbls + .threshold_split(&new_secret(), total, threshold) + .expect("bench setup: split"); + + (1..=threshold) + .map(|idx| { + let share = shares.get(&idx).expect("bench setup: share"); + let sig = tbls.sign(share, &MSG).expect("bench setup: sign"); + (idx, sig) + }) + .collect() +} + +fn bench_sign(c: &mut Criterion) { + let tbls = BlstImpl; + let secret = new_secret(); + + c.bench_function("tier1/tbls/sign", |b| { + b.iter(|| { + tbls.sign(black_box(&secret), black_box(&MSG)) + .expect("sign should succeed") + }) + }); +} + +fn bench_verify(c: &mut Criterion) { + let tbls = BlstImpl; + let secret = new_secret(); + let pubkey = tbls + .secret_to_public_key(&secret) + .expect("bench setup: pubkey"); + let sig = tbls.sign(&secret, &MSG).expect("bench setup: sign"); + + c.bench_function("tier1/tbls/verify", |b| { + b.iter(|| { + tbls.verify(black_box(&pubkey), black_box(&MSG), black_box(&sig)) + .expect("verify should succeed") + }) + }); +} + +fn bench_verify_aggregate(c: &mut Criterion) { + const KEYS: usize = 4; + + let tbls = BlstImpl; + let mut pubkeys: Vec = Vec::with_capacity(KEYS); + let mut sigs: Vec = Vec::with_capacity(KEYS); + + for _ in 0..KEYS { + let secret = new_secret(); + pubkeys.push( + tbls.secret_to_public_key(&secret) + .expect("bench setup: pubkey"), + ); + sigs.push(tbls.sign(&secret, &MSG).expect("bench setup: sign")); + } + + let agg_sig = tbls.aggregate(&sigs).expect("bench setup: aggregate"); + + c.bench_function("tier1/tbls/verify_aggregate", |b| { + b.iter(|| { + tbls.verify_aggregate(black_box(&pubkeys), black_box(agg_sig), black_box(&MSG)) + .expect("verify_aggregate should succeed") + }) + }); +} + +fn bench_threshold_split(c: &mut Criterion) { + let tbls = BlstImpl; + let secret = new_secret(); + + for (name, total, threshold) in SPLIT_CASES { + c.bench_function(&format!("tier1/tbls/threshold_split/{name}"), |b| { + b.iter(|| { + tbls.threshold_split(black_box(&secret), black_box(total), black_box(threshold)) + .expect("threshold_split should succeed") + }) + }); + } +} + +fn bench_threshold_aggregate(c: &mut Criterion) { + let tbls = BlstImpl; + + for (name, total, threshold) in SPLIT_CASES { + let partial_sigs = partial_signatures(total, threshold); + + c.bench_function(&format!("tier1/tbls/threshold_aggregate/{name}"), |b| { + b.iter(|| { + tbls.threshold_aggregate(black_box(&partial_sigs)) + .expect("threshold_aggregate should succeed") + }) + }); + } +} + +fn bench_recover_secret(c: &mut Criterion) { + let tbls = BlstImpl; + + for (name, total, threshold) in SPLIT_CASES { + let shares = tbls + .threshold_split(&new_secret(), total, threshold) + .expect("bench setup: split"); + let subset: HashMap = (1..=threshold) + .map(|idx| (idx, *shares.get(&idx).expect("bench setup: share"))) + .collect(); + + c.bench_function(&format!("tier1/tbls/recover_secret/{name}"), |b| { + b.iter(|| { + tbls.recover_secret(black_box(&subset)) + .expect("recover_secret should succeed") + }) + }); + } +} + +fn config() -> Criterion { + Criterion::default() + .sample_size(20) + .measurement_time(Duration::from_secs(3)) +} + +criterion_group! { + name = benches; + config = config(); + targets = bench_sign, + bench_verify, + bench_verify_aggregate, + bench_threshold_split, + bench_threshold_aggregate, + bench_recover_secret, +} +criterion_main!(benches); diff --git a/crates/crypto/src/blst_impl.rs b/crates/crypto/src/blst_impl.rs index 62832f65..9997aca0 100644 --- a/crates/crypto/src/blst_impl.rs +++ b/crates/crypto/src/blst_impl.rs @@ -8,20 +8,26 @@ use std::collections::{HashMap, HashSet}; use blst::{ - BLST_ERROR, + BLST_ERROR, MultiPoint, min_pk::{PublicKey as BlstPublicKey, SecretKey as BlstSecretKey, Signature as BlstSignature}, }; use rand_core::{CryptoRng, RngCore}; -use zeroize::Zeroizing; +use zeroize::{Zeroize, Zeroizing}; use crate::{ tbls::Tbls, - types::{BlsError, Error, Index, PrivateKey, PublicKey, SIGNATURE_LENGTH, Signature}, + types::{ + BlsError, Error, Index, PrivateKey, PublicKey, SCALAR_LENGTH, SIGNATURE_LENGTH, Signature, + }, }; /// Domain Separation Tag for Ethereum 2.0 BLS signatures const ETH2_DST: &[u8] = b"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_"; +/// Bit width of BLS12-381 scalars as consumed by blst multi-scalar +/// multiplication. +const SCALAR_BITS: usize = 255; + /// Serialized BLS12-381 G2 compressed point at infinity (the identity /// signature). This is the value Charon's Herumi `Aggregate` returns for an /// empty input slice: `sig.Serialize()` of a zero `bls.Sign`. The high byte @@ -117,11 +123,16 @@ impl Tbls for BlstImpl { poly.push(coeff); } - // Evaluate polynomial at points 1..total to create shares + // Evaluate polynomial at points 1..total to create shares. The fr-domain + // copy of the secret coefficients is wiped on drop. + let poly_fr = SecretFrVec::from_secrets(&poly); + let mut shares = HashMap::new(); for i in 1..=total { - let share = evaluate_polynomial(&poly, i)?; - shares.insert(i, share.to_bytes()); + let mut share_fr = evaluate_polynomial_fr(&poly_fr.0, i)?; + let share = secret_from_fr(&share_fr); + wipe_fr(std::slice::from_mut(&mut share_fr)); + shares.insert(i, share?.to_bytes()); } Ok(shares) @@ -296,32 +307,24 @@ fn aggregate_public_keys(pks: &[BlstPublicKey]) -> Result } } -/// Evaluate polynomial at point x +/// Evaluate polynomial at point x using Horner's method in the fr domain: /// poly(x) = a_0 + a_1*x + a_2*x^2 + ... + a_n*x^n -fn evaluate_polynomial(poly: &[BlstSecretKey], x: Index) -> Result { - if poly.is_empty() { +fn evaluate_polynomial_fr(poly: &[blst::blst_fr], x: Index) -> Result { + let Some(highest) = poly.last() else { return Err(Error::PolynomialIsEmpty); - } - - // Start with the constant term - let mut result = poly[0].clone(); + }; - // Compute powers of x and accumulate - let mut x_power = scalar_from_u64(x); + let x_fr = fr_from_scalar(&scalar_from_u64(x)); + let mut acc = *highest; - for coeff in poly.iter().skip(1) { - // result += coeff * x_power - let term = scalar_mult_secret(coeff, &x_power)?; - result = scalar_add_secret(&result, &term)?; - - // x_power *= x for next iteration - if poly.len() > 2 { - let x_scalar = scalar_from_u64(x); - x_power = scalar_mult_scalars(&x_power, &x_scalar)?; + unsafe { + for coeff in poly.iter().rev().skip(1) { + blst::blst_fr_mul(&mut acc, &acc, &x_fr); + blst::blst_fr_add(&mut acc, &acc, coeff); } } - Ok(result) + Ok(acc) } /// Lagrange interpolation of secret keys at x=0 @@ -334,17 +337,25 @@ fn lagrange_interpolate_secret( return Err(Error::IndicesSharesMismatch); } - // Compute Lagrange coefficients and interpolate let coeffs = compute_lagrange_coefficients(indices)?; - let mut result = BlstSecretKey::default(); + // The fr-domain copies of the shares and the accumulator hold secret + // material; both are wiped before returning. + let shares_fr = SecretFrVec::from_secrets(shares); + let mut acc = blst::blst_fr::default(); - for i in 0..shares.len() { - let term = scalar_mult_secret(&shares[i], &coeffs[i])?; - result = scalar_add_secret(&result, &term)?; + unsafe { + for (share, coeff) in shares_fr.0.iter().zip(&coeffs) { + let mut term = blst::blst_fr::default(); + blst::blst_fr_mul(&mut term, share, coeff); + blst::blst_fr_add(&mut acc, &acc, &term); + } } - Ok(result) + let result = secret_from_fr(&acc); + wipe_fr(std::slice::from_mut(&mut acc)); + + result } /// Lagrange interpolation of signatures at x=0 @@ -360,21 +371,25 @@ fn lagrange_interpolate_signature( // Compute Lagrange coefficients let coeffs = compute_lagrange_coefficients(indices)?; - // Multiply each signature by its Lagrange coefficient and aggregate - let first_sig_scaled = signature_mult(&signatures[0], &coeffs[0])?; - let mut result_p2 = blst::blst_p2::default(); + // Multi-scalar multiplication (Pippenger) of all signatures by their + // Lagrange coefficients in one pass, with a single final affine + // conversion (each affine conversion costs a field inversion). + let points: Vec = signatures + .iter() + .map(|sig| { + let affine: &blst::blst_p2_affine = sig.into(); + *affine + }) + .collect(); + + let mut scalar_bytes = Vec::with_capacity(SCALAR_LENGTH.saturating_mul(coeffs.len())); + for coeff in &coeffs { + scalar_bytes.extend_from_slice(&scalar_from_fr(coeff).b); + } - unsafe { - // Convert first scaled signature to projective - let first_affine: &blst::blst_p2_affine = (&first_sig_scaled).into(); - blst::blst_p2_from_affine(&mut result_p2, first_affine); - - for i in 1..signatures.len() { - let sig_scaled = signature_mult(&signatures[i], &coeffs[i])?; - let sig_affine: &blst::blst_p2_affine = (&sig_scaled).into(); - blst::blst_p2_add_or_double_affine(&mut result_p2, &result_p2, sig_affine); - } + let result_p2 = points.as_slice().mult(&scalar_bytes, SCALAR_BITS); + unsafe { // Convert back to affine let mut result_affine = blst::blst_p2_affine::default(); blst::blst_p2_to_affine(&mut result_affine, &result_p2); @@ -382,169 +397,125 @@ fn lagrange_interpolate_signature( } } -/// Compute Lagrange coefficients for interpolation at x=0 +/// Compute Lagrange coefficients for interpolation at x=0, in the fr domain: /// λ_i = ∏_{j≠i} (0 - x_j) / (x_i - x_j) = ∏_{j≠i} x_j / (x_j - x_i) -fn compute_lagrange_coefficients(indices: &[Index]) -> Result, Error> { +fn compute_lagrange_coefficients(indices: &[Index]) -> Result, Error> { // Check if indices are unique if indices.len() != indices.iter().collect::>().len() { return Err(Error::IndicesNotUnique); } - let mut coeffs = Vec::with_capacity(indices.len()); + let indices_fr: Vec = indices + .iter() + .map(|&x| fr_from_scalar(&scalar_from_u64(x))) + .collect(); + let one = fr_from_scalar(&scalar_from_u64(1)); - for (i, &x_i) in indices.iter().enumerate() { - let mut numerator = scalar_from_u64(1); - let mut denominator = scalar_from_u64(1); + let mut coeffs = Vec::with_capacity(indices.len()); - for (j, &x_j) in indices.iter().enumerate() { - if i == j { - continue; + unsafe { + for (i, x_i) in indices_fr.iter().enumerate() { + let mut numerator = one; + let mut denominator = one; + + for (j, x_j) in indices_fr.iter().enumerate() { + if i == j { + continue; + } + + // numerator *= x_j + blst::blst_fr_mul(&mut numerator, &numerator, x_j); + + // denominator *= (x_j - x_i), computed modulo the field order. + let mut diff = blst::blst_fr::default(); + blst::blst_fr_sub(&mut diff, x_j, x_i); + blst::blst_fr_mul(&mut denominator, &denominator, &diff); } - // numerator *= x_j - let x_j_scalar = scalar_from_u64(x_j); - numerator = scalar_mult_scalars(&numerator, &x_j_scalar)?; + // `blst_fr_eucl_inverse` below is variable-time, which is fine + // here: it only ever operates on public share indices. + // Unreachable with unique indices, but guard division regardless. + if scalar_from_fr(&denominator) == blst::blst_scalar::default() { + return Err(Error::DivisionByZero); + } - // denominator *= (x_j - x_i) - let diff = if x_j > x_i { - scalar_from_u64(x_j.abs_diff(x_i)) - } else { - // For negative differences, we need to work in the scalar field - // x_j - x_i (mod r) where r is the curve order - scalar_negate(&scalar_from_u64(x_i.abs_diff(x_j)))? - }; + // coeff = numerator / denominator + let mut inverse = blst::blst_fr::default(); + blst::blst_fr_eucl_inverse(&mut inverse, &denominator); - denominator = scalar_mult_scalars(&denominator, &diff)?; + let mut coeff = blst::blst_fr::default(); + blst::blst_fr_mul(&mut coeff, &numerator, &inverse); + coeffs.push(coeff); } - - // Compute numerator / denominator = numerator * denominator^{-1} - let coeff = scalar_div(&numerator, &denominator)?; - coeffs.push(coeff); } Ok(coeffs) } -/// Convert u64 to blst scalar -fn scalar_from_u64(val: u64) -> blst::blst_scalar { - let mut scalar = blst::blst_scalar::default(); - let limbs: [u64; 4] = [val, 0, 0, 0]; - unsafe { - blst::blst_scalar_from_uint64(&mut scalar, limbs.as_ptr()); - } - scalar +/// Converts a scalar to the fr (Montgomery) domain. +fn fr_from_scalar(scalar: &blst::blst_scalar) -> blst::blst_fr { + let mut fr = blst::blst_fr::default(); + unsafe { blst::blst_fr_from_scalar(&mut fr, scalar) }; + fr } -/// Multiply secret key by scalar -fn scalar_mult_secret( - sk: &BlstSecretKey, - scalar: &blst::blst_scalar, -) -> Result { - let sk_scalar = sk.into(); - let result_scalar = scalar_mult_scalars(sk_scalar, scalar)?; - let sk: &BlstSecretKey = (&result_scalar) - .try_into() - .map_err(|_| Error::FailedToConvertSkToBlstScalar)?; - Ok(sk.clone()) -} - -/// Add two secret keys -fn scalar_add_secret(sk1: &BlstSecretKey, sk2: &BlstSecretKey) -> Result { - let result = scalar_add(sk1.into(), sk2.into())?; - let sk: &BlstSecretKey = (&result) - .try_into() - .map_err(|_| Error::FailedToConvertScalarToSecretKey)?; - Ok(sk.clone()) +/// Converts an fr (Montgomery) value back to a scalar. +fn scalar_from_fr(fr: &blst::blst_fr) -> blst::blst_scalar { + let mut scalar = blst::blst_scalar::default(); + unsafe { blst::blst_scalar_from_fr(&mut scalar, fr) }; + scalar } -/// Multiply signature by scalar -fn signature_mult(sig: &BlstSignature, scalar: &blst::blst_scalar) -> Result { - let mut sig_proj = blst::blst_p2::default(); - let mut result_p2 = blst::blst_p2::default(); - let mut result_affine = blst::blst_p2_affine::default(); - - unsafe { - // Convert affine to projective - let sig_affine: &blst::blst_p2_affine = sig.into(); - blst::blst_p2_from_affine(&mut sig_proj, sig_affine); - // Multiply - blst::blst_p2_mult(&mut result_p2, &sig_proj, scalar.b.as_ptr(), 255); - // Convert back to affine - blst::blst_p2_to_affine(&mut result_affine, &result_p2); - } - - Ok(BlstSignature::from(result_affine)) +/// Converts an fr value to a secret key, validating it (nonzero, below the +/// group order) exactly like the previous scalar-domain conversion did. +fn secret_from_fr(fr: &blst::blst_fr) -> Result { + let mut scalar = scalar_from_fr(fr); + let result = <&BlstSecretKey>::try_from(&scalar) + .cloned() + .map_err(|_| Error::FailedToConvertScalarToSecretKey); + scalar.zeroize(); + result } -/// Add two scalars -fn scalar_add(a: &blst::blst_scalar, b: &blst::blst_scalar) -> Result { - let mut result = blst::blst_scalar::default(); - unsafe { - if blst::blst_sk_add_n_check(&mut result, a, b) { - Ok(result) - } else { - Err(Error::FailedToAddScalars) - } +/// Best-effort volatile wipe of fr values holding secret material. +fn wipe_fr(values: &mut [blst::blst_fr]) { + for value in values.iter_mut() { + // SAFETY: `value` is a valid, aligned, exclusive reference. + unsafe { std::ptr::write_volatile(value, blst::blst_fr::default()) }; } } -/// Multiply two scalars -fn scalar_mult_scalars( - a: &blst::blst_scalar, - b: &blst::blst_scalar, -) -> Result { - let mut result = blst::blst_scalar::default(); - unsafe { - if blst::blst_sk_mul_n_check(&mut result, a, b) { - Ok(result) - } else { - Err(Error::FailedToMultiplyScalars) - } +/// Fr-domain copies of secret keys, wiped on drop. +struct SecretFrVec(Vec); + +impl SecretFrVec { + fn from_secrets(secrets: &[BlstSecretKey]) -> Self { + Self( + secrets + .iter() + .map(|sk| { + let scalar: &blst::blst_scalar = sk.into(); + fr_from_scalar(scalar) + }) + .collect(), + ) } } -/// Negate a scalar -fn scalar_negate(a: &blst::blst_scalar) -> Result { - // To negate in the field, we compute (r - a) where r is the curve order - // But blst doesn't expose this directly, so we use: -a ≡ r - a - // We can compute this as: 0 - a - let zero = scalar_from_u64(0); - let mut result_scalar = blst::blst_scalar::default(); - - unsafe { - // Convert scalars to fr for arithmetic - let mut a_fr = blst::blst_fr::default(); - let mut zero_fr = blst::blst_fr::default(); - - blst::blst_fr_from_scalar(&mut a_fr, a); - blst::blst_fr_from_scalar(&mut zero_fr, &zero); - - let mut result_fr = blst::blst_fr::default(); - blst::blst_fr_sub(&mut result_fr, &zero_fr, &a_fr); - - blst::blst_scalar_from_fr(&mut result_scalar, &result_fr); +impl Drop for SecretFrVec { + fn drop(&mut self) { + wipe_fr(&mut self.0); } - - Ok(result_scalar) } -/// Divide two scalars (multiply by inverse) -fn scalar_div( - numerator: &blst::blst_scalar, - denominator: &blst::blst_scalar, -) -> Result { - let zero = blst::blst_scalar::default(); - if *denominator == zero { - return Err(Error::DivisionByZero); - } - - let mut inv_scalar = blst::blst_scalar::default(); - +/// Convert u64 to blst scalar +fn scalar_from_u64(val: u64) -> blst::blst_scalar { + let mut scalar = blst::blst_scalar::default(); + let limbs: [u64; 4] = [val, 0, 0, 0]; unsafe { - blst::blst_sk_inverse(&mut inv_scalar, denominator); + blst::blst_scalar_from_uint64(&mut scalar, limbs.as_ptr()); } - - scalar_mult_scalars(numerator, &inv_scalar) + scalar } #[cfg(test)] diff --git a/crates/dkg/Cargo.toml b/crates/dkg/Cargo.toml index 0cca0559..55af689f 100644 --- a/crates/dkg/Cargo.toml +++ b/crates/dkg/Cargo.toml @@ -6,6 +6,14 @@ repository.workspace = true license.workspace = true publish.workspace = true +[lib] +bench = false + +[features] +# Exposes the in-memory DKG harness to the criterion benches. +# Never enable in production builds. +bench-util = [] + [dependencies] bon.workspace = true prost.workspace = true @@ -46,6 +54,7 @@ pluto-build-proto.workspace = true [dev-dependencies] anyhow.workspace = true +criterion.workspace = true test-case.workspace = true clap.workspace = true hex.workspace = true @@ -56,5 +65,10 @@ serde_json.workspace = true tempfile.workspace = true wiremock.workspace = true +[[bench]] +name = "mem_dkg" +harness = false +path = "benches/mem_dkg.rs" + [lints] workspace = true diff --git a/crates/dkg/benches/mem_dkg.rs b/crates/dkg/benches/mem_dkg.rs new file mode 100644 index 00000000..8abd834b --- /dev/null +++ b/crates/dkg/benches/mem_dkg.rs @@ -0,0 +1,39 @@ +//! Benchmarks a full in-process FROST DKG ceremony over the in-memory +//! transport (no networking). +//! +//! Rust-only informational benchmark: charon has no in-memory DKG transport, +//! so the cross-implementation comparison happens at process level (tier 3). +//! Requires the `bench-util` feature: +//! `cargo bench -p pluto-dkg --features bench-util`. +#![allow(missing_docs)] + +use std::time::Duration; + +use criterion::{Criterion, criterion_group, criterion_main}; +use pluto_dkg::frost_bench_util::run_mem_dkg; + +const NODES: u32 = 4; +const THRESHOLD: u32 = 3; + +fn bench_mem_ceremony(c: &mut Criterion) { + let runtime = tokio::runtime::Runtime::new().expect("bench setup: tokio runtime"); + + for vals in [1u32, 10] { + c.bench_function(&format!("tier2/dkg/mem_ceremony/{vals}vals"), |b| { + b.iter(|| runtime.block_on(run_mem_dkg(NODES, THRESHOLD, vals))) + }); + } +} + +fn config() -> Criterion { + Criterion::default() + .sample_size(10) + .measurement_time(Duration::from_secs(5)) +} + +criterion_group! { + name = benches; + config = config(); + targets = bench_mem_ceremony, +} +criterion_main!(benches); diff --git a/crates/dkg/src/frost.rs b/crates/dkg/src/frost.rs index 42c0b600..e7bba891 100644 --- a/crates/dkg/src/frost.rs +++ b/crates/dkg/src/frost.rs @@ -15,7 +15,7 @@ use tracing::debug; use crate::share::Share; -type Round1Output = (HashMap, HashMap); +pub(crate) type Round1Output = (HashMap, HashMap); /// Identifies the source and target nodes and validator index the message /// belongs to. @@ -485,148 +485,9 @@ mod tests { use std::sync::Arc; use pluto_crypto::{blst_impl::BlstImpl, tbls::Tbls, types::Index}; - use tokio::sync::{Mutex, Notify}; use super::*; - - struct FrostMemTransport { - nodes: usize, - inner: Mutex, - notify: Notify, - } - - #[derive(Default)] - struct FrostMemTransportInner { - round1: usize, - round1_bcast: HashMap, - round1_shares: HashMap>, - round2: usize, - round2_bcast: HashMap, - } - - impl FrostMemTransport { - fn new(nodes: usize) -> Self { - Self { - nodes, - inner: Mutex::new(FrostMemTransportInner::default()), - notify: Notify::new(), - } - } - } - - #[async_trait] - impl FTransport for Arc { - async fn round1( - &mut self, - cancellation: &CancellationToken, - bcast: HashMap, - shares: HashMap, - ) -> Result { - let source_id = bcast - .keys() - .next() - .map(|key| key.source_id) - .ok_or(FrostError::MissingRoundState)?; - debug_assert!(bcast.keys().all(|key| key.source_id == source_id)); - - { - let mut inner = self.inner.lock().await; - if inner.round1 == self.nodes { - inner.round1 = 0; - inner.round1_bcast.clear(); - inner.round1_shares.clear(); - } - for (key, round1_bcast) in bcast { - inner.round1_bcast.insert( - MsgKey { - val_idx: key.val_idx, - source_id: key.source_id, - target_id: 0, - }, - round1_bcast, - ); - } - for (key, share) in shares { - inner - .round1_shares - .entry(key.target_id) - .or_default() - .insert(key, share); - } - inner.round1 = inner - .round1 - .checked_add(1) - .expect("test round counter should not overflow"); - } - self.notify.notify_waiters(); - - loop { - let notified = self.notify.notified(); - { - let inner = self.inner.lock().await; - if inner.round1 == self.nodes { - return Ok(( - inner.round1_bcast.clone(), - inner - .round1_shares - .get(&source_id) - .cloned() - .unwrap_or_default(), - )); - } - } - - tokio::select! { - _ = cancellation.cancelled() => return Err(FrostError::Cancelled), - _ = notified => {} - } - } - } - - async fn round2( - &mut self, - cancellation: &CancellationToken, - bcast: HashMap, - ) -> Result, FrostError> { - { - let mut inner = self.inner.lock().await; - if inner.round2 == self.nodes { - inner.round2 = 0; - inner.round2_bcast.clear(); - } - for (key, round2_bcast) in bcast { - inner.round2_bcast.insert( - MsgKey { - val_idx: key.val_idx, - source_id: key.source_id, - target_id: 0, - }, - round2_bcast, - ); - } - inner.round2 = inner - .round2 - .checked_add(1) - .expect("test round counter should not overflow"); - } - self.notify.notify_waiters(); - - loop { - let notified = self.notify.notified(); - { - let inner = self.inner.lock().await; - if inner.round2 == self.nodes { - return Ok(inner.round2_bcast.clone()); - } - } - - tokio::select! { - _ = cancellation.cancelled() => return Err(FrostError::Cancelled), - _ = notified => {} - } - } - } - } + use crate::frost_bench_util::{FrostMemTransport, run_mem_dkg}; #[tokio::test] async fn frost_dkg() { @@ -654,46 +515,6 @@ mod tests { ); } - async fn run_mem_dkg(nodes: u32, threshold: u32, vals: u32) -> Vec> { - let cancellation = CancellationToken::new(); - let tp = Arc::new(FrostMemTransport::new( - usize::try_from(nodes).expect("nodes should fit"), - )); - - let mut tasks = Vec::new(); - for i in 0..nodes { - let mut tp = Arc::clone(&tp); - let cancellation = cancellation.clone(); - tasks.push(tokio::spawn(async move { - run_frost_parallel( - cancellation, - &mut tp, - vals, - nodes, - threshold, - i.checked_add(1).expect("share index should not overflow"), - "0", - ) - .await - })); - } - - let mut node_shares = Vec::new(); - for task in tasks { - let shares = task - .await - .expect("task should not panic") - .expect("DKG should run"); - assert_eq!( - shares.len(), - usize::try_from(vals).expect("vals should fit") - ); - node_shares.push(shares); - } - - node_shares - } - #[tokio::test] async fn transport_returns_cancelled_while_waiting() { let cancellation = CancellationToken::new(); diff --git a/crates/dkg/src/frost_bench_util.rs b/crates/dkg/src/frost_bench_util.rs new file mode 100644 index 00000000..ca79559d --- /dev/null +++ b/crates/dkg/src/frost_bench_util.rs @@ -0,0 +1,202 @@ +//! In-memory FROST transport and ceremony driver shared by the unit tests +//! and the criterion benches (`bench-util` feature). Not for production use. + +use std::{collections::HashMap, sync::Arc}; + +use async_trait::async_trait; +use pluto_frost::kryptology::{Round1Bcast, Round2Bcast, ShamirShare}; +use tokio::sync::{Mutex, Notify}; +use tokio_util::sync::CancellationToken; + +use crate::{ + frost::{FTransport, FrostError, MsgKey, Round1Output, run_frost_parallel}, + share::Share, +}; + +/// In-memory [`FTransport`] delivering FROST round messages between +/// in-process participants without any networking. +pub struct FrostMemTransport { + nodes: usize, + inner: Mutex, + notify: Notify, +} + +#[derive(Default)] +struct FrostMemTransportInner { + round1: usize, + round1_bcast: HashMap, + round1_shares: HashMap>, + round2: usize, + round2_bcast: HashMap, +} + +impl FrostMemTransport { + /// Creates a transport for `nodes` participants. + pub fn new(nodes: usize) -> Self { + Self { + nodes, + inner: Mutex::new(FrostMemTransportInner::default()), + notify: Notify::new(), + } + } +} + +#[async_trait] +impl FTransport for Arc { + async fn round1( + &mut self, + cancellation: &CancellationToken, + bcast: HashMap, + shares: HashMap, + ) -> Result { + let source_id = bcast + .keys() + .next() + .map(|key| key.source_id) + .ok_or(FrostError::MissingRoundState)?; + debug_assert!(bcast.keys().all(|key| key.source_id == source_id)); + + { + let mut inner = self.inner.lock().await; + if inner.round1 == self.nodes { + inner.round1 = 0; + inner.round1_bcast.clear(); + inner.round1_shares.clear(); + } + for (key, round1_bcast) in bcast { + inner.round1_bcast.insert( + MsgKey { + val_idx: key.val_idx, + source_id: key.source_id, + target_id: 0, + }, + round1_bcast, + ); + } + for (key, share) in shares { + inner + .round1_shares + .entry(key.target_id) + .or_default() + .insert(key, share); + } + inner.round1 = inner + .round1 + .checked_add(1) + .expect("test round counter should not overflow"); + } + self.notify.notify_waiters(); + + loop { + let notified = self.notify.notified(); + { + let inner = self.inner.lock().await; + if inner.round1 == self.nodes { + return Ok(( + inner.round1_bcast.clone(), + inner + .round1_shares + .get(&source_id) + .cloned() + .unwrap_or_default(), + )); + } + } + + tokio::select! { + _ = cancellation.cancelled() => return Err(FrostError::Cancelled), + _ = notified => {} + } + } + } + + async fn round2( + &mut self, + cancellation: &CancellationToken, + bcast: HashMap, + ) -> Result, FrostError> { + { + let mut inner = self.inner.lock().await; + if inner.round2 == self.nodes { + inner.round2 = 0; + inner.round2_bcast.clear(); + } + for (key, round2_bcast) in bcast { + inner.round2_bcast.insert( + MsgKey { + val_idx: key.val_idx, + source_id: key.source_id, + target_id: 0, + }, + round2_bcast, + ); + } + inner.round2 = inner + .round2 + .checked_add(1) + .expect("test round counter should not overflow"); + } + self.notify.notify_waiters(); + + loop { + let notified = self.notify.notified(); + { + let inner = self.inner.lock().await; + if inner.round2 == self.nodes { + return Ok(inner.round2_bcast.clone()); + } + } + + tokio::select! { + _ = cancellation.cancelled() => return Err(FrostError::Cancelled), + _ = notified => {} + } + } + } +} + +/// Runs a full in-process FROST DKG ceremony over the in-memory +/// transport and returns every node's shares. +/// +/// # Panics +/// +/// Panics when the ceremony fails; callers are tests and benches. +pub async fn run_mem_dkg(nodes: u32, threshold: u32, vals: u32) -> Vec> { + let cancellation = CancellationToken::new(); + let tp = Arc::new(FrostMemTransport::new( + usize::try_from(nodes).expect("nodes should fit"), + )); + + let mut tasks = Vec::new(); + for i in 0..nodes { + let mut tp = Arc::clone(&tp); + let cancellation = cancellation.clone(); + tasks.push(tokio::spawn(async move { + run_frost_parallel( + cancellation, + &mut tp, + vals, + nodes, + threshold, + i.checked_add(1).expect("share index should not overflow"), + "0", + ) + .await + })); + } + + let mut node_shares = Vec::new(); + for task in tasks { + let shares = task + .await + .expect("task should not panic") + .expect("DKG should run"); + assert_eq!( + shares.len(), + usize::try_from(vals).expect("vals should fit") + ); + node_shares.push(shares); + } + + node_shares +} diff --git a/crates/dkg/src/lib.rs b/crates/dkg/src/lib.rs index 72e07923..ccbbd4a7 100644 --- a/crates/dkg/src/lib.rs +++ b/crates/dkg/src/lib.rs @@ -23,6 +23,10 @@ pub mod dkg; /// Kryptology-compatible FROST DKG orchestration. mod frost; +/// In-memory FROST transport and ceremony driver for tests and benches. +#[cfg(any(test, feature = "bench-util"))] +pub mod frost_bench_util; + /// FROST DKG P2P transport. mod frostp2p; diff --git a/crates/frost/Cargo.toml b/crates/frost/Cargo.toml index 554c30f7..e881e8a2 100644 --- a/crates/frost/Cargo.toml +++ b/crates/frost/Cargo.toml @@ -6,6 +6,9 @@ repository.workspace = true license.workspace = true publish.workspace = true +[lib] +bench = false + [dependencies] blst.workspace = true rand_core.workspace = true @@ -15,11 +18,17 @@ thiserror.workspace = true zeroize = { workspace = true, features = ["derive"] } [dev-dependencies] +criterion.workspace = true hex.workspace = true rand.workspace = true serde.workspace = true serde_json.workspace = true +[[bench]] +name = "frost" +harness = false +path = "benches/frost.rs" + [lints.rust] missing_docs = "deny" # Allow unsafe code for blst C bindings (overrides workspace forbid) diff --git a/crates/frost/benches/frost.rs b/crates/frost/benches/frost.rs new file mode 100644 index 00000000..ccee33db --- /dev/null +++ b/crates/frost/benches/frost.rs @@ -0,0 +1,153 @@ +//! Benchmarks for the kryptology-compatible FROST DKG rounds and BLS +//! partial-signature operations. +//! +//! Benchmark ids follow the cross-language pair naming used by +//! `perf/pairs.json`; the Go counterparts live in `perf/go-bench/`. +#![allow(missing_docs)] + +use std::{collections::BTreeMap, time::Duration}; + +use criterion::{BatchSize, Criterion, criterion_group, criterion_main}; +use pluto_frost::{ + KeyPackage, + kryptology::{ + BlsPartialSignature, BlsSignature, Round1Bcast, Round1Secret, ShamirShare, round1, round2, + }, +}; +use rand::rngs::OsRng; + +/// Must stay identical to the workload in +/// `perf/go-bench/frost_bench_test.go` so both sides time the same thing. +const THRESHOLD: u16 = 3; +const TOTAL: u16 = 4; +const CTX: u8 = 0; + +/// Must stay identical to the message in `perf/go-bench/frost_bench_test.go`. +const MSG: [u8; 32] = [0u8; 32]; + +struct Round2Input { + secret: Round1Secret, + bcasts: BTreeMap, + shares: BTreeMap, +} + +/// Runs round 1 for every participant and returns participant 1's round-2 +/// inputs: all broadcasts (a node's own round-1 broadcast is included, as in +/// a real ceremony) plus the Shamir shares addressed to participant 1. +fn round2_input() -> Round2Input { + let mut rng = OsRng; + let mut bcasts = BTreeMap::new(); + let mut shares_to_one = BTreeMap::new(); + let mut secret_one = None; + + for id in 1..=u32::from(TOTAL) { + let (bcast, mut shares, secret) = + round1(id, THRESHOLD, TOTAL, CTX, &mut rng).expect("bench setup: round1"); + + bcasts.insert(id, bcast); + + if id == 1 { + secret_one = Some(secret); + } else { + let share = shares.remove(&1).expect("bench setup: share for 1"); + shares_to_one.insert(id, share); + } + } + + Round2Input { + secret: secret_one.expect("bench setup: participant 1 secret"), + bcasts, + shares: shares_to_one, + } +} + +/// Runs a full in-process 3-of-4 DKG and returns every participant's +/// [`KeyPackage`]. +fn key_packages() -> Vec { + let mut rng = OsRng; + let mut bcasts = BTreeMap::new(); + let mut secrets = BTreeMap::new(); + let mut shares_by_target: BTreeMap> = BTreeMap::new(); + + for id in 1..=u32::from(TOTAL) { + let (bcast, shares, secret) = + round1(id, THRESHOLD, TOTAL, CTX, &mut rng).expect("bench setup: round1"); + + bcasts.insert(id, bcast); + secrets.insert(id, secret); + + for (target, share) in shares { + shares_by_target + .entry(target) + .or_default() + .insert(id, share); + } + } + + secrets + .into_iter() + .map(|(id, secret)| { + let shares = shares_by_target + .remove(&id) + .expect("bench setup: shares for participant"); + let (_, key_package, _) = + round2(secret, &bcasts, &shares).expect("bench setup: round2"); + key_package + }) + .collect() +} + +fn bench_round1(c: &mut Criterion) { + c.bench_function("tier1/frost/round1", |b| { + let mut rng = OsRng; + b.iter(|| round1(1, THRESHOLD, TOTAL, CTX, &mut rng).expect("round1 should succeed")) + }); +} + +fn bench_round2(c: &mut Criterion) { + c.bench_function("tier1/frost/round2", |b| { + b.iter_batched( + round2_input, + |input| { + round2(input.secret, &input.bcasts, &input.shares).expect("round2 should succeed") + }, + BatchSize::SmallInput, + ) + }); +} + +fn bench_partial_sign(c: &mut Criterion) { + let key_package = key_packages().into_iter().next().expect("bench setup"); + + c.bench_function("tier1/frost/partial_sign", |b| { + b.iter(|| BlsPartialSignature::from_key_package(&key_package, &MSG)) + }); +} + +fn bench_aggregate(c: &mut Criterion) { + let partials: Vec = key_packages() + .iter() + .take(usize::from(THRESHOLD)) + .map(|kp| BlsPartialSignature::from_key_package(kp, &MSG)) + .collect(); + + c.bench_function("tier1/frost/aggregate", |b| { + b.iter(|| { + BlsSignature::from_partial_signatures(THRESHOLD, &partials) + .expect("aggregate should succeed") + }) + }); +} + +fn config() -> Criterion { + Criterion::default() + .sample_size(20) + .measurement_time(Duration::from_secs(3)) +} + +criterion_group! { + name = benches; + config = config(); + targets = bench_round1, bench_round2, bench_partial_sign, bench_aggregate, +} +criterion_main!(benches); diff --git a/crates/frost/src/kryptology.rs b/crates/frost/src/kryptology.rs index 53bffb53..513b71ee 100644 --- a/crates/frost/src/kryptology.rs +++ b/crates/frost/src/kryptology.rs @@ -18,6 +18,13 @@ use zeroize::{Zeroize, ZeroizeOnDrop}; use super::*; +/// Byte length of a serialized BLS12-381 scalar. +const SCALAR_BYTES: usize = 32; + +/// Bit width of BLS12-381 scalars as consumed by blst multi-scalar +/// multiplication. +const SCALAR_BITS: usize = 255; + /// Errors from the kryptology-compatible FROST protocol. #[derive(Debug, thiserror::Error)] pub enum KryptologyError { @@ -647,12 +654,12 @@ impl BlsSignature { .map(|ps| Scalar::from(u64::from(ps.identifier))) .collect(); - let mut combined = blst_p2::default(); - - for (i, ps) in partial_sigs.iter().enumerate() { - // Lagrange coefficient: L_i(0) = prod_{j!=i} ( x_j / (x_j - x_i) ) + // Lagrange coefficients: L_i(0) = prod_{j!=i} ( x_j / (x_j - x_i) ), + // serialized little-endian for blst multi-scalar multiplication. + let mut lambda_bytes = Vec::with_capacity(SCALAR_BYTES * partial_sigs.len()); + for i in 0..partial_sigs.len() { let mut lambda = Scalar::ONE; - for (j, _) in partial_sigs.iter().enumerate() { + for j in 0..partial_sigs.len() { if i == j { continue; } @@ -663,14 +670,14 @@ impl BlsSignature { let den_inv = den.invert().ok_or(KryptologyError::InvalidSignerCount)?; lambda = lambda * num * den_inv; } - - let weighted = p2_mult(&ps.point, &lambda); - - let mut tmp = blst_p2::default(); - unsafe { blst_p2_add_or_double(&mut tmp, &combined, &weighted) }; - combined = tmp; + lambda_bytes.extend_from_slice(&lambda.to_bytes()); } + // Multi-scalar multiplication (Pippenger) over all partials at once; + // `p2_affines::from` batch-converts to affine with a single inversion. + let points: Vec = partial_sigs.iter().map(|ps| ps.point).collect(); + let combined = p2_affines::from(&points).mult(&lambda_bytes, SCALAR_BITS); + Ok(BlsSignature { point: combined }) } diff --git a/crates/k1util/Cargo.toml b/crates/k1util/Cargo.toml index 5179c0a7..f85892a5 100644 --- a/crates/k1util/Cargo.toml +++ b/crates/k1util/Cargo.toml @@ -6,6 +6,9 @@ repository.workspace = true license.workspace = true publish.workspace = true +[lib] +bench = false + [dependencies] thiserror.workspace = true k256.workspace = true diff --git a/crates/k1util/benches/k1util.rs b/crates/k1util/benches/k1util.rs index 49ccf04d..06dda0e0 100644 --- a/crates/k1util/benches/k1util.rs +++ b/crates/k1util/benches/k1util.rs @@ -7,8 +7,6 @@ use std::hint::black_box; use criterion::{Criterion, criterion_group, criterion_main}; use k256::{SecretKey, elliptic_curve::rand_core::OsRng}; - -// Assuming your crate is named "charon" - adjust if different use pluto_k1util::{K1_HASH_LEN, SIGNATURE_LEN, recover, sign, verify_64}; fn setup() -> (SecretKey, Vec, Vec) { @@ -23,7 +21,7 @@ fn bench_sign(c: &mut Criterion) { let key = SecretKey::random(&mut OsRng); let digest = vec![0u8; K1_HASH_LEN]; - c.bench_function("sign", |b| { + c.bench_function("tier1/k1/sign", |b| { b.iter(|| sign(black_box(&key), black_box(&digest)).expect("sign should succeed")) }); } @@ -32,7 +30,7 @@ fn bench_recover(c: &mut Criterion) { let (key, digest, sig) = setup(); let pubkey = key.public_key(); - c.bench_function("recover", |b| { + c.bench_function("tier1/k1/recover", |b| { b.iter(|| { let recovered = recover(black_box(&digest), black_box(&sig)).expect("recover should succeed"); @@ -45,7 +43,7 @@ fn bench_verify(c: &mut Criterion) { let (key, digest, sig) = setup(); let pubkey = key.public_key(); - c.bench_function("verify", |b| { + c.bench_function("tier1/k1/verify", |b| { b.iter(|| { let ok = verify_64( black_box(&pubkey), diff --git a/perf/FINDINGS.md b/perf/FINDINGS.md new file mode 100644 index 00000000..03f77396 --- /dev/null +++ b/perf/FINDINGS.md @@ -0,0 +1,187 @@ +# Pluto vs Charon — initial performance findings + +Date: 2026-07-15 · Host: Apple M3 Pro (darwin/arm64) · rustc 1.95.0 · go 1.25 (charon v1.7.1) +Produced by `./perf/run.sh` (see `perf/README.md`). Numbers are medians; raw +data in `perf/out/`. Ratios are pluto/charon — above 1.00x means Pluto is +slower. + +## TL;DR + +Pluto is **faster than Charon on most crypto and serialization hot paths** +(BLS sign/verify via blst, FROST DKG rounds up to 5x, protobuf, secp256k1 +verify/recover, `create enr`/`create cluster`, 2.6x smaller peak RSS, DKG +ceremony wall-time at parity). The suboptimal areas cluster into **five root +causes**, ordered by duty-hot-path impact: + +| # | Area | Ratio | On duty hot path? | +|---|---|---|---| +| 1 | QBFT consensus instance (spawn→decide) | 3.7–10x | yes — every duty | +| 2 | BLS threshold aggregate (sigagg) | 1.2–1.35x | yes — every duty | +| 3 | SSZ encode (attestation, proposal) | 3–4x | yes — every consensus msg | +| 4 | Shamir split / recover secret | 2.3–3.7x | no — ceremony only | +| 5 | scrypt keystore encryption / k1 sign | 2.4x / 1.8x | no — ceremony / p2p setup | + +## Findings in detail + +### 1. QBFT: 380µs vs 40µs (4 nodes), 712µs vs 195µs (10 nodes) — 3.7–10x + +`tier2/qbft/decide_*`. Same in-memory topology on both sides (identical +transport shape, never-firing timers, i64 values). Root causes observed in +`crates/core/src/qbft/mod.rs`: + +- **OS thread per participant** vs Go goroutines: Pluto's `qbft::run` is a + blocking function; the consensus wiring spawns threads per instance. Thread + spawn + crossbeam channel wakeups dominate the 4-node case. +- **50ms cancellation polling** (`CANCELLATION_POLL_INTERVAL`, used as + `default(...)` arms in both `select!` loops at mod.rs:473/671 and 745/786): + instance shutdown latency is up to 50ms vs Go's immediate `ctx.Done()`. + This did not affect the decide-latency numbers (teardown excluded) but + delays instance cleanup and holds threads/memory ~50ms longer per duty. +- Absolute numbers are still far below consensus timeouts (~1s), so the + practical impact per duty is modest — but at scale (many validators, many + concurrent instances) thread churn adds up. + +Suggested work: reuse a small worker pool or async task per instance instead +of dedicated threads; replace cancellation polling with a channel-based token +that can participate in `select!` directly. + +### 2. BLS threshold aggregate: 403µs vs 300µs (3-of-4), 947µs vs 792µs (7-of-10) — 1.2–1.35x + +`tier1/tbls/threshold_aggregate/*` and `tier1/frost/aggregate`. This runs in +`sigagg` for **every duty**. Root causes in +`crates/crypto/src/blst_impl.rs::lagrange_interpolate_signature` (line 352): + +- Each partial goes through `BlstSignature::from_bytes` (compressed + deserialization + validation) per call, then `signature_mult` round-trips + projective→affine per share before the additions. +- Scalar mults are done one-by-one; blst offers Pippenger multi-scalar + multiplication (`blst_p2s_mult_pippenger`) which wins even at 3–10 points. + +Interestingly Pluto's raw BLS sign/verify beats herumi by 1.4–1.7x, so the gap +is entirely in the interpolation plumbing, not blst itself. + +### 3. SSZ encode: attestation 193ns vs 46ns (4.2x), proposal 1.16µs vs 328ns (3.5x) + +`tier1/ssz/att_encode`, `tier1/ssz/proposal_encode` (+decode at 1.9x). +Encoding runs for every consensus value and parsigex exchange. Root causes in +`crates/core/src/ssz_codec.rs`: + +- `as_ssz_bytes()` (ethereum_ssz) starts from an empty `Vec` and grows it; + Go's fastssz generates `MarshalSSZTo` with the exact size preallocated. +- `encode_versioned_signed_proposal` (ssz_codec.rs:427) encodes the inner + block into its own `Vec`, then copies it into a second header-prefixed + buffer — a full extra allocation + copy per message. + +Suggested work: compute `ssz_bytes_len()` up front and encode into one +right-sized buffer; encode the inner block directly into the output buffer +after the header. `att_hash_root` (1.4–1.5x, tree_hash vs fastssz) is lower +priority. + +### 4. Shamir split / recover: 2.3–3.7x (ceremony-only) + +`tier1/tbls/threshold_split/*`, `tier1/tbls/recover_secret/*` (µs-scale, +runs only during `create cluster`/DKG). Likely the same per-op +serialize/deserialize round-trips through compressed bytes in `blst_impl.rs` +polynomial evaluation. Low priority; fix alongside finding 2. + +### 5. Process-level: secure keystores 2.4x, k1 sign 1.8x + +- `tier3/cli/create_cluster_4_secure`: 321ms vs 134ms. Scrypt params are + identical on both sides (n=2^18, verified in + `crates/eth2util/src/keystore/keystorev4.rs` vs + `charon/eth2util/keystore/keystore.go`), so this is the RustCrypto `scrypt` + crate being slower than Go's `x/crypto/scrypt` (no SIMD salsa20/8 core). + Options: SIMD-enabled scrypt implementation, or parallelize keystore + encryption across validators (charon encrypts sequentially too — easy win). +- `tier1/k1/sign`: 60–64µs vs 33µs — k256's base-point multiplication is + slower than decred's precomputed-table implementation (verify/recover are + *faster* in k256, so it's specifically sign). Used for ENR/p2p signatures, + not duty signing. Option: `secp256k1` (libsecp C bindings) for signing, or + accept. + +### Build config: thin LTO + codegen-units=1 experiment + +Applied to the bench profile only and re-measured: **~6–12% improvement on +pure-Rust paths** (ssz encode −6%, proposal encode −12%, k1util −7–10%), +**zero effect** on blst-bound (tbls) and synchronization-bound (QBFT) pairs. +Worth adopting for release builds (`[profile.release] lto = "thin", +codegen-units = 1`) as a free single-digit win, but it does not move any of +the structural findings above. + +## Where Pluto wins (no action needed) + +| pair | ratio | note | +|---|---|---| +| tier1/frost/round1 | 0.19x | 5x faster DKG round 1 | +| tier1/proto/qbft_marshal | 0.39x | prost vs Go protobuf | +| tier1/k1/verify | 0.48–0.54x | | +| tier1/frost/round2 | 0.50x | 2x faster DKG round 2 | +| tier1/tbls/verify_aggregate | 0.60x | blst vs herumi | +| tier1/tbls/verify / sign | 0.67x / 0.72x | blst vs herumi | +| tier3/cli/create_enr | 0.36x | | +| tier3/mem/create_cluster_rss | 0.39x | 14.7 MiB vs 37.6 MiB peak RSS | +| tier3/cli/create_cluster_4 / _10 (insecure) | 0.60x / 0.72x | | +| tier3/dkg/ceremony_4node | 1.00x | 125.7s vs 126.2s over real relay | + +## Status update (2026-07-15, after first optimization pass) + +Findings 2, 3 (partially), 4 (partially) and the build-config item are +addressed; measured on the same host, non-LTO bench profile: + +| pair | before | after | charon | ratio before → after | +|---|---|---|---|---| +| tier1/tbls/threshold_aggregate/3of4 | 407 µs | 205 µs | 300 µs | 1.35x → **0.68x** | +| tier1/tbls/threshold_aggregate/7of10 | 951 µs | 404 µs | 792 µs | 1.20x → **0.51x** | +| tier1/frost/aggregate | 346 µs | 148 µs | 300 µs | 1.15x → **0.49x** | +| tier1/tbls/threshold_split/7of10 | 15.1 µs | 10.0 µs | 5.0 µs | 3.0x → 2.0x | +| tier1/tbls/recover_secret/7of10 | 14.5 µs | 10.1 µs | 4.0 µs | 3.7x → 2.5x | +| tier1/tbls/threshold_split/3of4 | 3.9 µs | 3.3 µs | 1.7 µs | 2.3x → 2.0x | +| tier1/tbls/recover_secret/3of4 | 4.8 µs | 4.1 µs | 1.9 µs | 2.5x → 2.1x | + +What changed: + +- `blst_impl.rs`: Lagrange signature interpolation now uses blst Pippenger + multi-scalar multiplication with a single final affine conversion (was one + scalar mult + one field-inversion-costing affine conversion **per share**); + polynomial evaluation and secret interpolation moved to fr-domain Horner / + dot-product arithmetic (fr copies of secret material are volatile-wiped on + drop). Same treatment for `BlsSignature::from_partial_signatures` in + `pluto-frost`. +- `ssz_codec.rs`: all encoders pre-size their output buffers + (`ssz_bytes_len`), versioned encoders append the payload directly after the + header instead of encoding to a temporary `Vec` and copying, and the + unsigned Deneb/Electra/Fulu proposal path no longer **deep-clones the block, + KZG proofs and blobs** to serialize (borrowing `*BlockContentsRef` structs). + At the small bench fixture sizes this is ~2% on proposal encode and neutral + on attestation encode — the removed copy/clone scale with real block sizes + (MB-range with blobs). The residual attestation-encode gap vs fastssz sits + inside ethereum_ssz's derive machinery (its container encoder allocates an + internal variable-bytes buffer per call), which would need an upstream + change or hand-rolled encoders. +- `Cargo.toml`: release profile now sets `lto = "thin"`, `codegen-units = 1`. + +Remaining gaps, deliberate for now: + +- Shamir split/recover at ~2x: the residue is one checked `SecretKey` + conversion per share (kept — validates shares exactly like before) and + coefficient generation via HKDF `key_gen` where herumi draws raw CSPRNG + scalars. Ceremony-only path; not worth weakening key-generation hygiene. +- QBFT (finding 1) and scrypt/k1-sign (finding 5) untouched, as recommended. + +## Recommended order of work + +1. **SSZ encode buffer preallocation + header-copy removal** — small, contained + change in `ssz_codec.rs`, 3–4x on a per-message hot path. +2. **`lagrange_interpolate_signature` cleanup** (skip re-validation, avoid + affine round-trips, Pippenger) — per-duty win, contained in `blst_impl.rs`. +3. **QBFT instance lifecycle** — replace per-instance thread spawn and 50ms + cancellation polling; largest ratio but needs a design pass. +4. **Release profile: thin LTO + codegen-units=1** — free ~10% on Rust paths. +5. Ceremony-path items (scrypt, Shamir split, k1 sign) as background work. + +## Regenerating + +```bash +./perf/run.sh --tier 12 # tiers 1+2 (~20 min) +./perf/run.sh --tier 3 # process level (builds both binaries; DKG needs relay) +``` diff --git a/perf/README.md b/perf/README.md new file mode 100644 index 00000000..c084dabc --- /dev/null +++ b/perf/README.md @@ -0,0 +1,92 @@ +# Pluto vs Charon performance harness + +Automated performance comparison between Pluto (this repo) and the Go Charon +implementation. One command runs matching workloads on both sides, normalizes +the results, and renders a report that flags every pair where Pluto is slower +than Charon (`ratio > 1.15` → `SUBOPTIMAL`) — the work queue for optimization. + +## Quick start + +```bash +# Requires: the charon Go source at ./charon (or CHARON_SRC=...), Go with +# network access (GOTOOLCHAIN=auto downloads the pinned toolchain), python3. +# Tier 3 additionally requires hyperfine. + +./perf/run.sh --tier 12 # tiers 1+2, full sample counts +./perf/run.sh --tier 1 --quick # fast sanity pass +./perf/run.sh --tier all # everything incl. process-level (tier 3) +``` + +Outputs land in `perf/out/` (gitignored): + +- `report.md` — the human-readable comparison, sorted by pluto/charon ratio, + with a "Work on these" section listing SUBOPTIMAL pairs. +- `results.json` — machine-readable results (`{meta, results[]}`), suitable + for committing as `perf/baseline.json` to gate regressions. +- `go-bench.txt`, `hyperfine/*.json`, `dkg-times.json`, `cli-extra.json` — raw + inputs. + +## Tiers + +| Tier | What | How | +|---|---|---| +| 1 | Pure compute: BLS tbls (blst vs herumi), secp256k1, FROST DKG rounds, SSZ encode/decode/hash, protobuf | criterion benches in `crates/*/benches/` vs `go test -bench` in `perf/go-bench/` | +| 2 | In-memory components: full QBFT consensus instance (spawn → all decided), in-memory FROST DKG ceremony (Rust-only) | same, using in-process transports on both sides | +| 3 | Process level: `create enr` / `create cluster` wall time (hyperfine), full DKG ceremony via `scripts/dkg-runner`, peak RSS | `perf/cli-matrix.sh`, `perf/dkg-e2e.sh` | + +Tier 3's DKG ceremony needs relay connectivity (dkg-runner's default relay or +`RELAY_URL=...`); when it fails, run.sh logs a warning and the report simply +omits those rows. + +## How pairing works + +`perf/pairs.json` maps a canonical pair id (also the Rust criterion benchmark +id) to the Go benchmark name (`BenchmarkTier1TblsSign` → `Tier1TblsSign`). +Tier 3 inputs carry their own ids. Workloads are kept byte-identical across +languages: shared binary fixtures live in `perf/fixtures/` (generated once by +the Go side: `WRITE_FIXTURES=1 go test -run TestGenFixtures .` in +`perf/go-bench/`), and each Rust bench asserts a re-encode round-trip against +the fixture before timing, so a workload mismatch fails loudly instead of +comparing different work. + +Some pairs intentionally compare different backends doing the same production +job (noted in `workload`), e.g. `tier1/frost/partial_sign` pits Pluto's +kryptology-compatible blst signing against charon's herumi tbls signing — +that is what actually runs on each side during a mixed ceremony. + +## Adding a pair + +1. Add the Rust bench (`crates//benches/`, criterion id + `tierN//`; copy the `[[bench]]`/`[lib] bench = false` pattern). +2. Add the Go bench in `perf/go-bench/` named `BenchmarkTierN`. +3. Add the mapping to `perf/pairs.json`. +4. If the workload needs a fixture, extend `TestGenFixtures` and assert the + round-trip on the Rust side. + +## Build configs compared + +Benchmarks compare **as-shipped** configurations: Rust `--release` with the +workspace defaults (currently no LTO, `codegen-units = 16`) and Go's +`-trimpath -ldflags "-s -w"`. Tuning the Rust release profile is itself a +candidate optimization to evaluate with this harness. + +## CI + +`.github/workflows/perf.yml` runs tiers 1+2 nightly and on demand +(`workflow_dispatch`), pinning charon to the version in the workflow's +`CHARON_VERSION`. The report is appended to the job summary and uploaded as an +artifact. Regression gating: pass `--baseline perf/baseline.json` (a committed +blessed `results.json`); `render.py` exits 2 when any pair's ratio worsens +more than 10%, and refuses to compare baselines across differing os/arch. +Cross-language `SUBOPTIMAL` flags alone never fail CI — they are a work +queue, not a gate. + +## Notes / limitations + +- `bench-util` cargo features on `pluto-core` and `pluto-dkg` expose internal + modules and in-memory harnesses to the benches. Never enable in production. +- `pluto run` does not yet expose a Prometheus `/metrics` endpoint (the relay + does), so live-cluster latency comparison via the charon-mirroring + `core_*`/`p2p_*` metrics is not wired here yet. +- Numbers are per-platform (blst/herumi both ship per-arch assembly); do not + compare arm64-mac results against linux baselines. diff --git a/perf/cli-matrix.sh b/perf/cli-matrix.sh new file mode 100755 index 00000000..94c62fbd --- /dev/null +++ b/perf/cli-matrix.sh @@ -0,0 +1,133 @@ +#!/usr/bin/env bash +# Hyperfine matrix comparing pluto and charon CLI commands, plus peak-RSS +# capture for `create cluster`. +# +# Outputs: +# perf/out/hyperfine/tier3__cli__.json (hyperfine --export-json) +# perf/out/cli-extra.json (peak RSS entries) +# +# Environment: +# PLUTO_BIN Path to pluto binary. Default: target/release/pluto +# CHARON_BIN Path to charon binary. Default: perf/out/charon +# RUNS hyperfine runs per command (default 10) +# WARMUP hyperfine warmup runs (default 2) + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +OUT_DIR="${ROOT_DIR}/perf/out" +HF_DIR="${OUT_DIR}/hyperfine" +WORK_DIR="${OUT_DIR}/cli-matrix-work" + +: "${PLUTO_BIN:=${ROOT_DIR}/target/release/pluto}" +: "${CHARON_BIN:=${OUT_DIR}/charon}" +: "${RUNS:=10}" +: "${WARMUP:=2}" + +ADDR="0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF" + +log() { + printf '[cli-matrix] %s\n' "$*" >&2 +} + +for bin in "${PLUTO_BIN}" "${CHARON_BIN}"; do + if ! [[ -x "${bin}" ]]; then + log "ERROR: binary not found: ${bin}" + exit 1 + fi +done + +command -v hyperfine >/dev/null || { + log "ERROR: hyperfine is required" + exit 1 +} + +mkdir -p "${HF_DIR}" +rm -rf "${WORK_DIR}" +mkdir -p "${WORK_DIR}" + +# run_case +# Uses {dir} placeholder replaced with a per-command scratch dir. +run_case() { + local case_id="$1" + shift + + local pluto_dir="${WORK_DIR}/${case_id}-pluto" + local charon_dir="${WORK_DIR}/${case_id}-charon" + + local pluto_cmd charon_cmd + pluto_cmd="${PLUTO_BIN} $(printf '%s ' "${@//\{dir\}/${pluto_dir}}")" + charon_cmd="${CHARON_BIN} $(printf '%s ' "${@//\{dir\}/${charon_dir}}")" + + log "case ${case_id}" + hyperfine \ + --warmup "${WARMUP}" \ + --runs "${RUNS}" \ + --prepare "rm -rf ${pluto_dir} ${charon_dir}" \ + --command-name pluto "${pluto_cmd}" \ + --command-name charon "${charon_cmd}" \ + --export-json "${HF_DIR}/tier3__cli__${case_id}.json" +} + +run_case create_enr create enr --data-dir='{dir}' + +run_case create_cluster_4 create cluster \ + --cluster-dir='{dir}' \ + --nodes=4 --threshold=3 --num-validators=1 --network=goerli \ + --fee-recipient-addresses="${ADDR}" --withdrawal-addresses="${ADDR}" \ + --insecure-keys + +run_case create_cluster_10 create cluster \ + --cluster-dir='{dir}' \ + --nodes=10 --threshold=7 --num-validators=10 --network=goerli \ + --fee-recipient-addresses="${ADDR}" --withdrawal-addresses="${ADDR}" \ + --insecure-keys + +run_case create_cluster_4_secure create cluster \ + --cluster-dir='{dir}' \ + --nodes=4 --threshold=3 --num-validators=1 --network=goerli \ + --fee-recipient-addresses="${ADDR}" --withdrawal-addresses="${ADDR}" + +# --- Peak RSS of create cluster (10 nodes) ----------------------------------- + +peak_rss_bytes() { + local bin="$1" dir="$2" + local time_output rss + + rm -rf "${dir}" + + if [[ "$(uname)" == "Darwin" ]]; then + time_output=$({ /usr/bin/time -l "${bin}" create cluster \ + --cluster-dir="${dir}" \ + --nodes=10 --threshold=7 --num-validators=10 --network=goerli \ + --fee-recipient-addresses="${ADDR}" --withdrawal-addresses="${ADDR}" \ + --insecure-keys >/dev/null; } 2>&1) + rss=$(awk '/maximum resident set size/ {print $1}' <<<"${time_output}") + else + time_output=$({ /usr/bin/time -v "${bin}" create cluster \ + --cluster-dir="${dir}" \ + --nodes=10 --threshold=7 --num-validators=10 --network=goerli \ + --fee-recipient-addresses="${ADDR}" --withdrawal-addresses="${ADDR}" \ + --insecure-keys >/dev/null; } 2>&1) + rss=$(awk -F': ' '/Maximum resident set size/ {print $2 * 1024}' <<<"${time_output}") + fi + + printf '%s' "${rss}" +} + +log "capturing peak RSS for create cluster (10 nodes)" +PLUTO_RSS=$(peak_rss_bytes "${PLUTO_BIN}" "${WORK_DIR}/rss-pluto") +CHARON_RSS=$(peak_rss_bytes "${CHARON_BIN}" "${WORK_DIR}/rss-charon") + +cat >"${OUT_DIR}/cli-extra.json" <, "charon_value": }] +# +# Environment: +# PLUTO_BIN Path to pluto binary. Default: target/release/pluto +# CHARON_BIN Path to charon binary. Default: perf/out/charon +# NODES=4 THRESHOLD=3 REPS=3 +# RELAY_URL Forwarded to dkg-runner (defaults to its built-in relay). +# TIMEOUT Per-ceremony timeout seconds (default 300). + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +OUT_DIR="${ROOT_DIR}/perf/out" + +: "${PLUTO_BIN:=${ROOT_DIR}/target/release/pluto}" +: "${CHARON_BIN:=${OUT_DIR}/charon}" +: "${NODES:=4}" +: "${THRESHOLD:=3}" +: "${REPS:=3}" +: "${TIMEOUT:=300}" + +log() { + printf '[dkg-e2e] %s\n' "$*" >&2 +} + +for bin in "${PLUTO_BIN}" "${CHARON_BIN}"; do + if ! [[ -x "${bin}" ]]; then + log "ERROR: binary not found: ${bin}" + exit 1 + fi +done + +mkdir -p "${OUT_DIR}" + +# run_variant : prints median seconds. +run_variant() { + local variant="$1" pluto_nodes="$2" charon_nodes="$3" + local durations=() + + for rep in $(seq 1 "${REPS}"); do + local work_dir="${OUT_DIR}/dkg-run-${variant}-${rep}" + rm -rf "${work_dir}" + + log "variant=${variant} rep=${rep}/${REPS}" + + local start end + start=$(python3 -c 'import time; print(time.time())') + + NODES="${NODES}" \ + THRESHOLD="${THRESHOLD}" \ + PLUTO_NODES="${pluto_nodes}" \ + CHARON_NODES="${charon_nodes}" \ + PLUTO_BIN="${PLUTO_BIN}" \ + CHARON_BIN="${CHARON_BIN}" \ + WORK_DIR="${work_dir}" \ + TIMEOUT="${TIMEOUT}" \ + RUN_SMOKE_VERIFY=0 \ + CI=1 \ + "${ROOT_DIR}/scripts/dkg-runner/run.sh" >"${work_dir}.log" 2>&1 || { + log "variant=${variant} rep=${rep} FAILED (log: ${work_dir}.log)" + return 1 + } + + end=$(python3 -c 'import time; print(time.time())') + durations+=("$(python3 -c "print(${end} - ${start})")") + log "variant=${variant} rep=${rep} took ${durations[-1]}s" + done + + python3 -c "import statistics, sys; print(statistics.median([float(x) for x in sys.argv[1:]]))" "${durations[@]}" +} + +PLUTO_MEDIAN=$(run_variant pluto "${NODES}" 0) +CHARON_MEDIAN=$(run_variant charon 0 "${NODES}") + +cat >"${OUT_DIR}/dkg-times.json" < ../../charon + +// Charon's own replace directives must be mirrored here because replace +// directives only take effect in the main module, never transitively. +replace github.com/coinbase/kryptology => github.com/ObolNetwork/kryptology v0.1.0 + +replace github.com/attestantio/go-eth2-client => github.com/ObolNetwork/go-eth2-client v0.27.1-obol.1 diff --git a/perf/go-bench/go.sum b/perf/go-bench/go.sum new file mode 100644 index 00000000..8fbb13be --- /dev/null +++ b/perf/go-bench/go.sum @@ -0,0 +1,617 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo= +dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= +dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= +dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= +dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= +git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/ObolNetwork/go-eth2-client v0.27.1-obol.1 h1:bnUqEOoHVnIDXpDp8YwOUWWZuqr2xB9FGtuzqF/+rSI= +github.com/ObolNetwork/go-eth2-client v0.27.1-obol.1/go.mod h1:fvULSL9WtNskkOB4i+Yyr6BKpNHXvmpGZj9969fCrfY= +github.com/ObolNetwork/kryptology v0.1.0 h1:AhoG4My70+xMhEJSpVaJay/t+T/vIUNHQYLjsDJHulI= +github.com/ObolNetwork/kryptology v0.1.0/go.mod h1:/Wl7Js2f676GyXZDTaojf/O+l0fxFPWudbyjdFhkpSA= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= +github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= +github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= +github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/containerd/cgroups v0.0.0-20201119153540-4cbc285b3327/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE= +github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= +github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= +github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= +github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c h1:pFUpOrbxDR6AkioZ1ySsx5yxlDQZ8stG2b88gTPxgJU= +github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c/go.mod h1:6UhI8N9EjYm1c2odKpFpAYeR8dsBeM7PtzQhRgxRr9U= +github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8= +github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/elastic/gosigar v0.12.0/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs= +github.com/elastic/gosigar v0.14.3 h1:xwkKwPia+hSfg9GqrCUKYdId102m9qTJIIr7egmK/uo= +github.com/elastic/gosigar v0.14.3/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs= +github.com/emicklei/dot v1.8.0 h1:HnD60yAKFAevNeT+TPYr9pb8VB9bqdeSo0nzwIW6IOI= +github.com/emicklei/dot v1.8.0/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= +github.com/ferranbt/fastssz v1.0.0 h1:9EXXYsracSqQRBQiHeaVsG/KQeYblPf40hsQPb9Dzk8= +github.com/ferranbt/fastssz v1.0.0/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/flynn/noise v1.1.0 h1:KjPQoQCEFdZDiP03phOvGi11+SVVhBG2wOWAorLsstg= +github.com/flynn/noise v1.1.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag= +github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= +github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/goccy/go-yaml v1.17.0 h1:JJhayi67p5LeTLh9UJYFhayPIOGDZjAqQNoEzHhYvik= +github.com/goccy/go-yaml v1.17.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= +github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= +github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= +github.com/herumi/bls-eth-go-binary v1.36.4 h1:yff41RSbfyZwfE1NF/qddP5nXhgdU0c3RGOpYOoM7YM= +github.com/herumi/bls-eth-go-binary v1.36.4/go.mod h1:luAnRm3OsMQeokhGzpYmc0ZKwawY7o87PUEP11Z7r7U= +github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA= +github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= +github.com/huandu/go-assert v1.1.5 h1:fjemmA7sSfYHJD7CUqs9qTwwfdNAx7/j2/ZlHXzNB3c= +github.com/huandu/go-assert v1.1.5/go.mod h1:yOLvuqZwmcHIC5rIzrBhT7D3Q9c3GFnd0JrPVhn/06U= +github.com/huandu/go-clone v1.7.2 h1:3+Aq0Ed8XK+zKkLjE2dfHg0XrpIfcohBE1K+c8Usxoo= +github.com/huandu/go-clone v1.7.2/go.mod h1:ReGivhG6op3GYr+UY3lS6mxjKp7MIGTknuU5TbTVaXE= +github.com/huandu/go-clone/generic v1.6.0 h1:Wgmt/fUZ28r16F2Y3APotFD59sHk1p78K0XLdbUYN5U= +github.com/huandu/go-clone/generic v1.6.0/go.mod h1:xgd9ZebcMsBWWcBx5mVMCoqMX24gLWr5lQicr+nVXNs= +github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= +github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= +github.com/ipfs/go-cid v0.5.0 h1:goEKKhaGm0ul11IHA7I6p1GmKz8kEYniqFopaB5Otwg= +github.com/ipfs/go-cid v0.5.0/go.mod h1:0L7vmeNXpQpUS9vt+yEARkJ8rOg43DF3iPgn4GIN0mk= +github.com/ipfs/go-log/v2 v2.8.1 h1:Y/X36z7ASoLJaYIJAL4xITXgwf7RVeqb1+/25aq/Xk0= +github.com/ipfs/go-log/v2 v2.8.1/go.mod h1:NyhTBcZmh2Y55eWVjOeKf8M7e4pnJYM3yDZNxQBWEEY= +github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= +github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/jbenet/go-temp-err-catcher v0.1.0 h1:zpb3ZH6wIE8Shj2sKS+khgRvf7T7RABoLk/+KKHggpk= +github.com/jbenet/go-temp-err-catcher v0.1.0/go.mod h1:0kJRvmDZXNMIiJirNPEYfhpPwbGVtZVWC34vc5WLsDk= +github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= +github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= +github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jsternberg/zap-logfmt v1.3.0 h1:z1n1AOHVVydOOVuyphbOKyR4NICDQFiJMn1IK5hVQ5Y= +github.com/jsternberg/zap-logfmt v1.3.0/go.mod h1:N3DENp9WNmCZxvkBD/eReWwz1149BK6jEN9cQ4fNwZE= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/koron/go-ssdp v0.0.5 h1:E1iSMxIs4WqxTbIBLtmNBeOOC+1sCIXQeqTWVnpmwhk= +github.com/koron/go-ssdp v0.0.5/go.mod h1:Qm59B7hpKpDqfyRNWRNr00jGwLdXjDyZh6y7rH6VS0w= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= +github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= +github.com/libp2p/go-flow-metrics v0.2.0 h1:EIZzjmeOE6c8Dav0sNv35vhZxATIXWZg6j/C08XmmDw= +github.com/libp2p/go-flow-metrics v0.2.0/go.mod h1:st3qqfu8+pMfh+9Mzqb2GTiwrAGjIPszEjZmtksN8Jc= +github.com/libp2p/go-libp2p v0.41.1 h1:8ecNQVT5ev/jqALTvisSJeVNvXYJyK4NhQx1nNRXQZE= +github.com/libp2p/go-libp2p v0.41.1/go.mod h1:DcGTovJzQl/I7HMrby5ZRjeD0kQkGiy+9w6aEkSZpRI= +github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94= +github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8= +github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUIK5WDu6iPUA= +github.com/libp2p/go-libp2p-testing v0.12.0/go.mod h1:KcGDRXyN7sQCllucn1cOOS+Dmm7ujhfEyXQL5lvkcPg= +github.com/libp2p/go-msgio v0.3.0 h1:mf3Z8B1xcFN314sWX+2vOTShIE0Mmn2TXn3YCUQGNj0= +github.com/libp2p/go-msgio v0.3.0/go.mod h1:nyRM819GmVaF9LX3l03RMh10QdOroF++NBbxAb0mmDM= +github.com/libp2p/go-netroute v0.2.2 h1:Dejd8cQ47Qx2kRABg6lPwknU7+nBnFRpko45/fFPuZ8= +github.com/libp2p/go-netroute v0.2.2/go.mod h1:Rntq6jUAH0l9Gg17w5bFGhcC9a+vk4KNXs6s7IljKYE= +github.com/libp2p/go-reuseport v0.4.0 h1:nR5KU7hD0WxXCJbmw7r2rhRYruNRl2koHw8fQscQm2s= +github.com/libp2p/go-reuseport v0.4.0/go.mod h1:ZtI03j/wO5hZVDFo2jKywN6bYKWLOy8Se6DrI2E1cLU= +github.com/libp2p/go-yamux/v5 v5.0.0 h1:2djUh96d3Jiac/JpGkKs4TO49YhsfLopAoryfPmf+Po= +github.com/libp2p/go-yamux/v5 v5.0.0/go.mod h1:en+3cdX51U0ZslwRdRLrvQsdayFt3TSUKvBGErzpWbU= +github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= +github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd h1:br0buuQ854V8u83wA0rVZ8ttrq5CpaPZdvrK0LP2lOk= +github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd/go.mod h1:QuCEs1Nt24+FYQEqAAncTDPJIuGs+LxK1MCiFL25pMU= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= +github.com/miekg/dns v1.1.64 h1:wuZgD9wwCE6XMT05UU/mlSko71eRSXEAm2EbjQXLKnQ= +github.com/miekg/dns v1.1.64/go.mod h1:Dzw9769uoKVaLuODMDZz9M6ynFU6Em65csPuoi8G0ck= +github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c h1:bzE/A84HN25pxAuk9Eej1Kz9OUelF97nAc82bDquQI8= +github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c/go.mod h1:0SQS9kMwD2VsyFEB++InYyBJroV/FRmBgcydeSUcJms= +github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b h1:z78hV3sbSMAUoyUMM0I83AUIT6Hu17AWfgjzIbtrYFc= +github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b/go.mod h1:lxPUiZwKoFL8DUUmalo2yJJUCxbPKtm8OKfqr2/FTNU= +github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc h1:PTfri+PuQmWDqERdnNMiD9ZejrlswWrCpBEZgWOiTrc= +github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc/go.mod h1:cGKTAVKx4SxOuR/czcZ/E2RSJ3sfHs8FpHhQ5CWMf9s= +github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8RvIylQ358TN4wwqatJ8rNavkEINozVn9DtGI3dfQ= +github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= +github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= +github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mr-tron/base58 v1.1.2/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= +github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE= +github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= +github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= +github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= +github.com/multiformats/go-multiaddr v0.1.1/go.mod h1:aMKBKNEYmzmDmxfX88/vz+J5IU55txyt0p4aiWVohjo= +github.com/multiformats/go-multiaddr v0.16.1 h1:fgJ0Pitow+wWXzN9do+1b8Pyjmo8m5WhGfzpL82MpCw= +github.com/multiformats/go-multiaddr v0.16.1/go.mod h1:JSVUmXDjsVFiW7RjIFMP7+Ev+h1DTbiJgVeTV/tcmP0= +github.com/multiformats/go-multiaddr-dns v0.4.1 h1:whi/uCLbDS3mSEUMb1MsoT4uzUeZB0N32yzufqS0i5M= +github.com/multiformats/go-multiaddr-dns v0.4.1/go.mod h1:7hfthtB4E4pQwirrz+J0CcDUfbWzTqEzVyYKKIKpgkc= +github.com/multiformats/go-multiaddr-fmt v0.1.0 h1:WLEFClPycPkp4fnIzoFoV9FVd49/eQsuaL3/CWe167E= +github.com/multiformats/go-multiaddr-fmt v0.1.0/go.mod h1:hGtDIW4PU4BqJ50gW2quDuPVjyWNZxToGUh/HwTZYJo= +github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g= +github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= +github.com/multiformats/go-multicodec v0.9.0 h1:pb/dlPnzee/Sxv/j4PmkDRxCOi3hXTz3IbPKOXWJkmg= +github.com/multiformats/go-multicodec v0.9.0/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI16i14xuaojr/H7Ai54k= +github.com/multiformats/go-multihash v0.0.8/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa63epEDmG8nTduyAew= +github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= +github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= +github.com/multiformats/go-multistream v0.6.0 h1:ZaHKbsL404720283o4c/IHQXiS6gb8qAN5EIJ4PN5EA= +github.com/multiformats/go-multistream v0.6.0/go.mod h1:MOyoG5otO24cHIg8kf9QW2/NozURlkP/rvi2FQJyCPg= +github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= +github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= +github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= +github.com/onsi/ginkgo/v2 v2.23.3 h1:edHxnszytJ4lD9D5Jjc4tiDkPBZ3siDeJJkUZJJVkp0= +github.com/onsi/ginkgo/v2 v2.23.3/go.mod h1:zXTP6xIp3U8aVuXN8ENK9IXRaTjFnpVB9mGmaSRvxnM= +github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= +github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= +github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.2.1 h1:S4k4ryNgEpxW1dzyqffOmhI1BHYcjzU8lpJfSlR0xww= +github.com/opencontainers/runtime-spec v1.2.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= +github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= +github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= +github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o= +github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M= +github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= +github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk= +github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE= +github.com/pion/dtls/v3 v3.0.6 h1:7Hkd8WhAJNbRgq9RgdNh1aaWlZlGpYTzdqjy9x9sK2E= +github.com/pion/dtls/v3 v3.0.6/go.mod h1:iJxNQ3Uhn1NZWOMWlLxEEHAN5yX7GyPvvKw04v9bzYU= +github.com/pion/ice/v4 v4.0.9 h1:VKgU4MwA2LUDVLq+WBkpEHTcAb8c5iCvFMECeuPOZNk= +github.com/pion/ice/v4 v4.0.9/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw= +github.com/pion/interceptor v0.1.39 h1:Y6k0bN9Y3Lg/Wb21JBWp480tohtns8ybJ037AGr9UuA= +github.com/pion/interceptor v0.1.39/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic= +github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= +github.com/pion/logging v0.2.3 h1:gHuf0zpoh1GW67Nr6Gj4cv5Z9ZscU7g/EaoC/Ke/igI= +github.com/pion/logging v0.2.3/go.mod h1:z8YfknkquMe1csOrxK5kc+5/ZPAzMxbKLX5aXpbpC90= +github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM= +github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA= +github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= +github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= +github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo= +github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0= +github.com/pion/rtp v1.8.18 h1:yEAb4+4a8nkPCecWzQB6V/uEU18X1lQCGAQCjP+pyvU= +github.com/pion/rtp v1.8.18/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk= +github.com/pion/sctp v1.8.37 h1:ZDmGPtRPX9mKCiVXtMbTWybFw3z/hVKAZgU81wcOrqs= +github.com/pion/sctp v1.8.37/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE= +github.com/pion/sdp/v3 v3.0.11 h1:VhgVSopdsBKwhCFoyyPmT1fKMeV9nLMrEKxNOdy3IVI= +github.com/pion/sdp/v3 v3.0.11/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E= +github.com/pion/srtp/v3 v3.0.4 h1:2Z6vDVxzrX3UHEgrUyIGM4rRouoC7v+NiF1IHtp9B5M= +github.com/pion/srtp/v3 v3.0.4/go.mod h1:1Jx3FwDoxpRaTh1oRV8A/6G1BnFL+QI82eK4ms8EEJQ= +github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4= +github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8= +github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw= +github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU= +github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= +github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0= +github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q= +github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E= +github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= +github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= +github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM= +github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA= +github.com/pion/webrtc/v4 v4.0.14 h1:nyds/sFRR+HvmWoBa6wrL46sSfpArE0qR883MBW96lg= +github.com/pion/webrtc/v4 v4.0.14/go.mod h1:R3+qTnQTS03UzwDarYecgioNf7DYgTsldxnCXB821Kk= +github.com/pk910/dynamic-ssz v0.0.6 h1:Tu97LSc2TtCyqRfoSbhG9XuR/FbA7CkKeAnlkgUydFY= +github.com/pk910/dynamic-ssz v0.0.6/go.mod h1:b6CrLaB2X7pYA+OSEEbkgXDEcRnjLOZIxZTsMuO/Y9c= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/protolambda/eth2-shuffle v1.1.0 h1:gixIBI84IeugTwwHXm8vej1bSSEhueBCSryA4lAKRLU= +github.com/protolambda/eth2-shuffle v1.1.0/go.mod h1:FhA2c0tN15LTC+4T9DNVm+55S7uXTTjQ8TQnBuXlkF8= +github.com/prysmaticlabs/go-bitfield v0.0.0-20240618144021-706c95b2dd15 h1:lC8kiphgdOBTcbTvo8MwkvpKjO0SlAgjv4xIK5FGJ94= +github.com/prysmaticlabs/go-bitfield v0.0.0-20240618144021-706c95b2dd15/go.mod h1:8svFBIKKu31YriBG/pNizo9N0Jr9i5PQ+dFkxWg3x5k= +github.com/prysmaticlabs/gohashtree v0.0.4-beta h1:H/EbCuXPeTV3lpKeXGPpEV9gsUpkqOOVnWapUyeWro4= +github.com/prysmaticlabs/gohashtree v0.0.4-beta/go.mod h1:BFdtALS+Ffhg3lGQIHv9HDWuHS8cTvHZzrHWxwOtGOs= +github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= +github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= +github.com/quic-go/quic-go v0.50.1 h1:unsgjFIUqW8a2oopkY7YNONpV1gYND6Nt9hnt1PN94Q= +github.com/quic-go/quic-go v0.50.1/go.mod h1:Vim6OmUvlYdwBhXP9ZVrtGmCMWa3wEqhq3NgYrI8b4E= +github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66 h1:4WFk6u3sOT6pLa1kQ50ZVdm8BQFgJNA117cepZxtLIg= +github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66/go.mod h1:Vp72IJajgeOL6ddqrAhmp7IM9zbTcgkQxD/YdxrVwMw= +github.com/r3labs/sse/v2 v2.10.0 h1:hFEkLLFY4LDifoHdiCN/LlGBAdVJYsANaLqNYa1l/v0= +github.com/r3labs/sse/v2 v2.10.0/go.mod h1:Igau6Whc+F17QUgML1fYe1VPZzTV6EMCnYktEmkNJ7I= +github.com/raulk/go-watchdog v1.3.0 h1:oUmdlHxdkXRJlwfG0O9omj8ukerm8MEQavSiDTEtBsk= +github.com/raulk/go-watchdog v1.3.0/go.mod h1:fIvOnLbF0b0ZwkB9YU4mOW9Did//4vPZtDqv66NfsMU= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= +github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= +github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= +github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= +github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= +github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= +github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw= +github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI= +github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU= +github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag= +github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg= +github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw= +github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y= +github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= +github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q= +github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ= +github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I= +github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0= +github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ= +github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk= +github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= +github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= +github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= +github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= +github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= +github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= +github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= +github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 h1:kJxSDN4SgWWTjG/hPp3O7LCGLcHXFlvS2/FFOrwL+SE= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0/go.mod h1:mgIOzS7iZeKJdeB8/NYHrJ48fdGc71Llo5bJ1J4DWUE= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= +go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= +go.uber.org/dig v1.18.1 h1:rLww6NuajVjeQn+49u5NcezUJEGwd5uXmyoCKW2g5Es= +go.uber.org/dig v1.18.1/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= +go.uber.org/fx v1.23.0 h1:lIr/gYWQGfTwGcSXWXu4vP5Ws6iqnNEIY+F/aFzCKTg= +go.uber.org/fx v1.23.0/go.mod h1:o/D9n+2mLP6v1EG+qsdT1O8wKopYAsqZasju97SDFCU= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= +golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= +golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= +golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= +golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= +golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= +golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191116160921-f9c825593386/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= +golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20180810173357-98c5dad5d1a0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053 h1:dHQOQddU4YHS5gY33/6klKjq7Gp3WwMyOXGNp5nzRj8= +golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053/go.mod h1:+nZKN+XVh4LCiA9DV3ywrzN4gumyCnKjau3NGb9SGoE= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= +golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= +golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI= +golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= +golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= +google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY= +google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc= +google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4= +google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/Knetic/govaluate.v3 v3.0.0 h1:18mUyIt4ZlRlFZAAfVetz4/rzlJs9yhN+U02F4u1AOc= +gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E= +gopkg.in/cenkalti/backoff.v1 v1.1.0 h1:Arh75ttbsvlpVA7WtVpH4u9h6Zl46xuptxqLxPiSo4Y= +gopkg.in/cenkalti/backoff.v1 v1.1.0/go.mod h1:J6Vskwqd+OMVJl8C33mmtxTBs2gyzfv7UDAkHu8BrjI= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +lukechampine.com/blake3 v1.4.0 h1:xDbKOZCVbnZsfzM6mHSYcGRHZ3YrLDzqz8XnV4uaD5w= +lukechampine.com/blake3 v1.4.0/go.mod h1:MQJNQCTnR+kwOP/JEZSxj3MaQjp80FOFSNMMHXcSeX0= +sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= +sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= diff --git a/perf/go-bench/k1util_bench_test.go b/perf/go-bench/k1util_bench_test.go new file mode 100644 index 00000000..367c8048 --- /dev/null +++ b/perf/go-bench/k1util_bench_test.go @@ -0,0 +1,78 @@ +package perfbench + +import ( + "testing" + + k1 "github.com/decred/dcrd/dcrec/secp256k1/v4" + "github.com/obolnetwork/charon/app/k1util" +) + +// digest32 mirrors the 32-byte all-zero digest used by the Rust k1util bench. +var digest32 = make([]byte, 32) + +func BenchmarkTier1K1Sign(b *testing.B) { + key, err := k1.GeneratePrivateKey() + if err != nil { + b.Fatal(err) + } + + b.ReportAllocs() + + for b.Loop() { + if _, err := k1util.Sign(key, digest32); err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkTier1K1Recover(b *testing.B) { + key, err := k1.GeneratePrivateKey() + if err != nil { + b.Fatal(err) + } + + sig, err := k1util.Sign(key, digest32) + if err != nil { + b.Fatal(err) + } + + b.ReportAllocs() + + for b.Loop() { + recovered, err := k1util.Recover(digest32, sig) + if err != nil { + b.Fatal(err) + } + + if !recovered.IsEqual(key.PubKey()) { + b.Fatal("recovered wrong public key") + } + } +} + +func BenchmarkTier1K1Verify(b *testing.B) { + key, err := k1.GeneratePrivateKey() + if err != nil { + b.Fatal(err) + } + + sig, err := k1util.Sign(key, digest32) + if err != nil { + b.Fatal(err) + } + + pubkey := key.PubKey() + + b.ReportAllocs() + + for b.Loop() { + ok, err := k1util.Verify64(pubkey, digest32, sig[:64]) + if err != nil { + b.Fatal(err) + } + + if !ok { + b.Fatal("signature did not verify") + } + } +} diff --git a/perf/go-bench/proto_bench_test.go b/perf/go-bench/proto_bench_test.go new file mode 100644 index 00000000..e35059ad --- /dev/null +++ b/perf/go-bench/proto_bench_test.go @@ -0,0 +1,38 @@ +package perfbench + +import ( + "testing" + + pbv1 "github.com/obolnetwork/charon/core/corepb/v1" + "google.golang.org/protobuf/proto" +) + +func BenchmarkTier1ProtoQbftMarshal(b *testing.B) { + data := loadFixture(b, "qbft_consensus_msg.pb") + + msg := new(pbv1.QBFTConsensusMsg) + if err := proto.Unmarshal(data, msg); err != nil { + b.Fatal(err) + } + + b.ReportAllocs() + + for b.Loop() { + if _, err := proto.Marshal(msg); err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkTier1ProtoQbftUnmarshal(b *testing.B) { + data := loadFixture(b, "qbft_consensus_msg.pb") + + b.ReportAllocs() + + for b.Loop() { + msg := new(pbv1.QBFTConsensusMsg) + if err := proto.Unmarshal(data, msg); err != nil { + b.Fatal(err) + } + } +} diff --git a/perf/go-bench/qbft_bench_test.go b/perf/go-bench/qbft_bench_test.go new file mode 100644 index 00000000..105e047b --- /dev/null +++ b/perf/go-bench/qbft_bench_test.go @@ -0,0 +1,172 @@ +package perfbench + +import ( + "context" + "sync" + "testing" + "time" + + "github.com/obolnetwork/charon/core/qbft" +) + +// Mirrors the Rust tier2 QBFT bench: N in-process participants, i64 values, +// never-firing round timers, blocking fan-out broadcast, value 42, measured +// from spawn to all processes deciding. + +const ( + qbftInstance = int64(1) + qbftValue = int64(42) + qbftFIFOLimit = 100 +) + +type benchMsg struct { + typ qbft.MsgType + inst int64 + source int64 + round int64 + value int64 + pr int64 + pv int64 + justify []qbft.Msg[int64, int64, int64] +} + +var _ qbft.Msg[int64, int64, int64] = benchMsg{} + +func (m benchMsg) Type() qbft.MsgType { return m.typ } + +func (m benchMsg) Instance() int64 { return m.inst } + +func (m benchMsg) Source() int64 { return m.source } + +func (m benchMsg) Round() int64 { return m.round } + +func (m benchMsg) Value() int64 { return m.value } + +func (m benchMsg) ValueSource() (int64, error) { return m.value, nil } + +func (m benchMsg) PreparedRound() int64 { return m.pr } + +func (m benchMsg) PreparedValue() int64 { return m.pv } + +func (m benchMsg) Justification() []qbft.Msg[int64, int64, int64] { return m.justify } + +// runQbftConsensus runs one happy-path consensus instance with n processes +// and returns once every process has decided. +func runQbftConsensus(b *testing.B, n int64) { + b.Helper() + + // Setup is excluded from timing, matching the Rust bench which times from + // spawn to all-decided. + b.StopTimer() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + receives := make([]chan qbft.Msg[int64, int64, int64], n) + for i := range receives { + receives[i] = make(chan qbft.Msg[int64, int64, int64], 1024) + } + + decided := make(chan int64, n) + + def := qbft.Definition[int64, int64, int64]{ + IsLeader: func(instance int64, round, process int64) bool { + return (instance+round)%n == process + }, + NewTimer: func(round int64) (<-chan time.Time, func()) { + return make(chan time.Time), func() {} + }, + Compare: func(_ context.Context, _ qbft.Msg[int64, int64, int64], _ <-chan int64, + _ int64, returnErr chan error, _ chan int64, + ) { + returnErr <- nil + }, + Decide: func(_ context.Context, _ int64, value int64, _ []qbft.Msg[int64, int64, int64]) { + decided <- value + }, + LogUponRule: func(context.Context, int64, int64, int64, qbft.Msg[int64, int64, int64], qbft.UponRule) { + }, + LogRoundChange: func(context.Context, int64, int64, int64, int64, qbft.UponRule, []qbft.Msg[int64, int64, int64]) { + }, + LogUnjust: func(context.Context, int64, int64, qbft.Msg[int64, int64, int64]) {}, + Nodes: int(n), + FIFOLimit: qbftFIFOLimit, + } + + broadcast := func(ctx context.Context, typ qbft.MsgType, instance int64, source int64, + round int64, value int64, pr int64, pv int64, justification []qbft.Msg[int64, int64, int64], + ) error { + msg := benchMsg{ + typ: typ, + inst: instance, + source: source, + round: round, + value: value, + pr: pr, + pv: pv, + justify: justification, + } + + for _, ch := range receives { + select { + case ch <- msg: + case <-ctx.Done(): + return ctx.Err() + } + } + + return nil + } + + var wg sync.WaitGroup + + b.StartTimer() + + for i := int64(1); i <= n; i++ { + trans := qbft.Transport[int64, int64, int64]{ + Broadcast: broadcast, + Receive: receives[i-1], + } + + wg.Add(1) + + go func(i int64) { + defer wg.Done() + // Returns context.Canceled after the bench cancels below. + _ = qbft.Run(ctx, def, trans, qbftInstance, i, + qbft.InputValue(qbftValue), qbft.InputValueSource(qbftValue)) + }(i) + } + + for i := int64(0); i < n; i++ { + select { + case <-decided: + case <-time.After(30 * time.Second): + b.Fatal("timed out waiting for decide") + } + } + + // Teardown is excluded from timing, matching the Rust bench. + b.StopTimer() + cancel() + wg.Wait() + b.StartTimer() +} + +func BenchmarkTier2QbftDecide(b *testing.B) { + for _, tc := range []struct { + name string + n int64 + }{ + {name: "4of4", n: 4}, + {name: "7of10", n: 10}, + } { + b.Run(tc.name, func(b *testing.B) { + b.ReportAllocs() + + for b.Loop() { + runQbftConsensus(b, tc.n) + } + }) + } +} diff --git a/perf/go-bench/ssz_bench_test.go b/perf/go-bench/ssz_bench_test.go new file mode 100644 index 00000000..cb38a22e --- /dev/null +++ b/perf/go-bench/ssz_bench_test.go @@ -0,0 +1,85 @@ +package perfbench + +import ( + "testing" + + eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/obolnetwork/charon/core" +) + +func BenchmarkTier1SszAttEncode(b *testing.B) { + data := loadFixture(b, "phase0_attestation.ssz") + + att := new(eth2p0.Attestation) + if err := att.UnmarshalSSZ(data); err != nil { + b.Fatal(err) + } + + b.ReportAllocs() + + for b.Loop() { + if _, err := att.MarshalSSZ(); err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkTier1SszAttDecode(b *testing.B) { + data := loadFixture(b, "phase0_attestation.ssz") + + b.ReportAllocs() + + for b.Loop() { + att := new(eth2p0.Attestation) + if err := att.UnmarshalSSZ(data); err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkTier1SszAttHashRoot(b *testing.B) { + data := loadFixture(b, "phase0_attestation.ssz") + + att := new(eth2p0.Attestation) + if err := att.UnmarshalSSZ(data); err != nil { + b.Fatal(err) + } + + b.ReportAllocs() + + for b.Loop() { + if _, err := att.HashTreeRoot(); err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkTier1SszProposalEncode(b *testing.B) { + data := loadFixture(b, "deneb_signed_proposal.ssz") + + proposal := new(core.VersionedSignedProposal) + if err := proposal.UnmarshalSSZ(data); err != nil { + b.Fatal(err) + } + + b.ReportAllocs() + + for b.Loop() { + if _, err := proposal.MarshalSSZ(); err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkTier1SszProposalDecode(b *testing.B) { + data := loadFixture(b, "deneb_signed_proposal.ssz") + + b.ReportAllocs() + + for b.Loop() { + proposal := new(core.VersionedSignedProposal) + if err := proposal.UnmarshalSSZ(data); err != nil { + b.Fatal(err) + } + } +} diff --git a/perf/go-bench/tbls_bench_test.go b/perf/go-bench/tbls_bench_test.go new file mode 100644 index 00000000..ac2d2321 --- /dev/null +++ b/perf/go-bench/tbls_bench_test.go @@ -0,0 +1,198 @@ +// Package perfbench benchmarks charon's hot paths with workloads that mirror +// the Pluto criterion benches one-to-one. Pair mapping lives in perf/pairs.json. +package perfbench + +import ( + "testing" + + "github.com/obolnetwork/charon/tbls" +) + +// msg32 mirrors the 32-byte all-zero message used by the Rust benches. +var msg32 = make([]byte, 32) + +type splitCase struct { + name string + total uint + threshold uint +} + +var splitCases = []splitCase{ + {name: "3of4", total: 4, threshold: 3}, + {name: "7of10", total: 10, threshold: 7}, +} + +func BenchmarkTier1TblsSign(b *testing.B) { + secret, err := tbls.GenerateSecretKey() + if err != nil { + b.Fatal(err) + } + + b.ReportAllocs() + + for b.Loop() { + if _, err := tbls.Sign(secret, msg32); err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkTier1TblsVerify(b *testing.B) { + secret, err := tbls.GenerateSecretKey() + if err != nil { + b.Fatal(err) + } + + pubkey, err := tbls.SecretToPublicKey(secret) + if err != nil { + b.Fatal(err) + } + + sig, err := tbls.Sign(secret, msg32) + if err != nil { + b.Fatal(err) + } + + b.ReportAllocs() + + for b.Loop() { + if err := tbls.Verify(pubkey, msg32, sig); err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkTier1TblsVerifyAggregate(b *testing.B) { + const keys = 4 + + var ( + pubkeys []tbls.PublicKey + sigs []tbls.Signature + ) + + for range keys { + secret, err := tbls.GenerateSecretKey() + if err != nil { + b.Fatal(err) + } + + pubkey, err := tbls.SecretToPublicKey(secret) + if err != nil { + b.Fatal(err) + } + + sig, err := tbls.Sign(secret, msg32) + if err != nil { + b.Fatal(err) + } + + pubkeys = append(pubkeys, pubkey) + sigs = append(sigs, sig) + } + + aggSig, err := tbls.Aggregate(sigs) + if err != nil { + b.Fatal(err) + } + + b.ReportAllocs() + + for b.Loop() { + if err := tbls.VerifyAggregate(pubkeys, aggSig, msg32); err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkTier1TblsThresholdSplit(b *testing.B) { + secret, err := tbls.GenerateSecretKey() + if err != nil { + b.Fatal(err) + } + + for _, tc := range splitCases { + b.Run(tc.name, func(b *testing.B) { + b.ReportAllocs() + + for b.Loop() { + if _, err := tbls.ThresholdSplit(secret, tc.total, tc.threshold); err != nil { + b.Fatal(err) + } + } + }) + } +} + +func BenchmarkTier1TblsThresholdAggregate(b *testing.B) { + for _, tc := range splitCases { + b.Run(tc.name, func(b *testing.B) { + partialSigs := partialSignatures(b, tc) + + b.ReportAllocs() + + for b.Loop() { + if _, err := tbls.ThresholdAggregate(partialSigs); err != nil { + b.Fatal(err) + } + } + }) + } +} + +func BenchmarkTier1TblsRecoverSecret(b *testing.B) { + for _, tc := range splitCases { + b.Run(tc.name, func(b *testing.B) { + secret, err := tbls.GenerateSecretKey() + if err != nil { + b.Fatal(err) + } + + shares, err := tbls.ThresholdSplit(secret, tc.total, tc.threshold) + if err != nil { + b.Fatal(err) + } + + subset := make(map[int]tbls.PrivateKey) + for idx := 1; idx <= int(tc.threshold); idx++ { + subset[idx] = shares[idx] + } + + b.ReportAllocs() + + for b.Loop() { + if _, err := tbls.RecoverSecret(subset, tc.total, tc.threshold); err != nil { + b.Fatal(err) + } + } + }) + } +} + +// partialSignatures returns threshold partial signatures over msg32 from a +// fresh threshold-split key, keyed by 1-indexed share ID. +func partialSignatures(b *testing.B, tc splitCase) map[int]tbls.Signature { + b.Helper() + + secret, err := tbls.GenerateSecretKey() + if err != nil { + b.Fatal(err) + } + + shares, err := tbls.ThresholdSplit(secret, tc.total, tc.threshold) + if err != nil { + b.Fatal(err) + } + + partialSigs := make(map[int]tbls.Signature) + + for idx := 1; idx <= int(tc.threshold); idx++ { + sig, err := tbls.Sign(shares[idx], msg32) + if err != nil { + b.Fatal(err) + } + + partialSigs[idx] = sig + } + + return partialSigs +} diff --git a/perf/pairs.json b/perf/pairs.json new file mode 100644 index 00000000..2335cdf9 --- /dev/null +++ b/perf/pairs.json @@ -0,0 +1,166 @@ +{ + "pairs": [ + { + "id": "tier1/tbls/sign", + "tier": 1, + "go": "Tier1TblsSign", + "workload": "BLS sign, 32-byte msg" + }, + { + "id": "tier1/tbls/verify", + "tier": 1, + "go": "Tier1TblsVerify", + "workload": "BLS verify, 32-byte msg" + }, + { + "id": "tier1/tbls/verify_aggregate", + "tier": 1, + "go": "Tier1TblsVerifyAggregate", + "workload": "FastAggregateVerify, 4 pubkeys" + }, + { + "id": "tier1/tbls/threshold_split/3of4", + "tier": 1, + "go": "Tier1TblsThresholdSplit/3of4", + "workload": "Shamir split 3-of-4" + }, + { + "id": "tier1/tbls/threshold_split/7of10", + "tier": 1, + "go": "Tier1TblsThresholdSplit/7of10", + "workload": "Shamir split 7-of-10" + }, + { + "id": "tier1/tbls/threshold_aggregate/3of4", + "tier": 1, + "go": "Tier1TblsThresholdAggregate/3of4", + "workload": "Lagrange-aggregate 3 partial sigs" + }, + { + "id": "tier1/tbls/threshold_aggregate/7of10", + "tier": 1, + "go": "Tier1TblsThresholdAggregate/7of10", + "workload": "Lagrange-aggregate 7 partial sigs" + }, + { + "id": "tier1/tbls/recover_secret/3of4", + "tier": 1, + "go": "Tier1TblsRecoverSecret/3of4", + "workload": "Recover secret from 3 shares" + }, + { + "id": "tier1/tbls/recover_secret/7of10", + "tier": 1, + "go": "Tier1TblsRecoverSecret/7of10", + "workload": "Recover secret from 7 shares" + }, + { + "id": "tier1/frost/round1", + "tier": 1, + "go": "Tier1FrostRound1", + "workload": "FROST DKG round 1, 3-of-4 (poly + Feldman commitments + PoK)" + }, + { + "id": "tier1/frost/round2", + "tier": 1, + "go": "Tier1FrostRound2", + "workload": "FROST DKG round 2, 3-of-4 (verify commitments + PoKs, derive share)" + }, + { + "id": "tier1/frost/partial_sign", + "tier": 1, + "go": "Tier1TblsSign", + "workload": "DKG partial sign, 32-byte msg (pluto kryptology-compat blst vs charon herumi tbls)" + }, + { + "id": "tier1/frost/aggregate", + "tier": 1, + "go": "Tier1TblsThresholdAggregate/3of4", + "workload": "DKG combine 3 partial sigs (pluto kryptology-compat blst vs charon herumi tbls)" + }, + { + "id": "tier1/ssz/att_encode", + "tier": 1, + "go": "Tier1SszAttEncode", + "workload": "SSZ encode phase0 attestation (261B fixture)" + }, + { + "id": "tier1/ssz/att_decode", + "tier": 1, + "go": "Tier1SszAttDecode", + "workload": "SSZ decode phase0 attestation" + }, + { + "id": "tier1/ssz/att_hash_root", + "tier": 1, + "go": "Tier1SszAttHashRoot", + "workload": "hash tree root of phase0 attestation" + }, + { + "id": "tier1/ssz/proposal_encode", + "tier": 1, + "go": "Tier1SszProposalEncode", + "workload": "SSZ encode versioned signed deneb proposal" + }, + { + "id": "tier1/ssz/proposal_decode", + "tier": 1, + "go": "Tier1SszProposalDecode", + "workload": "SSZ decode versioned signed deneb proposal" + }, + { + "id": "tier1/proto/qbft_marshal", + "tier": 1, + "go": "Tier1ProtoQbftMarshal", + "workload": "protobuf marshal QBFTConsensusMsg (4 justifications + 1KiB value)" + }, + { + "id": "tier1/proto/qbft_unmarshal", + "tier": 1, + "go": "Tier1ProtoQbftUnmarshal", + "workload": "protobuf unmarshal QBFTConsensusMsg" + }, + { + "id": "tier2/qbft/decide_4of4", + "tier": 2, + "go": "Tier2QbftDecide/4of4", + "workload": "in-memory QBFT: 4 processes, happy path, spawn to all-decided" + }, + { + "id": "tier2/qbft/decide_7of10", + "tier": 2, + "go": "Tier2QbftDecide/7of10", + "workload": "in-memory QBFT: 10 processes, happy path, spawn to all-decided" + }, + { + "id": "tier2/dkg/mem_ceremony/1vals", + "tier": 2, + "go": null, + "workload": "in-memory FROST DKG ceremony, 3-of-4, 1 validator (Rust-only)" + }, + { + "id": "tier2/dkg/mem_ceremony/10vals", + "tier": 2, + "go": null, + "workload": "in-memory FROST DKG ceremony, 3-of-4, 10 validators (Rust-only)" + }, + { + "id": "tier1/k1/sign", + "tier": 1, + "go": "Tier1K1Sign", + "workload": "secp256k1 ECDSA sign, 32-byte digest" + }, + { + "id": "tier1/k1/recover", + "tier": 1, + "go": "Tier1K1Recover", + "workload": "secp256k1 pubkey recovery" + }, + { + "id": "tier1/k1/verify", + "tier": 1, + "go": "Tier1K1Verify", + "workload": "secp256k1 ECDSA verify (64-byte sig)" + } + ] +} diff --git a/perf/report/normalize.py b/perf/report/normalize.py new file mode 100755 index 00000000..b1bcbfef --- /dev/null +++ b/perf/report/normalize.py @@ -0,0 +1,261 @@ +#!/usr/bin/env python3 +"""Normalize heterogeneous benchmark outputs into a single results.json. + +Inputs: + - criterion: target/criterion/**/new/{benchmark.json,estimates.json} + - go: `go test -bench` text output (ns/op, B/op, allocs/op) + - hyperfine: perf/out/hyperfine/*.json (tier 3 CLI pairs, optional) + - dkg: perf/out/dkg-times.json (tier 3 ceremony timings, optional) + +Only pairs listed in perf/pairs.json are emitted (plus tier-3 inputs, which +carry their own ids). Uses Python stdlib only. +""" + +import argparse +import datetime +import json +import pathlib +import platform +import re +import statistics +import subprocess +import sys + +GO_BENCH_RE = re.compile( + r"^Benchmark(\S+?)(?:-\d+)?\s+\d+\s+([\d.]+)\s+ns/op" + r"(?:\s+([\d.]+)\s+B/op\s+([\d.]+)\s+allocs/op)?" +) + + +def run_cmd(args): + try: + return subprocess.run( + args, capture_output=True, text=True, check=True, timeout=30 + ).stdout.strip() + except Exception: # noqa: BLE001 - meta fields are best-effort + return None + + +def collect_meta(repo_root): + return { + "date": datetime.datetime.now(datetime.timezone.utc).isoformat(timespec="seconds"), + "host": platform.node(), + "os": platform.system().lower(), + "arch": platform.machine(), + "rustc": run_cmd(["rustc", "--version"]), + "go": run_cmd(["go", "version"]), + "git_sha": run_cmd(["git", "-C", str(repo_root), "rev-parse", "--short", "HEAD"]), + } + + +def parse_criterion(criterion_dir): + """Return {full_id: mean_ns} from criterion's on-disk estimates.""" + results = {} + root = pathlib.Path(criterion_dir) + if not root.is_dir(): + return results + + for bench_json in root.rglob("new/benchmark.json"): + estimates = bench_json.parent / "estimates.json" + if not estimates.is_file(): + continue + + try: + full_id = json.loads(bench_json.read_text())["full_id"] + mean_ns = json.loads(estimates.read_text())["mean"]["point_estimate"] + except (json.JSONDecodeError, KeyError): + continue + + results[full_id] = float(mean_ns) + + return results + + +def parse_go_bench(go_file): + """Return {name: {ns, bytes_alloc, allocs}} with medians across -count runs.""" + samples = {} + path = pathlib.Path(go_file) + if not path.is_file(): + return {} + + for line in path.read_text().splitlines(): + m = GO_BENCH_RE.match(line.strip()) + if not m: + continue + + name, ns, bytes_alloc, allocs = m.groups() + entry = samples.setdefault(name, {"ns": [], "bytes": [], "allocs": []}) + entry["ns"].append(float(ns)) + if bytes_alloc is not None: + entry["bytes"].append(float(bytes_alloc)) + entry["allocs"].append(float(allocs)) + + results = {} + for name, entry in samples.items(): + results[name] = { + "ns": statistics.median(entry["ns"]), + "bytes_alloc": statistics.median(entry["bytes"]) if entry["bytes"] else None, + "allocs": statistics.median(entry["allocs"]) if entry["allocs"] else None, + } + + return results + + +def parse_hyperfine(hyperfine_dir): + """Return [{id, pluto_ns, charon_ns}] from hyperfine export JSONs. + + Convention: one JSON per pair, filename `tier3__cli__create_enr.json` maps + to id `tier3/cli/create_enr`; commands are named `pluto` and `charon` via + hyperfine's --command-name. + """ + out = [] + root = pathlib.Path(hyperfine_dir) if hyperfine_dir else None + if not root or not root.is_dir(): + return out + + for f in sorted(root.glob("*.json")): + try: + data = json.loads(f.read_text()) + except json.JSONDecodeError: + continue + + times = {} + for res in data.get("results", []): + name = res.get("command", "") + if name in ("pluto", "charon"): + times[name] = float(res["median"]) * 1e9 + + if "pluto" in times and "charon" in times: + out.append( + { + "id": f.stem.replace("__", "/"), + "pluto_ns": times["pluto"], + "charon_ns": times["charon"], + } + ) + + return out + + +def parse_extra(files): + """Return [{id, unit, pluto_ns, charon_ns}] from extra-timings JSONs. + + Each file holds a list of entries: {id, unit ("s"|"ns"|"bytes"), + pluto_value, charon_value}. Values in seconds are converted to ns; other + units pass through with `unit` preserved. + """ + out = [] + for f in files or []: + path = pathlib.Path(f) + if not path.is_file(): + continue + + for entry in json.loads(path.read_text()): + unit = entry.get("unit", "ns") + scale = 1e9 if unit == "s" else 1.0 + item = {"id": entry["id"], "unit": "ns" if unit == "s" else unit} + if entry.get("pluto_value") is not None: + item["pluto_ns"] = float(entry["pluto_value"]) * scale + if entry.get("charon_value") is not None: + item["charon_ns"] = float(entry["charon_value"]) * scale + out.append(item) + + return out + + +def status_for(pluto_ns, charon_ns, threshold): + if pluto_ns is None and charon_ns is None: + return "MISSING" + if charon_ns is None: + return "PLUTO_ONLY" + if pluto_ns is None: + return "CHARON_ONLY" + return "SUBOPTIMAL" if pluto_ns / charon_ns > threshold else "OK" + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--pairs", required=True) + parser.add_argument("--criterion", help="criterion output dir (target/criterion)") + parser.add_argument("--go", help="go test -bench output file") + parser.add_argument("--hyperfine", help="dir with hyperfine export JSONs") + parser.add_argument( + "--extra", + nargs="*", + help="extra-timings JSONs (dkg-times.json, cli-extra.json)", + ) + parser.add_argument("--suboptimal-threshold", type=float, default=1.15) + parser.add_argument("--repo-root", default=".") + parser.add_argument("-o", "--output", required=True) + args = parser.parse_args() + + pairs = json.loads(pathlib.Path(args.pairs).read_text())["pairs"] + criterion = parse_criterion(args.criterion) if args.criterion else {} + go_bench = parse_go_bench(args.go) if args.go else {} + + results = [] + for pair in pairs: + rust_id = pair.get("rust", pair["id"]) + pluto_ns = criterion.get(rust_id) + go_entry = go_bench.get(pair["go"]) if pair.get("go") else None + + pluto = {"ns": pluto_ns} if pluto_ns is not None else None + charon = ( + { + "ns": go_entry["ns"], + "bytes_alloc": go_entry["bytes_alloc"], + "allocs": go_entry["allocs"], + } + if go_entry + else None + ) + + charon_ns = go_entry["ns"] if go_entry else None + status = status_for(pluto_ns, charon_ns, args.suboptimal_threshold) + if status == "MISSING": + continue + + results.append( + { + "id": pair["id"], + "tier": pair["tier"], + "workload": pair.get("workload"), + "pluto": pluto, + "charon": charon, + "ratio": ( + round(pluto_ns / charon_ns, 3) + if pluto_ns is not None and charon_ns is not None + else None + ), + "status": status, + } + ) + + for item in parse_hyperfine(args.hyperfine) + parse_extra(args.extra): + pluto_ns = item.get("pluto_ns") + charon_ns = item.get("charon_ns") + results.append( + { + "id": item["id"], + "tier": 3, + "workload": None, + "unit": item.get("unit", "ns"), + "pluto": {"ns": pluto_ns} if pluto_ns is not None else None, + "charon": {"ns": charon_ns} if charon_ns is not None else None, + "ratio": ( + round(pluto_ns / charon_ns, 3) + if pluto_ns is not None and charon_ns is not None + else None + ), + "status": status_for(pluto_ns, charon_ns, args.suboptimal_threshold), + } + ) + + output = {"meta": collect_meta(args.repo_root), "results": results} + pathlib.Path(args.output).write_text(json.dumps(output, indent=2) + "\n") + + print(f"normalize: wrote {len(results)} results to {args.output}", file=sys.stderr) + + +if __name__ == "__main__": + main() diff --git a/perf/report/render.py b/perf/report/render.py new file mode 100755 index 00000000..6b5fb5fd --- /dev/null +++ b/perf/report/render.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 +"""Render results.json into a Markdown report; gate CI on baseline regressions. + +Exit codes: + 0 - OK (SUBOPTIMAL flags alone never fail the run; they are the work queue) + 1 - harness error (bad inputs, incomparable baseline) + 2 - regression: a pair's ratio worsened more than --regression-threshold + relative to the ratio recorded in --baseline +""" + +import argparse +import json +import pathlib +import sys + + +def fmt_ns(ns, unit="ns"): + if ns is None: + return "-" + if unit == "bytes": + if ns < 1_048_576: + return f"{ns / 1_024:.0f} KiB" + return f"{ns / 1_048_576:.1f} MiB" + if ns < 1_000: + return f"{ns:.0f} ns" + if ns < 1_000_000: + return f"{ns / 1_000:.2f} µs" + if ns < 1_000_000_000: + return f"{ns / 1_000_000:.2f} ms" + return f"{ns / 1_000_000_000:.2f} s" + + +def fmt_ratio(ratio): + return f"{ratio:.2f}x" if ratio is not None else "-" + + +def load_baseline(path, meta): + baseline = json.loads(pathlib.Path(path).read_text()) + base_meta = baseline.get("meta", {}) + for key in ("os", "arch"): + if base_meta.get(key) != meta.get(key): + print( + f"render: baseline {key}={base_meta.get(key)} does not match " + f"current {key}={meta.get(key)}; refusing to compare", + file=sys.stderr, + ) + sys.exit(1) + + return {r["id"]: r.get("ratio") for r in baseline.get("results", [])} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("results") + parser.add_argument("--baseline", help="blessed results.json to gate against") + parser.add_argument("--regression-threshold", type=float, default=1.10) + parser.add_argument("-o", "--output", required=True) + args = parser.parse_args() + + data = json.loads(pathlib.Path(args.results).read_text()) + meta = data["meta"] + results = data["results"] + + baseline_ratios = load_baseline(args.baseline, meta) if args.baseline else {} + + lines = ["# Pluto vs Charon performance report", ""] + lines.append(f"- date: {meta.get('date')}") + lines.append(f"- host: {meta.get('host')} ({meta.get('os')}/{meta.get('arch')})") + lines.append(f"- rustc: {meta.get('rustc')}") + lines.append(f"- go: {meta.get('go')}") + lines.append(f"- git: {meta.get('git_sha')}") + lines.append("") + + suboptimal = [r for r in results if r["status"] == "SUBOPTIMAL"] + if suboptimal: + lines.append("## Work on these (Pluto slower than Charon)") + lines.append("") + lines.append("| pair | pluto | charon | ratio | workload |") + lines.append("|---|---|---|---|---|") + for r in sorted(suboptimal, key=lambda r: -(r["ratio"] or 0)): + lines.append( + f"| **{r['id']}** | {fmt_ns(r['pluto']['ns'])} " + f"| {fmt_ns(r['charon']['ns'])} | **{fmt_ratio(r['ratio'])}** " + f"| {r.get('workload') or ''} |" + ) + lines.append("") + else: + lines.append("## No pairs flagged SUBOPTIMAL") + lines.append("") + + regressions = [] + + for tier in sorted({r["tier"] for r in results}): + tier_results = [r for r in results if r["tier"] == tier] + comparable = [r for r in tier_results if r["ratio"] is not None] + single_sided = [r for r in tier_results if r["ratio"] is None] + + lines.append(f"## Tier {tier}") + lines.append("") + header = "| pair | pluto | charon | ratio | flag |" + separator = "|---|---|---|---|---|" + if baseline_ratios: + header += " vs baseline |" + separator += "---|" + lines.append(header) + lines.append(separator) + + for r in sorted(comparable, key=lambda r: -r["ratio"]): + flag = r["status"] if r["status"] != "OK" else "" + unit = r.get("unit", "ns") + row = ( + f"| {r['id']} | {fmt_ns(r['pluto']['ns'], unit)} " + f"| {fmt_ns(r['charon']['ns'], unit)} | {fmt_ratio(r['ratio'])} | {flag} |" + ) + if baseline_ratios: + base = baseline_ratios.get(r["id"]) + if base is not None and r["ratio"] is not None: + delta = r["ratio"] / base + row += f" {delta:+.1%} |".replace("%", "%") + row = row.replace("+-", "-") + if delta > args.regression_threshold: + regressions.append((r["id"], base, r["ratio"])) + else: + row += " new |" + lines.append(row) + + if single_sided: + lines.append("") + lines.append("Informational (single-sided):") + lines.append("") + for r in single_sided: + side = r["pluto"] or r["charon"] + lines.append( + f"- `{r['id']}` ({r['status']}): {fmt_ns(side['ns'], r.get('unit', 'ns'))}" + ) + + lines.append("") + + if regressions: + lines.append("## Regressions vs baseline") + lines.append("") + for rid, base, current in regressions: + lines.append(f"- `{rid}`: ratio {base:.2f}x -> {current:.2f}x") + lines.append("") + + pathlib.Path(args.output).write_text("\n".join(lines)) + print(f"render: wrote {args.output}", file=sys.stderr) + + if regressions: + print(f"render: {len(regressions)} regression(s) vs baseline", file=sys.stderr) + sys.exit(2) + + +if __name__ == "__main__": + main() diff --git a/perf/run.sh b/perf/run.sh new file mode 100755 index 00000000..f5c9e2c5 --- /dev/null +++ b/perf/run.sh @@ -0,0 +1,179 @@ +#!/usr/bin/env bash +# Pluto vs Charon performance-comparison harness. +# +# Runs matching Rust (criterion) and Go (go test -bench) workloads, plus +# optional process-level comparisons, normalizes everything into +# perf/out/results.json and renders perf/out/report.md. +# +# Usage: perf/run.sh [--tier 1|2|3|all] [--filter ] [--quick] +# [--baseline ] [--fail-threshold ] +# +# Environment: +# CHARON_SRC Path to the charon Go source checkout. Defaults to ./charon. + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +OUT_DIR="${ROOT_DIR}/perf/out" + +: "${CHARON_SRC:=${ROOT_DIR}/charon}" + +TIER="all" +FILTER="" +QUICK=0 +BASELINE="" +FAIL_THRESHOLD="1.15" + +usage() { + sed -n '2,13p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//' +} + +while (($#)); do + case "$1" in + --tier) + TIER="${2:?--tier requires a value}" + shift 2 + ;; + --filter) + FILTER="${2:?--filter requires a value}" + shift 2 + ;; + --quick) + QUICK=1 + shift + ;; + --baseline) + BASELINE="${2:?--baseline requires a path}" + shift 2 + ;; + --fail-threshold) + FAIL_THRESHOLD="${2:?--fail-threshold requires a value}" + shift 2 + ;; + -h | --help) + usage + exit 0 + ;; + *) + printf 'perf/run.sh: unknown argument %s\n' "$1" >&2 + usage >&2 + exit 1 + ;; + esac +done + +tier_enabled() { + [[ "${TIER}" == "all" || "${TIER}" == *"$1"* ]] +} + +log() { + printf '[perf] %s\n' "$*" >&2 +} + +mkdir -p "${OUT_DIR}" + +export GOTOOLCHAIN=auto + +if [[ ! -d "${CHARON_SRC}" ]]; then + log "ERROR: charon source not found at ${CHARON_SRC} (set CHARON_SRC)" + exit 1 +fi + +GO_COUNT=6 +CRITERION_FLAGS=(--noplot) +if ((QUICK)); then + GO_COUNT=2 + CRITERION_FLAGS+=(--quick) +fi + +# --- Tier 1+2: Rust criterion benches --------------------------------------- + +# Package list entries are "[:]". +RUST_BENCH_PACKAGES=() +if tier_enabled 1; then + RUST_BENCH_PACKAGES+=(pluto-crypto pluto-k1util pluto-frost) +fi +if tier_enabled 1 || tier_enabled 2; then + # pluto-core benches cover tier 1 (ssz, proto) and tier 2 (qbft). + RUST_BENCH_PACKAGES+=(pluto-core:bench-util) +fi +if tier_enabled 2; then + RUST_BENCH_PACKAGES+=(pluto-dkg:bench-util) +fi + +if ((${#RUST_BENCH_PACKAGES[@]})); then + log "running Rust criterion benches: ${RUST_BENCH_PACKAGES[*]}" + # Remove stale criterion estimates so the report never mixes runs. + rm -rf "${ROOT_DIR}/target/criterion" + + for entry in "${RUST_BENCH_PACKAGES[@]}"; do + pkg="${entry%%:*}" + features="${entry#"${pkg}"}" + features="${features#:}" + (cd "${ROOT_DIR}" && cargo bench -p "${pkg}" --benches \ + ${features:+--features "${features}"} -- \ + "${CRITERION_FLAGS[@]}" ${FILTER:+"${FILTER}"}) + done +fi + +# --- Tier 1+2: Go benches ---------------------------------------------------- + +if tier_enabled 1 || tier_enabled 2; then + GO_TIERS="" + tier_enabled 1 && GO_TIERS+="1" + tier_enabled 2 && GO_TIERS+="2" + GO_BENCH_REGEX="^BenchmarkTier[${GO_TIERS}]" + if [[ -n "${FILTER}" ]]; then + GO_BENCH_REGEX="${FILTER}" + fi + + log "running Go benches (count=${GO_COUNT})" + (cd "${ROOT_DIR}/perf/go-bench" && go test \ + -bench "${GO_BENCH_REGEX}" \ + -benchmem \ + -count "${GO_COUNT}" \ + -run '^$' \ + -timeout 60m \ + . | tee "${OUT_DIR}/go-bench.txt") +fi + +# --- Tier 3: process-level --------------------------------------------------- + +if tier_enabled 3; then + log "building pluto (release) and charon binaries" + (cd "${ROOT_DIR}" && cargo build --release -p pluto-cli) + (cd "${CHARON_SRC}" && go build -trimpath -ldflags "-s -w" \ + -o "${OUT_DIR}/charon" .) + + log "running CLI matrix (hyperfine)" + PLUTO_BIN="${ROOT_DIR}/target/release/pluto" \ + CHARON_BIN="${OUT_DIR}/charon" \ + "${ROOT_DIR}/perf/cli-matrix.sh" + + log "running timed DKG ceremonies (dkg-runner)" + if ! PLUTO_BIN="${ROOT_DIR}/target/release/pluto" \ + CHARON_BIN="${OUT_DIR}/charon" \ + "${ROOT_DIR}/perf/dkg-e2e.sh"; then + log "WARNING: dkg-e2e failed (needs relay connectivity); continuing without it" + fi +fi + +# --- Normalize + render ------------------------------------------------------- + +log "normalizing results" +python3 "${ROOT_DIR}/perf/report/normalize.py" \ + --pairs "${ROOT_DIR}/perf/pairs.json" \ + --criterion "${ROOT_DIR}/target/criterion" \ + --go "${OUT_DIR}/go-bench.txt" \ + --hyperfine "${OUT_DIR}/hyperfine" \ + --extra "${OUT_DIR}/dkg-times.json" "${OUT_DIR}/cli-extra.json" \ + --suboptimal-threshold "${FAIL_THRESHOLD}" \ + --repo-root "${ROOT_DIR}" \ + -o "${OUT_DIR}/results.json" + +log "rendering report" +python3 "${ROOT_DIR}/perf/report/render.py" "${OUT_DIR}/results.json" \ + ${BASELINE:+--baseline "${BASELINE}"} \ + -o "${OUT_DIR}/report.md" + +log "report: ${OUT_DIR}/report.md"