From 17b5a2ec2163b4ddf8a7da0aa8b40259870381e2 Mon Sep 17 00:00:00 2001 From: Ivan Kalinin Date: Thu, 17 Jul 2025 10:29:48 +0200 Subject: [PATCH 01/21] feat(slasher): add slasher crates --- Cargo.lock | 26 ++ Cargo.toml | 20 +- cli/Cargo.toml | 11 +- cli/src/node/mod.rs | 4 + collator/Cargo.toml | 1 + collator/src/validator/impls/std_impl/mod.rs | 19 +- .../src/validator/impls/std_impl/session.rs | 95 +++++- collator/src/validator/mod.rs | 8 +- collator/tests/collation_tests.rs | 1 + collator/tests/validator_tests.rs | 154 ++++++++- scripts/gen-dashboard.py | 15 + slasher-traits/Cargo.toml | 19 ++ slasher-traits/LICENSE-APACHE | 1 + slasher-traits/LICENSE-MIT | 1 + slasher-traits/src/lib.rs | 6 + slasher-traits/src/validator.rs | 302 ++++++++++++++++++ slasher/Cargo.toml | 29 ++ slasher/LICENSE-APACHE | 1 + slasher/LICENSE-MIT | 1 + slasher/src/collector/validator_events.rs | 283 ++++++++++++++++ slasher/src/lib.rs | 95 ++++++ slasher/src/util.rs | 103 ++++++ 22 files changed, 1150 insertions(+), 45 deletions(-) create mode 100644 slasher-traits/Cargo.toml create mode 120000 slasher-traits/LICENSE-APACHE create mode 120000 slasher-traits/LICENSE-MIT create mode 100644 slasher-traits/src/lib.rs create mode 100644 slasher-traits/src/validator.rs create mode 100644 slasher/Cargo.toml create mode 120000 slasher/LICENSE-APACHE create mode 120000 slasher/LICENSE-MIT create mode 100644 slasher/src/collector/validator_events.rs create mode 100644 slasher/src/lib.rs create mode 100644 slasher/src/util.rs diff --git a/Cargo.lock b/Cargo.lock index 818378ba28..604eb7f38a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4095,6 +4095,7 @@ dependencies = [ "tycho-crypto", "tycho-network", "tycho-rpc", + "tycho-slasher", "tycho-storage", "tycho-types", "tycho-util", @@ -4146,6 +4147,7 @@ dependencies = [ "tycho-crypto", "tycho-executor", "tycho-network", + "tycho-slasher-traits", "tycho-storage", "tycho-types", "tycho-util", @@ -4427,6 +4429,30 @@ dependencies = [ "tycho-util", ] +[[package]] +name = "tycho-slasher" +version = "0.3.9" +dependencies = [ + "anyhow", + "arc-swap", + "metrics", + "parking_lot", + "scopeguard", + "tokio", + "tracing", + "tycho-crypto", + "tycho-slasher-traits", + "tycho-types", + "tycho-util", +] + +[[package]] +name = "tycho-slasher-traits" +version = "0.3.9" +dependencies = [ + "tycho-types", +] + [[package]] name = "tycho-storage" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index bcd1fac120..8bdb32a660 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,14 +19,16 @@ members = [ "gen-protos", "network", "rpc", + "rpc-subscriptions", "simulator", + "slasher", + "slasher-traits", "storage", "storage-traits", "tycho-build-info", "util", "util-proc", "wu-tuner", - "rpc-subscriptions", ] [workspace.dependencies] @@ -61,7 +63,10 @@ exponential-backoff = "1.2.1" fdlimit = "0.3.0" futures-executor = "0.3" futures-util = "0.3" -governor = { version = "0.9", default-features = false, features = ["std", "quanta"] } +governor = { version = "0.9", default-features = false, features = [ + "std", + "quanta", +] } getip = "0.2.1" hdrhistogram = "7.5.4" hex = "0.4" @@ -86,7 +91,10 @@ proc-macro2 = "1.0" prost = "0.14.3" prost-build = "0.14.3" quick_cache = "0.6.21" -quinn = { version = "0.11.9", default-features = false, features = ["runtime-tokio", "rustls"] } +quinn = { version = "0.11.9", default-features = false, features = [ + "runtime-tokio", + "rustls", +] } quote = "1.0" object_store = "0.13" rand = "0.9" @@ -129,7 +137,9 @@ tikv-jemalloc-ctl = { version = "0.6.1", features = ["stats"] } tl-proto = "0.5.4" tokio = { version = "1", default-features = false } tokio-stream = "0.1.18" -tokio-util = { version = "0.7.18", default-features = false, features = ["codec"] } +tokio-util = { version = "0.7.18", default-features = false, features = [ + "codec", +] } tower = "0.5" tower-http = "0.6" tracing = "0.1" @@ -158,6 +168,8 @@ tycho-core = { path = "./core", version = "0.3.9" } tycho-network = { path = "./network", version = "0.3.9" } tycho-rpc-subscriptions = { path = "./rpc-subscriptions", version = "0.3.9" } tycho-rpc = { path = "./rpc", version = "0.3.9" } +tycho-slasher = { path = "./slasher", version = "0.3.9" } +tycho-slasher-traits = { path = "./slasher-traits", version = "0.3.9" } tycho-storage = { path = "./storage", version = "0.3.9" } tycho-storage-traits = { path = "./storage-traits", version = "0.3.9" } tycho-util = { path = "./util", version = "0.3.9" } diff --git a/cli/Cargo.toml b/cli/Cargo.toml index dfaf84f4e6..3c17bb4274 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -1,7 +1,13 @@ [package] name = "tycho-cli" description = "Node CLI." -include = ["src/**/*.rs", "res/**/*.boc", "./LICENSE-*", "./README.md", "build.rs"] +include = [ + "src/**/*.rs", + "res/**/*.boc", + "./LICENSE-*", + "./README.md", + "build.rs", +] version.workspace = true authors.workspace = true edition.workspace = true @@ -55,14 +61,15 @@ weedb = { workspace = true } # local deps tycho-block-util = { workspace = true } tycho-collator = { workspace = true } +tycho-consensus = { workspace = true } tycho-control = { workspace = true, features = ["full"] } tycho-core = { workspace = true, features = ["cli"] } tycho-network = { workspace = true } tycho-rpc = { workspace = true, features = ["http2"] } +tycho-slasher = { workspace = true } tycho-storage = { workspace = true } tycho-util = { workspace = true, features = ["cli"] } tycho-wu-tuner = { workspace = true } -tycho-consensus = { workspace = true } [dev-dependencies] tycho-collator = { workspace = true, features = ["test"] } diff --git a/cli/src/node/mod.rs b/cli/src/node/mod.rs index 3934c6febb..761db5783d 100644 --- a/cli/src/node/mod.rs +++ b/cli/src/node/mod.rs @@ -222,6 +222,9 @@ impl Node { let top_shards = mc_state.get_top_shards()?; message_queue_adapter.clear_uncommitted_state(&top_shards)?; + // NOTE: Stub + let slasher = tycho_slasher::Slasher::new(base.keypair.clone()); + let validator = ValidatorStdImpl::new( ValidatorNetworkContext { network: base.network.clone(), @@ -231,6 +234,7 @@ impl Node { }, base.keypair.clone(), self.validator_config, + slasher.validator_events_listener(), ); // Explicitly handle the initial state diff --git a/collator/Cargo.toml b/collator/Cargo.toml index 1f9f6b901a..a09876787f 100644 --- a/collator/Cargo.toml +++ b/collator/Cargo.toml @@ -73,6 +73,7 @@ tycho-consensus = { workspace = true } tycho-core = { workspace = true } tycho-executor = { workspace = true } tycho-network = { workspace = true } +tycho-slasher-traits = { workspace = true } tycho-storage = { workspace = true } tycho-util = { workspace = true } tycho-vm = { workspace = true } diff --git a/collator/src/validator/impls/std_impl/mod.rs b/collator/src/validator/impls/std_impl/mod.rs index fd00f72358..cc1a3975a5 100644 --- a/collator/src/validator/impls/std_impl/mod.rs +++ b/collator/src/validator/impls/std_impl/mod.rs @@ -7,6 +7,7 @@ use indexmap::{self, IndexMap}; use serde::{Deserialize, Serialize}; use session::DebugLogValidatorSesssion; use tycho_crypto::ed25519::KeyPair; +use tycho_slasher_traits::{ValidatorEvents, ValidatorEventsListener}; use tycho_types::models::*; use tycho_util::{FastHashMap, serde_helpers}; @@ -76,6 +77,7 @@ impl ValidatorStdImpl { net_context: ValidatorNetworkContext, keypair: Arc, config: ValidatorStdImplConfig, + recorder: Arc, ) -> Self { Self { inner: Arc::new(Inner { @@ -83,6 +85,7 @@ impl ValidatorStdImpl { keypair, sessions: Default::default(), config, + events: ValidatorEvents::new(recorder), }), } } @@ -91,18 +94,19 @@ impl ValidatorStdImpl { #[async_trait] impl Validator for ValidatorStdImpl { fn add_session(&self, info: AddSession<'_>) -> Result<()> { - let session = ValidatorSession::new( - &self.inner.net_context, - self.inner.keypair.clone(), - &self.inner.config, - info, - )?; - let mut sessions = self.inner.sessions.lock(); let shard_sessions = sessions.entry(info.shard_ident).or_default(); match shard_sessions.entry(info.session_id) { indexmap::map::Entry::Vacant(entry) => { + let session = ValidatorSession::new( + &self.inner.net_context, + self.inner.keypair.clone(), + &self.inner.config, + info, + &self.inner.events, + )?; + tracing::debug!( target: tracing_targets::VALIDATOR, session = ?DebugLogValidatorSesssion(&session), @@ -217,6 +221,7 @@ struct Inner { keypair: Arc, sessions: parking_lot::Mutex, config: ValidatorStdImplConfig, + events: ValidatorEvents, } type Sessions = FastHashMap; diff --git a/collator/src/validator/impls/std_impl/session.rs b/collator/src/validator/impls/std_impl/session.rs index e27f66dec4..25a229ba93 100644 --- a/collator/src/validator/impls/std_impl/session.rs +++ b/collator/src/validator/impls/std_impl/session.rs @@ -1,8 +1,8 @@ use std::fmt; use std::future::IntoFuture; use std::pin::{Pin, pin}; -use std::sync::Arc; use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering}; +use std::sync::{Arc, Weak}; use std::task::{Context, Poll, Waker}; use anyhow::Result; @@ -14,8 +14,9 @@ use scc::TreeIndex; use tokio::sync::{Notify, Semaphore}; use tokio_util::sync::CancellationToken; use tracing::Instrument; -use tycho_crypto::ed25519::KeyPair; +use tycho_crypto::ed25519::KeyPair use tycho_network::{OverlayId, PeerId, PrivateOverlay}; +use tycho_slasher_traits::{BlockValidationScope, ValidatorEvents, ValidatorSessionScope}; use tycho_types::models::*; use tycho_util::FastHashMap; use tycho_util::futures::JoinTask; @@ -60,12 +61,13 @@ impl ValidatorSession { key_pair: Arc, config: &ValidatorStdImplConfig, info: AddSession<'_>, + events: &ValidatorEvents, ) -> Result { // Prepare a map with other validators let mut validators = FastHashMap::default(); - for descr in info.validators { + for (i, descr) in info.validators.iter().enumerate() { // TODO: Skip invalid entries? But what should we do with the total weight? - let validator_info = BriefValidatorDescr::try_from(descr)?; + let validator_info = BriefValidatorDescr::from_descr(i as u16, descr)?; validators.insert(validator_info.peer_id, validator_info); } @@ -73,8 +75,8 @@ impl ValidatorSession { let weight_threshold = max_weight.saturating_mul(2) / 3 + 1; let peer_id = net_context.network.peer_id(); - let own_weight = match validators.remove(peer_id) { - Some(info) => info.weight, + let (own_weight, own_validator_idx) = match validators.remove(peer_id) { + Some(info) => (info.weight, info.validator_idx), None => anyhow::bail!("node is not in the validator set"), }; @@ -82,6 +84,13 @@ impl ValidatorSession { let peer_ids = validators.values().map(|v| v.peer_id).collect::>(); + // Create events scope + let events_scope = events.begin_session( + info.session_id.into(), + info.start_block_seqno, + info.validators, + ); + // Create the session state let state = Arc::new(SessionState { shard_ident: info.shard_ident, @@ -91,6 +100,7 @@ impl ValidatorSession { cached_signatures: TreeIndex::new(), cancelled: AtomicBool::new(false), cancelled_signal: Notify::new(), + events_scope, }); // Create the private overlay @@ -124,6 +134,7 @@ impl ValidatorSession { key_pair, peer_id: *peer_id, own_weight, + own_validator_idx, state, min_seqno: AtomicU32::new(info.start_block_seqno), }), @@ -147,6 +158,7 @@ impl ValidatorSession { } pub fn cancel(&self) { + self.inner.state.events_scope.finish(); self.inner.state.cancelled.store(true, Ordering::Release); self.inner.state.cancelled_signal.notify_waiters(); } @@ -180,6 +192,15 @@ impl ValidatorSession { debug_assert_eq!(self.inner.state.shard_ident, block_id.shard); + let events_scope = scopeguard::guard( + Arc::new(self.inner.state.events_scope.begin_block(block_id)), + |scope| { + // Discard block if parent (this) future was cancelled. + // Due to spawned tasks we need to explicitly call this method. + scope.discard(); + }, + ); + self.inner .min_seqno .fetch_max(block_id.seqno, Ordering::Release); @@ -196,8 +217,8 @@ impl ValidatorSession { // Prepare block signatures let block_signatures = match &cached { - Some(cached) => self.reuse_signatures(block_id, cached.clone()).await, - None => self.prepare_new_signatures(block_id), + Some(cached) => self.reuse_signatures(block_id, &events_scope, cached).await, + None => self.prepare_new_signatures(block_id, &events_scope), } .build(block_id, state.weight_threshold); @@ -231,6 +252,10 @@ impl ValidatorSession { *self.inner.client.peer_id(), block_signatures.own_signature.clone(), ); + + // Notify listeners about the own signature + events_scope.receive_signature(self.inner.own_validator_idx, true); + let mut total_weight = self.inner.own_weight; let semaphore = Arc::new(Semaphore::new(self.inner.config.max_parallel_requests)); @@ -306,6 +331,8 @@ impl ValidatorSession { total_weight += validator_info.weight; } + scopeguard::ScopeGuard::into_inner(events_scope).commit(); + tracing::info!(target: tracing_targets::VALIDATOR, "finished"); Ok(ValidationStatus::Complete(ValidationComplete { signatures: result, @@ -313,7 +340,11 @@ impl ValidatorSession { })) } - fn prepare_new_signatures(&self, block_id: &BlockId) -> BlockSignaturesBuilder { + fn prepare_new_signatures( + &self, + block_id: &BlockId, + events_scope: &Arc, + ) -> BlockSignaturesBuilder { let data = Block::build_data_for_sign(block_id); // Prepare our own signature @@ -341,13 +372,15 @@ impl ValidatorSession { own_signature, other_signatures, total_weight: self.inner.own_weight, + events_scope: Arc::downgrade(events_scope), } } async fn reuse_signatures( &self, block_id: &BlockId, - cached: Arc, + events_scope: &Arc, + cached: &Arc, ) -> BlockSignaturesBuilder { let data = Block::build_data_for_sign(block_id); let block_id = *block_id; @@ -356,8 +389,9 @@ impl ValidatorSession { let my_peer_id = self.inner.peer_id; let validators = self.inner.state.validators.clone(); let mut total_weight = self.inner.own_weight; - let span = tracing::Span::current(); + let events_scope = Arc::downgrade(events_scope); + let cached = cached.clone(); tycho_util::sync::rayon_run(move || { let _span = span.enter(); @@ -386,6 +420,8 @@ impl ValidatorSession { }; let validator_info = validators.get(peer_id).expect("peer info out of sync"); + let validator_idx = validator_info.validator_idx; + if !validator_info.public_key.verify_raw(&data, &signature) { tracing::warn!( target: tracing_targets::VALIDATOR, @@ -401,10 +437,17 @@ impl ValidatorSession { metrics::counter!(METRIC_INVALID_SIGNATURES_CACHED_TOTAL).increment(1); - // TODO: Somehow mark that this validator sent an invalid signature? + if let Some(scope) = events_scope.upgrade() { + scope.receive_signature(validator_idx, false); + } + break 'stored Default::default(); } + if let Some(scope) = events_scope.upgrade() { + scope.receive_signature(validator_idx, true); + } + total_weight += validator_info.weight; Some(signature) }; @@ -416,6 +459,7 @@ impl ValidatorSession { own_signature, other_signatures, total_weight, + events_scope, } }) .await @@ -446,6 +490,7 @@ impl fmt::Debug for DebugLogValidatorSesssion<'_> { .field("session_id", &self.0.inner.session_id) .field("public_key", &self.0.inner.key_pair.public_key) .field("peer_id", &self.0.inner.peer_id) + .field("own_validator_idx", &self.0.inner.own_validator_idx) .field("own_weight", &self.0.inner.own_weight) .field("weight_threshold", &self.0.inner.state.weight_threshold) .field("start_block_seqno", &self.0.inner.start_block_seqno) @@ -462,6 +507,7 @@ struct Inner { client: ValidatorClient, key_pair: Arc, peer_id: PeerId, + own_validator_idx: u16, own_weight: u64, state: Arc, min_seqno: AtomicU32, @@ -613,6 +659,7 @@ struct SessionState { cached_signatures: TreeIndex>, cancelled: AtomicBool, cancelled_signal: Notify, + events_scope: ValidatorSessionScope, } impl SessionState { @@ -649,10 +696,16 @@ impl SessionState { // TODO: Store that the signature is invalid to avoid further checks on retries // TODO: Collect statistics on invalid signatures to slash the malicious validator metrics::counter!(METRIC_INVALID_SIGNATURES_IN_TOTAL).increment(1); + + if let Some(scope) = block.events_scope.upgrade() { + scope.receive_signature(validator_info.validator_idx, false); + } + return Err(ValidationError::InvalidSignature); } let mut can_notify = false; + let mut record_event = false; match &*slot.compare_and_swap(&None::>, Some(signature.clone())) { None => { slot.notify(); @@ -664,6 +717,7 @@ impl SessionState { + validator_info.weight; can_notify = total_weight >= self.weight_threshold; + record_event = true; } Some(saved) => { if saved.as_ref() != signature.as_ref() { @@ -674,8 +728,18 @@ impl SessionState { } } - if can_notify { - block.validated.store(true, Ordering::Release); + // NOTE: We can only record event if the block was not marked as validated. + let was_sealed = if can_notify { + block.validated.swap(true, Ordering::Release) + } else { + block.validated.load(Ordering::Relaxed) + }; + + if record_event + && !was_sealed + && let Some(scope) = block.events_scope.upgrade() + { + scope.receive_signature(validator_info.validator_idx, true); } Ok(()) @@ -691,6 +755,7 @@ struct BlockSignaturesBuilder { own_signature: Arc<[u8; 64]>, other_signatures: SignatureSlotsMap, total_weight: u64, + events_scope: Weak, } impl BlockSignaturesBuilder { @@ -704,6 +769,7 @@ impl BlockSignaturesBuilder { total_weight: AtomicU64::new(self.total_weight), validated: AtomicBool::new(self.total_weight >= weight_threshold), cancelled: CancellationToken::new(), + events_scope: self.events_scope, }) } } @@ -715,6 +781,7 @@ struct BlockSignatures { total_weight: AtomicU64, validated: AtomicBool, cancelled: CancellationToken, + events_scope: Weak, } impl Drop for BlockSignatures { diff --git a/collator/src/validator/mod.rs b/collator/src/validator/mod.rs index 652750edf7..b06a8b6636 100644 --- a/collator/src/validator/mod.rs +++ b/collator/src/validator/mod.rs @@ -92,12 +92,11 @@ pub struct BriefValidatorDescr { pub peer_id: PeerId, pub public_key: PublicKey, pub weight: u64, + pub validator_idx: u16, } -impl TryFrom<&ValidatorDescription> for BriefValidatorDescr { - type Error = anyhow::Error; - - fn try_from(descr: &ValidatorDescription) -> Result { +impl BriefValidatorDescr { + pub fn from_descr(validator_idx: u16, descr: &ValidatorDescription) -> Result { let Some(public_key) = PublicKey::from_bytes(descr.public_key.0) else { anyhow::bail!("invalid validator public key"); }; @@ -106,6 +105,7 @@ impl TryFrom<&ValidatorDescription> for BriefValidatorDescr { peer_id: PeerId(descr.public_key.0), public_key, weight: descr.weight, + validator_idx, }) } } diff --git a/collator/tests/collation_tests.rs b/collator/tests/collation_tests.rs index 7151ef8a28..815ae19b3e 100644 --- a/collator/tests/collation_tests.rs +++ b/collator/tests/collation_tests.rs @@ -26,6 +26,7 @@ use tycho_core::global_config::ZerostateId; use tycho_core::node::NodeKeys; use tycho_core::storage::CoreStorage; use tycho_crypto::ed25519; +use tycho_slasher_traits::NoopValidatorEventsListener; use tycho_storage::StorageContext; use tycho_types::models::{BlockId, BlockIdShort, ShardIdent}; diff --git a/collator/tests/validator_tests.rs b/collator/tests/validator_tests.rs index 390b4603e0..03621b89a2 100644 --- a/collator/tests/validator_tests.rs +++ b/collator/tests/validator_tests.rs @@ -9,6 +9,7 @@ use tycho_collator::validator::{ }; use tycho_crypto::ed25519; use tycho_network::{DhtClient, PeerInfo}; +use tycho_slasher_traits::NoopValidatorEventsListener; use tycho_types::cell::HashBytes; use tycho_types::models::{BlockId, ShardIdent, ValidatorDescription}; use tycho_util::futures::JoinTask; @@ -45,6 +46,7 @@ impl ValidatorNode { validator_network, keypair.clone(), ValidatorStdImplConfig::default(), + Arc::new(NoopValidatorEventsListener), ); Self { @@ -107,6 +109,7 @@ async fn validator_signatures_match() -> Result<()> { const NODE_COUNT: usize = 13; const SESSION_COUNT: usize = 5; + const REQUIRED_SIGS: usize = (NODE_COUNT * 2) / 3 + 1; let zerostate_id = BlockId { shard: ShardIdent::MASTERCHAIN, @@ -156,11 +159,47 @@ async fn validator_signatures_match() -> Result<()> { let BriefStatus::Complete(signature_count) = status else { panic!("must not be skipped"); }; - assert!(signature_count > (NODE_COUNT * 2) / 3); + assert!( + signature_count >= REQUIRED_SIGS, + "expected at least {REQUIRED_SIGS} signatures, got {signature_count}" + ); tracing::info!(%peer_id, ?status, "validation completed"); } + // TODO: Build test around some test-only events collector + + // let short = block_id.as_short_id(); + // let range = short..=short; + + // for node in &nodes { + // let events = node.event_collector.stats_for_blocks(range.clone()); + + // // check current node signature + // let self_stat = events + // .get(&node.descr.peer_id) + // .expect("current node should have stats"); + + // assert_eq!(self_stat.invalid, 0); + // assert_eq!(self_stat.valid, 1); + + // // check total valid signatures + // let total_valid: usize = events.values().filter(|s| s.valid > 0).count(); + + // assert!( + // total_valid >= REQUIRED_SIGS, + // "total_valid ({total_valid}) < REQUIRED_SIGS ({REQUIRED_SIGS})" + // ); + + // // check that no invalid signatures were given + // for (peer, stat) in &events { + // assert_eq!( + // stat.invalid, 0, + // "peer {peer:?} has invalid signatures: {stat:?}" + // ); + // } + // } + for node in &nodes { node.validator .cancel_validation(&block_id.as_short_id(), Some(session_id))?; @@ -184,6 +223,7 @@ async fn malicious_validators_are_ignored() -> Result<()> { const MALICIOUS_NODE_COUNT: usize = 3; const SESSION_COUNT: usize = 5; + const REQUIRED_SIGS: usize = (NODE_COUNT * 2) / 3 + 1; // 9 let zerostate_id = BlockId { shard: ShardIdent::MASTERCHAIN, @@ -246,7 +286,12 @@ async fn malicious_validators_are_ignored() -> Result<()> { match &status { ValidationStatus::Complete(res) => { - assert!(res.signatures.len() > (NODE_COUNT * 2) / 3); + let sigs = res.signatures.len(); + assert!(sigs >= REQUIRED_SIGS, "need {REQUIRED_SIGS}, got {sigs}"); + assert!( + sigs <= NODE_COUNT - MALICIOUS_NODE_COUNT, + "malicious sigs leaked, got {sigs}" + ); } ValidationStatus::Skipped => panic!("good validator skipped block"), } @@ -270,6 +315,38 @@ async fn malicious_validators_are_ignored() -> Result<()> { } } + // TODO: Build test around some test-only events collector + + // let short = block_id.as_short_id(); + // let range = short..=short; + // for (i, node) in nodes.iter().enumerate() { + // let stats = node.event_collector.stats_for_blocks(range.clone()); + // let s = stats.get(&node.descr.peer_id); + + // if i < MALICIOUS_NODE_COUNT { + // // malicious node must not have valid stats + // assert!( + // s.is_none_or(|st| st.valid == 0), + // "malicious node {:?} has valid sigs in stats: {:?}", + // node.descr.peer_id, + // s + // ); + // } else { + // // good node must have valid stats + // let st = s.expect("good node must have stats"); + // assert_eq!( + // st.valid, 1, + // "good node {:?} valid !=1 {:?}", + // node.descr.peer_id, st + // ); + // assert_eq!( + // st.invalid, 0, + // "good node {:?} invalid !=0 {:?}", + // node.descr.peer_id, st + // ); + // } + // } + block_id.seqno += 1; } } @@ -316,27 +393,33 @@ async fn network_gets_stuck_without_signatures() -> Result<()> { let mut good_validators = futures_util::stream::FuturesOrdered::new(); let mut bad_validators = futures_util::stream::FuturesOrdered::new(); + let mut nodes_blocks = Vec::with_capacity(NODE_COUNT); for (i, node) in nodes.iter().enumerate() { + let is_malicious = i < malicious_node_count; + + let mut blk = block_id; + if is_malicious { + blk.root_hash = rand::random(); + } + + nodes_blocks.push(blk); + let peer_id = node.descr.peer_id; let validator = node.validator.clone(); - let is_malicious = i < malicious_node_count; - if is_malicious { - bad_validators.push_back(JoinTask::new(async move { - let mut block_id = block_id; - block_id.root_hash = rand::random(); + let fut = async move { + let res = validator.validate(session_id, &blk).await; + (peer_id, res) + }; - let res = validator.validate(session_id, &block_id).await; - (peer_id, res) - })); + if is_malicious { + bad_validators.push_back(JoinTask::new(fut)); } else { - good_validators.push_back(JoinTask::new(async move { - let res = validator.validate(session_id, &block_id).await; - (peer_id, res) - })); + good_validators.push_back(JoinTask::new(fut)); } } + // let range = block_id.as_short_id()..=block_id.as_short_id(); tokio::select! { _ = good_validators.next() => { panic!("good validator completed block"); @@ -346,6 +429,49 @@ async fn network_gets_stuck_without_signatures() -> Result<()> { } _ = tokio::time::sleep(STUCK_DURATION) => { tracing::info!("network got stuck as expected"); + + // TODO: Build test around some test-only events collector + + // // 1) check event collector in each node + // for node in &nodes { + // let events = node.event_collector.stats_for_blocks(range.clone()); + // // each node should have no events + // assert_eq!(events.len(), 0); + // } + + // // 2) notify all nodes about validation completion + // for (i, node) in nodes.iter().enumerate() { + // let block_id = nodes_blocks.get(i) + // .expect("should have block id for each node"); + // node.event_collector.on_validation_complete( + // &SessionCtx { session_id }, + // block_id, + // )?; + // } + + // // 3) calc total valid and invalid signatures + // for (i, node) in nodes.iter().enumerate() { + // let is_malicious = i < malicious_node_count; + // let events = node.event_collector.stats_for_blocks(range.clone()); + // let total_invalid = events.values().map(|s| s.invalid).sum::() as usize; + // let total_valid = events.values().map(|s| s.valid).sum::() as usize; + // if is_malicious { + // // valid only self-own signature because block has a random root hash + // assert_eq!(total_valid, 1, + // "malicious node {:?} has valid signatures", node.descr.peer_id); + // // malicious nodes should have no valid signatures except their own + // assert_eq!(total_invalid, NODE_COUNT - 1, + // "malicious node {:?} has valid signatures", node.descr.peer_id); + // } else { + // // good nodes should have valid signatures from all other good nodes + // assert_eq!(total_valid, NODE_COUNT - malicious_node_count, + // "good node {:?} has no valid signatures", node.descr.peer_id); + // // good nodes should have invalid signatures from all malicious nodes + // assert_eq!(total_invalid, malicious_node_count, + // "good node {:?} has invalid signatures", node.descr.peer_id); + + // } + // } } } diff --git a/scripts/gen-dashboard.py b/scripts/gen-dashboard.py index 4a0c8415fe..655590ed62 100755 --- a/scripts/gen-dashboard.py +++ b/scripts/gen-dashboard.py @@ -2744,6 +2744,21 @@ def validator() -> RowPanel: "tycho_validator_invalid_signatures_cached_total", "Number of cached invalid signatures", ), + create_heatmap_panel( + "tycho_validator_collector_get_stats_for_blocks_time", + "Collector: get stats for blocks", + ), + create_heatmap_panel( + "tycho_validator_collector_truncate_range_time", "Collector: truncate range" + ), + create_gauge_panel( + "tycho_validator_collector_valid_sigs_total_count", + "Collector: total valid signatures in stats", + ), + create_gauge_panel( + "tycho_validator_collector_invalid_sigs_total_count", + "Collector: total invalid signatures in stats", + ), ] return create_row("Validator", metrics) diff --git a/slasher-traits/Cargo.toml b/slasher-traits/Cargo.toml new file mode 100644 index 0000000000..879b0c6cfc --- /dev/null +++ b/slasher-traits/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "tycho-slasher-traits" +description = "Tycho slasher traits." +include = ["src/**/*.rs", "src/**/*.tl", "./LICENSE-*", "./README.md"] +version.workspace = true +authors.workspace = true +edition.workspace = true +rust-version.workspace = true +repository.workspace = true +license.workspace = true + +[dependencies] +# crates.io deps + +# local deps +tycho-types = { workspace = true } + +[lints] +workspace = true diff --git a/slasher-traits/LICENSE-APACHE b/slasher-traits/LICENSE-APACHE new file mode 120000 index 0000000000..965b606f33 --- /dev/null +++ b/slasher-traits/LICENSE-APACHE @@ -0,0 +1 @@ +../LICENSE-APACHE \ No newline at end of file diff --git a/slasher-traits/LICENSE-MIT b/slasher-traits/LICENSE-MIT new file mode 120000 index 0000000000..76219eb72e --- /dev/null +++ b/slasher-traits/LICENSE-MIT @@ -0,0 +1 @@ +../LICENSE-MIT \ No newline at end of file diff --git a/slasher-traits/src/lib.rs b/slasher-traits/src/lib.rs new file mode 100644 index 0000000000..7effcbd34c --- /dev/null +++ b/slasher-traits/src/lib.rs @@ -0,0 +1,6 @@ +pub use self::validator::{ + BlockValidationScope, NoopValidatorEventsRecorder, ReceivedSignature, ValidationSessionId, + ValidatorEvents, ValidatorEventsListener, ValidatorSessionScope, +}; + +mod validator; diff --git a/slasher-traits/src/validator.rs b/slasher-traits/src/validator.rs new file mode 100644 index 0000000000..f93b1f3e23 --- /dev/null +++ b/slasher-traits/src/validator.rs @@ -0,0 +1,302 @@ +use std::mem::MaybeUninit; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, AtomicU8, Ordering}; + +use tycho_types::models::{BlockId, ValidatorDescription}; + +// TODO: Decide how to be with this collator-defined type +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct ValidationSessionId { + /// Validation round seqno. + pub seqno: u32, + /// Validator subset short seqno. + pub short_hash: u32, +} + +// TEMP +impl From<(u32, u32)> for ValidationSessionId { + #[inline] + fn from(value: (u32, u32)) -> Self { + Self { + seqno: value.0, + short_hash: value.1, + } + } +} + +// TEMP +impl Ord for ValidationSessionId { + #[inline] + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + (self.seqno, self.short_hash).cmp(&(other.seqno, other.short_hash)) + } +} + +// TEMP +impl PartialOrd for ValidationSessionId { + #[inline] + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +pub struct ValidatorEvents { + listener: Arc, +} + +impl ValidatorEvents { + pub fn new(recorder: Arc) -> Self { + Self { listener: recorder } + } + + pub fn begin_session( + &self, + session_id: ValidationSessionId, + first_mc_seqno: u32, + validators: &[ValidatorDescription], + ) -> ValidatorSessionScope { + self.listener + .on_session_started(session_id, first_mc_seqno, validators); + ValidatorSessionScope { + recorder: self.listener.clone(), + session_id, + validator_count: validators.len(), + is_sealed: AtomicBool::new(false), + } + } +} + +pub struct ValidatorSessionScope { + recorder: Arc, + session_id: ValidationSessionId, + validator_count: usize, + is_sealed: AtomicBool, +} + +impl ValidatorSessionScope { + pub fn begin_block(&self, block_id: &BlockId) -> BlockValidationScope { + BlockValidationScope { + recorder: self.recorder.clone(), + session_id: self.session_id, + block_id: *block_id, + signature_slots: vec![0; self.validator_count] + .into_iter() + .map(AtomicU8::new) + .collect::>(), + is_sealed: AtomicBool::new(false), + } + } + + pub fn finish(&self) { + if self.seal() { + self.recorder.on_session_finished(self.session_id); + } + } + + fn seal(&self) -> bool { + !self.is_sealed.swap(true, Ordering::Release) + } +} + +impl Drop for ValidatorSessionScope { + fn drop(&mut self) { + self.finish(); + } +} + +pub struct BlockValidationScope { + recorder: Arc, + session_id: ValidationSessionId, + block_id: BlockId, + signature_slots: Box<[AtomicU8]>, + is_sealed: AtomicBool, +} + +impl BlockValidationScope { + pub fn session_id(&self) -> ValidationSessionId { + self.session_id + } + + pub fn block_id(&self) -> &BlockId { + &self.block_id + } + + pub fn receive_signature(&self, validator_idx: u16, is_valid: bool) -> bool { + let mask = if is_valid { + ReceivedSignature::VALID_SIGNATURE_BIT + } else { + ReceivedSignature::INVALID_SIGNATURE_BIT + }; + + if let Some(status) = self.signature_slots.get(validator_idx as usize) { + status.fetch_or(mask, Ordering::Release) & mask == 0 + } else { + false + } + } + + pub fn commit(&self) -> bool { + if self.seal() { + // TODO: Use some unsafe magic to make this closer to a NOOP. + let mut signatures = Arc::new_uninit_slice(self.signature_slots.len()); + for (res, slot) in std::iter::zip( + Arc::get_mut(&mut signatures).unwrap(), + &self.signature_slots, + ) { + *res = MaybeUninit::new(ReceivedSignature(slot.load(Ordering::Acquire))); + } + // SAFETY: All items were initialized. + let signatures = unsafe { signatures.assume_init() }; + + self.recorder + .on_block_validated(self.session_id, &self.block_id, signatures); + true + } else { + false + } + } + + pub fn discard(&self) -> bool { + if self.seal() { + self.recorder + .on_block_skipped(self.session_id, &self.block_id); + true + } else { + false + } + } + + fn seal(&self) -> bool { + !self.is_sealed.swap(true, Ordering::Release) + } +} + +impl Drop for BlockValidationScope { + fn drop(&mut self) { + self.discard(); + } +} + +#[derive(Clone, Copy)] +#[repr(transparent)] +pub struct ReceivedSignature(u8); + +impl ReceivedSignature { + const VALID_SIGNATURE_BIT: u8 = 0b01; + const INVALID_SIGNATURE_BIT: u8 = 0b10; + + pub fn has_valid_signature(&self) -> bool { + self.0 & Self::VALID_SIGNATURE_BIT != 0 + } + + pub fn has_invalid_signature(&self) -> bool { + self.0 & Self::INVALID_SIGNATURE_BIT != 0 + } +} + +/// Unified event-sink interface for the validator. +/// +/// Implementations can decide whether to perform work inline or forward the +/// event into an async task / channel. No async methods are used here to keep +/// the trait usable in both sync and async contexts. +pub trait ValidatorEventsListener: Send + Sync + 'static { + /// Called exactly once when a new validation session is created. + fn on_session_started( + &self, + session_id: ValidationSessionId, + first_mc_seqno: u32, + validators: &[ValidatorDescription], + ); + + /// Called when the session is complete. + fn on_session_finished(&self, session_id: ValidationSessionId); + + /// Called when validation is complete for a block. + fn on_block_validated( + &self, + session_id: ValidationSessionId, + block_id: &BlockId, + signatures: Arc<[ReceivedSignature]>, + ); + + /// Called when validation is skipped for a block. + fn on_block_skipped(&self, session_id: ValidationSessionId, block_id: &BlockId); +} + +#[derive(Debug, Clone, Copy)] +pub struct NoopValidatorEventsRecorder; + +impl ValidatorEventsListener for NoopValidatorEventsRecorder { + fn on_session_started( + &self, + _session_id: ValidationSessionId, + _first_mc_seqno: u32, + _validators: &[ValidatorDescription], + ) { + } + + fn on_session_finished(&self, _session_id: ValidationSessionId) {} + + fn on_block_validated( + &self, + _session_id: ValidationSessionId, + _block_id: &BlockId, + _signatures: Arc<[ReceivedSignature]>, + ) { + } + + fn on_block_skipped(&self, _session_id: ValidationSessionId, _block_id: &BlockId) {} +} + +macro_rules! impl_recorder_for_tuples { + ($(($($ty:ident: $n:tt),+)),*$(,)?) => { + $(impl<$($ty),+> ValidatorEventsListener for ($($ty,)+) + where + $($ty: ValidatorEventsListener,)+ + { + fn on_session_started( + &self, + session_id: ValidationSessionId, + first_mc_seqno: u32, + validators: &[ValidatorDescription], + ) { + $(self.$n.on_session_started(session_id, first_mc_seqno, validators);)+ + } + + fn on_session_finished(&self, session_id: ValidationSessionId) { + $(self.$n.on_session_finished(session_id);)+ + } + + fn on_block_validated( + &self, + session_id: ValidationSessionId, + block_id: &BlockId, + signatures: Arc<[ReceivedSignature]>, + ) { + impl_recorder_for_tuples!(@call_on_validated self, session_id, block_id, signatures, $($n)+); + } + + fn on_block_skipped(&self, session_id: ValidationSessionId, block_id: &BlockId) { + $(self.$n.on_block_skipped(session_id, block_id);)+ + } + })* + }; + + (@call_on_validated $self:ident, $sid:ident, $block_id:ident, $signatures:ident, $n:tt $($rest:tt)+) => { + $self.$n.on_block_validated($sid, $block_id, $signatures.clone()); + impl_recorder_for_tuples!(@call_on_validated $self, $sid, $block_id, $signatures, $($rest)+) + }; + (@call_on_validated $self:ident, $sid:ident, $block_id:ident, $signatures:ident, $n:tt) => { + $self.$n.on_block_validated($sid, $block_id, $signatures); + }; +} + +impl_recorder_for_tuples! { + (T0: 0), + (T0: 0, T1: 1), + (T0: 0, T1: 1, T2: 2), + (T0: 0, T1: 1, T2: 2, T3: 3), + (T0: 0, T1: 1, T2: 2, T3: 3, T4: 4), + (T0: 0, T1: 1, T2: 2, T3: 3, T4: 4, T5: 5), + (T0: 0, T1: 1, T2: 2, T3: 3, T4: 4, T5: 5, T6: 6), +} diff --git a/slasher/Cargo.toml b/slasher/Cargo.toml new file mode 100644 index 0000000000..78251194b4 --- /dev/null +++ b/slasher/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "tycho-slasher" +description = "Tycho slasher implementation." +include = ["src/**/*.rs", "src/**/*.tl", "./LICENSE-*", "./README.md"] +version.workspace = true +authors.workspace = true +edition.workspace = true +rust-version.workspace = true +repository.workspace = true +license.workspace = true + +[dependencies] +# crates.io deps +anyhow = { workspace = true } +arc-swap = { workspace = true } +metrics = { workspace = true } +parking_lot = { workspace = true } +scopeguard = { workspace = true } +tokio = { workspace = true, features = ["sync"] } +tracing = { workspace = true } +tycho-crypto = { workspace = true } +tycho-types = { workspace = true } + +# local deps +tycho-slasher-traits = { workspace = true } +tycho-util = { workspace = true } + +[lints] +workspace = true diff --git a/slasher/LICENSE-APACHE b/slasher/LICENSE-APACHE new file mode 120000 index 0000000000..965b606f33 --- /dev/null +++ b/slasher/LICENSE-APACHE @@ -0,0 +1 @@ +../LICENSE-APACHE \ No newline at end of file diff --git a/slasher/LICENSE-MIT b/slasher/LICENSE-MIT new file mode 120000 index 0000000000..76219eb72e --- /dev/null +++ b/slasher/LICENSE-MIT @@ -0,0 +1 @@ +../LICENSE-MIT \ No newline at end of file diff --git a/slasher/src/collector/validator_events.rs b/slasher/src/collector/validator_events.rs new file mode 100644 index 0000000000..cf2ae9b819 --- /dev/null +++ b/slasher/src/collector/validator_events.rs @@ -0,0 +1,283 @@ +use std::sync::Arc; +use std::sync::atomic::{AtomicU32, AtomicUsize, Ordering}; + +use anyhow::Result; +use arc_swap::ArcSwap; +use tokio::sync::mpsc; +use tracing::instrument; +use tycho_slasher_traits::{ReceivedSignature, ValidationSessionId, ValidatorEventsListener}; +use tycho_types::dict; +use tycho_types::models::{BlockId, ValidatorDescription}; +use tycho_types::prelude::*; +use tycho_util::{DashMapEntry, FastDashMap, FastHashMap}; + +use crate::util::AtomicBitSet; + +// Gauges +const METRIC_SLASHER_PENDING_BLOCKS: &str = "tycho_slasher_pending_blocks"; +const METRIC_SLASHER_COMPLETE_BLOCKS: &str = "tycho_slasher_complete_blocks"; +const METRIC_SLASHER_LATEST_COMPLETE_BLOCK: &str = "tycho_slasher_latest_complete_block"; +const METRIC_SLASHER_BLOCKS_TAKEN_UNTIL: &str = "tycho_slasher_blocks_taken_until"; + +#[derive(Default)] +pub struct ValidatorEventsCollector { + default_batch_size: AtomicUsize, + sessions: FastDashMap, +} + +struct SessionState { + batch_size: usize, + validator_count: usize, + current_batch: ArcSwap, + latest_seqno: AtomicU32, + complete_batches: Option>, +} + +struct BlocksBatch { + start_seqno: u32, + committed_blocks: AtomicBitSet, + signatures_history: Box<[AtomicBitSet]>, +} + +// === Collector impl === + +impl ValidatorEventsCollector { + pub fn new(default_batch_size: usize) -> Self { + Self { + default_batch_size: AtomicUsize::new(default_batch_size), + sessions: Default::default(), + } + } + + pub fn set_default_batch_size(&self, batch_size: usize) { + self.default_batch_size.store(batch_size, Ordering::Release); + } + + pub fn init_session( + &self, + session_id: ValidationSessionId, + batch_size: usize, + complete_batches: mpsc::Sender, + ) -> bool { + let Some(mut session) = self.sessions.get_mut(&session_id) else { + return false; + }; + + // Reset the current batch if its size has changed. + // TODO: Split or grow the previous batch to not discard events. + if session.batch_size != batch_size { + session.batch_size = batch_size; + session.current_batch.store(Arc::new(BlocksBatch::new( + session.latest_seqno.load(Ordering::Acquire), + batch_size, + session.validator_count, + ))); + } + + session.complete_batches = Some(complete_batches); + + true + } + + pub fn skip_session(&self, session_id: ValidationSessionId) -> bool { + self.sessions.remove(&session_id).is_some() + } +} + +impl ValidatorEventsListener for ValidatorEventsCollector { + #[instrument(skip_all, fields(session_id = ?session_id))] + fn on_session_started( + &self, + session_id: ValidationSessionId, + first_mc_seqno: u32, + validators: &[ValidatorDescription], + ) { + tracing::debug!(first_mc_seqno, "on_session_open"); + + let validator_count = validators.len(); + let mut peer_id_to_index = + FastHashMap::with_capacity_and_hasher(validator_count, Default::default()); + let mut peer_ids = Vec::with_capacity(validator_count); + for validator in validators { + if peer_id_to_index + .insert(validator.public_key, peer_ids.len()) + .is_none() + { + peer_ids.push(validator.public_key); + } + } + + if let DashMapEntry::Vacant(v) = self.pending.entry(session_id) { + v.insert(PendingBlocks { + peer_ids: Arc::from(peer_ids), + peer_id_to_index, + pending_blocks: Default::default(), + }); + } else { + tracing::warn!("duplicate session"); + } + } + + #[instrument(skip_all, fields(session_id = ?session_id))] + fn on_session_finished(&self, session_id: ValidationSessionId) { + tracing::debug!("on_session_drop"); + if let Some((_, entry)) = self.pending.remove(&session_id) { + let removed_count = entry.pending_blocks.len(); + metrics::gauge!(METRIC_SLASHER_PENDING_BLOCKS).decrement(removed_count as f64); + } + } + + #[instrument(skip_all, fields(session_id = ?session_id))] + fn on_block_validated( + &self, + session_id: ValidationSessionId, + block_id: &BlockId, + signatures: Arc<[ReceivedSignature]>, + ) { + if !block_id.is_masterchain() { + // Ignore for non-masterchain blocks (just in case). + return; + } + + scopeguard::defer! { + self.update_latest_complete_block_seqno(block_id.seqno); + } + + tracing::debug!(%block_id, "on_validation_complete"); + let Some(session) = self.pending.get(&session_id) else { + tracing::warn!("session not found, ignoring validation_complete event"); + return; + }; + + let Some((_, block)) = session.pending_blocks.remove(block_id) else { + tracing::warn!("no signatures found for a complete session"); + return; + }; + + let peer_ids = session.peer_ids.clone(); + drop(session); + + metrics::gauge!(METRIC_SLASHER_PENDING_BLOCKS).decrement(1); + + let block = CompleteBlock { + seqno: block_id.seqno, + root_hash: block_id.root_hash, + file_hash: block_id.file_hash, + session_id, + peer_ids, + peer_signatures: AtomicSignatureState::freeze_boxed_slice(block.peer_signatures), + }; + + let mut complete = self.complete.lock(); + + // FIXME: Is this really needed? Can we even start validating block from the future first? + if block_id.seqno <= *self.latest_complete_block.borrow() { + tracing::info!("skipping an old validation result"); + return; + } + + complete.insert(block.seqno, block); + } + + #[instrument(skip_all, fields(session_id = ?session_id))] + fn on_block_skipped(&self, session_id: ValidationSessionId, block_id: &BlockId) { + if !block_id.is_masterchain() { + // Ignore for non-masterchain blocks (just in case). + return; + } + + scopeguard::defer! { + self.update_latest_complete_block_seqno(block_id.seqno); + } + + tracing::debug!(%block_id, "on_validation_skipped"); + let Some(session) = self.pending.get(&session_id) else { + tracing::warn!("session not found, skipping validation_skipped event"); + return; + }; + + let was_pending = session.pending_blocks.remove(block_id).is_some(); + drop(session); + + if was_pending { + metrics::gauge!(METRIC_SLASHER_PENDING_BLOCKS).decrement(1); + } + } +} + +// === Blocks batch impl === + +impl BlocksBatch { + fn new(start_seqno: u32, len: usize, validator_count: usize) -> Self { + Self { + start_seqno, + committed_blocks: AtomicBitSet::with_capacity(len), + signatures_history: (0..validator_count) + .into_iter() + .map(|_| AtomicBitSet::with_capacity(len * 2)) + .collect::>(), + } + } + + pub fn start_seqno(&self) -> u32 { + self.start_seqno + } + + pub fn seqno_after(&self) -> u32 { + self.start_seqno + .saturating_add(self.committed_blocks.len() as u32) + } + + pub fn contains_seqno(&self, seqno: u32) -> bool { + (self.start_seqno..self.seqno_after()).contains(&seqno) + } + + fn commit_signatures( + &mut self, + mut seqno: u32, + signatures: &[ReceivedSignature], + ) -> Result<()> { + anyhow::ensure!( + self.contains_seqno(seqno), + "seqno is out of range: got {seqno}, expected {}..{}", + self.start_seqno, + self.seqno_after(), + ); + anyhow::ensure!( + signatures.len() == self.signatures_history.len(), + "signature count mismatch: got {}, expected {}", + signatures.len(), + self.signatures_history.len(), + ); + seqno -= self.start_seqno; + + self.committed_blocks.set(seqno as usize, true); + for (history, received) in std::iter::zip(&mut self.signatures_history, signatures) { + let idx = (seqno as usize) * 2; + history.set(idx, received.has_invalid_signature()); + history.set(idx + 1, received.has_valid_signature()); + } + + Ok(()) + } + + fn build_cell(&self) -> Result { + let cx = Cell::empty_context(); + let mut b = CellBuilder::new(); + b.store_u32(self.start_seqno)?; + self.committed_blocks.store_into(&mut b, cx)?; + + let Some(dict_root) = dict::build_dict_from_sorted_iter( + self.signatures_history + .iter() + .enumerate() + .map(|(idx, bitset)| (idx as u16, bitset)), + cx, + )? + else { + return Err(tycho_types::error::Error::InvalidData); + }; + b.store_reference(dict_root)?; + b.build_ext(cx) + } +} diff --git a/slasher/src/lib.rs b/slasher/src/lib.rs new file mode 100644 index 0000000000..8ceb4a0730 --- /dev/null +++ b/slasher/src/lib.rs @@ -0,0 +1,95 @@ +use std::sync::Arc; + +use tokio::task::AbortHandle; +use tycho_crypto::ed25519; +use tycho_slasher_traits::ValidatorEventsListener; +use tycho_types::prelude::*; + +use self::collector::ValidatorEventsCollector; + +pub mod collector { + pub use self::validator_events::*; + + mod validator_events; + // TODO: mod mempool_events; +} + +mod util; + +pub struct SlasherParams { + pub node_keys: Arc, + pub initial_mc_seqno: u32, +} + +// NOTE: Stub +pub struct Slasher { + #[allow(unused)] + node_keys: Arc, + validator_events_collector: Arc, + validator_events_task_handle: AbortHandle, +} + +impl Slasher { + pub fn new(node_keys: Arc) -> Self { + let collector = Arc::new(ValidatorEventsCollector::default()); + let collector_task = tokio::task::spawn(process_validator_events(collector.clone())); + + Self { + node_keys, + validator_events_collector: collector, + validator_events_task_handle: collector_task.abort_handle(), + } + } + + pub fn validator_events_listener(&self) -> Arc { + self.validator_events_collector.clone() + } +} + +impl Drop for Slasher { + fn drop(&mut self) { + self.validator_events_task_handle.abort(); + } +} + +// === Tasks === + +#[tracing::instrument(skip_all)] +async fn process_validator_events(collector: Arc) { + tracing::info!("started"); + scopeguard::defer! { tracing::info!("finished"); }; + + const BATCH_STEP: u32 = 100; + + let mut latest_block_seqno = collector.subscribe_to_latest_block_seqno(); + + // TODO: Use more sensible initial seqno. + let mut processed_upto = 0u32; + let mut buffer = Vec::with_capacity(BATCH_STEP as _); + loop { + if *latest_block_seqno.borrow_and_update() <= processed_upto + BATCH_STEP { + latest_block_seqno + .changed() + .await + .expect("sender is never dropped while `collector` is alive"); + continue; + } + + buffer.clear(); + collector.take_batch(processed_upto + BATCH_STEP, &mut buffer); + buffer.retain(|item| item.seqno > processed_upto); + + let mut buffer = buffer.as_slice(); + while let Some(first) = buffer.first() { + let session_id = first.session_id; + let batch_size = buffer + .iter() + .take_while(|item| item.session_id == session_id) + .count(); + } + + // TODO: Build a voting matrix from completed blocks + + processed_upto += BATCH_STEP; + } +} diff --git a/slasher/src/util.rs b/slasher/src/util.rs new file mode 100644 index 0000000000..2a3a830bd6 --- /dev/null +++ b/slasher/src/util.rs @@ -0,0 +1,103 @@ +use std::ptr::NonNull; +use std::sync::atomic::{AtomicUsize, Ordering}; + +use tycho_types::prelude::*; + +pub struct AtomicBitSet { + data: NonNull, + length: usize, +} + +unsafe impl Send for AtomicBitSet {} +unsafe impl Sync for AtomicBitSet {} + +impl AtomicBitSet { + pub const BLOCK_BITS: usize = std::mem::size_of::() * 8; + + pub fn with_capacity(bits: usize) -> Self { + let data = vec![0; block_count(bits)] + .into_iter() + .map(Block::new) + .collect::>(); + + Self { + data: unsafe { NonNull::new_unchecked(Box::into_raw(data)).cast() }, + length: bits, + } + } + + pub fn len(&self) -> usize { + self.length + } + + pub fn is_zero(&self) -> bool { + self.as_slice() + .iter() + .all(|item| item.load(Ordering::Acquire) == 0) + } + + pub fn set(&self, bit: usize, enabled: bool) { + assert!( + bit < self.length, + "set at index {bit} exceeds bitset size {}", + self.length + ); + + // SAFETY: `bit` is whithin the range. + unsafe { self.set_unchecked(bit, enabled) } + } + + unsafe fn set_unchecked(&self, bit: usize, enabled: bool) { + let block = bit / Self::BLOCK_BITS; + let rem = bit % Self::BLOCK_BITS; + + let block = unsafe { &*self.data.as_ptr().add(block) }; + if enabled { + block.fetch_or(1 << rem, Ordering::Release); + } else { + block.fetch_and(!(1 << rem), Ordering::Release); + } + } + + pub fn as_slice(&self) -> &[Block] { + // SAFETY: Data was allocated for this exact block count. + unsafe { std::slice::from_raw_parts(self.data.as_ptr(), block_count(self.length)) } + } +} + +impl Drop for AtomicBitSet { + fn drop(&mut self) { + drop(unsafe { + Box::<[Block]>::from_raw(std::ptr::slice_from_raw_parts_mut( + self.data.as_ptr(), + block_count(self.length), + )) + }); + } +} + +impl Store for AtomicBitSet { + fn store_into( + &self, + b: &mut CellBuilder, + _: &dyn CellContext, + ) -> Result<(), tycho_types::error::Error> { + let Ok::(mut remaining_bits) = self.length.try_into() else { + return Err(tycho_types::error::Error::CellOverflow); + }; + + for block in self.as_slice() { + let bits = std::cmp::min(remaining_bits, Self::BLOCK_BITS as u16); + remaining_bits -= bits; + b.store_uint(block.load(Ordering::Acquire) as u64, bits)?; + } + + Ok(()) + } +} + +fn block_count(bits: usize) -> usize { + bits.div_ceil(AtomicBitSet::BLOCK_BITS) +} + +type Block = AtomicUsize; From dc35fa5681097dd6e6364b4a02748a244a0d5a89 Mon Sep 17 00:00:00 2001 From: Ivan Kalinin Date: Tue, 25 Nov 2025 14:47:37 +0100 Subject: [PATCH 02/21] feat(collator): propagate original vset item index --- Cargo.lock | 4 + block-util/src/block/block_proof_stuff.rs | 6 +- collator/src/manager/mod.rs | 14 +- collator/src/manager/utils.rs | 6 +- .../src/validator/impls/std_impl/session.rs | 4 +- collator/src/validator/mod.rs | 10 +- slasher-traits/src/validator.rs | 10 +- slasher/Cargo.toml | 4 + slasher/src/bc/mod.rs | 177 +++++++++++ slasher/src/bc/stub_contract.rs | 77 +++++ slasher/src/collector/validator_events.rs | 285 ++++++++---------- slasher/src/lib.rs | 119 ++++---- 12 files changed, 474 insertions(+), 242 deletions(-) create mode 100644 slasher/src/bc/mod.rs create mode 100644 slasher/src/bc/stub_contract.rs diff --git a/Cargo.lock b/Cargo.lock index 604eb7f38a..d9aa81701e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4435,11 +4435,15 @@ version = "0.3.9" dependencies = [ "anyhow", "arc-swap", + "dashmap", + "futures-util", "metrics", "parking_lot", "scopeguard", "tokio", "tracing", + "tycho-block-util", + "tycho-core", "tycho-crypto", "tycho-slasher-traits", "tycho-types", diff --git a/block-util/src/block/block_proof_stuff.rs b/block-util/src/block/block_proof_stuff.rs index c59e8ce743..bd0cd4df4c 100644 --- a/block-util/src/block/block_proof_stuff.rs +++ b/block-util/src/block/block_proof_stuff.rs @@ -284,7 +284,7 @@ impl BlockProofStuff { let weight = match signatures .signatures - .check_signatures(&subset.validators, &checked_data) + .check_signatures(subset.validators.iter().map(AsRef::as_ref), &checked_data) { Ok(weight) => weight, Err(e) => anyhow::bail!("proof contains invalid signatures: {e:?}"), @@ -520,7 +520,7 @@ fn pre_check_key_block_proof(virt_block: &Block) -> Result<()> { #[derive(Clone, Debug)] pub struct ValidatorSubsetInfo { - pub validators: Vec, + pub validators: Vec, pub short_hash: u32, } @@ -531,7 +531,7 @@ impl ValidatorSubsetInfo { shuffle_validators: bool, ) -> Result { let Some((validators, short_hash)) = - validator_set.compute_mc_subset(cc_seqno, shuffle_validators) + validator_set.compute_mc_subset_indexed(cc_seqno, shuffle_validators) else { anyhow::bail!("failed to compute a validator subset"); }; diff --git a/collator/src/manager/mod.rs b/collator/src/manager/mod.rs index 0e88035169..51ff2b6ec4 100644 --- a/collator/src/manager/mod.rs +++ b/collator/src/manager/mod.rs @@ -15,8 +15,8 @@ use tycho_core::global_config::MempoolGlobalConfig; use tycho_core::storage::{LoadStateHint, StateNotFound}; use tycho_crypto::ed25519::KeyPair; use tycho_types::models::{ - BlockId, BlockIdShort, CollationConfig, GlobalCapabilities, ProcessedUptoInfo, ShardIdent, - ValidatorDescription, + BlockId, BlockIdShort, CollationConfig, GlobalCapabilities, IndexedValidatorDescription, + ProcessedUptoInfo, ShardIdent, ValidatorDescription, }; use tycho_util::futures::{AwaitBlocking, JoinTask}; use tycho_util::metrics::HistogramGuard; @@ -2342,13 +2342,15 @@ where let mut subset_cache = FastHashMap::new(); let mut get_validator_subset = |shard_id| match subset_cache.entry(shard_id) { hash_map::Entry::Occupied(entry) => { - let (subset, hash_short): &(Arc>, u32) = - entry.get(); + let (subset, hash_short): &( + Arc>, + u32, + ) = entry.get(); Result::<_>::Ok((subset.clone(), *hash_short)) } hash_map::Entry::Vacant(entry) => { let (subset, hash_short) = full_validators_set - .compute_mc_subset(current_session_seqno, collation_config.shuffle_mc_validators) + .compute_mc_subset_indexed(current_session_seqno, collation_config.shuffle_mc_validators) .ok_or_else(|| anyhow!( "Error calculating subset of validators for session (shard_id = {}, seqno = {})", ShardIdent::MASTERCHAIN, @@ -2357,7 +2359,7 @@ where let subset: FastHashMap<_, _> = subset .into_iter() - .map(|vldr| (vldr.public_key.into(), vldr)) + .map(|vldr| (vldr.desc.public_key.into(), vldr)) .collect(); let subset = Arc::new(subset); diff --git a/collator/src/manager/utils.rs b/collator/src/manager/utils.rs index 35ed7e282c..bd49de7c83 100644 --- a/collator/src/manager/utils.rs +++ b/collator/src/manager/utils.rs @@ -3,9 +3,9 @@ use tycho_types::models::ValidatorDescription; use tycho_util::FastHashMap; #[cfg(not(any(feature = "test", test)))] -pub fn find_us_in_collators_set( +pub fn find_us_in_collators_set( keypair: &KeyPair, - set: &FastHashMap<[u8; 32], ValidatorDescription>, + set: &FastHashMap<[u8; 32], T>, ) -> Option { let local_pubkey = keypair.public_key; if set.contains_key(local_pubkey.as_bytes()) { @@ -16,7 +16,7 @@ pub fn find_us_in_collators_set( } #[cfg(any(test, feature = "test"))] -pub fn find_us_in_collators_set( +pub fn find_us_in_collators_set( keypair: &KeyPair, _set: &FastHashMap<[u8; 32], ValidatorDescription>, ) -> Option { diff --git a/collator/src/validator/impls/std_impl/session.rs b/collator/src/validator/impls/std_impl/session.rs index 25a229ba93..9cb2b9b237 100644 --- a/collator/src/validator/impls/std_impl/session.rs +++ b/collator/src/validator/impls/std_impl/session.rs @@ -65,9 +65,9 @@ impl ValidatorSession { ) -> Result { // Prepare a map with other validators let mut validators = FastHashMap::default(); - for (i, descr) in info.validators.iter().enumerate() { + for item in info.validators { // TODO: Skip invalid entries? But what should we do with the total weight? - let validator_info = BriefValidatorDescr::from_descr(i as u16, descr)?; + let validator_info = BriefValidatorDescr::from_descr(item)?; validators.insert(validator_info.peer_id, validator_info); } diff --git a/collator/src/validator/mod.rs b/collator/src/validator/mod.rs index b06a8b6636..aeefdc2d1d 100644 --- a/collator/src/validator/mod.rs +++ b/collator/src/validator/mod.rs @@ -4,7 +4,9 @@ use anyhow::Result; use async_trait::async_trait; use tycho_crypto::ed25519::PublicKey; use tycho_network::{Network, OverlayService, PeerId, PeerResolver}; -use tycho_types::models::{BlockId, BlockIdShort, ShardIdent, ValidatorDescription}; +use tycho_types::models::{ + BlockId, BlockIdShort, IndexedValidatorDescription, ShardIdent, ValidatorDescription, +}; use tycho_util::FastHashMap; pub use self::impls::*; @@ -70,7 +72,7 @@ pub struct AddSession<'a> { pub shard_ident: ShardIdent, pub start_block_seqno: u32, pub session_id: ValidationSessionId, - pub validators: &'a [ValidatorDescription], + pub validators: &'a [IndexedValidatorDescription], } #[derive(Debug, Clone)] @@ -96,7 +98,7 @@ pub struct BriefValidatorDescr { } impl BriefValidatorDescr { - pub fn from_descr(validator_idx: u16, descr: &ValidatorDescription) -> Result { + pub fn from_descr(descr: &IndexedValidatorDescription) -> Result { let Some(public_key) = PublicKey::from_bytes(descr.public_key.0) else { anyhow::bail!("invalid validator public key"); }; @@ -105,7 +107,7 @@ impl BriefValidatorDescr { peer_id: PeerId(descr.public_key.0), public_key, weight: descr.weight, - validator_idx, + validator_idx: descr.validator_idx, }) } } diff --git a/slasher-traits/src/validator.rs b/slasher-traits/src/validator.rs index f93b1f3e23..dbeefbbfec 100644 --- a/slasher-traits/src/validator.rs +++ b/slasher-traits/src/validator.rs @@ -2,7 +2,7 @@ use std::mem::MaybeUninit; use std::sync::Arc; use std::sync::atomic::{AtomicBool, AtomicU8, Ordering}; -use tycho_types::models::{BlockId, ValidatorDescription}; +use tycho_types::models::{BlockId, IndexedValidatorDescription}; // TODO: Decide how to be with this collator-defined type #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -53,7 +53,7 @@ impl ValidatorEvents { &self, session_id: ValidationSessionId, first_mc_seqno: u32, - validators: &[ValidatorDescription], + validators: &[IndexedValidatorDescription], ) -> ValidatorSessionScope { self.listener .on_session_started(session_id, first_mc_seqno, validators); @@ -205,7 +205,7 @@ pub trait ValidatorEventsListener: Send + Sync + 'static { &self, session_id: ValidationSessionId, first_mc_seqno: u32, - validators: &[ValidatorDescription], + validators: &[IndexedValidatorDescription], ); /// Called when the session is complete. @@ -231,7 +231,7 @@ impl ValidatorEventsListener for NoopValidatorEventsRecorder { &self, _session_id: ValidationSessionId, _first_mc_seqno: u32, - _validators: &[ValidatorDescription], + _validators: &[IndexedValidatorDescription], ) { } @@ -258,7 +258,7 @@ macro_rules! impl_recorder_for_tuples { &self, session_id: ValidationSessionId, first_mc_seqno: u32, - validators: &[ValidatorDescription], + validators: &[IndexedValidatorDescription], ) { $(self.$n.on_session_started(session_id, first_mc_seqno, validators);)+ } diff --git a/slasher/Cargo.toml b/slasher/Cargo.toml index 78251194b4..0fe90cb060 100644 --- a/slasher/Cargo.toml +++ b/slasher/Cargo.toml @@ -13,6 +13,8 @@ license.workspace = true # crates.io deps anyhow = { workspace = true } arc-swap = { workspace = true } +dashmap = { workspace = true } +futures-util = { workspace = true} metrics = { workspace = true } parking_lot = { workspace = true } scopeguard = { workspace = true } @@ -22,6 +24,8 @@ tycho-crypto = { workspace = true } tycho-types = { workspace = true } # local deps +tycho-block-util = { workspace = true } +tycho-core = { workspace = true } tycho-slasher-traits = { workspace = true } tycho-util = { workspace = true } diff --git a/slasher/src/bc/mod.rs b/slasher/src/bc/mod.rs new file mode 100644 index 0000000000..bbf8be40ac --- /dev/null +++ b/slasher/src/bc/mod.rs @@ -0,0 +1,177 @@ +use std::num::NonZeroU32; +use std::time::Duration; + +use anyhow::Result; +use tokio::sync::oneshot; +use tycho_crypto::ed25519; +use tycho_slasher_traits::{ReceivedSignature, ValidationSessionId}; +use tycho_types::cell::{HashBytes, Lazy}; +use tycho_types::models::{ + AccountBlock, AccountState, BlockchainConfigParams, OwnedMessage, StdAddr, +}; +use tycho_util::FastDashMap; + +use crate::util::AtomicBitSet; + +mod stub_contract; + +#[derive(Clone, Copy)] +pub struct EncodeBlocksBatchMessage<'a> { + pub state: &'a AccountState, + pub session_id: ValidationSessionId, + pub batch: &'a BlocksBatch, + pub validator_idx: u16, + pub keypair: &'a ed25519::KeyPair, + pub ttl: Duration, +} + +pub trait SlasherContract: Send + Sync + 'static { + fn find_account_address(&self, config: &BlockchainConfigParams) -> Result>; + + fn get_batch_size(&self, state: &AccountState) -> Result; + + fn encode_blocks_batch_message( + &self, + params: &EncodeBlocksBatchMessage<'_>, + ) -> Result; +} + +pub struct SignedMessage { + pub message: Lazy, + pub expire_at: u32, +} + +pub struct ContractSubscription { + address: StdAddr, + pending_messages: FastDashMap, +} + +impl ContractSubscription { + pub fn new(address: &StdAddr) -> Self { + Self { + address: address.clone(), + pending_messages: Default::default(), + } + } + + pub fn address(&self) -> &StdAddr { + &self.address + } + + pub fn track_message( + &self, + msg_hash: &HashBytes, + expire_at: u32, + ) -> Result> { + use dashmap::mapref::entry::Entry; + + let (tx, rx) = oneshot::channel(); + match self.pending_messages.entry(*msg_hash) { + Entry::Vacant(entry) => { + entry.insert(PendingMessage { expire_at, tx }); + Ok(rx) + } + Entry::Occupied(_) => anyhow::bail!("duplicate external message: {msg_hash}"), + } + } + + pub fn handle_account_transactions(&self, account_block: &AccountBlock) -> Result<()> { + for entry in account_block.transactions.iter() { + let (_, _, tx) = entry?; + let tx_hash = tx.repr_hash(); + let tx = tx.load()?; + + let Some(in_msg) = tx.in_msg else { + continue; + }; + + if let Some((_, pending)) = self.pending_messages.remove(in_msg.repr_hash()) { + pending + .tx + .send(MessageDeliveryStatus::Sent { tx_hash: *tx_hash }) + .ok(); + } + } + + Ok(()) + } + + pub fn cleanup_expired_messages(&self, now_sec: u32) { + self.pending_messages + .retain(|_, msg| msg.expire_at >= now_sec); + } +} + +struct PendingMessage { + expire_at: u32, + tx: oneshot::Sender, +} + +#[derive(Debug, Clone, Copy)] +enum MessageDeliveryStatus { + Sent { tx_hash: HashBytes }, + Expired, +} + +pub struct BlocksBatch { + pub start_seqno: u32, + pub committed_blocks: AtomicBitSet, + pub signatures_history: Box<[SignatureHistory]>, +} + +impl BlocksBatch { + fn new(start_seqno: u32, len: NonZeroU32, map_ids: &[u16]) -> Self { + let len = len.get() as usize; + + Self { + start_seqno, + committed_blocks: AtomicBitSet::with_capacity(len), + signatures_history: map_ids + .iter() + .map(|validator_idx| SignatureHistory { + validator_idx: *validator_idx, + bits: AtomicBitSet::with_capacity(len * 2), + }) + .collect::>(), + } + } + + pub fn is_empty(&self) -> bool { + self.committed_blocks.is_zero() + } + + pub fn start_seqno(&self) -> u32 { + self.start_seqno + } + + pub fn seqno_after(&self) -> u32 { + self.start_seqno + .saturating_add(self.committed_blocks.len() as u32) + } + + pub fn contains_seqno(&self, seqno: u32) -> bool { + (self.start_seqno..self.seqno_after()).contains(&seqno) + } + + pub fn commit_signatures(&self, mut seqno: u32, signatures: &[ReceivedSignature]) -> bool { + if !self.contains_seqno(seqno) || signatures.len() != self.signatures_history.len() { + return false; + } + + seqno -= self.start_seqno; + + self.committed_blocks.set(seqno as usize, true); + for (history, received) in std::iter::zip(&self.signatures_history, signatures) { + let idx = (seqno as usize) * 2; + history.bits.set(idx, received.has_invalid_signature()); + history.bits.set(idx + 1, received.has_valid_signature()); + } + + true + } +} + +pub struct SignatureHistory { + pub validator_idx: u16, + pub bits: AtomicBitSet, +} diff --git a/slasher/src/bc/stub_contract.rs b/slasher/src/bc/stub_contract.rs new file mode 100644 index 0000000000..678805941a --- /dev/null +++ b/slasher/src/bc/stub_contract.rs @@ -0,0 +1,77 @@ +use std::num::NonZeroU32; + +use anyhow::{Context, Result}; +use tycho_types::cell::Lazy; +use tycho_types::dict; +use tycho_types::models::{ + AccountState, BlockchainConfigParams, ExtInMsgInfo, MsgInfo, OwnedMessage, StdAddr, +}; +use tycho_types::prelude::*; + +use super::{BlocksBatch, SignedMessage, SlasherContract}; + +pub struct StubContract; + +impl SlasherContract for StubContract { + fn find_account_address(&self, _config: &BlockchainConfigParams) -> Result> { + Ok(None) + } + + fn get_batch_size(&self, _state: &AccountState) -> Result { + Ok(NonZeroU32::new(100).unwrap()) + } + + fn encode_blocks_batch_message( + &self, + params: &super::EncodeBlocksBatchMessage<'_>, + ) -> Result { + let cell = CellBuilder::build_from(StoreBlocksBatch(params.batch)) + .context("failed to serialize blocks batch")?; + + let now = tycho_util::time::now_millis(); + + let expire_at = (now / 1000).saturating_add(params.ttl.as_secs()) as u32; + let message = Lazy::new(&OwnedMessage { + info: MsgInfo::ExtIn(ExtInMsgInfo { + // Stub address. + dst: StdAddr::new(-1, HashBytes::ZERO).into(), + ..Default::default() + }), + init: None, + body: cell.into(), + layout: None, + })?; + + Ok(SignedMessage { message, expire_at }) + } +} + +struct StoreBlocksBatch<'a>(&'a BlocksBatch); + +impl Store for StoreBlocksBatch<'_> { + fn store_into( + &self, + builder: &mut CellBuilder, + context: &dyn CellContext, + ) -> Result<(), tycho_types::error::Error> { + let batch = self.0; + + builder.store_u32(batch.start_seqno)?; + batch.committed_blocks.store_into(builder, context)?; + + // A subset contains items in no particular order, + // so we need to sort by them to simplify remapping to vset. + let mut entries = batch + .signatures_history + .iter() + .map(|item| (item.validator_idx, &item.bits)) + .collect::>(); + entries.sort_unstable_by_key(|(a, _)| *a); + + let Some(dict_root) = dict::build_dict_from_sorted_iter(entries, context)? else { + // Subset must not be empty. + return Err(tycho_types::error::Error::InvalidData); + }; + builder.store_reference(dict_root) + } +} diff --git a/slasher/src/collector/validator_events.rs b/slasher/src/collector/validator_events.rs index cf2ae9b819..e39e9edf50 100644 --- a/slasher/src/collector/validator_events.rs +++ b/slasher/src/collector/validator_events.rs @@ -1,63 +1,53 @@ +use std::num::NonZeroU32; use std::sync::Arc; -use std::sync::atomic::{AtomicU32, AtomicUsize, Ordering}; +use std::sync::atomic::{AtomicU32, Ordering}; -use anyhow::Result; -use arc_swap::ArcSwap; +use anyhow::{Context, Result}; use tokio::sync::mpsc; use tracing::instrument; use tycho_slasher_traits::{ReceivedSignature, ValidationSessionId, ValidatorEventsListener}; -use tycho_types::dict; -use tycho_types::models::{BlockId, ValidatorDescription}; +use tycho_types::models::{BlockId, IndexedValidatorDescription}; use tycho_types::prelude::*; -use tycho_util::{DashMapEntry, FastDashMap, FastHashMap}; +use tycho_util::{DashMapEntry, FastDashMap}; -use crate::util::AtomicBitSet; - -// Gauges -const METRIC_SLASHER_PENDING_BLOCKS: &str = "tycho_slasher_pending_blocks"; -const METRIC_SLASHER_COMPLETE_BLOCKS: &str = "tycho_slasher_complete_blocks"; -const METRIC_SLASHER_LATEST_COMPLETE_BLOCK: &str = "tycho_slasher_latest_complete_block"; -const METRIC_SLASHER_BLOCKS_TAKEN_UNTIL: &str = "tycho_slasher_blocks_taken_until"; +use crate::bc::BlocksBatch; #[derive(Default)] pub struct ValidatorEventsCollector { - default_batch_size: AtomicUsize, + default_batch_size: AtomicU32, sessions: FastDashMap, } struct SessionState { - batch_size: usize, - validator_count: usize, - current_batch: ArcSwap, - latest_seqno: AtomicU32, - complete_batches: Option>, -} - -struct BlocksBatch { - start_seqno: u32, - committed_blocks: AtomicBitSet, - signatures_history: Box<[AtomicBitSet]>, + batch_size: NonZeroU32, + /// Maps each subset item with its original vset index. + validator_indices: Box<[u16]>, + current_batch: BlocksBatch, + first_seqno: u32, + next_expected_seqno: u32, + complete_batches: Option>, } // === Collector impl === impl ValidatorEventsCollector { - pub fn new(default_batch_size: usize) -> Self { + pub fn new(default_batch_size: NonZeroU32) -> Self { Self { - default_batch_size: AtomicUsize::new(default_batch_size), + default_batch_size: AtomicU32::new(default_batch_size.get()), sessions: Default::default(), } } - pub fn set_default_batch_size(&self, batch_size: usize) { - self.default_batch_size.store(batch_size, Ordering::Release); + pub fn set_default_batch_size(&self, batch_size: NonZeroU32) { + self.default_batch_size + .store(batch_size.get(), Ordering::Release); } pub fn init_session( &self, session_id: ValidationSessionId, - batch_size: usize, - complete_batches: mpsc::Sender, + batch_size: NonZeroU32, + complete_batches: mpsc::UnboundedSender, ) -> bool { let Some(mut session) = self.sessions.get_mut(&session_id) else { return false; @@ -67,11 +57,11 @@ impl ValidatorEventsCollector { // TODO: Split or grow the previous batch to not discard events. if session.batch_size != batch_size { session.batch_size = batch_size; - session.current_batch.store(Arc::new(BlocksBatch::new( - session.latest_seqno.load(Ordering::Acquire), + session.current_batch = BlocksBatch::new( + session.align_seqno(session.next_expected_seqno), batch_size, - session.validator_count, - ))); + &session.validator_indices, + ); } session.complete_batches = Some(complete_batches); @@ -90,28 +80,27 @@ impl ValidatorEventsListener for ValidatorEventsCollector { &self, session_id: ValidationSessionId, first_mc_seqno: u32, - validators: &[ValidatorDescription], + validators: &[IndexedValidatorDescription], ) { tracing::debug!(first_mc_seqno, "on_session_open"); - let validator_count = validators.len(); - let mut peer_id_to_index = - FastHashMap::with_capacity_and_hasher(validator_count, Default::default()); - let mut peer_ids = Vec::with_capacity(validator_count); - for validator in validators { - if peer_id_to_index - .insert(validator.public_key, peer_ids.len()) - .is_none() - { - peer_ids.push(validator.public_key); - } - } + let validator_indices = validators + .iter() + .map(|item| item.validator_idx) + .collect::>(); + + let batch_size = NonZeroU32::new(self.default_batch_size.load(Ordering::Acquire)).unwrap(); + let current_batch = BlocksBatch::new(first_mc_seqno, batch_size, &validator_indices); - if let DashMapEntry::Vacant(v) = self.pending.entry(session_id) { - v.insert(PendingBlocks { - peer_ids: Arc::from(peer_ids), - peer_id_to_index, - pending_blocks: Default::default(), + if let DashMapEntry::Vacant(v) = self.sessions.entry(session_id) { + v.insert(SessionState { + batch_size, + validator_indices, + current_batch, + first_seqno: first_mc_seqno, + next_expected_seqno: first_mc_seqno, + // Will be initialized later via `init_session`. + complete_batches: None, }); } else { tracing::warn!("duplicate session"); @@ -121,9 +110,10 @@ impl ValidatorEventsListener for ValidatorEventsCollector { #[instrument(skip_all, fields(session_id = ?session_id))] fn on_session_finished(&self, session_id: ValidationSessionId) { tracing::debug!("on_session_drop"); - if let Some((_, entry)) = self.pending.remove(&session_id) { - let removed_count = entry.pending_blocks.len(); - metrics::gauge!(METRIC_SLASHER_PENDING_BLOCKS).decrement(removed_count as f64); + if let Some((_, session)) = self.sessions.remove(&session_id) + && let Err(e) = session.commit_batch(&session.current_batch) + { + tracing::warn!("failed to commit blocks batch on finish: {e:?}"); } } @@ -139,44 +129,12 @@ impl ValidatorEventsListener for ValidatorEventsCollector { return; } - scopeguard::defer! { - self.update_latest_complete_block_seqno(block_id.seqno); - } - tracing::debug!(%block_id, "on_validation_complete"); - let Some(session) = self.pending.get(&session_id) else { - tracing::warn!("session not found, ignoring validation_complete event"); - return; - }; - - let Some((_, block)) = session.pending_blocks.remove(block_id) else { - tracing::warn!("no signatures found for a complete session"); + let Some(mut session) = self.sessions.get_mut(&session_id) else { + tracing::warn!("session not found, ignoring on_block_validated event"); return; }; - - let peer_ids = session.peer_ids.clone(); - drop(session); - - metrics::gauge!(METRIC_SLASHER_PENDING_BLOCKS).decrement(1); - - let block = CompleteBlock { - seqno: block_id.seqno, - root_hash: block_id.root_hash, - file_hash: block_id.file_hash, - session_id, - peer_ids, - peer_signatures: AtomicSignatureState::freeze_boxed_slice(block.peer_signatures), - }; - - let mut complete = self.complete.lock(); - - // FIXME: Is this really needed? Can we even start validating block from the future first? - if block_id.seqno <= *self.latest_complete_block.borrow() { - tracing::info!("skipping an old validation result"); - return; - } - - complete.insert(block.seqno, block); + session.handle_block(block_id.seqno, Some(signatures.as_ref())); } #[instrument(skip_all, fields(session_id = ?session_id))] @@ -186,98 +144,93 @@ impl ValidatorEventsListener for ValidatorEventsCollector { return; } - scopeguard::defer! { - self.update_latest_complete_block_seqno(block_id.seqno); - } - - tracing::debug!(%block_id, "on_validation_skipped"); - let Some(session) = self.pending.get(&session_id) else { - tracing::warn!("session not found, skipping validation_skipped event"); + tracing::debug!(%block_id, "on_block_skipped"); + let Some(mut session) = self.sessions.get_mut(&session_id) else { + tracing::warn!("session not found, ignoring on_block_skipped event"); return; }; - - let was_pending = session.pending_blocks.remove(block_id).is_some(); - drop(session); - - if was_pending { - metrics::gauge!(METRIC_SLASHER_PENDING_BLOCKS).decrement(1); - } + session.handle_block(block_id.seqno, None); } } -// === Blocks batch impl === +// === Session state impl === -impl BlocksBatch { - fn new(start_seqno: u32, len: usize, validator_count: usize) -> Self { - Self { - start_seqno, - committed_blocks: AtomicBitSet::with_capacity(len), - signatures_history: (0..validator_count) - .into_iter() - .map(|_| AtomicBitSet::with_capacity(len * 2)) - .collect::>(), - } - } +impl SessionState { + fn handle_block(&mut self, seqno: u32, signatures: Option<&[ReceivedSignature]>) -> bool { + let to_commit = match self.try_advance_current_batch(seqno) { + AdvanceBlockStatus::TooOld => return false, + AdvanceBlockStatus::Unchanged => None, + AdvanceBlockStatus::Replaced(batch) => Some(batch), + }; - pub fn start_seqno(&self) -> u32 { - self.start_seqno - } + let event_type = match signatures { + Some(signatures) => { + self.current_batch + .commit_signatures(seqno, signatures) + .expect("ranges must be consistent"); + "validated" + } + None => "skipped", + }; - pub fn seqno_after(&self) -> u32 { - self.start_seqno - .saturating_add(self.committed_blocks.len() as u32) + if let Some(batch) = to_commit + && let Err(e) = self.commit_batch(&batch) + { + tracing::error!(event_type, "failed to commit blocks batch: {e:?}"); + } + true } - pub fn contains_seqno(&self, seqno: u32) -> bool { - (self.start_seqno..self.seqno_after()).contains(&seqno) - } + fn try_advance_current_batch(&mut self, seqno: u32) -> AdvanceBlockStatus { + if seqno < self.next_expected_seqno { + return AdvanceBlockStatus::TooOld; + } else if self.current_batch.contains_seqno(seqno) { + return AdvanceBlockStatus::Unchanged; + } - fn commit_signatures( - &mut self, - mut seqno: u32, - signatures: &[ReceivedSignature], - ) -> Result<()> { - anyhow::ensure!( - self.contains_seqno(seqno), - "seqno is out of range: got {seqno}, expected {}..{}", - self.start_seqno, - self.seqno_after(), - ); - anyhow::ensure!( - signatures.len() == self.signatures_history.len(), - "signature count mismatch: got {}, expected {}", - signatures.len(), - self.signatures_history.len(), + let start_seqno = self.align_seqno(seqno); + let prev_batch = std::mem::replace( + &mut self.current_batch, + BlocksBatch::new(start_seqno, self.batch_size, &self.validator_indices), ); - seqno -= self.start_seqno; + self.next_expected_seqno = seqno + 1; + + AdvanceBlockStatus::Replaced(prev_batch) + } - self.committed_blocks.set(seqno as usize, true); - for (history, received) in std::iter::zip(&mut self.signatures_history, signatures) { - let idx = (seqno as usize) * 2; - history.set(idx, received.has_invalid_signature()); - history.set(idx + 1, received.has_valid_signature()); + fn commit_batch(&self, batch: &BlocksBatch) -> Result<()> { + if batch.is_empty() { + return Ok(()); } + let cell = batch + .build_cell() + .context("failed to pack batch into a cell")?; + + let Some(tx) = &self.complete_batches else { + anyhow::bail!("not initialized"); + }; + + if tx.send(cell).is_err() { + anyhow::bail!("channel closed"); + } Ok(()) } - fn build_cell(&self) -> Result { - let cx = Cell::empty_context(); - let mut b = CellBuilder::new(); - b.store_u32(self.start_seqno)?; - self.committed_blocks.store_into(&mut b, cx)?; - - let Some(dict_root) = dict::build_dict_from_sorted_iter( - self.signatures_history - .iter() - .enumerate() - .map(|(idx, bitset)| (idx as u16, bitset)), - cx, - )? - else { - return Err(tycho_types::error::Error::InvalidData); - }; - b.store_reference(dict_root)?; - b.build_ext(cx) + fn align_seqno(&self, seqno: u32) -> u32 { + assert!(seqno >= self.first_seqno); + + // Example: + // batch_size = 100 + // first_seqno = 101 + // seqno = 250 + // result = 250 - (250 - 101) % 100 = 201 + seqno - (seqno - self.first_seqno) % self.batch_size.get() } } + +enum AdvanceBlockStatus { + TooOld, + Unchanged, + Replaced(BlocksBatch), +} diff --git a/slasher/src/lib.rs b/slasher/src/lib.rs index 8ceb4a0730..abe5dbe007 100644 --- a/slasher/src/lib.rs +++ b/slasher/src/lib.rs @@ -1,10 +1,16 @@ use std::sync::Arc; -use tokio::task::AbortHandle; +use anyhow::{Context, Result}; +use arc_swap::ArcSwapOption; +use futures_util::future::BoxFuture; +use tycho_core::block_strider::{StateSubscriber, StateSubscriberContext}; use tycho_crypto::ed25519; use tycho_slasher_traits::ValidatorEventsListener; -use tycho_types::prelude::*; +pub use self::bc::{ + BlocksBatch, ContractSubscription, EncodeBlocksBatchMessage, SignatureHistory, SignedMessage, + SlasherContract, +}; use self::collector::ValidatorEventsCollector; pub mod collector { @@ -14,6 +20,7 @@ pub mod collector { // TODO: mod mempool_events; } +mod bc; mod util; pub struct SlasherParams { @@ -22,74 +29,80 @@ pub struct SlasherParams { } // NOTE: Stub +#[derive(Clone)] +#[repr(transparent)] pub struct Slasher { - #[allow(unused)] - node_keys: Arc, - validator_events_collector: Arc, - validator_events_task_handle: AbortHandle, + inner: Arc, } impl Slasher { - pub fn new(node_keys: Arc) -> Self { + pub fn new(node_keys: Arc, contract: C) -> Self { let collector = Arc::new(ValidatorEventsCollector::default()); - let collector_task = tokio::task::spawn(process_validator_events(collector.clone())); Self { - node_keys, - validator_events_collector: collector, - validator_events_task_handle: collector_task.abort_handle(), + inner: Arc::new(Inner { + node_keys, + validator_events_collector: collector, + contract: Box::new(contract), + subscription: ArcSwapOption::empty(), + }), } } pub fn validator_events_listener(&self) -> Arc { - self.validator_events_collector.clone() - } -} - -impl Drop for Slasher { - fn drop(&mut self) { - self.validator_events_task_handle.abort(); + self.inner.validator_events_collector.clone() } -} - -// === Tasks === - -#[tracing::instrument(skip_all)] -async fn process_validator_events(collector: Arc) { - tracing::info!("started"); - scopeguard::defer! { tracing::info!("finished"); }; - - const BATCH_STEP: u32 = 100; - let mut latest_block_seqno = collector.subscribe_to_latest_block_seqno(); - - // TODO: Use more sensible initial seqno. - let mut processed_upto = 0u32; - let mut buffer = Vec::with_capacity(BATCH_STEP as _); - loop { - if *latest_block_seqno.borrow_and_update() <= processed_upto + BATCH_STEP { - latest_block_seqno - .changed() - .await - .expect("sender is never dropped while `collector` is alive"); - continue; + async fn handle_state_impl(&self, cx: &StateSubscriberContext) -> Result<()> { + if !cx.block.id().is_masterchain() { + return Ok(()); } - buffer.clear(); - collector.take_batch(processed_upto + BATCH_STEP, &mut buffer); - buffer.retain(|item| item.seqno > processed_upto); - - let mut buffer = buffer.as_slice(); - while let Some(first) = buffer.first() { - let session_id = first.session_id; - let batch_size = buffer - .iter() - .take_while(|item| item.session_id == session_id) - .count(); + let this = self.inner.as_ref(); + + // Check config updates + let config_params = cx.state.config_params()?; + let Some(slasher_address) = this + .contract + .find_account_address(&config_params) + .context("failed to find contract address")? + .filter(|addr| addr.is_masterchain()) + else { + return Ok(()); + }; + + let subscription = match this.subscription.load_full() { + Some(s) if s.address() == &slasher_address => s, + // TODO: Use `ArcSwap::compare_and_swap`? + _ => { + let s = Arc::new(ContractSubscription::new(&slasher_address)); + this.subscription.store(Some(s.clone())); + s + } + }; + + let extra = cx.block.load_extra()?.account_blocks.load()?; + if let Some((_, account_block)) = extra.get(&slasher_address.address)? { + subscription.handle_account_transactions(&account_block)?; } - // TODO: Build a voting matrix from completed blocks + Ok(()) + } +} - processed_upto += BATCH_STEP; +impl StateSubscriber for Slasher { + type HandleStateFut<'a> = BoxFuture<'a, Result<()>>; + + #[inline] + fn handle_state<'a>(&'a self, cx: &'a StateSubscriberContext) -> Self::HandleStateFut<'a> { + Box::pin(self.handle_state_impl(cx)) } } + +struct Inner { + #[allow(unused)] + node_keys: Arc, + validator_events_collector: Arc, + contract: Box, + subscription: ArcSwapOption, +} From b63c2702bbf6b48b85dec849da69ade8a14d9c9d Mon Sep 17 00:00:00 2001 From: Ivan Kalinin Date: Fri, 28 Nov 2025 13:01:39 +0100 Subject: [PATCH 03/21] feat(core): add boxed block/state subscribers --- core/src/block_strider/mod.rs | 7 +- .../subscriber/box_subscriber.rs | 374 ++++++++++++++++++ core/src/block_strider/subscriber/mod.rs | 20 + slasher/src/bc/mod.rs | 2 +- slasher/src/collector/validator_events.rs | 8 +- 5 files changed, 404 insertions(+), 7 deletions(-) create mode 100644 core/src/block_strider/subscriber/box_subscriber.rs diff --git a/core/src/block_strider/mod.rs b/core/src/block_strider/mod.rs index 55000accbe..333219b459 100644 --- a/core/src/block_strider/mod.rs +++ b/core/src/block_strider/mod.rs @@ -36,9 +36,10 @@ pub use self::state_applier::ShardStateApplier; pub use self::subscriber::test::PrintSubscriber; pub use self::subscriber::{ ArchiveSubscriber, ArchiveSubscriberContext, ArchiveSubscriberExt, BlockSubscriber, - BlockSubscriberContext, BlockSubscriberExt, ChainSubscriber, DelayedTasks, - DelayedTasksJoinHandle, DelayedTasksSpawner, MetricsSubscriber, NoopSubscriber, - StateSubscriber, StateSubscriberContext, StateSubscriberExt, + BlockSubscriberContext, BlockSubscriberExt, BoxBlockSubscriber, BoxStateSubscriber, + ChainSubscriber, DelayedTasks, DelayedTasksJoinHandle, DelayedTasksSpawner, MetricsSubscriber, + NoopSubscriber, OptionHandleFut, OptionPrepareFut, StateSubscriber, StateSubscriberContext, + StateSubscriberExt, }; use crate::storage::CoreStorage; diff --git a/core/src/block_strider/subscriber/box_subscriber.rs b/core/src/block_strider/subscriber/box_subscriber.rs new file mode 100644 index 0000000000..328c22bd1e --- /dev/null +++ b/core/src/block_strider/subscriber/box_subscriber.rs @@ -0,0 +1,374 @@ +use std::any::Any; +use std::sync::atomic::{AtomicPtr, Ordering}; + +use anyhow::Result; +use futures_util::FutureExt; +use futures_util::future::BoxFuture; + +use super::{StateSubscriber, StateSubscriberContext}; +use crate::block_strider::subscriber::{BlockSubscriber, BlockSubscriberContext}; + +// === Boxed BlockSubscriber === + +pub struct BoxBlockSubscriber { + data: AtomicPtr<()>, + vtable: &'static BlockVtable, +} + +impl BoxBlockSubscriber { + pub fn new(subscriber: S) -> Self { + let ptr = Box::into_raw(Box::new(subscriber)); + + Self { + data: AtomicPtr::new(ptr.cast()), + vtable: const { BlockVtable::new::() }, + } + } +} + +impl BlockSubscriber for BoxBlockSubscriber { + type Prepared = BoxPrepared; + + type PrepareBlockFut<'a> = PrepareBlockFut<'a>; + type HandleBlockFut<'a> = HandleBlockFut<'a>; + + #[inline] + fn prepare_block<'a>(&'a self, cx: &'a BlockSubscriberContext) -> Self::PrepareBlockFut<'a> { + unsafe { (self.vtable.prepare_block)(&self.data, cx) } + } + + #[inline] + fn handle_block<'a>( + &'a self, + cx: &'a BlockSubscriberContext, + prepared: Self::Prepared, + ) -> Self::HandleBlockFut<'a> { + unsafe { (self.vtable.handle_block)(&self.data, cx, prepared) } + } +} + +impl Drop for BoxBlockSubscriber { + fn drop(&mut self) { + unsafe { (self.vtable.drop)(&mut self.data) } + } +} + +// Vtable must enforce this behavior +unsafe impl Send for BoxBlockSubscriber {} +unsafe impl Sync for BoxBlockSubscriber {} + +struct BlockVtable { + prepare_block: PrepareBlockFn, + handle_block: HandleBlockFn, + drop: DropFn, +} + +impl BlockVtable { + const fn new() -> &'static Self { + &Self { + prepare_block: |ptr, cx| { + let subscriber = unsafe { &*ptr.load(Ordering::Relaxed).cast::() }; + subscriber + .prepare_block(cx) + .map(|result| result.map(|data| Box::new(data) as BoxPrepared)) + .boxed() + }, + handle_block: |ptr, cx, prepared| { + let subscriber = unsafe { &*ptr.load(Ordering::Relaxed).cast::() }; + match prepared.downcast::() { + Ok(prepared) => subscriber.handle_block(cx, *prepared).boxed(), + Err(_) => Box::pin(futures_util::future::ready(Err(anyhow::Error::from( + PreparedTypeMismatch, + )))), + } + }, + drop: |ptr| { + drop(unsafe { Box::::from_raw(ptr.get_mut().cast::()) }); + }, + } + } +} + +type BoxPrepared = Box; + +type PrepareBlockFn = + for<'a> unsafe fn(&AtomicPtr<()>, &'a BlockSubscriberContext) -> PrepareBlockFut<'a>; +type HandleBlockFn = for<'a> unsafe fn( + &AtomicPtr<()>, + &'a BlockSubscriberContext, + BoxPrepared, +) -> HandleBlockFut<'a>; + +type PrepareBlockFut<'a> = BoxFuture<'a, Result>; +type HandleBlockFut<'a> = BoxFuture<'a, Result<()>>; + +#[derive(thiserror::Error, Debug)] +#[error("prepared type mismatch")] +struct PreparedTypeMismatch; + +// === Boxed StateSubscriber === + +pub struct BoxStateSubscriber { + data: AtomicPtr<()>, + vtable: &'static StateVtable, +} + +impl BoxStateSubscriber { + pub fn new(subscriber: S) -> Self { + let ptr = Box::into_raw(Box::new(subscriber)); + + Self { + data: AtomicPtr::new(ptr.cast()), + vtable: const { StateVtable::new::() }, + } + } +} + +impl StateSubscriber for BoxStateSubscriber { + type HandleStateFut<'a> = HandleStateFut<'a>; + + #[inline] + fn handle_state<'a>(&'a self, cx: &'a StateSubscriberContext) -> Self::HandleStateFut<'a> { + unsafe { (self.vtable.handle_state)(&self.data, cx) } + } +} + +impl Drop for BoxStateSubscriber { + fn drop(&mut self) { + unsafe { (self.vtable.drop)(&mut self.data) } + } +} + +// Vtable must enforce this behavior +unsafe impl Send for BoxStateSubscriber {} +unsafe impl Sync for BoxStateSubscriber {} + +struct StateVtable { + handle_state: HandleStateFn, + drop: DropFn, +} + +impl StateVtable { + const fn new() -> &'static Self { + &Self { + handle_state: |ptr, cx| { + let provider = unsafe { &*ptr.load(Ordering::Relaxed).cast::() }; + provider.handle_state(cx).boxed() + }, + drop: |ptr| { + drop(unsafe { Box::::from_raw(ptr.get_mut().cast::()) }); + }, + } + } +} + +type HandleStateFn = + for<'a> unsafe fn(&AtomicPtr<()>, &'a StateSubscriberContext) -> HandleStateFut<'a>; + +type HandleStateFut<'a> = BoxFuture<'a, Result<()>>; + +// === Common Stuff === + +type DropFn = unsafe fn(&mut AtomicPtr<()>); + +#[cfg(test)] +mod tests { + use std::sync::Arc; + use std::sync::atomic::AtomicUsize; + + use tycho_block_util::archive::ArchiveData; + use tycho_block_util::block::BlockStuff; + use tycho_block_util::state::{MinRefMcStateTracker, ShardStateStuff}; + use tycho_types::cell::{Cell, CellBuilder, CellFamily, Lazy}; + use tycho_types::models::{ShardIdent, ShardStateUnsplit}; + + use super::*; + use crate::block_strider::DelayedTasks; + + #[tokio::test] + async fn boxed_block_subscriber_works() -> Result<()> { + struct SubscriberState { + prepare_block_called: AtomicUsize, + handle_block_called: AtomicUsize, + dropped: AtomicUsize, + } + + #[derive(Debug, PartialEq, Eq)] + struct Prepared(u32); + + struct Subscriber { + state: Arc, + } + + impl Drop for Subscriber { + fn drop(&mut self) { + self.state.dropped.fetch_add(1, Ordering::Relaxed); + } + } + + impl BlockSubscriber for Subscriber { + type Prepared = Prepared; + type PrepareBlockFut<'a> = futures_util::future::Ready>; + type HandleBlockFut<'a> = futures_util::future::Ready>; + + fn prepare_block<'a>( + &'a self, + _cx: &'a BlockSubscriberContext, + ) -> Self::PrepareBlockFut<'a> { + self.state + .prepare_block_called + .fetch_add(1, Ordering::Relaxed); + futures_util::future::ready(Ok(Prepared(123))) + } + + fn handle_block<'a>( + &'a self, + _cx: &'a BlockSubscriberContext, + _prepared: Self::Prepared, + ) -> Self::HandleBlockFut<'a> { + self.state + .handle_block_called + .fetch_add(1, Ordering::Relaxed); + futures_util::future::ready(Ok(())) + } + } + + let state = Arc::new(SubscriberState { + prepare_block_called: AtomicUsize::new(0), + handle_block_called: AtomicUsize::new(0), + dropped: AtomicUsize::new(0), + }); + let boxed = BoxBlockSubscriber::new(Subscriber { + state: state.clone(), + }); + + let cx = BlockSubscriberContext { + mc_block_id: Default::default(), + mc_is_key_block: false, + is_key_block: false, + block: BlockStuff::new_empty(ShardIdent::MASTERCHAIN, 0), + archive_data: ArchiveData::Existing, + delayed: DelayedTasks::new().1, + }; + + assert_eq!(state.prepare_block_called.load(Ordering::Relaxed), 0); + assert_eq!(state.handle_block_called.load(Ordering::Relaxed), 0); + assert_eq!(state.dropped.load(Ordering::Relaxed), 0); + + for i in 0..2 { + let res = boxed.prepare_block(&cx).await.unwrap(); + assert_eq!(res.downcast_ref::(), Some(&Prepared(123))); + assert_eq!(state.prepare_block_called.load(Ordering::Relaxed), i + 1); + assert_eq!(state.handle_block_called.load(Ordering::Relaxed), i); + assert_eq!(state.dropped.load(Ordering::Relaxed), 0); + + boxed.handle_block(&cx, res).await.unwrap(); + assert_eq!(state.prepare_block_called.load(Ordering::Relaxed), i + 1); + assert_eq!(state.handle_block_called.load(Ordering::Relaxed), i + 1); + assert_eq!(state.dropped.load(Ordering::Relaxed), 0); + } + + assert_eq!(Arc::strong_count(&state), 2); + drop(boxed); + + assert_eq!(state.prepare_block_called.load(Ordering::Acquire), 2); + assert_eq!(state.handle_block_called.load(Ordering::Acquire), 2); + assert_eq!(state.dropped.load(Ordering::Acquire), 1); + + assert_eq!(Arc::strong_count(&state), 1); + + Ok(()) + } + + #[tokio::test] + async fn boxed_state_subscriber_works() -> Result<()> { + struct SubscriberState { + handle_state_called: AtomicUsize, + dropped: AtomicUsize, + } + + struct Subscriber { + state: Arc, + } + + impl Drop for Subscriber { + fn drop(&mut self) { + self.state.dropped.fetch_add(1, Ordering::Relaxed); + } + } + + impl StateSubscriber for Subscriber { + type HandleStateFut<'a> = futures_util::future::Ready>; + + fn handle_state<'a>( + &'a self, + _cx: &'a StateSubscriberContext, + ) -> Self::HandleStateFut<'a> { + self.state + .handle_state_called + .fetch_add(1, Ordering::Relaxed); + futures_util::future::ready(Ok(())) + } + } + + let state = Arc::new(SubscriberState { + handle_state_called: AtomicUsize::new(0), + dropped: AtomicUsize::new(0), + }); + let boxed = BoxStateSubscriber::new(Subscriber { + state: state.clone(), + }); + + let cx = StateSubscriberContext { + mc_block_id: Default::default(), + mc_is_key_block: false, + is_key_block: false, + block: BlockStuff::new_empty(ShardIdent::MASTERCHAIN, 0), + archive_data: ArchiveData::Existing, + state: ShardStateStuff::from_root( + &Default::default(), + CellBuilder::build_from(ShardStateUnsplit { + global_id: 0, + shard_ident: ShardIdent::MASTERCHAIN, + seqno: 0, + vert_seqno: 0, + gen_utime: 0, + gen_utime_ms: 0, + gen_lt: 0, + min_ref_mc_seqno: 0, + processed_upto: Lazy::from_raw(Cell::empty_cell())?, + before_split: false, + accounts: Lazy::from_raw(Cell::empty_cell())?, + overload_history: 0, + underload_history: 0, + total_balance: Default::default(), + total_validator_fees: Default::default(), + libraries: Default::default(), + master_ref: None, + custom: None, + })?, + MinRefMcStateTracker::new().insert_untracked(), + )?, + delayed: DelayedTasks::new().1, + }; + + assert_eq!(state.handle_state_called.load(Ordering::Relaxed), 0); + assert_eq!(state.dropped.load(Ordering::Relaxed), 0); + + for i in 0..2 { + boxed.handle_state(&cx).await.unwrap(); + assert_eq!(state.handle_state_called.load(Ordering::Relaxed), i + 1); + assert_eq!(state.dropped.load(Ordering::Relaxed), 0); + } + + assert_eq!(Arc::strong_count(&state), 2); + drop(boxed); + + assert_eq!(state.handle_state_called.load(Ordering::Acquire), 2); + assert_eq!(state.dropped.load(Ordering::Acquire), 1); + + assert_eq!(Arc::strong_count(&state), 1); + + Ok(()) + } +} diff --git a/core/src/block_strider/subscriber/mod.rs b/core/src/block_strider/subscriber/mod.rs index 9c94d0afbd..df5330976c 100644 --- a/core/src/block_strider/subscriber/mod.rs +++ b/core/src/block_strider/subscriber/mod.rs @@ -8,12 +8,14 @@ use tycho_block_util::block::BlockStuff; use tycho_block_util::state::ShardStateStuff; use tycho_types::models::*; +pub use self::box_subscriber::{BoxBlockSubscriber, BoxStateSubscriber}; pub use self::futures::{ DelayedTasks, DelayedTasksJoinHandle, DelayedTasksSpawner, OptionHandleFut, OptionPrepareFut, }; pub use self::metrics_subscriber::MetricsSubscriber; use crate::storage::CoreStorage; +mod box_subscriber; mod futures; mod metrics_subscriber; @@ -120,10 +122,19 @@ impl BlockSubscriber for Arc { } pub trait BlockSubscriberExt: Sized { + fn boxed(self) -> BoxBlockSubscriber; + fn chain(self, other: T) -> ChainSubscriber; } impl BlockSubscriberExt for B { + fn boxed(self) -> BoxBlockSubscriber { + castaway::match_type!(self, { + BoxBlockSubscriber as subscriber => subscriber, + subscriber => BoxBlockSubscriber::new(subscriber), + }) + } + fn chain(self, other: T) -> ChainSubscriber { ChainSubscriber { left: self, @@ -184,10 +195,19 @@ impl StateSubscriber for Arc { } pub trait StateSubscriberExt: Sized { + fn boxed(self) -> BoxStateSubscriber; + fn chain(self, other: T) -> ChainSubscriber; } impl StateSubscriberExt for B { + fn boxed(self) -> BoxStateSubscriber { + castaway::match_type!(self, { + BoxStateSubscriber as subscriber => subscriber, + subscriber => BoxStateSubscriber::new(subscriber), + }) + } + fn chain(self, other: T) -> ChainSubscriber { ChainSubscriber { left: self, diff --git a/slasher/src/bc/mod.rs b/slasher/src/bc/mod.rs index bbf8be40ac..af5fab2f9b 100644 --- a/slasher/src/bc/mod.rs +++ b/slasher/src/bc/mod.rs @@ -120,7 +120,7 @@ pub struct BlocksBatch { } impl BlocksBatch { - fn new(start_seqno: u32, len: NonZeroU32, map_ids: &[u16]) -> Self { + pub fn new(start_seqno: u32, len: NonZeroU32, map_ids: &[u16]) -> Self { let len = len.get() as usize; Self { diff --git a/slasher/src/collector/validator_events.rs b/slasher/src/collector/validator_events.rs index e39e9edf50..3664249592 100644 --- a/slasher/src/collector/validator_events.rs +++ b/slasher/src/collector/validator_events.rs @@ -12,6 +12,10 @@ use tycho_util::{DashMapEntry, FastDashMap}; use crate::bc::BlocksBatch; +pub trait BlockBatchesStore { + fn known_batch_size(&self) -> AtomicU32; +} + #[derive(Default)] pub struct ValidatorEventsCollector { default_batch_size: AtomicU32, @@ -165,9 +169,7 @@ impl SessionState { let event_type = match signatures { Some(signatures) => { - self.current_batch - .commit_signatures(seqno, signatures) - .expect("ranges must be consistent"); + self.current_batch.commit_signatures(seqno, signatures); "validated" } None => "skipped", From 6ceb15577ef92444acf880d42266ce3921afc135 Mon Sep 17 00:00:00 2001 From: Ivan Kalinin Date: Wed, 24 Dec 2025 12:33:01 +0100 Subject: [PATCH 04/21] feat(slasher): send block batches to the slasher contract --- Cargo.lock | 4 + .../src/validator/impls/std_impl/session.rs | 1 + collator/src/validator/mod.rs | 4 +- slasher-traits/Cargo.toml | 2 + slasher-traits/src/validator.rs | 35 ++- slasher/Cargo.toml | 4 +- slasher/src/bc/mod.rs | 5 +- slasher/src/bc/stub_contract.rs | 6 +- slasher/src/collector/validator_events.rs | 103 +++++++-- slasher/src/lib.rs | 200 ++++++++++++++++-- 10 files changed, 317 insertions(+), 47 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d9aa81701e..179c02263f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4440,7 +4440,9 @@ dependencies = [ "metrics", "parking_lot", "scopeguard", + "serde", "tokio", + "tokio-util", "tracing", "tycho-block-util", "tycho-core", @@ -4454,7 +4456,9 @@ dependencies = [ name = "tycho-slasher-traits" version = "0.3.9" dependencies = [ + "indexmap", "tycho-types", + "tycho-util", ] [[package]] diff --git a/collator/src/validator/impls/std_impl/session.rs b/collator/src/validator/impls/std_impl/session.rs index 9cb2b9b237..6826d46656 100644 --- a/collator/src/validator/impls/std_impl/session.rs +++ b/collator/src/validator/impls/std_impl/session.rs @@ -88,6 +88,7 @@ impl ValidatorSession { let events_scope = events.begin_session( info.session_id.into(), info.start_block_seqno, + own_validator_idx, info.validators, ); diff --git a/collator/src/validator/mod.rs b/collator/src/validator/mod.rs index aeefdc2d1d..65c40f910e 100644 --- a/collator/src/validator/mod.rs +++ b/collator/src/validator/mod.rs @@ -4,9 +4,7 @@ use anyhow::Result; use async_trait::async_trait; use tycho_crypto::ed25519::PublicKey; use tycho_network::{Network, OverlayService, PeerId, PeerResolver}; -use tycho_types::models::{ - BlockId, BlockIdShort, IndexedValidatorDescription, ShardIdent, ValidatorDescription, -}; +use tycho_types::models::{BlockId, BlockIdShort, IndexedValidatorDescription, ShardIdent}; use tycho_util::FastHashMap; pub use self::impls::*; diff --git a/slasher-traits/Cargo.toml b/slasher-traits/Cargo.toml index 879b0c6cfc..3d765d57ee 100644 --- a/slasher-traits/Cargo.toml +++ b/slasher-traits/Cargo.toml @@ -11,9 +11,11 @@ license.workspace = true [dependencies] # crates.io deps +indexmap = { workspace = true } # local deps tycho-types = { workspace = true } +tycho-util = { workspace = true } [lints] workspace = true diff --git a/slasher-traits/src/validator.rs b/slasher-traits/src/validator.rs index dbeefbbfec..40cd076857 100644 --- a/slasher-traits/src/validator.rs +++ b/slasher-traits/src/validator.rs @@ -2,7 +2,9 @@ use std::mem::MaybeUninit; use std::sync::Arc; use std::sync::atomic::{AtomicBool, AtomicU8, Ordering}; +use indexmap::IndexMap; use tycho_types::models::{BlockId, IndexedValidatorDescription}; +use tycho_util::FastHasherState; // TODO: Decide how to be with this collator-defined type #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -53,14 +55,24 @@ impl ValidatorEvents { &self, session_id: ValidationSessionId, first_mc_seqno: u32, + own_validator_idx: u16, validators: &[IndexedValidatorDescription], ) -> ValidatorSessionScope { self.listener - .on_session_started(session_id, first_mc_seqno, validators); + .on_session_started(session_id, first_mc_seqno, own_validator_idx, validators); + + let mut remap = IndexMap::::with_capacity_and_hasher( + validators.len(), + Default::default(), + ); + for (i, validator) in validators.iter().enumerate() { + remap.insert(validator.validator_idx, i as u16); + } + ValidatorSessionScope { recorder: self.listener.clone(), session_id, - validator_count: validators.len(), + remap_ids: Arc::new(remap), is_sealed: AtomicBool::new(false), } } @@ -69,7 +81,7 @@ impl ValidatorEvents { pub struct ValidatorSessionScope { recorder: Arc, session_id: ValidationSessionId, - validator_count: usize, + remap_ids: Arc>, is_sealed: AtomicBool, } @@ -78,8 +90,9 @@ impl ValidatorSessionScope { BlockValidationScope { recorder: self.recorder.clone(), session_id: self.session_id, + remap_ids: self.remap_ids.clone(), block_id: *block_id, - signature_slots: vec![0; self.validator_count] + signature_slots: vec![0; self.remap_ids.len()] .into_iter() .map(AtomicU8::new) .collect::>(), @@ -107,6 +120,7 @@ impl Drop for ValidatorSessionScope { pub struct BlockValidationScope { recorder: Arc, session_id: ValidationSessionId, + remap_ids: Arc>, block_id: BlockId, signature_slots: Box<[AtomicU8]>, is_sealed: AtomicBool, @@ -128,7 +142,11 @@ impl BlockValidationScope { ReceivedSignature::INVALID_SIGNATURE_BIT }; - if let Some(status) = self.signature_slots.get(validator_idx as usize) { + let Some(slot_id) = self.remap_ids.get(&validator_idx) else { + return false; + }; + + if let Some(status) = self.signature_slots.get(*slot_id as usize) { status.fetch_or(mask, Ordering::Release) & mask == 0 } else { false @@ -138,7 +156,7 @@ impl BlockValidationScope { pub fn commit(&self) -> bool { if self.seal() { // TODO: Use some unsafe magic to make this closer to a NOOP. - let mut signatures = Arc::new_uninit_slice(self.signature_slots.len()); + let mut signatures = Arc::new_uninit_slice(self.signature_slots.len() as usize); for (res, slot) in std::iter::zip( Arc::get_mut(&mut signatures).unwrap(), &self.signature_slots, @@ -205,6 +223,7 @@ pub trait ValidatorEventsListener: Send + Sync + 'static { &self, session_id: ValidationSessionId, first_mc_seqno: u32, + own_validator_idx: u16, validators: &[IndexedValidatorDescription], ); @@ -231,6 +250,7 @@ impl ValidatorEventsListener for NoopValidatorEventsRecorder { &self, _session_id: ValidationSessionId, _first_mc_seqno: u32, + _own_validator_idx: u16, _validators: &[IndexedValidatorDescription], ) { } @@ -258,9 +278,10 @@ macro_rules! impl_recorder_for_tuples { &self, session_id: ValidationSessionId, first_mc_seqno: u32, + own_validator_idx: u16, validators: &[IndexedValidatorDescription], ) { - $(self.$n.on_session_started(session_id, first_mc_seqno, validators);)+ + $(self.$n.on_session_started(session_id, first_mc_seqno, own_validator_idx, validators);)+ } fn on_session_finished(&self, session_id: ValidationSessionId) { diff --git a/slasher/Cargo.toml b/slasher/Cargo.toml index 0fe90cb060..228d6ae224 100644 --- a/slasher/Cargo.toml +++ b/slasher/Cargo.toml @@ -14,11 +14,13 @@ license.workspace = true anyhow = { workspace = true } arc-swap = { workspace = true } dashmap = { workspace = true } -futures-util = { workspace = true} +futures-util = { workspace = true } metrics = { workspace = true } parking_lot = { workspace = true } scopeguard = { workspace = true } +serde = { workspace = true } tokio = { workspace = true, features = ["sync"] } +tokio-util = { workspace = true } tracing = { workspace = true } tycho-crypto = { workspace = true } tycho-types = { workspace = true } diff --git a/slasher/src/bc/mod.rs b/slasher/src/bc/mod.rs index af5fab2f9b..ce123b8c58 100644 --- a/slasher/src/bc/mod.rs +++ b/slasher/src/bc/mod.rs @@ -17,7 +17,6 @@ mod stub_contract; #[derive(Clone, Copy)] pub struct EncodeBlocksBatchMessage<'a> { - pub state: &'a AccountState, pub session_id: ValidationSessionId, pub batch: &'a BlocksBatch, pub validator_idx: u16, @@ -28,6 +27,8 @@ pub struct EncodeBlocksBatchMessage<'a> { pub trait SlasherContract: Send + Sync + 'static { fn find_account_address(&self, config: &BlockchainConfigParams) -> Result>; + fn default_batch_size(&self) -> NonZeroU32; + fn get_batch_size(&self, state: &AccountState) -> Result; fn encode_blocks_batch_message( @@ -108,7 +109,7 @@ struct PendingMessage { } #[derive(Debug, Clone, Copy)] -enum MessageDeliveryStatus { +pub enum MessageDeliveryStatus { Sent { tx_hash: HashBytes }, Expired, } diff --git a/slasher/src/bc/stub_contract.rs b/slasher/src/bc/stub_contract.rs index 678805941a..80784256bc 100644 --- a/slasher/src/bc/stub_contract.rs +++ b/slasher/src/bc/stub_contract.rs @@ -17,8 +17,12 @@ impl SlasherContract for StubContract { Ok(None) } + fn default_batch_size(&self) -> NonZeroU32 { + NonZeroU32::new(100).unwrap() + } + fn get_batch_size(&self, _state: &AccountState) -> Result { - Ok(NonZeroU32::new(100).unwrap()) + Ok(self.default_batch_size()) } fn encode_blocks_batch_message( diff --git a/slasher/src/collector/validator_events.rs b/slasher/src/collector/validator_events.rs index 3664249592..5c61b742d2 100644 --- a/slasher/src/collector/validator_events.rs +++ b/slasher/src/collector/validator_events.rs @@ -1,25 +1,37 @@ +use std::collections::VecDeque; use std::num::NonZeroU32; -use std::sync::Arc; use std::sync::atomic::{AtomicU32, Ordering}; +use std::sync::{Arc, Mutex}; -use anyhow::{Context, Result}; +use anyhow::Result; use tokio::sync::mpsc; use tracing::instrument; +use tycho_crypto::ed25519; use tycho_slasher_traits::{ReceivedSignature, ValidationSessionId, ValidatorEventsListener}; use tycho_types::models::{BlockId, IndexedValidatorDescription}; -use tycho_types::prelude::*; use tycho_util::{DashMapEntry, FastDashMap}; use crate::bc::BlocksBatch; +const INIT_QUEUE_CAPACITY: usize = 3; + pub trait BlockBatchesStore { fn known_batch_size(&self) -> AtomicU32; } -#[derive(Default)] pub struct ValidatorEventsCollector { default_batch_size: AtomicU32, sessions: FastDashMap, + init_queue: Mutex>, + init_queue_capacity: usize, +} + +#[derive(Debug, Clone)] +pub struct ValidatorSessionInfo { + pub session_id: ValidationSessionId, + pub first_mc_seqno: u32, + pub own_validator_idx: u16, + pub validators: Arc<[IndexedValidatorDescription]>, } struct SessionState { @@ -29,19 +41,47 @@ struct SessionState { current_batch: BlocksBatch, first_seqno: u32, next_expected_seqno: u32, - complete_batches: Option>, + complete_batches: Option>, } +pub type BlocksBatchTx = mpsc::UnboundedSender; +pub type BlocksBatchRx = mpsc::UnboundedReceiver; + // === Collector impl === impl ValidatorEventsCollector { pub fn new(default_batch_size: NonZeroU32) -> Self { + let init_queue_capacity = INIT_QUEUE_CAPACITY; + let init_queue = Mutex::new(VecDeque::with_capacity(init_queue_capacity)); + Self { default_batch_size: AtomicU32::new(default_batch_size.get()), sessions: Default::default(), + init_queue, + init_queue_capacity, } } + pub fn pop_session_to_init(&self, mc_seqno: u32) -> Option { + let mut queue = self.init_queue.lock().unwrap(); + if let Some(info) = queue.front() + && info.first_mc_seqno > mc_seqno + { + return None; + } + queue.pop_front() + } + + fn push_session_to_init(&self, info: ValidatorSessionInfo) { + let mut items = self.init_queue.lock().unwrap(); + if items.len() >= self.init_queue_capacity + && let Some(info) = items.pop_front() + { + tracing::warn!(session_id = ?info.session_id, "session info dropped from init queue"); + } + items.push_back(info); + } + pub fn set_default_batch_size(&self, batch_size: NonZeroU32) { self.default_batch_size .store(batch_size.get(), Ordering::Release); @@ -51,7 +91,7 @@ impl ValidatorEventsCollector { &self, session_id: ValidationSessionId, batch_size: NonZeroU32, - complete_batches: mpsc::UnboundedSender, + complete_batches: BlocksBatchTx, ) -> bool { let Some(mut session) = self.sessions.get_mut(&session_id) else { return false; @@ -84,6 +124,7 @@ impl ValidatorEventsListener for ValidatorEventsCollector { &self, session_id: ValidationSessionId, first_mc_seqno: u32, + own_validator_idx: u16, validators: &[IndexedValidatorDescription], ) { tracing::debug!(first_mc_seqno, "on_session_open"); @@ -96,6 +137,8 @@ impl ValidatorEventsListener for ValidatorEventsCollector { let batch_size = NonZeroU32::new(self.default_batch_size.load(Ordering::Acquire)).unwrap(); let current_batch = BlocksBatch::new(first_mc_seqno, batch_size, &validator_indices); + let validators = Arc::<[IndexedValidatorDescription]>::from(validators); + if let DashMapEntry::Vacant(v) = self.sessions.entry(session_id) { v.insert(SessionState { batch_size, @@ -106,6 +149,13 @@ impl ValidatorEventsListener for ValidatorEventsCollector { // Will be initialized later via `init_session`. complete_batches: None, }); + + self.push_session_to_init(ValidatorSessionInfo { + session_id, + first_mc_seqno, + own_validator_idx, + validators, + }); } else { tracing::warn!("duplicate session"); } @@ -115,7 +165,7 @@ impl ValidatorEventsListener for ValidatorEventsCollector { fn on_session_finished(&self, session_id: ValidationSessionId) { tracing::debug!("on_session_drop"); if let Some((_, session)) = self.sessions.remove(&session_id) - && let Err(e) = session.commit_batch(&session.current_batch) + && let Err(e) = session.commit_final_batch() { tracing::warn!("failed to commit blocks batch on finish: {e:?}"); } @@ -157,6 +207,22 @@ impl ValidatorEventsListener for ValidatorEventsCollector { } } +// === Validator session info impl === + +impl ValidatorSessionInfo { + pub fn can_participate(&self, public_key: &ed25519::PublicKey) -> bool { + let Some(desc) = self + .validators + .iter() + .find(|item| item.validator_idx == self.own_validator_idx) + else { + return false; + }; + + public_key.as_bytes() == desc.public_key.as_array() + } +} + // === Session state impl === impl SessionState { @@ -176,7 +242,7 @@ impl SessionState { }; if let Some(batch) = to_commit - && let Err(e) = self.commit_batch(&batch) + && let Err(e) = self.commit_batch(batch) { tracing::error!(event_type, "failed to commit blocks batch: {e:?}"); } @@ -200,20 +266,27 @@ impl SessionState { AdvanceBlockStatus::Replaced(prev_batch) } - fn commit_batch(&self, batch: &BlocksBatch) -> Result<()> { + fn commit_batch(&self, batch: BlocksBatch) -> Result<()> { + Self::commit_batch_impl(&self.complete_batches, batch) + } + + fn commit_final_batch(self) -> Result<()> { + Self::commit_batch_impl(&self.complete_batches, self.current_batch) + } + + fn commit_batch_impl( + complete_batches: &Option, + batch: BlocksBatch, + ) -> Result<()> { if batch.is_empty() { return Ok(()); } - let cell = batch - .build_cell() - .context("failed to pack batch into a cell")?; - - let Some(tx) = &self.complete_batches else { + let Some(tx) = complete_batches else { anyhow::bail!("not initialized"); }; - if tx.send(cell).is_err() { + if tx.send(batch).is_err() { anyhow::bail!("channel closed"); } Ok(()) diff --git a/slasher/src/lib.rs b/slasher/src/lib.rs index abe5dbe007..ed7fa74857 100644 --- a/slasher/src/lib.rs +++ b/slasher/src/lib.rs @@ -1,17 +1,28 @@ +use std::num::NonZeroU32; use std::sync::Arc; +use std::time::Duration; use anyhow::{Context, Result}; use arc_swap::ArcSwapOption; use futures_util::future::BoxFuture; +use serde::{Deserialize, Serialize}; +use tokio::sync::mpsc; +use tokio_util::sync::CancellationToken; +use tracing::instrument; use tycho_core::block_strider::{StateSubscriber, StateSubscriberContext}; +use tycho_core::blockchain_rpc::BlockchainRpcClient; use tycho_crypto::ed25519; -use tycho_slasher_traits::ValidatorEventsListener; +use tycho_slasher_traits::{ValidationSessionId, ValidatorEventsListener}; +use tycho_types::boc::Boc; +use tycho_util::futures::JoinTask; +use tycho_util::serde_helpers; +use self::bc::MessageDeliveryStatus; pub use self::bc::{ BlocksBatch, ContractSubscription, EncodeBlocksBatchMessage, SignatureHistory, SignedMessage, SlasherContract, }; -use self::collector::ValidatorEventsCollector; +use self::collector::{ValidatorEventsCollector, ValidatorSessionInfo}; pub mod collector { pub use self::validator_events::*; @@ -23,42 +34,75 @@ pub mod collector { mod bc; mod util; -pub struct SlasherParams { - pub node_keys: Arc, - pub initial_mc_seqno: u32, +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SlasherConfig { + /// TTL of messages to the slasher contract. + /// + /// Default: `30s` + #[serde(with = "serde_helpers::humantime")] + pub message_ttl: Duration, + /// Interval between message delivery attempts. + /// + /// Default: `1s` + #[serde(with = "serde_helpers::humantime")] + pub message_retry_interval: Duration, + + /// Additional time to wait for the previous batch delivery. + /// + /// Default: `5s` + #[serde(with = "serde_helpers::humantime")] + pub prev_delivery_timeout: Option, +} + +impl Default for SlasherConfig { + fn default() -> Self { + Self { + message_ttl: Duration::from_secs(30), + message_retry_interval: Duration::from_secs(1), + prev_delivery_timeout: Some(Duration::from_secs(5)), + } + } } -// NOTE: Stub -#[derive(Clone)] -#[repr(transparent)] pub struct Slasher { - inner: Arc, + validator_events_collector: Arc, + shared: Arc, + cancellation_token: CancellationToken, } impl Slasher { - pub fn new(node_keys: Arc, contract: C) -> Self { - let collector = Arc::new(ValidatorEventsCollector::default()); + pub fn new( + node_keys: Arc, + contract: C, + blockchain_rpc_client: BlockchainRpcClient, + config: SlasherConfig, + ) -> Self { + let collector = Arc::new(ValidatorEventsCollector::new(contract.default_batch_size())); Self { - inner: Arc::new(Inner { + validator_events_collector: collector, + shared: Arc::new(SlasherSharedState { + config, node_keys, - validator_events_collector: collector, contract: Box::new(contract), subscription: ArcSwapOption::empty(), + blockchain_rpc_client, }), + cancellation_token: Default::default(), } } pub fn validator_events_listener(&self) -> Arc { - self.inner.validator_events_collector.clone() + self.validator_events_collector.clone() } async fn handle_state_impl(&self, cx: &StateSubscriberContext) -> Result<()> { if !cx.block.id().is_masterchain() { return Ok(()); } + let mc_seqno = cx.block.id().seqno; - let this = self.inner.as_ref(); + let this = self.shared.as_ref(); // Check config updates let config_params = cx.state.config_params()?; @@ -86,10 +130,45 @@ impl Slasher { subscription.handle_account_transactions(&account_block)?; } + // TODO: Get or update batch size from the contract + let batch_size = NonZeroU32::new(100).unwrap(); + + while let Some(session_info) = self + .validator_events_collector + .pop_session_to_init(mc_seqno) + { + let session_id = session_info.session_id; + if !session_info.can_participate(&this.node_keys.public_key) { + tracing::info!(?session_id, "skipping session"); + continue; + } + + let (tx, rx) = mpsc::unbounded_channel::(); + if !self + .validator_events_collector + .init_session(session_id, batch_size, tx) + { + tracing::warn!(?session_id, "session removed before init"); + continue; + } + + let token = self.cancellation_token.clone(); + let shared = self.shared.clone(); + tokio::task::spawn( + token.run_until_cancelled_owned(shared.send_batches_to_contract(session_info, rx)), + ); + } + Ok(()) } } +impl Drop for Slasher { + fn drop(&mut self) { + self.cancellation_token.cancel(); + } +} + impl StateSubscriber for Slasher { type HandleStateFut<'a> = BoxFuture<'a, Result<()>>; @@ -99,10 +178,95 @@ impl StateSubscriber for Slasher { } } -struct Inner { - #[allow(unused)] +struct SlasherSharedState { + config: SlasherConfig, node_keys: Arc, - validator_events_collector: Arc, contract: Box, subscription: ArcSwapOption, + blockchain_rpc_client: BlockchainRpcClient, +} + +impl SlasherSharedState { + #[instrument(skip_all, fields(session_id = ?info.session_id))] + async fn send_batches_to_contract( + self: Arc, + info: ValidatorSessionInfo, + mut rx: collector::BlocksBatchRx, + ) { + tracing::info!("started"); + scopeguard::defer!(tracing::info!("finished")); + + let mut send_task = None; + + while let Some(batch) = rx.recv().await { + if let Some(send_task) = send_task.take() + && let Some(timeout) = self.config.prev_delivery_timeout + && tokio::time::timeout(timeout, send_task).await.is_err() + { + tracing::warn!("timeout on waiting for the previous batch to be delivered"); + } + + send_task = Some(JoinTask::new(self.clone().deliver_batch_message( + info.session_id, + info.own_validator_idx, + batch, + ))); + } + } + + async fn deliver_batch_message( + self: Arc, + session_id: ValidationSessionId, + validator_idx: u16, + batch: BlocksBatch, + ) { + let params = EncodeBlocksBatchMessage { + session_id, + batch: &batch, + validator_idx, + keypair: &self.node_keys, + ttl: self.config.message_ttl, + }; + + loop { + let Some(subscription) = self.subscription.load_full() else { + tracing::warn!("no slasher contract subscription"); + break; + }; + + let signed = match self.contract.encode_blocks_batch_message(¶ms) { + Ok(signed) => signed, + Err(e) => { + tracing::error!("failed to encode batch message: {e:?}"); + return; + } + }; + let message_hash = *signed.message.repr_hash(); + let boc = Boc::encode(signed.message.into_inner()); + + match subscription.track_message(&message_hash, signed.expire_at) { + Ok(res) => { + self.blockchain_rpc_client + .broadcast_external_message(&boc) + .await; + drop(boc); + + match res.await { + Ok(MessageDeliveryStatus::Sent { tx_hash }) => { + tracing::info!(%tx_hash, "batch message delivered"); + return; + } + Ok(MessageDeliveryStatus::Expired) => { + // TODO: Execute transaction locally to guess the reason. + tracing::warn!("batch message expired"); + } + Err(_) => return, + } + } + Err(e) => tracing::warn!("failed to track message: {e:?}"), + } + + tokio::time::sleep(self.config.message_retry_interval).await; + } + } } From f97a88ae80b7d3f000ff13d2fd7394b35273149d Mon Sep 17 00:00:00 2001 From: Ivan Kalinin Date: Thu, 25 Dec 2025 18:35:21 +0100 Subject: [PATCH 05/21] feat(slasher): add stub contract --- cli/src/node/config.rs | 5 ++ cli/src/node/mod.rs | 17 +++++- collator/src/manager/mod.rs | 2 +- collator/src/manager/utils.rs | 3 +- .../src/validator/impls/std_impl/session.rs | 2 +- collator/tests/validator_tests.rs | 30 +++++----- contracts/scripts/genSlasherStub.ts | 57 +++++++++++++++++++ contracts/scripts/printElectorData.ts | 6 +- contracts/src/slasher-stub.tolk | 51 +++++++++++++++++ contracts/wrappers/SlasherStub.compile.ts | 9 +++ contracts/wrappers/SlasherStub.ts | 30 ++++++++++ scripts/build-contracts.sh | 1 + slasher-traits/src/validator.rs | 2 +- slasher/Cargo.toml | 2 +- slasher/src/bc/mod.rs | 17 +++++- slasher/src/bc/stub_contract.rs | 44 +++++++++++--- slasher/src/collector/validator_events.rs | 6 +- slasher/src/lib.rs | 48 ++++++++++------ 18 files changed, 276 insertions(+), 56 deletions(-) create mode 100644 contracts/scripts/genSlasherStub.ts create mode 100644 contracts/src/slasher-stub.tolk create mode 100644 contracts/wrappers/SlasherStub.compile.ts create mode 100644 contracts/wrappers/SlasherStub.ts diff --git a/cli/src/node/config.rs b/cli/src/node/config.rs index 31da0f5566..aebde661c4 100644 --- a/cli/src/node/config.rs +++ b/cli/src/node/config.rs @@ -10,6 +10,7 @@ use tycho_control::ControlServerConfig; use tycho_core::node::NodeBaseConfig; use tycho_crypto::ed25519; use tycho_rpc::RpcConfig; +use tycho_slasher::SlasherConfig; use tycho_types::cell::HashBytes; use tycho_types::models::StdAddr; use tycho_util::cli::config::ThreadPoolConfig; @@ -165,6 +166,9 @@ pub struct NodeConfig { pub validator: ValidatorStdImplConfig, + #[partial] + pub slasher: SlasherConfig, + #[partial] pub rpc: Option, @@ -191,6 +195,7 @@ impl Default for NodeConfig { mempool: Default::default(), internal_queue: Default::default(), validator: Default::default(), + slasher: Default::default(), rpc: Some(Default::default()), control: Default::default(), metrics: Some(Default::default()), diff --git a/cli/src/node/mod.rs b/cli/src/node/mod.rs index 761db5783d..f69032bc0b 100644 --- a/cli/src/node/mod.rs +++ b/cli/src/node/mod.rs @@ -32,6 +32,7 @@ use tycho_core::node::{NodeBase, NodeKeys}; use tycho_core::storage::NodeSyncState; use tycho_network::InboundRequestMeta; use tycho_rpc::{NodeBaseInitRpc, RpcConfig}; +use tycho_slasher::SlasherConfig; use tycho_types::models::*; use tycho_util::futures::JoinTask; use tycho_wu_tuner::service::WuTunerServiceBuilder; @@ -57,6 +58,7 @@ pub struct Node { collator_config: CollatorConfig, validator_config: ValidatorStdImplConfig, internal_queue_config: QueueConfig, + slasher_config: SlasherConfig, mempool_config_override: Option, /// Path to the work units tuner config. @@ -131,6 +133,7 @@ impl Node { collator_config: node_config.collator, validator_config: node_config.validator, internal_queue_config: node_config.internal_queue, + slasher_config: node_config.slasher, mempool_config_override: global_config.mempool, wu_tuner_config_path, }) @@ -223,7 +226,12 @@ impl Node { message_queue_adapter.clear_uncommitted_state(&top_shards)?; // NOTE: Stub - let slasher = tycho_slasher::Slasher::new(base.keypair.clone()); + let slasher = tycho_slasher::Slasher::new( + base.keypair.clone(), + tycho_slasher::StubSlasherContract, + base.blockchain_rpc_client.clone(), + self.slasher_config, + ); let validator = ValidatorStdImpl::new( ValidatorNetworkContext { @@ -353,7 +361,12 @@ impl Node { ( ShardStateApplier::new( base.core_storage.clone(), - (collator_subcriber, rpc_state_subscriber, control_server), + ( + collator_subcriber, + rpc_state_subscriber, + control_server, + slasher, + ), ), rpc_block_subscriber, base.validator_resolver().clone(), diff --git a/collator/src/manager/mod.rs b/collator/src/manager/mod.rs index 51ff2b6ec4..ad0c4e5607 100644 --- a/collator/src/manager/mod.rs +++ b/collator/src/manager/mod.rs @@ -16,7 +16,7 @@ use tycho_core::storage::{LoadStateHint, StateNotFound}; use tycho_crypto::ed25519::KeyPair; use tycho_types::models::{ BlockId, BlockIdShort, CollationConfig, GlobalCapabilities, IndexedValidatorDescription, - ProcessedUptoInfo, ShardIdent, ValidatorDescription, + ProcessedUptoInfo, ShardIdent, }; use tycho_util::futures::{AwaitBlocking, JoinTask}; use tycho_util::metrics::HistogramGuard; diff --git a/collator/src/manager/utils.rs b/collator/src/manager/utils.rs index bd49de7c83..fa20847242 100644 --- a/collator/src/manager/utils.rs +++ b/collator/src/manager/utils.rs @@ -1,5 +1,4 @@ use tycho_crypto::ed25519::{KeyPair, PublicKey}; -use tycho_types::models::ValidatorDescription; use tycho_util::FastHashMap; #[cfg(not(any(feature = "test", test)))] @@ -18,7 +17,7 @@ pub fn find_us_in_collators_set( #[cfg(any(test, feature = "test"))] pub fn find_us_in_collators_set( keypair: &KeyPair, - _set: &FastHashMap<[u8; 32], ValidatorDescription>, + _set: &FastHashMap<[u8; 32], T>, ) -> Option { Some(keypair.public_key) } diff --git a/collator/src/validator/impls/std_impl/session.rs b/collator/src/validator/impls/std_impl/session.rs index 6826d46656..79339bdafe 100644 --- a/collator/src/validator/impls/std_impl/session.rs +++ b/collator/src/validator/impls/std_impl/session.rs @@ -14,7 +14,7 @@ use scc::TreeIndex; use tokio::sync::{Notify, Semaphore}; use tokio_util::sync::CancellationToken; use tracing::Instrument; -use tycho_crypto::ed25519::KeyPair +use tycho_crypto::ed25519::KeyPair; use tycho_network::{OverlayId, PeerId, PrivateOverlay}; use tycho_slasher_traits::{BlockValidationScope, ValidatorEvents, ValidatorSessionScope}; use tycho_types::models::*; diff --git a/collator/tests/validator_tests.rs b/collator/tests/validator_tests.rs index 03621b89a2..d352685bee 100644 --- a/collator/tests/validator_tests.rs +++ b/collator/tests/validator_tests.rs @@ -9,9 +9,9 @@ use tycho_collator::validator::{ }; use tycho_crypto::ed25519; use tycho_network::{DhtClient, PeerInfo}; -use tycho_slasher_traits::NoopValidatorEventsListener; +use tycho_slasher_traits::NoopValidatorEventsRecorder; use tycho_types::cell::HashBytes; -use tycho_types::models::{BlockId, ShardIdent, ValidatorDescription}; +use tycho_types::models::{BlockId, IndexedValidatorDescription, ShardIdent, ValidatorDescription}; use tycho_util::futures::JoinTask; mod common; @@ -24,7 +24,7 @@ struct ValidatorNode { } impl ValidatorNode { - fn generate(zerostate_id: &BlockId, rng: &mut impl rand::Rng) -> Self { + fn generate(zerostate_id: &BlockId, rng: &mut impl rand::Rng, idx: u16) -> Self { let secret_key = rng.random::(); let keypair = Arc::new(ed25519::KeyPair::from(&secret_key)); @@ -33,6 +33,7 @@ impl ValidatorNode { peer_id: *validator_network.network.peer_id(), public_key: keypair.public_key, weight: 1, + validator_idx: idx, }; let network = &validator_network.network; @@ -46,7 +47,7 @@ impl ValidatorNode { validator_network, keypair.clone(), ValidatorStdImplConfig::default(), - Arc::new(NoopValidatorEventsListener), + Arc::new(NoopValidatorEventsRecorder), ); Self { @@ -64,7 +65,7 @@ fn generate_network( rng: &mut impl rand::Rng, ) -> Vec { let nodes = (0..node_count) - .map(|_| ValidatorNode::generate(zerostate_id, rng)) + .map(|i| ValidatorNode::generate(zerostate_id, rng, i as u16)) .collect::>(); for i in 0..nodes.len() { @@ -83,16 +84,19 @@ fn generate_network( nodes } -fn make_description(seqno: u32, nodes: &[ValidatorNode]) -> Vec { +fn make_description(seqno: u32, nodes: &[ValidatorNode]) -> Vec { let mut validators = Vec::with_capacity(nodes.len()); let mut prev_total_weight = 0; - for node in nodes { - validators.push(ValidatorDescription { - public_key: HashBytes(*node.descr.public_key.as_bytes()), - weight: 1, - adnl_addr: Some(HashBytes(*node.descr.peer_id.as_bytes())), - mc_seqno_since: seqno, - prev_total_weight, + for (i, node) in nodes.iter().enumerate() { + validators.push(IndexedValidatorDescription { + desc: ValidatorDescription { + public_key: HashBytes(*node.descr.public_key.as_bytes()), + weight: 1, + adnl_addr: Some(HashBytes(*node.descr.peer_id.as_bytes())), + mc_seqno_since: seqno, + prev_total_weight, + }, + validator_idx: i as u16, }); prev_total_weight += node.descr.weight; } diff --git a/contracts/scripts/genSlasherStub.ts b/contracts/scripts/genSlasherStub.ts new file mode 100644 index 0000000000..c54001f5e3 --- /dev/null +++ b/contracts/scripts/genSlasherStub.ts @@ -0,0 +1,57 @@ +import arg from "arg"; +import { address, beginCell, storeAccount, toNano } from "@ton/core"; +import { storeSlasherStubData } from "../wrappers/SlasherStub"; +import { compile } from "@ton/blueprint"; + +async function main() { + const args = arg({ + "--balance": String, + }); + const balance = args["--balance"]; + if (balance == null) { + throw new Error("`--balance` option is missing"); + } + + const code = await compile("SlasherStub"); + + const account = beginCell() + .storeBit(true) + .store( + storeAccount({ + addr: address( + "-1:0000000000000000000000000000000000000000000000000000000000000000" + ), + storage: { + balance: { + coins: toNano(balance), + }, + lastTransLt: 0n, + state: { + type: "active", + state: { + code, + data: beginCell() + .store( + storeSlasherStubData({ + updatedAtMs: 0n, + }) + ) + .endCell(), + }, + }, + }, + storageStats: { + used: { + bits: 0n, + cells: 0n, + }, + lastPaid: 0, + storageExtra: null, + }, + }) + ) + .endCell(); + console.log(account.toBoc().toString("base64")); +} + +main().catch(console.error); diff --git a/contracts/scripts/printElectorData.ts b/contracts/scripts/printElectorData.ts index 0931025282..eb5d49456a 100644 --- a/contracts/scripts/printElectorData.ts +++ b/contracts/scripts/printElectorData.ts @@ -6,10 +6,10 @@ import { } from "@tychosdk/emulator"; import { Blockchain } from "@ton/sandbox"; import { Address, Cell, Dictionary } from "@ton/core"; -import { Elector, loadElectorData } from "../wrappers/Elector"; +import { loadElectorData } from "../wrappers/Elector"; const ELECTOR_ADDRESS = Address.parse( - "-1:3333333333333333333333333333333333333333333333333333333333333333" + "-1:3333333333333333333333333333333333333333333333333333333333333333", ); async function main() { @@ -44,7 +44,7 @@ async function main() { cs.remainingBits != 0 ? cs.loadDict( Dictionary.Keys.BigUint(256), - Dictionary.Values.BitString(0) + Dictionary.Values.BitString(0), ) : null; diff --git a/contracts/src/slasher-stub.tolk b/contracts/src/slasher-stub.tolk new file mode 100644 index 0000000000..e071bd906d --- /dev/null +++ b/contracts/src/slasher-stub.tolk @@ -0,0 +1,51 @@ +import "@stdlib/gas-payments" +import "lib/config-params" + +const ERROR_INVALID_SIGNATURE = 40 +const ERROR_VALIDATOR_NOT_FOUND = 50 +const ERROR_REPLAY_PROTECTION = 52 +const ERROR_MESSAGE_EXPIRED = 57 + +const REPLAY_OFFSET_MS = 5000 +const FUTURE_OFFSET_SEC = 60 + +struct Storage { + updatedAtMs: uint64 +} + +fun Storage.load(): Storage { + return Storage.fromCell(contract.getData()); +} + +fun Storage.save(self) { + contract.setData(self.toCell()); +} + +fun onInternalMessage(_in: InMessage) {} + +fun onExternalMessage(inMsg: slice) { + val signature = inMsg.loadBits(512); + val signedBody = inMsg; + val createdAtMs = inMsg.loadUint(64); + val expireAtSec = inMsg.loadUint(32); + val validatorIdx = inMsg.loadUint(16); + val _batch = inMsg.loadRef(); + inMsg.assertEnd(); + assert(blockchain.now() <= expireAtSec, ERROR_MESSAGE_EXPIRED); + + var data = Storage.load(); + assert(createdAtMs > (data.updatedAtMs - REPLAY_OFFSET_MS) && + createdAtMs <= (blockchain.now() + FUTURE_OFFSET_SEC) * 1000, ERROR_REPLAY_PROTECTION); + + var validatorCs = CurrentVset.getValidatorDescription(validatorIdx); + assert(validatorCs != null, ERROR_VALIDATOR_NOT_FOUND); + val validator = ValidatorDescr.readFromSlice(mutate validatorCs); + + val toSign = beginCell().storeSlice(signedBody).endCell(); + assert(isSignatureValid(toSign.hash(), signature, validator.pubkey), ERROR_INVALID_SIGNATURE); + + data.updatedAtMs = max(createdAtMs, data.updatedAtMs); + data.save(); + + acceptExternalMessage(); +} diff --git a/contracts/wrappers/SlasherStub.compile.ts b/contracts/wrappers/SlasherStub.compile.ts new file mode 100644 index 0000000000..44aa7aeb51 --- /dev/null +++ b/contracts/wrappers/SlasherStub.compile.ts @@ -0,0 +1,9 @@ +import { CompilerConfig } from "@ton/blueprint"; + +export const compile: CompilerConfig = { + lang: "tolk", + entrypoint: "src/slasher-stub.tolk", + withStackComments: true, + withSrcLineComments: true, + experimentalOptions: "", +}; diff --git a/contracts/wrappers/SlasherStub.ts b/contracts/wrappers/SlasherStub.ts new file mode 100644 index 0000000000..ae36cce683 --- /dev/null +++ b/contracts/wrappers/SlasherStub.ts @@ -0,0 +1,30 @@ +import { Address, Builder, Cell, Contract, Slice } from "@ton/core"; + +export type SlasherStubData = { + updatedAtMs: bigint; +}; + +export function loadSlasherStubData(cs: Slice): SlasherStubData { + return { + updatedAtMs: cs.loadUintBig(64), + }; +} + +export function storeSlasherStubData( + s: SlasherStubData +): (builder: Builder) => void { + return (builder) => { + builder.storeUint(s.updatedAtMs, 64); + }; +} + +export class SlasherStub implements Contract { + constructor( + readonly address: Address, + readonly init?: { code: Cell; data: Cell } + ) {} + + static createFromAddress(address: Address) { + return new SlasherStub(address); + } +} diff --git a/scripts/build-contracts.sh b/scripts/build-contracts.sh index 05ecc4e58f..91d3ad33eb 100755 --- a/scripts/build-contracts.sh +++ b/scripts/build-contracts.sh @@ -19,3 +19,4 @@ yarn build --all copy_code Elector copy_code ElectorPoA copy_code Config +copy_code SlasherStub diff --git a/slasher-traits/src/validator.rs b/slasher-traits/src/validator.rs index 40cd076857..299464bb3d 100644 --- a/slasher-traits/src/validator.rs +++ b/slasher-traits/src/validator.rs @@ -156,7 +156,7 @@ impl BlockValidationScope { pub fn commit(&self) -> bool { if self.seal() { // TODO: Use some unsafe magic to make this closer to a NOOP. - let mut signatures = Arc::new_uninit_slice(self.signature_slots.len() as usize); + let mut signatures = Arc::new_uninit_slice(self.signature_slots.len()); for (res, slot) in std::iter::zip( Arc::get_mut(&mut signatures).unwrap(), &self.signature_slots, diff --git a/slasher/Cargo.toml b/slasher/Cargo.toml index 228d6ae224..0bc01f7ff8 100644 --- a/slasher/Cargo.toml +++ b/slasher/Cargo.toml @@ -23,7 +23,7 @@ tokio = { workspace = true, features = ["sync"] } tokio-util = { workspace = true } tracing = { workspace = true } tycho-crypto = { workspace = true } -tycho-types = { workspace = true } +tycho-types = { workspace = true, features = ["abi", "models"] } # local deps tycho-block-util = { workspace = true } diff --git a/slasher/src/bc/mod.rs b/slasher/src/bc/mod.rs index ce123b8c58..c1148ed6d7 100644 --- a/slasher/src/bc/mod.rs +++ b/slasher/src/bc/mod.rs @@ -11,12 +11,14 @@ use tycho_types::models::{ }; use tycho_util::FastDashMap; +pub use self::stub_contract::StubSlasherContract; use crate::util::AtomicBitSet; mod stub_contract; #[derive(Clone, Copy)] pub struct EncodeBlocksBatchMessage<'a> { + pub address: &'a StdAddr, pub session_id: ValidationSessionId, pub batch: &'a BlocksBatch, pub validator_idx: u16, @@ -85,8 +87,10 @@ impl ContractSubscription { let Some(in_msg) = tx.in_msg else { continue; }; + let msg_hash = in_msg.repr_hash(); + tracing::debug!(%tx_hash, %msg_hash, "found slasher transaction"); - if let Some((_, pending)) = self.pending_messages.remove(in_msg.repr_hash()) { + if let Some((_, pending)) = self.pending_messages.remove(msg_hash) { pending .tx .send(MessageDeliveryStatus::Sent { tx_hash: *tx_hash }) @@ -98,8 +102,15 @@ impl ContractSubscription { } pub fn cleanup_expired_messages(&self, now_sec: u32) { - self.pending_messages - .retain(|_, msg| msg.expire_at >= now_sec); + let mut dropped = 0usize; + self.pending_messages.retain(|_, msg| { + let retain = msg.expire_at >= now_sec; + dropped += !retain as usize; + retain + }); + if dropped > 0 { + tracing::warn!(dropped, "dropped pending messages"); + } } } diff --git a/slasher/src/bc/stub_contract.rs b/slasher/src/bc/stub_contract.rs index 80784256bc..b95b1984cd 100644 --- a/slasher/src/bc/stub_contract.rs +++ b/slasher/src/bc/stub_contract.rs @@ -1,6 +1,7 @@ use std::num::NonZeroU32; use anyhow::{Context, Result}; +use tycho_types::abi::extend_signature_with_id; use tycho_types::cell::Lazy; use tycho_types::dict; use tycho_types::models::{ @@ -10,15 +11,21 @@ use tycho_types::prelude::*; use super::{BlocksBatch, SignedMessage, SlasherContract}; -pub struct StubContract; +const PARAM_IDX: u32 = 666; -impl SlasherContract for StubContract { - fn find_account_address(&self, _config: &BlockchainConfigParams) -> Result> { - Ok(None) +pub struct StubSlasherContract; + +impl SlasherContract for StubSlasherContract { + fn find_account_address(&self, config: &BlockchainConfigParams) -> Result> { + let Some(raw) = config.get_raw_cell_ref(PARAM_IDX)? else { + return Ok(None); + }; + let address = raw.parse::()?; + Ok(Some(StdAddr::new(-1, address))) } fn default_batch_size(&self) -> NonZeroU32 { - NonZeroU32::new(100).unwrap() + NonZeroU32::new(10).unwrap() } fn get_batch_size(&self, _state: &AccountState) -> Result { @@ -33,16 +40,35 @@ impl SlasherContract for StubContract { .context("failed to serialize blocks batch")?; let now = tycho_util::time::now_millis(); - let expire_at = (now / 1000).saturating_add(params.ttl.as_secs()) as u32; + let body_to_sign = { + let mut b = CellBuilder::new(); + b.store_u64(now)?; + b.store_u32(expire_at)?; + b.store_u16(params.validator_idx)?; + b.store_reference(cell)?; + b.build()? + }; + + // TODO: Add support for signature id. + let signature = params.keypair.sign_raw(&extend_signature_with_id( + body_to_sign.repr_hash().as_array(), + None, + )); + let body = { + let mut b = CellBuilder::new(); + b.store_raw(&signature, 512)?; + b.store_slice(body_to_sign.as_slice()?)?; + b.build()? + }; + let message = Lazy::new(&OwnedMessage { info: MsgInfo::ExtIn(ExtInMsgInfo { - // Stub address. - dst: StdAddr::new(-1, HashBytes::ZERO).into(), + dst: params.address.clone().into(), ..Default::default() }), init: None, - body: cell.into(), + body: body.into(), layout: None, })?; diff --git a/slasher/src/collector/validator_events.rs b/slasher/src/collector/validator_events.rs index 5c61b742d2..ab9dbb7ad0 100644 --- a/slasher/src/collector/validator_events.rs +++ b/slasher/src/collector/validator_events.rs @@ -127,7 +127,7 @@ impl ValidatorEventsListener for ValidatorEventsCollector { own_validator_idx: u16, validators: &[IndexedValidatorDescription], ) { - tracing::debug!(first_mc_seqno, "on_session_open"); + tracing::debug!(first_mc_seqno, "on_session_started"); let validator_indices = validators .iter() @@ -163,7 +163,7 @@ impl ValidatorEventsListener for ValidatorEventsCollector { #[instrument(skip_all, fields(session_id = ?session_id))] fn on_session_finished(&self, session_id: ValidationSessionId) { - tracing::debug!("on_session_drop"); + tracing::debug!("on_session_finished"); if let Some((_, session)) = self.sessions.remove(&session_id) && let Err(e) = session.commit_final_batch() { @@ -183,7 +183,7 @@ impl ValidatorEventsListener for ValidatorEventsCollector { return; } - tracing::debug!(%block_id, "on_validation_complete"); + tracing::debug!(%block_id, "on_block_validated"); let Some(mut session) = self.sessions.get_mut(&session_id) else { tracing::warn!("session not found, ignoring on_block_validated event"); return; diff --git a/slasher/src/lib.rs b/slasher/src/lib.rs index ed7fa74857..187682519f 100644 --- a/slasher/src/lib.rs +++ b/slasher/src/lib.rs @@ -14,13 +14,13 @@ use tycho_core::blockchain_rpc::BlockchainRpcClient; use tycho_crypto::ed25519; use tycho_slasher_traits::{ValidationSessionId, ValidatorEventsListener}; use tycho_types::boc::Boc; +use tycho_util::config::PartialConfig; use tycho_util::futures::JoinTask; use tycho_util::serde_helpers; -use self::bc::MessageDeliveryStatus; pub use self::bc::{ - BlocksBatch, ContractSubscription, EncodeBlocksBatchMessage, SignatureHistory, SignedMessage, - SlasherContract, + BlocksBatch, ContractSubscription, EncodeBlocksBatchMessage, MessageDeliveryStatus, + SignatureHistory, SignedMessage, SlasherContract, StubSlasherContract, }; use self::collector::{ValidatorEventsCollector, ValidatorSessionInfo}; @@ -34,7 +34,7 @@ pub mod collector { mod bc; mod util; -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialConfig)] pub struct SlasherConfig { /// TTL of messages to the slasher contract. /// @@ -108,17 +108,20 @@ impl Slasher { let config_params = cx.state.config_params()?; let Some(slasher_address) = this .contract - .find_account_address(&config_params) + .find_account_address(config_params) .context("failed to find contract address")? .filter(|addr| addr.is_masterchain()) else { return Ok(()); }; + tracing::trace!(%slasher_address); + let subscription = match this.subscription.load_full() { Some(s) if s.address() == &slasher_address => s, // TODO: Use `ArcSwap::compare_and_swap`? _ => { + tracing::info!(%slasher_address, "slasher address changed"); let s = Arc::new(ContractSubscription::new(&slasher_address)); this.subscription.store(Some(s.clone())); s @@ -126,18 +129,19 @@ impl Slasher { }; let extra = cx.block.load_extra()?.account_blocks.load()?; - if let Some((_, account_block)) = extra.get(&slasher_address.address)? { + if let Some((_, account_block)) = extra.get(slasher_address.address)? { subscription.handle_account_transactions(&account_block)?; } // TODO: Get or update batch size from the contract - let batch_size = NonZeroU32::new(100).unwrap(); + let batch_size = NonZeroU32::new(10).unwrap(); while let Some(session_info) = self .validator_events_collector .pop_session_to_init(mc_seqno) { let session_id = session_info.session_id; + tracing::info!(?session_id, "found session to init"); if !session_info.can_participate(&this.node_keys.public_key) { tracing::info!(?session_id, "skipping session"); continue; @@ -220,20 +224,21 @@ impl SlasherSharedState { validator_idx: u16, batch: BlocksBatch, ) { - let params = EncodeBlocksBatchMessage { - session_id, - batch: &batch, - validator_idx, - keypair: &self.node_keys, - ttl: self.config.message_ttl, - }; - loop { let Some(subscription) = self.subscription.load_full() else { tracing::warn!("no slasher contract subscription"); break; }; + let params = EncodeBlocksBatchMessage { + address: subscription.address(), + session_id, + batch: &batch, + validator_idx, + keypair: &self.node_keys, + ttl: self.config.message_ttl, + }; + let signed = match self.contract.encode_blocks_batch_message(¶ms) { Ok(signed) => signed, Err(e) => { @@ -241,11 +246,20 @@ impl SlasherSharedState { return; } }; - let message_hash = *signed.message.repr_hash(); + let msg_hash = *signed.message.repr_hash(); let boc = Boc::encode(signed.message.into_inner()); - match subscription.track_message(&message_hash, signed.expire_at) { + match subscription.track_message(&msg_hash, signed.expire_at) { Ok(res) => { + tracing::info!( + %msg_hash, + address = %params.address, + session_id = ?params.session_id, + validator_idx = params.validator_idx, + batch_seqno = batch.start_seqno, + block_count = batch.committed_blocks.len(), + "sending blocks batch" + ); self.blockchain_rpc_client .broadcast_external_message(&boc) .await; From 0300fa016741a212c01a31931fade0ab8662b52b Mon Sep 17 00:00:00 2001 From: Ivan Kalinin Date: Thu, 15 Jan 2026 15:53:08 +0100 Subject: [PATCH 06/21] feat(slasher): persistent storage --- Cargo.lock | 3 + cli/src/node/mod.rs | 4 +- contracts/src/slasher-stub.tolk | 60 +++++- contracts/tests/Slasher.spec.ts | 99 ++++++++++ contracts/wrappers/SlasherStub.ts | 56 +++++- slasher-traits/src/validator.rs | 6 +- slasher/Cargo.toml | 3 + slasher/src/bc/mod.rs | 76 +++++--- slasher/src/bc/stub_contract.rs | 196 ++++++++++++++++--- slasher/src/lib.rs | 121 +++++++++--- slasher/src/proto.tl | 23 +++ slasher/src/storage/db.rs | 82 ++++++++ slasher/src/storage/mod.rs | 89 +++++++++ slasher/src/storage/models.rs | 153 +++++++++++++++ slasher/src/util.rs | 305 ++++++++++++++++++++++++++---- 15 files changed, 1160 insertions(+), 116 deletions(-) create mode 100644 contracts/tests/Slasher.spec.ts create mode 100644 slasher/src/proto.tl create mode 100644 slasher/src/storage/db.rs create mode 100644 slasher/src/storage/mod.rs create mode 100644 slasher/src/storage/models.rs diff --git a/Cargo.lock b/Cargo.lock index 179c02263f..19366eef2d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4441,6 +4441,7 @@ dependencies = [ "parking_lot", "scopeguard", "serde", + "tl-proto", "tokio", "tokio-util", "tracing", @@ -4448,8 +4449,10 @@ dependencies = [ "tycho-core", "tycho-crypto", "tycho-slasher-traits", + "tycho-storage", "tycho-types", "tycho-util", + "weedb", ] [[package]] diff --git a/cli/src/node/mod.rs b/cli/src/node/mod.rs index f69032bc0b..ec0285cabc 100644 --- a/cli/src/node/mod.rs +++ b/cli/src/node/mod.rs @@ -230,8 +230,10 @@ impl Node { base.keypair.clone(), tycho_slasher::StubSlasherContract, base.blockchain_rpc_client.clone(), + &base.storage_context, self.slasher_config, - ); + ) + .context("failed to create slasher")?; let validator = ValidatorStdImpl::new( ValidatorNetworkContext { diff --git a/contracts/src/slasher-stub.tolk b/contracts/src/slasher-stub.tolk index e071bd906d..424dc43ffd 100644 --- a/contracts/src/slasher-stub.tolk +++ b/contracts/src/slasher-stub.tolk @@ -1,10 +1,13 @@ import "@stdlib/gas-payments" +import "@stdlib/tvm-dicts" import "lib/config-params" const ERROR_INVALID_SIGNATURE = 40 const ERROR_VALIDATOR_NOT_FOUND = 50 const ERROR_REPLAY_PROTECTION = 52 const ERROR_MESSAGE_EXPIRED = 57 +const ERROR_INVALID_BLOCKS_BATCH = 100 +const ERROR_NO_SLASHER_CONFIG = 101 const REPLAY_OFFSET_MS = 5000 const FUTURE_OFFSET_SEC = 60 @@ -21,6 +24,27 @@ fun Storage.save(self) { contract.setData(self.toCell()); } +// +// === Slasher Config param === +// +const PARAM_IDX_SLASHER_PARAMS = 666 + +struct (0x01) SlasherParams { + address: bits256 + blocks_batch_size: uint8 +} + +// +// === Getters === +// +get fun is_blocks_batch_valid(batch: cell): bool { + val params = loadSlasherParams(); + return validateBlocksBatch(batch.beginParse(), params.blocks_batch_size); +} + +// +// === Logic === +// fun onInternalMessage(_in: InMessage) {} fun onExternalMessage(inMsg: slice) { @@ -29,9 +53,11 @@ fun onExternalMessage(inMsg: slice) { val createdAtMs = inMsg.loadUint(64); val expireAtSec = inMsg.loadUint(32); val validatorIdx = inMsg.loadUint(16); - val _batch = inMsg.loadRef(); + val batch = inMsg.loadRef(); inMsg.assertEnd(); assert(blockchain.now() <= expireAtSec, ERROR_MESSAGE_EXPIRED); + val params = loadSlasherParams(); + assert(validateBlocksBatch(batch.beginParse(), params.blocks_batch_size), ERROR_INVALID_BLOCKS_BATCH); var data = Storage.load(); assert(createdAtMs > (data.updatedAtMs - REPLAY_OFFSET_MS) && @@ -49,3 +75,35 @@ fun onExternalMessage(inMsg: slice) { acceptExternalMessage(); } + +fun validateBlocksBatch(batch: slice, batch_size: int): bool { + // TODO: Assert that start seqno is recent enough and not from the future. + val _startSeqno = batch.loadUint(32); + batch.skipBits(batch_size); + val history = batch.loadRef() as dict; + if (!batch.isEmpty()) { + return false; + } + + var iterNext = -1; + do { + val (validatorIdx, cs, found) = history.uDictGetNext(16, iterNext); + if (found) { + iterNext = validatorIdx!; + + // TODO: Check that validator idx is in the mc validators range. + val (csBits, csRefs) = cs!.remainingBitsAndRefsCount(); + if (csBits != batch_size * 2 || csRefs != 0) { + return false; + } + } + } while (found); + + return true; +} + +fun loadSlasherParams(): SlasherParams { + val param = blockchain.configParam(PARAM_IDX_SLASHER_PARAMS); + assert(param != null, ERROR_NO_SLASHER_CONFIG); + return SlasherParams.fromCell(param); +} diff --git a/contracts/tests/Slasher.spec.ts b/contracts/tests/Slasher.spec.ts new file mode 100644 index 0000000000..9d6f74682a --- /dev/null +++ b/contracts/tests/Slasher.spec.ts @@ -0,0 +1,99 @@ +import { compile } from "@ton/blueprint"; +import { + address, + beginCell, + Cell, + Dictionary, + OpenedContract, + toNano, +} from "@ton/core"; +import { Blockchain, createShardAccount, SmartContract } from "@ton/sandbox"; +import { TychoExecutor } from "@tychosdk/emulator"; +import { + PARAM_IDX_SLASHER_PARAMS, + SlasherStub, + storeSlasherParams, + storeSlasherStubData, +} from "../wrappers/SlasherStub"; + +const SLASHER_ADDR = address( + "-1:6666666666666666666666666666666666666666666666666666666666666666", +); +const BLOCKS_BATCH_SIZE = 10; + +describe("Slasher", () => { + let config: Cell; + let code: Cell; + let executor: TychoExecutor; + let blockchain: Blockchain; + let slasher: SmartContract; + + beforeAll(async () => { + const parsedConfig = Dictionary.loadDirect( + Dictionary.Keys.Uint(32), + Dictionary.Values.Cell(), + TychoExecutor.defaultConfig, + ); + parsedConfig.set( + PARAM_IDX_SLASHER_PARAMS, + beginCell() + .store( + storeSlasherParams({ + address: SLASHER_ADDR.hash, + blocksBatchSize: BLOCKS_BATCH_SIZE, + }), + ) + .endCell(), + ); + config = beginCell().storeDictDirect(parsedConfig).endCell(); + + code = await compile("SlasherStub", { debugInfo: true }); + executor = await TychoExecutor.create(); + }); + + beforeEach(async () => { + blockchain = await Blockchain.create({ + config, + executor, + }); + + await blockchain.setShardAccount( + SLASHER_ADDR, + createShardAccount({ + address: SLASHER_ADDR, + balance: toNano(500), + code, + data: beginCell() + .store( + storeSlasherStubData({ + updatedAtMs: 0n, + }), + ) + .endCell(), + workchain: -1, + }), + ); + + slasher = await blockchain.getContract(SLASHER_ADDR); + await blockchain.setVerbosityForAddress(slasher.address, { + blockchainLogs: true, + debugLogs: true, + // vmLogs: "vm_logs_full", + }); + }); + + it("should accept valid blocks batch", async () => { + const { isValid } = await getters(blockchain, slasher).isBlocksBatchValid( + Cell.fromBase64( + "te6ccgEBCAEAMAABCwAAAObYYAECAswFAgIBIAQDAAfRCgDAAAdpRQBgAgEgBwYAB2UFAGAAB/SKAMA=", + ), + ); + expect(isValid).toBe(true); + }); +}); + +function getters(blockchain: Blockchain, slasher: SmartContract) { + return blockchain.openContract( + SlasherStub.createFromAddress(slasher.address), + ); +} diff --git a/contracts/wrappers/SlasherStub.ts b/contracts/wrappers/SlasherStub.ts index ae36cce683..93c6ac1e0d 100644 --- a/contracts/wrappers/SlasherStub.ts +++ b/contracts/wrappers/SlasherStub.ts @@ -1,4 +1,42 @@ -import { Address, Builder, Cell, Contract, Slice } from "@ton/core"; +import { + Address, + Builder, + Cell, + Contract, + ContractProvider, + Slice, +} from "@ton/core"; +import { UnknownTagError } from "./util"; + +export const PARAM_IDX_SLASHER_PARAMS = 666; + +const SLASHER_PARAMS_TAG = 0x01; +export type SlasherParams = { + /// Slasher address in the masterchain. + address: Buffer; + blocksBatchSize: number; +}; + +export function loadSlasherParams(cs: Slice): SlasherParams { + const tag = cs.loadUint(8); + if (tag != SLASHER_PARAMS_TAG) { + throw new UnknownTagError({ tag, bits: 8 }); + } + return { + address: cs.loadBuffer(32), + blocksBatchSize: cs.loadUint(8), + }; +} + +export function storeSlasherParams( + s: SlasherParams, +): (builder: Builder) => void { + return (builder) => { + builder.storeUint(SLASHER_PARAMS_TAG, 8); + builder.storeBuffer(s.address, 32); + builder.storeUint(s.blocksBatchSize, 8); + }; +} export type SlasherStubData = { updatedAtMs: bigint; @@ -11,7 +49,7 @@ export function loadSlasherStubData(cs: Slice): SlasherStubData { } export function storeSlasherStubData( - s: SlasherStubData + s: SlasherStubData, ): (builder: Builder) => void { return (builder) => { builder.storeUint(s.updatedAtMs, 64); @@ -21,10 +59,22 @@ export function storeSlasherStubData( export class SlasherStub implements Contract { constructor( readonly address: Address, - readonly init?: { code: Cell; data: Cell } + readonly init?: { code: Cell; data: Cell }, ) {} static createFromAddress(address: Address) { return new SlasherStub(address); } + + async isBlocksBatchValid(provider: ContractProvider, blocksBatch: Cell) { + const { stack } = await provider.get("is_blocks_batch_valid", [ + { + type: "cell", + cell: blocksBatch, + }, + ]); + return { + isValid: stack.readBoolean(), + }; + } } diff --git a/slasher-traits/src/validator.rs b/slasher-traits/src/validator.rs index 299464bb3d..283fbad79f 100644 --- a/slasher-traits/src/validator.rs +++ b/slasher-traits/src/validator.rs @@ -197,11 +197,11 @@ impl Drop for BlockValidationScope { #[derive(Clone, Copy)] #[repr(transparent)] -pub struct ReceivedSignature(u8); +pub struct ReceivedSignature(pub u8); impl ReceivedSignature { - const VALID_SIGNATURE_BIT: u8 = 0b01; - const INVALID_SIGNATURE_BIT: u8 = 0b10; + pub const VALID_SIGNATURE_BIT: u8 = 0b01; + pub const INVALID_SIGNATURE_BIT: u8 = 0b10; pub fn has_valid_signature(&self) -> bool { self.0 & Self::VALID_SIGNATURE_BIT != 0 diff --git a/slasher/Cargo.toml b/slasher/Cargo.toml index 0bc01f7ff8..4bb6c8dd82 100644 --- a/slasher/Cargo.toml +++ b/slasher/Cargo.toml @@ -19,16 +19,19 @@ metrics = { workspace = true } parking_lot = { workspace = true } scopeguard = { workspace = true } serde = { workspace = true } +tl-proto = { workspace = true } tokio = { workspace = true, features = ["sync"] } tokio-util = { workspace = true } tracing = { workspace = true } tycho-crypto = { workspace = true } tycho-types = { workspace = true, features = ["abi", "models"] } +weedb = { workspace = true } # local deps tycho-block-util = { workspace = true } tycho-core = { workspace = true } tycho-slasher-traits = { workspace = true } +tycho-storage = { workspace = true } tycho-util = { workspace = true } [lints] diff --git a/slasher/src/bc/mod.rs b/slasher/src/bc/mod.rs index c1148ed6d7..d0729f749d 100644 --- a/slasher/src/bc/mod.rs +++ b/slasher/src/bc/mod.rs @@ -7,12 +7,12 @@ use tycho_crypto::ed25519; use tycho_slasher_traits::{ReceivedSignature, ValidationSessionId}; use tycho_types::cell::{HashBytes, Lazy}; use tycho_types::models::{ - AccountBlock, AccountState, BlockchainConfigParams, OwnedMessage, StdAddr, + BlockchainConfigParams, OwnedMessage, SignatureContext, StdAddr, Transaction, }; use tycho_util::FastDashMap; pub use self::stub_contract::StubSlasherContract; -use crate::util::AtomicBitSet; +use crate::util::BitSet; mod stub_contract; @@ -22,21 +22,30 @@ pub struct EncodeBlocksBatchMessage<'a> { pub session_id: ValidationSessionId, pub batch: &'a BlocksBatch, pub validator_idx: u16, + pub signature_context: SignatureContext, pub keypair: &'a ed25519::KeyPair, pub ttl: Duration, } pub trait SlasherContract: Send + Sync + 'static { - fn find_account_address(&self, config: &BlockchainConfigParams) -> Result>; - fn default_batch_size(&self) -> NonZeroU32; - fn get_batch_size(&self, state: &AccountState) -> Result; + fn find_params(&self, config: &BlockchainConfigParams) -> Result>; fn encode_blocks_batch_message( &self, params: &EncodeBlocksBatchMessage<'_>, ) -> Result; + + fn decode_event(&self, tx: &Transaction) -> Result>; +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SlasherParams { + /// Address in masterchain. + pub address: HashBytes, + /// Blocks batch size. + pub blocks_batch_size: NonZeroU32, } pub struct SignedMessage { @@ -78,26 +87,18 @@ impl ContractSubscription { } } - pub fn handle_account_transactions(&self, account_block: &AccountBlock) -> Result<()> { - for entry in account_block.transactions.iter() { - let (_, _, tx) = entry?; - let tx_hash = tx.repr_hash(); - let tx = tx.load()?; - - let Some(in_msg) = tx.in_msg else { - continue; - }; - let msg_hash = in_msg.repr_hash(); - tracing::debug!(%tx_hash, %msg_hash, "found slasher transaction"); - - if let Some((_, pending)) = self.pending_messages.remove(msg_hash) { - pending - .tx - .send(MessageDeliveryStatus::Sent { tx_hash: *tx_hash }) - .ok(); - } + pub fn handle_account_transaction(&self, tx_hash: &HashBytes, tx: &Transaction) -> Result<()> { + let Some(in_msg) = &tx.in_msg else { + return Ok(()); + }; + let msg_hash = in_msg.repr_hash(); + + if let Some((_, pending)) = self.pending_messages.remove(msg_hash) { + pending + .tx + .send(MessageDeliveryStatus::Sent { tx_hash: *tx_hash }) + .ok(); } - Ok(()) } @@ -125,9 +126,23 @@ pub enum MessageDeliveryStatus { Expired, } +// TODO: Add mempool batches or votes here +#[derive(Debug, PartialEq, Eq)] +pub enum SlasherContractEvent { + SubmitBlocksBatch(SubmitBlocksBatch), +} + +// TODO: Propagate session id? +#[derive(Debug, PartialEq, Eq)] +pub struct SubmitBlocksBatch { + pub validator_idx: u16, + pub blocks_batch: BlocksBatch, +} + +#[derive(Debug, PartialEq, Eq)] pub struct BlocksBatch { pub start_seqno: u32, - pub committed_blocks: AtomicBitSet, + pub committed_blocks: BitSet, pub signatures_history: Box<[SignatureHistory]>, } @@ -137,12 +152,12 @@ impl BlocksBatch { Self { start_seqno, - committed_blocks: AtomicBitSet::with_capacity(len), + committed_blocks: BitSet::with_capacity(len), signatures_history: map_ids .iter() .map(|validator_idx| SignatureHistory { validator_idx: *validator_idx, - bits: AtomicBitSet::with_capacity(len * 2), + bits: BitSet::with_capacity(len * 2), }) .collect::>(), } @@ -165,7 +180,7 @@ impl BlocksBatch { (self.start_seqno..self.seqno_after()).contains(&seqno) } - pub fn commit_signatures(&self, mut seqno: u32, signatures: &[ReceivedSignature]) -> bool { + pub fn commit_signatures(&mut self, mut seqno: u32, signatures: &[ReceivedSignature]) -> bool { if !self.contains_seqno(seqno) || signatures.len() != self.signatures_history.len() { return false; } @@ -173,7 +188,7 @@ impl BlocksBatch { seqno -= self.start_seqno; self.committed_blocks.set(seqno as usize, true); - for (history, received) in std::iter::zip(&self.signatures_history, signatures) { + for (history, received) in std::iter::zip(&mut self.signatures_history, signatures) { let idx = (seqno as usize) * 2; history.bits.set(idx, received.has_invalid_signature()); history.bits.set(idx + 1, received.has_valid_signature()); @@ -183,7 +198,8 @@ impl BlocksBatch { } } +#[derive(Debug, PartialEq, Eq)] pub struct SignatureHistory { pub validator_idx: u16, - pub bits: AtomicBitSet, + pub bits: BitSet, } diff --git a/slasher/src/bc/stub_contract.rs b/slasher/src/bc/stub_contract.rs index b95b1984cd..ebea4f3224 100644 --- a/slasher/src/bc/stub_contract.rs +++ b/slasher/src/bc/stub_contract.rs @@ -1,42 +1,60 @@ -use std::num::NonZeroU32; +use std::num::{NonZeroU8, NonZeroU32}; use anyhow::{Context, Result}; -use tycho_types::abi::extend_signature_with_id; use tycho_types::cell::Lazy; use tycho_types::dict; use tycho_types::models::{ - AccountState, BlockchainConfigParams, ExtInMsgInfo, MsgInfo, OwnedMessage, StdAddr, + BlockchainConfigParams, ComputePhase, ExtInMsgInfo, Message, MsgInfo, OwnedMessage, TxInfo, }; use tycho_types::prelude::*; -use super::{BlocksBatch, SignedMessage, SlasherContract}; +use super::{ + BlocksBatch, SignatureHistory, SignedMessage, SlasherContract, SlasherContractEvent, + SlasherParams, SubmitBlocksBatch, +}; +use crate::util::BitSet; + +/// ```tlb +/// slasher_params#01 +/// address:bits256 +/// blocks_batch_size:uint8 +/// { blocks_batch_size > 0 } +/// = ConfigParam 666; +/// ``` +#[derive(Debug, Store, Load)] +#[tlb(tag = "#01")] +pub struct StubSlasherParams { + pub address: HashBytes, + pub blocks_batch_size: NonZeroU8, +} -const PARAM_IDX: u32 = 666; +impl StubSlasherParams { + pub const IDX: u32 = 666; +} pub struct StubSlasherContract; impl SlasherContract for StubSlasherContract { - fn find_account_address(&self, config: &BlockchainConfigParams) -> Result> { - let Some(raw) = config.get_raw_cell_ref(PARAM_IDX)? else { - return Ok(None); - }; - let address = raw.parse::()?; - Ok(Some(StdAddr::new(-1, address))) - } - fn default_batch_size(&self) -> NonZeroU32 { NonZeroU32::new(10).unwrap() } - fn get_batch_size(&self, _state: &AccountState) -> Result { - Ok(self.default_batch_size()) + fn find_params(&self, config: &BlockchainConfigParams) -> Result> { + let Some(raw) = config.get_raw_cell_ref(StubSlasherParams::IDX)? else { + return Ok(None); + }; + let params = raw.parse::()?; + Ok(Some(SlasherParams { + address: params.address, + blocks_batch_size: params.blocks_batch_size.into(), + })) } fn encode_blocks_batch_message( &self, params: &super::EncodeBlocksBatchMessage<'_>, ) -> Result { - let cell = CellBuilder::build_from(StoreBlocksBatch(params.batch)) + let cell = CellBuilder::build_from(BlocksBatchBc::wrap(params.batch)) .context("failed to serialize blocks batch")?; let now = tycho_util::time::now_millis(); @@ -50,11 +68,11 @@ impl SlasherContract for StubSlasherContract { b.build()? }; - // TODO: Add support for signature id. - let signature = params.keypair.sign_raw(&extend_signature_with_id( - body_to_sign.repr_hash().as_array(), - None, - )); + let signature = params.keypair.sign_raw( + ¶ms + .signature_context + .apply(body_to_sign.repr_hash().as_array()), + ); let body = { let mut b = CellBuilder::new(); b.store_raw(&signature, 512)?; @@ -74,17 +92,96 @@ impl SlasherContract for StubSlasherContract { Ok(SignedMessage { message, expire_at }) } + + fn decode_event( + &self, + tx: &tycho_types::models::Transaction, + ) -> Result> { + 'check: { + if let TxInfo::Ordinary(info) = tx.load_info()? + && let ComputePhase::Executed(ph) = info.compute_phase + && ph.exit_code == 0 + { + break 'check; + } + return Ok(None); + }; + + let Some(in_msg) = &tx.in_msg else { + return Ok(None); + }; + let msg = in_msg.parse::>()?; + if !msg.info.is_external_in() { + return Ok(None); + } + + // TODO: Add message op + let mut body = msg.body; + body.skip_first(512 + 64 + 32, 0)?; + let validator_idx = body.load_u16()?; + let mut batch_cs = body.load_reference_as_slice()?; + let BlocksBatchBc(blocks_batch) = <_>::load_from(&mut batch_cs)?; + if !body.is_empty() || !batch_cs.is_empty() { + return Err(tycho_types::error::Error::CellOverflow.into()); + } + + Ok(Some(SlasherContractEvent::SubmitBlocksBatch( + SubmitBlocksBatch { + validator_idx, + blocks_batch, + }, + ))) + } } -struct StoreBlocksBatch<'a>(&'a BlocksBatch); +#[repr(transparent)] +struct BlocksBatchBc(BlocksBatch); + +impl BlocksBatchBc { + fn wrap(inner: &BlocksBatch) -> &Self { + // SAFETY: `BlocksBatchBc` has the same layout as `BlocksBatch`. + unsafe { &*(inner as *const BlocksBatch).cast::() } + } +} -impl Store for StoreBlocksBatch<'_> { +impl<'a> Load<'a> for BlocksBatchBc { + fn load_from(slice: &mut CellSlice<'a>) -> Result { + let start_seqno = slice.load_u32()?; + + let block_count = slice.size_bits() as usize; + let committed_blocks = BitSet::load_from_cs(block_count, slice)?; + + let mut signatures_history = Vec::new(); + + let dict = Dict::>::from_raw(Some(slice.load_reference_cloned()?)); + for entry in dict.iter() { + let (validator_idx, mut cs) = entry?; + let bits = BitSet::load_from_cs(block_count * 2, &mut cs)?; + if !cs.is_empty() { + return Err(tycho_types::error::Error::CellOverflow); + } + + signatures_history.push(SignatureHistory { + validator_idx, + bits, + }); + } + + Ok(Self(BlocksBatch { + start_seqno, + committed_blocks, + signatures_history: signatures_history.into_boxed_slice(), + })) + } +} + +impl Store for BlocksBatchBc { fn store_into( &self, builder: &mut CellBuilder, context: &dyn CellContext, ) -> Result<(), tycho_types::error::Error> { - let batch = self.0; + let batch = &self.0; builder.store_u32(batch.start_seqno)?; batch.committed_blocks.store_into(builder, context)?; @@ -105,3 +202,54 @@ impl Store for StoreBlocksBatch<'_> { builder.store_reference(dict_root) } } + +#[cfg(test)] +mod tests { + use tycho_slasher_traits::ReceivedSignature; + + use super::*; + + #[test] + fn blocks_batch_cell() { + let mut batch = BlocksBatch::new(230, NonZeroU32::new(10).unwrap(), &[5, 10, 12, 3]); + + for (seqno, signatures) in [ + (230, [ + ReceivedSignature(ReceivedSignature::VALID_SIGNATURE_BIT), + ReceivedSignature(ReceivedSignature::VALID_SIGNATURE_BIT), + ReceivedSignature(0), + ReceivedSignature(ReceivedSignature::INVALID_SIGNATURE_BIT), + ]), + (231, [ + ReceivedSignature(0), + ReceivedSignature(ReceivedSignature::VALID_SIGNATURE_BIT), + ReceivedSignature(ReceivedSignature::INVALID_SIGNATURE_BIT), + ReceivedSignature(ReceivedSignature::VALID_SIGNATURE_BIT), + ]), + (233, [ + ReceivedSignature(ReceivedSignature::VALID_SIGNATURE_BIT), + ReceivedSignature(ReceivedSignature::VALID_SIGNATURE_BIT), + ReceivedSignature(ReceivedSignature::VALID_SIGNATURE_BIT), + ReceivedSignature(ReceivedSignature::VALID_SIGNATURE_BIT), + ]), + (234, [ + ReceivedSignature(ReceivedSignature::VALID_SIGNATURE_BIT), + ReceivedSignature(ReceivedSignature::VALID_SIGNATURE_BIT), + ReceivedSignature(ReceivedSignature::VALID_SIGNATURE_BIT), + ReceivedSignature(ReceivedSignature::VALID_SIGNATURE_BIT), + ]), + (239, [ + ReceivedSignature(ReceivedSignature::VALID_SIGNATURE_BIT), + ReceivedSignature(ReceivedSignature::VALID_SIGNATURE_BIT), + ReceivedSignature(ReceivedSignature::VALID_SIGNATURE_BIT), + ReceivedSignature(ReceivedSignature::VALID_SIGNATURE_BIT), + ]), + ] { + let committed = batch.commit_signatures(seqno, &signatures); + assert!(committed); + } + + let cell = CellBuilder::build_from(BlocksBatchBc::wrap(&batch)).unwrap(); + println!("{}", Boc::encode_base64(cell)); + } +} diff --git a/slasher/src/lib.rs b/slasher/src/lib.rs index 187682519f..7f1809fb22 100644 --- a/slasher/src/lib.rs +++ b/slasher/src/lib.rs @@ -1,9 +1,8 @@ -use std::num::NonZeroU32; use std::sync::Arc; use std::time::Duration; use anyhow::{Context, Result}; -use arc_swap::ArcSwapOption; +use arc_swap::{ArcSwap, ArcSwapOption}; use futures_util::future::BoxFuture; use serde::{Deserialize, Serialize}; use tokio::sync::mpsc; @@ -13,7 +12,9 @@ use tycho_core::block_strider::{StateSubscriber, StateSubscriberContext}; use tycho_core::blockchain_rpc::BlockchainRpcClient; use tycho_crypto::ed25519; use tycho_slasher_traits::{ValidationSessionId, ValidatorEventsListener}; +use tycho_storage::StorageContext; use tycho_types::boc::Boc; +use tycho_types::models::{SignatureContext, StdAddr}; use tycho_util::config::PartialConfig; use tycho_util::futures::JoinTask; use tycho_util::serde_helpers; @@ -23,6 +24,8 @@ pub use self::bc::{ SignatureHistory, SignedMessage, SlasherContract, StubSlasherContract, }; use self::collector::{ValidatorEventsCollector, ValidatorSessionInfo}; +use self::storage::SlasherStorage; +use self::util::AtomicValidationSessionId; pub mod collector { pub use self::validator_events::*; @@ -32,6 +35,8 @@ pub mod collector { } mod bc; +#[expect(unused)] +mod storage; mod util; #[derive(Debug, Clone, Serialize, Deserialize, PartialConfig)] @@ -75,11 +80,15 @@ impl Slasher { node_keys: Arc, contract: C, blockchain_rpc_client: BlockchainRpcClient, + storage_context: &StorageContext, config: SlasherConfig, - ) -> Self { + ) -> Result { + let storage = + SlasherStorage::open(storage_context).context("failed to open slasher storage")?; + let collector = Arc::new(ValidatorEventsCollector::new(contract.default_batch_size())); - Self { + Ok(Self { validator_events_collector: collector, shared: Arc::new(SlasherSharedState { config, @@ -87,9 +96,18 @@ impl Slasher { contract: Box::new(contract), subscription: ArcSwapOption::empty(), blockchain_rpc_client, + storage, + known_session_id: AtomicValidationSessionId::new(ValidationSessionId { + seqno: 0, + short_hash: 0, + }), + signature_context: ArcSwap::new(Arc::new(SignatureContext { + global_id: 0, + capabilities: Default::default(), + })), }), cancellation_token: Default::default(), - } + }) } pub fn validator_events_listener(&self) -> Arc { @@ -103,23 +121,52 @@ impl Slasher { let mc_seqno = cx.block.id().seqno; let this = self.shared.as_ref(); - - // Check config updates - let config_params = cx.state.config_params()?; - let Some(slasher_address) = this + let state_extra = cx.state.state_extra()?; + + // Sync signature context (TODO: do it only when config changes) + let global = state_extra.config.get_global_version()?; + self.shared + .signature_context + .store(Arc::new(SignatureContext { + global_id: cx.block.as_ref().global_id, + capabilities: global.capabilities, + })); + + // Check config updates (TODO: do it only when config changes) + let Some(slasher_params) = this .contract - .find_account_address(config_params) - .context("failed to find contract address")? - .filter(|addr| addr.is_masterchain()) + .find_params(&state_extra.config) + .context("failed to find slasher params")? else { return Ok(()); }; + self.validator_events_collector + .set_default_batch_size(slasher_params.blocks_batch_size); + let slasher_address = StdAddr::new_masterchain(slasher_params.address); - tracing::trace!(%slasher_address); + let session_id_from_block = ValidationSessionId { + seqno: state_extra.validator_info.catchain_seqno, + short_hash: state_extra.validator_info.validator_list_hash_short, + }; + tracing::trace!(?slasher_params, ?session_id_from_block); + + // Clear old sessions if needed + // TODO: Add metrics. + if session_id_from_block != this.known_session_id.load() { + let span = tracing::Span::current(); + let storage = this.storage.clone(); + tokio::task::spawn_blocking(move || { + let _span = span.enter(); + storage.remove_outdated_batches(session_id_from_block) + }) + .await??; + + this.known_session_id.set(session_id_from_block); + } + // Handle subscription let subscription = match this.subscription.load_full() { Some(s) if s.address() == &slasher_address => s, - // TODO: Use `ArcSwap::compare_and_swap`? _ => { tracing::info!(%slasher_address, "slasher address changed"); let s = Arc::new(ContractSubscription::new(&slasher_address)); @@ -130,12 +177,37 @@ impl Slasher { let extra = cx.block.load_extra()?.account_blocks.load()?; if let Some((_, account_block)) = extra.get(slasher_address.address)? { - subscription.handle_account_transactions(&account_block)?; + for entry in account_block.transactions.iter() { + let (_, _, tx) = entry?; + let tx_hash = tx.repr_hash(); + let tx = tx.load()?; + + tracing::debug!( + %tx_hash, + msg_hash = ?tx.in_msg.as_ref().map(|msg| msg.repr_hash()), + "found slasher transaction", + ); + + subscription.handle_account_transaction(tx_hash, &tx)?; + + match self.shared.contract.decode_event(&tx) { + Ok(Some(event)) => match event { + bc::SlasherContractEvent::SubmitBlocksBatch(submitted) => { + // TODO: Move into blocking. + this.storage.store_blocks_batch( + session_id_from_block, + submitted.validator_idx, + &submitted.blocks_batch, + )?; + tokio::task::yield_now().await; + } + }, + Ok(None) => {} + Err(e) => tracing::warn!(%tx_hash, "failed to parse slasher event: {e:?}"), + } + } } - // TODO: Get or update batch size from the contract - let batch_size = NonZeroU32::new(10).unwrap(); - while let Some(session_info) = self .validator_events_collector .pop_session_to_init(mc_seqno) @@ -148,10 +220,11 @@ impl Slasher { } let (tx, rx) = mpsc::unbounded_channel::(); - if !self - .validator_events_collector - .init_session(session_id, batch_size, tx) - { + if !self.validator_events_collector.init_session( + session_id, + slasher_params.blocks_batch_size, + tx, + ) { tracing::warn!(?session_id, "session removed before init"); continue; } @@ -188,6 +261,9 @@ struct SlasherSharedState { contract: Box, subscription: ArcSwapOption, blockchain_rpc_client: BlockchainRpcClient, + storage: SlasherStorage, + known_session_id: AtomicValidationSessionId, + signature_context: ArcSwap, } impl SlasherSharedState { @@ -235,6 +311,7 @@ impl SlasherSharedState { session_id, batch: &batch, validator_idx, + signature_context: **self.signature_context.load(), keypair: &self.node_keys, ttl: self.config.message_ttl, }; diff --git a/slasher/src/proto.tl b/slasher/src/proto.tl new file mode 100644 index 0000000000..252a9b464c --- /dev/null +++ b/slasher/src/proto.tl @@ -0,0 +1,23 @@ +---types--- + +/** +* @param start_seqno seqno of the first block in batch +* @param committed_blocks bitset with committed blocks +* @param entries all non-empty histories for unique validator indexes +*/ +slasher.blocksBatch + start_seqno:int + committed_blocks:bitset + entries:(vector slasher.signatureHistory) + = slasher.BlocksBatch; + +/** +* @param validator_idx validator index relative to the validator set +* @param bits history bits (2 for each block) +*/ +slasher.signatureHistory + validator_idx:int + bits:bitset + = slasher.SignatureHistory; + +bitset length:int data:bytes = BitSet; \ No newline at end of file diff --git a/slasher/src/storage/db.rs b/slasher/src/storage/db.rs new file mode 100644 index 0000000000..78b2c29f21 --- /dev/null +++ b/slasher/src/storage/db.rs @@ -0,0 +1,82 @@ +use tycho_storage::kv::{ + Migrations, NamedTables, StateVersionProvider, TableContext, WithMigrations, +}; +use tycho_util::sync::CancellationFlag; +use weedb::{MigrationError, Semver, WeeDb}; + +pub type SlasherDb = WeeDb; + +impl NamedTables for SlasherTables { + const NAME: &'static str = "slasher"; +} + +impl WithMigrations for SlasherTables { + const VERSION: Semver = [0, 1, 0]; + + type VersionProvider = StateVersionProvider; + + fn new_version_provider() -> Self::VersionProvider { + StateVersionProvider::new::() + } + + fn register_migrations( + _migrations: &mut Migrations, + _cancelled: CancellationFlag, + ) -> Result<(), MigrationError> { + Ok(()) + } +} + +// TODO: Add a table for temp batches. +weedb::tables! { + pub struct SlasherTables { + pub state: tables::State, + pub block_batches: tables::BlockBatches, + } +} + +pub mod tables { + use tycho_storage::kv::{ + TableContext, default_block_based_table_factory, optimize_for_point_lookup, + zstd_block_based_table_factory, + }; + use weedb::rocksdb::Options; + use weedb::{ColumnFamily, ColumnFamilyOptions}; + + /// Stores generic node parameters + /// - Key: `...` + /// - Value: `...` + pub struct State; + + impl ColumnFamily for State { + const NAME: &'static str = "state"; + } + + impl ColumnFamilyOptions for State { + fn options(opts: &mut Options, ctx: &mut TableContext) { + default_block_based_table_factory(opts, ctx); + + opts.set_optimize_filters_for_hits(true); + optimize_for_point_lookup(opts, ctx); + } + } + + /// Code hash with account address + /// - Key: `session_id: (u32 BE, u32 BE), validator_idx: u16 BE, start_block: u32 BE` + /// - Value: blocks batch + pub struct BlockBatches; + + impl BlockBatches { + pub const KEY_LEN: usize = 4 + 4 + 2 + 4; + } + + impl ColumnFamily for BlockBatches { + const NAME: &'static str = "block_batches"; + } + + impl ColumnFamilyOptions for BlockBatches { + fn options(opts: &mut Options, ctx: &mut TableContext) { + zstd_block_based_table_factory(opts, ctx); + } + } +} diff --git a/slasher/src/storage/mod.rs b/slasher/src/storage/mod.rs new file mode 100644 index 0000000000..d0549fc9c6 --- /dev/null +++ b/slasher/src/storage/mod.rs @@ -0,0 +1,89 @@ +use std::sync::Arc; + +use anyhow::Result; +use tycho_slasher_traits::ValidationSessionId; +use tycho_storage::StorageContext; +use tycho_types::cell::HashBytes; +use weedb::OwnedSnapshot; + +use self::db::{SlasherDb, tables}; +use self::models::StoredBlocksBatch; +use crate::BlocksBatch; + +pub mod db; +pub mod models; + +const SLASHER_DB_SUBDIR: &str = "slasher"; + +#[derive(Clone)] +#[repr(transparent)] +pub struct SlasherStorage { + inner: Arc, +} + +impl SlasherStorage { + pub fn open(ctx: &StorageContext) -> Result { + let db = ctx.open_preconfigured(SLASHER_DB_SUBDIR)?; + + Ok(Self { + inner: Arc::new(Inner { db }), + }) + } + + pub fn db(&self) -> &SlasherDb { + &self.inner.db + } + + /// Creates a new snapshot. + pub fn snapshot(&self) -> SlasherStorageSnapshot { + SlasherStorageSnapshot { + snapshot: Arc::new(self.inner.db.owned_snapshot()), + } + } + + pub fn store_blocks_batch( + &self, + session_id: ValidationSessionId, + validator_idx: u16, + batch: &BlocksBatch, + ) -> Result<()> { + let mut key = [0u8; tables::BlockBatches::KEY_LEN]; + key[0..4].copy_from_slice(&session_id.seqno.to_be_bytes()); + key[4..8].copy_from_slice(&session_id.short_hash.to_be_bytes()); + key[8..10].copy_from_slice(&validator_idx.to_be_bytes()); + key[10..14].copy_from_slice(&batch.start_seqno.to_be_bytes()); + + let value = tl_proto::serialize(StoredBlocksBatch::wrap(batch)); + + self.inner.db.block_batches.insert(key.as_slice(), value)?; + Ok(()) + } + + /// Removes all block batches for sessions BEFORE the specified. + /// + /// NOTE: Does not touch "rotated" sessions (same seqno but different `short_hash`), + /// because we cannot order them properly. + pub fn remove_outdated_batches(&self, latest_session_id: ValidationSessionId) -> Result<()> { + let db = &self.inner.db; + + let mut key = [0u8; tables::BlockBatches::KEY_LEN]; + key[0..4].copy_from_slice(&latest_session_id.seqno.to_be_bytes()); + + db.rocksdb().delete_range_cf_opt( + &db.block_batches.cf(), + [0u8; tables::BlockBatches::KEY_LEN], + key, + db.block_batches.write_config(), + )?; + Ok(()) + } +} + +struct Inner { + db: SlasherDb, +} + +#[derive(Clone)] +pub struct SlasherStorageSnapshot { + snapshot: Arc, +} diff --git a/slasher/src/storage/models.rs b/slasher/src/storage/models.rs new file mode 100644 index 0000000000..a3844e8e25 --- /dev/null +++ b/slasher/src/storage/models.rs @@ -0,0 +1,153 @@ +use tl_proto::{TlError, TlPacket, TlRead, TlResult, TlWrite}; +use tycho_util::FastHashSet; + +use crate::util::BitSet; +use crate::{BlocksBatch, SignatureHistory}; + +#[repr(transparent)] +pub struct StoredBlocksBatch(pub BlocksBatch); + +impl StoredBlocksBatch { + pub const TL_ID: u32 = tl_proto::id!("slasher.blocksBatch", scheme = "proto.tl"); + + const MAX_SAFE_COMMITTED_BLOCKS: usize = 500; + const MAX_SAFE_HISTORY_COUNT: usize = 1000; + + #[inline] + pub const fn wrap(inner: &BlocksBatch) -> &Self { + // SAFETY: `StoredBlocksBatch` has the same layout as `BlocksBatch`. + unsafe { &*(inner as *const BlocksBatch).cast::() } + } +} + +impl TlWrite for StoredBlocksBatch { + type Repr = tl_proto::Boxed; + + // TODO: Simplify becase all signature histories are equal in size. + fn max_size_hint(&self) -> usize { + 4 + 4 + + self.0.committed_blocks.max_size_hint() + + 4 + + self + .0 + .signatures_history + .iter() + .map(|item| 4 + item.bits.max_size_hint()) + .sum::() + } + + fn write_to(&self, packet: &mut P) { + packet.write_u32(Self::TL_ID); + packet.write_u32(self.0.start_seqno); + self.0.committed_blocks.write_to(packet); + packet.write_u32(self.0.signatures_history.len() as u32); + for item in &self.0.signatures_history { + packet.write_u32(item.validator_idx as u32); + item.bits.write_to(packet); + } + } +} + +impl<'tl> TlRead<'tl> for StoredBlocksBatch { + type Repr = tl_proto::Boxed; + + fn read_from(packet: &mut &'tl [u8]) -> TlResult { + if u32::read_from(packet)? != Self::TL_ID { + return Err(TlError::UnknownConstructor); + } + + let start_seqno = u32::read_from(packet)?; + let committed_blocks = BitSet::read_from(packet)?; + let block_count = committed_blocks.len(); + if start_seqno.checked_add(block_count as u32).is_none() { + return Err(TlError::InvalidData); + } + + let history_count = u32::read_from(packet)? as usize; + if history_count > Self::MAX_SAFE_HISTORY_COUNT + || block_count > Self::MAX_SAFE_COMMITTED_BLOCKS + { + return Err(TlError::InvalidData); + } + + let mut signatures_history = Vec::with_capacity(history_count); + let mut unique_indices = + FastHashSet::with_capacity_and_hasher(history_count, Default::default()); + for _ in 0..history_count { + let Ok(validator_idx) = u16::try_from(u32::read_from(packet)?) else { + return Err(TlError::InvalidData); + }; + if !unique_indices.insert(validator_idx) { + return Err(TlError::InvalidData); + } + let bits = BitSet::read_from(packet)?; + if bits.len() != block_count * 2 { + return Err(TlError::InvalidData); + } + signatures_history.push(SignatureHistory { + validator_idx, + bits, + }); + } + + Ok(Self(BlocksBatch { + start_seqno, + committed_blocks, + signatures_history: signatures_history.into_boxed_slice(), + })) + } +} + +#[cfg(test)] +mod tests { + use std::num::NonZeroU32; + + use tycho_slasher_traits::ReceivedSignature; + + use super::*; + + #[test] + fn blocks_batch_tl_repr() { + let mut batch = BlocksBatch::new(230, NonZeroU32::new(100).unwrap(), &[5, 10, 12, 3]); + + for (seqno, signatures) in [ + (230, [ + ReceivedSignature(ReceivedSignature::VALID_SIGNATURE_BIT), + ReceivedSignature(ReceivedSignature::VALID_SIGNATURE_BIT), + ReceivedSignature(0), + ReceivedSignature(ReceivedSignature::INVALID_SIGNATURE_BIT), + ]), + (250, [ + ReceivedSignature(0), + ReceivedSignature(ReceivedSignature::VALID_SIGNATURE_BIT), + ReceivedSignature(ReceivedSignature::INVALID_SIGNATURE_BIT), + ReceivedSignature(ReceivedSignature::VALID_SIGNATURE_BIT), + ]), + (251, [ + ReceivedSignature(ReceivedSignature::VALID_SIGNATURE_BIT), + ReceivedSignature(ReceivedSignature::VALID_SIGNATURE_BIT), + ReceivedSignature(ReceivedSignature::VALID_SIGNATURE_BIT), + ReceivedSignature(ReceivedSignature::VALID_SIGNATURE_BIT), + ]), + (300, [ + ReceivedSignature(ReceivedSignature::VALID_SIGNATURE_BIT), + ReceivedSignature(ReceivedSignature::VALID_SIGNATURE_BIT), + ReceivedSignature(ReceivedSignature::VALID_SIGNATURE_BIT), + ReceivedSignature(ReceivedSignature::VALID_SIGNATURE_BIT), + ]), + (329, [ + ReceivedSignature(ReceivedSignature::VALID_SIGNATURE_BIT), + ReceivedSignature(ReceivedSignature::VALID_SIGNATURE_BIT), + ReceivedSignature(ReceivedSignature::VALID_SIGNATURE_BIT), + ReceivedSignature(ReceivedSignature::VALID_SIGNATURE_BIT), + ]), + ] { + let committed = batch.commit_signatures(seqno, &signatures); + assert!(committed); + } + + let stored = tl_proto::serialize(StoredBlocksBatch::wrap(&batch)); + let loaded = tl_proto::deserialize::(&stored).unwrap(); + assert_eq!(batch, loaded.0); + } +} diff --git a/slasher/src/util.rs b/slasher/src/util.rs index 2a3a830bd6..92d60d888d 100644 --- a/slasher/src/util.rs +++ b/slasher/src/util.rs @@ -1,42 +1,126 @@ use std::ptr::NonNull; -use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::atomic::{AtomicU64, Ordering}; +use tl_proto::{TlError, TlRead, TlResult, TlWrite}; +use tycho_slasher_traits::ValidationSessionId; use tycho_types::prelude::*; -pub struct AtomicBitSet { - data: NonNull, +// === AtomicValidationSessionId === + +pub struct AtomicValidationSessionId(AtomicU64); + +impl AtomicValidationSessionId { + pub const fn new(value: ValidationSessionId) -> Self { + Self(AtomicU64::new(Self::pack_id(value))) + } + + pub fn set(&self, value: ValidationSessionId) { + self.0.store(Self::pack_id(value), Ordering::Release); + } + + pub fn load(&self) -> ValidationSessionId { + Self::unpack_id(self.0.load(Ordering::Acquire)) + } + + #[inline] + const fn pack_id(value: ValidationSessionId) -> u64 { + const _: () = const { + let id = ValidationSessionId { + seqno: 0, + short_hash: 0, + }; + assert!(std::mem::size_of_val(&id.seqno) == 4); + assert!(std::mem::size_of_val(&id.short_hash) == 4); + }; + + ((value.seqno as u64) << 32) | (value.short_hash as u64) + } + + #[inline] + const fn unpack_id(value: u64) -> ValidationSessionId { + ValidationSessionId { + seqno: (value >> 32) as u32, + short_hash: value as u32, + } + } +} + +// === BitSet === + +pub struct BitSet { + data: Option>, length: usize, } -unsafe impl Send for AtomicBitSet {} -unsafe impl Sync for AtomicBitSet {} +unsafe impl Send for BitSet {} +unsafe impl Sync for BitSet {} + +impl BitSet { + pub const EMPTY: Self = Self { + data: None, + length: 0, + }; -impl AtomicBitSet { pub const BLOCK_BITS: usize = std::mem::size_of::() * 8; pub fn with_capacity(bits: usize) -> Self { - let data = vec![0; block_count(bits)] - .into_iter() - .map(Block::new) - .collect::>(); + if bits == 0 { + return Self::EMPTY; + } + + let data = Vec::::into_boxed_slice(vec![0; block_count(bits)]); Self { - data: unsafe { NonNull::new_unchecked(Box::into_raw(data)).cast() }, + data: Some(unsafe { NonNull::new_unchecked(Box::into_raw(data)).cast() }), length: bits, } } + pub fn load_from_cs( + bits: usize, + cs: &mut CellSlice<'_>, + ) -> Result { + if bits == 0 { + return Ok(Self::EMPTY); + } + if bits > tycho_types::cell::MAX_BIT_LEN as usize { + return Err(tycho_types::error::Error::CellUnderflow); + } + let mut buffer = [0u8; 128]; + let bytes = cs.load_raw(&mut buffer, bits as u16)?; + debug_assert_eq!(bytes.len(), bits.div_ceil(8)); + + let (chunks, tail) = bytes.as_chunks::<{ Self::BLOCK_BITS / 8 }>(); + + let mut data: Vec = vec![0; block_count(bits)]; + for (data, chunk) in std::iter::zip(&mut data, chunks) { + *data = Block::from_be_bytes(*chunk).reverse_bits(); + } + if let Some(data) = data.last_mut() + && !tail.is_empty() + { + let mut buffer = [0u8; Self::BLOCK_BITS / 8]; + buffer[0..tail.len()].copy_from_slice(tail); + *data = Block::from_be_bytes(buffer).reverse_bits(); + } + + Ok(Self { + data: Some(unsafe { + NonNull::new_unchecked(Box::into_raw(data.into_boxed_slice())).cast() + }), + length: bits, + }) + } + pub fn len(&self) -> usize { self.length } pub fn is_zero(&self) -> bool { - self.as_slice() - .iter() - .all(|item| item.load(Ordering::Acquire) == 0) + self.as_slice().iter().all(|item| *item == 0) } - pub fn set(&self, bit: usize, enabled: bool) { + pub fn set(&mut self, bit: usize, enabled: bool) { assert!( bit < self.length, "set at index {bit} exceeds bitset size {}", @@ -47,36 +131,70 @@ impl AtomicBitSet { unsafe { self.set_unchecked(bit, enabled) } } - unsafe fn set_unchecked(&self, bit: usize, enabled: bool) { + unsafe fn set_unchecked(&mut self, bit: usize, enabled: bool) { + let Some(data) = self.data else { + return; + }; + let block = bit / Self::BLOCK_BITS; let rem = bit % Self::BLOCK_BITS; - let block = unsafe { &*self.data.as_ptr().add(block) }; + let block = unsafe { &mut *data.as_ptr().add(block) }; if enabled { - block.fetch_or(1 << rem, Ordering::Release); + *block |= 1 << rem; } else { - block.fetch_and(!(1 << rem), Ordering::Release); + *block &= !(1 << rem); } } pub fn as_slice(&self) -> &[Block] { - // SAFETY: Data was allocated for this exact block count. - unsafe { std::slice::from_raw_parts(self.data.as_ptr(), block_count(self.length)) } + match self.data { + Some(data) => { + // SAFETY: Data was allocated for this exact block count. + unsafe { std::slice::from_raw_parts(data.as_ptr(), block_count(self.length)) } + } + None => &[], + } + } +} + +impl std::fmt::Debug for BitSet { + #[inline] + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut remaining_bits = self.length; + for block in self.as_slice() { + let bits = std::cmp::min(remaining_bits, Self::BLOCK_BITS); + remaining_bits -= bits; + + let block = block.reverse_bits() >> (Self::BLOCK_BITS - bits); + write!(f, "{block:0bits$b}")?; + } + Ok(()) } } -impl Drop for AtomicBitSet { +impl Eq for BitSet {} +impl PartialEq for BitSet { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.length == other.length && self.as_slice() == other.as_slice() + } +} + +impl Drop for BitSet { fn drop(&mut self) { - drop(unsafe { - Box::<[Block]>::from_raw(std::ptr::slice_from_raw_parts_mut( - self.data.as_ptr(), - block_count(self.length), - )) - }); + if let Some(data) = self.data { + drop(unsafe { + Box::<[Block]>::from_raw(std::ptr::slice_from_raw_parts_mut( + data.as_ptr(), + block_count(self.length), + )) + }); + } } } -impl Store for AtomicBitSet { +impl Store for BitSet { fn store_into( &self, b: &mut CellBuilder, @@ -89,15 +207,138 @@ impl Store for AtomicBitSet { for block in self.as_slice() { let bits = std::cmp::min(remaining_bits, Self::BLOCK_BITS as u16); remaining_bits -= bits; - b.store_uint(block.load(Ordering::Acquire) as u64, bits)?; + + let block = block.reverse_bits() >> (Self::BLOCK_BITS - bits as usize); + b.store_uint(block, bits)?; } Ok(()) } } +impl TlWrite for BitSet { + type Repr = tl_proto::Bare; + + fn max_size_hint(&self) -> usize { + 4 + tl_proto::bytes_max_size_hint(std::mem::size_of_val(self.as_slice())) + } + + fn write_to

(&self, packet: &mut P) + where + P: tl_proto::TlPacket, + { + packet.write_u32(self.length as u32); + + let bytes = match self.data { + Some(data) => unsafe { + std::slice::from_raw_parts( + data.as_ptr().cast::(), + block_count(self.length) * std::mem::size_of::(), + ) + }, + None => &[], + }; + <&[u8] as TlWrite>::write_to(&bytes, packet); + } +} + +impl<'tl> TlRead<'tl> for BitSet { + type Repr = tl_proto::Bare; + + fn read_from(packet: &mut &'tl [u8]) -> TlResult { + let length = u32::read_from(packet)? as usize; + let bytes = <&[u8]>::read_from(packet)?; + + let block_count = block_count(length); + let Some(expected_byte_count) = block_count.checked_mul(std::mem::size_of::()) + else { + return Err(TlError::InvalidData); + }; + if expected_byte_count != bytes.len() { + return Err(TlError::InvalidData); + } + + if block_count == 0 { + return Ok(Self::EMPTY); + } + + let mut data = Box::<[Block]>::new_uninit_slice(block_count); + debug_assert_eq!( + data.len() * std::mem::size_of::(), + expected_byte_count + ); + + // SAFETY: `data` has the exact same number of bytes allocated. + unsafe { + std::ptr::copy_nonoverlapping( + bytes.as_ptr(), + data.as_mut_ptr().cast::(), + expected_byte_count, + ); + } + + Ok(Self { + // SAFETY: We are constructing a non-null pointer right out of the `Box`. + data: Some(unsafe { NonNull::new_unchecked(Box::into_raw(data).cast::()) }), + length, + }) + } +} + fn block_count(bits: usize) -> usize { - bits.div_ceil(AtomicBitSet::BLOCK_BITS) + bits.div_ceil(BitSet::BLOCK_BITS) } -type Block = AtomicUsize; +type Block = u64; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn bitset_cell_repr() { + // Empty bitset + let cell = CellBuilder::build_from(BitSet::EMPTY).unwrap(); + assert!(cell.is_empty()); + let parsed = BitSet::load_from_cs(0, &mut cell.as_slice().unwrap()).unwrap(); + assert_eq!(BitSet::EMPTY, parsed); + + // Not-empty bitset + let mut bitset = BitSet::with_capacity(100); + for i in 0..bitset.len() { + // Some random but distinct pattern to catch alignment bugs. + if i % 7 == 0 || (50..=90).contains(&i) { + bitset.set(i, true); + } + } + + let cell = CellBuilder::build_from(&bitset).unwrap(); + assert_eq!(cell.bit_len() as usize, bitset.len()); + + let parsed = BitSet::load_from_cs(100, &mut cell.as_slice().unwrap()).unwrap(); + assert_eq!(bitset, parsed); + } + + #[test] + fn bitset_tl_repr() { + // Empty bitset + let stored = tl_proto::serialize(&BitSet::EMPTY); + let parsed = tl_proto::deserialize::(&stored).unwrap(); + assert_eq!(BitSet::EMPTY, parsed); + + // Not-empty bitset + let mut bitset = BitSet::with_capacity(100); + for i in 0..bitset.len() { + // Some random but distinct pattern to catch alignment bugs. + if i % 7 == 0 || (50..=90).contains(&i) { + bitset.set(i, true); + } + } + + println!("{bitset:?}"); + + let stored = tl_proto::serialize(&bitset); + let parsed = tl_proto::deserialize::(&stored).unwrap(); + assert_eq!(bitset, parsed); + } +} From 68fac2dc4be9b77e41ff6a6a1e5328117b8b6988 Mon Sep 17 00:00:00 2001 From: Ivan Kalinin Date: Mon, 2 Feb 2026 14:43:21 +0100 Subject: [PATCH 07/21] feat(slasher): check blocks batch seqno range --- collator/tests/collation_tests.rs | 1 - contracts/package.json | 6 +- contracts/src/slasher-stub.tolk | 95 ++++++++++--- contracts/tests/Slasher.spec.ts | 126 +++++++++++++++--- contracts/wrappers/SlasherStub.ts | 11 +- contracts/wrappers/util.ts | 116 +++++++++++++--- contracts/yarn.lock | 42 +++--- .../subscriber/box_subscriber.rs | 1 + 8 files changed, 321 insertions(+), 77 deletions(-) diff --git a/collator/tests/collation_tests.rs b/collator/tests/collation_tests.rs index 815ae19b3e..7151ef8a28 100644 --- a/collator/tests/collation_tests.rs +++ b/collator/tests/collation_tests.rs @@ -26,7 +26,6 @@ use tycho_core::global_config::ZerostateId; use tycho_core::node::NodeKeys; use tycho_core::storage::CoreStorage; use tycho_crypto::ed25519; -use tycho_slasher_traits::NoopValidatorEventsListener; use tycho_storage::StorageContext; use tycho_types::models::{BlockId, BlockIdShort, ShardIdent}; diff --git a/contracts/package.json b/contracts/package.json index 0283c35401..2227863ccf 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -9,18 +9,18 @@ "release": "blueprint pack && npm publish --access public" }, "dependencies": { - "@ton/core": "~0" + "@ton/core": ">=0.62.0" }, "devDependencies": { "@tact-lang/compiler": "1.6.13", "@ton-community/func-js": ">=0.10.0", "@ton/blueprint": ">=0.40.0", "@ton/crypto": "^3.3.0", - "@ton/sandbox": ">=0.37.0", + "@ton/sandbox": ">=0.39.0", "@ton/test-utils": ">=0.11.0", "@ton/tolk-js": ">=1.0.0", "@ton/ton": ">=15.2.1 <16.0.0", - "@tychosdk/emulator": "^0.2.0", + "@tychosdk/emulator": "^0.2.6", "@types/jest": "^30.0.0", "@types/node": "^22.15.32", "arg": "^5.0.2", diff --git a/contracts/src/slasher-stub.tolk b/contracts/src/slasher-stub.tolk index 424dc43ffd..daa027fd58 100644 --- a/contracts/src/slasher-stub.tolk +++ b/contracts/src/slasher-stub.tolk @@ -8,6 +8,7 @@ const ERROR_REPLAY_PROTECTION = 52 const ERROR_MESSAGE_EXPIRED = 57 const ERROR_INVALID_BLOCKS_BATCH = 100 const ERROR_NO_SLASHER_CONFIG = 101 +const ERROR_NO_PREV_BLOCK_ID = 102 const REPLAY_OFFSET_MS = 5000 const FUTURE_OFFSET_SEC = 60 @@ -31,15 +32,24 @@ const PARAM_IDX_SLASHER_PARAMS = 666 struct (0x01) SlasherParams { address: bits256 - blocks_batch_size: uint8 + blocksBatchSize: uint8 } // // === Getters === // -get fun is_blocks_batch_valid(batch: cell): bool { +get fun is_blocks_batch_valid(batch: cell, mcSeqno: int): bool { val params = loadSlasherParams(); - return validateBlocksBatch(batch.beginParse(), params.blocks_batch_size); + val vset = lazy ValidatorSet.fromCell(blockchain.configParam(PARAM_IDX_CURRENT_VSET)!); + val validatorCount = min(vset.total, vset.main); + return validateBlocksBatch( + batch.beginParse(), + { + batchSize: params.blocksBatchSize, + mcSeqno, + validatorCount, + } + ); } // @@ -56,30 +66,55 @@ fun onExternalMessage(inMsg: slice) { val batch = inMsg.loadRef(); inMsg.assertEnd(); assert(blockchain.now() <= expireAtSec, ERROR_MESSAGE_EXPIRED); - val params = loadSlasherParams(); - assert(validateBlocksBatch(batch.beginParse(), params.blocks_batch_size), ERROR_INVALID_BLOCKS_BATCH); + + val toSign = beginCell().storeSlice(signedBody).endCell(); + val vset = lazy ValidatorSet.fromCell(blockchain.configParam(PARAM_IDX_CURRENT_VSET)!); + var (validatorCs, validatorFound) = vset.list.uDictGet(16, validatorIdx); + assert(validatorFound, ERROR_VALIDATOR_NOT_FOUND); + val validatorPubkey = ValidatorDescr.readPubkeyOnly(validatorCs!); + assert(isSignatureValid(toSign.hash(), signature, validatorPubkey), ERROR_INVALID_SIGNATURE); + + val validatorCount = min(vset.total, vset.main); + + val batchSize = loadSlasherParams().blocksBatchSize; + val mcSeqno = blockchain.prevMcSeqno() + 1; + assert(validateBlocksBatch( + batch.beginParse(), + { + batchSize, + validatorCount, + mcSeqno, + } + ), ERROR_INVALID_BLOCKS_BATCH); var data = Storage.load(); assert(createdAtMs > (data.updatedAtMs - REPLAY_OFFSET_MS) && createdAtMs <= (blockchain.now() + FUTURE_OFFSET_SEC) * 1000, ERROR_REPLAY_PROTECTION); - var validatorCs = CurrentVset.getValidatorDescription(validatorIdx); - assert(validatorCs != null, ERROR_VALIDATOR_NOT_FOUND); - val validator = ValidatorDescr.readFromSlice(mutate validatorCs); - - val toSign = beginCell().storeSlice(signedBody).endCell(); - assert(isSignatureValid(toSign.hash(), signature, validator.pubkey), ERROR_INVALID_SIGNATURE); - data.updatedAtMs = max(createdAtMs, data.updatedAtMs); data.save(); acceptExternalMessage(); } -fun validateBlocksBatch(batch: slice, batch_size: int): bool { - // TODO: Assert that start seqno is recent enough and not from the future. - val _startSeqno = batch.loadUint(32); - batch.skipBits(batch_size); +struct ValidateBlocksBatchParams { + batchSize: int + mcSeqno: int + validatorCount: int +} + +fun validateBlocksBatch(batch: slice, params: ValidateBlocksBatchParams): bool { + val startSeqno = batch.loadUint(32); + if (startSeqno + params.batchSize >= params.mcSeqno) { + // Batch contains blocks that were not produced yet. + return false; + } + if (startSeqno + params.batchSize * 2 < params.mcSeqno) { + // Batch contains too old blocks. + return false; + } + + batch.skipBits(params.batchSize); val history = batch.loadRef() as dict; if (!batch.isEmpty()) { return false; @@ -91,9 +126,12 @@ fun validateBlocksBatch(batch: slice, batch_size: int): bool { if (found) { iterNext = validatorIdx!; - // TODO: Check that validator idx is in the mc validators range. + if (validatorIdx! >= params.validatorCount) { + return false; + } + val (csBits, csRefs) = cs!.remainingBitsAndRefsCount(); - if (csBits != batch_size * 2 || csRefs != 0) { + if (csBits != params.batchSize * 2 || csRefs != 0) { return false; } } @@ -107,3 +145,24 @@ fun loadSlasherParams(): SlasherParams { assert(param != null, ERROR_NO_SLASHER_CONFIG); return SlasherParams.fromCell(param); } + +fun blockchain.prevMcSeqno(): int { + val prevBlocks = blockchain.prevMcBlocks(); + assert(prevBlocks != null, ERROR_NO_PREV_BLOCK_ID); + // Item at index 0 is the latest mc seqno. + // Inner item at index 2 is a seqno of the block id. + return prevBlocks.0.2; +} + +fun ValidatorDescr.readPubkeyOnly(s: slice): int { + val tag = s.loadUint(8); + assert((tag & ~0x20) == VALIDATOR_DESCR_TAG_SIMPLE, ERROR_INVALID_VALIDATOR_DESCR); + assert(s.loadUint(32) == PUBKEY_TAG_ED25519, ERROR_INVALID_VALIDATOR_DESCR); + return s.loadUint(256); +} + +@pure +fun blockchain.prevMcBlocks(): [BlockId] | null + asm "PREVMCBLOCKS" + +type BlockId = [int, int, int, int, int] diff --git a/contracts/tests/Slasher.spec.ts b/contracts/tests/Slasher.spec.ts index 9d6f74682a..a3a7343be8 100644 --- a/contracts/tests/Slasher.spec.ts +++ b/contracts/tests/Slasher.spec.ts @@ -1,13 +1,20 @@ +import assert from "assert"; import { compile } from "@ton/blueprint"; import { address, beginCell, + BitString, Cell, Dictionary, - OpenedContract, toNano, } from "@ton/core"; import { Blockchain, createShardAccount, SmartContract } from "@ton/sandbox"; +import { + getSecureRandomBytes, + KeyPair, + keyPairFromSeed, + sign, +} from "@ton/crypto"; import { TychoExecutor } from "@tychosdk/emulator"; import { PARAM_IDX_SLASHER_PARAMS, @@ -15,26 +22,39 @@ import { storeSlasherParams, storeSlasherStubData, } from "../wrappers/SlasherStub"; +import { + bufferToBigInt, + ConfigParams, + makeStubValidatorSet, + ValidatorDescrValue, +} from "../wrappers/util"; const SLASHER_ADDR = address( "-1:6666666666666666666666666666666666666666666666666666666666666666", ); const BLOCKS_BATCH_SIZE = 10; +const SAMPLE_BLOCKS_BATCH = Cell.fromBase64( + "te6ccgEBCAEAMAABCwAAAObYYAECAswFAgIBIAQDAAfRCgDAAAdpRQBgAgEgBwYAB2UFAGAAB/SKAMA=", +); + describe("Slasher", () => { let config: Cell; let code: Cell; let executor: TychoExecutor; let blockchain: Blockchain; let slasher: SmartContract; + let keypair: KeyPair; beforeAll(async () => { - const parsedConfig = Dictionary.loadDirect( - Dictionary.Keys.Uint(32), - Dictionary.Values.Cell(), - TychoExecutor.defaultConfig, - ); - parsedConfig.set( + keypair = await getSecureRandomBytes(32).then(keyPairFromSeed); + + const params = new ConfigParams(TychoExecutor.defaultConfig); + params.setSignatureModifiers({ + signatureWithId: false, + signatureDomain: false, + }); + params.setRaw( PARAM_IDX_SLASHER_PARAMS, beginCell() .store( @@ -45,7 +65,28 @@ describe("Slasher", () => { ) .endCell(), ); - config = beginCell().storeDictDirect(parsedConfig).endCell(); + + const vset = await makeStubValidatorSet({ + utimeSince: 0, + utimeUntil: 1 << 30, + validatorCount: 13, + }); + vset.validators.set(0, { + pubkey: bufferToBigInt(keypair.publicKey), + weight: 1n, + adnlAddr: null, + }); + params.setCurrentVset(vset); + + const fundamentalAddresses = Dictionary.load( + Dictionary.Keys.Buffer(32), + Dictionary.Values.BitString(0), + params.getRaw(31)!, + ); + fundamentalAddresses.set(SLASHER_ADDR.hash, BitString.EMPTY); + params.setRaw(31, beginCell().storeDict(fundamentalAddresses).endCell()); + + config = params.toCell(); code = await compile("SlasherStub", { debugInfo: true }); executor = await TychoExecutor.create(); @@ -75,21 +116,70 @@ describe("Slasher", () => { ); slasher = await blockchain.getContract(SLASHER_ADDR); - await blockchain.setVerbosityForAddress(slasher.address, { - blockchainLogs: true, - debugLogs: true, - // vmLogs: "vm_logs_full", - }); }); it("should accept valid blocks batch", async () => { - const { isValid } = await getters(blockchain, slasher).isBlocksBatchValid( - Cell.fromBase64( - "te6ccgEBCAEAMAABCwAAAObYYAECAswFAgIBIAQDAAfRCgDAAAdpRQBgAgEgBwYAB2UFAGAAB/SKAMA=", - ), - ); + const { isValid } = await getters(blockchain, slasher).isBlocksBatchValid({ + blocksBatch: SAMPLE_BLOCKS_BATCH, + mcSeqno: 241, + }); expect(isValid).toBe(true); }); + + it("should accept valid messages", async () => { + const now = 10000000; + blockchain.now = now; + + const nowMs = now * 1000 + 500; + const expireAt = ~~(nowMs / 1000) + 60; + + const bodyToSign = beginCell() + .storeUint(nowMs, 64) + .storeUint(expireAt, 32) + .storeUint(0, 16) + .storeRef(SAMPLE_BLOCKS_BATCH) + .endCell(); + const signature = sign(bodyToSign.hash(), keypair.secretKey); + const body = beginCell() + .storeBuffer(signature, 64) + .storeSlice(bodyToSign.asSlice()) + .endCell(); + + // slasher.setVerbosity({ + // blockchainLogs: true, + // debugLogs: true, + // vmLogs: "vm_logs_full", + // }); + + blockchain.prevBlocks = { + lastMcBlocks: [ + { + workchain: -1, + shard: 1n << 63n, + seqno: 241, + rootHash: Buffer.alloc(32), + fileHash: Buffer.alloc(32), + }, + ], + prevKeyBlock: { + workchain: -1, + shard: 1n << 63n, + seqno: 0, + rootHash: Buffer.alloc(32), + fileHash: Buffer.alloc(32), + }, + }; + const tx = await slasher.receiveMessage({ + info: { + type: "external-in", + dest: SLASHER_ADDR, + importFee: 0n, + }, + body, + }); + assert(tx.description.type === "generic"); + expect(tx.description.aborted).toBe(false); + }); }); function getters(blockchain: Blockchain, slasher: SmartContract) { diff --git a/contracts/wrappers/SlasherStub.ts b/contracts/wrappers/SlasherStub.ts index 93c6ac1e0d..1d7e54c16f 100644 --- a/contracts/wrappers/SlasherStub.ts +++ b/contracts/wrappers/SlasherStub.ts @@ -66,11 +66,18 @@ export class SlasherStub implements Contract { return new SlasherStub(address); } - async isBlocksBatchValid(provider: ContractProvider, blocksBatch: Cell) { + async isBlocksBatchValid( + provider: ContractProvider, + args: { blocksBatch: Cell; mcSeqno: number }, + ) { const { stack } = await provider.get("is_blocks_batch_valid", [ { type: "cell", - cell: blocksBatch, + cell: args.blocksBatch, + }, + { + type: "int", + value: BigInt(args.mcSeqno), }, ]); return { diff --git a/contracts/wrappers/util.ts b/contracts/wrappers/util.ts index a2199bd382..490cc3e340 100644 --- a/contracts/wrappers/util.ts +++ b/contracts/wrappers/util.ts @@ -5,6 +5,7 @@ import { Builder, Cell, Dictionary, + DictionaryValue, Message, Slice, toNano, @@ -32,6 +33,60 @@ export class ConfigParams { }; } + setRaw(idx: number, value: Cell) { + this.dict.set(idx, value); + } + + getRaw(idx: number): Cell | undefined { + return this.dict.get(idx); + } + + setSignatureModifiers(args: { + signatureWithId: boolean; + signatureDomain: boolean; + }) { + const GLOBAL_VERSION_TAG = 0xc4; + const SIGNATURE_WITH_ID_FLAG = 0x4000000n; + const SIGNATURE_DOMAIN_FLAG = 0x800000000n; + + const setFlag = (value: bigint, flag: bigint, set: boolean) => { + if (set) { + return value | flag; + } else { + return value & ~flag; + } + }; + + const global = this.dict.get(8)?.asSlice(); + if (global == null) { + return; + } + + const tag = global.loadUint(8); + if (tag != GLOBAL_VERSION_TAG) { + throw new UnknownTagError({ tag, bits: 8 }); + } + const version = global.loadUint(32); + let capabilities = global.loadUintBig(64); + capabilities = setFlag( + capabilities, + SIGNATURE_WITH_ID_FLAG, + args.signatureWithId, + ); + capabilities = setFlag( + capabilities, + SIGNATURE_DOMAIN_FLAG, + args.signatureDomain, + ); + + const newGlobal = beginCell() + .storeUint(tag, 8) + .storeUint(version, 32) + .storeUint(capabilities, 64) + .endCell(); + this.dict.set(8, newGlobal); + } + getVsetTimings(): VsetTimings { const cell = this.dict.get(15)!; const cs = cell.beginParse(); @@ -139,10 +194,10 @@ export async function makeStubValidatorSet(args: { utimeUntil: number; validatorCount: number; }): Promise { - let validators = Dictionary.empty(Dictionary.Keys.Uint(16), { - parse: loadValidatorDescr, - serialize: storeValidatorDescr, - }); + let validators = Dictionary.empty( + Dictionary.Keys.Uint(16), + ValidatorDescrValue, + ); for (let i = 0; i < args.validatorCount; i++) { validators.set(i, { @@ -167,20 +222,29 @@ export function loadValidatorSet(cs: Slice): ValidatorSet { throw new UnknownTagError({ tag, bits: 8 }); } + const utimeSince = cs.loadUint(32); + const utimeUntil = cs.loadUint(32); + const total = cs.loadUint(16); + const main = cs.loadUint(16); + const totalWeight = cs.loadUintBig(64); + const validators = cs.loadDict(Dictionary.Keys.Uint(16), ValidatorDescrValue); + if (validators.size != total) { + throw new Error( + `validator count mismatch: expected=${total}, got=${validators.size}`, + ); + } + return { - utimeSince: cs.loadUint(32), - utimeUntil: cs.loadUint(32), - main: cs.loadUint(16), - totalWeight: cs.loadUintBig(64), - validators: cs.loadDict(Dictionary.Keys.Uint(16), { - parse: loadValidatorDescr, - serialize: storeValidatorDescr, - }), + utimeSince, + utimeUntil, + main, + totalWeight, + validators, }; } export function storeValidatorSet( - vset: ValidatorSet + vset: ValidatorSet, ): (builder: Builder) => void { return (builder: Builder) => { builder.storeUint(VALIDATOR_SET_TAG, 8); @@ -194,9 +258,18 @@ export function storeValidatorSet( } const VALIDATOR_DESCR_TAG_SIMPLE = 0x53; -const VALIDATOR_DESCR_TAG_WITH_ADDR = 0x53; +const VALIDATOR_DESCR_TAG_WITH_ADDR = 0x73; const PUBKEY_TAG_ED25519 = 0x8e81278a; +export const ValidatorDescrValue: DictionaryValue = { + serialize: (src, builder) => builder.store(storeValidatorDescr(src)), + parse: (cs) => { + const res = loadValidatorDescr(cs); + cs.endParse(); + return res; + }, +}; + export type ValidatorDescr = { pubkey: bigint; weight: bigint; @@ -212,8 +285,13 @@ export function loadValidatorDescr(cs: Slice): ValidatorDescr { default: throw new UnknownTagError({ tag, bits: 8 }); } - const withAddr = tag === VALIDATOR_DESCR_TAG_WITH_ADDR; + + const pubkeyTag = cs.loadUint(32); + if (pubkeyTag != PUBKEY_TAG_ED25519) { + throw new UnknownTagError({ tag: pubkeyTag, bits: 32 }); + } + return { pubkey: cs.loadUintBig(256), weight: cs.loadUintBig(64), @@ -222,14 +300,14 @@ export function loadValidatorDescr(cs: Slice): ValidatorDescr { } export function storeValidatorDescr( - d: ValidatorDescr + d: ValidatorDescr, ): (builder: Builder) => void { return (builder: Builder) => { builder.storeUint( d.adnlAddr != null ? VALIDATOR_DESCR_TAG_WITH_ADDR : VALIDATOR_DESCR_TAG_SIMPLE, - 8 + 8, ); builder.storeUint(PUBKEY_TAG_ED25519, 32); builder.storeUint(d.pubkey, 256); @@ -283,7 +361,7 @@ export class ValidatorAccount { public createStakeMessage( electorAddress: Address, - args: ParticipateInElections + args: ParticipateInElections, ): Message { return simpleInternal({ src: this.address, @@ -370,7 +448,7 @@ export class UnknownTagError extends Error { super( `unknown tag 0x${args.tag .toString(16) - .padStart(Math.ceil(args.bits / 8), "0")}` + .padStart(Math.ceil(args.bits / 8), "0")}`, ); } } diff --git a/contracts/yarn.lock b/contracts/yarn.lock index ef19805c59..7d010efb0e 100644 --- a/contracts/yarn.lock +++ b/contracts/yarn.lock @@ -786,14 +786,19 @@ ton-lite-client "^3.1.1" ts-node "^10.9.1" -"@ton/core@0.60.1", "@ton/core@^0.60.1": +"@ton/core@0.60.1": version "0.60.1" resolved "https://registry.yarnpkg.com/@ton/core/-/core-0.60.1.tgz#cc9a62fb308d7597b1217dc8e44c7e2dcc0aceaa" integrity sha512-8FwybYbfkk57C3l9gvnlRhRBHbLYmeu0LbB1z9N+dhDz0Z+FJW8w0TJlks8CgHrAFxsT3FlR2LsqFnsauMp38w== dependencies: symbol.inspect "1.0.1" -"@ton/core@^0.61.0", "@ton/core@~0": +"@ton/core@>=0.62.0": + version "0.63.0" + resolved "https://registry.yarnpkg.com/@ton/core/-/core-0.63.0.tgz#87487072f707d8ac7fc00983d5aa707409728c07" + integrity sha512-uBc0WQNYVzjAwPvIazf0Ryhpv4nJd4dKIuHoj766gUdwe8sVzGM+TxKKKJETL70hh/mxACyUlR4tAwN0LWDNow== + +"@ton/core@^0.61.0": version "0.61.0" resolved "https://registry.yarnpkg.com/@ton/core/-/core-0.61.0.tgz#09b37801cb2f5a942020fcc992be1e99f4b16689" integrity sha512-0qyVfP2dDue2bq80ydXggo2MlufcmzuFk6G94qRrZxvyQ3NSe4UeBTeRf1gQmN7tywgTsX2gS61e4yvJrlUu4Q== @@ -816,16 +821,16 @@ jssha "3.2.0" tweetnacl "1.0.3" -"@ton/sandbox@>=0.37.0": - version "0.37.0" - resolved "https://registry.yarnpkg.com/@ton/sandbox/-/sandbox-0.37.0.tgz#8315977379fabeaee8398dd89a72e1a0fb2ffd9f" - integrity sha512-1WK79g2cksOJPLsGtF/U8eZwSjw92jw7Jzb6R0wzUfWwZ8S9hEyQZcevkU8FOVDMWXJO84i+8is8GW8TsYZpFg== +"@ton/sandbox@>=0.39.0": + version "0.41.0" + resolved "https://registry.yarnpkg.com/@ton/sandbox/-/sandbox-0.41.0.tgz#a0c81cca5dedb1891e1cf3f621f730f6bac63539" + integrity sha512-+WRWiHfm62xQebVt6BvLb2UhVphpBHCwSby8R5vP9llzdVck+XEs+p4csIkZBh6gRQsy1Xomzh1PpgZS5XVE3A== dependencies: "@vscode/debugadapter" "^1.68.0" chalk "^4.1.2" fflate "^0.8.2" table "^6.9.0" - ton-assembly "0.1.0" + ton-assembly "0.6.1" "@ton/sandbox@^0.32.2": version "0.32.2" @@ -923,10 +928,10 @@ dependencies: tslib "^2.4.0" -"@tychosdk/emulator@^0.2.0": - version "0.2.0" - resolved "https://registry.yarnpkg.com/@tychosdk/emulator/-/emulator-0.2.0.tgz#d1d28542d28b13b087e278924acdaa96002928f1" - integrity sha512-hyvd/iCqjUfBArgR/BjY3znjVSPB4xeDnzQi9rnf4SaMfKqzuIBk0jK02Y3g/n5egy02Hh+PfWeUoKr029z6zw== +"@tychosdk/emulator@^0.2.6": + version "0.2.6" + resolved "https://registry.yarnpkg.com/@tychosdk/emulator/-/emulator-0.2.6.tgz#5f52aaddd71d8c14a5cffe82b400eacd41ae01ad" + integrity sha512-03pJ9RroOpyVQ7006Ib7YSYeQBXS3ZeLHriU53jliKR0hZQmU+JqxFyKpa2DAvzM+G8VYTvBKWTwsH1XVdkUlw== dependencies: axios "^1.8.4" zod "^3.24.2" @@ -3351,14 +3356,14 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" -ton-assembly@0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/ton-assembly/-/ton-assembly-0.1.0.tgz#a3f1da50c2d2be1cd0f9837a9399ea80f80021eb" - integrity sha512-2k0vIKleGMXXu6yXV8L1eICClGZVwqfs4G0FgBCgC6Qgani6pAqoLGNBNjcdw3554Ua3E+1K4oz/oW+GhZfm+w== +ton-assembly@0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/ton-assembly/-/ton-assembly-0.6.1.tgz#ad8c48e317b4dcc71903d17515275ecf9fcdb8a6" + integrity sha512-HZNDD2Cy8DQ9UY+8eCgCFY9RBnHEA+Abxo6chDtZQqsX1xo97UZzhMWzK+bYoe1w9gsGugi+1kOH7cpjzDN6jQ== dependencies: - "@ton/core" "^0.60.1" "@tonstudio/parser-runtime" "^0.0.1" cac "^6.7.14" + ton-source-map "^0.2.2" ton-lite-client@^3.1.1: version "3.1.1" @@ -3372,6 +3377,11 @@ ton-lite-client@^3.1.1: ton-tl "^1.0.1" tweetnacl "^1.0.3" +ton-source-map@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/ton-source-map/-/ton-source-map-0.2.2.tgz#a7b647a085d23a05172b26c110d7197ab4446f9a" + integrity sha512-T9as2Cmv5aqFbELd0ZxIyY3NRPGxf3ltpVN8rm+uIXMMDlNaGW3Wf6jFcaJYwkRNB2eR52PhNbt5tI5lwgL1Cg== + ton-tl@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/ton-tl/-/ton-tl-1.0.1.tgz#210756ca6a136a0f405c29733dce182c4e1fc1f6" diff --git a/core/src/block_strider/subscriber/box_subscriber.rs b/core/src/block_strider/subscriber/box_subscriber.rs index 328c22bd1e..25f5b92e48 100644 --- a/core/src/block_strider/subscriber/box_subscriber.rs +++ b/core/src/block_strider/subscriber/box_subscriber.rs @@ -246,6 +246,7 @@ mod tests { mc_block_id: Default::default(), mc_is_key_block: false, is_key_block: false, + is_top_block: true, block: BlockStuff::new_empty(ShardIdent::MASTERCHAIN, 0), archive_data: ArchiveData::Existing, delayed: DelayedTasks::new().1, From ff207d0cb9cf1e17fd10a9b6c3d5e69b2af3cbbb Mon Sep 17 00:00:00 2001 From: MrWad3r Date: Tue, 24 Feb 2026 20:00:59 +0100 Subject: [PATCH 08/21] chore(core): change ValidationSessionId signature --- collator/src/manager/mod.rs | 18 +++++++---- collator/src/types.rs | 25 +++++++++----- collator/src/validator/impls/std_impl/mod.rs | 2 +- collator/src/validator/mod.rs | 2 +- collator/tests/validator_tests.rs | 10 ++++-- slasher-traits/src/validator.rs | 16 +++++---- slasher/src/lib.rs | 22 +++++++++---- slasher/src/storage/db.rs | 4 +-- slasher/src/storage/mod.rs | 11 +++---- slasher/src/util.rs | 34 ++++---------------- 10 files changed, 75 insertions(+), 69 deletions(-) diff --git a/collator/src/manager/mod.rs b/collator/src/manager/mod.rs index ad0c4e5607..73795a99eb 100644 --- a/collator/src/manager/mod.rs +++ b/collator/src/manager/mod.rs @@ -2328,7 +2328,9 @@ where self.apply_split_merge_actions(&new_shards_info)?; // find out the actual collation session start round from master state - let current_session_seqno = mc_data.validator_info.catchain_seqno; + let catchain_seqno = mc_data.validator_info.catchain_seqno; + let vset_switch_round = mc_data.consensus_info.vset_switch_round; + let validation_session_id = (catchain_seqno, vset_switch_round); // we need full validators set to define the subset for each session and to check if current node should collate let full_validators_set = mc_data.config.get_current_validator_set()?; @@ -2350,11 +2352,11 @@ where } hash_map::Entry::Vacant(entry) => { let (subset, hash_short) = full_validators_set - .compute_mc_subset_indexed(current_session_seqno, collation_config.shuffle_mc_validators) + .compute_mc_subset_indexed(catchain_seqno, collation_config.shuffle_mc_validators) .ok_or_else(|| anyhow!( - "Error calculating subset of validators for session (shard_id = {}, seqno = {})", + "Error calculating subset of validators for catchain session (shard_id = {}, seqno = {})", ShardIdent::MASTERCHAIN, - current_session_seqno, + catchain_seqno, ))?; let subset: FastHashMap<_, _> = subset @@ -2409,7 +2411,8 @@ where if local_pubkey.is_some() { // start new session when seqno changed or subset changed for the same seqno if existing_session_info.collators().short_hash == hash_short - && existing_session_info.seqno() == current_session_seqno + && existing_session_info.get_validation_session_id() + == validation_session_id { sessions_to_keep.push((shard_id, existing_session_info, block_ids)); } else { @@ -2449,7 +2452,7 @@ where tracing::info!( target: tracing_targets::COLLATION_MANAGER, "Will start new collation sessions: {:?}", - DebugIter(sessions_to_start.iter().map(|(s, _)| (s, current_session_seqno))), + DebugIter(sessions_to_start.iter().map(|(s, _)| (s, validation_session_id))), ); } @@ -2460,7 +2463,8 @@ where let new_session_info = Arc::new(CollationSessionInfo::new( shard_id, - current_session_seqno, + validation_session_id.0, + validation_session_id.1, ValidatorSubsetInfo { validators: subset.values().cloned().collect(), short_hash: hash_short, diff --git a/collator/src/types.rs b/collator/src/types.rs index 7e6bbe6669..231acfccc3 100644 --- a/collator/src/types.rs +++ b/collator/src/types.rs @@ -354,39 +354,48 @@ pub(crate) type CollationSessionId = (ShardIdent, u32, u32); #[derive(Clone)] pub struct CollationSessionInfo { shard: ShardIdent, - /// Sequence number of the collation session - seqno: u32, + vset_switch_round: u32, + catchain_seqno: u32, collators: ValidatorSubsetInfo, current_collator_keypair: Option>, } + +// #[derive(Clone, PartialEq, Eq)] +// pub struct SessionId { +// pub vset_switch_round: u32, +// pub catchain_seqno: u32, +// } + impl CollationSessionInfo { pub fn new( shard: ShardIdent, - seqno: u32, + catchain_seqno: u32, + vset_switch_round: u32, collators: ValidatorSubsetInfo, current_collator_keypair: Option>, ) -> Self { Self { shard, - seqno, + vset_switch_round, + catchain_seqno, collators, current_collator_keypair, } } pub fn id(&self) -> CollationSessionId { - (self.shard, self.seqno, self.collators.short_hash) + (self.shard, self.catchain_seqno, self.collators.short_hash) } pub fn get_validation_session_id(&self) -> ValidationSessionId { - (self.seqno, self.collators.short_hash) + (self.vset_switch_round, self.catchain_seqno) } pub fn shard(&self) -> ShardIdent { self.shard } pub fn seqno(&self) -> u32 { - self.seqno + self.catchain_seqno } pub fn collators(&self) -> &ValidatorSubsetInfo { @@ -401,7 +410,7 @@ impl fmt::Debug for CollationSessionInfo { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("CollationSessionInfo") .field("shard", &self.shard) - .field("seqno", &self.seqno) + .field("catchain_seqno", &self.catchain_seqno) .field("collators", &self.collators) .field( "current_collator_pubkey", diff --git a/collator/src/validator/impls/std_impl/mod.rs b/collator/src/validator/impls/std_impl/mod.rs index cc1a3975a5..d32c4ad722 100644 --- a/collator/src/validator/impls/std_impl/mod.rs +++ b/collator/src/validator/impls/std_impl/mod.rs @@ -225,5 +225,5 @@ struct Inner { } type Sessions = FastHashMap; -/// We use `IndexMap` because "subset short hash" component of session id is not sequential +/// We use `IndexMap` to keep deterministic insertion order for latest-session scans. type ShardSessions = IndexMap; diff --git a/collator/src/validator/mod.rs b/collator/src/validator/mod.rs index 65c40f910e..02f9659101 100644 --- a/collator/src/validator/mod.rs +++ b/collator/src/validator/mod.rs @@ -52,7 +52,7 @@ pub struct ValidatorNetworkContext { pub zerostate_id: BlockId, } -/// (seqno, subset `short_hash`) +/// (`vset_switch_round`, `catchain_seqno`) pub type ValidationSessionId = (u32, u32); pub trait CompositeValidationSessionId { diff --git a/collator/tests/validator_tests.rs b/collator/tests/validator_tests.rs index d352685bee..1cb278a03e 100644 --- a/collator/tests/validator_tests.rs +++ b/collator/tests/validator_tests.rs @@ -128,7 +128,9 @@ async fn validator_signatures_match() -> Result<()> { ..zerostate_id }; for session_seqno in (0..).step_by(1000).take(SESSION_COUNT) { - let session_id = (session_seqno, 0); + let vset_switch_round = session_seqno + 100; + let catchain_seqno = session_seqno + 200; + let session_id = (catchain_seqno, vset_switch_round); tracing::info!(?session_id, %block_id, "adding session"); @@ -242,7 +244,9 @@ async fn malicious_validators_are_ignored() -> Result<()> { ..zerostate_id }; for session_seqno in (0..).step_by(1000).take(SESSION_COUNT) { - let session_id = (session_seqno, 0); + let vset_switch_round = session_seqno + 100; + let catchain_seqno = session_seqno + 200; + let session_id = (catchain_seqno, vset_switch_round); tracing::info!(?session_id, %block_id, "adding session"); @@ -380,7 +384,7 @@ async fn network_gets_stuck_without_signatures() -> Result<()> { seqno: 1, ..zerostate_id }; - let session_id = (0, 0); + let session_id = (100, 200); let validators = make_description(block_id.seqno, &nodes); for node in &nodes { diff --git a/slasher-traits/src/validator.rs b/slasher-traits/src/validator.rs index 283fbad79f..433794df80 100644 --- a/slasher-traits/src/validator.rs +++ b/slasher-traits/src/validator.rs @@ -9,10 +9,10 @@ use tycho_util::FastHasherState; // TODO: Decide how to be with this collator-defined type #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct ValidationSessionId { - /// Validation round seqno. - pub seqno: u32, - /// Validator subset short seqno. - pub short_hash: u32, + /// Incremental sequence number. + // pub seqno: u32, + pub vset_switch_round: u32, + pub catchain_seqno: u32, } // TEMP @@ -20,8 +20,9 @@ impl From<(u32, u32)> for ValidationSessionId { #[inline] fn from(value: (u32, u32)) -> Self { Self { - seqno: value.0, - short_hash: value.1, + // seqno: value.0, + vset_switch_round: value.0, + catchain_seqno: value.1, } } } @@ -30,7 +31,8 @@ impl From<(u32, u32)> for ValidationSessionId { impl Ord for ValidationSessionId { #[inline] fn cmp(&self, other: &Self) -> std::cmp::Ordering { - (self.seqno, self.short_hash).cmp(&(other.seqno, other.short_hash)) + (self.vset_switch_round, self.catchain_seqno) + .cmp(&(other.vset_switch_round, other.catchain_seqno)) } } diff --git a/slasher/src/lib.rs b/slasher/src/lib.rs index 7f1809fb22..8e818f5979 100644 --- a/slasher/src/lib.rs +++ b/slasher/src/lib.rs @@ -98,8 +98,8 @@ impl Slasher { blockchain_rpc_client, storage, known_session_id: AtomicValidationSessionId::new(ValidationSessionId { - seqno: 0, - short_hash: 0, + vset_switch_round: 0, + catchain_seqno: 0, }), signature_context: ArcSwap::new(Arc::new(SignatureContext { global_id: 0, @@ -144,15 +144,25 @@ impl Slasher { .set_default_batch_size(slasher_params.blocks_batch_size); let slasher_address = StdAddr::new_masterchain(slasher_params.address); - let session_id_from_block = ValidationSessionId { - seqno: state_extra.validator_info.catchain_seqno, - short_hash: state_extra.validator_info.validator_list_hash_short, + let catchain_seqno = state_extra.validator_info.catchain_seqno; + let vset_switch_round = state_extra.consensus_info.vset_switch_round; + + let known_session_id = this.known_session_id.load(); + let session_id_from_block = if known_session_id.vset_switch_round == vset_switch_round + && known_session_id.catchain_seqno == catchain_seqno + { + known_session_id + } else { + ValidationSessionId { + vset_switch_round, + catchain_seqno, + } }; tracing::trace!(?slasher_params, ?session_id_from_block); // Clear old sessions if needed // TODO: Add metrics. - if session_id_from_block != this.known_session_id.load() { + if session_id_from_block != known_session_id { let span = tracing::Span::current(); let storage = this.storage.clone(); tokio::task::spawn_blocking(move || { diff --git a/slasher/src/storage/db.rs b/slasher/src/storage/db.rs index 78b2c29f21..88bab8c7ab 100644 --- a/slasher/src/storage/db.rs +++ b/slasher/src/storage/db.rs @@ -61,8 +61,8 @@ pub mod tables { } } - /// Code hash with account address - /// - Key: `session_id: (u32 BE, u32 BE), validator_idx: u16 BE, start_block: u32 BE` + /// Block batches submitted by validators + /// - Key: `session_id: (seqno u32 BE, vset_switch_round u32 BE, catchain_seqno u32 BE), validator_idx: u16 BE, start_block: u32 BE` /// - Value: blocks batch pub struct BlockBatches; diff --git a/slasher/src/storage/mod.rs b/slasher/src/storage/mod.rs index d0549fc9c6..ead243f446 100644 --- a/slasher/src/storage/mod.rs +++ b/slasher/src/storage/mod.rs @@ -48,8 +48,9 @@ impl SlasherStorage { batch: &BlocksBatch, ) -> Result<()> { let mut key = [0u8; tables::BlockBatches::KEY_LEN]; - key[0..4].copy_from_slice(&session_id.seqno.to_be_bytes()); - key[4..8].copy_from_slice(&session_id.short_hash.to_be_bytes()); + // key[0..4].copy_from_slice(&session_id.seqno.to_be_bytes()); + key[0..4].copy_from_slice(&session_id.catchain_seqno.to_be_bytes()); + key[4..8].copy_from_slice(&session_id.vset_switch_round.to_be_bytes()); key[8..10].copy_from_slice(&validator_idx.to_be_bytes()); key[10..14].copy_from_slice(&batch.start_seqno.to_be_bytes()); @@ -60,14 +61,12 @@ impl SlasherStorage { } /// Removes all block batches for sessions BEFORE the specified. - /// - /// NOTE: Does not touch "rotated" sessions (same seqno but different `short_hash`), - /// because we cannot order them properly. pub fn remove_outdated_batches(&self, latest_session_id: ValidationSessionId) -> Result<()> { let db = &self.inner.db; let mut key = [0u8; tables::BlockBatches::KEY_LEN]; - key[0..4].copy_from_slice(&latest_session_id.seqno.to_be_bytes()); + key[0..4].copy_from_slice(&latest_session_id.catchain_seqno.to_be_bytes()); + key[4..8].copy_from_slice(&latest_session_id.vset_switch_round.to_be_bytes()); db.rocksdb().delete_range_cf_opt( &db.block_batches.cf(), diff --git a/slasher/src/util.rs b/slasher/src/util.rs index 92d60d888d..8ed464cc38 100644 --- a/slasher/src/util.rs +++ b/slasher/src/util.rs @@ -1,47 +1,25 @@ use std::ptr::NonNull; -use std::sync::atomic::{AtomicU64, Ordering}; +use parking_lot::RwLock; use tl_proto::{TlError, TlRead, TlResult, TlWrite}; use tycho_slasher_traits::ValidationSessionId; use tycho_types::prelude::*; // === AtomicValidationSessionId === -pub struct AtomicValidationSessionId(AtomicU64); +pub struct AtomicValidationSessionId(RwLock); impl AtomicValidationSessionId { - pub const fn new(value: ValidationSessionId) -> Self { - Self(AtomicU64::new(Self::pack_id(value))) + pub fn new(value: ValidationSessionId) -> Self { + Self(RwLock::new(value)) } pub fn set(&self, value: ValidationSessionId) { - self.0.store(Self::pack_id(value), Ordering::Release); + *self.0.write() = value; } pub fn load(&self) -> ValidationSessionId { - Self::unpack_id(self.0.load(Ordering::Acquire)) - } - - #[inline] - const fn pack_id(value: ValidationSessionId) -> u64 { - const _: () = const { - let id = ValidationSessionId { - seqno: 0, - short_hash: 0, - }; - assert!(std::mem::size_of_val(&id.seqno) == 4); - assert!(std::mem::size_of_val(&id.short_hash) == 4); - }; - - ((value.seqno as u64) << 32) | (value.short_hash as u64) - } - - #[inline] - const fn unpack_id(value: u64) -> ValidationSessionId { - ValidationSessionId { - seqno: (value >> 32) as u32, - short_hash: value as u32, - } + *self.0.read() } } From 2961c29ce8926c7194ea4b315f19fd419f4c0eda Mon Sep 17 00:00:00 2001 From: MrWad3r Date: Tue, 10 Mar 2026 18:41:12 +0100 Subject: [PATCH 09/21] chore(contracts): propagate new validation session id into contract --- collator/src/types.rs | 2 +- .../src/validator/impls/std_impl/session.rs | 2 +- collator/src/validator/mod.rs | 11 +- contracts/src/slasher-stub.tolk | 5 +- contracts/tests/Slasher.spec.ts | 7 +- contracts/yarn.lock | 2072 +++++++++++------ slasher-traits/src/validator.rs | 12 +- slasher/src/bc/mod.rs | 2 +- slasher/src/bc/stub_contract.rs | 10 + slasher/src/lib.rs | 28 +- slasher/src/storage/db.rs | 25 +- slasher/src/storage/mod.rs | 12 +- slasher/src/util.rs | 34 +- 13 files changed, 1465 insertions(+), 757 deletions(-) diff --git a/collator/src/types.rs b/collator/src/types.rs index 231acfccc3..a27a888bb0 100644 --- a/collator/src/types.rs +++ b/collator/src/types.rs @@ -388,7 +388,7 @@ impl CollationSessionInfo { } pub fn get_validation_session_id(&self) -> ValidationSessionId { - (self.vset_switch_round, self.catchain_seqno) + (self.catchain_seqno, self.vset_switch_round) } pub fn shard(&self) -> ShardIdent { diff --git a/collator/src/validator/impls/std_impl/session.rs b/collator/src/validator/impls/std_impl/session.rs index 79339bdafe..7ac2d9c09d 100644 --- a/collator/src/validator/impls/std_impl/session.rs +++ b/collator/src/validator/impls/std_impl/session.rs @@ -965,7 +965,7 @@ fn compute_session_overlay_id( zerostate_root_hash: zerostate_id.root_hash.0, zerostate_file_hash: zerostate_id.file_hash.0, shard_ident: *shard_ident, - session_seqno: session_id.seqno(), + session_seqno: session_id.catchain_seqno(), })) } diff --git a/collator/src/validator/mod.rs b/collator/src/validator/mod.rs index 02f9659101..d2003ee251 100644 --- a/collator/src/validator/mod.rs +++ b/collator/src/validator/mod.rs @@ -52,17 +52,22 @@ pub struct ValidatorNetworkContext { pub zerostate_id: BlockId, } -/// (`vset_switch_round`, `catchain_seqno`) +/// (`catchain_seqno`, `vset_switch_round`) pub type ValidationSessionId = (u32, u32); pub trait CompositeValidationSessionId { - fn seqno(&self) -> u32; + fn catchain_seqno(&self) -> u32; + fn vset_switch_round(&self) -> u32; } impl CompositeValidationSessionId for ValidationSessionId { - fn seqno(&self) -> u32 { + fn catchain_seqno(&self) -> u32 { self.0 } + + fn vset_switch_round(&self) -> u32 { + self.1 + } } #[derive(Debug, Clone, Copy)] diff --git a/contracts/src/slasher-stub.tolk b/contracts/src/slasher-stub.tolk index daa027fd58..f0a965ac58 100644 --- a/contracts/src/slasher-stub.tolk +++ b/contracts/src/slasher-stub.tolk @@ -55,13 +55,16 @@ get fun is_blocks_batch_valid(batch: cell, mcSeqno: int): bool { // // === Logic === // -fun onInternalMessage(_in: InMessage) {} +fun onInternalMessage(_in: InMessage) { +} fun onExternalMessage(inMsg: slice) { val signature = inMsg.loadBits(512); val signedBody = inMsg; val createdAtMs = inMsg.loadUint(64); val expireAtSec = inMsg.loadUint(32); + val _catchainSeqno = inMsg.loadUint(32); + val _vsetSwitchRound = inMsg.loadUint(32); val validatorIdx = inMsg.loadUint(16); val batch = inMsg.loadRef(); inMsg.assertEnd(); diff --git a/contracts/tests/Slasher.spec.ts b/contracts/tests/Slasher.spec.ts index a3a7343be8..4ed660a7d8 100644 --- a/contracts/tests/Slasher.spec.ts +++ b/contracts/tests/Slasher.spec.ts @@ -132,11 +132,16 @@ describe("Slasher", () => { const nowMs = now * 1000 + 500; const expireAt = ~~(nowMs / 1000) + 60; + const catchainSeqno = 0; + const vsetSwitchRound = 0; + const validatorIdx = 0; const bodyToSign = beginCell() .storeUint(nowMs, 64) .storeUint(expireAt, 32) - .storeUint(0, 16) + .storeUint(catchainSeqno, 32) + .storeUint(vsetSwitchRound, 32) + .storeUint(validatorIdx, 16) .storeRef(SAMPLE_BLOCKS_BATCH) .endCell(); const signature = sign(bodyToSign.hash(), keypair.secretKey); diff --git a/contracts/yarn.lock b/contracts/yarn.lock index 7d010efb0e..9ab77505ce 100644 --- a/contracts/yarn.lock +++ b/contracts/yarn.lock @@ -4,7 +4,7 @@ "@ampproject/remapping@^2.2.0": version "2.3.0" - resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4" + resolved "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz" integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw== dependencies: "@jridgewell/gen-mapping" "^0.3.5" @@ -12,12 +12,12 @@ "@assemblyscript/loader@^0.9.4": version "0.9.4" - resolved "https://registry.yarnpkg.com/@assemblyscript/loader/-/loader-0.9.4.tgz#a483c54c1253656bb33babd464e3154a173e1577" + resolved "https://registry.npmjs.org/@assemblyscript/loader/-/loader-0.9.4.tgz" integrity sha512-HazVq9zwTVwGmqdwYzu7WyQ6FQVZ7SwET0KKQuKm55jD0IfUpZgN0OPIiZG3zV1iSrVYcN0bdwLRXI/VNCYsUA== -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.27.1": +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.27.1.tgz#200f715e66d52a23b221a9435534a91cc13ad5be" + resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz" integrity sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg== dependencies: "@babel/helper-validator-identifier" "^7.27.1" @@ -26,12 +26,12 @@ "@babel/compat-data@^7.27.2": version "7.28.0" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.28.0.tgz#9fc6fd58c2a6a15243cd13983224968392070790" + resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz" integrity sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw== -"@babel/core@^7.23.9", "@babel/core@^7.27.4": +"@babel/core@^7.0.0", "@babel/core@^7.0.0 || ^8.0.0-0", "@babel/core@^7.0.0-0", "@babel/core@^7.11.0", "@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.23.9", "@babel/core@^7.27.4", "@babel/core@^7.8.0", "@babel/core@>=7.0.0-beta.0 <8": version "7.28.3" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.28.3.tgz#aceddde69c5d1def69b839d09efa3e3ff59c97cb" + resolved "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz" integrity sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ== dependencies: "@ampproject/remapping" "^2.2.0" @@ -50,9 +50,9 @@ json5 "^2.2.3" semver "^6.3.1" -"@babel/generator@^7.27.5", "@babel/generator@^7.28.3": +"@babel/generator@^7.27.5", "@babel/generator@^7.28.3", "@babel/generator@^7.7.2": version "7.28.3" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.28.3.tgz#9626c1741c650cbac39121694a0f2d7451b8ef3e" + resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz" integrity sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw== dependencies: "@babel/parser" "^7.28.3" @@ -63,7 +63,7 @@ "@babel/helper-compilation-targets@^7.27.2": version "7.27.2" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz#46a0f6efab808d51d29ce96858dd10ce8732733d" + resolved "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz" integrity sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ== dependencies: "@babel/compat-data" "^7.27.2" @@ -74,12 +74,12 @@ "@babel/helper-globals@^7.28.0": version "7.28.0" - resolved "https://registry.yarnpkg.com/@babel/helper-globals/-/helper-globals-7.28.0.tgz#b9430df2aa4e17bc28665eadeae8aa1d985e6674" + resolved "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz" integrity sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw== "@babel/helper-module-imports@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz#7ef769a323e2655e126673bb6d2d6913bbead204" + resolved "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz" integrity sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w== dependencies: "@babel/traverse" "^7.27.1" @@ -87,7 +87,7 @@ "@babel/helper-module-transforms@^7.28.3": version "7.28.3" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz#a2b37d3da3b2344fe085dab234426f2b9a2fa5f6" + resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz" integrity sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw== dependencies: "@babel/helper-module-imports" "^7.27.1" @@ -96,161 +96,161 @@ "@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.27.1", "@babel/helper-plugin-utils@^7.8.0": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz#ddb2f876534ff8013e6c2b299bf4d39b3c51d44c" + resolved "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz" integrity sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw== "@babel/helper-string-parser@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687" + resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz" integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA== "@babel/helper-validator-identifier@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz#a7054dcc145a967dd4dc8fee845a57c1316c9df8" + resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz" integrity sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow== "@babel/helper-validator-option@^7.27.1": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz#fa52f5b1e7db1ab049445b421c4471303897702f" + resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz" integrity sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg== "@babel/helpers@^7.28.3": version "7.28.3" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.28.3.tgz#b83156c0a2232c133d1b535dd5d3452119c7e441" + resolved "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.3.tgz" integrity sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw== dependencies: "@babel/template" "^7.27.2" "@babel/types" "^7.28.2" -"@babel/parser@^7.1.0", "@babel/parser@^7.20.7", "@babel/parser@^7.23.9", "@babel/parser@^7.27.2", "@babel/parser@^7.28.3": +"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.23.9", "@babel/parser@^7.27.2", "@babel/parser@^7.28.3": version "7.28.3" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.3.tgz#d2d25b814621bca5fe9d172bc93792547e7a2a71" + resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz" integrity sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA== dependencies: "@babel/types" "^7.28.2" "@babel/plugin-syntax-async-generators@^7.8.4": version "7.8.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz" integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== dependencies: "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-bigint@^7.8.3": version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz#4c9a6f669f5d0cdf1b90a1671e9a146be5300cea" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz" integrity sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg== dependencies: "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-class-properties@^7.12.13": version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz" integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== dependencies: "@babel/helper-plugin-utils" "^7.12.13" "@babel/plugin-syntax-class-static-block@^7.14.5": version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz#195df89b146b4b78b3bf897fd7a257c84659d406" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz" integrity sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw== dependencies: "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-syntax-import-attributes@^7.24.7": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz#34c017d54496f9b11b61474e7ea3dfd5563ffe07" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz" integrity sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-syntax-import-meta@^7.10.4": version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz" integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-json-strings@^7.8.3": version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz" integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-jsx@^7.27.1": +"@babel/plugin-syntax-jsx@^7.27.1", "@babel/plugin-syntax-jsx@^7.7.2": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz#2f9beb5eff30fa507c5532d107daac7b888fa34c" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz" integrity sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w== dependencies: "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-syntax-logical-assignment-operators@^7.10.4": version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz" integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz" integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== dependencies: "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-numeric-separator@^7.10.4": version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz" integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-object-rest-spread@^7.8.3": version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz" integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== dependencies: "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-optional-catch-binding@^7.8.3": version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz" integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== dependencies: "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-optional-chaining@^7.8.3": version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz" integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== dependencies: "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-private-property-in-object@^7.14.5": version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz#0dc6671ec0ea22b6e94a1114f857970cd39de1ad" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz" integrity sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg== dependencies: "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-syntax-top-level-await@^7.14.5": version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz" integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== dependencies: "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-syntax-typescript@^7.27.1": +"@babel/plugin-syntax-typescript@^7.27.1", "@babel/plugin-syntax-typescript@^7.7.2": version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz#5147d29066a793450f220c63fa3a9431b7e6dd18" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz" integrity sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ== dependencies: "@babel/helper-plugin-utils" "^7.27.1" -"@babel/template@^7.27.2": +"@babel/template@^7.27.2", "@babel/template@^7.3.3": version "7.27.2" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.27.2.tgz#fa78ceed3c4e7b63ebf6cb39e5852fca45f6809d" + resolved "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz" integrity sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw== dependencies: "@babel/code-frame" "^7.27.1" @@ -259,7 +259,7 @@ "@babel/traverse@^7.27.1", "@babel/traverse@^7.28.3": version "7.28.3" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.28.3.tgz#6911a10795d2cce43ec6a28cffc440cca2593434" + resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.3.tgz" integrity sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ== dependencies: "@babel/code-frame" "^7.27.1" @@ -270,9 +270,9 @@ "@babel/types" "^7.28.2" debug "^4.3.1" -"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.27.1", "@babel/types@^7.27.3", "@babel/types@^7.28.2": +"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.27.1", "@babel/types@^7.27.3", "@babel/types@^7.28.2", "@babel/types@^7.3.3": version "7.28.2" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.2.tgz#da9db0856a9a88e0a13b019881d7513588cf712b" + resolved "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz" integrity sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ== dependencies: "@babel/helper-string-parser" "^7.27.1" @@ -280,41 +280,19 @@ "@bcoe/v8-coverage@^0.2.3": version "0.2.3" - resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" + resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== "@cspotcode/source-map-support@^0.8.0": version "0.8.1" - resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" + resolved "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz" integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== dependencies: "@jridgewell/trace-mapping" "0.3.9" -"@emnapi/core@^1.4.3": - version "1.4.5" - resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.4.5.tgz#bfbb0cbbbb9f96ec4e2c4fd917b7bbe5495ceccb" - integrity sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q== - dependencies: - "@emnapi/wasi-threads" "1.0.4" - tslib "^2.4.0" - -"@emnapi/runtime@^1.4.3": - version "1.4.5" - resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.4.5.tgz#c67710d0661070f38418b6474584f159de38aba9" - integrity sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg== - dependencies: - tslib "^2.4.0" - -"@emnapi/wasi-threads@1.0.4": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.0.4.tgz#703fc094d969e273b1b71c292523b2f792862bf4" - integrity sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g== - dependencies: - tslib "^2.4.0" - "@inquirer/external-editor@^1.0.0": version "1.0.1" - resolved "https://registry.yarnpkg.com/@inquirer/external-editor/-/external-editor-1.0.1.tgz#ab0a82c5719a963fb469021cde5cd2b74fea30f8" + resolved "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.1.tgz" integrity sha512-Oau4yL24d2B5IL4ma4UpbQigkVhzPDXLoqy1ggK4gnHg/stmkffJE4oOXHXF3uz0UEpywG68KcyXsyYpA1Re/Q== dependencies: chardet "^2.1.0" @@ -322,14 +300,14 @@ "@ipld/dag-pb@^2.0.2": version "2.1.18" - resolved "https://registry.yarnpkg.com/@ipld/dag-pb/-/dag-pb-2.1.18.tgz#12d63e21580e87c75fd1a2c62e375a78e355c16f" + resolved "https://registry.npmjs.org/@ipld/dag-pb/-/dag-pb-2.1.18.tgz" integrity sha512-ZBnf2fuX9y3KccADURG5vb9FaOeMjFkCrNysB0PtftME/4iCTjxfaLoNq/IAh5fTqUOMXvryN6Jyka4ZGuMLIg== dependencies: multiformats "^9.5.4" "@isaacs/cliui@^8.0.2": version "8.0.2" - resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" + resolved "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz" integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== dependencies: string-width "^5.1.2" @@ -341,7 +319,7 @@ "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" - resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" + resolved "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz" integrity sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ== dependencies: camelcase "^5.3.1" @@ -352,12 +330,24 @@ "@istanbuljs/schema@^0.1.2", "@istanbuljs/schema@^0.1.3": version "0.1.3" - resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" + resolved "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz" integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== +"@jest/console@^29.7.0": + version "29.7.0" + resolved "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz" + integrity sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg== + dependencies: + "@jest/types" "^29.6.3" + "@types/node" "*" + chalk "^4.0.0" + jest-message-util "^29.7.0" + jest-util "^29.7.0" + slash "^3.0.0" + "@jest/console@30.0.5": version "30.0.5" - resolved "https://registry.yarnpkg.com/@jest/console/-/console-30.0.5.tgz#d7d027c2db5c64c20a973b7f3e57b49956d6c335" + resolved "https://registry.npmjs.org/@jest/console/-/console-30.0.5.tgz" integrity sha512-xY6b0XiL0Nav3ReresUarwl2oIz1gTnxGbGpho9/rbUWsLH0f1OD/VT84xs8c7VmH7MChnLb0pag6PhZhAdDiA== dependencies: "@jest/types" "30.0.5" @@ -367,9 +357,43 @@ jest-util "30.0.5" slash "^3.0.0" +"@jest/core@^29.7.0": + version "29.7.0" + resolved "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz" + integrity sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg== + dependencies: + "@jest/console" "^29.7.0" + "@jest/reporters" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + ci-info "^3.2.0" + exit "^0.1.2" + graceful-fs "^4.2.9" + jest-changed-files "^29.7.0" + jest-config "^29.7.0" + jest-haste-map "^29.7.0" + jest-message-util "^29.7.0" + jest-regex-util "^29.6.3" + jest-resolve "^29.7.0" + jest-resolve-dependencies "^29.7.0" + jest-runner "^29.7.0" + jest-runtime "^29.7.0" + jest-snapshot "^29.7.0" + jest-util "^29.7.0" + jest-validate "^29.7.0" + jest-watcher "^29.7.0" + micromatch "^4.0.4" + pretty-format "^29.7.0" + slash "^3.0.0" + strip-ansi "^6.0.0" + "@jest/core@30.0.5": version "30.0.5" - resolved "https://registry.yarnpkg.com/@jest/core/-/core-30.0.5.tgz#b5778922d2928f676636e3ec199829554e61e452" + resolved "https://registry.npmjs.org/@jest/core/-/core-30.0.5.tgz" integrity sha512-fKD0OulvRsXF1hmaFgHhVJzczWzA1RXMMo9LTPuFXo9q/alDbME3JIyWYqovWsUBWSoBcsHaGPSLF9rz4l9Qeg== dependencies: "@jest/console" "30.0.5" @@ -403,12 +427,22 @@ "@jest/diff-sequences@30.0.1": version "30.0.1" - resolved "https://registry.yarnpkg.com/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz#0ededeae4d071f5c8ffe3678d15f3a1be09156be" + resolved "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz" integrity sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw== +"@jest/environment@^29.7.0": + version "29.7.0" + resolved "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz" + integrity sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw== + dependencies: + "@jest/fake-timers" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + jest-mock "^29.7.0" + "@jest/environment@30.0.5": version "30.0.5" - resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-30.0.5.tgz#eaaae0403c7d3f8414053c2224acc3011e1c3a1b" + resolved "https://registry.npmjs.org/@jest/environment/-/environment-30.0.5.tgz" integrity sha512-aRX7WoaWx1oaOkDQvCWImVQ8XNtdv5sEWgk4gxR6NXb7WBUnL5sRak4WRzIQRZ1VTWPvV4VI4mgGjNL9TeKMYA== dependencies: "@jest/fake-timers" "30.0.5" @@ -416,24 +450,51 @@ "@types/node" "*" jest-mock "30.0.5" +"@jest/expect-utils@^29.7.0": + version "29.7.0" + resolved "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz" + integrity sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA== + dependencies: + jest-get-type "^29.6.3" + "@jest/expect-utils@30.0.5": version "30.0.5" - resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-30.0.5.tgz#9d42e4b8bc80367db30abc6c42b2cb14073f66fc" + resolved "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.0.5.tgz" integrity sha512-F3lmTT7CXWYywoVUGTCmom0vXq3HTTkaZyTAzIy+bXSBizB7o5qzlC9VCtq0arOa8GqmNsbg/cE9C6HLn7Szew== dependencies: "@jest/get-type" "30.0.1" +"@jest/expect@^29.7.0": + version "29.7.0" + resolved "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz" + integrity sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ== + dependencies: + expect "^29.7.0" + jest-snapshot "^29.7.0" + "@jest/expect@30.0.5": version "30.0.5" - resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-30.0.5.tgz#2bbd101df4869f5d171c3cfee881f810f1525005" + resolved "https://registry.npmjs.org/@jest/expect/-/expect-30.0.5.tgz" integrity sha512-6udac8KKrtTtC+AXZ2iUN/R7dp7Ydry+Fo6FPFnDG54wjVMnb6vW/XNlf7Xj8UDjAE3aAVAsR4KFyKk3TCXmTA== dependencies: expect "30.0.5" jest-snapshot "30.0.5" +"@jest/fake-timers@^29.7.0": + version "29.7.0" + resolved "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz" + integrity sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ== + dependencies: + "@jest/types" "^29.6.3" + "@sinonjs/fake-timers" "^10.0.2" + "@types/node" "*" + jest-message-util "^29.7.0" + jest-mock "^29.7.0" + jest-util "^29.7.0" + "@jest/fake-timers@30.0.5": version "30.0.5" - resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-30.0.5.tgz#c028a9465a44b7744cb2368196bed89ce13c7054" + resolved "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.0.5.tgz" integrity sha512-ZO5DHfNV+kgEAeP3gK3XlpJLL4U3Sz6ebl/n68Uwt64qFFs5bv4bfEEjyRGK5uM0C90ewooNgFuKMdkbEoMEXw== dependencies: "@jest/types" "30.0.5" @@ -445,12 +506,12 @@ "@jest/get-type@30.0.1": version "30.0.1" - resolved "https://registry.yarnpkg.com/@jest/get-type/-/get-type-30.0.1.tgz#0d32f1bbfba511948ad247ab01b9007724fc9f52" + resolved "https://registry.npmjs.org/@jest/get-type/-/get-type-30.0.1.tgz" integrity sha512-AyYdemXCptSRFirI5EPazNxyPwAL0jXt3zceFjaj8NFiKP9pOi0bfXonf6qkf82z2t3QWPeLCWWw4stPBzctLw== -"@jest/globals@30.0.5": +"@jest/globals@*", "@jest/globals@30.0.5": version "30.0.5" - resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-30.0.5.tgz#ca70e0ac08ab40417cf8cd92bcb76116c2ccca63" + resolved "https://registry.npmjs.org/@jest/globals/-/globals-30.0.5.tgz" integrity sha512-7oEJT19WW4oe6HR7oLRvHxwlJk2gev0U9px3ufs8sX9PoD1Eza68KF0/tlN7X0dq/WVsBScXQGgCldA1V9Y/jA== dependencies: "@jest/environment" "30.0.5" @@ -458,17 +519,57 @@ "@jest/types" "30.0.5" jest-mock "30.0.5" +"@jest/globals@^29.7.0": + version "29.7.0" + resolved "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz" + integrity sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ== + dependencies: + "@jest/environment" "^29.7.0" + "@jest/expect" "^29.7.0" + "@jest/types" "^29.6.3" + jest-mock "^29.7.0" + "@jest/pattern@30.0.1": version "30.0.1" - resolved "https://registry.yarnpkg.com/@jest/pattern/-/pattern-30.0.1.tgz#d5304147f49a052900b4b853dedb111d080e199f" + resolved "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz" integrity sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA== dependencies: "@types/node" "*" jest-regex-util "30.0.1" +"@jest/reporters@^29.7.0": + version "29.7.0" + resolved "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz" + integrity sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg== + dependencies: + "@bcoe/v8-coverage" "^0.2.3" + "@jest/console" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" + "@jridgewell/trace-mapping" "^0.3.18" + "@types/node" "*" + chalk "^4.0.0" + collect-v8-coverage "^1.0.0" + exit "^0.1.2" + glob "^7.1.3" + graceful-fs "^4.2.9" + istanbul-lib-coverage "^3.0.0" + istanbul-lib-instrument "^6.0.0" + istanbul-lib-report "^3.0.0" + istanbul-lib-source-maps "^4.0.0" + istanbul-reports "^3.1.3" + jest-message-util "^29.7.0" + jest-util "^29.7.0" + jest-worker "^29.7.0" + slash "^3.0.0" + string-length "^4.0.1" + strip-ansi "^6.0.0" + v8-to-istanbul "^9.0.1" + "@jest/reporters@30.0.5": version "30.0.5" - resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-30.0.5.tgz#b83585e6448d390a8d92a641c567f1655976d5c6" + resolved "https://registry.npmjs.org/@jest/reporters/-/reporters-30.0.5.tgz" integrity sha512-mafft7VBX4jzED1FwGC1o/9QUM2xebzavImZMeqnsklgcyxBto8mV4HzNSzUrryJ+8R9MFOM3HgYuDradWR+4g== dependencies: "@bcoe/v8-coverage" "^0.2.3" @@ -495,16 +596,23 @@ string-length "^4.0.2" v8-to-istanbul "^9.0.1" +"@jest/schemas@^29.6.3": + version "29.6.3" + resolved "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz" + integrity sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA== + dependencies: + "@sinclair/typebox" "^0.27.8" + "@jest/schemas@30.0.5": version "30.0.5" - resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-30.0.5.tgz#7bdf69fc5a368a5abdb49fd91036c55225846473" + resolved "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz" integrity sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA== dependencies: "@sinclair/typebox" "^0.34.0" "@jest/snapshot-utils@30.0.5": version "30.0.5" - resolved "https://registry.yarnpkg.com/@jest/snapshot-utils/-/snapshot-utils-30.0.5.tgz#e23a0e786f174e8cff7f150c1cfbdc9cb7cc81a4" + resolved "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.0.5.tgz" integrity sha512-XcCQ5qWHLvi29UUrowgDFvV4t7ETxX91CbDczMnoqXPOIcZOxyNdSjm6kV5XMc8+HkxfRegU/MUmnTbJRzGrUQ== dependencies: "@jest/types" "30.0.5" @@ -512,18 +620,37 @@ graceful-fs "^4.2.11" natural-compare "^1.4.0" +"@jest/source-map@^29.6.3": + version "29.6.3" + resolved "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz" + integrity sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw== + dependencies: + "@jridgewell/trace-mapping" "^0.3.18" + callsites "^3.0.0" + graceful-fs "^4.2.9" + "@jest/source-map@30.0.1": version "30.0.1" - resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-30.0.1.tgz#305ebec50468f13e658b3d5c26f85107a5620aaa" + resolved "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz" integrity sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg== dependencies: "@jridgewell/trace-mapping" "^0.3.25" callsites "^3.1.0" graceful-fs "^4.2.11" +"@jest/test-result@^29.7.0": + version "29.7.0" + resolved "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz" + integrity sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA== + dependencies: + "@jest/console" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/istanbul-lib-coverage" "^2.0.0" + collect-v8-coverage "^1.0.0" + "@jest/test-result@30.0.5": version "30.0.5" - resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-30.0.5.tgz#064c5210c24d5ea192fb02ceddad3be1cfa557c8" + resolved "https://registry.npmjs.org/@jest/test-result/-/test-result-30.0.5.tgz" integrity sha512-wPyztnK0gbDMQAJZ43tdMro+qblDHH1Ru/ylzUo21TBKqt88ZqnKKK2m30LKmLLoKtR2lxdpCC/P3g1vfKcawQ== dependencies: "@jest/console" "30.0.5" @@ -531,9 +658,19 @@ "@types/istanbul-lib-coverage" "^2.0.6" collect-v8-coverage "^1.0.2" +"@jest/test-sequencer@^29.7.0": + version "29.7.0" + resolved "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz" + integrity sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw== + dependencies: + "@jest/test-result" "^29.7.0" + graceful-fs "^4.2.9" + jest-haste-map "^29.7.0" + slash "^3.0.0" + "@jest/test-sequencer@30.0.5": version "30.0.5" - resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-30.0.5.tgz#c6dba8fc3c386dd793c087626e8508ff1ead19f4" + resolved "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.0.5.tgz" integrity sha512-Aea/G1egWoIIozmDD7PBXUOxkekXl7ueGzrsGGi1SbeKgQqCYCIf+wfbflEbf2LiPxL8j2JZGLyrzZagjvW4YQ== dependencies: "@jest/test-result" "30.0.5" @@ -541,9 +678,9 @@ jest-haste-map "30.0.5" slash "^3.0.0" -"@jest/transform@30.0.5": +"@jest/transform@^29.0.0 || ^30.0.0", "@jest/transform@30.0.5": version "30.0.5" - resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-30.0.5.tgz#f8ca2e9f7466b77b406807d3bef1f6790dd384e4" + resolved "https://registry.npmjs.org/@jest/transform/-/transform-30.0.5.tgz" integrity sha512-Vk8amLQCmuZyy6GbBht1Jfo9RSdBtg7Lks+B0PecnjI8J+PCLQPGh7uI8Q/2wwpW2gLdiAfiHNsmekKlywULqg== dependencies: "@babel/core" "^7.27.4" @@ -562,9 +699,30 @@ slash "^3.0.0" write-file-atomic "^5.0.1" -"@jest/types@30.0.5": +"@jest/transform@^29.7.0": + version "29.7.0" + resolved "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz" + integrity sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw== + dependencies: + "@babel/core" "^7.11.6" + "@jest/types" "^29.6.3" + "@jridgewell/trace-mapping" "^0.3.18" + babel-plugin-istanbul "^6.1.1" + chalk "^4.0.0" + convert-source-map "^2.0.0" + fast-json-stable-stringify "^2.1.0" + graceful-fs "^4.2.9" + jest-haste-map "^29.7.0" + jest-regex-util "^29.6.3" + jest-util "^29.7.0" + micromatch "^4.0.4" + pirates "^4.0.4" + slash "^3.0.0" + write-file-atomic "^4.0.2" + +"@jest/types@^29.0.0 || ^30.0.0", "@jest/types@30.0.5": version "30.0.5" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-30.0.5.tgz#29a33a4c036e3904f1cfd94f6fe77f89d2e1cc05" + resolved "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz" integrity sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ== dependencies: "@jest/pattern" "30.0.1" @@ -575,9 +733,21 @@ "@types/yargs" "^17.0.33" chalk "^4.1.2" +"@jest/types@^29.6.3": + version "29.6.3" + resolved "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz" + integrity sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw== + dependencies: + "@jest/schemas" "^29.6.3" + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^17.0.8" + chalk "^4.0.0" + "@jridgewell/gen-mapping@^0.3.12", "@jridgewell/gen-mapping@^0.3.5": version "0.3.13" - resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz#6342a19f44347518c93e43b1ac69deb3c4656a1f" + resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz" integrity sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA== dependencies: "@jridgewell/sourcemap-codec" "^1.5.0" @@ -585,90 +755,81 @@ "@jridgewell/resolve-uri@^3.0.3", "@jridgewell/resolve-uri@^3.1.0": version "3.1.2" - resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz" integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== "@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0": version "1.5.5" - resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba" + resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz" integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== +"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.23", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25", "@jridgewell/trace-mapping@^0.3.28": + version "0.3.30" + resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz" + integrity sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + "@jridgewell/trace-mapping@0.3.9": version "0.3.9" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" + resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz" integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== dependencies: "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" -"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.23", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25", "@jridgewell/trace-mapping@^0.3.28": - version "0.3.30" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz#4a76c4daeee5df09f5d3940e087442fb36ce2b99" - integrity sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q== - dependencies: - "@jridgewell/resolve-uri" "^3.1.0" - "@jridgewell/sourcemap-codec" "^1.4.14" - "@multiformats/murmur3@^1.0.3": version "1.1.3" - resolved "https://registry.yarnpkg.com/@multiformats/murmur3/-/murmur3-1.1.3.tgz#70349166992e5f981f1ddff0200fa775b2bf6606" + resolved "https://registry.npmjs.org/@multiformats/murmur3/-/murmur3-1.1.3.tgz" integrity sha512-wAPLUErGR8g6Lt+bAZn6218k9YQPym+sjszsXL6o4zfxbA22P+gxWZuuD9wDbwL55xrKO5idpcuQUX7/E3oHcw== dependencies: multiformats "^9.5.4" murmurhash3js-revisited "^3.0.0" -"@napi-rs/wasm-runtime@^0.2.11": - version "0.2.12" - resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz#3e78a8b96e6c33a6c517e1894efbd5385a7cb6f2" - integrity sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ== - dependencies: - "@emnapi/core" "^1.4.3" - "@emnapi/runtime" "^1.4.3" - "@tybys/wasm-util" "^0.10.0" - "@noble/ed25519@^1.6.1": version "1.7.5" - resolved "https://registry.yarnpkg.com/@noble/ed25519/-/ed25519-1.7.5.tgz#94df8bdb9fec9c4644a56007eecb57b0e9fbd0d7" + resolved "https://registry.npmjs.org/@noble/ed25519/-/ed25519-1.7.5.tgz" integrity sha512-xuS0nwRMQBvSxDa7UxMb61xTiH3MxTgUfhyPUALVIe0FlOAz4sjELwyDRyUvqeEYfRSG9qNjFIycqLZppg4RSA== "@noble/hashes@^1.2.0": version "1.8.0" - resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.8.0.tgz#cee43d801fcef9644b11b8194857695acd5f815a" + resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz" integrity sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A== "@pkgjs/parseargs@^0.11.0": version "0.11.0" - resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" + resolved "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== "@pkgr/core@^0.2.9": version "0.2.9" - resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.2.9.tgz#d229a7b7f9dac167a156992ef23c7f023653f53b" + resolved "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz" integrity sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA== "@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": version "1.1.2" - resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" + resolved "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz" integrity sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ== "@protobufjs/base64@^1.1.2": version "1.1.2" - resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735" + resolved "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz" integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg== "@protobufjs/codegen@^2.0.4": version "2.0.4" - resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb" + resolved "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz" integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg== "@protobufjs/eventemitter@^1.1.0": version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70" + resolved "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz" integrity sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q== "@protobufjs/fetch@^1.1.0": version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45" + resolved "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz" integrity sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ== dependencies: "@protobufjs/aspromise" "^1.1.1" @@ -676,51 +837,63 @@ "@protobufjs/float@^1.0.2": version "1.0.2" - resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1" + resolved "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz" integrity sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ== "@protobufjs/inquire@^1.1.0": version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089" + resolved "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz" integrity sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q== "@protobufjs/path@^1.1.2": version "1.1.2" - resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d" + resolved "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz" integrity sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA== "@protobufjs/pool@^1.1.0": version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54" + resolved "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz" integrity sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw== "@protobufjs/utf8@^1.1.0": version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" + resolved "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz" integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== +"@sinclair/typebox@^0.27.8": + version "0.27.8" + resolved "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz" + integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== + "@sinclair/typebox@^0.34.0": version "0.34.40" - resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.34.40.tgz#740056ea8d8aaada2ac1ce414c2f074798283b92" + resolved "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.40.tgz" integrity sha512-gwBNIP8ZAYev/ORDWW0QvxdwPXwxBtLsdsJgSc7eDIRt8ubP+rxUBzPsrwnu16fgEF8Bx4lh/+mvQvJzcTM6Kw== -"@sinonjs/commons@^3.0.1": +"@sinonjs/commons@^3.0.0", "@sinonjs/commons@^3.0.1": version "3.0.1" - resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-3.0.1.tgz#1029357e44ca901a615585f6d27738dbc89084cd" + resolved "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz" integrity sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ== dependencies: type-detect "4.0.8" +"@sinonjs/fake-timers@^10.0.2": + version "10.3.0" + resolved "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz" + integrity sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA== + dependencies: + "@sinonjs/commons" "^3.0.0" + "@sinonjs/fake-timers@^13.0.0": version "13.0.5" - resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz#36b9dbc21ad5546486ea9173d6bea063eb1717d5" + resolved "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz" integrity sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw== dependencies: "@sinonjs/commons" "^3.0.1" -"@tact-lang/compiler@1.6.13": +"@tact-lang/compiler@>=1.6.5", "@tact-lang/compiler@1.6.13": version "1.6.13" - resolved "https://registry.yarnpkg.com/@tact-lang/compiler/-/compiler-1.6.13.tgz#b2f7e6eb75b52fea052369d8ac34c061f63d5c99" + resolved "https://registry.npmjs.org/@tact-lang/compiler/-/compiler-1.6.13.tgz" integrity sha512-lrgT/kCgC+nuppB4zPSDCAcLQ6EauTJ3NEBX4prEBBRmJ8aexYAfFUfVayskZw96JrDjReNIhnD8dG/yU0Fk+w== dependencies: "@tact-lang/opcode" "^0.3.0" @@ -736,7 +909,7 @@ "@tact-lang/opcode@^0.3.0": version "0.3.2" - resolved "https://registry.yarnpkg.com/@tact-lang/opcode/-/opcode-0.3.2.tgz#0d103c5c5d3360348bf72b1b20fc507066fc4e01" + resolved "https://registry.npmjs.org/@tact-lang/opcode/-/opcode-0.3.2.tgz" integrity sha512-ZFsgOBTCxsKkYYOKomdaHMc8VSOFQKTbjLR1mYq6NFYyTdaz69gHEqgsIEZ0URSNWGg2er5H+LXlv8+8Tlt7sA== dependencies: "@ton/core" "^0.61.0" @@ -745,24 +918,24 @@ "@ton-api/client@^0.2.0": version "0.2.0" - resolved "https://registry.yarnpkg.com/@ton-api/client/-/client-0.2.0.tgz#82ca5cfba84919fd5010260d5695496af5b4e785" + resolved "https://registry.npmjs.org/@ton-api/client/-/client-0.2.0.tgz" integrity sha512-m/T8Nroq4rghUcg72Bbt2He9x5g9RlrP4F9rs7J4DVYLMHnKQTlKDo9JMD/feLXtHxOMh/YGJNcb+BGBkNbNug== dependencies: core-js-pure "^3.38.0" "@ton-api/ton-adapter@^0.2.0": version "0.2.0" - resolved "https://registry.yarnpkg.com/@ton-api/ton-adapter/-/ton-adapter-0.2.0.tgz#d118ed8fe683c670907b0d94d52ee3847b8050f4" + resolved "https://registry.npmjs.org/@ton-api/ton-adapter/-/ton-adapter-0.2.0.tgz" integrity sha512-0l1Y7pgi6/N6HOqRAdgOemDssYB4sXKtHWSKgm+cDL754ZMP3gsj/6pEmgOo/H7+itsKf4t0UqOU0q2JuqG/zw== "@ton-community/func-js-bin@0.4.6-wasmfix.debugger.0": version "0.4.6-wasmfix.debugger.0" - resolved "https://registry.yarnpkg.com/@ton-community/func-js-bin/-/func-js-bin-0.4.6-wasmfix.debugger.0.tgz#26af588cf4c0a10ba4d2cdfdcdc50b330edea19e" + resolved "https://registry.npmjs.org/@ton-community/func-js-bin/-/func-js-bin-0.4.6-wasmfix.debugger.0.tgz" integrity sha512-g23zEoaTn5Rja6TBTZxX3E4zh+PlWnt7iRJJT5mPuUvQXhWB9wkx9VNZN8KpTdICJIXTW5b5wEE/801W14xQvg== "@ton-community/func-js@>=0.10.0": version "0.10.0" - resolved "https://registry.yarnpkg.com/@ton-community/func-js/-/func-js-0.10.0.tgz#6f35f990255c5d1ea730d7ca9f5b4b2e2933f17a" + resolved "https://registry.npmjs.org/@ton-community/func-js/-/func-js-0.10.0.tgz" integrity sha512-YvkRTwkwc7e54Ig7oRKGercE91Fi+EuEDLO1kp/RnwslcUcgKQ+w2P7OFMNh/FWCJnU3ADhOBtfTU+dta+XHpw== dependencies: "@ton-community/func-js-bin" "0.4.6-wasmfix.debugger.0" @@ -771,7 +944,7 @@ "@ton/blueprint@>=0.40.0": version "0.40.0" - resolved "https://registry.yarnpkg.com/@ton/blueprint/-/blueprint-0.40.0.tgz#22a569a623eed8368fc4f758f4ef29b7599f13fb" + resolved "https://registry.npmjs.org/@ton/blueprint/-/blueprint-0.40.0.tgz" integrity sha512-DL0PDSgsZm7qmQTlCq+KbWjvzFTSQnRy4YQOHPfMpe3/Hla3BeblYiBCwFjk6ev7LXxcEPMlmliRnyXBO8DZuQ== dependencies: "@ton-api/client" "^0.2.0" @@ -786,44 +959,52 @@ ton-lite-client "^3.1.1" ts-node "^10.9.1" -"@ton/core@0.60.1": - version "0.60.1" - resolved "https://registry.yarnpkg.com/@ton/core/-/core-0.60.1.tgz#cc9a62fb308d7597b1217dc8e44c7e2dcc0aceaa" - integrity sha512-8FwybYbfkk57C3l9gvnlRhRBHbLYmeu0LbB1z9N+dhDz0Z+FJW8w0TJlks8CgHrAFxsT3FlR2LsqFnsauMp38w== +"@ton/core@^0.61.0": + version "0.61.0" + resolved "https://registry.npmjs.org/@ton/core/-/core-0.61.0.tgz" + integrity sha512-0qyVfP2dDue2bq80ydXggo2MlufcmzuFk6G94qRrZxvyQ3NSe4UeBTeRf1gQmN7tywgTsX2gS61e4yvJrlUu4Q== dependencies: symbol.inspect "1.0.1" -"@ton/core@>=0.62.0": - version "0.63.0" - resolved "https://registry.yarnpkg.com/@ton/core/-/core-0.63.0.tgz#87487072f707d8ac7fc00983d5aa707409728c07" - integrity sha512-uBc0WQNYVzjAwPvIazf0Ryhpv4nJd4dKIuHoj766gUdwe8sVzGM+TxKKKJETL70hh/mxACyUlR4tAwN0LWDNow== +"@ton/core@>=0.49.2", "@ton/core@>=0.56.0", "@ton/core@>=0.59.0", "@ton/core@>=0.60.0", "@ton/core@>=0.61.0", "@ton/core@>=0.62.0": + version "0.63.1" + resolved "https://registry.npmjs.org/@ton/core/-/core-0.63.1.tgz" + integrity sha512-hDWMjlKzc18W2E4OeV3hUP8ohRJNHPD4Wd1+AQJj8zshZyCRT0usrvnExgbNUTo/vntDqCGMzgYWbXxyaA+L4g== -"@ton/core@^0.61.0": - version "0.61.0" - resolved "https://registry.yarnpkg.com/@ton/core/-/core-0.61.0.tgz#09b37801cb2f5a942020fcc992be1e99f4b16689" - integrity sha512-0qyVfP2dDue2bq80ydXggo2MlufcmzuFk6G94qRrZxvyQ3NSe4UeBTeRf1gQmN7tywgTsX2gS61e4yvJrlUu4Q== +"@ton/core@0.60.1": + version "0.60.1" + resolved "https://registry.npmjs.org/@ton/core/-/core-0.60.1.tgz" + integrity sha512-8FwybYbfkk57C3l9gvnlRhRBHbLYmeu0LbB1z9N+dhDz0Z+FJW8w0TJlks8CgHrAFxsT3FlR2LsqFnsauMp38w== dependencies: symbol.inspect "1.0.1" "@ton/crypto-primitives@2.1.0": version "2.1.0" - resolved "https://registry.yarnpkg.com/@ton/crypto-primitives/-/crypto-primitives-2.1.0.tgz#8c9277c250b59aae3c819e0d6bd61e44d998e9ca" + resolved "https://registry.npmjs.org/@ton/crypto-primitives/-/crypto-primitives-2.1.0.tgz" integrity sha512-PQesoyPgqyI6vzYtCXw4/ZzevePc4VGcJtFwf08v10OevVJHVfW238KBdpj1kEDQkxWLeuNHEpTECNFKnP6tow== dependencies: jssha "3.2.0" -"@ton/crypto@^3.2.0", "@ton/crypto@^3.3.0": +"@ton/crypto@^3.2.0", "@ton/crypto@^3.3.0", "@ton/crypto@>=3.2.0", "@ton/crypto@>=3.3.0": version "3.3.0" - resolved "https://registry.yarnpkg.com/@ton/crypto/-/crypto-3.3.0.tgz#019103df6540fbc1d8102979b4587bc85ff9779e" + resolved "https://registry.npmjs.org/@ton/crypto/-/crypto-3.3.0.tgz" integrity sha512-/A6CYGgA/H36OZ9BbTaGerKtzWp50rg67ZCH2oIjV1NcrBaCK9Z343M+CxedvM7Haf3f/Ee9EhxyeTp0GKMUpA== dependencies: "@ton/crypto-primitives" "2.1.0" jssha "3.2.0" tweetnacl "1.0.3" -"@ton/sandbox@>=0.39.0": +"@ton/sandbox@^0.32.2": + version "0.32.2" + resolved "https://registry.npmjs.org/@ton/sandbox/-/sandbox-0.32.2.tgz" + integrity sha512-D+Yuyka3pMuoD1KPufRGzE3iFZ0QLyba/xC5mfrXoLtV111ubKxc7RscndOsggeru0bdDYm0i/iaWO5YQWqUfw== + dependencies: + chalk "^4.1.2" + table "^6.9.0" + +"@ton/sandbox@>=0.34.0", "@ton/sandbox@>=0.39.0": version "0.41.0" - resolved "https://registry.yarnpkg.com/@ton/sandbox/-/sandbox-0.41.0.tgz#a0c81cca5dedb1891e1cf3f621f730f6bac63539" + resolved "https://registry.npmjs.org/@ton/sandbox/-/sandbox-0.41.0.tgz" integrity sha512-+WRWiHfm62xQebVt6BvLb2UhVphpBHCwSby8R5vP9llzdVck+XEs+p4csIkZBh6gRQsy1Xomzh1PpgZS5XVE3A== dependencies: "@vscode/debugadapter" "^1.68.0" @@ -832,31 +1013,23 @@ table "^6.9.0" ton-assembly "0.6.1" -"@ton/sandbox@^0.32.2": - version "0.32.2" - resolved "https://registry.yarnpkg.com/@ton/sandbox/-/sandbox-0.32.2.tgz#e949d1df2ae98c1552901597200209163dd8cc42" - integrity sha512-D+Yuyka3pMuoD1KPufRGzE3iFZ0QLyba/xC5mfrXoLtV111ubKxc7RscndOsggeru0bdDYm0i/iaWO5YQWqUfw== - dependencies: - chalk "^4.1.2" - table "^6.9.0" - -"@ton/test-utils@>=0.11.0": +"@ton/test-utils@>=0.11.0", "@ton/test-utils@>=0.7.0": version "0.11.0" - resolved "https://registry.yarnpkg.com/@ton/test-utils/-/test-utils-0.11.0.tgz#002ec16ce585f930ebdca3c93d20cde8aea6bc30" + resolved "https://registry.npmjs.org/@ton/test-utils/-/test-utils-0.11.0.tgz" integrity sha512-GFYUGsNdT+0xNU62aG+RG605sGYoLqLTEpfmR5TR2RjDZm+noDA50Dp0ImWGXBhD74/RrMKPaJ6KvzFgLC4vNg== dependencies: node-inspect-extracted "^2.0.0" -"@ton/tolk-js@>=1.0.0": +"@ton/tolk-js@>=0.13.0", "@ton/tolk-js@>=1.0.0": version "1.0.0" - resolved "https://registry.yarnpkg.com/@ton/tolk-js/-/tolk-js-1.0.0.tgz#f9863d8ed7016ea50890e5d33d9972042ef61c0f" + resolved "https://registry.npmjs.org/@ton/tolk-js/-/tolk-js-1.0.0.tgz" integrity sha512-OWFybjqo7MYZBB2XSuALEaH3uMJQeQHKbpbTmCWXJYnZskhPT4jwBshZ0gDvTskag++rwyPkcYTNJsfi6ngMXw== dependencies: arg "^5.0.2" -"@ton/ton@>=15.2.1 <16.0.0": +"@ton/ton@>=15.2.1", "@ton/ton@>=15.2.1 <16.0.0": version "15.3.1" - resolved "https://registry.yarnpkg.com/@ton/ton/-/ton-15.3.1.tgz#c20688b27eb8ce8474610843804a7599679c38a2" + resolved "https://registry.npmjs.org/@ton/ton/-/ton-15.3.1.tgz" integrity sha512-+UuvbE0o0VIU/0W90STO+emRIDr3Vs39LdbX5ySm/Ra+RQJSiH0KX6TDOFqWDmD2Wzk4/zw21KwSiZ6Xjk8zlw== dependencies: axios "^1.6.7" @@ -867,21 +1040,21 @@ "@tonconnect/isomorphic-eventsource@^0.0.1": version "0.0.1" - resolved "https://registry.yarnpkg.com/@tonconnect/isomorphic-eventsource/-/isomorphic-eventsource-0.0.1.tgz#199e5a86c31dad706b79826f65879e0d77d3dd51" + resolved "https://registry.npmjs.org/@tonconnect/isomorphic-eventsource/-/isomorphic-eventsource-0.0.1.tgz" integrity sha512-ODk48pMlqLSOvu3fM0R1sdlz/Cv2y4hSfwtXmLq9ky9+H7ZQfw/16ElpIJ69B4lUvHycxrueNgrRtF9PJHoGMw== dependencies: eventsource "^2.0.2" "@tonconnect/isomorphic-fetch@^0.0.2": version "0.0.2" - resolved "https://registry.yarnpkg.com/@tonconnect/isomorphic-fetch/-/isomorphic-fetch-0.0.2.tgz#c09ff05a409ec89262c369d4bf27305820cdaa33" + resolved "https://registry.npmjs.org/@tonconnect/isomorphic-fetch/-/isomorphic-fetch-0.0.2.tgz" integrity sha512-DAyA4oL7MqbBo9k8+8E+YiWsGCYi6UMhDTcsZjhgzhESkBNG6b+NBkpb1KH4oi0xDZQoknFtY9XogJLuQtSMQQ== dependencies: node-fetch "^2.6.9" "@tonconnect/protocol@^2.2.5": version "2.3.0" - resolved "https://registry.yarnpkg.com/@tonconnect/protocol/-/protocol-2.3.0.tgz#3b1ab9a56185ad676f52889a5fb44a611cf7a059" + resolved "https://registry.npmjs.org/@tonconnect/protocol/-/protocol-2.3.0.tgz" integrity sha512-OxrmcXF/EsSdGeASP9VpTRojuMtTV87DKYFLRq4ZJvF/Hirfm2cgcxzzj2uksEGm5IIR010UWo6b38RuokNwFQ== dependencies: tweetnacl "^1.0.3" @@ -889,56 +1062,49 @@ "@tonconnect/sdk@^2.2.0": version "2.2.0" - resolved "https://registry.yarnpkg.com/@tonconnect/sdk/-/sdk-2.2.0.tgz#8b0432102a4634ed3a1d2de1f44e1e03e4059591" + resolved "https://registry.npmjs.org/@tonconnect/sdk/-/sdk-2.2.0.tgz" integrity sha512-8plnAXzaLhapUnt47ZqAOQSIQ8NHSvgTSR74QVJdPWqg8128smgGM4cDYewKdBfTD6Lup0odT1WMMrJu+rE4NQ== dependencies: "@tonconnect/isomorphic-eventsource" "^0.0.1" "@tonconnect/isomorphic-fetch" "^0.0.2" "@tonconnect/protocol" "^2.2.5" -"@tonstudio/parser-runtime@0.0.1", "@tonstudio/parser-runtime@^0.0.1": +"@tonstudio/parser-runtime@^0.0.1", "@tonstudio/parser-runtime@0.0.1": version "0.0.1" - resolved "https://registry.yarnpkg.com/@tonstudio/parser-runtime/-/parser-runtime-0.0.1.tgz#469955fb7ea354d4fadaa5964359b11fd17f926b" + resolved "https://registry.npmjs.org/@tonstudio/parser-runtime/-/parser-runtime-0.0.1.tgz" integrity sha512-5s4fLkXWxa4SAd7QGGvJXe13GakEo0J3VF5dUI/i3A//bGZxMwCp1FcnbErpNs3y0LcAZoXE5FCUnDowDQptqw== "@tsconfig/node10@^1.0.7": version "1.0.11" - resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.11.tgz#6ee46400685f130e278128c7b38b7e031ff5b2f2" + resolved "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz" integrity sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw== "@tsconfig/node12@^1.0.7": version "1.0.11" - resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" + resolved "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz" integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== "@tsconfig/node14@^1.0.0": version "1.0.3" - resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" + resolved "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz" integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== "@tsconfig/node16@^1.0.2": version "1.0.4" - resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" + resolved "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz" integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== -"@tybys/wasm-util@^0.10.0": - version "0.10.0" - resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.0.tgz#2fd3cd754b94b378734ce17058d0507c45c88369" - integrity sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ== - dependencies: - tslib "^2.4.0" - "@tychosdk/emulator@^0.2.6": version "0.2.6" - resolved "https://registry.yarnpkg.com/@tychosdk/emulator/-/emulator-0.2.6.tgz#5f52aaddd71d8c14a5cffe82b400eacd41ae01ad" + resolved "https://registry.npmjs.org/@tychosdk/emulator/-/emulator-0.2.6.tgz" integrity sha512-03pJ9RroOpyVQ7006Ib7YSYeQBXS3ZeLHriU53jliKR0hZQmU+JqxFyKpa2DAvzM+G8VYTvBKWTwsH1XVdkUlw== dependencies: axios "^1.8.4" zod "^3.24.2" -"@types/babel__core@^7.20.5": +"@types/babel__core@^7.1.14", "@types/babel__core@^7.20.5": version "7.20.5" - resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017" + resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz" integrity sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA== dependencies: "@babel/parser" "^7.20.7" @@ -949,55 +1115,62 @@ "@types/babel__generator@*": version "7.27.0" - resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.27.0.tgz#b5819294c51179957afaec341442f9341e4108a9" + resolved "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz" integrity sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg== dependencies: "@babel/types" "^7.0.0" "@types/babel__template@*": version "7.4.4" - resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.4.tgz#5672513701c1b2199bc6dad636a9d7491586766f" + resolved "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz" integrity sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A== dependencies: "@babel/parser" "^7.1.0" "@babel/types" "^7.0.0" -"@types/babel__traverse@*": +"@types/babel__traverse@*", "@types/babel__traverse@^7.0.6": version "7.28.0" - resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.28.0.tgz#07d713d6cce0d265c9849db0cbe62d3f61f36f74" + resolved "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz" integrity sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q== dependencies: "@babel/types" "^7.28.2" "@types/bn.js@^5.1.0": version "5.2.0" - resolved "https://registry.yarnpkg.com/@types/bn.js/-/bn.js-5.2.0.tgz#4349b9710e98f9ab3cdc50f1c5e4dcbd8ef29c80" + resolved "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.2.0.tgz" integrity sha512-DLbJ1BPqxvQhIGbeu8VbUC1DiAiahHtAYvA0ZEAa4P31F7IaArc8z3C3BRQdWX4mtLQuABG4yzp76ZrS02Ui1Q== dependencies: "@types/node" "*" -"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.1", "@types/istanbul-lib-coverage@^2.0.6": +"@types/graceful-fs@^4.1.3": + version "4.1.9" + resolved "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz" + integrity sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ== + dependencies: + "@types/node" "*" + +"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1", "@types/istanbul-lib-coverage@^2.0.6": version "2.0.6" - resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7" + resolved "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz" integrity sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w== "@types/istanbul-lib-report@*": version "3.0.3" - resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz#53047614ae72e19fc0401d872de3ae2b4ce350bf" + resolved "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz" integrity sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA== dependencies: "@types/istanbul-lib-coverage" "*" -"@types/istanbul-reports@^3.0.4": +"@types/istanbul-reports@^3.0.0", "@types/istanbul-reports@^3.0.4": version "3.0.4" - resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz#0f03e3d2f670fbdac586e34b433783070cc16f54" + resolved "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz" integrity sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ== dependencies: "@types/istanbul-lib-report" "*" "@types/jest@^30.0.0": version "30.0.0" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-30.0.0.tgz#5e85ae568006712e4ad66f25433e9bdac8801f1d" + resolved "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz" integrity sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA== dependencies: expect "^30.0.0" @@ -1005,174 +1178,80 @@ "@types/long@^4.0.1": version "4.0.2" - resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a" + resolved "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz" integrity sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA== -"@types/node@*", "@types/node@>=13.7.0": - version "24.3.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-24.3.0.tgz#89b09f45cb9a8ee69466f18ee5864e4c3eb84dec" - integrity sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow== - dependencies: - undici-types "~7.10.0" - -"@types/node@^22.15.32": +"@types/node@*", "@types/node@^22.15.32", "@types/node@>=13.7.0", "@types/node@>=18": version "22.17.2" - resolved "https://registry.yarnpkg.com/@types/node/-/node-22.17.2.tgz#47a93d6f4b79327da63af727e7c54e8cab8c4d33" + resolved "https://registry.npmjs.org/@types/node/-/node-22.17.2.tgz" integrity sha512-gL6z5N9Jm9mhY+U2KXZpteb+09zyffliRkZyZOHODGATyC5B1Jt/7TzuuiLkFsSUMLbS1OLmlj/E+/3KF4Q/4w== dependencies: undici-types "~6.21.0" "@types/pegjs@^0.10.3": version "0.10.6" - resolved "https://registry.yarnpkg.com/@types/pegjs/-/pegjs-0.10.6.tgz#bc20fc4809fed4cddab8d0dbee0e568803741a82" + resolved "https://registry.npmjs.org/@types/pegjs/-/pegjs-0.10.6.tgz" integrity sha512-eLYXDbZWXh2uxf+w8sXS8d6KSoXTswfps6fvCUuVAGN8eRpfe7h9eSRydxiSJvo9Bf+GzifsDOr9TMQlmJdmkw== -"@types/stack-utils@^2.0.3": +"@types/stack-utils@^2.0.0", "@types/stack-utils@^2.0.3": version "2.0.3" - resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8" + resolved "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz" integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw== "@types/yargs-parser@*": version "21.0.3" - resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.3.tgz#815e30b786d2e8f0dcd85fd5bcf5e1a04d008f15" + resolved "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz" integrity sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ== -"@types/yargs@^17.0.33": +"@types/yargs@^17.0.33", "@types/yargs@^17.0.8": version "17.0.33" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.33.tgz#8c32303da83eec050a84b3c7ae7b9f922d13e32d" + resolved "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz" integrity sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA== dependencies: "@types/yargs-parser" "*" "@ungap/structured-clone@^1.3.0": version "1.3.0" - resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz#d06bbb384ebcf6c505fde1c3d0ed4ddffe0aaff8" + resolved "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz" integrity sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g== -"@unrs/resolver-binding-android-arm-eabi@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz#9f5b04503088e6a354295e8ea8fe3cb99e43af81" - integrity sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw== - -"@unrs/resolver-binding-android-arm64@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz#7414885431bd7178b989aedc4d25cccb3865bc9f" - integrity sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g== - -"@unrs/resolver-binding-darwin-arm64@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz#b4a8556f42171fb9c9f7bac8235045e82aa0cbdf" - integrity sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g== - -"@unrs/resolver-binding-darwin-x64@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz#fd4d81257b13f4d1a083890a6a17c00de571f0dc" - integrity sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ== - -"@unrs/resolver-binding-freebsd-x64@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz#d2513084d0f37c407757e22f32bd924a78cfd99b" - integrity sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw== - -"@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz#844d2605d057488d77fab09705f2866b86164e0a" - integrity sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw== - -"@unrs/resolver-binding-linux-arm-musleabihf@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz#204892995cefb6bd1d017d52d097193bc61ddad3" - integrity sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw== - -"@unrs/resolver-binding-linux-arm64-gnu@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz#023eb0c3aac46066a10be7a3f362e7b34f3bdf9d" - integrity sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ== - -"@unrs/resolver-binding-linux-arm64-musl@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz#9e6f9abb06424e3140a60ac996139786f5d99be0" - integrity sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w== - -"@unrs/resolver-binding-linux-ppc64-gnu@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz#b111417f17c9d1b02efbec8e08398f0c5527bb44" - integrity sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA== - -"@unrs/resolver-binding-linux-riscv64-gnu@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz#92ffbf02748af3e99873945c9a8a5ead01d508a9" - integrity sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ== - -"@unrs/resolver-binding-linux-riscv64-musl@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz#0bec6f1258fc390e6b305e9ff44256cb207de165" - integrity sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew== - -"@unrs/resolver-binding-linux-s390x-gnu@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz#577843a084c5952f5906770633ccfb89dac9bc94" - integrity sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg== - "@unrs/resolver-binding-linux-x64-gnu@1.11.1": version "1.11.1" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz#36fb318eebdd690f6da32ac5e0499a76fa881935" + resolved "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz" integrity sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w== "@unrs/resolver-binding-linux-x64-musl@1.11.1": version "1.11.1" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz#bfb9af75f783f98f6a22c4244214efe4df1853d6" + resolved "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz" integrity sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA== -"@unrs/resolver-binding-wasm32-wasi@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz#752c359dd875684b27429500d88226d7cc72f71d" - integrity sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ== - dependencies: - "@napi-rs/wasm-runtime" "^0.2.11" - -"@unrs/resolver-binding-win32-arm64-msvc@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz#ce5735e600e4c2fbb409cd051b3b7da4a399af35" - integrity sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw== - -"@unrs/resolver-binding-win32-ia32-msvc@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz#72fc57bc7c64ec5c3de0d64ee0d1810317bc60a6" - integrity sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ== - -"@unrs/resolver-binding-win32-x64-msvc@1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz#538b1e103bf8d9864e7b85cc96fa8d6fb6c40777" - integrity sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g== - "@vscode/debugadapter@^1.68.0": version "1.68.0" - resolved "https://registry.yarnpkg.com/@vscode/debugadapter/-/debugadapter-1.68.0.tgz#abb23463cb750ca4a6f0834c5d4db659258dc159" + resolved "https://registry.npmjs.org/@vscode/debugadapter/-/debugadapter-1.68.0.tgz" integrity sha512-D6gk5Fw2y4FV8oYmltoXpj+VAZexxJFopN/mcZ6YcgzQE9dgq2L45Aj3GLxScJOD6GeLILcxJIaA8l3v11esGg== dependencies: "@vscode/debugprotocol" "1.68.0" "@vscode/debugprotocol@1.68.0": version "1.68.0" - resolved "https://registry.yarnpkg.com/@vscode/debugprotocol/-/debugprotocol-1.68.0.tgz#e558ba6affe1be7aff4ec824599f316b61d9a69d" + resolved "https://registry.npmjs.org/@vscode/debugprotocol/-/debugprotocol-1.68.0.tgz" integrity sha512-2J27dysaXmvnfuhFGhfeuxfHRXunqNPxtBoR3koiTOA9rdxWNDTa1zIFLCFMSHJ9MPTPKFcBeblsyaCJCIlQxg== acorn-walk@^8.1.1: version "8.3.4" - resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.4.tgz#794dd169c3977edf4ba4ea47583587c5866236b7" + resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz" integrity sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g== dependencies: acorn "^8.11.0" acorn@^8.11.0, acorn@^8.4.1: version "8.15.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816" + resolved "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz" integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg== adnl@^1.0.3: version "1.0.3" - resolved "https://registry.yarnpkg.com/adnl/-/adnl-1.0.3.tgz#1fb328b70c2fdce2617667efa9ef750bc5561d6a" + resolved "https://registry.npmjs.org/adnl/-/adnl-1.0.3.tgz" integrity sha512-P+jGWhWTp0f4EskKie5rUA+EnQINzq0qvu1N3UkNSjynIOWCzl0wOPGUo3IAJ2r9/MI9DnebRYO0e4wyX7qbBw== dependencies: "@noble/ed25519" "^1.6.1" @@ -1185,12 +1264,12 @@ adnl@^1.0.3: aes-js@^3.1.2: version "3.1.2" - resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-3.1.2.tgz#db9aabde85d5caabbfc0d4f2a4446960f627146a" + resolved "https://registry.npmjs.org/aes-js/-/aes-js-3.1.2.tgz" integrity sha512-e5pEa2kBnBOgR4Y/p20pskXI74UEz7de8ZGVo58asOtvSVG5YAbJeELPZxOmt+Bnz3rX753YKhfIn4X4l1PPRQ== ajv@^8.0.1: version "8.17.1" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6" + resolved "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz" integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g== dependencies: fast-deep-equal "^3.1.3" @@ -1200,41 +1279,46 @@ ajv@^8.0.1: ansi-escapes@^4.2.1, ansi-escapes@^4.3.2: version "4.3.2" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz" integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== dependencies: type-fest "^0.21.3" ansi-regex@^5.0.1: version "5.0.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== ansi-regex@^6.0.1: version "6.2.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.2.0.tgz#2f302e7550431b1b7762705fffb52cf1ffa20447" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz" integrity sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg== ansi-styles@^4.0.0, ansi-styles@^4.1.0: version "4.3.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== dependencies: color-convert "^2.0.1" +ansi-styles@^5.0.0: + version "5.2.0" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz" + integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== + ansi-styles@^5.2.0: version "5.2.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz" integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== ansi-styles@^6.1.0: version "6.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz" integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== -anymatch@^3.1.3: +anymatch@^3.0.3, anymatch@^3.1.3: version "3.1.3" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + resolved "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz" integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== dependencies: normalize-path "^3.0.0" @@ -1242,43 +1326,43 @@ anymatch@^3.1.3: arg@^4.1.0: version "4.1.3" - resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + resolved "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz" integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== arg@^5.0.2: version "5.0.2" - resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c" + resolved "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz" integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg== argparse@^1.0.7: version "1.0.10" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + resolved "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz" integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== dependencies: sprintf-js "~1.0.2" astral-regex@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" + resolved "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz" integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== asynckit@^0.4.0: version "0.4.0" - resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== axios@^1.6.7, axios@^1.7.7, axios@^1.8.4: version "1.11.0" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.11.0.tgz#c2ec219e35e414c025b2095e8b8280278478fdb6" + resolved "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz" integrity sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA== dependencies: follow-redirects "^1.15.6" form-data "^4.0.4" proxy-from-env "^1.1.0" -babel-jest@30.0.5: +"babel-jest@^29.0.0 || ^30.0.0", babel-jest@30.0.5: version "30.0.5" - resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-30.0.5.tgz#7cc7dd03d0d613125d458521f635b8c2361e89cc" + resolved "https://registry.npmjs.org/babel-jest/-/babel-jest-30.0.5.tgz" integrity sha512-mRijnKimhGDMsizTvBTWotwNpzrkHr+VvZUQBof2AufXKB8NXrL1W69TG20EvOz7aevx6FTJIaBuBkYxS8zolg== dependencies: "@jest/transform" "30.0.5" @@ -1289,9 +1373,33 @@ babel-jest@30.0.5: graceful-fs "^4.2.11" slash "^3.0.0" +babel-jest@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz" + integrity sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg== + dependencies: + "@jest/transform" "^29.7.0" + "@types/babel__core" "^7.1.14" + babel-plugin-istanbul "^6.1.1" + babel-preset-jest "^29.6.3" + chalk "^4.0.0" + graceful-fs "^4.2.9" + slash "^3.0.0" + +babel-plugin-istanbul@^6.1.1: + version "6.1.1" + resolved "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz" + integrity sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@istanbuljs/load-nyc-config" "^1.0.0" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-instrument "^5.0.4" + test-exclude "^6.0.0" + babel-plugin-istanbul@^7.0.0: version "7.0.0" - resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.0.tgz#629a178f63b83dc9ecee46fd20266283b1f11280" + resolved "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.0.tgz" integrity sha512-C5OzENSx/A+gt7t4VH1I2XsflxyPUmXRFPKBxt33xncdOmq7oROVM3bZv9Ysjjkv8OJYDMa+tKuKMvqU/H3xdw== dependencies: "@babel/helper-plugin-utils" "^7.0.0" @@ -1300,18 +1408,28 @@ babel-plugin-istanbul@^7.0.0: istanbul-lib-instrument "^6.0.2" test-exclude "^6.0.0" +babel-plugin-jest-hoist@^29.6.3: + version "29.6.3" + resolved "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz" + integrity sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg== + dependencies: + "@babel/template" "^7.3.3" + "@babel/types" "^7.3.3" + "@types/babel__core" "^7.1.14" + "@types/babel__traverse" "^7.0.6" + babel-plugin-jest-hoist@30.0.1: version "30.0.1" - resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.0.1.tgz#f271b2066d2c1fb26a863adb8e13f85b06247125" + resolved "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.0.1.tgz" integrity sha512-zTPME3pI50NsFW8ZBaVIOeAxzEY7XHlmWeXXu9srI+9kNfzCUTy8MFan46xOGZY8NZThMqq+e3qZUKsvXbasnQ== dependencies: "@babel/template" "^7.27.2" "@babel/types" "^7.27.3" "@types/babel__core" "^7.20.5" -babel-preset-current-node-syntax@^1.1.0: +babel-preset-current-node-syntax@^1.0.0, babel-preset-current-node-syntax@^1.1.0: version "1.2.0" - resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz#20730d6cdc7dda5d89401cab10ac6a32067acde6" + resolved "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz" integrity sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg== dependencies: "@babel/plugin-syntax-async-generators" "^7.8.4" @@ -1330,9 +1448,17 @@ babel-preset-current-node-syntax@^1.1.0: "@babel/plugin-syntax-private-property-in-object" "^7.14.5" "@babel/plugin-syntax-top-level-await" "^7.14.5" +babel-preset-jest@^29.6.3: + version "29.6.3" + resolved "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz" + integrity sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA== + dependencies: + babel-plugin-jest-hoist "^29.6.3" + babel-preset-current-node-syntax "^1.0.0" + babel-preset-jest@30.0.1: version "30.0.1" - resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-30.0.1.tgz#7d28db9531bce264e846c8483d54236244b8ae88" + resolved "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.0.1.tgz" integrity sha512-+YHejD5iTWI46cZmcc/YtX4gaKBtdqCHCVfuVinizVpbmyjO3zYmeuyFdfA8duRqQZfgCAMlsfmkVbJ+e2MAJw== dependencies: babel-plugin-jest-hoist "30.0.1" @@ -1340,17 +1466,17 @@ babel-preset-jest@30.0.1: balanced-match@^1.0.0: version "1.0.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== base64-js@^1.3.1: version "1.5.1" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== bl@^4.1.0: version "4.1.0" - resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" + resolved "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz" integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== dependencies: buffer "^5.5.0" @@ -1359,7 +1485,7 @@ bl@^4.1.0: bl@^5.0.0: version "5.1.0" - resolved "https://registry.yarnpkg.com/bl/-/bl-5.1.0.tgz#183715f678c7188ecef9fe475d90209400624273" + resolved "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz" integrity sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ== dependencies: buffer "^6.0.3" @@ -1368,7 +1494,7 @@ bl@^5.0.0: blockstore-core@1.0.5: version "1.0.5" - resolved "https://registry.yarnpkg.com/blockstore-core/-/blockstore-core-1.0.5.tgz#2e34b6a7faae0d4b6c98dc8573c6f998eb457f36" + resolved "https://registry.npmjs.org/blockstore-core/-/blockstore-core-1.0.5.tgz" integrity sha512-i/9CUMMvBALVbtSqUIuiWB3tk//a4Q2I2CEWiBuYNnhJvk/DWplXjLt8Sqc5VGkRVXVPSsEuH8fUtqJt5UFYcA== dependencies: err-code "^3.0.1" @@ -1382,12 +1508,12 @@ blockstore-core@1.0.5: bn.js@^5.2.0: version "5.2.2" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.2.tgz#82c09f9ebbb17107cd72cb7fd39bd1f9d0aaa566" + resolved "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz" integrity sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw== brace-expansion@^1.1.7: version "1.1.12" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.12.tgz#ab9b454466e5a8cc3a187beaad580412a9c5b843" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz" integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg== dependencies: balanced-match "^1.0.0" @@ -1395,21 +1521,21 @@ brace-expansion@^1.1.7: brace-expansion@^2.0.1: version "2.0.2" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.2.tgz#54fc53237a613d854c7bd37463aad17df87214e7" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz" integrity sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ== dependencies: balanced-match "^1.0.0" braces@^3.0.3: version "3.0.3" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + resolved "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz" integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== dependencies: fill-range "^7.1.1" -browserslist@^4.24.0: +browserslist@^4.24.0, "browserslist@>= 4.21.0": version "4.25.3" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.25.3.tgz#9167c9cbb40473f15f75f85189290678b99b16c5" + resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.25.3.tgz" integrity sha512-cDGv1kkDI4/0e5yON9yM5G/0A5u8sf5TnmdX5C9qHzI9PPu++sQ9zjm1k9NiOrf3riY4OkK0zSGqfvJyJsgCBQ== dependencies: caniuse-lite "^1.0.30001735" @@ -1419,26 +1545,26 @@ browserslist@^4.24.0: bs-logger@^0.2.6: version "0.2.6" - resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8" + resolved "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz" integrity sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog== dependencies: fast-json-stable-stringify "2.x" bser@2.1.1: version "2.1.1" - resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" + resolved "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz" integrity sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ== dependencies: node-int64 "^0.4.0" buffer-from@^1.0.0: version "1.1.2" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== buffer@^5.5.0: version "5.7.1" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" + resolved "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz" integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== dependencies: base64-js "^1.3.1" @@ -1446,7 +1572,7 @@ buffer@^5.5.0: buffer@^6.0.3: version "6.0.3" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + resolved "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz" integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== dependencies: base64-js "^1.3.1" @@ -1454,45 +1580,50 @@ buffer@^6.0.3: cac@^6.7.14: version "6.7.14" - resolved "https://registry.yarnpkg.com/cac/-/cac-6.7.14.tgz#804e1e6f506ee363cb0e3ccbb09cad5dd9870959" + resolved "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz" integrity sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ== call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" + resolved "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz" integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== dependencies: es-errors "^1.3.0" function-bind "^1.1.2" -callsites@^3.1.0: +callsites@^3.0.0, callsites@^3.1.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== camelcase@^5.3.1: version "5.3.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + resolved "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== +camelcase@^6.2.0: + version "6.3.0" + resolved "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + camelcase@^6.3.0: version "6.3.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + resolved "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== caniuse-lite@^1.0.30001735: version "1.0.30001736" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001736.tgz#3710a99cf154b653590fb6a57f81ee34173c3b47" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001736.tgz" integrity sha512-ImpN5gLEY8gWeqfLUyEF4b7mYWcYoR2Si1VhnrbM4JizRFmfGaAQ12PhNykq6nvI4XvKLrsp8Xde74D5phJOSw== case-shift@^2.5.3: version "2.5.3" - resolved "https://registry.yarnpkg.com/case-shift/-/case-shift-2.5.3.tgz#b86ca38a75ade4efabd65dd1b064eab42288d844" + resolved "https://registry.npmjs.org/case-shift/-/case-shift-2.5.3.tgz" integrity sha512-6SdS9W5xu82Kj1Z6f14h0zsbWTdXGtD0RftPNnqbAFFqqlzX1SMFi1E1NDIBg5LC2m9EYWWPUV80nTb3iu2e6Q== -chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2: +chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2: version "4.1.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== dependencies: ansi-styles "^4.1.0" @@ -1500,44 +1631,54 @@ chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2: char-regex@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" + resolved "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz" integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== chardet@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/chardet/-/chardet-2.1.0.tgz#1007f441a1ae9f9199a4a67f6e978fb0aa9aa3fe" + resolved "https://registry.npmjs.org/chardet/-/chardet-2.1.0.tgz" integrity sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA== +ci-info@^3.2.0: + version "3.9.0" + resolved "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz" + integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== + ci-info@^4.2.0: version "4.3.0" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-4.3.0.tgz#c39b1013f8fdbd28cd78e62318357d02da160cd7" + resolved "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz" integrity sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ== +cjs-module-lexer@^1.0.0: + version "1.4.3" + resolved "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz" + integrity sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q== + cjs-module-lexer@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-2.1.0.tgz#586e87d4341cb2661850ece5190232ccdebcff8b" + resolved "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.1.0.tgz" integrity sha512-UX0OwmYRYQQetfrLEZeewIFFI+wSTofC+pMBLNuH3RUuu/xzG1oz84UCEDOSoQlN3fZ4+AzmV50ZYvGqkMh9yA== cli-cursor@^3.1.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" + resolved "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz" integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== dependencies: restore-cursor "^3.1.0" cli-spinners@^2.5.0: version "2.9.2" - resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.9.2.tgz#1773a8f4b9c4d6ac31563df53b3fc1d79462fe41" + resolved "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz" integrity sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg== cli-width@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" + resolved "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz" integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== cliui@^8.0.1: version "8.0.1" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" + resolved "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz" integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== dependencies: string-width "^4.2.0" @@ -1546,66 +1687,79 @@ cliui@^8.0.1: clone@^1.0.2: version "1.0.4" - resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" + resolved "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz" integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== co@^4.6.0: version "4.6.0" - resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + resolved "https://registry.npmjs.org/co/-/co-4.6.0.tgz" integrity sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ== -collect-v8-coverage@^1.0.2: +collect-v8-coverage@^1.0.0, collect-v8-coverage@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz#c0b29bcd33bcd0779a1344c2136051e6afd3d9e9" + resolved "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz" integrity sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q== color-convert@^2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== dependencies: color-name "~1.1.4" color-name@~1.1.4: version "1.1.4" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== combined-stream@^1.0.8: version "1.0.8" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz" integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== dependencies: delayed-stream "~1.0.0" concat-map@0.0.1: version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== convert-source-map@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" + resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz" integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== core-js-pure@^3.38.0: version "3.45.1" - resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.45.1.tgz#b129d86a5f7f8380378577c7eaee83608570a05a" + resolved "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.45.1.tgz" integrity sha512-OHnWFKgTUshEU8MK+lOs1H8kC8GkTi9Z1tvNkxrCcw9wl3MJIO7q2ld77wjWn4/xuGrVu2X+nME1iIIPBSdyEQ== crc-32@^1.2.2: version "1.2.2" - resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.2.tgz#3cad35a934b8bf71f25ca524b6da51fb7eace2ff" + resolved "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz" integrity sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ== +create-jest@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz" + integrity sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q== + dependencies: + "@jest/types" "^29.6.3" + chalk "^4.0.0" + exit "^0.1.2" + graceful-fs "^4.2.9" + jest-config "^29.7.0" + jest-util "^29.7.0" + prompts "^2.0.1" + create-require@^1.1.0: version "1.1.1" - resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + resolved "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== cross-spawn@^7.0.3, cross-spawn@^7.0.6: version "7.0.6" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz" integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== dependencies: path-key "^3.1.0" @@ -1614,56 +1768,61 @@ cross-spawn@^7.0.3, cross-spawn@^7.0.6: dataloader@^2.0.0, dataloader@^2.1.0: version "2.2.3" - resolved "https://registry.yarnpkg.com/dataloader/-/dataloader-2.2.3.tgz#42d10b4913515f5b37c6acedcb4960d6ae1b1517" + resolved "https://registry.npmjs.org/dataloader/-/dataloader-2.2.3.tgz" integrity sha512-y2krtASINtPFS1rSDjacrFgn1dcUuoREVabwlOGOe4SdxenREqwjwjElAdwvbGM7kgZz9a3KVicWR7vcz8rnzA== debug@^4.1.0, debug@^4.1.1, debug@^4.3.1: version "4.4.1" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.1.tgz#e5a8bc6cbc4c6cd3e64308b0693a3d4fa550189b" + resolved "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz" integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ== dependencies: ms "^2.1.3" -dedent@^1.6.0: +dedent@^1.0.0, dedent@^1.6.0: version "1.6.0" - resolved "https://registry.yarnpkg.com/dedent/-/dedent-1.6.0.tgz#79d52d6389b1ffa67d2bcef59ba51847a9d503b2" + resolved "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz" integrity sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA== -deepmerge@^4.3.1: +deepmerge@^4.2.2, deepmerge@^4.3.1: version "4.3.1" - resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" + resolved "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz" integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== defaults@^1.0.3: version "1.0.4" - resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.4.tgz#b0b02062c1e2aa62ff5d9528f0f98baa90978d7a" + resolved "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz" integrity sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A== dependencies: clone "^1.0.2" delayed-stream@~1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== -detect-newline@^3.1.0: +detect-newline@^3.0.0, detect-newline@^3.1.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" + resolved "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz" integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== +diff-sequences@^29.6.3: + version "29.6.3" + resolved "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz" + integrity sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q== + diff@^4.0.1: version "4.0.2" - resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + resolved "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== dotenv@^16.1.4: version "16.6.1" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.6.1.tgz#773f0e69527a8315c7285d5ee73c4459d20a8020" + resolved "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz" integrity sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow== dunder-proto@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + resolved "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz" integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== dependencies: call-bind-apply-helpers "^1.0.1" @@ -1672,61 +1831,61 @@ dunder-proto@^1.0.1: eastasianwidth@^0.2.0: version "0.2.0" - resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" + resolved "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz" integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== electron-to-chromium@^1.5.204: version "1.5.207" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.207.tgz#0fedde3eec615065ee95531c09a10578644c5552" + resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.207.tgz" integrity sha512-mryFrrL/GXDTmAtIVMVf+eIXM09BBPlO5IQ7lUyKmK8d+A4VpRGG+M3ofoVef6qyF8s60rJei8ymlJxjUA8Faw== emittery@^0.13.1: version "0.13.1" - resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.13.1.tgz#c04b8c3457490e0847ae51fced3af52d338e3dad" + resolved "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz" integrity sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ== emoji-regex@^8.0.0: version "8.0.0" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== emoji-regex@^9.2.2: version "9.2.2" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" + resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz" integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== err-code@^3.0.1: version "3.0.1" - resolved "https://registry.yarnpkg.com/err-code/-/err-code-3.0.1.tgz#a444c7b992705f2b120ee320b09972eef331c920" + resolved "https://registry.npmjs.org/err-code/-/err-code-3.0.1.tgz" integrity sha512-GiaH0KJUewYok+eeY05IIgjtAe4Yltygk9Wqp1V5yVWLdhf0hYZchRjNIT9bb0mSwRcIusT3cx7PJUf3zEIfUA== error-ex@^1.3.1: version "1.3.2" - resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + resolved "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz" integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== dependencies: is-arrayish "^0.2.1" es-define-property@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + resolved "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz" integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== es-errors@^1.3.0: version "1.3.0" - resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + resolved "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz" integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" + resolved "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz" integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== dependencies: es-errors "^1.3.0" es-set-tostringtag@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d" + resolved "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz" integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA== dependencies: es-errors "^1.3.0" @@ -1736,37 +1895,37 @@ es-set-tostringtag@^2.1.0: escalade@^3.1.1, escalade@^3.2.0: version "3.2.0" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + resolved "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz" integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== escape-string-regexp@^1.0.5: version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz" integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== escape-string-regexp@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz" integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== esprima@^4.0.0: version "4.0.1" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + resolved "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== events@^3.3.0: version "3.3.0" - resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + resolved "https://registry.npmjs.org/events/-/events-3.3.0.tgz" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== eventsource@^2.0.2: version "2.0.2" - resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-2.0.2.tgz#76dfcc02930fb2ff339520b6d290da573a9e8508" + resolved "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz" integrity sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA== -execa@^5.1.1: +execa@^5.0.0, execa@^5.1.1: version "5.1.1" - resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + resolved "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz" integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== dependencies: cross-spawn "^7.0.3" @@ -1781,12 +1940,28 @@ execa@^5.1.1: exit-x@^0.2.2: version "0.2.2" - resolved "https://registry.yarnpkg.com/exit-x/-/exit-x-0.2.2.tgz#1f9052de3b8d99a696b10dad5bced9bdd5c3aa64" + resolved "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz" integrity sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ== -expect@30.0.5, expect@^30.0.0: +exit@^0.1.2: + version "0.1.2" + resolved "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz" + integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== + +expect@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz" + integrity sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw== + dependencies: + "@jest/expect-utils" "^29.7.0" + jest-get-type "^29.6.3" + jest-matcher-utils "^29.7.0" + jest-message-util "^29.7.0" + jest-util "^29.7.0" + +expect@^30.0.0, expect@30.0.5: version "30.0.5" - resolved "https://registry.yarnpkg.com/expect/-/expect-30.0.5.tgz#c23bf193c5e422a742bfd2990ad990811de41a5a" + resolved "https://registry.npmjs.org/expect/-/expect-30.0.5.tgz" integrity sha512-P0te2pt+hHI5qLJkIR+iMvS+lYUZml8rKKsohVHAGY+uClp9XVbdyYNJOIjSRpHVp8s8YqxJCiHUkSYZGr8rtQ== dependencies: "@jest/expect-utils" "30.0.5" @@ -1798,48 +1973,48 @@ expect@30.0.5, expect@^30.0.0: fast-deep-equal@^3.1.3: version "3.1.3" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== -fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.1.0: +fast-json-stable-stringify@^2.1.0, fast-json-stable-stringify@2.x: version "2.1.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== fast-uri@^3.0.1: version "3.0.6" - resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.6.tgz#88f130b77cfaea2378d56bf970dea21257a68748" + resolved "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz" integrity sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw== -fb-watchman@^2.0.2: +fb-watchman@^2.0.0, fb-watchman@^2.0.2: version "2.0.2" - resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.2.tgz#e9524ee6b5c77e9e5001af0f85f3adbb8623255c" + resolved "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz" integrity sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA== dependencies: bser "2.1.1" fflate@^0.8.2: version "0.8.2" - resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.8.2.tgz#fc8631f5347812ad6028bbe4a2308b2792aa1dea" + resolved "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz" integrity sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A== figures@^3.0.0: version "3.2.0" - resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" + resolved "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz" integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== dependencies: escape-string-regexp "^1.0.5" fill-range@^7.1.1: version "7.1.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz" integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== dependencies: to-regex-range "^5.0.1" find-up@^4.0.0, find-up@^4.1.0: version "4.1.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + resolved "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz" integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== dependencies: locate-path "^5.0.0" @@ -1847,12 +2022,12 @@ find-up@^4.0.0, find-up@^4.1.0: follow-redirects@^1.15.6: version "1.15.11" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.11.tgz#777d73d72a92f8ec4d2e410eb47352a56b8e8340" + resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz" integrity sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ== foreground-child@^3.1.0: version "3.3.1" - resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.1.tgz#32e8e9ed1b68a3497befb9ac2b6adf92a638576f" + resolved "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz" integrity sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw== dependencies: cross-spawn "^7.0.6" @@ -1860,7 +2035,7 @@ foreground-child@^3.1.0: form-data@^4.0.4: version "4.0.4" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.4.tgz#784cdcce0669a9d68e94d11ac4eea98088edd2c4" + resolved "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz" integrity sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow== dependencies: asynckit "^0.4.0" @@ -1871,32 +2046,27 @@ form-data@^4.0.4: fs.realpath@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== -fsevents@^2.3.3: - version "2.3.3" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" - integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== - function-bind@^1.1.2: version "1.1.2" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== gensync@^1.0.0-beta.2: version "1.0.0-beta.2" - resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz" integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== get-caller-file@^2.0.5: version "2.0.5" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== get-intrinsic@^1.2.6: version "1.3.0" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" + resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz" integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== dependencies: call-bind-apply-helpers "^1.0.2" @@ -1912,12 +2082,12 @@ get-intrinsic@^1.2.6: get-package-type@^0.1.0: version "0.1.0" - resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" + resolved "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz" integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== get-proto@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" + resolved "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz" integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== dependencies: dunder-proto "^1.0.1" @@ -1925,12 +2095,12 @@ get-proto@^1.0.1: get-stream@^6.0.0: version "6.0.1" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + resolved "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz" integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== glob@^10.3.10: version "10.4.5" - resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956" + resolved "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz" integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg== dependencies: foreground-child "^3.1.0" @@ -1940,9 +2110,21 @@ glob@^10.3.10: package-json-from-dist "^1.0.0" path-scurry "^1.11.1" +glob@^7.1.3: + version "7.2.3" + resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + glob@^7.1.4: version "7.2.3" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== dependencies: fs.realpath "^1.0.0" @@ -1954,7 +2136,7 @@ glob@^7.1.4: glob@^8.1.0: version "8.1.0" - resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" + resolved "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz" integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== dependencies: fs.realpath "^1.0.0" @@ -1965,17 +2147,17 @@ glob@^8.1.0: gopd@^1.2.0: version "1.2.0" - resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + resolved "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz" integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== -graceful-fs@^4.2.11: +graceful-fs@^4.2.11, graceful-fs@^4.2.9: version "4.2.11" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== hamt-sharding@^2.0.0: version "2.0.1" - resolved "https://registry.yarnpkg.com/hamt-sharding/-/hamt-sharding-2.0.1.tgz#f45686d0339e74b03b233bee1bde9587727129b6" + resolved "https://registry.npmjs.org/hamt-sharding/-/hamt-sharding-2.0.1.tgz" integrity sha512-vnjrmdXG9dDs1m/H4iJ6z0JFI2NtgsW5keRkTcM85NGak69Mkf5PHUqBz+Xs0T4sg0ppvj9O5EGAJo40FTxmmA== dependencies: sparse-array "^1.3.1" @@ -1983,7 +2165,7 @@ hamt-sharding@^2.0.0: handlebars@^4.7.8: version "4.7.8" - resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.8.tgz#41c42c18b1be2365439188c77c6afae71c0cd9e9" + resolved "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz" integrity sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ== dependencies: minimist "^1.2.5" @@ -1995,53 +2177,53 @@ handlebars@^4.7.8: has-flag@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== has-symbols@^1.0.3, has-symbols@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz" integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== has-tostringtag@^1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + resolved "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz" integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== dependencies: has-symbols "^1.0.3" hasown@^2.0.2: version "2.0.2" - resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz" integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== dependencies: function-bind "^1.1.2" html-escaper@^2.0.0: version "2.0.2" - resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" + resolved "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== human-signals@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + resolved "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz" integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== iconv-lite@^0.6.3: version "0.6.3" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz" integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== dependencies: safer-buffer ">= 2.1.2 < 3.0.0" ieee754@^1.1.13, ieee754@^1.2.1: version "1.2.1" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== -import-local@^3.2.0: +import-local@^3.0.2, import-local@^3.2.0: version "3.2.0" - resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.2.0.tgz#c3d5c745798c02a6f8b897726aba5100186ee260" + resolved "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz" integrity sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA== dependencies: pkg-dir "^4.2.0" @@ -2049,25 +2231,25 @@ import-local@^3.2.0: imurmurhash@^0.1.4: version "0.1.4" - resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== inflight@^1.0.4: version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== dependencies: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.3, inherits@^2.0.4: +inherits@^2.0.3, inherits@^2.0.4, inherits@2: version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== inquirer@^8.2.5: version "8.2.7" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.2.7.tgz#62f6b931a9b7f8735dc42db927316d8fb6f71de8" + resolved "https://registry.npmjs.org/inquirer/-/inquirer-8.2.7.tgz" integrity sha512-UjOaSel/iddGZJ5xP/Eixh6dY1XghiBw4XK13rCCIJcJfyhhoul/7KhLLUGtebEj6GDYM6Vnx/mVsjx2L/mFIA== dependencies: "@inquirer/external-editor" "^1.0.0" @@ -2088,7 +2270,7 @@ inquirer@^8.2.5: interface-blockstore@^2.0.2, interface-blockstore@^2.0.3: version "2.0.3" - resolved "https://registry.yarnpkg.com/interface-blockstore/-/interface-blockstore-2.0.3.tgz#b85270eb5180e65e46c9f66980a0fa4d98f5d73e" + resolved "https://registry.npmjs.org/interface-blockstore/-/interface-blockstore-2.0.3.tgz" integrity sha512-OwVUnlNcx7H5HloK0Myv6c/C1q9cNG11HX6afdeU6q6kbuNj8jKCwVnmJHhC94LZaJ+9hvVOk4IUstb3Esg81w== dependencies: interface-store "^2.0.2" @@ -2096,12 +2278,12 @@ interface-blockstore@^2.0.2, interface-blockstore@^2.0.3: interface-store@^2.0.1, interface-store@^2.0.2: version "2.0.2" - resolved "https://registry.yarnpkg.com/interface-store/-/interface-store-2.0.2.tgz#83175fd2b0c501585ed96db54bb8ba9d55fce34c" + resolved "https://registry.npmjs.org/interface-store/-/interface-store-2.0.2.tgz" integrity sha512-rScRlhDcz6k199EkHqT8NpM87ebN89ICOzILoBHgaG36/WX50N32BnU/kpZgCGPLhARRAWUUX5/cyaIjt7Kipg== ipfs-unixfs-importer@9.0.10: version "9.0.10" - resolved "https://registry.yarnpkg.com/ipfs-unixfs-importer/-/ipfs-unixfs-importer-9.0.10.tgz#2527ea0b4e018a9e80fa981101485babcd05c494" + resolved "https://registry.npmjs.org/ipfs-unixfs-importer/-/ipfs-unixfs-importer-9.0.10.tgz" integrity sha512-W+tQTVcSmXtFh7FWYWwPBGXJ1xDgREbIyI1E5JzDcimZLIyT5gGMfxR3oKPxxWj+GKMpP5ilvMQrbsPzWcm3Fw== dependencies: "@ipld/dag-pb" "^2.0.2" @@ -2122,7 +2304,7 @@ ipfs-unixfs-importer@9.0.10: ipfs-unixfs@^6.0.0: version "6.0.9" - resolved "https://registry.yarnpkg.com/ipfs-unixfs/-/ipfs-unixfs-6.0.9.tgz#f6613b8e081d83faa43ed96e016a694c615a9374" + resolved "https://registry.npmjs.org/ipfs-unixfs/-/ipfs-unixfs-6.0.9.tgz" integrity sha512-0DQ7p0/9dRB6XCb0mVCTli33GzIzSVx5udpJuVM47tGcD+W+Bl4LsnoLswd3ggNnNEakMv1FdoFITiEnchXDqQ== dependencies: err-code "^3.0.1" @@ -2130,62 +2312,80 @@ ipfs-unixfs@^6.0.0: is-arrayish@^0.2.1: version "0.2.1" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + resolved "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz" integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== +is-core-module@^2.16.0: + version "2.16.1" + resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz" + integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w== + dependencies: + hasown "^2.0.2" + is-fullwidth-code-point@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz" integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== -is-generator-fn@^2.1.0: +is-generator-fn@^2.0.0, is-generator-fn@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" + resolved "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz" integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== is-interactive@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e" + resolved "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz" integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w== is-number@^7.0.0: version "7.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== is-plain-obj@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" + resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz" integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== is-stream@^2.0.0: version "2.0.1" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + resolved "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz" integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== is-unicode-supported@^0.1.0: version "0.1.0" - resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + resolved "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz" integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== isexe@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== isomorphic-ws@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz#e5529148912ecb9b451b46ed44d53dae1ce04bbf" + resolved "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz" integrity sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw== istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: version "3.2.2" - resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz#2d166c4b0644d43a39f04bf6c2edd1e585f31756" + resolved "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz" integrity sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg== +istanbul-lib-instrument@^5.0.4: + version "5.2.1" + resolved "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz" + integrity sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg== + dependencies: + "@babel/core" "^7.12.3" + "@babel/parser" "^7.14.7" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-coverage "^3.2.0" + semver "^6.3.0" + istanbul-lib-instrument@^6.0.0, istanbul-lib-instrument@^6.0.2: version "6.0.3" - resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz#fa15401df6c15874bcb2105f773325d78c666765" + resolved "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz" integrity sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q== dependencies: "@babel/core" "^7.23.9" @@ -2196,16 +2396,25 @@ istanbul-lib-instrument@^6.0.0, istanbul-lib-instrument@^6.0.2: istanbul-lib-report@^3.0.0: version "3.0.1" - resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz#908305bac9a5bd175ac6a74489eafd0fc2445a7d" + resolved "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz" integrity sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw== dependencies: istanbul-lib-coverage "^3.0.0" make-dir "^4.0.0" supports-color "^7.1.0" +istanbul-lib-source-maps@^4.0.0: + version "4.0.1" + resolved "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz" + integrity sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw== + dependencies: + debug "^4.1.1" + istanbul-lib-coverage "^3.0.0" + source-map "^0.6.1" + istanbul-lib-source-maps@^5.0.0: version "5.0.6" - resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz#acaef948df7747c8eb5fbf1265cb980f6353a441" + resolved "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz" integrity sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A== dependencies: "@jridgewell/trace-mapping" "^0.3.23" @@ -2214,7 +2423,7 @@ istanbul-lib-source-maps@^5.0.0: istanbul-reports@^3.1.3: version "3.2.0" - resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.2.0.tgz#cb4535162b5784aa623cee21a7252cf2c807ac93" + resolved "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz" integrity sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA== dependencies: html-escaper "^2.0.0" @@ -2222,62 +2431,97 @@ istanbul-reports@^3.1.3: it-all@^1.0.4, it-all@^1.0.5: version "1.0.6" - resolved "https://registry.yarnpkg.com/it-all/-/it-all-1.0.6.tgz#852557355367606295c4c3b7eff0136f07749335" + resolved "https://registry.npmjs.org/it-all/-/it-all-1.0.6.tgz" integrity sha512-3cmCc6Heqe3uWi3CVM/k51fa/XbMFpQVzFoDsV0IZNHSQDyAXl3c4MjHkFX5kF3922OGj7Myv1nSEUgRtcuM1A== it-batch@^1.0.8, it-batch@^1.0.9: version "1.0.9" - resolved "https://registry.yarnpkg.com/it-batch/-/it-batch-1.0.9.tgz#7e95aaacb3f9b1b8ca6c8b8367892171d6a5b37f" + resolved "https://registry.npmjs.org/it-batch/-/it-batch-1.0.9.tgz" integrity sha512-7Q7HXewMhNFltTsAMdSz6luNhyhkhEtGGbYek/8Xb/GiqYMtwUmopE1ocPSiJKKp3rM4Dt045sNFoUu+KZGNyA== it-drain@^1.0.4: version "1.0.5" - resolved "https://registry.yarnpkg.com/it-drain/-/it-drain-1.0.5.tgz#0466d4e286b37bcd32599d4e99b37a87cb8cfdf6" + resolved "https://registry.npmjs.org/it-drain/-/it-drain-1.0.5.tgz" integrity sha512-r/GjkiW1bZswC04TNmUnLxa6uovme7KKwPhc+cb1hHU65E3AByypHH6Pm91WHuvqfFsm+9ws0kPtDBV3/8vmIg== it-filter@^1.0.2: version "1.0.3" - resolved "https://registry.yarnpkg.com/it-filter/-/it-filter-1.0.3.tgz#66ea0cc4bf84af71bebd353c05a9c5735fcba751" + resolved "https://registry.npmjs.org/it-filter/-/it-filter-1.0.3.tgz" integrity sha512-EI3HpzUrKjTH01miLHWmhNWy3Xpbx4OXMXltgrNprL5lDpF3giVpHIouFpr5l+evXw6aOfxhnt01BIB+4VQA+w== it-first@^1.0.6: version "1.0.7" - resolved "https://registry.yarnpkg.com/it-first/-/it-first-1.0.7.tgz#a4bef40da8be21667f7d23e44dae652f5ccd7ab1" + resolved "https://registry.npmjs.org/it-first/-/it-first-1.0.7.tgz" integrity sha512-nvJKZoBpZD/6Rtde6FXqwDqDZGF1sCADmr2Zoc0hZsIvnE449gRFnGctxDf09Bzc/FWnHXAdaHVIetY6lrE0/g== it-parallel-batch@^1.0.9: version "1.0.11" - resolved "https://registry.yarnpkg.com/it-parallel-batch/-/it-parallel-batch-1.0.11.tgz#f889b4e1c7a62ef24111dbafbaaa010b33d00f69" + resolved "https://registry.npmjs.org/it-parallel-batch/-/it-parallel-batch-1.0.11.tgz" integrity sha512-UWsWHv/kqBpMRmyZJzlmZeoAMA0F3SZr08FBdbhtbe+MtoEBgr/ZUAKrnenhXCBrsopy76QjRH2K/V8kNdupbQ== dependencies: it-batch "^1.0.9" it-take@^1.0.1: version "1.0.2" - resolved "https://registry.yarnpkg.com/it-take/-/it-take-1.0.2.tgz#b5f1570014db7c3454897898b69bb7ac9c3bffc1" + resolved "https://registry.npmjs.org/it-take/-/it-take-1.0.2.tgz" integrity sha512-u7I6qhhxH7pSevcYNaMECtkvZW365ARqAIt9K+xjdK1B2WUDEjQSfETkOCT8bxFq/59LqrN3cMLUtTgmDBaygw== jackspeak@^3.1.2: version "3.4.3" - resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a" + resolved "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz" integrity sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw== dependencies: "@isaacs/cliui" "^8.0.2" optionalDependencies: "@pkgjs/parseargs" "^0.11.0" +jest-changed-files@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz" + integrity sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w== + dependencies: + execa "^5.0.0" + jest-util "^29.7.0" + p-limit "^3.1.0" + jest-changed-files@30.0.5: version "30.0.5" - resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-30.0.5.tgz#ec448f83bd9caa894dd7da8707f207c356a19924" + resolved "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.0.5.tgz" integrity sha512-bGl2Ntdx0eAwXuGpdLdVYVr5YQHnSZlQ0y9HVDu565lCUAe9sj6JOtBbMmBBikGIegne9piDDIOeiLVoqTkz4A== dependencies: execa "^5.1.1" jest-util "30.0.5" p-limit "^3.1.0" +jest-circus@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz" + integrity sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw== + dependencies: + "@jest/environment" "^29.7.0" + "@jest/expect" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + chalk "^4.0.0" + co "^4.6.0" + dedent "^1.0.0" + is-generator-fn "^2.0.0" + jest-each "^29.7.0" + jest-matcher-utils "^29.7.0" + jest-message-util "^29.7.0" + jest-runtime "^29.7.0" + jest-snapshot "^29.7.0" + jest-util "^29.7.0" + p-limit "^3.1.0" + pretty-format "^29.7.0" + pure-rand "^6.0.0" + slash "^3.0.0" + stack-utils "^2.0.3" + jest-circus@30.0.5: version "30.0.5" - resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-30.0.5.tgz#9b4d44feb56c7ffe14411ad7fc08af188c5d4da7" + resolved "https://registry.npmjs.org/jest-circus/-/jest-circus-30.0.5.tgz" integrity sha512-h/sjXEs4GS+NFFfqBDYT7y5Msfxh04EwWLhQi0F8kuWpe+J/7tICSlswU8qvBqumR3kFgHbfu7vU6qruWWBPug== dependencies: "@jest/environment" "30.0.5" @@ -2301,9 +2545,26 @@ jest-circus@30.0.5: slash "^3.0.0" stack-utils "^2.0.6" +jest-cli@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz" + integrity sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg== + dependencies: + "@jest/core" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/types" "^29.6.3" + chalk "^4.0.0" + create-jest "^29.7.0" + exit "^0.1.2" + import-local "^3.0.2" + jest-config "^29.7.0" + jest-util "^29.7.0" + jest-validate "^29.7.0" + yargs "^17.3.1" + jest-cli@30.0.5: version "30.0.5" - resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-30.0.5.tgz#c3fbfdabd1a5c428429476f915a1ba6d0774cc50" + resolved "https://registry.npmjs.org/jest-cli/-/jest-cli-30.0.5.tgz" integrity sha512-Sa45PGMkBZzF94HMrlX4kUyPOwUpdZasaliKN3mifvDmkhLYqLLg8HQTzn6gq7vJGahFYMQjXgyJWfYImKZzOw== dependencies: "@jest/core" "30.0.5" @@ -2317,9 +2578,37 @@ jest-cli@30.0.5: jest-validate "30.0.5" yargs "^17.7.2" +jest-config@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz" + integrity sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ== + dependencies: + "@babel/core" "^7.11.6" + "@jest/test-sequencer" "^29.7.0" + "@jest/types" "^29.6.3" + babel-jest "^29.7.0" + chalk "^4.0.0" + ci-info "^3.2.0" + deepmerge "^4.2.2" + glob "^7.1.3" + graceful-fs "^4.2.9" + jest-circus "^29.7.0" + jest-environment-node "^29.7.0" + jest-get-type "^29.6.3" + jest-regex-util "^29.6.3" + jest-resolve "^29.7.0" + jest-runner "^29.7.0" + jest-util "^29.7.0" + jest-validate "^29.7.0" + micromatch "^4.0.4" + parse-json "^5.2.0" + pretty-format "^29.7.0" + slash "^3.0.0" + strip-json-comments "^3.1.1" + jest-config@30.0.5: version "30.0.5" - resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-30.0.5.tgz#567cf39b595229b786506a496c22e222d5e8d480" + resolved "https://registry.npmjs.org/jest-config/-/jest-config-30.0.5.tgz" integrity sha512-aIVh+JNOOpzUgzUnPn5FLtyVnqc3TQHVMupYtyeURSb//iLColiMIR8TxCIDKyx9ZgjKnXGucuW68hCxgbrwmA== dependencies: "@babel/core" "^7.27.4" @@ -2347,9 +2636,19 @@ jest-config@30.0.5: slash "^3.0.0" strip-json-comments "^3.1.1" +jest-diff@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz" + integrity sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw== + dependencies: + chalk "^4.0.0" + diff-sequences "^29.6.3" + jest-get-type "^29.6.3" + pretty-format "^29.7.0" + jest-diff@30.0.5: version "30.0.5" - resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-30.0.5.tgz#b40f81e0c0d13e5b81c4d62b0d0dfa6a524ee0fd" + resolved "https://registry.npmjs.org/jest-diff/-/jest-diff-30.0.5.tgz" integrity sha512-1UIqE9PoEKaHcIKvq2vbibrCog4Y8G0zmOxgQUVEiTqwR5hJVMCoDsN1vFvI5JvwD37hjueZ1C4l2FyGnfpE0A== dependencies: "@jest/diff-sequences" "30.0.1" @@ -2357,16 +2656,34 @@ jest-diff@30.0.5: chalk "^4.1.2" pretty-format "30.0.5" +jest-docblock@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz" + integrity sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g== + dependencies: + detect-newline "^3.0.0" + jest-docblock@30.0.1: version "30.0.1" - resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-30.0.1.tgz#545ff59f2fa88996bd470dba7d3798a8421180b1" + resolved "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.0.1.tgz" integrity sha512-/vF78qn3DYphAaIc3jy4gA7XSAz167n9Bm/wn/1XhTLW7tTBIzXtCJpb/vcmc73NIIeeohCbdL94JasyXUZsGA== dependencies: detect-newline "^3.1.0" +jest-each@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz" + integrity sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ== + dependencies: + "@jest/types" "^29.6.3" + chalk "^4.0.0" + jest-get-type "^29.6.3" + jest-util "^29.7.0" + pretty-format "^29.7.0" + jest-each@30.0.5: version "30.0.5" - resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-30.0.5.tgz#5962264ff246cd757ba44db096c1bc5b4835173e" + resolved "https://registry.npmjs.org/jest-each/-/jest-each-30.0.5.tgz" integrity sha512-dKjRsx1uZ96TVyejD3/aAWcNKy6ajMaN531CwWIsrazIqIoXI9TnnpPlkrEYku/8rkS3dh2rbH+kMOyiEIv0xQ== dependencies: "@jest/get-type" "30.0.1" @@ -2375,9 +2692,21 @@ jest-each@30.0.5: jest-util "30.0.5" pretty-format "30.0.5" +jest-environment-node@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz" + integrity sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw== + dependencies: + "@jest/environment" "^29.7.0" + "@jest/fake-timers" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + jest-mock "^29.7.0" + jest-util "^29.7.0" + jest-environment-node@30.0.5: version "30.0.5" - resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-30.0.5.tgz#6a98dd80e0384ead67ed05643381395f6cda93c9" + resolved "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.0.5.tgz" integrity sha512-ppYizXdLMSvciGsRsMEnv/5EFpvOdXBaXRBzFUDPWrsfmog4kYrOGWXarLllz6AXan6ZAA/kYokgDWuos1IKDA== dependencies: "@jest/environment" "30.0.5" @@ -2388,9 +2717,33 @@ jest-environment-node@30.0.5: jest-util "30.0.5" jest-validate "30.0.5" +jest-get-type@^29.6.3: + version "29.6.3" + resolved "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz" + integrity sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw== + +jest-haste-map@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz" + integrity sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA== + dependencies: + "@jest/types" "^29.6.3" + "@types/graceful-fs" "^4.1.3" + "@types/node" "*" + anymatch "^3.0.3" + fb-watchman "^2.0.0" + graceful-fs "^4.2.9" + jest-regex-util "^29.6.3" + jest-util "^29.7.0" + jest-worker "^29.7.0" + micromatch "^4.0.4" + walker "^1.0.8" + optionalDependencies: + fsevents "^2.3.2" + jest-haste-map@30.0.5: version "30.0.5" - resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-30.0.5.tgz#fdd0daa322b02eb34267854cff2859fae21e92a6" + resolved "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.0.5.tgz" integrity sha512-dkmlWNlsTSR0nH3nRfW5BKbqHefLZv0/6LCccG0xFCTWcJu8TuEwG+5Cm75iBfjVoockmO6J35o5gxtFSn5xeg== dependencies: "@jest/types" "30.0.5" @@ -2406,17 +2759,35 @@ jest-haste-map@30.0.5: optionalDependencies: fsevents "^2.3.3" +jest-leak-detector@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz" + integrity sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw== + dependencies: + jest-get-type "^29.6.3" + pretty-format "^29.7.0" + jest-leak-detector@30.0.5: version "30.0.5" - resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-30.0.5.tgz#00cfd2b323f48d8f4416b0a3e05fcf4c51f18864" + resolved "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.0.5.tgz" integrity sha512-3Uxr5uP8jmHMcsOtYMRB/zf1gXN3yUIc+iPorhNETG54gErFIiUhLvyY/OggYpSMOEYqsmRxmuU4ZOoX5jpRFg== dependencies: "@jest/get-type" "30.0.1" pretty-format "30.0.5" +jest-matcher-utils@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz" + integrity sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g== + dependencies: + chalk "^4.0.0" + jest-diff "^29.7.0" + jest-get-type "^29.6.3" + pretty-format "^29.7.0" + jest-matcher-utils@30.0.5: version "30.0.5" - resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-30.0.5.tgz#dff3334be58faea4a5e1becc228656fbbfc2467d" + resolved "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.0.5.tgz" integrity sha512-uQgGWt7GOrRLP1P7IwNWwK1WAQbq+m//ZY0yXygyfWp0rJlksMSLQAA4wYQC3b6wl3zfnchyTx+k3HZ5aPtCbQ== dependencies: "@jest/get-type" "30.0.1" @@ -2424,9 +2795,24 @@ jest-matcher-utils@30.0.5: jest-diff "30.0.5" pretty-format "30.0.5" +jest-message-util@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz" + integrity sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w== + dependencies: + "@babel/code-frame" "^7.12.13" + "@jest/types" "^29.6.3" + "@types/stack-utils" "^2.0.0" + chalk "^4.0.0" + graceful-fs "^4.2.9" + micromatch "^4.0.4" + pretty-format "^29.7.0" + slash "^3.0.0" + stack-utils "^2.0.3" + jest-message-util@30.0.5: version "30.0.5" - resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-30.0.5.tgz#dd12ffec91dd3fa6a59cbd538a513d8e239e070c" + resolved "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.0.5.tgz" integrity sha512-NAiDOhsK3V7RU0Aa/HnrQo+E4JlbarbmI3q6Pi4KcxicdtjV82gcIUrejOtczChtVQR4kddu1E1EJlW6EN9IyA== dependencies: "@babel/code-frame" "^7.27.1" @@ -2439,36 +2825,58 @@ jest-message-util@30.0.5: slash "^3.0.0" stack-utils "^2.0.6" +jest-mock@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz" + integrity sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw== + dependencies: + "@jest/types" "^29.6.3" + "@types/node" "*" + jest-util "^29.7.0" + jest-mock@30.0.5: version "30.0.5" - resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-30.0.5.tgz#ef437e89212560dd395198115550085038570bdd" + resolved "https://registry.npmjs.org/jest-mock/-/jest-mock-30.0.5.tgz" integrity sha512-Od7TyasAAQX/6S+QCbN6vZoWOMwlTtzzGuxJku1GhGanAjz9y+QsQkpScDmETvdc9aSXyJ/Op4rhpMYBWW91wQ== dependencies: "@jest/types" "30.0.5" "@types/node" "*" jest-util "30.0.5" -jest-pnp-resolver@^1.2.3: +jest-pnp-resolver@^1.2.2, jest-pnp-resolver@^1.2.3: version "1.2.3" - resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz#930b1546164d4ad5937d5540e711d4d38d4cad2e" + resolved "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz" integrity sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w== +jest-regex-util@^29.6.3: + version "29.6.3" + resolved "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz" + integrity sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg== + jest-regex-util@30.0.1: version "30.0.1" - resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-30.0.1.tgz#f17c1de3958b67dfe485354f5a10093298f2a49b" + resolved "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz" integrity sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA== +jest-resolve-dependencies@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz" + integrity sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA== + dependencies: + jest-regex-util "^29.6.3" + jest-snapshot "^29.7.0" + jest-resolve-dependencies@30.0.5: version "30.0.5" - resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-30.0.5.tgz#53be4c51d296c84a0e75608e7b77b6fe92dbac29" + resolved "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.0.5.tgz" integrity sha512-/xMvBR4MpwkrHW4ikZIWRttBBRZgWK4d6xt3xW1iRDSKt4tXzYkMkyPfBnSCgv96cpkrctfXs6gexeqMYqdEpw== dependencies: jest-regex-util "30.0.1" jest-snapshot "30.0.5" -jest-resolve@30.0.5: +jest-resolve@*, jest-resolve@30.0.5: version "30.0.5" - resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-30.0.5.tgz#f52f91600070b7073db465dc553eee5471ea8e06" + resolved "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.0.5.tgz" integrity sha512-d+DjBQ1tIhdz91B79mywH5yYu76bZuE96sSbxj8MkjWVx5WNdt1deEFRONVL4UkKLSrAbMkdhb24XN691yDRHg== dependencies: chalk "^4.1.2" @@ -2480,9 +2888,51 @@ jest-resolve@30.0.5: slash "^3.0.0" unrs-resolver "^1.7.11" +jest-resolve@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz" + integrity sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA== + dependencies: + chalk "^4.0.0" + graceful-fs "^4.2.9" + jest-haste-map "^29.7.0" + jest-pnp-resolver "^1.2.2" + jest-util "^29.7.0" + jest-validate "^29.7.0" + resolve "^1.20.0" + resolve.exports "^2.0.0" + slash "^3.0.0" + +jest-runner@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz" + integrity sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ== + dependencies: + "@jest/console" "^29.7.0" + "@jest/environment" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + chalk "^4.0.0" + emittery "^0.13.1" + graceful-fs "^4.2.9" + jest-docblock "^29.7.0" + jest-environment-node "^29.7.0" + jest-haste-map "^29.7.0" + jest-leak-detector "^29.7.0" + jest-message-util "^29.7.0" + jest-resolve "^29.7.0" + jest-runtime "^29.7.0" + jest-util "^29.7.0" + jest-watcher "^29.7.0" + jest-worker "^29.7.0" + p-limit "^3.1.0" + source-map-support "0.5.13" + jest-runner@30.0.5: version "30.0.5" - resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-30.0.5.tgz#5cbaaf85964246da4f65d697f186846f23cd9b5a" + resolved "https://registry.npmjs.org/jest-runner/-/jest-runner-30.0.5.tgz" integrity sha512-JcCOucZmgp+YuGgLAXHNy7ualBx4wYSgJVWrYMRBnb79j9PD0Jxh0EHvR5Cx/r0Ce+ZBC4hCdz2AzFFLl9hCiw== dependencies: "@jest/console" "30.0.5" @@ -2508,9 +2958,37 @@ jest-runner@30.0.5: p-limit "^3.1.0" source-map-support "0.5.13" +jest-runtime@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz" + integrity sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ== + dependencies: + "@jest/environment" "^29.7.0" + "@jest/fake-timers" "^29.7.0" + "@jest/globals" "^29.7.0" + "@jest/source-map" "^29.6.3" + "@jest/test-result" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + chalk "^4.0.0" + cjs-module-lexer "^1.0.0" + collect-v8-coverage "^1.0.0" + glob "^7.1.3" + graceful-fs "^4.2.9" + jest-haste-map "^29.7.0" + jest-message-util "^29.7.0" + jest-mock "^29.7.0" + jest-regex-util "^29.6.3" + jest-resolve "^29.7.0" + jest-snapshot "^29.7.0" + jest-util "^29.7.0" + slash "^3.0.0" + strip-bom "^4.0.0" + jest-runtime@30.0.5: version "30.0.5" - resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-30.0.5.tgz#d6a7e22687264240d1786d6f7682ac6a2872e552" + resolved "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.0.5.tgz" integrity sha512-7oySNDkqpe4xpX5PPiJTe5vEa+Ak/NnNz2bGYZrA1ftG3RL3EFlHaUkA1Cjx+R8IhK0Vg43RML5mJedGTPNz3A== dependencies: "@jest/environment" "30.0.5" @@ -2536,9 +3014,35 @@ jest-runtime@30.0.5: slash "^3.0.0" strip-bom "^4.0.0" +jest-snapshot@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz" + integrity sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw== + dependencies: + "@babel/core" "^7.11.6" + "@babel/generator" "^7.7.2" + "@babel/plugin-syntax-jsx" "^7.7.2" + "@babel/plugin-syntax-typescript" "^7.7.2" + "@babel/types" "^7.3.3" + "@jest/expect-utils" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" + babel-preset-current-node-syntax "^1.0.0" + chalk "^4.0.0" + expect "^29.7.0" + graceful-fs "^4.2.9" + jest-diff "^29.7.0" + jest-get-type "^29.6.3" + jest-matcher-utils "^29.7.0" + jest-message-util "^29.7.0" + jest-util "^29.7.0" + natural-compare "^1.4.0" + pretty-format "^29.7.0" + semver "^7.5.3" + jest-snapshot@30.0.5: version "30.0.5" - resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-30.0.5.tgz#6600716eef2e6d8ea1dd788ae4385f3a2791b11f" + resolved "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.0.5.tgz" integrity sha512-T00dWU/Ek3LqTp4+DcW6PraVxjk28WY5Ua/s+3zUKSERZSNyxTqhDXCWKG5p2HAJ+crVQ3WJ2P9YVHpj1tkW+g== dependencies: "@babel/core" "^7.27.4" @@ -2563,9 +3067,9 @@ jest-snapshot@30.0.5: semver "^7.7.2" synckit "^0.11.8" -jest-util@30.0.5: +"jest-util@^29.0.0 || ^30.0.0", jest-util@30.0.5: version "30.0.5" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-30.0.5.tgz#035d380c660ad5f1748dff71c4105338e05f8669" + resolved "https://registry.npmjs.org/jest-util/-/jest-util-30.0.5.tgz" integrity sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g== dependencies: "@jest/types" "30.0.5" @@ -2575,9 +3079,33 @@ jest-util@30.0.5: graceful-fs "^4.2.11" picomatch "^4.0.2" +jest-util@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz" + integrity sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA== + dependencies: + "@jest/types" "^29.6.3" + "@types/node" "*" + chalk "^4.0.0" + ci-info "^3.2.0" + graceful-fs "^4.2.9" + picomatch "^2.2.3" + +jest-validate@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz" + integrity sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw== + dependencies: + "@jest/types" "^29.6.3" + camelcase "^6.2.0" + chalk "^4.0.0" + jest-get-type "^29.6.3" + leven "^3.1.0" + pretty-format "^29.7.0" + jest-validate@30.0.5: version "30.0.5" - resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-30.0.5.tgz#d26fd218b8d566bff48fd98880b8ea94fd0d8456" + resolved "https://registry.npmjs.org/jest-validate/-/jest-validate-30.0.5.tgz" integrity sha512-ouTm6VFHaS2boyl+k4u+Qip4TSH7Uld5tyD8psQ8abGgt2uYYB8VwVfAHWHjHc0NWmGGbwO5h0sCPOGHHevefw== dependencies: "@jest/get-type" "30.0.1" @@ -2587,9 +3115,23 @@ jest-validate@30.0.5: leven "^3.1.0" pretty-format "30.0.5" +jest-watcher@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz" + integrity sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g== + dependencies: + "@jest/test-result" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + emittery "^0.13.1" + jest-util "^29.7.0" + string-length "^4.0.1" + jest-watcher@30.0.5: version "30.0.5" - resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-30.0.5.tgz#90db6e3f582b88085bde58f7555cbdd3a1beb10d" + resolved "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.0.5.tgz" integrity sha512-z9slj/0vOwBDBjN3L4z4ZYaA+pG56d6p3kTUhFRYGvXbXMWhXmb/FIxREZCD06DYUwDKKnj2T80+Pb71CQ0KEg== dependencies: "@jest/test-result" "30.0.5" @@ -2601,9 +3143,19 @@ jest-watcher@30.0.5: jest-util "30.0.5" string-length "^4.0.2" +jest-worker@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz" + integrity sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw== + dependencies: + "@types/node" "*" + jest-util "^29.7.0" + merge-stream "^2.0.0" + supports-color "^8.0.0" + jest-worker@30.0.5: version "30.0.5" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-30.0.5.tgz#0b85cbab10610303e8d84e214f94d8f052c3cd04" + resolved "https://registry.npmjs.org/jest-worker/-/jest-worker-30.0.5.tgz" integrity sha512-ojRXsWzEP16NdUuBw/4H/zkZdHOa7MMYCk4E430l+8fELeLg/mqmMlRhjL7UNZvQrDmnovWZV4DxX03fZF48fQ== dependencies: "@types/node" "*" @@ -2612,9 +3164,9 @@ jest-worker@30.0.5: merge-stream "^2.0.0" supports-color "^8.1.1" -jest@^30.0.0: +"jest@^29.0.0 || ^30.0.0", "jest@^29.5.0 || ^30.0.5", jest@^30.0.0: version "30.0.5" - resolved "https://registry.yarnpkg.com/jest/-/jest-30.0.5.tgz#ee62729fb77829790d67c660d852350fbde315ce" + resolved "https://registry.npmjs.org/jest/-/jest-30.0.5.tgz" integrity sha512-y2mfcJywuTUkvLm2Lp1/pFX8kTgMO5yyQGq/Sk/n2mN7XWYp4JsCZ/QXW34M8YScgk8bPZlREH04f6blPnoHnQ== dependencies: "@jest/core" "30.0.5" @@ -2622,14 +3174,24 @@ jest@^30.0.0: import-local "^3.2.0" jest-cli "30.0.5" +jest@^29.5.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz" + integrity sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw== + dependencies: + "@jest/core" "^29.7.0" + "@jest/types" "^29.6.3" + import-local "^3.0.2" + jest-cli "^29.7.0" + js-tokens@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== js-yaml@^3.13.1: version "3.14.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" + resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz" integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== dependencies: argparse "^1.0.7" @@ -2637,64 +3199,69 @@ js-yaml@^3.13.1: jsesc@^3.0.2: version "3.1.0" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d" + resolved "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz" integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== json-parse-even-better-errors@^2.3.0: version "2.3.1" - resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + resolved "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz" integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== json-schema-traverse@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz" integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== json5@^2.2.3: version "2.2.3" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + resolved "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== jssha@3.2.0: version "3.2.0" - resolved "https://registry.yarnpkg.com/jssha/-/jssha-3.2.0.tgz#88ec50b866dd1411deaddbe6b3e3692e4c710f16" + resolved "https://registry.npmjs.org/jssha/-/jssha-3.2.0.tgz" integrity sha512-QuruyBENDWdN4tZwJbQq7/eAK85FqrI4oDbXjy5IBhYD+2pTJyBUWZe8ctWaCkrV0gy6AaelgOZZBMeswEa/6Q== +kleur@^3.0.3: + version "3.0.3" + resolved "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz" + integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== + leven@^3.1.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" + resolved "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz" integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== lines-and-columns@^1.1.6: version "1.2.4" - resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" + resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz" integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== locate-path@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + resolved "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz" integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== dependencies: p-locate "^4.1.0" lodash.memoize@^4.1.2: version "4.1.2" - resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + resolved "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz" integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag== lodash.truncate@^4.4.2: version "4.4.2" - resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" + resolved "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz" integrity sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw== lodash@^4.17.21: version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== log-symbols@^4.1.0: version "4.1.0" - resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + resolved "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz" integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== dependencies: chalk "^4.1.0" @@ -2702,65 +3269,65 @@ log-symbols@^4.1.0: long@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" + resolved "https://registry.npmjs.org/long/-/long-4.0.0.tgz" integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== +lru_map@^0.4.1: + version "0.4.1" + resolved "https://registry.npmjs.org/lru_map/-/lru_map-0.4.1.tgz" + integrity sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg== + lru-cache@^10.2.0: version "10.4.3" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" + resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz" integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== lru-cache@^5.1.1: version "5.1.1" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz" integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== dependencies: yallist "^3.0.2" -lru_map@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/lru_map/-/lru_map-0.4.1.tgz#f7b4046283c79fb7370c36f8fca6aee4324b0a98" - integrity sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg== - make-dir@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-4.0.0.tgz#c3c2307a771277cd9638305f915c29ae741b614e" + resolved "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz" integrity sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw== dependencies: semver "^7.5.3" make-error@^1.1.1, make-error@^1.3.6: version "1.3.6" - resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + resolved "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== makeerror@1.0.12: version "1.0.12" - resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.12.tgz#3e5dd2079a82e812e983cc6610c4a2cb0eaa801a" + resolved "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz" integrity sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg== dependencies: tmpl "1.0.5" math-intrinsics@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" + resolved "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz" integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== merge-options@^3.0.4: version "3.0.4" - resolved "https://registry.yarnpkg.com/merge-options/-/merge-options-3.0.4.tgz#84709c2aa2a4b24c1981f66c179fe5565cc6dbb7" + resolved "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz" integrity sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ== dependencies: is-plain-obj "^2.1.0" merge-stream@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + resolved "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz" integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== -micromatch@^4.0.8: +micromatch@^4.0.4, micromatch@^4.0.8: version "4.0.8" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz" integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== dependencies: braces "^3.0.3" @@ -2768,138 +3335,138 @@ micromatch@^4.0.8: mime-db@1.52.0: version "1.52.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== mime-types@^2.1.12: version "2.1.35" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== dependencies: mime-db "1.52.0" mimic-fn@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== minimatch@^3.0.4, minimatch@^3.1.1: version "3.1.2" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== dependencies: brace-expansion "^1.1.7" minimatch@^5.0.1: version "5.1.6" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz" integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== dependencies: brace-expansion "^2.0.1" minimatch@^9.0.4: version "9.0.5" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz" integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== dependencies: brace-expansion "^2.0.1" minimist@^1.2.5: version "1.2.8" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== "minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.1.2: version "7.1.2" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" + resolved "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz" integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== ms@^2.1.3: version "2.1.3" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== multiformats@^9.0.4, multiformats@^9.4.2, multiformats@^9.4.7, multiformats@^9.5.4: version "9.9.0" - resolved "https://registry.yarnpkg.com/multiformats/-/multiformats-9.9.0.tgz#c68354e7d21037a8f1f8833c8ccd68618e8f1d37" + resolved "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz" integrity sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg== murmurhash3js-revisited@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/murmurhash3js-revisited/-/murmurhash3js-revisited-3.0.0.tgz#6bd36e25de8f73394222adc6e41fa3fac08a5869" + resolved "https://registry.npmjs.org/murmurhash3js-revisited/-/murmurhash3js-revisited-3.0.0.tgz" integrity sha512-/sF3ee6zvScXMb1XFJ8gDsSnY+X8PbOyjIuBhtgis10W2Jx4ZjIhikUCIF9c4gpJxVnQIsPAFrSwTCuAjicP6g== mute-stream@0.0.8: version "0.0.8" - resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" + resolved "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== napi-postinstall@^0.3.0: version "0.3.3" - resolved "https://registry.yarnpkg.com/napi-postinstall/-/napi-postinstall-0.3.3.tgz#93d045c6b576803ead126711d3093995198c6eb9" + resolved "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.3.tgz" integrity sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow== natural-compare@^1.4.0: version "1.4.0" - resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== neo-async@^2.6.2: version "2.6.2" - resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + resolved "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== node-fetch@^2.6.1, node-fetch@^2.6.9: version "2.7.0" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz" integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== dependencies: whatwg-url "^5.0.0" node-inspect-extracted@^2.0.0: version "2.0.2" - resolved "https://registry.yarnpkg.com/node-inspect-extracted/-/node-inspect-extracted-2.0.2.tgz#e5500e79f6bc03517175881c991f3bfaea67115a" + resolved "https://registry.npmjs.org/node-inspect-extracted/-/node-inspect-extracted-2.0.2.tgz" integrity sha512-8qm9+tu/GmbA/uWQRs6ad8KstyhH94a0pXpRVGHaJ9wHlJbetCYoCwXbKILaaMcE+wgbgpOpzcCnipkL8vTqxA== node-int64@^0.4.0: version "0.4.0" - resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" + resolved "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz" integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== node-releases@^2.0.19: version "2.0.19" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314" + resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz" integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw== normalize-path@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== npm-run-path@^4.0.1: version "4.0.1" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + resolved "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz" integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== dependencies: path-key "^3.0.0" once@^1.3.0: version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== dependencies: wrappy "1" onetime@^5.1.0, onetime@^5.1.2: version "5.1.2" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + resolved "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz" integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== dependencies: mimic-fn "^2.1.0" ora@^5.4.1: version "5.4.1" - resolved "https://registry.yarnpkg.com/ora/-/ora-5.4.1.tgz#1b2678426af4ac4a509008e5e4ac9e9959db9e18" + resolved "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz" integrity sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ== dependencies: bl "^4.1.0" @@ -2914,38 +3481,38 @@ ora@^5.4.1: p-limit@^2.2.0: version "2.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + resolved "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz" integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== dependencies: p-try "^2.0.0" p-limit@^3.1.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + resolved "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz" integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== dependencies: yocto-queue "^0.1.0" p-locate@^4.1.0: version "4.1.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + resolved "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz" integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== dependencies: p-limit "^2.2.0" p-try@^2.0.0: version "2.2.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + resolved "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== package-json-from-dist@^1.0.0: version "1.0.1" - resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505" + resolved "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz" integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw== parse-json@^5.2.0: version "5.2.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + resolved "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz" integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== dependencies: "@babel/code-frame" "^7.0.0" @@ -2955,27 +3522,32 @@ parse-json@^5.2.0: path-exists@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz" integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== path-is-absolute@^1.0.0: version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== path-key@^3.0.0, path-key@^3.1.0: version "3.1.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== path-normalize@^6.0.13: version "6.0.13" - resolved "https://registry.yarnpkg.com/path-normalize/-/path-normalize-6.0.13.tgz#f80575c85ef041366040b00cdbeea97b8e255231" + resolved "https://registry.npmjs.org/path-normalize/-/path-normalize-6.0.13.tgz" integrity sha512-PfC1Pc+IEhI77UEN731pj2nMs9gHAV36IA6IW6VdXWjoQesf+jtO9hdMUqTRS6mwR0T5rmyUrQzd5vw0VwL1Lw== +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + path-scurry@^1.11.1: version "1.11.1" - resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" + resolved "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz" integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== dependencies: lru-cache "^10.2.0" @@ -2983,48 +3555,65 @@ path-scurry@^1.11.1: pegjs@^0.10.0: version "0.10.0" - resolved "https://registry.yarnpkg.com/pegjs/-/pegjs-0.10.0.tgz#cf8bafae6eddff4b5a7efb185269eaaf4610ddbd" + resolved "https://registry.npmjs.org/pegjs/-/pegjs-0.10.0.tgz" integrity sha512-qI5+oFNEGi3L5HAxDwN2LA4Gg7irF70Zs25edhjld9QemOgp0CbvMtbFcMvFtEo1OityPrcCzkQFB8JP/hxgow== picocolors@^1.1.1: version "1.1.1" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz" integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== -picomatch@^2.0.4, picomatch@^2.3.1: +picomatch@^2.0.4, picomatch@^2.2.3, picomatch@^2.3.1: version "2.3.1" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== picomatch@^4.0.2: version "4.0.3" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.3.tgz#796c76136d1eead715db1e7bad785dedd695a042" + resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz" integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q== -pirates@^4.0.7: +pirates@^4.0.4, pirates@^4.0.7: version "4.0.7" - resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.7.tgz#643b4a18c4257c8a65104b73f3049ce9a0a15e22" + resolved "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz" integrity sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA== pkg-dir@^4.2.0: version "4.2.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + resolved "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz" integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== dependencies: find-up "^4.0.0" -pretty-format@30.0.5, pretty-format@^30.0.0: +pretty-format@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz" + integrity sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ== + dependencies: + "@jest/schemas" "^29.6.3" + ansi-styles "^5.0.0" + react-is "^18.0.0" + +pretty-format@^30.0.0, pretty-format@30.0.5: version "30.0.5" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-30.0.5.tgz#e001649d472800396c1209684483e18a4d250360" + resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz" integrity sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw== dependencies: "@jest/schemas" "30.0.5" ansi-styles "^5.2.0" react-is "^18.3.1" +prompts@^2.0.1: + version "2.4.2" + resolved "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz" + integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== + dependencies: + kleur "^3.0.3" + sisteransi "^1.0.5" + protobufjs@^6.10.2: version "6.11.4" - resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.11.4.tgz#29a412c38bf70d89e537b6d02d904a6f448173aa" + resolved "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.4.tgz" integrity sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw== dependencies: "@protobufjs/aspromise" "^1.1.2" @@ -3043,22 +3632,27 @@ protobufjs@^6.10.2: proxy-from-env@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + resolved "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz" integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== +pure-rand@^6.0.0: + version "6.1.0" + resolved "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz" + integrity sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA== + pure-rand@^7.0.0: version "7.0.1" - resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-7.0.1.tgz#6f53a5a9e3e4a47445822af96821ca509ed37566" + resolved "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz" integrity sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ== qrcode-terminal@^0.12.0: version "0.12.0" - resolved "https://registry.yarnpkg.com/qrcode-terminal/-/qrcode-terminal-0.12.0.tgz#bb5b699ef7f9f0505092a3748be4464fe71b5819" + resolved "https://registry.npmjs.org/qrcode-terminal/-/qrcode-terminal-0.12.0.tgz" integrity sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ== rabin-wasm@^0.1.4: version "0.1.5" - resolved "https://registry.yarnpkg.com/rabin-wasm/-/rabin-wasm-0.1.5.tgz#5b625ca007d6a2cbc1456c78ae71d550addbc9c9" + resolved "https://registry.npmjs.org/rabin-wasm/-/rabin-wasm-0.1.5.tgz" integrity sha512-uWgQTo7pim1Rnj5TuWcCewRDTf0PEFTSlaUjWP4eY9EbLV9em08v89oCz/WO+wRxpYuO36XEHp4wgYQnAgOHzA== dependencies: "@assemblyscript/loader" "^0.9.4" @@ -3068,14 +3662,14 @@ rabin-wasm@^0.1.4: node-fetch "^2.6.1" readable-stream "^3.6.0" -react-is@^18.3.1: +react-is@^18.0.0, react-is@^18.3.1: version "18.3.1" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" + resolved "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz" integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== readable-stream@^3.4.0, readable-stream@^3.6.0: version "3.6.2" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz" integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== dependencies: inherits "^2.0.3" @@ -3084,29 +3678,43 @@ readable-stream@^3.4.0, readable-stream@^3.6.0: require-directory@^2.1.1: version "2.1.1" - resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + resolved "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz" integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== require-from-string@^2.0.2: version "2.0.2" - resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + resolved "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz" integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== resolve-cwd@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" + resolved "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz" integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== dependencies: resolve-from "^5.0.0" resolve-from@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== +resolve.exports@^2.0.0: + version "2.0.3" + resolved "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz" + integrity sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A== + +resolve@^1.20.0: + version "1.22.10" + resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz" + integrity sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w== + dependencies: + is-core-module "^2.16.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + restore-cursor@^3.1.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" + resolved "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz" integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== dependencies: onetime "^5.1.0" @@ -3114,66 +3722,81 @@ restore-cursor@^3.1.0: run-async@^2.4.0: version "2.4.1" - resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" + resolved "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz" integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== rxjs@^7.5.5: version "7.8.2" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.2.tgz#955bc473ed8af11a002a2be52071bf475638607b" + resolved "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz" integrity sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA== dependencies: tslib "^2.1.0" safe-buffer@~5.2.0: version "5.2.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== "safer-buffer@>= 2.1.2 < 3.0.0": version "2.1.2" - resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -semver@^6.3.1: +semver@^6.3.0, semver@^6.3.1: version "6.3.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.5.3, semver@^7.5.4, semver@^7.7.2: +semver@^7.5.3: + version "7.7.2" + resolved "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz" + integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA== + +semver@^7.5.4: version "7.7.2" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58" + resolved "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz" + integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA== + +semver@^7.7.2: + version "7.7.2" + resolved "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz" integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA== shebang-command@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz" integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== dependencies: shebang-regex "^3.0.0" shebang-regex@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -signal-exit@^3.0.2, signal-exit@^3.0.3: +signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: version "3.0.7" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== signal-exit@^4.0.1: version "4.1.0" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz" integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== +sisteransi@^1.0.5: + version "1.0.5" + resolved "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz" + integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== + slash@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== slice-ansi@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" + resolved "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz" integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== dependencies: ansi-styles "^4.0.0" @@ -3182,7 +3805,7 @@ slice-ansi@^4.0.0: source-map-support@0.5.13: version "0.5.13" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932" + resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz" integrity sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w== dependencies: buffer-from "^1.0.0" @@ -3190,29 +3813,36 @@ source-map-support@0.5.13: source-map@^0.6.0, source-map@^0.6.1: version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== sparse-array@^1.3.1: version "1.3.2" - resolved "https://registry.yarnpkg.com/sparse-array/-/sparse-array-1.3.2.tgz#0e1a8b71706d356bc916fe754ff496d450ec20b0" + resolved "https://registry.npmjs.org/sparse-array/-/sparse-array-1.3.2.tgz" integrity sha512-ZT711fePGn3+kQyLuv1fpd3rNSkNF8vd5Kv2D+qnOANeyKs3fx6bUMGWRPvgTTcYV64QMqZKZwcuaQSP3AZ0tg== sprintf-js@~1.0.2: version "1.0.3" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz" integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== -stack-utils@^2.0.6: +stack-utils@^2.0.3, stack-utils@^2.0.6: version "2.0.6" - resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.6.tgz#aaf0748169c02fc33c8232abccf933f54a1cc34f" + resolved "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz" integrity sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ== dependencies: escape-string-regexp "^2.0.0" -string-length@^4.0.2: +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string-length@^4.0.1, string-length@^4.0.2: version "4.0.2" - resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" + resolved "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz" integrity sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ== dependencies: char-regex "^1.0.2" @@ -3220,7 +3850,7 @@ string-length@^4.0.2: "string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== dependencies: emoji-regex "^8.0.0" @@ -3229,7 +3859,7 @@ string-length@^4.0.2: string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== dependencies: emoji-regex "^8.0.0" @@ -3238,85 +3868,90 @@ string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" + resolved "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz" integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== dependencies: eastasianwidth "^0.2.0" emoji-regex "^9.2.2" strip-ansi "^7.0.1" -string_decoder@^1.1.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - "strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== dependencies: ansi-regex "^5.0.1" strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== dependencies: ansi-regex "^5.0.1" strip-ansi@^7.0.1: version "7.1.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz" integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== dependencies: ansi-regex "^6.0.1" strip-bom@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" + resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz" integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== strip-final-newline@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + resolved "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz" integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== strip-json-comments@^3.1.1: version "3.1.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== supports-color@^7.1.0: version "7.2.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== dependencies: has-flag "^4.0.0" +supports-color@^8.0.0: + version "8.1.1" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + supports-color@^8.1.1: version "8.1.1" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz" integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== dependencies: has-flag "^4.0.0" +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + symbol.inspect@1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/symbol.inspect/-/symbol.inspect-1.0.1.tgz#e13125b8038c4996eb0dfa1567332ad4dcd0763f" + resolved "https://registry.npmjs.org/symbol.inspect/-/symbol.inspect-1.0.1.tgz" integrity sha512-YQSL4duoHmLhsTD1Pw8RW6TZ5MaTX5rXJnqacJottr2P2LZBF/Yvrc3ku4NUpMOm8aM0KOCqM+UAkMA5HWQCzQ== synckit@^0.11.8: version "0.11.11" - resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.11.11.tgz#c0b619cf258a97faa209155d9cd1699b5c998cb0" + resolved "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz" integrity sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw== dependencies: "@pkgr/core" "^0.2.9" table@^6.9.0: version "6.9.0" - resolved "https://registry.yarnpkg.com/table/-/table-6.9.0.tgz#50040afa6264141c7566b3b81d4d82c47a8668f5" + resolved "https://registry.npmjs.org/table/-/table-6.9.0.tgz" integrity sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A== dependencies: ajv "^8.0.1" @@ -3327,12 +3962,12 @@ table@^6.9.0: teslabot@^1.3.0, teslabot@^1.5.0: version "1.5.0" - resolved "https://registry.yarnpkg.com/teslabot/-/teslabot-1.5.0.tgz#70f544516699ca5f696d8ae94f3d12cd495d5cd6" + resolved "https://registry.npmjs.org/teslabot/-/teslabot-1.5.0.tgz" integrity sha512-e2MmELhCgrgZEGo7PQu/6bmYG36IDH+YrBI1iGm6jovXkeDIGa3pZ2WSqRjzkuw2vt1EqfkZoV5GpXgqL8QJVg== test-exclude@^6.0.0: version "6.0.0" - resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" + resolved "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz" integrity sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w== dependencies: "@istanbuljs/schema" "^0.1.2" @@ -3341,24 +3976,24 @@ test-exclude@^6.0.0: through@^2.3.6: version "2.3.8" - resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + resolved "https://registry.npmjs.org/through/-/through-2.3.8.tgz" integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== tmpl@1.0.5: version "1.0.5" - resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" + resolved "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz" integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw== to-regex-range@^5.0.1: version "5.0.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz" integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== dependencies: is-number "^7.0.0" ton-assembly@0.6.1: version "0.6.1" - resolved "https://registry.yarnpkg.com/ton-assembly/-/ton-assembly-0.6.1.tgz#ad8c48e317b4dcc71903d17515275ecf9fcdb8a6" + resolved "https://registry.npmjs.org/ton-assembly/-/ton-assembly-0.6.1.tgz" integrity sha512-HZNDD2Cy8DQ9UY+8eCgCFY9RBnHEA+Abxo6chDtZQqsX1xo97UZzhMWzK+bYoe1w9gsGugi+1kOH7cpjzDN6jQ== dependencies: "@tonstudio/parser-runtime" "^0.0.1" @@ -3367,7 +4002,7 @@ ton-assembly@0.6.1: ton-lite-client@^3.1.1: version "3.1.1" - resolved "https://registry.yarnpkg.com/ton-lite-client/-/ton-lite-client-3.1.1.tgz#9b5655eb56c84164ebf2075117077c6d21c17ce6" + resolved "https://registry.npmjs.org/ton-lite-client/-/ton-lite-client-3.1.1.tgz" integrity sha512-jhgwRC0txsekBact1rFwgGE3DdgRnMDk2htHZjzLgO9PupdVLAkoFJJ3K9LvtXgY7bKFRcTI+GZWCZk2xRJ0Ig== dependencies: adnl "^1.0.3" @@ -3379,12 +4014,12 @@ ton-lite-client@^3.1.1: ton-source-map@^0.2.2: version "0.2.2" - resolved "https://registry.yarnpkg.com/ton-source-map/-/ton-source-map-0.2.2.tgz#a7b647a085d23a05172b26c110d7197ab4446f9a" + resolved "https://registry.npmjs.org/ton-source-map/-/ton-source-map-0.2.2.tgz" integrity sha512-T9as2Cmv5aqFbELd0ZxIyY3NRPGxf3ltpVN8rm+uIXMMDlNaGW3Wf6jFcaJYwkRNB2eR52PhNbt5tI5lwgL1Cg== ton-tl@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/ton-tl/-/ton-tl-1.0.1.tgz#210756ca6a136a0f405c29733dce182c4e1fc1f6" + resolved "https://registry.npmjs.org/ton-tl/-/ton-tl-1.0.1.tgz" integrity sha512-dAHJSWEW0CRNm/sdWVhola9/OZc/yHmLOXlSNr9I6l0WaVZmGhwkmDuzvMm1ZJ3Dvhf5tYN+iAUSSgmf8Q+P0g== dependencies: "@types/bn.js" "^5.1.0" @@ -3396,12 +4031,12 @@ ton-tl@^1.0.1: tr46@~0.0.3: version "0.0.3" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz" integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== ts-jest@^29.4.0: version "29.4.1" - resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.4.1.tgz#42d33beb74657751d315efb9a871fe99e3b9b519" + resolved "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.1.tgz" integrity sha512-SaeUtjfpg9Uqu8IbeDKtdaS0g8lS6FT6OzM3ezrDfErPJPHNDo/Ey+VFGP1bQIDfagYDLyRpd7O15XpG1Es2Uw== dependencies: bs-logger "^0.2.6" @@ -3414,9 +4049,9 @@ ts-jest@^29.4.0: type-fest "^4.41.0" yargs-parser "^21.1.1" -ts-node@^10.9.1, ts-node@^10.9.2: +ts-node@^10.9.1, ts-node@^10.9.2, ts-node@>=9.0.0: version "10.9.2" - resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f" + resolved "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz" integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== dependencies: "@cspotcode/source-map-support" "^0.8.0" @@ -3433,66 +4068,61 @@ ts-node@^10.9.1, ts-node@^10.9.2: v8-compile-cache-lib "^3.0.1" yn "3.1.1" -tslib@^2.1.0, tslib@^2.4.0: +tslib@^2.1.0: version "2.8.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== tweetnacl-util@^0.15.1: version "0.15.1" - resolved "https://registry.yarnpkg.com/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz#b80fcdb5c97bcc508be18c44a4be50f022eea00b" + resolved "https://registry.npmjs.org/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz" integrity sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw== -tweetnacl@1.0.3, tweetnacl@^1.0.3: +tweetnacl@^1.0.3, tweetnacl@1.0.3: version "1.0.3" - resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.3.tgz#ac0af71680458d8a6378d0d0d050ab1407d35596" + resolved "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz" integrity sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw== type-detect@4.0.8: version "4.0.8" - resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" + resolved "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== type-fest@^0.21.3: version "0.21.3" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz" integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== type-fest@^4.41.0: version "4.41.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.41.0.tgz#6ae1c8e5731273c2bf1f58ad39cbae2c91a46c58" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz" integrity sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA== -typescript@^5.9.2: +typescript@^5.9.2, typescript@>=2.7, "typescript@>=4.3 <6": version "5.9.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.2.tgz#d93450cddec5154a2d5cabe3b8102b83316fb2a6" + resolved "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz" integrity sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A== uglify-js@^3.1.4: version "3.19.3" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.19.3.tgz#82315e9bbc6f2b25888858acd1fff8441035b77f" + resolved "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz" integrity sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ== uint8arrays@^3.0.0: version "3.1.1" - resolved "https://registry.yarnpkg.com/uint8arrays/-/uint8arrays-3.1.1.tgz#2d8762acce159ccd9936057572dade9459f65ae0" + resolved "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.1.1.tgz" integrity sha512-+QJa8QRnbdXVpHYjLoTpJIdCTiw9Ir62nocClWuXIq2JIh4Uta0cQsTSpFL678p2CN8B+XSApwcU+pQEqVpKWg== dependencies: multiformats "^9.4.2" undici-types@~6.21.0: version "6.21.0" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.21.0.tgz#691d00af3909be93a7faa13be61b3a5b50ef12cb" + resolved "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz" integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ== -undici-types@~7.10.0: - version "7.10.0" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.10.0.tgz#4ac2e058ce56b462b056e629cc6a02393d3ff350" - integrity sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag== - unrs-resolver@^1.7.11: version "1.11.1" - resolved "https://registry.yarnpkg.com/unrs-resolver/-/unrs-resolver-1.11.1.tgz#be9cd8686c99ef53ecb96df2a473c64d304048a9" + resolved "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz" integrity sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg== dependencies: napi-postinstall "^0.3.0" @@ -3519,7 +4149,7 @@ unrs-resolver@^1.7.11: update-browserslist-db@^1.1.3: version "1.1.3" - resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz#348377dd245216f9e7060ff50b15a1b740b75420" + resolved "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz" integrity sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw== dependencies: escalade "^3.2.0" @@ -3527,17 +4157,17 @@ update-browserslist-db@^1.1.3: util-deprecate@^1.0.1: version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== v8-compile-cache-lib@^3.0.1: version "3.0.1" - resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" + resolved "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz" integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== v8-to-istanbul@^9.0.1: version "9.3.0" - resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz#b9572abfa62bd556c16d75fdebc1a411d5ff3175" + resolved "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz" integrity sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA== dependencies: "@jridgewell/trace-mapping" "^0.3.12" @@ -3546,26 +4176,26 @@ v8-to-istanbul@^9.0.1: walker@^1.0.8: version "1.0.8" - resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f" + resolved "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz" integrity sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ== dependencies: makeerror "1.0.12" wcwidth@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" + resolved "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz" integrity sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg== dependencies: defaults "^1.0.3" webidl-conversions@^3.0.0: version "3.0.1" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz" integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== whatwg-url@^5.0.0: version "5.0.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz" integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== dependencies: tr46 "~0.0.3" @@ -3573,19 +4203,19 @@ whatwg-url@^5.0.0: which@^2.0.1: version "2.0.2" - resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== dependencies: isexe "^2.0.0" wordwrap@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" + resolved "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz" integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== dependencies: ansi-styles "^4.0.0" @@ -3594,7 +4224,7 @@ wordwrap@^1.0.0: wrap-ansi@^6.0.1: version "6.2.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz" integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== dependencies: ansi-styles "^4.0.0" @@ -3603,7 +4233,7 @@ wrap-ansi@^6.0.1: wrap-ansi@^7.0.0: version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== dependencies: ansi-styles "^4.0.0" @@ -3612,7 +4242,7 @@ wrap-ansi@^7.0.0: wrap-ansi@^8.1.0: version "8.1.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz" integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== dependencies: ansi-styles "^6.1.0" @@ -3621,45 +4251,53 @@ wrap-ansi@^8.1.0: wrappy@1: version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== +write-file-atomic@^4.0.2: + version "4.0.2" + resolved "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz" + integrity sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg== + dependencies: + imurmurhash "^0.1.4" + signal-exit "^3.0.7" + write-file-atomic@^5.0.1: version "5.0.1" - resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-5.0.1.tgz#68df4717c55c6fa4281a7860b4c2ba0a6d2b11e7" + resolved "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz" integrity sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw== dependencies: imurmurhash "^0.1.4" signal-exit "^4.0.1" -ws@^8.8.1: +ws@*, ws@^8.8.1: version "8.18.3" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.3.tgz#b56b88abffde62791c639170400c93dcb0c95472" + resolved "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz" integrity sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg== y18n@^5.0.5: version "5.0.8" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz" integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== yallist@^3.0.2: version "3.1.1" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + resolved "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== yaml@^2.7.1: version "2.8.1" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.8.1.tgz#1870aa02b631f7e8328b93f8bc574fac5d6c4d79" + resolved "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz" integrity sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw== yargs-parser@^21.1.1: version "21.1.1" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" + resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz" integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== -yargs@^17.7.2: +yargs@^17.3.1, yargs@^17.7.2: version "17.7.2" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" + resolved "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz" integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== dependencies: cliui "^8.0.1" @@ -3672,15 +4310,15 @@ yargs@^17.7.2: yn@3.1.1: version "3.1.1" - resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + resolved "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz" integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== yocto-queue@^0.1.0: version "0.1.0" - resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== zod@^3.21.4, zod@^3.22.4, zod@^3.24.2: version "3.25.76" - resolved "https://registry.yarnpkg.com/zod/-/zod-3.25.76.tgz#26841c3f6fd22a6a2760e7ccb719179768471e34" + resolved "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz" integrity sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ== diff --git a/slasher-traits/src/validator.rs b/slasher-traits/src/validator.rs index 433794df80..63fb9494ab 100644 --- a/slasher-traits/src/validator.rs +++ b/slasher-traits/src/validator.rs @@ -9,10 +9,8 @@ use tycho_util::FastHasherState; // TODO: Decide how to be with this collator-defined type #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct ValidationSessionId { - /// Incremental sequence number. - // pub seqno: u32, - pub vset_switch_round: u32, pub catchain_seqno: u32, + pub vset_switch_round: u32, } // TEMP @@ -21,8 +19,8 @@ impl From<(u32, u32)> for ValidationSessionId { fn from(value: (u32, u32)) -> Self { Self { // seqno: value.0, - vset_switch_round: value.0, - catchain_seqno: value.1, + catchain_seqno: value.0, + vset_switch_round: value.1, } } } @@ -31,8 +29,8 @@ impl From<(u32, u32)> for ValidationSessionId { impl Ord for ValidationSessionId { #[inline] fn cmp(&self, other: &Self) -> std::cmp::Ordering { - (self.vset_switch_round, self.catchain_seqno) - .cmp(&(other.vset_switch_round, other.catchain_seqno)) + (self.catchain_seqno, self.vset_switch_round) + .cmp(&(other.catchain_seqno, other.vset_switch_round)) } } diff --git a/slasher/src/bc/mod.rs b/slasher/src/bc/mod.rs index d0729f749d..fe877fe3f2 100644 --- a/slasher/src/bc/mod.rs +++ b/slasher/src/bc/mod.rs @@ -132,9 +132,9 @@ pub enum SlasherContractEvent { SubmitBlocksBatch(SubmitBlocksBatch), } -// TODO: Propagate session id? #[derive(Debug, PartialEq, Eq)] pub struct SubmitBlocksBatch { + pub session_id: ValidationSessionId, pub validator_idx: u16, pub blocks_batch: BlocksBatch, } diff --git a/slasher/src/bc/stub_contract.rs b/slasher/src/bc/stub_contract.rs index ebea4f3224..e3c771fc0c 100644 --- a/slasher/src/bc/stub_contract.rs +++ b/slasher/src/bc/stub_contract.rs @@ -1,6 +1,7 @@ use std::num::{NonZeroU8, NonZeroU32}; use anyhow::{Context, Result}; +use tycho_slasher_traits::ValidationSessionId; use tycho_types::cell::Lazy; use tycho_types::dict; use tycho_types::models::{ @@ -63,6 +64,8 @@ impl SlasherContract for StubSlasherContract { let mut b = CellBuilder::new(); b.store_u64(now)?; b.store_u32(expire_at)?; + b.store_u32(params.session_id.catchain_seqno)?; + b.store_u32(params.session_id.vset_switch_round)?; b.store_u16(params.validator_idx)?; b.store_reference(cell)?; b.build()? @@ -118,6 +121,12 @@ impl SlasherContract for StubSlasherContract { // TODO: Add message op let mut body = msg.body; body.skip_first(512 + 64 + 32, 0)?; + let catchain_seqno = body.load_u32()?; + let vset_switch_round = body.load_u32()?; + let session_id = ValidationSessionId { + vset_switch_round, + catchain_seqno, + }; let validator_idx = body.load_u16()?; let mut batch_cs = body.load_reference_as_slice()?; let BlocksBatchBc(blocks_batch) = <_>::load_from(&mut batch_cs)?; @@ -127,6 +136,7 @@ impl SlasherContract for StubSlasherContract { Ok(Some(SlasherContractEvent::SubmitBlocksBatch( SubmitBlocksBatch { + session_id, validator_idx, blocks_batch, }, diff --git a/slasher/src/lib.rs b/slasher/src/lib.rs index 8e818f5979..888f30dd11 100644 --- a/slasher/src/lib.rs +++ b/slasher/src/lib.rs @@ -147,29 +147,23 @@ impl Slasher { let catchain_seqno = state_extra.validator_info.catchain_seqno; let vset_switch_round = state_extra.consensus_info.vset_switch_round; - let known_session_id = this.known_session_id.load(); - let session_id_from_block = if known_session_id.vset_switch_round == vset_switch_round - && known_session_id.catchain_seqno == catchain_seqno - { - known_session_id - } else { - ValidationSessionId { - vset_switch_round, - catchain_seqno, - } + let session_id_from_block = ValidationSessionId { + vset_switch_round, + catchain_seqno, }; tracing::trace!(?slasher_params, ?session_id_from_block); // Clear old sessions if needed // TODO: Add metrics. - if session_id_from_block != known_session_id { + if session_id_from_block != this.known_session_id.load() { let span = tracing::Span::current(); let storage = this.storage.clone(); - tokio::task::spawn_blocking(move || { - let _span = span.enter(); - storage.remove_outdated_batches(session_id_from_block) - }) - .await??; + // tokio::task::spawn_blocking(move || { + // let _span = span.enter(); + // // TODO: should really clear batches right away? + // storage.remove_outdated_batches(session_id_from_block) + // }) + // .await??; this.known_session_id.set(session_id_from_block); } @@ -205,7 +199,7 @@ impl Slasher { bc::SlasherContractEvent::SubmitBlocksBatch(submitted) => { // TODO: Move into blocking. this.storage.store_blocks_batch( - session_id_from_block, + submitted.session_id, submitted.validator_idx, &submitted.blocks_batch, )?; diff --git a/slasher/src/storage/db.rs b/slasher/src/storage/db.rs index 88bab8c7ab..12d54057a8 100644 --- a/slasher/src/storage/db.rs +++ b/slasher/src/storage/db.rs @@ -31,6 +31,7 @@ impl WithMigrations for SlasherTables { weedb::tables! { pub struct SlasherTables { pub state: tables::State, + pub sessions: tables::Sessions, pub block_batches: tables::BlockBatches, } } @@ -43,6 +44,28 @@ pub mod tables { use weedb::rocksdb::Options; use weedb::{ColumnFamily, ColumnFamilyOptions}; + /// Stores list of validation sessions + /// - Key: `session_id: (catchain_seqno u32 BE, vset_switch_round u32 BE)` + /// - Value: () + pub struct Sessions; + + impl Sessions { + pub const KEY_LEN: usize = 4 + 4; + } + + impl ColumnFamily for Sessions { + const NAME: &'static str = "sessions"; + } + + impl ColumnFamilyOptions for Sessions { + fn options(opts: &mut Options, ctx: &mut TableContext) { + default_block_based_table_factory(opts, ctx); + + opts.set_optimize_filters_for_hits(true); + optimize_for_point_lookup(opts, ctx); + } + } + /// Stores generic node parameters /// - Key: `...` /// - Value: `...` @@ -62,7 +85,7 @@ pub mod tables { } /// Block batches submitted by validators - /// - Key: `session_id: (seqno u32 BE, vset_switch_round u32 BE, catchain_seqno u32 BE), validator_idx: u16 BE, start_block: u32 BE` + /// - Key: `session_id: (catchain_seqno u32 BE, vset_switch_round u32 BE), validator_idx: u16 BE, start_block: u32 BE` /// - Value: blocks batch pub struct BlockBatches; diff --git a/slasher/src/storage/mod.rs b/slasher/src/storage/mod.rs index ead243f446..284d4bea71 100644 --- a/slasher/src/storage/mod.rs +++ b/slasher/src/storage/mod.rs @@ -48,7 +48,6 @@ impl SlasherStorage { batch: &BlocksBatch, ) -> Result<()> { let mut key = [0u8; tables::BlockBatches::KEY_LEN]; - // key[0..4].copy_from_slice(&session_id.seqno.to_be_bytes()); key[0..4].copy_from_slice(&session_id.catchain_seqno.to_be_bytes()); key[4..8].copy_from_slice(&session_id.vset_switch_round.to_be_bytes()); key[8..10].copy_from_slice(&validator_idx.to_be_bytes()); @@ -64,6 +63,17 @@ impl SlasherStorage { pub fn remove_outdated_batches(&self, latest_session_id: ValidationSessionId) -> Result<()> { let db = &self.inner.db; + let mut session_key = [0u8; tables::Sessions::KEY_LEN]; + session_key[0..4].copy_from_slice(&latest_session_id.catchain_seqno.to_be_bytes()); + session_key[4..8].copy_from_slice(&latest_session_id.vset_switch_round.to_be_bytes()); + + db.rocksdb().delete_range_cf_opt( + &db.sessions.cf(), + [0u8; tables::Sessions::KEY_LEN], + session_key, + db.sessions.write_config(), + )?; + let mut key = [0u8; tables::BlockBatches::KEY_LEN]; key[0..4].copy_from_slice(&latest_session_id.catchain_seqno.to_be_bytes()); key[4..8].copy_from_slice(&latest_session_id.vset_switch_round.to_be_bytes()); diff --git a/slasher/src/util.rs b/slasher/src/util.rs index 8ed464cc38..a48e0fec55 100644 --- a/slasher/src/util.rs +++ b/slasher/src/util.rs @@ -1,25 +1,47 @@ use std::ptr::NonNull; +use std::sync::atomic::{AtomicU64, Ordering}; -use parking_lot::RwLock; use tl_proto::{TlError, TlRead, TlResult, TlWrite}; use tycho_slasher_traits::ValidationSessionId; use tycho_types::prelude::*; // === AtomicValidationSessionId === -pub struct AtomicValidationSessionId(RwLock); +pub struct AtomicValidationSessionId(AtomicU64); impl AtomicValidationSessionId { - pub fn new(value: ValidationSessionId) -> Self { - Self(RwLock::new(value)) + pub const fn new(value: ValidationSessionId) -> Self { + Self(AtomicU64::new(Self::pack_id(value))) } pub fn set(&self, value: ValidationSessionId) { - *self.0.write() = value; + self.0.store(Self::pack_id(value), Ordering::Release); } pub fn load(&self) -> ValidationSessionId { - *self.0.read() + Self::unpack_id(self.0.load(Ordering::Acquire)) + } + + #[inline] + const fn pack_id(value: ValidationSessionId) -> u64 { + const _: () = const { + let id = ValidationSessionId { + catchain_seqno: 0, + vset_switch_round: 0, + }; + assert!(std::mem::size_of_val(&id.catchain_seqno) == 4); + assert!(std::mem::size_of_val(&id.vset_switch_round) == 4); + }; + + ((value.catchain_seqno as u64) << 32) | (value.vset_switch_round as u64) + } + + #[inline] + const fn unpack_id(value: u64) -> ValidationSessionId { + ValidationSessionId { + catchain_seqno: (value >> 32) as u32, + vset_switch_round: value as u32, + } } } From aa8ef2c349bf3ce10cad1bbe2762d07ad0c89992 Mon Sep 17 00:00:00 2001 From: MrWad3r Date: Wed, 11 Mar 2026 13:16:55 +0100 Subject: [PATCH 10/21] feat(slasher): add slasher analyzer to create signatures reports --- slasher/Cargo.toml | 4 + slasher/src/analyzer.rs | 323 +++++++++++++++++++++++++++++++++ slasher/src/lib.rs | 56 ++++-- slasher/src/proto.tl | 26 ++- slasher/src/storage/db.rs | 23 +++ slasher/src/storage/mod.rs | 327 +++++++++++++++++++++++++++++----- slasher/src/storage/models.rs | 109 +++++++++++- slasher/src/util.rs | 18 ++ 8 files changed, 827 insertions(+), 59 deletions(-) create mode 100644 slasher/src/analyzer.rs diff --git a/slasher/Cargo.toml b/slasher/Cargo.toml index 4bb6c8dd82..b6bb4a546f 100644 --- a/slasher/Cargo.toml +++ b/slasher/Cargo.toml @@ -36,3 +36,7 @@ tycho-util = { workspace = true } [lints] workspace = true + +[dev-dependencies] +tokio = { workspace = true, features = ["rt", "macros", "sync"] } +tycho-storage = { workspace = true, features = ["test"] } diff --git a/slasher/src/analyzer.rs b/slasher/src/analyzer.rs new file mode 100644 index 0000000000..a8df426e4e --- /dev/null +++ b/slasher/src/analyzer.rs @@ -0,0 +1,323 @@ +use std::collections::BTreeMap; + +use tycho_slasher_traits::ValidationSessionId; +use tycho_util::{FastHashMap, FastHashSet}; + +use crate::BlocksBatch; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SessionPenaltyReport { + pub session_id: ValidationSessionId, + pub total_blocks_in_session: u32, + pub offenders: Box<[ValidatorPenalty]>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ValidatorPenalty { + pub validator_idx: u16, + pub missing_signatures: u32, + pub invalid_signatures: u32, +} + +#[derive(Debug, Default, Clone, Copy)] +struct ObservedSignature { + has_valid_signature: bool, + has_invalid_signature: bool, +} + +#[derive(Debug, Default, Clone, Copy)] +struct SignatureTotals { + missing_signatures: u32, + invalid_signatures: u32, +} + +pub fn analyze_session( + session_id: ValidationSessionId, + batches: &[BlocksBatch], +) -> SessionPenaltyReport { + let mut validators = FastHashSet::default(); + let mut blocks = BTreeMap::>::new(); + + for batch in batches { + for history in &batch.signatures_history { + validators.insert(history.validator_idx); + } + + for offset in 0..batch.committed_blocks.len() { + if !batch.committed_blocks.get(offset) { + continue; + } + + let seqno = batch.start_seqno + offset as u32; + let signatures = blocks.entry(seqno).or_default(); + + // Different validators can submit overlapping matrices for the same block. + // We merge them by taking the union of observed bits, but a + // `(block, validator_idx)` pair must never end up with both `valid` + // and `invalid` states at once. If that happens, the input data is + // internally inconsistent and we fail fast instead of guessing. + for history in &batch.signatures_history { + let offset = offset * 2; + let has_invalid_signature = history.bits.get(offset); + let has_valid_signature = history.bits.get(offset + 1); + assert!( + !(has_invalid_signature && has_valid_signature), + "slasher analyzer invariant violated: validator {} has both valid and invalid bits for block {}", + history.validator_idx, + seqno, + ); + + let observed = signatures.entry(history.validator_idx).or_default(); + observed.has_invalid_signature |= has_invalid_signature; + observed.has_valid_signature |= has_valid_signature; + assert!( + !(observed.has_invalid_signature && observed.has_valid_signature), + "slasher analyzer invariant violated: validator {} has conflicting observations for block {}", + history.validator_idx, + seqno, + ); + } + } + } + + let total_blocks_in_session = blocks.len() as u32; + let threshold = total_blocks_in_session / 2; + + let mut validators = validators.into_iter().collect::>(); + validators.sort_unstable(); + + let mut totals = FastHashMap::::default(); + for signatures in blocks.values() { + for &validator_idx in &validators { + let observed = signatures.get(&validator_idx).copied().unwrap_or_default(); + let totals = totals.entry(validator_idx).or_default(); + if !observed.has_valid_signature { + totals.missing_signatures += 1; + } + if observed.has_invalid_signature { + totals.invalid_signatures += 1; + } + } + } + + let offenders = validators + .into_iter() + .filter_map(|validator_idx| { + let totals = totals.get(&validator_idx).copied().unwrap_or_default(); + let penalty_score = totals + .missing_signatures + .saturating_add(totals.invalid_signatures); + (penalty_score > threshold).then_some(ValidatorPenalty { + validator_idx, + missing_signatures: totals.missing_signatures, + invalid_signatures: totals.invalid_signatures, + }) + }) + .collect::>() + .into_boxed_slice(); + + SessionPenaltyReport { + session_id, + total_blocks_in_session, + offenders, + } +} + +pub fn emit_report_metrics(report: &SessionPenaltyReport) { + let labels = session_labels(report.session_id); + metrics::gauge!("tycho_slasher_session_blocks_total", &labels) + .set(report.total_blocks_in_session as f64); + metrics::gauge!("tycho_slasher_session_penalty_candidates_total", &labels) + .set(report.offenders.len() as f64); + + for offender in &report.offenders { + let validator_idx = format!("{}", offender.validator_idx); + let labels = [ + ( + "catchain_seqno", + format!("{}", report.session_id.catchain_seqno), + ), + ( + "vset_switch_round", + format!("{}", report.session_id.vset_switch_round), + ), + ("validator_idx", validator_idx.clone()), + ]; + metrics::gauge!("tycho_slasher_penalty_candidate", &labels).set(1); + + let labels = [ + ( + "catchain_seqno", + format!("{}", report.session_id.catchain_seqno), + ), + ( + "vset_switch_round", + format!("{}", report.session_id.vset_switch_round), + ), + ("validator_idx", validator_idx), + ]; + metrics::gauge!( + "tycho_slasher_penalty_candidate_missing_signatures", + &labels + ) + .set(offender.missing_signatures as f64); + metrics::gauge!( + "tycho_slasher_penalty_candidate_invalid_signatures", + &labels + ) + .set(offender.invalid_signatures as f64); + } +} + +pub fn clear_report_metrics(report: &SessionPenaltyReport) { + let labels = session_labels(report.session_id); + metrics::gauge!("tycho_slasher_session_blocks_total", &labels).set(0); + metrics::gauge!("tycho_slasher_session_penalty_candidates_total", &labels).set(0); + + for offender in &report.offenders { + let validator_idx = format!("{}", offender.validator_idx); + let labels = [ + ( + "catchain_seqno", + format!("{}", report.session_id.catchain_seqno), + ), + ( + "vset_switch_round", + format!("{}", report.session_id.vset_switch_round), + ), + ("validator_idx", validator_idx.clone()), + ]; + metrics::gauge!("tycho_slasher_penalty_candidate", &labels).set(0); + + let labels = [ + ( + "catchain_seqno", + format!("{}", report.session_id.catchain_seqno), + ), + ( + "vset_switch_round", + format!("{}", report.session_id.vset_switch_round), + ), + ("validator_idx", validator_idx), + ]; + metrics::gauge!( + "tycho_slasher_penalty_candidate_missing_signatures", + &labels + ) + .set(0); + metrics::gauge!( + "tycho_slasher_penalty_candidate_invalid_signatures", + &labels + ) + .set(0); + } +} + +fn session_labels(session_id: ValidationSessionId) -> [(&'static str, String); 2] { + [ + ("catchain_seqno", format!("{}", session_id.catchain_seqno)), + ( + "vset_switch_round", + format!("{}", session_id.vset_switch_round), + ), + ] +} + +#[cfg(test)] +mod tests { + use std::num::NonZeroU32; + + use tycho_slasher_traits::{ReceivedSignature, ValidationSessionId}; + + use super::*; + + #[test] + fn analyzes_combined_penalty_threshold() { + let session_id = ValidationSessionId { + catchain_seqno: 7, + vset_switch_round: 9, + }; + let batch = make_batch(100, &[ + (100, &[(1, 0), (2, 0b10), (3, 0b01)]), + (101, &[(1, 0), (2, 0b01), (3, 0b01)]), + (102, &[(1, 0b10), (2, 0b10), (3, 0b01)]), + (103, &[(1, 0b01), (2, 0b01), (3, 0b01)]), + ]); + + let report = analyze_session(session_id, &[batch]); + + assert_eq!(report.total_blocks_in_session, 4); + assert_eq!(report.offenders.as_ref(), &[ + ValidatorPenalty { + validator_idx: 1, + missing_signatures: 3, + invalid_signatures: 1, + }, + ValidatorPenalty { + validator_idx: 2, + missing_signatures: 2, + invalid_signatures: 2, + }, + ]); + } + + #[test] + fn merges_overlapping_batches_from_multiple_observers() { + let session_id = ValidationSessionId { + catchain_seqno: 11, + vset_switch_round: 13, + }; + let missing = make_batch(200, &[(200, &[(1, 0)])]); + let valid = make_batch(200, &[(200, &[(1, 0b01)])]); + + let report = analyze_session(session_id, &[missing, valid]); + + assert_eq!(report.total_blocks_in_session, 1); + assert!(report.offenders.is_empty()); + } + + #[test] + #[should_panic(expected = "slasher analyzer invariant violated")] + fn panics_on_dual_signature_bits() { + let session_id = ValidationSessionId { + catchain_seqno: 17, + vset_switch_round: 19, + }; + let batch = make_batch(300, &[(300, &[(1, 0b11)])]); + + let _ = analyze_session(session_id, &[batch]); + } + + fn make_batch(start_seqno: u32, blocks: &[(u32, &[(u16, u8)])]) -> BlocksBatch { + let end_seqno = blocks.iter().map(|(seqno, _)| *seqno).max().unwrap(); + let mut validators = blocks + .iter() + .flat_map(|(_, signatures)| signatures.iter().map(|(validator_idx, _)| *validator_idx)) + .collect::>(); + validators.sort_unstable(); + validators.dedup(); + + let mut batch = BlocksBatch::new( + start_seqno, + NonZeroU32::new(end_seqno - start_seqno + 1).unwrap(), + &validators, + ); + + for (seqno, signatures) in blocks { + let mut slots = validators + .iter() + .map(|validator_idx| { + let bits = signatures + .iter() + .find_map(|(item, bits)| (*item == *validator_idx).then_some(*bits)) + .unwrap_or(0); + ReceivedSignature(bits) + }) + .collect::>(); + assert!(batch.commit_signatures(*seqno, &slots)); + slots.clear(); + } + + batch + } +} diff --git a/slasher/src/lib.rs b/slasher/src/lib.rs index 888f30dd11..8f0c9db0e5 100644 --- a/slasher/src/lib.rs +++ b/slasher/src/lib.rs @@ -19,6 +19,7 @@ use tycho_util::config::PartialConfig; use tycho_util::futures::JoinTask; use tycho_util::serde_helpers; +pub use self::analyzer::{SessionPenaltyReport, ValidatorPenalty}; pub use self::bc::{ BlocksBatch, ContractSubscription, EncodeBlocksBatchMessage, MessageDeliveryStatus, SignatureHistory, SignedMessage, SlasherContract, StubSlasherContract, @@ -27,6 +28,7 @@ use self::collector::{ValidatorEventsCollector, ValidatorSessionInfo}; use self::storage::SlasherStorage; use self::util::AtomicValidationSessionId; +mod analyzer; pub mod collector { pub use self::validator_events::*; @@ -35,7 +37,6 @@ pub mod collector { } mod bc; -#[expect(unused)] mod storage; mod util; @@ -147,25 +148,20 @@ impl Slasher { let catchain_seqno = state_extra.validator_info.catchain_seqno; let vset_switch_round = state_extra.consensus_info.vset_switch_round; - let session_id_from_block = ValidationSessionId { + let current_session_id = ValidationSessionId { vset_switch_round, catchain_seqno, }; - tracing::trace!(?slasher_params, ?session_id_from_block); + tracing::trace!(?slasher_params, ?current_session_id); - // Clear old sessions if needed // TODO: Add metrics. - if session_id_from_block != this.known_session_id.load() { - let span = tracing::Span::current(); - let storage = this.storage.clone(); - // tokio::task::spawn_blocking(move || { - // let _span = span.enter(); - // // TODO: should really clear batches right away? - // storage.remove_outdated_batches(session_id_from_block) - // }) - // .await??; - - this.known_session_id.set(session_id_from_block); + if current_session_id != this.known_session_id.load() { + tracing::info!( + old_session_id = ?this.known_session_id.load(), + ?current_session_id, + "slasher observed validation session change", + ); + this.known_session_id.set(current_session_id); } // Handle subscription @@ -198,11 +194,13 @@ impl Slasher { Ok(Some(event)) => match event { bc::SlasherContractEvent::SubmitBlocksBatch(submitted) => { // TODO: Move into blocking. - this.storage.store_blocks_batch( + if let Some(report) = this.storage.store_blocks_batch( submitted.session_id, submitted.validator_idx, &submitted.blocks_batch, - )?; + )? { + analyzer::clear_report_metrics(&report); + } tokio::task::yield_now().await; } }, @@ -212,6 +210,8 @@ impl Slasher { } } + self.shared.analyze_completed_sessions()?; + while let Some(session_info) = self .validator_events_collector .pop_session_to_init(mc_seqno) @@ -271,6 +271,28 @@ struct SlasherSharedState { } impl SlasherSharedState { + fn analyze_completed_sessions(&self) -> Result<()> { + let snapshot = self.storage.snapshot(); + let Some(latest_session_id) = snapshot.load_latest_session_id()? else { + return Ok(()); + }; + + for session_id in snapshot.load_distinct_session_ids()? { + if session_id >= latest_session_id + || snapshot.load_session_report(session_id)?.is_some() + { + continue; + } + + let batches = snapshot.load_batches_for_session(session_id)?; + let report = analyzer::analyze_session(session_id, &batches); + self.storage.store_session_report(&report)?; + analyzer::emit_report_metrics(&report); + } + + Ok(()) + } + #[instrument(skip_all, fields(session_id = ?info.session_id))] async fn send_batches_to_contract( self: Arc, diff --git a/slasher/src/proto.tl b/slasher/src/proto.tl index 252a9b464c..0d1edeb926 100644 --- a/slasher/src/proto.tl +++ b/slasher/src/proto.tl @@ -20,4 +20,28 @@ slasher.signatureHistory bits:bitset = slasher.SignatureHistory; -bitset length:int data:bytes = BitSet; \ No newline at end of file +/** +* @param catchain_seqno validation session catchain seqno +* @param vset_switch_round validation session vset switch round +* @param total_blocks_in_session total committed blocks merged for this session +* @param offenders validators we want to punish on stage 1 +*/ +slasher.sessionPenaltyReport + catchain_seqno:int + vset_switch_round:int + total_blocks_in_session:int + offenders:(vector slasher.validatorPenalty) + = slasher.SessionPenaltyReport; + +/** +* @param validator_idx validator index relative to the validator set +* @param missing_signatures blocks where no valid signature was observed +* @param invalid_signatures blocks where an invalid signature was observed +*/ +slasher.validatorPenalty + validator_idx:int + missing_signatures:int + invalid_signatures:int + = slasher.ValidatorPenalty; + +bitset length:int data:bytes = BitSet; diff --git a/slasher/src/storage/db.rs b/slasher/src/storage/db.rs index 12d54057a8..7ce0640d1d 100644 --- a/slasher/src/storage/db.rs +++ b/slasher/src/storage/db.rs @@ -33,6 +33,7 @@ weedb::tables! { pub state: tables::State, pub sessions: tables::Sessions, pub block_batches: tables::BlockBatches, + pub session_reports: tables::SessionReports, } } @@ -66,6 +67,28 @@ pub mod tables { } } + /// Cached analyzer result for a completed validation session. + /// - Key: `session_id: (catchain_seqno u32 BE, vset_switch_round u32 BE)` + /// - Value: `SessionPenaltyReport` + pub struct SessionReports; + + impl SessionReports { + pub const KEY_LEN: usize = 4 + 4; + } + + impl ColumnFamily for SessionReports { + const NAME: &'static str = "session_reports"; + } + + impl ColumnFamilyOptions for SessionReports { + fn options(opts: &mut Options, ctx: &mut TableContext) { + default_block_based_table_factory(opts, ctx); + + opts.set_optimize_filters_for_hits(true); + optimize_for_point_lookup(opts, ctx); + } + } + /// Stores generic node parameters /// - Key: `...` /// - Value: `...` diff --git a/slasher/src/storage/mod.rs b/slasher/src/storage/mod.rs index 284d4bea71..d04fc8f8d4 100644 --- a/slasher/src/storage/mod.rs +++ b/slasher/src/storage/mod.rs @@ -1,14 +1,13 @@ use std::sync::Arc; -use anyhow::Result; +use anyhow::{Context, Result}; use tycho_slasher_traits::ValidationSessionId; use tycho_storage::StorageContext; -use tycho_types::cell::HashBytes; use weedb::OwnedSnapshot; use self::db::{SlasherDb, tables}; -use self::models::StoredBlocksBatch; -use crate::BlocksBatch; +use self::models::{StoredBlocksBatch, StoredSessionPenaltyReport}; +use crate::{BlocksBatch, SessionPenaltyReport}; pub mod db; pub mod models; @@ -30,13 +29,10 @@ impl SlasherStorage { }) } - pub fn db(&self) -> &SlasherDb { - &self.inner.db - } - /// Creates a new snapshot. pub fn snapshot(&self) -> SlasherStorageSnapshot { SlasherStorageSnapshot { + db: self.inner.db.clone(), snapshot: Arc::new(self.inner.db.owned_snapshot()), } } @@ -46,46 +42,61 @@ impl SlasherStorage { session_id: ValidationSessionId, validator_idx: u16, batch: &BlocksBatch, - ) -> Result<()> { - let mut key = [0u8; tables::BlockBatches::KEY_LEN]; - key[0..4].copy_from_slice(&session_id.catchain_seqno.to_be_bytes()); - key[4..8].copy_from_slice(&session_id.vset_switch_round.to_be_bytes()); - key[8..10].copy_from_slice(&validator_idx.to_be_bytes()); - key[10..14].copy_from_slice(&batch.start_seqno.to_be_bytes()); + ) -> Result> { + let key = block_batches_key(session_id, validator_idx, batch.start_seqno); let value = tl_proto::serialize(StoredBlocksBatch::wrap(batch)); self.inner.db.block_batches.insert(key.as_slice(), value)?; - Ok(()) + self.take_session_report(session_id) } - /// Removes all block batches for sessions BEFORE the specified. - pub fn remove_outdated_batches(&self, latest_session_id: ValidationSessionId) -> Result<()> { - let db = &self.inner.db; - - let mut session_key = [0u8; tables::Sessions::KEY_LEN]; - session_key[0..4].copy_from_slice(&latest_session_id.catchain_seqno.to_be_bytes()); - session_key[4..8].copy_from_slice(&latest_session_id.vset_switch_round.to_be_bytes()); - - db.rocksdb().delete_range_cf_opt( - &db.sessions.cf(), - [0u8; tables::Sessions::KEY_LEN], - session_key, - db.sessions.write_config(), - )?; - - let mut key = [0u8; tables::BlockBatches::KEY_LEN]; - key[0..4].copy_from_slice(&latest_session_id.catchain_seqno.to_be_bytes()); - key[4..8].copy_from_slice(&latest_session_id.vset_switch_round.to_be_bytes()); - - db.rocksdb().delete_range_cf_opt( - &db.block_batches.cf(), - [0u8; tables::BlockBatches::KEY_LEN], - key, - db.block_batches.write_config(), - )?; + pub fn store_session_report(&self, report: &SessionPenaltyReport) -> Result<()> { + let key = session_key(report.session_id); + let value = tl_proto::serialize(StoredSessionPenaltyReport::wrap(report)); + self.inner + .db + .session_reports + .insert(key.as_slice(), value)?; Ok(()) } + + pub fn load_session_report( + &self, + session_id: ValidationSessionId, + ) -> Result> { + let table = &self.inner.db.session_reports; + let key = session_key(session_id); + let Some(value) = self + .inner + .db + .rocksdb() + .get_cf(&table.cf(), key.as_slice())? + else { + return Ok(None); + }; + + let report = tl_proto::deserialize::(&value) + .context("failed to deserialize slasher session report")? + .0; + Ok(Some(report)) + } + + fn take_session_report( + &self, + session_id: ValidationSessionId, + ) -> Result> { + let report = self.load_session_report(session_id)?; + if report.is_some() { + let key = session_key(session_id); + self.inner.db.rocksdb().delete_cf_opt( + &self.inner.db.session_reports.cf(), + key.as_slice(), + self.inner.db.session_reports.write_config(), + )?; + } + Ok(report) + } } struct Inner { @@ -94,5 +105,241 @@ struct Inner { #[derive(Clone)] pub struct SlasherStorageSnapshot { + db: SlasherDb, snapshot: Arc, } + +impl SlasherStorageSnapshot { + pub fn load_latest_session_id(&self) -> Result> { + let table = &self.db.block_batches; + let mut read_config = table.new_read_config(); + read_config.set_snapshot(self.snapshot.as_ref()); + + let cf = table.cf(); + let mut iter = self.db.rocksdb().raw_iterator_cf_opt(&cf, read_config); + iter.seek_to_last(); + + match iter.key() { + Some(key) => Ok(Some(parse_session_id_prefix(key))), + None => { + iter.status()?; + Ok(None) + } + } + } + + pub fn load_distinct_session_ids(&self) -> Result> { + let table = &self.db.block_batches; + let mut read_config = table.new_read_config(); + read_config.set_snapshot(self.snapshot.as_ref()); + + let cf = table.cf(); + let mut iter = self.db.rocksdb().raw_iterator_cf_opt(&cf, read_config); + iter.seek_to_first(); + + let mut items = Vec::new(); + let mut prev = None; + loop { + let key = match iter.key() { + Some(key) => key, + None => { + iter.status()?; + break; + } + }; + + let session_id = parse_session_id_prefix(key); + if prev != Some(session_id) { + items.push(session_id); + prev = Some(session_id); + } + + iter.next(); + } + + Ok(items) + } + + pub fn load_batches_for_session( + &self, + session_id: ValidationSessionId, + ) -> Result> { + let table = &self.db.block_batches; + let mut read_config = table.new_read_config(); + read_config.set_snapshot(self.snapshot.as_ref()); + + let prefix = session_key(session_id); + read_config.set_iterate_lower_bound(prefix.as_slice()); + + let cf = table.cf(); + let mut iter = self.db.rocksdb().raw_iterator_cf_opt(&cf, read_config); + iter.seek(prefix.as_slice()); + + let mut items = Vec::new(); + while let Some((key, value)) = iter.item() { + if &key[0..tables::Sessions::KEY_LEN] != prefix.as_slice() { + break; + } + + let batch = tl_proto::deserialize::(value) + .context("failed to deserialize slasher blocks batch")? + .0; + items.push(batch); + iter.next(); + } + iter.status()?; + + Ok(items) + } + + pub fn load_session_report( + &self, + session_id: ValidationSessionId, + ) -> Result> { + let table = &self.db.session_reports; + let mut read_config = table.new_read_config(); + read_config.set_snapshot(self.snapshot.as_ref()); + + let key = session_key(session_id); + let Some(value) = + self.db + .rocksdb() + .get_pinned_cf_opt(&table.cf(), key.as_slice(), &read_config)? + else { + return Ok(None); + }; + + let report = tl_proto::deserialize::(value.as_ref()) + .context("failed to deserialize slasher session report")? + .0; + Ok(Some(report)) + } +} + +fn session_key(session_id: ValidationSessionId) -> [u8; tables::SessionReports::KEY_LEN] { + let mut key = [0u8; tables::SessionReports::KEY_LEN]; + key[0..4].copy_from_slice(&session_id.catchain_seqno.to_be_bytes()); + key[4..8].copy_from_slice(&session_id.vset_switch_round.to_be_bytes()); + key +} + +fn block_batches_key( + session_id: ValidationSessionId, + validator_idx: u16, + start_seqno: u32, +) -> [u8; tables::BlockBatches::KEY_LEN] { + let mut key = [0u8; tables::BlockBatches::KEY_LEN]; + key[0..4].copy_from_slice(&session_id.catchain_seqno.to_be_bytes()); + key[4..8].copy_from_slice(&session_id.vset_switch_round.to_be_bytes()); + key[8..10].copy_from_slice(&validator_idx.to_be_bytes()); + key[10..14].copy_from_slice(&start_seqno.to_be_bytes()); + key +} + +fn parse_session_id_prefix(key: &[u8]) -> ValidationSessionId { + ValidationSessionId { + catchain_seqno: u32::from_be_bytes(key[0..4].try_into().unwrap()), + vset_switch_round: u32::from_be_bytes(key[4..8].try_into().unwrap()), + } +} + +#[cfg(test)] +mod tests { + use std::num::NonZeroU32; + + use tycho_slasher_traits::{ReceivedSignature, ValidationSessionId}; + use tycho_storage::StorageContext; + + use super::*; + use crate::{SessionPenaltyReport, ValidatorPenalty}; + + #[tokio::test(flavor = "current_thread")] + async fn reads_sessions_and_invalidates_reports() { + let (ctx, _tmp_dir) = StorageContext::new_temp().await.unwrap(); + let storage = SlasherStorage::open(&ctx).unwrap(); + + let session_1 = ValidationSessionId { + catchain_seqno: 2, + vset_switch_round: 10, + }; + let session_2 = ValidationSessionId { + catchain_seqno: 2, + vset_switch_round: 11, + }; + + storage + .store_blocks_batch(session_1, 1, &make_batch(100, &[(100, &[(1, 0b01)])])) + .unwrap(); + storage + .store_blocks_batch(session_1, 2, &make_batch(110, &[(110, &[(1, 0b01)])])) + .unwrap(); + storage + .store_blocks_batch(session_2, 1, &make_batch(120, &[(120, &[(1, 0b01)])])) + .unwrap(); + + let report = SessionPenaltyReport { + session_id: session_1, + total_blocks_in_session: 1, + offenders: vec![ValidatorPenalty { + validator_idx: 1, + missing_signatures: 1, + invalid_signatures: 0, + }] + .into_boxed_slice(), + }; + storage.store_session_report(&report).unwrap(); + assert_eq!( + storage.load_session_report(session_1).unwrap(), + Some(report.clone()) + ); + + let stale = storage + .store_blocks_batch(session_1, 3, &make_batch(130, &[(130, &[(1, 0b01)])])) + .unwrap(); + assert_eq!(stale, Some(report)); + assert_eq!(storage.load_session_report(session_1).unwrap(), None); + + let snapshot = storage.snapshot(); + assert_eq!(snapshot.load_latest_session_id().unwrap(), Some(session_2)); + assert_eq!(snapshot.load_distinct_session_ids().unwrap(), vec![ + session_1, session_2 + ]); + assert_eq!( + snapshot.load_batches_for_session(session_1).unwrap().len(), + 3 + ); + assert_eq!(snapshot.load_session_report(session_1).unwrap(), None); + } + + fn make_batch(start_seqno: u32, blocks: &[(u32, &[(u16, u8)])]) -> BlocksBatch { + let end_seqno = blocks.iter().map(|(seqno, _)| *seqno).max().unwrap(); + let mut validators = blocks + .iter() + .flat_map(|(_, signatures)| signatures.iter().map(|(validator_idx, _)| *validator_idx)) + .collect::>(); + validators.sort_unstable(); + validators.dedup(); + + let mut batch = BlocksBatch::new( + start_seqno, + NonZeroU32::new(end_seqno - start_seqno + 1).unwrap(), + &validators, + ); + + for (seqno, signatures) in blocks { + let signatures = validators + .iter() + .map(|validator_idx| { + let bits = signatures + .iter() + .find_map(|(item, bits)| (*item == *validator_idx).then_some(*bits)) + .unwrap_or(0); + ReceivedSignature(bits) + }) + .collect::>(); + assert!(batch.commit_signatures(*seqno, &signatures)); + } + + batch + } +} diff --git a/slasher/src/storage/models.rs b/slasher/src/storage/models.rs index a3844e8e25..fb9a9a675c 100644 --- a/slasher/src/storage/models.rs +++ b/slasher/src/storage/models.rs @@ -1,8 +1,9 @@ use tl_proto::{TlError, TlPacket, TlRead, TlResult, TlWrite}; +use tycho_slasher_traits::ValidationSessionId; use tycho_util::FastHashSet; use crate::util::BitSet; -use crate::{BlocksBatch, SignatureHistory}; +use crate::{BlocksBatch, SessionPenaltyReport, SignatureHistory, ValidatorPenalty}; #[repr(transparent)] pub struct StoredBlocksBatch(pub BlocksBatch); @@ -98,6 +99,84 @@ impl<'tl> TlRead<'tl> for StoredBlocksBatch { } } +#[repr(transparent)] +pub struct StoredSessionPenaltyReport(pub SessionPenaltyReport); + +impl StoredSessionPenaltyReport { + pub const TL_ID: u32 = tl_proto::id!("slasher.sessionPenaltyReport", scheme = "proto.tl"); + + #[inline] + pub const fn wrap(inner: &SessionPenaltyReport) -> &Self { + // SAFETY: `StoredSessionPenaltyReport` has the same layout as `SessionPenaltyReport`. + unsafe { &*(inner as *const SessionPenaltyReport).cast::() } + } +} + +impl TlWrite for StoredSessionPenaltyReport { + type Repr = tl_proto::Boxed; + + fn max_size_hint(&self) -> usize { + 4 + 4 + 4 + 4 + 4 + self.0.offenders.len() * (4 + 4 + 4) + } + + fn write_to(&self, packet: &mut P) { + packet.write_u32(Self::TL_ID); + packet.write_u32(self.0.session_id.catchain_seqno); + packet.write_u32(self.0.session_id.vset_switch_round); + packet.write_u32(self.0.total_blocks_in_session); + packet.write_u32(self.0.offenders.len() as u32); + for offender in &self.0.offenders { + packet.write_u32(offender.validator_idx as u32); + packet.write_u32(offender.missing_signatures); + packet.write_u32(offender.invalid_signatures); + } + } +} + +impl<'tl> TlRead<'tl> for StoredSessionPenaltyReport { + type Repr = tl_proto::Boxed; + + fn read_from(packet: &mut &'tl [u8]) -> TlResult { + if u32::read_from(packet)? != Self::TL_ID { + return Err(TlError::UnknownConstructor); + } + + let session_id = ValidationSessionId { + catchain_seqno: u32::read_from(packet)?, + vset_switch_round: u32::read_from(packet)?, + }; + let total_blocks_in_session = u32::read_from(packet)?; + let offender_count = u32::read_from(packet)? as usize; + + let mut offenders = Vec::with_capacity(offender_count); + let mut unique_indices = + FastHashSet::with_capacity_and_hasher(offender_count, Default::default()); + for _ in 0..offender_count { + let Ok(validator_idx) = u16::try_from(u32::read_from(packet)?) else { + return Err(TlError::InvalidData); + }; + if !unique_indices.insert(validator_idx) { + return Err(TlError::InvalidData); + } + + let missing_signatures = u32::read_from(packet)?; + let invalid_signatures = u32::read_from(packet)?; + + offenders.push(ValidatorPenalty { + validator_idx, + missing_signatures, + invalid_signatures, + }); + } + + Ok(Self(SessionPenaltyReport { + session_id, + total_blocks_in_session, + offenders: offenders.into_boxed_slice(), + })) + } +} + #[cfg(test)] mod tests { use std::num::NonZeroU32; @@ -150,4 +229,32 @@ mod tests { let loaded = tl_proto::deserialize::(&stored).unwrap(); assert_eq!(batch, loaded.0); } + + #[test] + fn session_penalty_report_tl_repr() { + let report = SessionPenaltyReport { + session_id: ValidationSessionId { + catchain_seqno: 5, + vset_switch_round: 8, + }, + total_blocks_in_session: 10, + offenders: vec![ + ValidatorPenalty { + validator_idx: 1, + missing_signatures: 6, + invalid_signatures: 0, + }, + ValidatorPenalty { + validator_idx: 4, + missing_signatures: 7, + invalid_signatures: 7, + }, + ] + .into_boxed_slice(), + }; + + let stored = tl_proto::serialize(StoredSessionPenaltyReport::wrap(&report)); + let loaded = tl_proto::deserialize::(&stored).unwrap(); + assert_eq!(report, loaded.0); + } } diff --git a/slasher/src/util.rs b/slasher/src/util.rs index a48e0fec55..300b801025 100644 --- a/slasher/src/util.rs +++ b/slasher/src/util.rs @@ -120,6 +120,24 @@ impl BitSet { self.as_slice().iter().all(|item| *item == 0) } + pub fn get(&self, bit: usize) -> bool { + assert!( + bit < self.length, + "get at index {bit} exceeds bitset size {}", + self.length + ); + + let Some(data) = self.data else { + return false; + }; + + let block = bit / Self::BLOCK_BITS; + let rem = bit % Self::BLOCK_BITS; + + // SAFETY: `bit` is whithin the range. + unsafe { (*data.as_ptr().add(block) & (1 << rem)) != 0 } + } + pub fn set(&mut self, bit: usize, enabled: bool) { assert!( bit < self.length, From 85ed5bf08e9b04abb150490fd868eb7cea741c4a Mon Sep 17 00:00:00 2001 From: MrWad3r Date: Fri, 20 Mar 2026 14:26:19 +0100 Subject: [PATCH 11/21] chore(contract): bump dependencies --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index 8bdb32a660..31059b89d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -178,6 +178,7 @@ tycho-wu-tuner = { path = "./wu-tuner", version = "0.3.9" } [patch.crates-io] # patches here +tycho-types = { git = "https://github.com/broxus/tycho-types.git", rev = "ce1f6fb7e755f7de1d9df612b2417e9155be9e7e" } [workspace.lints.rust] future_incompatible = "warn" From 6d2cc48e1c0b031b5807b516bb4ee9c9bdd2de6a Mon Sep 17 00:00:00 2001 From: MrWad3r Date: Tue, 17 Mar 2026 12:19:11 +0100 Subject: [PATCH 12/21] chore(slasher): update cc_seqno derivation from `KbNextSessionUpdate` --- collator/src/collator/do_collate/finalize.rs | 45 +++++++++++++++----- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/collator/src/collator/do_collate/finalize.rs b/collator/src/collator/do_collate/finalize.rs index 48f723a60d..a2bd474da1 100644 --- a/collator/src/collator/do_collate/finalize.rs +++ b/collator/src/collator/do_collate/finalize.rs @@ -886,6 +886,7 @@ impl Phase { validator_info = session_update.apply( &mut consensus_info, + prev_state_extra.validator_info.catchain_seqno, next_session_start_round, session_start.is_curr_switch_after_pause, )?; @@ -1602,6 +1603,7 @@ mod vset_update_start { pub fn apply( &self, consensus_info: &mut ConsensusInfo, + prev_catchain_seqno: u32, next_session_start_round: u32, is_curr_switch_after_pause: bool, ) -> Result> { @@ -1611,6 +1613,10 @@ mod vset_update_start { return Ok(None); } + let catchain_seqno = prev_catchain_seqno + .checked_add(1) + .context("catchain seqno overflow")?; + // simultaneously update session_seqno in collation and consensus if v_(sub)_set changes; // genesis change (recovery or config) should not rotate validators by itself, so it // doesn't allow to apply scheduled v_set immediately despite it splits dag history @@ -1628,12 +1634,12 @@ mod vset_update_start { // calculate next validator subset and hash let current_vset = self.current_vset.parse::()?; - let Some((_, validator_list_hash_short)) = current_vset - .compute_mc_subset(next_session_start_round, self.shuffle_mc_validators) + let Some((_, validator_list_hash_short)) = + current_vset.compute_mc_subset(catchain_seqno, self.shuffle_mc_validators) else { anyhow::bail!( "Error calculating subset of validators for next session \ - (shard_id = {}, session_seqno = {next_session_start_round})", + (shard_id = {}, catchain_seqno = {catchain_seqno})", ShardIdent::MASTERCHAIN, ); }; @@ -1641,7 +1647,7 @@ mod vset_update_start { Ok(Some(ValidatorInfo { validator_list_hash_short, // TODO: rename field in types - catchain_seqno: next_session_start_round, + catchain_seqno, nx_cc_updated: true, })) } @@ -1780,12 +1786,17 @@ mod vset_update_start { assert_eq!(next_1, after_pause_round); let validator_info = stub_update - .apply(&mut cons_info, next_1, start_1.is_curr_switch_after_pause) + .apply( + &mut cons_info, + 10, + next_1, + start_1.is_curr_switch_after_pause, + ) .unwrap() .unwrap(); assert_eq!(cons_info.prev_vset_switch_round, 0); assert_eq!(cons_info.vset_switch_round, after_pause_round); - assert_eq!(validator_info.catchain_seqno, next_1); + assert_eq!(validator_info.catchain_seqno, 11); // Second vset change while switch is still "applied/too close": push by full history. @@ -1805,12 +1816,17 @@ mod vset_update_start { assert_eq!(next_2, (next_1 + cons_conf.max_total_rounds() + 1)); let validator_info = stub_update - .apply(&mut cons_info, next_2, start_2.is_curr_switch_after_pause) + .apply( + &mut cons_info, + validator_info.catchain_seqno, + next_2, + start_2.is_curr_switch_after_pause, + ) .unwrap() .unwrap(); assert_eq!(cons_info.prev_vset_switch_round, next_1); assert_eq!(cons_info.vset_switch_round, next_2); - assert_eq!(validator_info.catchain_seqno, next_2); + assert_eq!(validator_info.catchain_seqno, 12); // Third vset change while switch is far in the future: keep the same switch round. @@ -1831,12 +1847,17 @@ mod vset_update_start { assert_eq!(next_3, next_2); let validator_info = stub_update - .apply(&mut cons_info, next_3, start_3.is_curr_switch_after_pause) + .apply( + &mut cons_info, + validator_info.catchain_seqno, + next_3, + start_3.is_curr_switch_after_pause, + ) .unwrap() .unwrap(); assert_eq!(cons_info.prev_vset_switch_round, next_1); assert_eq!(cons_info.vset_switch_round, next_2); - assert_eq!(validator_info.catchain_seqno, next_2); + assert_eq!(validator_info.catchain_seqno, 13); } #[test] @@ -1852,7 +1873,9 @@ mod vset_update_start { let mut cons_info = random_consensus_info(); let before = cons_info; - let validator_info = update.apply(&mut cons_info, random(), true).unwrap(); + let validator_info = update + .apply(&mut cons_info, random(), random(), true) + .unwrap(); assert!(validator_info.is_none(), "{update:?} {cons_info:?}"); assert_eq!(cons_info, before, "{update:?} {cons_info:?}"); From 712a481c4af8c5d85f3ca5d8f3afcbed7b2867d2 Mon Sep 17 00:00:00 2001 From: MrWad3r Date: Tue, 17 Mar 2026 13:00:13 +0100 Subject: [PATCH 13/21] chore(slasher): add slasher generation to zerostate --- cli/res/slasher_code.boc | Bin 0 -> 400 bytes cli/src/cmd/tools/gen_zerostate.rs | 68 ++++++++++++++++++++++++++++- 2 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 cli/res/slasher_code.boc diff --git a/cli/res/slasher_code.boc b/cli/res/slasher_code.boc new file mode 100644 index 0000000000000000000000000000000000000000..87ef7baa6868e0a3b36d03be1bb547c064513d1f GIT binary patch literal 400 zcmV;B0dM}b?woQ#0to>C0e}Dj6#oG9q!aYK^2iGT0s$Zb2Lb^|0|o*D%mf7h3)zRn zkCFrc$GO`@+x7>hKBgx?M|~!~3gN&+=rha3lAFdW3Y!C;a zphw^t0rv8P2-g@O(?8Q6(;qR@A2HJp)Y14OPy@U2xdX_~$$%pGGtk#82$b@_(?2oO z57Q6RKQZ(L7Jv}+4sW6IveO3vfZwpdQ}U_P9|3|8j)5nNy7H;l3;zQ708nE25b~&6 zwg`a&n)ovyZt~D&(AO*o4D!F$|1r}C(Fg#3_%r|@Zt~D(ZxC-1qyx}FGw=-Z;AHJY u(9=KBAb}Hzp(44Q_#>t~fdlBMBEG`_k#OSjw6+Pz%RkBORQLd}f7LZfz_m00 literal 0 HcmV?d00001 diff --git a/cli/src/cmd/tools/gen_zerostate.rs b/cli/src/cmd/tools/gen_zerostate.rs index 779622679b..016f4a4f12 100644 --- a/cli/src/cmd/tools/gen_zerostate.rs +++ b/cli/src/cmd/tools/gen_zerostate.rs @@ -125,6 +125,10 @@ struct ZerostateConfig { #[serde(default, with = "Boc", skip_serializing_if = "Option::is_none")] elector_code: Option, + slasher_balance: Tokens, + #[serde(default, with = "Boc", skip_serializing_if = "Option::is_none")] + slasher_code: Option, + #[serde(with = "serde_account_states")] accounts: FastHashMap, @@ -210,6 +214,9 @@ impl ZerostateConfig { if let Some(minter_address) = minter_address { fundamental_addresses.set(minter_address, ())?; } + if let Some(slasher_params) = self.params.get::()? { + fundamental_addresses.set(slasher_params.address, ())?; + } self.params.set::(&fundamental_addresses)?; } @@ -311,6 +318,25 @@ impl ZerostateConfig { ); } + // Slasher + if let Some(slasher_params) = self.params.get::()? { + let prev = self.accounts.insert( + slasher_params.address, + build_slasher_account( + &slasher_params.address, + self.slasher_balance, + self.slasher_code.clone(), + )? + .into(), + ); + if prev.is_some() { + anyhow::bail!( + "full slasher account state cannot be specified manually, \ + use \"slasher_code\" param instead" + ); + } + } + // Minter match (&self.minter_public_key, self.params.get::()?) { (Some(public_key), Some(minter_address)) => { @@ -500,10 +526,12 @@ impl Default for ZerostateConfig { global_id: 0, config_public_key: *zero_public_key(), minter_public_key: None, - config_balance: Tokens::new(500_000_000_000), // 500 + config_balance: default_special_account_balance(), config_code: None, - elector_balance: Tokens::new(500_000_000_000), // 500 + elector_balance: default_special_account_balance(), elector_code: None, + slasher_balance: default_special_account_balance(), + slasher_code: None, accounts: Default::default(), validators: Default::default(), params: make_default_params().unwrap(), @@ -944,6 +972,38 @@ fn build_elector_account( Ok(account) } +fn build_slasher_account( + address: &HashBytes, + balance: Tokens, + custom_code: Option, +) -> Result { + const SLASHER_CODE: &[u8] = include_bytes!("../../../res/slasher_code.boc"); + + let code = custom_code.unwrap_or_else(|| Boc::decode(SLASHER_CODE).unwrap()); + + let mut data = CellBuilder::new(); + data.store_u64(0)?; + let data = data.build()?; + + let mut account = Account { + address: StdAddr::new(-1, *address).into(), + storage_stat: Default::default(), + last_trans_lt: 0, + balance: balance.into(), + state: AccountState::Active(StateInit { + split_depth: None, + special: None, + code: Some(code), + data: Some(data), + libraries: Dict::new(), + }), + }; + + account.storage_stat.used = compute_storage_used(&account)?; + + Ok(account) +} + fn build_minter_account(pubkey: &ed25519::PublicKey, address: &HashBytes) -> Result { const MINTER_STATE: &[u8] = include_bytes!("../../../res/minter_state.boc"); @@ -976,6 +1036,10 @@ fn zero_public_key() -> &'static ed25519::PublicKey { KEY.get_or_init(|| ed25519::PublicKey::from_bytes([0; 32]).unwrap()) } +fn default_special_account_balance() -> Tokens { + Tokens::new(500_000_000_000) // 500 +} + mod serde_account_states { use serde::de::Deserializer; use serde::ser::{SerializeMap, Serializer}; From ab84b8324816c359e54362f0173477cd073dad1a Mon Sep 17 00:00:00 2001 From: MrWad3r Date: Tue, 17 Mar 2026 16:17:32 +0100 Subject: [PATCH 14/21] chore(slasher): add logs with `slasher` target --- cli/src/cmd/tools/gen_zerostate.rs | 7 ++ slasher/src/analyzer.rs | 99 ------------------- slasher/src/bc/mod.rs | 47 +++++++-- slasher/src/collector/validator_events.rs | 50 ++++++++-- slasher/src/lib.rs | 112 ++++++++++++++++++---- slasher/src/storage/mod.rs | 101 ------------------- slasher/src/tracing_targets.rs | 1 + 7 files changed, 179 insertions(+), 238 deletions(-) create mode 100644 slasher/src/tracing_targets.rs diff --git a/cli/src/cmd/tools/gen_zerostate.rs b/cli/src/cmd/tools/gen_zerostate.rs index 016f4a4f12..eed3d215c0 100644 --- a/cli/src/cmd/tools/gen_zerostate.rs +++ b/cli/src/cmd/tools/gen_zerostate.rs @@ -1,4 +1,5 @@ use std::collections::hash_map; +use std::num::NonZeroU8; use std::path::PathBuf; use std::sync::OnceLock; @@ -837,6 +838,12 @@ fn make_default_params() -> Result { // Param 31 params.set_fundamental_addresses(&[HashBytes([0x00; 32]), HashBytes([0x33; 32])])?; + // Param 666 + params.set::(&SlasherParamsConfig { + address: HashBytes([0x66; 32]), + batch_size: NonZeroU8::new(100).unwrap(), + })?; + // Param 43 params.set_size_limits(&SizeLimitsConfig { max_msg_bits: 1 << 21, diff --git a/slasher/src/analyzer.rs b/slasher/src/analyzer.rs index a8df426e4e..ec06e28c44 100644 --- a/slasher/src/analyzer.rs +++ b/slasher/src/analyzer.rs @@ -222,102 +222,3 @@ fn session_labels(session_id: ValidationSessionId) -> [(&'static str, String); 2 ), ] } - -#[cfg(test)] -mod tests { - use std::num::NonZeroU32; - - use tycho_slasher_traits::{ReceivedSignature, ValidationSessionId}; - - use super::*; - - #[test] - fn analyzes_combined_penalty_threshold() { - let session_id = ValidationSessionId { - catchain_seqno: 7, - vset_switch_round: 9, - }; - let batch = make_batch(100, &[ - (100, &[(1, 0), (2, 0b10), (3, 0b01)]), - (101, &[(1, 0), (2, 0b01), (3, 0b01)]), - (102, &[(1, 0b10), (2, 0b10), (3, 0b01)]), - (103, &[(1, 0b01), (2, 0b01), (3, 0b01)]), - ]); - - let report = analyze_session(session_id, &[batch]); - - assert_eq!(report.total_blocks_in_session, 4); - assert_eq!(report.offenders.as_ref(), &[ - ValidatorPenalty { - validator_idx: 1, - missing_signatures: 3, - invalid_signatures: 1, - }, - ValidatorPenalty { - validator_idx: 2, - missing_signatures: 2, - invalid_signatures: 2, - }, - ]); - } - - #[test] - fn merges_overlapping_batches_from_multiple_observers() { - let session_id = ValidationSessionId { - catchain_seqno: 11, - vset_switch_round: 13, - }; - let missing = make_batch(200, &[(200, &[(1, 0)])]); - let valid = make_batch(200, &[(200, &[(1, 0b01)])]); - - let report = analyze_session(session_id, &[missing, valid]); - - assert_eq!(report.total_blocks_in_session, 1); - assert!(report.offenders.is_empty()); - } - - #[test] - #[should_panic(expected = "slasher analyzer invariant violated")] - fn panics_on_dual_signature_bits() { - let session_id = ValidationSessionId { - catchain_seqno: 17, - vset_switch_round: 19, - }; - let batch = make_batch(300, &[(300, &[(1, 0b11)])]); - - let _ = analyze_session(session_id, &[batch]); - } - - fn make_batch(start_seqno: u32, blocks: &[(u32, &[(u16, u8)])]) -> BlocksBatch { - let end_seqno = blocks.iter().map(|(seqno, _)| *seqno).max().unwrap(); - let mut validators = blocks - .iter() - .flat_map(|(_, signatures)| signatures.iter().map(|(validator_idx, _)| *validator_idx)) - .collect::>(); - validators.sort_unstable(); - validators.dedup(); - - let mut batch = BlocksBatch::new( - start_seqno, - NonZeroU32::new(end_seqno - start_seqno + 1).unwrap(), - &validators, - ); - - for (seqno, signatures) in blocks { - let mut slots = validators - .iter() - .map(|validator_idx| { - let bits = signatures - .iter() - .find_map(|(item, bits)| (*item == *validator_idx).then_some(*bits)) - .unwrap_or(0); - ReceivedSignature(bits) - }) - .collect::>(); - assert!(batch.commit_signatures(*seqno, &slots)); - slots.clear(); - } - - batch - } -} diff --git a/slasher/src/bc/mod.rs b/slasher/src/bc/mod.rs index fe877fe3f2..d424d2dde4 100644 --- a/slasher/src/bc/mod.rs +++ b/slasher/src/bc/mod.rs @@ -12,6 +12,7 @@ use tycho_types::models::{ use tycho_util::FastDashMap; pub use self::stub_contract::StubSlasherContract; +use crate::tracing_targets; use crate::util::BitSet; mod stub_contract; @@ -87,9 +88,13 @@ impl ContractSubscription { } } - pub fn handle_account_transaction(&self, tx_hash: &HashBytes, tx: &Transaction) -> Result<()> { + pub fn handle_account_transaction( + &self, + tx_hash: &HashBytes, + tx: &Transaction, + ) -> Result { let Some(in_msg) = &tx.in_msg else { - return Ok(()); + return Ok(false); }; let msg_hash = in_msg.repr_hash(); @@ -98,19 +103,31 @@ impl ContractSubscription { .tx .send(MessageDeliveryStatus::Sent { tx_hash: *tx_hash }) .ok(); + return Ok(true); } - Ok(()) + Ok(false) } pub fn cleanup_expired_messages(&self, now_sec: u32) { - let mut dropped = 0usize; - self.pending_messages.retain(|_, msg| { - let retain = msg.expire_at >= now_sec; - dropped += !retain as usize; - retain - }); + let expired = self + .pending_messages + .iter() + .filter_map(|entry| (entry.expire_at < now_sec).then_some(*entry.key())) + .collect::>(); + + let dropped = expired.len(); + for msg_hash in expired { + if let Some((_, pending)) = self.pending_messages.remove(&msg_hash) { + pending.tx.send(MessageDeliveryStatus::Expired).ok(); + } + } + if dropped > 0 { - tracing::warn!(dropped, "dropped pending messages"); + tracing::warn!( + target: tracing_targets::SLASHER, + dropped, + "dropped pending messages" + ); } } } @@ -176,6 +193,16 @@ impl BlocksBatch { .saturating_add(self.committed_blocks.len() as u32) } + pub fn committed_block_count(&self) -> usize { + (0..self.committed_blocks.len()) + .filter(|offset| self.committed_blocks.get(*offset)) + .count() + } + + pub fn validator_count(&self) -> usize { + self.signatures_history.len() + } + pub fn contains_seqno(&self, seqno: u32) -> bool { (self.start_seqno..self.seqno_after()).contains(&seqno) } diff --git a/slasher/src/collector/validator_events.rs b/slasher/src/collector/validator_events.rs index ab9dbb7ad0..0e742bf8dc 100644 --- a/slasher/src/collector/validator_events.rs +++ b/slasher/src/collector/validator_events.rs @@ -12,6 +12,7 @@ use tycho_types::models::{BlockId, IndexedValidatorDescription}; use tycho_util::{DashMapEntry, FastDashMap}; use crate::bc::BlocksBatch; +use crate::tracing_targets; const INIT_QUEUE_CAPACITY: usize = 3; @@ -77,7 +78,11 @@ impl ValidatorEventsCollector { if items.len() >= self.init_queue_capacity && let Some(info) = items.pop_front() { - tracing::warn!(session_id = ?info.session_id, "session info dropped from init queue"); + tracing::warn!( + target: tracing_targets::SLASHER, + session_id = ?info.session_id, + "session info dropped from init queue" + ); } items.push_back(info); } @@ -127,7 +132,11 @@ impl ValidatorEventsListener for ValidatorEventsCollector { own_validator_idx: u16, validators: &[IndexedValidatorDescription], ) { - tracing::debug!(first_mc_seqno, "on_session_started"); + tracing::debug!( + target: tracing_targets::SLASHER, + first_mc_seqno, + "on_session_started" + ); let validator_indices = validators .iter() @@ -157,17 +166,20 @@ impl ValidatorEventsListener for ValidatorEventsCollector { validators, }); } else { - tracing::warn!("duplicate session"); + tracing::warn!(target: tracing_targets::SLASHER, "duplicate session"); } } #[instrument(skip_all, fields(session_id = ?session_id))] fn on_session_finished(&self, session_id: ValidationSessionId) { - tracing::debug!("on_session_finished"); + tracing::debug!(target: tracing_targets::SLASHER, "on_session_finished"); if let Some((_, session)) = self.sessions.remove(&session_id) && let Err(e) = session.commit_final_batch() { - tracing::warn!("failed to commit blocks batch on finish: {e:?}"); + tracing::warn!( + target: tracing_targets::SLASHER, + "failed to commit blocks batch on finish: {e:?}" + ); } } @@ -183,9 +195,16 @@ impl ValidatorEventsListener for ValidatorEventsCollector { return; } - tracing::debug!(%block_id, "on_block_validated"); + tracing::debug!( + target: tracing_targets::SLASHER, + %block_id, + "on_block_validated" + ); let Some(mut session) = self.sessions.get_mut(&session_id) else { - tracing::warn!("session not found, ignoring on_block_validated event"); + tracing::warn!( + target: tracing_targets::SLASHER, + "session not found, ignoring on_block_validated event" + ); return; }; session.handle_block(block_id.seqno, Some(signatures.as_ref())); @@ -198,9 +217,16 @@ impl ValidatorEventsListener for ValidatorEventsCollector { return; } - tracing::debug!(%block_id, "on_block_skipped"); + tracing::debug!( + target: tracing_targets::SLASHER, + %block_id, + "on_block_skipped" + ); let Some(mut session) = self.sessions.get_mut(&session_id) else { - tracing::warn!("session not found, ignoring on_block_skipped event"); + tracing::warn!( + target: tracing_targets::SLASHER, + "session not found, ignoring on_block_skipped event" + ); return; }; session.handle_block(block_id.seqno, None); @@ -244,7 +270,11 @@ impl SessionState { if let Some(batch) = to_commit && let Err(e) = self.commit_batch(batch) { - tracing::error!(event_type, "failed to commit blocks batch: {e:?}"); + tracing::error!( + target: tracing_targets::SLASHER, + event_type, + "failed to commit blocks batch: {e:?}" + ); } true } diff --git a/slasher/src/lib.rs b/slasher/src/lib.rs index 8f0c9db0e5..fa9dc2372c 100644 --- a/slasher/src/lib.rs +++ b/slasher/src/lib.rs @@ -38,9 +38,11 @@ pub mod collector { mod bc; mod storage; +mod tracing_targets; mod util; #[derive(Debug, Clone, Serialize, Deserialize, PartialConfig)] +#[serde(default)] pub struct SlasherConfig { /// TTL of messages to the slasher contract. /// @@ -152,11 +154,16 @@ impl Slasher { vset_switch_round, catchain_seqno, }; - tracing::trace!(?slasher_params, ?current_session_id); + tracing::trace!( + target: tracing_targets::SLASHER, + ?slasher_params, + ?current_session_id + ); // TODO: Add metrics. if current_session_id != this.known_session_id.load() { tracing::info!( + target: tracing_targets::SLASHER, old_session_id = ?this.known_session_id.load(), ?current_session_id, "slasher observed validation session change", @@ -168,13 +175,19 @@ impl Slasher { let subscription = match this.subscription.load_full() { Some(s) if s.address() == &slasher_address => s, _ => { - tracing::info!(%slasher_address, "slasher address changed"); + tracing::info!( + target: tracing_targets::SLASHER, + %slasher_address, + "slasher address changed" + ); let s = Arc::new(ContractSubscription::new(&slasher_address)); this.subscription.store(Some(s.clone())); s } }; + subscription.cleanup_expired_messages(cx.block.load_info()?.gen_utime); + let extra = cx.block.load_extra()?.account_blocks.load()?; if let Some((_, account_block)) = extra.get(slasher_address.address)? { for entry in account_block.transactions.iter() { @@ -183,16 +196,36 @@ impl Slasher { let tx = tx.load()?; tracing::debug!( + target: tracing_targets::SLASHER, %tx_hash, msg_hash = ?tx.in_msg.as_ref().map(|msg| msg.repr_hash()), "found slasher transaction", ); - subscription.handle_account_transaction(tx_hash, &tx)?; + let matched_own_message = subscription.handle_account_transaction(tx_hash, &tx)?; match self.shared.contract.decode_event(&tx) { Ok(Some(event)) => match event { bc::SlasherContractEvent::SubmitBlocksBatch(submitted) => { + let batch = &submitted.blocks_batch; + tracing::info!( + target: tracing_targets::SLASHER, + %tx_hash, + session_id = ?submitted.session_id, + validator_idx = submitted.validator_idx, + batch_start_seqno = batch.start_seqno(), + batch_seqno_after = batch.seqno_after(), + batch_slots = batch.committed_blocks.len(), + committed_blocks = batch.committed_block_count(), + validators = batch.validator_count(), + "{}", + if matched_own_message { + "own blocks batch committed by slasher" + } else { + "received blocks batch from validator" + } + ); + // TODO: Move into blocking. if let Some(report) = this.storage.store_blocks_batch( submitted.session_id, @@ -205,7 +238,11 @@ impl Slasher { } }, Ok(None) => {} - Err(e) => tracing::warn!(%tx_hash, "failed to parse slasher event: {e:?}"), + Err(e) => tracing::warn!( + target: tracing_targets::SLASHER, + %tx_hash, + "failed to parse slasher event: {e:?}" + ), } } } @@ -217,9 +254,17 @@ impl Slasher { .pop_session_to_init(mc_seqno) { let session_id = session_info.session_id; - tracing::info!(?session_id, "found session to init"); + tracing::info!( + target: tracing_targets::SLASHER, + ?session_id, + "found session to init" + ); if !session_info.can_participate(&this.node_keys.public_key) { - tracing::info!(?session_id, "skipping session"); + tracing::info!( + target: tracing_targets::SLASHER, + ?session_id, + "skipping session" + ); continue; } @@ -229,7 +274,11 @@ impl Slasher { slasher_params.blocks_batch_size, tx, ) { - tracing::warn!(?session_id, "session removed before init"); + tracing::warn!( + target: tracing_targets::SLASHER, + ?session_id, + "session removed before init" + ); continue; } @@ -299,8 +348,8 @@ impl SlasherSharedState { info: ValidatorSessionInfo, mut rx: collector::BlocksBatchRx, ) { - tracing::info!("started"); - scopeguard::defer!(tracing::info!("finished")); + tracing::info!(target: tracing_targets::SLASHER, "started"); + scopeguard::defer!(tracing::info!(target: tracing_targets::SLASHER, "finished")); let mut send_task = None; @@ -309,7 +358,10 @@ impl SlasherSharedState { && let Some(timeout) = self.config.prev_delivery_timeout && tokio::time::timeout(timeout, send_task).await.is_err() { - tracing::warn!("timeout on waiting for the previous batch to be delivered"); + tracing::warn!( + target: tracing_targets::SLASHER, + "timeout on waiting for the previous batch to be delivered" + ); } send_task = Some(JoinTask::new(self.clone().deliver_batch_message( @@ -328,7 +380,7 @@ impl SlasherSharedState { ) { loop { let Some(subscription) = self.subscription.load_full() else { - tracing::warn!("no slasher contract subscription"); + tracing::warn!(target: tracing_targets::SLASHER, "no slasher contract subscription"); break; }; @@ -345,7 +397,10 @@ impl SlasherSharedState { let signed = match self.contract.encode_blocks_batch_message(¶ms) { Ok(signed) => signed, Err(e) => { - tracing::error!("failed to encode batch message: {e:?}"); + tracing::error!( + target: tracing_targets::SLASHER, + "failed to encode batch message: {e:?}" + ); return; } }; @@ -355,13 +410,17 @@ impl SlasherSharedState { match subscription.track_message(&msg_hash, signed.expire_at) { Ok(res) => { tracing::info!( + target: tracing_targets::SLASHER, %msg_hash, address = %params.address, session_id = ?params.session_id, validator_idx = params.validator_idx, - batch_seqno = batch.start_seqno, - block_count = batch.committed_blocks.len(), - "sending blocks batch" + batch_start_seqno = batch.start_seqno(), + batch_seqno_after = batch.seqno_after(), + batch_slots = batch.committed_blocks.len(), + committed_blocks = batch.committed_block_count(), + validators = batch.validator_count(), + "sending own blocks batch to slasher" ); self.blockchain_rpc_client .broadcast_external_message(&boc) @@ -370,17 +429,34 @@ impl SlasherSharedState { match res.await { Ok(MessageDeliveryStatus::Sent { tx_hash }) => { - tracing::info!(%tx_hash, "batch message delivered"); + tracing::info!( + target: tracing_targets::SLASHER, + %tx_hash, + session_id = ?params.session_id, + validator_idx = params.validator_idx, + batch_start_seqno = batch.start_seqno(), + batch_seqno_after = batch.seqno_after(), + batch_slots = batch.committed_blocks.len(), + committed_blocks = batch.committed_block_count(), + validators = batch.validator_count(), + "own blocks batch delivered" + ); return; } Ok(MessageDeliveryStatus::Expired) => { // TODO: Execute transaction locally to guess the reason. - tracing::warn!("batch message expired"); + tracing::warn!( + target: tracing_targets::SLASHER, + "batch message expired" + ); } Err(_) => return, } } - Err(e) => tracing::warn!("failed to track message: {e:?}"), + Err(e) => tracing::warn!( + target: tracing_targets::SLASHER, + "failed to track message: {e:?}" + ), } tokio::time::sleep(self.config.message_retry_interval).await; diff --git a/slasher/src/storage/mod.rs b/slasher/src/storage/mod.rs index d04fc8f8d4..3192914a9b 100644 --- a/slasher/src/storage/mod.rs +++ b/slasher/src/storage/mod.rs @@ -242,104 +242,3 @@ fn parse_session_id_prefix(key: &[u8]) -> ValidationSessionId { vset_switch_round: u32::from_be_bytes(key[4..8].try_into().unwrap()), } } - -#[cfg(test)] -mod tests { - use std::num::NonZeroU32; - - use tycho_slasher_traits::{ReceivedSignature, ValidationSessionId}; - use tycho_storage::StorageContext; - - use super::*; - use crate::{SessionPenaltyReport, ValidatorPenalty}; - - #[tokio::test(flavor = "current_thread")] - async fn reads_sessions_and_invalidates_reports() { - let (ctx, _tmp_dir) = StorageContext::new_temp().await.unwrap(); - let storage = SlasherStorage::open(&ctx).unwrap(); - - let session_1 = ValidationSessionId { - catchain_seqno: 2, - vset_switch_round: 10, - }; - let session_2 = ValidationSessionId { - catchain_seqno: 2, - vset_switch_round: 11, - }; - - storage - .store_blocks_batch(session_1, 1, &make_batch(100, &[(100, &[(1, 0b01)])])) - .unwrap(); - storage - .store_blocks_batch(session_1, 2, &make_batch(110, &[(110, &[(1, 0b01)])])) - .unwrap(); - storage - .store_blocks_batch(session_2, 1, &make_batch(120, &[(120, &[(1, 0b01)])])) - .unwrap(); - - let report = SessionPenaltyReport { - session_id: session_1, - total_blocks_in_session: 1, - offenders: vec![ValidatorPenalty { - validator_idx: 1, - missing_signatures: 1, - invalid_signatures: 0, - }] - .into_boxed_slice(), - }; - storage.store_session_report(&report).unwrap(); - assert_eq!( - storage.load_session_report(session_1).unwrap(), - Some(report.clone()) - ); - - let stale = storage - .store_blocks_batch(session_1, 3, &make_batch(130, &[(130, &[(1, 0b01)])])) - .unwrap(); - assert_eq!(stale, Some(report)); - assert_eq!(storage.load_session_report(session_1).unwrap(), None); - - let snapshot = storage.snapshot(); - assert_eq!(snapshot.load_latest_session_id().unwrap(), Some(session_2)); - assert_eq!(snapshot.load_distinct_session_ids().unwrap(), vec![ - session_1, session_2 - ]); - assert_eq!( - snapshot.load_batches_for_session(session_1).unwrap().len(), - 3 - ); - assert_eq!(snapshot.load_session_report(session_1).unwrap(), None); - } - - fn make_batch(start_seqno: u32, blocks: &[(u32, &[(u16, u8)])]) -> BlocksBatch { - let end_seqno = blocks.iter().map(|(seqno, _)| *seqno).max().unwrap(); - let mut validators = blocks - .iter() - .flat_map(|(_, signatures)| signatures.iter().map(|(validator_idx, _)| *validator_idx)) - .collect::>(); - validators.sort_unstable(); - validators.dedup(); - - let mut batch = BlocksBatch::new( - start_seqno, - NonZeroU32::new(end_seqno - start_seqno + 1).unwrap(), - &validators, - ); - - for (seqno, signatures) in blocks { - let signatures = validators - .iter() - .map(|validator_idx| { - let bits = signatures - .iter() - .find_map(|(item, bits)| (*item == *validator_idx).then_some(*bits)) - .unwrap_or(0); - ReceivedSignature(bits) - }) - .collect::>(); - assert!(batch.commit_signatures(*seqno, &signatures)); - } - - batch - } -} diff --git a/slasher/src/tracing_targets.rs b/slasher/src/tracing_targets.rs new file mode 100644 index 0000000000..afd2daf608 --- /dev/null +++ b/slasher/src/tracing_targets.rs @@ -0,0 +1 @@ +pub const SLASHER: &str = "slasher"; From 6c9d19e320d690a85d8b2531ff2683310ef06ced Mon Sep 17 00:00:00 2001 From: MrWad3r Date: Thu, 26 Mar 2026 11:40:38 +0100 Subject: [PATCH 15/21] chore(slasher): review pt.1 --- collator/src/collator/do_collate/finalize.rs | 4 +- slasher/src/analyzer.rs | 320 ++++++++---------- slasher/src/lib.rs | 68 +++- slasher/src/proto.tl | 76 ++++- slasher/src/storage/db.rs | 87 +++-- slasher/src/storage/mod.rs | 328 ++++++++++++++---- slasher/src/storage/models.rs | 336 +++++++++++++------ 7 files changed, 814 insertions(+), 405 deletions(-) diff --git a/collator/src/collator/do_collate/finalize.rs b/collator/src/collator/do_collate/finalize.rs index a2bd474da1..555ddd83eb 100644 --- a/collator/src/collator/do_collate/finalize.rs +++ b/collator/src/collator/do_collate/finalize.rs @@ -1634,8 +1634,8 @@ mod vset_update_start { // calculate next validator subset and hash let current_vset = self.current_vset.parse::()?; - let Some((_, validator_list_hash_short)) = - current_vset.compute_mc_subset(catchain_seqno, self.shuffle_mc_validators) + let Some((_, validator_list_hash_short)) = current_vset + .compute_mc_subset(next_session_start_round, self.shuffle_mc_validators) else { anyhow::bail!( "Error calculating subset of validators for next session \ diff --git a/slasher/src/analyzer.rs b/slasher/src/analyzer.rs index ec06e28c44..427f613e41 100644 --- a/slasher/src/analyzer.rs +++ b/slasher/src/analyzer.rs @@ -1,224 +1,188 @@ -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use tycho_slasher_traits::ValidationSessionId; -use tycho_util::{FastHashMap, FastHashSet}; +use tycho_types::cell::HashBytes; use crate::BlocksBatch; +#[derive(Debug, PartialEq, Eq)] +pub struct ObservedBlocksBatch { + pub observer_validator_idx: u16, + pub batch: BlocksBatch, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SessionMeta { + pub session_id: ValidationSessionId, + pub epoch_start_session_id: ValidationSessionId, + pub validator_indices: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct VsetEpoch { + pub start_session_id: ValidationSessionId, + pub vset_hash: HashBytes, + pub next_epoch_start_session_id: Option, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct SessionPenaltyReport { pub session_id: ValidationSessionId, - pub total_blocks_in_session: u32, - pub offenders: Box<[ValidatorPenalty]>, + pub epoch_start_session_id: ValidationSessionId, + pub session_weight: u32, + pub validators: Box<[SessionValidatorScore]>, } #[derive(Debug, Clone, PartialEq, Eq)] -pub struct ValidatorPenalty { +pub struct SessionValidatorScore { pub validator_idx: u16, - pub missing_signatures: u32, - pub invalid_signatures: u32, + pub earned_points: u64, + pub max_points: u64, + pub is_bad: bool, } -#[derive(Debug, Default, Clone, Copy)] -struct ObservedSignature { - has_valid_signature: bool, - has_invalid_signature: bool, +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct VsetPenaltyReport { + pub epoch_start_session_id: ValidationSessionId, + pub vset_hash: HashBytes, + pub validators: Box<[VsetValidatorPenalty]>, } -#[derive(Debug, Default, Clone, Copy)] -struct SignatureTotals { - missing_signatures: u32, - invalid_signatures: u32, +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct VsetValidatorPenalty { + pub validator_idx: u16, + pub bad_sessions_weight: u64, + pub total_sessions_weight: u64, + pub is_bad: bool, } pub fn analyze_session( - session_id: ValidationSessionId, - batches: &[BlocksBatch], + meta: &SessionMeta, + batches: &[ObservedBlocksBatch], ) -> SessionPenaltyReport { - let mut validators = FastHashSet::default(); - let mut blocks = BTreeMap::>::new(); + let mut committed_blocks = BTreeSet::new(); + let mut observed_rows = BTreeSet::new(); + // observer -> observed : points * session weight + let mut validator_points = BTreeMap::<(u16, u16), u64>::new(); - for batch in batches { - for history in &batch.signatures_history { - validators.insert(history.validator_idx); - } + for item in batches { + observed_rows.insert(item.observer_validator_idx); - for offset in 0..batch.committed_blocks.len() { - if !batch.committed_blocks.get(offset) { + for offset in 0..item.batch.committed_blocks.len() { + if !item.batch.committed_blocks.get(offset) { continue; } - let seqno = batch.start_seqno + offset as u32; - let signatures = blocks.entry(seqno).or_default(); - - // Different validators can submit overlapping matrices for the same block. - // We merge them by taking the union of observed bits, but a - // `(block, validator_idx)` pair must never end up with both `valid` - // and `invalid` states at once. If that happens, the input data is - // internally inconsistent and we fail fast instead of guessing. - for history in &batch.signatures_history { - let offset = offset * 2; - let has_invalid_signature = history.bits.get(offset); - let has_valid_signature = history.bits.get(offset + 1); - assert!( - !(has_invalid_signature && has_valid_signature), - "slasher analyzer invariant violated: validator {} has both valid and invalid bits for block {}", - history.validator_idx, - seqno, - ); - - let observed = signatures.entry(history.validator_idx).or_default(); - observed.has_invalid_signature |= has_invalid_signature; - observed.has_valid_signature |= has_valid_signature; - assert!( - !(observed.has_invalid_signature && observed.has_valid_signature), - "slasher analyzer invariant violated: validator {} has conflicting observations for block {}", - history.validator_idx, - seqno, - ); + committed_blocks.insert(item.batch.start_seqno + offset as u32); + + for history in &item.batch.signatures_history { + let bit_offset = offset * 2; + let has_invalid_signature = history.bits.get(bit_offset); + let has_valid_signature = history.bits.get(bit_offset + 1); + + if !(has_invalid_signature && has_valid_signature) { + tracing::warn!( + "slasher analyzer invariant violated: observer {} saw validator {} as both valid and invalid in session {:?}", + item.observer_validator_idx, + history.validator_idx, + meta.session_id, + ); + continue; + } + + if has_valid_signature { + *validator_points + .entry((item.observer_validator_idx, history.validator_idx)) + .or_default() += 1; + } } } } - let total_blocks_in_session = blocks.len() as u32; - let threshold = total_blocks_in_session / 2; - - let mut validators = validators.into_iter().collect::>(); - validators.sort_unstable(); + let session_weight = committed_blocks.len() as u64; - let mut totals = FastHashMap::::default(); - for signatures in blocks.values() { - for &validator_idx in &validators { - let observed = signatures.get(&validator_idx).copied().unwrap_or_default(); - let totals = totals.entry(validator_idx).or_default(); - if !observed.has_valid_signature { - totals.missing_signatures += 1; - } - if observed.has_invalid_signature { - totals.invalid_signatures += 1; - } - } - } + let mut validator_indices = meta.validator_indices.clone(); + validator_indices.sort_unstable(); + validator_indices.dedup(); - let offenders = validators + let validators = validator_indices .into_iter() - .filter_map(|validator_idx| { - let totals = totals.get(&validator_idx).copied().unwrap_or_default(); - let penalty_score = totals - .missing_signatures - .saturating_add(totals.invalid_signatures); - (penalty_score > threshold).then_some(ValidatorPenalty { + .map(|validator_idx| { + let max_rows = + observed_rows.len() as u64 - u64::from(observed_rows.contains(&validator_idx)); + let max_points = max_rows + .saturating_mul(session_weight) + .saturating_mul(session_weight); + + let earned_points = observed_rows + .iter() + .copied() + .filter(|observer| *observer != validator_idx) + .map(|observer| { + validator_points + .get(&(observer, validator_idx)) + .copied() + .unwrap_or_default() + }) + .sum::() + .saturating_mul(session_weight); + + SessionValidatorScore { validator_idx, - missing_signatures: totals.missing_signatures, - invalid_signatures: totals.invalid_signatures, - }) + earned_points, + max_points, + is_bad: max_points > 0 && earned_points.saturating_mul(2) < max_points, + } }) .collect::>() .into_boxed_slice(); SessionPenaltyReport { - session_id, - total_blocks_in_session, - offenders, + session_id: meta.session_id, + epoch_start_session_id: meta.epoch_start_session_id, + session_weight: session_weight as u32, + validators, } } -pub fn emit_report_metrics(report: &SessionPenaltyReport) { - let labels = session_labels(report.session_id); - metrics::gauge!("tycho_slasher_session_blocks_total", &labels) - .set(report.total_blocks_in_session as f64); - metrics::gauge!("tycho_slasher_session_penalty_candidates_total", &labels) - .set(report.offenders.len() as f64); - - for offender in &report.offenders { - let validator_idx = format!("{}", offender.validator_idx); - let labels = [ - ( - "catchain_seqno", - format!("{}", report.session_id.catchain_seqno), - ), - ( - "vset_switch_round", - format!("{}", report.session_id.vset_switch_round), - ), - ("validator_idx", validator_idx.clone()), - ]; - metrics::gauge!("tycho_slasher_penalty_candidate", &labels).set(1); - - let labels = [ - ( - "catchain_seqno", - format!("{}", report.session_id.catchain_seqno), - ), - ( - "vset_switch_round", - format!("{}", report.session_id.vset_switch_round), - ), - ("validator_idx", validator_idx), - ]; - metrics::gauge!( - "tycho_slasher_penalty_candidate_missing_signatures", - &labels - ) - .set(offender.missing_signatures as f64); - metrics::gauge!( - "tycho_slasher_penalty_candidate_invalid_signatures", - &labels - ) - .set(offender.invalid_signatures as f64); +pub fn analyze_vset_epoch( + epoch: &VsetEpoch, + session_reports: &[SessionPenaltyReport], + bad_sessions_weight_threshold: u64, +) -> VsetPenaltyReport { + let mut validators = BTreeMap::::new(); + + for report in session_reports { + let session_weight = u64::from(report.session_weight); + + for item in &report.validators { + let penalty = validators + .entry(item.validator_idx) + .or_insert(VsetValidatorPenalty { + validator_idx: item.validator_idx, + bad_sessions_weight: 0, + total_sessions_weight: 0, + is_bad: false, + }); + penalty.total_sessions_weight = + penalty.total_sessions_weight.saturating_add(session_weight); + if item.is_bad { + penalty.bad_sessions_weight = + penalty.bad_sessions_weight.saturating_add(session_weight); + } + } } -} -pub fn clear_report_metrics(report: &SessionPenaltyReport) { - let labels = session_labels(report.session_id); - metrics::gauge!("tycho_slasher_session_blocks_total", &labels).set(0); - metrics::gauge!("tycho_slasher_session_penalty_candidates_total", &labels).set(0); - - for offender in &report.offenders { - let validator_idx = format!("{}", offender.validator_idx); - let labels = [ - ( - "catchain_seqno", - format!("{}", report.session_id.catchain_seqno), - ), - ( - "vset_switch_round", - format!("{}", report.session_id.vset_switch_round), - ), - ("validator_idx", validator_idx.clone()), - ]; - metrics::gauge!("tycho_slasher_penalty_candidate", &labels).set(0); - - let labels = [ - ( - "catchain_seqno", - format!("{}", report.session_id.catchain_seqno), - ), - ( - "vset_switch_round", - format!("{}", report.session_id.vset_switch_round), - ), - ("validator_idx", validator_idx), - ]; - metrics::gauge!( - "tycho_slasher_penalty_candidate_missing_signatures", - &labels - ) - .set(0); - metrics::gauge!( - "tycho_slasher_penalty_candidate_invalid_signatures", - &labels - ) - .set(0); + for item in validators.values_mut() { + item.is_bad = item.bad_sessions_weight > bad_sessions_weight_threshold; } -} -fn session_labels(session_id: ValidationSessionId) -> [(&'static str, String); 2] { - [ - ("catchain_seqno", format!("{}", session_id.catchain_seqno)), - ( - "vset_switch_round", - format!("{}", session_id.vset_switch_round), - ), - ] + VsetPenaltyReport { + epoch_start_session_id: epoch.start_session_id, + vset_hash: epoch.vset_hash, + validators: validators + .into_values() + .collect::>() + .into_boxed_slice(), + } } diff --git a/slasher/src/lib.rs b/slasher/src/lib.rs index fa9dc2372c..e70eb1526a 100644 --- a/slasher/src/lib.rs +++ b/slasher/src/lib.rs @@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize}; use tokio::sync::mpsc; use tokio_util::sync::CancellationToken; use tracing::instrument; +use tycho_block_util::config::BlockchainConfigExt; use tycho_core::block_strider::{StateSubscriber, StateSubscriberContext}; use tycho_core::blockchain_rpc::BlockchainRpcClient; use tycho_crypto::ed25519; @@ -19,7 +20,9 @@ use tycho_util::config::PartialConfig; use tycho_util::futures::JoinTask; use tycho_util::serde_helpers; -pub use self::analyzer::{SessionPenaltyReport, ValidatorPenalty}; +pub use self::analyzer::{ + SessionPenaltyReport, SessionValidatorScore, VsetPenaltyReport, VsetValidatorPenalty, +}; pub use self::bc::{ BlocksBatch, ContractSubscription, EncodeBlocksBatchMessage, MessageDeliveryStatus, SignatureHistory, SignedMessage, SlasherContract, StubSlasherContract, @@ -60,6 +63,11 @@ pub struct SlasherConfig { /// Default: `5s` #[serde(with = "serde_helpers::humantime")] pub prev_delivery_timeout: Option, + + /// Absolute threshold of bad-session weight after which validator is bad in a vset epoch. + /// + /// Default: `1000` + pub bad_sessions_weight_threshold: u64, } impl Default for SlasherConfig { @@ -68,6 +76,7 @@ impl Default for SlasherConfig { message_ttl: Duration::from_secs(30), message_retry_interval: Duration::from_secs(1), prev_delivery_timeout: Some(Duration::from_secs(5)), + bad_sessions_weight_threshold: 1000, } } } @@ -154,10 +163,15 @@ impl Slasher { vset_switch_round, catchain_seqno, }; + let current_vset_hash = *state_extra + .config + .get_current_validator_set_raw()? + .repr_hash(); tracing::trace!( target: tracing_targets::SLASHER, ?slasher_params, - ?current_session_id + ?current_session_id, + current_vset_hash = %current_vset_hash, ); // TODO: Add metrics. @@ -170,6 +184,8 @@ impl Slasher { ); this.known_session_id.set(current_session_id); } + this.storage + .update_current_vset_epoch(current_session_id, current_vset_hash)?; // Handle subscription let subscription = match this.subscription.load_full() { @@ -227,12 +243,17 @@ impl Slasher { ); // TODO: Move into blocking. - if let Some(report) = this.storage.store_blocks_batch( + if !this.storage.store_blocks_batch( submitted.session_id, submitted.validator_idx, &submitted.blocks_batch, )? { - analyzer::clear_report_metrics(&report); + tracing::warn!( + target: tracing_targets::SLASHER, + session_id = ?submitted.session_id, + current_vset_hash = %current_vset_hash, + "ignoring observed blocks batch without known epoch" + ); } tokio::task::yield_now().await; } @@ -247,7 +268,7 @@ impl Slasher { } } - self.shared.analyze_completed_sessions()?; + self.shared.analyze_closed_vset_epochs()?; while let Some(session_info) = self .validator_events_collector @@ -320,23 +341,34 @@ struct SlasherSharedState { } impl SlasherSharedState { - fn analyze_completed_sessions(&self) -> Result<()> { + fn analyze_closed_vset_epochs(&self) -> Result<()> { let snapshot = self.storage.snapshot(); - let Some(latest_session_id) = snapshot.load_latest_session_id()? else { - return Ok(()); - }; - - for session_id in snapshot.load_distinct_session_ids()? { - if session_id >= latest_session_id - || snapshot.load_session_report(session_id)?.is_some() - { + for epoch in snapshot.load_closed_vset_epochs()? { + if snapshot.load_vset_report(epoch.start_session_id)?.is_some() { continue; } - let batches = snapshot.load_batches_for_session(session_id)?; - let report = analyzer::analyze_session(session_id, &batches); - self.storage.store_session_report(&report)?; - analyzer::emit_report_metrics(&report); + let mut session_reports = Vec::new(); + for meta in snapshot.load_sessions_for_epoch(epoch.start_session_id)? { + let report = match snapshot.load_session_report(meta.session_id)? { + Some(report) => report, + None => { + let batches = + snapshot.load_observed_batches_for_session(meta.session_id)?; + let report = analyzer::analyze_session(&meta, &batches); + self.storage.store_session_report(&report)?; + report + } + }; + session_reports.push(report); + } + + let report = analyzer::analyze_vset_epoch( + &epoch, + &session_reports, + self.config.bad_sessions_weight_threshold, + ); + self.storage.store_vset_report(&report)?; } Ok(()) diff --git a/slasher/src/proto.tl b/slasher/src/proto.tl index 0d1edeb926..8aff1ec547 100644 --- a/slasher/src/proto.tl +++ b/slasher/src/proto.tl @@ -21,27 +21,79 @@ slasher.signatureHistory = slasher.SignatureHistory; /** -* @param catchain_seqno validation session catchain seqno -* @param vset_switch_round validation session vset switch round -* @param total_blocks_in_session total committed blocks merged for this session -* @param offenders validators we want to punish on stage 1 +* @param catchain_seqno validation session catchain seqno +* @param vset_switch_round validation session vset switch round +* @param epoch_catchain_seqno catchain seqno of the vset epoch start +* @param epoch_vset_switch_round vset switch round of the vset epoch start +* @param session_weight unique committed blocks observed in the session +* @param validators per-validator weighted scores in the session */ slasher.sessionPenaltyReport catchain_seqno:int vset_switch_round:int - total_blocks_in_session:int - offenders:(vector slasher.validatorPenalty) + epoch_catchain_seqno:int + epoch_vset_switch_round:int + session_weight:int + validators:(vector slasher.sessionValidatorScore) = slasher.SessionPenaltyReport; /** * @param validator_idx validator index relative to the validator set -* @param missing_signatures blocks where no valid signature was observed -* @param invalid_signatures blocks where an invalid signature was observed +* @param earned_points weighted points received from observed validators +* @param max_points weighted maximum possible score from observed validators +* @param is_bad whether validator is bad in this session */ -slasher.validatorPenalty +slasher.sessionValidatorScore validator_idx:int - missing_signatures:int - invalid_signatures:int - = slasher.ValidatorPenalty; + earned_points:long + max_points:long + is_bad:int + = slasher.SessionValidatorScore; + +/** +* @param epoch_catchain_seqno catchain seqno of the vset epoch start +* @param epoch_vset_switch_round vset switch round of the vset epoch start +* @param vset_hash validator set hash +* @param validators per-validator verdict in the epoch +*/ +slasher.vsetPenaltyReport + epoch_catchain_seqno:int + epoch_vset_switch_round:int + vset_hash:int256 + validators:(vector slasher.vsetValidatorPenalty) + = slasher.VsetPenaltyReport; + +/** +* @param validator_idx validator index relative to the validator set +* @param bad_sessions_weight sum of session weights where validator was bad +* @param total_sessions_weight total observed session weight for validator +* @param is_bad final epoch verdict +*/ +slasher.vsetValidatorPenalty + validator_idx:int + bad_sessions_weight:long + total_sessions_weight:long + is_bad:int + = slasher.VsetValidatorPenalty; + +/** +* @param vset_hash validator set hash +* @param has_next_epoch whether epoch is already closed +* @param next_epoch_catchain_seqno next epoch start catchain seqno +* @param next_epoch_vset_switch_round next epoch start vset switch round +*/ +slasher.vsetEpoch + vset_hash:int256 + has_next_epoch:int + next_epoch_catchain_seqno:int + next_epoch_vset_switch_round:int + = slasher.VsetEpoch; + +/** +* @param validator_indices validator indices participating in the session +*/ +slasher.sessionMeta + validator_indices:(vector int) + = slasher.SessionMeta; bitset length:int data:bytes = BitSet; diff --git a/slasher/src/storage/db.rs b/slasher/src/storage/db.rs index 7ce0640d1d..542d1f313a 100644 --- a/slasher/src/storage/db.rs +++ b/slasher/src/storage/db.rs @@ -27,13 +27,14 @@ impl WithMigrations for SlasherTables { } } -// TODO: Add a table for temp batches. weedb::tables! { pub struct SlasherTables { pub state: tables::State, - pub sessions: tables::Sessions, + pub vset_epochs: tables::VsetEpochs, + pub session_meta: tables::SessionMeta, pub block_batches: tables::BlockBatches, pub session_reports: tables::SessionReports, + pub vset_reports: tables::VsetReports, } } @@ -45,20 +46,14 @@ pub mod tables { use weedb::rocksdb::Options; use weedb::{ColumnFamily, ColumnFamilyOptions}; - /// Stores list of validation sessions - /// - Key: `session_id: (catchain_seqno u32 BE, vset_switch_round u32 BE)` - /// - Value: () - pub struct Sessions; - - impl Sessions { - pub const KEY_LEN: usize = 4 + 4; - } + /// Stores generic node parameters. + pub struct State; - impl ColumnFamily for Sessions { - const NAME: &'static str = "sessions"; + impl ColumnFamily for State { + const NAME: &'static str = "state"; } - impl ColumnFamilyOptions for Sessions { + impl ColumnFamilyOptions for State { fn options(opts: &mut Options, ctx: &mut TableContext) { default_block_based_table_factory(opts, ctx); @@ -67,20 +62,18 @@ pub mod tables { } } - /// Cached analyzer result for a completed validation session. - /// - Key: `session_id: (catchain_seqno u32 BE, vset_switch_round u32 BE)` - /// - Value: `SessionPenaltyReport` - pub struct SessionReports; + /// Stores validator-set epochs keyed by their start validation session. + pub struct VsetEpochs; - impl SessionReports { + impl VsetEpochs { pub const KEY_LEN: usize = 4 + 4; } - impl ColumnFamily for SessionReports { - const NAME: &'static str = "session_reports"; + impl ColumnFamily for VsetEpochs { + const NAME: &'static str = "vset_epochs"; } - impl ColumnFamilyOptions for SessionReports { + impl ColumnFamilyOptions for VsetEpochs { fn options(opts: &mut Options, ctx: &mut TableContext) { default_block_based_table_factory(opts, ctx); @@ -89,16 +82,18 @@ pub mod tables { } } - /// Stores generic node parameters - /// - Key: `...` - /// - Value: `...` - pub struct State; + /// Stores session metadata grouped by epoch. + pub struct SessionMeta; - impl ColumnFamily for State { - const NAME: &'static str = "state"; + impl SessionMeta { + pub const KEY_LEN: usize = VsetEpochs::KEY_LEN + 4 + 4; } - impl ColumnFamilyOptions for State { + impl ColumnFamily for SessionMeta { + const NAME: &'static str = "session_meta"; + } + + impl ColumnFamilyOptions for SessionMeta { fn options(opts: &mut Options, ctx: &mut TableContext) { default_block_based_table_factory(opts, ctx); @@ -125,4 +120,40 @@ pub mod tables { zstd_block_based_table_factory(opts, ctx); } } + + /// Cached analyzer result for a single validation session. + pub struct SessionReports; + + impl SessionReports { + pub const KEY_LEN: usize = 4 + 4; + } + + impl ColumnFamily for SessionReports { + const NAME: &'static str = "session_reports"; + } + + impl ColumnFamilyOptions for SessionReports { + fn options(opts: &mut Options, ctx: &mut TableContext) { + default_block_based_table_factory(opts, ctx); + + opts.set_optimize_filters_for_hits(true); + optimize_for_point_lookup(opts, ctx); + } + } + + /// Final analyzer result for a closed validator-set epoch. + pub struct VsetReports; + + impl ColumnFamily for VsetReports { + const NAME: &'static str = "vset_reports"; + } + + impl ColumnFamilyOptions for VsetReports { + fn options(opts: &mut Options, ctx: &mut TableContext) { + default_block_based_table_factory(opts, ctx); + + opts.set_optimize_filters_for_hits(true); + optimize_for_point_lookup(opts, ctx); + } + } } diff --git a/slasher/src/storage/mod.rs b/slasher/src/storage/mod.rs index 3192914a9b..981952f9ed 100644 --- a/slasher/src/storage/mod.rs +++ b/slasher/src/storage/mod.rs @@ -3,11 +3,18 @@ use std::sync::Arc; use anyhow::{Context, Result}; use tycho_slasher_traits::ValidationSessionId; use tycho_storage::StorageContext; +use tycho_types::cell::HashBytes; use weedb::OwnedSnapshot; use self::db::{SlasherDb, tables}; -use self::models::{StoredBlocksBatch, StoredSessionPenaltyReport}; -use crate::{BlocksBatch, SessionPenaltyReport}; +use self::models::{ + StoredBlocksBatch, StoredSessionMeta, StoredSessionPenaltyReport, StoredVsetEpoch, + StoredVsetPenaltyReport, +}; +use crate::BlocksBatch; +use crate::analyzer::{ + ObservedBlocksBatch, SessionMeta, SessionPenaltyReport, VsetEpoch, VsetPenaltyReport, +}; pub mod db; pub mod models; @@ -29,7 +36,6 @@ impl SlasherStorage { }) } - /// Creates a new snapshot. pub fn snapshot(&self) -> SlasherStorageSnapshot { SlasherStorageSnapshot { db: self.inner.db.clone(), @@ -37,18 +43,70 @@ impl SlasherStorage { } } + pub fn update_current_vset_epoch( + &self, + current_session_id: ValidationSessionId, + current_vset_hash: HashBytes, + ) -> Result<()> { + let latest = self.load_latest_vset_epoch()?; + + match latest { + // just same vset. do nothing + Some(epoch) if epoch.vset_hash == current_vset_hash => Ok(()), + // we have new session. old persists for analyze + Some(mut epoch) => { + if epoch.next_epoch_start_session_id.is_none() { + epoch.next_epoch_start_session_id = Some(current_session_id); + self.store_vset_epoch(&epoch)?; + } + + self.store_vset_epoch(&VsetEpoch { + start_session_id: current_session_id, + vset_hash: current_vset_hash, + next_epoch_start_session_id: None, + }) + } + None => self.store_vset_epoch(&VsetEpoch { + start_session_id: current_session_id, + vset_hash: current_vset_hash, + next_epoch_start_session_id: None, + }), + } + } + pub fn store_blocks_batch( &self, session_id: ValidationSessionId, - validator_idx: u16, + observer_validator_idx: u16, batch: &BlocksBatch, - ) -> Result> { - let key = block_batches_key(session_id, validator_idx, batch.start_seqno); + ) -> Result { + let Some(epoch) = self.find_epoch_for_session(session_id)? else { + return Ok(false); + }; + let key = block_batches_key(session_id, observer_validator_idx, batch.start_seqno); let value = tl_proto::serialize(StoredBlocksBatch::wrap(batch)); - self.inner.db.block_batches.insert(key.as_slice(), value)?; - self.take_session_report(session_id) + + let mut validator_indices = batch + .signatures_history + .iter() + .map(|item| item.validator_idx) + .collect::>(); + + validator_indices.sort(); + validator_indices.dedup(); + + // TODO: just upsert for now, maybe we can load and then save if absent + self.store_session_meta(&SessionMeta { + session_id, + epoch_start_session_id: epoch.start_session_id, + validator_indices, + })?; + + self.clear_intermediate_data(&epoch, &session_id)?; + + Ok(true) } pub fn store_session_report(&self, report: &SessionPenaltyReport) -> Result<()> { @@ -61,41 +119,108 @@ impl SlasherStorage { Ok(()) } - pub fn load_session_report( + pub fn store_vset_report(&self, report: &VsetPenaltyReport) -> Result<()> { + let key = session_key(report.epoch_start_session_id); + let value = tl_proto::serialize(StoredVsetPenaltyReport::wrap(report)); + self.inner.db.vset_reports.insert(key.as_slice(), value)?; + Ok(()) + } + + fn store_vset_epoch(&self, epoch: &VsetEpoch) -> Result<()> { + let key = session_key(epoch.start_session_id); + let value = tl_proto::serialize(StoredVsetEpoch::wrap(epoch)); + self.inner.db.vset_epochs.insert(key.as_slice(), value)?; + Ok(()) + } + + fn store_session_meta(&self, meta: &SessionMeta) -> Result<()> { + let key = session_meta_key(meta.epoch_start_session_id, meta.session_id); + let value = tl_proto::serialize(StoredSessionMeta::wrap(meta)); + self.inner.db.session_meta.insert(key.as_slice(), value)?; + Ok(()) + } + + // todo: should we clean after each batch or just after session + fn clear_intermediate_data( &self, - session_id: ValidationSessionId, - ) -> Result> { - let table = &self.inner.db.session_reports; - let key = session_key(session_id); - let Some(value) = self + epoch: &VsetEpoch, + session_id: &ValidationSessionId, + ) -> Result<()> { + self.delete_session_report(*session_id)?; + self.delete_vset_report(epoch.start_session_id)?; + Ok(()) + } + + fn delete_session_report(&self, session_id: ValidationSessionId) -> Result<()> { + self.delete_by_key( + &self.inner.db.session_reports.cf(), + session_key(session_id).as_slice(), + self.inner.db.session_reports.write_config(), + ) + } + + fn delete_vset_report(&self, epoch_start_session_id: ValidationSessionId) -> Result<()> { + self.delete_by_key( + &self.inner.db.vset_reports.cf(), + session_key(epoch_start_session_id).as_slice(), + self.inner.db.vset_reports.write_config(), + ) + } + + fn delete_by_key( + &self, + cf: &impl weedb::rocksdb::AsColumnFamilyRef, + key: &[u8], + write_config: &weedb::rocksdb::WriteOptions, + ) -> Result<()> { + self.inner + .db + .rocksdb() + .delete_cf_opt(cf, key, write_config)?; + Ok(()) + } + + fn load_latest_vset_epoch(&self) -> Result> { + let table = &self.inner.db.vset_epochs; + let read_config = table.new_read_config(); + let cf = table.cf(); + let mut iter = self .inner .db .rocksdb() - .get_cf(&table.cf(), key.as_slice())? - else { - return Ok(None); - }; + .raw_iterator_cf_opt(&cf, read_config); + iter.seek_to_last(); - let report = tl_proto::deserialize::(&value) - .context("failed to deserialize slasher session report")? - .0; - Ok(Some(report)) + let epoch = match iter.item() { + Some((key, value)) => Some(parse_vset_epoch(key, value)?), + None => { + iter.status()?; + None + } + }; + Ok(epoch) } - fn take_session_report( - &self, - session_id: ValidationSessionId, - ) -> Result> { - let report = self.load_session_report(session_id)?; - if report.is_some() { - let key = session_key(session_id); - self.inner.db.rocksdb().delete_cf_opt( - &self.inner.db.session_reports.cf(), - key.as_slice(), - self.inner.db.session_reports.write_config(), - )?; - } - Ok(report) + fn find_epoch_for_session(&self, session_id: ValidationSessionId) -> Result> { + let table = &self.inner.db.vset_epochs; + let read_config = table.new_read_config(); + let cf = table.cf(); + let mut iter = self + .inner + .db + .rocksdb() + .raw_iterator_cf_opt(&cf, read_config); + let key = session_key(session_id); + iter.seek_for_prev(key.as_slice()); + + let epoch = match iter.item() { + Some((key, value)) => Some(parse_vset_epoch(key, value)?), + None => { + iter.status()?; + None + } + }; + Ok(epoch) } } @@ -110,60 +235,61 @@ pub struct SlasherStorageSnapshot { } impl SlasherStorageSnapshot { - pub fn load_latest_session_id(&self) -> Result> { - let table = &self.db.block_batches; + pub fn load_closed_vset_epochs(&self) -> Result> { + let table = &self.db.vset_epochs; let mut read_config = table.new_read_config(); read_config.set_snapshot(self.snapshot.as_ref()); let cf = table.cf(); let mut iter = self.db.rocksdb().raw_iterator_cf_opt(&cf, read_config); - iter.seek_to_last(); + iter.seek_to_first(); - match iter.key() { - Some(key) => Ok(Some(parse_session_id_prefix(key))), - None => { - iter.status()?; - Ok(None) + let mut items = Vec::new(); + while let Some((key, value)) = iter.item() { + let epoch = parse_vset_epoch(key, value)?; + if epoch.next_epoch_start_session_id.is_some() { + items.push(epoch); } + iter.next(); } + iter.status()?; + + Ok(items) } - pub fn load_distinct_session_ids(&self) -> Result> { - let table = &self.db.block_batches; + pub fn load_sessions_for_epoch( + &self, + epoch_start_session_id: ValidationSessionId, + ) -> Result> { + let table = &self.db.session_meta; let mut read_config = table.new_read_config(); read_config.set_snapshot(self.snapshot.as_ref()); + let prefix = session_key(epoch_start_session_id); + read_config.set_iterate_lower_bound(prefix.as_slice()); + let cf = table.cf(); let mut iter = self.db.rocksdb().raw_iterator_cf_opt(&cf, read_config); - iter.seek_to_first(); + iter.seek(prefix.as_slice()); let mut items = Vec::new(); - let mut prev = None; - loop { - let key = match iter.key() { - Some(key) => key, - None => { - iter.status()?; - break; - } - }; - - let session_id = parse_session_id_prefix(key); - if prev != Some(session_id) { - items.push(session_id); - prev = Some(session_id); + while let Some((key, value)) = iter.item() { + if &key[..tables::VsetEpochs::KEY_LEN] != prefix.as_slice() { + break; } + items.push(parse_session_meta(key, value)?); iter.next(); } + iter.status()?; Ok(items) } - pub fn load_batches_for_session( + pub fn load_observed_batches_for_session( &self, session_id: ValidationSessionId, - ) -> Result> { + ) -> Result> { let table = &self.db.block_batches; let mut read_config = table.new_read_config(); read_config.set_snapshot(self.snapshot.as_ref()); @@ -177,14 +303,17 @@ impl SlasherStorageSnapshot { let mut items = Vec::new(); while let Some((key, value)) = iter.item() { - if &key[0..tables::Sessions::KEY_LEN] != prefix.as_slice() { + if &key[..tables::SessionReports::KEY_LEN] != prefix.as_slice() { break; } let batch = tl_proto::deserialize::(value) .context("failed to deserialize slasher blocks batch")? .0; - items.push(batch); + items.push(ObservedBlocksBatch { + observer_validator_idx: parse_observer_validator_idx(key), + batch, + }); iter.next(); } iter.status()?; @@ -214,31 +343,88 @@ impl SlasherStorageSnapshot { .0; Ok(Some(report)) } + + pub fn load_vset_report( + &self, + epoch_start_session_id: ValidationSessionId, + ) -> Result> { + let table = &self.db.vset_reports; + let mut read_config = table.new_read_config(); + read_config.set_snapshot(self.snapshot.as_ref()); + + let key = session_key(epoch_start_session_id); + let Some(value) = + self.db + .rocksdb() + .get_pinned_cf_opt(&table.cf(), key.as_slice(), &read_config)? + else { + return Ok(None); + }; + + let report = tl_proto::deserialize::(value.as_ref()) + .context("failed to deserialize slasher vset report")? + .0; + Ok(Some(report)) + } } fn session_key(session_id: ValidationSessionId) -> [u8; tables::SessionReports::KEY_LEN] { let mut key = [0u8; tables::SessionReports::KEY_LEN]; - key[0..4].copy_from_slice(&session_id.catchain_seqno.to_be_bytes()); + key[..4].copy_from_slice(&session_id.catchain_seqno.to_be_bytes()); key[4..8].copy_from_slice(&session_id.vset_switch_round.to_be_bytes()); key } +fn session_meta_key( + epoch_start_session_id: ValidationSessionId, + session_id: ValidationSessionId, +) -> [u8; tables::SessionMeta::KEY_LEN] { + let mut key = [0u8; tables::SessionMeta::KEY_LEN]; + key[..8].copy_from_slice(&session_key(epoch_start_session_id)); + key[8..12].copy_from_slice(&session_id.catchain_seqno.to_be_bytes()); + key[12..16].copy_from_slice(&session_id.vset_switch_round.to_be_bytes()); + key +} + fn block_batches_key( session_id: ValidationSessionId, - validator_idx: u16, + observer_validator_idx: u16, start_seqno: u32, ) -> [u8; tables::BlockBatches::KEY_LEN] { let mut key = [0u8; tables::BlockBatches::KEY_LEN]; - key[0..4].copy_from_slice(&session_id.catchain_seqno.to_be_bytes()); - key[4..8].copy_from_slice(&session_id.vset_switch_round.to_be_bytes()); - key[8..10].copy_from_slice(&validator_idx.to_be_bytes()); + key[..8].copy_from_slice(&session_key(session_id)); + key[8..10].copy_from_slice(&observer_validator_idx.to_be_bytes()); key[10..14].copy_from_slice(&start_seqno.to_be_bytes()); key } fn parse_session_id_prefix(key: &[u8]) -> ValidationSessionId { ValidationSessionId { - catchain_seqno: u32::from_be_bytes(key[0..4].try_into().unwrap()), + catchain_seqno: u32::from_be_bytes(key[..4].try_into().unwrap()), vset_switch_round: u32::from_be_bytes(key[4..8].try_into().unwrap()), } } + +fn parse_observer_validator_idx(key: &[u8]) -> u16 { + u16::from_be_bytes(key[8..10].try_into().unwrap()) +} + +fn parse_vset_epoch(key: &[u8], value: &[u8]) -> Result { + let mut epoch = tl_proto::deserialize::(value) + .context("failed to deserialize slasher vset epoch")? + .0; + epoch.start_session_id = parse_session_id_prefix(key); + Ok(epoch) +} + +fn parse_session_meta(key: &[u8], value: &[u8]) -> Result { + let mut meta = tl_proto::deserialize::(value) + .context("failed to deserialize slasher session meta")? + .0; + meta.epoch_start_session_id = parse_session_id_prefix(&key[..8]); + meta.session_id = ValidationSessionId { + catchain_seqno: u32::from_be_bytes(key[8..12].try_into().unwrap()), + vset_switch_round: u32::from_be_bytes(key[12..16].try_into().unwrap()), + }; + Ok(meta) +} diff --git a/slasher/src/storage/models.rs b/slasher/src/storage/models.rs index fb9a9a675c..d741aefecf 100644 --- a/slasher/src/storage/models.rs +++ b/slasher/src/storage/models.rs @@ -1,9 +1,14 @@ use tl_proto::{TlError, TlPacket, TlRead, TlResult, TlWrite}; use tycho_slasher_traits::ValidationSessionId; +use tycho_types::cell::HashBytes; use tycho_util::FastHashSet; +use crate::analyzer::{ + SessionMeta, SessionPenaltyReport, SessionValidatorScore, VsetEpoch, VsetPenaltyReport, + VsetValidatorPenalty, +}; use crate::util::BitSet; -use crate::{BlocksBatch, SessionPenaltyReport, SignatureHistory, ValidatorPenalty}; +use crate::{BlocksBatch, SignatureHistory}; #[repr(transparent)] pub struct StoredBlocksBatch(pub BlocksBatch); @@ -24,7 +29,6 @@ impl StoredBlocksBatch { impl TlWrite for StoredBlocksBatch { type Repr = tl_proto::Boxed; - // TODO: Simplify becase all signature histories are equal in size. fn max_size_hint(&self) -> usize { 4 + 4 + self.0.committed_blocks.max_size_hint() @@ -99,6 +103,133 @@ impl<'tl> TlRead<'tl> for StoredBlocksBatch { } } +#[repr(transparent)] +pub struct StoredVsetEpoch(pub VsetEpoch); + +impl StoredVsetEpoch { + pub const TL_ID: u32 = tl_proto::id!("slasher.vsetEpoch", scheme = "proto.tl"); + + #[inline] + pub const fn wrap(inner: &VsetEpoch) -> &Self { + // SAFETY: `StoredVsetEpoch` has the same layout as `VsetEpoch`. + unsafe { &*(inner as *const VsetEpoch).cast::() } + } +} + +impl TlWrite for StoredVsetEpoch { + type Repr = tl_proto::Boxed; + + fn max_size_hint(&self) -> usize { + 4 + 32 + 4 + 4 + 4 + } + + fn write_to(&self, packet: &mut P) { + packet.write_u32(Self::TL_ID); + packet.write_raw_slice(&self.0.vset_hash.0); + packet.write_u32(u32::from(self.0.next_epoch_start_session_id.is_some())); + let next_session_id = self + .0 + .next_epoch_start_session_id + .unwrap_or(ValidationSessionId { + catchain_seqno: 0, + vset_switch_round: 0, + }); + packet.write_u32(next_session_id.catchain_seqno); + packet.write_u32(next_session_id.vset_switch_round); + } +} + +impl<'tl> TlRead<'tl> for StoredVsetEpoch { + type Repr = tl_proto::Boxed; + + fn read_from(packet: &mut &'tl [u8]) -> TlResult { + if u32::read_from(packet)? != Self::TL_ID { + return Err(TlError::UnknownConstructor); + } + + let vset_hash = read_hash_bytes(packet)?; + let has_next_epoch = u32::read_from(packet)? != 0; + let next_session_id = ValidationSessionId { + catchain_seqno: u32::read_from(packet)?, + vset_switch_round: u32::read_from(packet)?, + }; + + Ok(Self(VsetEpoch { + start_session_id: ValidationSessionId { + catchain_seqno: 0, + vset_switch_round: 0, + }, + vset_hash, + next_epoch_start_session_id: has_next_epoch.then_some(next_session_id), + })) + } +} + +#[repr(transparent)] +pub struct StoredSessionMeta(pub SessionMeta); + +impl StoredSessionMeta { + pub const TL_ID: u32 = tl_proto::id!("slasher.sessionMeta", scheme = "proto.tl"); + + #[inline] + pub const fn wrap(inner: &SessionMeta) -> &Self { + // SAFETY: `StoredSessionMeta` has the same layout as `SessionMeta`. + unsafe { &*(inner as *const SessionMeta).cast::() } + } +} + +impl TlWrite for StoredSessionMeta { + type Repr = tl_proto::Boxed; + + fn max_size_hint(&self) -> usize { + 4 + 4 + self.0.validator_indices.len() * 4 + } + + fn write_to(&self, packet: &mut P) { + packet.write_u32(Self::TL_ID); + packet.write_u32(self.0.validator_indices.len() as u32); + for validator_idx in &self.0.validator_indices { + packet.write_u32(u32::from(*validator_idx)); + } + } +} + +impl<'tl> TlRead<'tl> for StoredSessionMeta { + type Repr = tl_proto::Boxed; + + fn read_from(packet: &mut &'tl [u8]) -> TlResult { + if u32::read_from(packet)? != Self::TL_ID { + return Err(TlError::UnknownConstructor); + } + + let validator_count = u32::read_from(packet)? as usize; + let mut validator_indices = Vec::with_capacity(validator_count); + let mut unique_indices = + FastHashSet::with_capacity_and_hasher(validator_count, Default::default()); + for _ in 0..validator_count { + let Ok(validator_idx) = u16::try_from(u32::read_from(packet)?) else { + return Err(TlError::InvalidData); + }; + if !unique_indices.insert(validator_idx) { + return Err(TlError::InvalidData); + } + validator_indices.push(validator_idx); + } + + Ok(Self(SessionMeta { + session_id: ValidationSessionId { + catchain_seqno: 0, + vset_switch_round: 0, + }, + epoch_start_session_id: ValidationSessionId { + catchain_seqno: 0, + vset_switch_round: 0, + }, + validator_indices, + })) + } +} + #[repr(transparent)] pub struct StoredSessionPenaltyReport(pub SessionPenaltyReport); @@ -116,19 +247,22 @@ impl TlWrite for StoredSessionPenaltyReport { type Repr = tl_proto::Boxed; fn max_size_hint(&self) -> usize { - 4 + 4 + 4 + 4 + 4 + self.0.offenders.len() * (4 + 4 + 4) + 4 + 4 + 4 + 4 + 4 + 4 + self.0.validators.len() * (4 + 8 + 8 + 4) } fn write_to(&self, packet: &mut P) { packet.write_u32(Self::TL_ID); packet.write_u32(self.0.session_id.catchain_seqno); packet.write_u32(self.0.session_id.vset_switch_round); - packet.write_u32(self.0.total_blocks_in_session); - packet.write_u32(self.0.offenders.len() as u32); - for offender in &self.0.offenders { - packet.write_u32(offender.validator_idx as u32); - packet.write_u32(offender.missing_signatures); - packet.write_u32(offender.invalid_signatures); + packet.write_u32(self.0.epoch_start_session_id.catchain_seqno); + packet.write_u32(self.0.epoch_start_session_id.vset_switch_round); + packet.write_u32(self.0.session_weight); + packet.write_u32(self.0.validators.len() as u32); + for item in &self.0.validators { + packet.write_u32(u32::from(item.validator_idx)); + packet.write_u64(item.earned_points); + packet.write_u64(item.max_points); + packet.write_u32(u32::from(item.is_bad)); } } } @@ -145,13 +279,17 @@ impl<'tl> TlRead<'tl> for StoredSessionPenaltyReport { catchain_seqno: u32::read_from(packet)?, vset_switch_round: u32::read_from(packet)?, }; - let total_blocks_in_session = u32::read_from(packet)?; - let offender_count = u32::read_from(packet)? as usize; + let epoch_start_session_id = ValidationSessionId { + catchain_seqno: u32::read_from(packet)?, + vset_switch_round: u32::read_from(packet)?, + }; + let session_weight = u32::read_from(packet)?; + let validator_count = u32::read_from(packet)? as usize; - let mut offenders = Vec::with_capacity(offender_count); + let mut validators = Vec::with_capacity(validator_count); let mut unique_indices = - FastHashSet::with_capacity_and_hasher(offender_count, Default::default()); - for _ in 0..offender_count { + FastHashSet::with_capacity_and_hasher(validator_count, Default::default()); + for _ in 0..validator_count { let Ok(validator_idx) = u16::try_from(u32::read_from(packet)?) else { return Err(TlError::InvalidData); }; @@ -159,102 +297,108 @@ impl<'tl> TlRead<'tl> for StoredSessionPenaltyReport { return Err(TlError::InvalidData); } - let missing_signatures = u32::read_from(packet)?; - let invalid_signatures = u32::read_from(packet)?; - - offenders.push(ValidatorPenalty { + validators.push(SessionValidatorScore { validator_idx, - missing_signatures, - invalid_signatures, + earned_points: u64::read_from(packet)?, + max_points: u64::read_from(packet)?, + is_bad: u32::read_from(packet)? != 0, }); } Ok(Self(SessionPenaltyReport { session_id, - total_blocks_in_session, - offenders: offenders.into_boxed_slice(), + epoch_start_session_id, + session_weight, + validators: validators.into_boxed_slice(), })) } } -#[cfg(test)] -mod tests { - use std::num::NonZeroU32; - - use tycho_slasher_traits::ReceivedSignature; - - use super::*; - - #[test] - fn blocks_batch_tl_repr() { - let mut batch = BlocksBatch::new(230, NonZeroU32::new(100).unwrap(), &[5, 10, 12, 3]); - - for (seqno, signatures) in [ - (230, [ - ReceivedSignature(ReceivedSignature::VALID_SIGNATURE_BIT), - ReceivedSignature(ReceivedSignature::VALID_SIGNATURE_BIT), - ReceivedSignature(0), - ReceivedSignature(ReceivedSignature::INVALID_SIGNATURE_BIT), - ]), - (250, [ - ReceivedSignature(0), - ReceivedSignature(ReceivedSignature::VALID_SIGNATURE_BIT), - ReceivedSignature(ReceivedSignature::INVALID_SIGNATURE_BIT), - ReceivedSignature(ReceivedSignature::VALID_SIGNATURE_BIT), - ]), - (251, [ - ReceivedSignature(ReceivedSignature::VALID_SIGNATURE_BIT), - ReceivedSignature(ReceivedSignature::VALID_SIGNATURE_BIT), - ReceivedSignature(ReceivedSignature::VALID_SIGNATURE_BIT), - ReceivedSignature(ReceivedSignature::VALID_SIGNATURE_BIT), - ]), - (300, [ - ReceivedSignature(ReceivedSignature::VALID_SIGNATURE_BIT), - ReceivedSignature(ReceivedSignature::VALID_SIGNATURE_BIT), - ReceivedSignature(ReceivedSignature::VALID_SIGNATURE_BIT), - ReceivedSignature(ReceivedSignature::VALID_SIGNATURE_BIT), - ]), - (329, [ - ReceivedSignature(ReceivedSignature::VALID_SIGNATURE_BIT), - ReceivedSignature(ReceivedSignature::VALID_SIGNATURE_BIT), - ReceivedSignature(ReceivedSignature::VALID_SIGNATURE_BIT), - ReceivedSignature(ReceivedSignature::VALID_SIGNATURE_BIT), - ]), - ] { - let committed = batch.commit_signatures(seqno, &signatures); - assert!(committed); - } +#[repr(transparent)] +pub struct StoredVsetPenaltyReport(pub VsetPenaltyReport); + +impl StoredVsetPenaltyReport { + pub const TL_ID: u32 = tl_proto::id!("slasher.vsetPenaltyReport", scheme = "proto.tl"); - let stored = tl_proto::serialize(StoredBlocksBatch::wrap(&batch)); - let loaded = tl_proto::deserialize::(&stored).unwrap(); - assert_eq!(batch, loaded.0); + #[inline] + pub const fn wrap(inner: &VsetPenaltyReport) -> &Self { + // SAFETY: `StoredVsetPenaltyReport` has the same layout as `VsetPenaltyReport`. + unsafe { &*(inner as *const VsetPenaltyReport).cast::() } } +} - #[test] - fn session_penalty_report_tl_repr() { - let report = SessionPenaltyReport { - session_id: ValidationSessionId { - catchain_seqno: 5, - vset_switch_round: 8, - }, - total_blocks_in_session: 10, - offenders: vec![ - ValidatorPenalty { - validator_idx: 1, - missing_signatures: 6, - invalid_signatures: 0, - }, - ValidatorPenalty { - validator_idx: 4, - missing_signatures: 7, - invalid_signatures: 7, - }, - ] - .into_boxed_slice(), +impl TlWrite for StoredVsetPenaltyReport { + type Repr = tl_proto::Boxed; + + fn max_size_hint(&self) -> usize { + 4 + 4 + 4 + 32 + 4 + self.0.validators.len() * (4 + 8 + 8 + 4) + } + + fn write_to(&self, packet: &mut P) { + packet.write_u32(Self::TL_ID); + packet.write_u32(self.0.epoch_start_session_id.catchain_seqno); + packet.write_u32(self.0.epoch_start_session_id.vset_switch_round); + packet.write_raw_slice(&self.0.vset_hash.0); + packet.write_u32(self.0.validators.len() as u32); + for item in &self.0.validators { + packet.write_u32(u32::from(item.validator_idx)); + packet.write_u64(item.bad_sessions_weight); + packet.write_u64(item.total_sessions_weight); + packet.write_u32(u32::from(item.is_bad)); + } + } +} + +impl<'tl> TlRead<'tl> for StoredVsetPenaltyReport { + type Repr = tl_proto::Boxed; + + fn read_from(packet: &mut &'tl [u8]) -> TlResult { + if u32::read_from(packet)? != Self::TL_ID { + return Err(TlError::UnknownConstructor); + } + + let epoch_start_session_id = ValidationSessionId { + catchain_seqno: u32::read_from(packet)?, + vset_switch_round: u32::read_from(packet)?, }; + let vset_hash = read_hash_bytes(packet)?; + let validator_count = u32::read_from(packet)? as usize; + + let mut validators = Vec::with_capacity(validator_count); + let mut unique_indices = + FastHashSet::with_capacity_and_hasher(validator_count, Default::default()); + for _ in 0..validator_count { + let Ok(validator_idx) = u16::try_from(u32::read_from(packet)?) else { + return Err(TlError::InvalidData); + }; + if !unique_indices.insert(validator_idx) { + return Err(TlError::InvalidData); + } - let stored = tl_proto::serialize(StoredSessionPenaltyReport::wrap(&report)); - let loaded = tl_proto::deserialize::(&stored).unwrap(); - assert_eq!(report, loaded.0); + validators.push(VsetValidatorPenalty { + validator_idx, + bad_sessions_weight: u64::read_from(packet)?, + total_sessions_weight: u64::read_from(packet)?, + is_bad: u32::read_from(packet)? != 0, + }); + } + + Ok(Self(VsetPenaltyReport { + epoch_start_session_id, + vset_hash, + validators: validators.into_boxed_slice(), + })) } } + +fn read_hash_bytes(packet: &mut &[u8]) -> TlResult { + if packet.len() < size_of::() { + return Err(TlError::UnexpectedEof); + } + + let (hash, tail) = packet.split_at(size_of::()); + *packet = tail; + let mut bytes = [0; size_of::()]; + bytes.copy_from_slice(hash); + Ok(HashBytes(bytes)) +} From a2f882a0321f1aad85a5ea4492f83c95f5b73352 Mon Sep 17 00:00:00 2001 From: MrWad3r Date: Fri, 27 Mar 2026 16:31:39 +0100 Subject: [PATCH 16/21] chore(slasher): fix signature condition --- Cargo.lock | 9 ++-- Cargo.toml | 2 +- cli/src/cmd/tools/gen_zerostate.rs | 74 ++---------------------------- slasher/src/analyzer.rs | 24 +++++----- slasher/src/lib.rs | 71 ++++++++++++++++++++++++++-- slasher/src/storage/mod.rs | 1 + 6 files changed, 88 insertions(+), 93 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 19366eef2d..76f52657a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4498,8 +4498,7 @@ dependencies = [ [[package]] name = "tycho-types" version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85ebf3e9cb2b0e515adc25e4b30f46bd70cbd4d67edfaca7e3440c0ab7405086" +source = "git+https://github.com/broxus/tycho-types.git?rev=aeea4e8d007e8a64439d3d923a3752fc823b2256#aeea4e8d007e8a64439d3d923a3752fc823b2256" dependencies = [ "ahash", "anyhow", @@ -4532,8 +4531,7 @@ dependencies = [ [[package]] name = "tycho-types-abi-proc" version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c813c08a03554252747f9e5e88485d9af4c30077394a1c3bb6d774ddca56b07" +source = "git+https://github.com/broxus/tycho-types.git?rev=aeea4e8d007e8a64439d3d923a3752fc823b2256#aeea4e8d007e8a64439d3d923a3752fc823b2256" dependencies = [ "anyhow", "proc-macro2", @@ -4544,8 +4542,7 @@ dependencies = [ [[package]] name = "tycho-types-proc" version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad05cf4ab89631f8c11d85c3aa80f781502440f75361d251f866e0d76ae9d31" +source = "git+https://github.com/broxus/tycho-types.git?rev=aeea4e8d007e8a64439d3d923a3752fc823b2256#aeea4e8d007e8a64439d3d923a3752fc823b2256" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 31059b89d7..db7fa0d5dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -178,7 +178,7 @@ tycho-wu-tuner = { path = "./wu-tuner", version = "0.3.9" } [patch.crates-io] # patches here -tycho-types = { git = "https://github.com/broxus/tycho-types.git", rev = "ce1f6fb7e755f7de1d9df612b2417e9155be9e7e" } +tycho-types = { git = "https://github.com/broxus/tycho-types.git", rev = "aeea4e8d007e8a64439d3d923a3752fc823b2256" } [workspace.lints.rust] future_incompatible = "warn" diff --git a/cli/src/cmd/tools/gen_zerostate.rs b/cli/src/cmd/tools/gen_zerostate.rs index eed3d215c0..906e0b7e26 100644 --- a/cli/src/cmd/tools/gen_zerostate.rs +++ b/cli/src/cmd/tools/gen_zerostate.rs @@ -1,5 +1,4 @@ use std::collections::hash_map; -use std::num::NonZeroU8; use std::path::PathBuf; use std::sync::OnceLock; @@ -126,10 +125,6 @@ struct ZerostateConfig { #[serde(default, with = "Boc", skip_serializing_if = "Option::is_none")] elector_code: Option, - slasher_balance: Tokens, - #[serde(default, with = "Boc", skip_serializing_if = "Option::is_none")] - slasher_code: Option, - #[serde(with = "serde_account_states")] accounts: FastHashMap, @@ -215,6 +210,7 @@ impl ZerostateConfig { if let Some(minter_address) = minter_address { fundamental_addresses.set(minter_address, ())?; } + if let Some(slasher_params) = self.params.get::()? { fundamental_addresses.set(slasher_params.address, ())?; } @@ -319,25 +315,6 @@ impl ZerostateConfig { ); } - // Slasher - if let Some(slasher_params) = self.params.get::()? { - let prev = self.accounts.insert( - slasher_params.address, - build_slasher_account( - &slasher_params.address, - self.slasher_balance, - self.slasher_code.clone(), - )? - .into(), - ); - if prev.is_some() { - anyhow::bail!( - "full slasher account state cannot be specified manually, \ - use \"slasher_code\" param instead" - ); - } - } - // Minter match (&self.minter_public_key, self.params.get::()?) { (Some(public_key), Some(minter_address)) => { @@ -527,12 +504,10 @@ impl Default for ZerostateConfig { global_id: 0, config_public_key: *zero_public_key(), minter_public_key: None, - config_balance: default_special_account_balance(), + config_balance: Tokens::new(500_000_000_000), config_code: None, - elector_balance: default_special_account_balance(), + elector_balance: Tokens::new(500_000_000_000), elector_code: None, - slasher_balance: default_special_account_balance(), - slasher_code: None, accounts: Default::default(), validators: Default::default(), params: make_default_params().unwrap(), @@ -837,13 +812,6 @@ fn make_default_params() -> Result { // Param 31 params.set_fundamental_addresses(&[HashBytes([0x00; 32]), HashBytes([0x33; 32])])?; - - // Param 666 - params.set::(&SlasherParamsConfig { - address: HashBytes([0x66; 32]), - batch_size: NonZeroU8::new(100).unwrap(), - })?; - // Param 43 params.set_size_limits(&SizeLimitsConfig { max_msg_bits: 1 << 21, @@ -979,38 +947,6 @@ fn build_elector_account( Ok(account) } -fn build_slasher_account( - address: &HashBytes, - balance: Tokens, - custom_code: Option, -) -> Result { - const SLASHER_CODE: &[u8] = include_bytes!("../../../res/slasher_code.boc"); - - let code = custom_code.unwrap_or_else(|| Boc::decode(SLASHER_CODE).unwrap()); - - let mut data = CellBuilder::new(); - data.store_u64(0)?; - let data = data.build()?; - - let mut account = Account { - address: StdAddr::new(-1, *address).into(), - storage_stat: Default::default(), - last_trans_lt: 0, - balance: balance.into(), - state: AccountState::Active(StateInit { - split_depth: None, - special: None, - code: Some(code), - data: Some(data), - libraries: Dict::new(), - }), - }; - - account.storage_stat.used = compute_storage_used(&account)?; - - Ok(account) -} - fn build_minter_account(pubkey: &ed25519::PublicKey, address: &HashBytes) -> Result { const MINTER_STATE: &[u8] = include_bytes!("../../../res/minter_state.boc"); @@ -1043,10 +979,6 @@ fn zero_public_key() -> &'static ed25519::PublicKey { KEY.get_or_init(|| ed25519::PublicKey::from_bytes([0; 32]).unwrap()) } -fn default_special_account_balance() -> Tokens { - Tokens::new(500_000_000_000) // 500 -} - mod serde_account_states { use serde::de::Deserializer; use serde::ser::{SerializeMap, Serializer}; diff --git a/slasher/src/analyzer.rs b/slasher/src/analyzer.rs index 427f613e41..22ec105d2f 100644 --- a/slasher/src/analyzer.rs +++ b/slasher/src/analyzer.rs @@ -61,12 +61,12 @@ pub fn analyze_session( batches: &[ObservedBlocksBatch], ) -> SessionPenaltyReport { let mut committed_blocks = BTreeSet::new(); - let mut observed_rows = BTreeSet::new(); + let mut observed_validators = BTreeSet::new(); // observer -> observed : points * session weight let mut validator_points = BTreeMap::<(u16, u16), u64>::new(); for item in batches { - observed_rows.insert(item.observer_validator_idx); + observed_validators.insert(item.observer_validator_idx); for offset in 0..item.batch.committed_blocks.len() { if !item.batch.committed_blocks.get(offset) { @@ -80,7 +80,7 @@ pub fn analyze_session( let has_invalid_signature = history.bits.get(bit_offset); let has_valid_signature = history.bits.get(bit_offset + 1); - if !(has_invalid_signature && has_valid_signature) { + if has_invalid_signature && has_valid_signature { tracing::warn!( "slasher analyzer invariant violated: observer {} saw validator {} as both valid and invalid in session {:?}", item.observer_validator_idx, @@ -102,19 +102,18 @@ pub fn analyze_session( let session_weight = committed_blocks.len() as u64; let mut validator_indices = meta.validator_indices.clone(); - validator_indices.sort_unstable(); + validator_indices.sort(); validator_indices.dedup(); let validators = validator_indices .into_iter() .map(|validator_idx| { - let max_rows = - observed_rows.len() as u64 - u64::from(observed_rows.contains(&validator_idx)); - let max_points = max_rows - .saturating_mul(session_weight) - .saturating_mul(session_weight); + let max_rows = observed_validators.len() as u64 + - u64::from(observed_validators.contains(&validator_idx)); + let max_session_points = max_rows.saturating_mul(session_weight); + //.saturating_mul(session_weight); - let earned_points = observed_rows + let earned_points = observed_validators .iter() .copied() .filter(|observer| *observer != validator_idx) @@ -130,8 +129,9 @@ pub fn analyze_session( SessionValidatorScore { validator_idx, earned_points, - max_points, - is_bad: max_points > 0 && earned_points.saturating_mul(2) < max_points, + max_points: max_session_points, + is_bad: max_session_points > 0 + && earned_points.saturating_mul(2) < max_session_points, } }) .collect::>() diff --git a/slasher/src/lib.rs b/slasher/src/lib.rs index e70eb1526a..f8a6d69e9b 100644 --- a/slasher/src/lib.rs +++ b/slasher/src/lib.rs @@ -66,7 +66,7 @@ pub struct SlasherConfig { /// Absolute threshold of bad-session weight after which validator is bad in a vset epoch. /// - /// Default: `1000` + /// Default: `100` pub bad_sessions_weight_threshold: u64, } @@ -76,7 +76,7 @@ impl Default for SlasherConfig { message_ttl: Duration::from_secs(30), message_retry_interval: Duration::from_secs(1), prev_delivery_timeout: Some(Duration::from_secs(5)), - bad_sessions_weight_threshold: 1000, + bad_sessions_weight_threshold: 100, } } } @@ -167,6 +167,7 @@ impl Slasher { .config .get_current_validator_set_raw()? .repr_hash(); + tracing::trace!( target: tracing_targets::SLASHER, ?slasher_params, @@ -343,11 +344,27 @@ struct SlasherSharedState { impl SlasherSharedState { fn analyze_closed_vset_epochs(&self) -> Result<()> { let snapshot = self.storage.snapshot(); - for epoch in snapshot.load_closed_vset_epochs()? { + let closed_vset_epoches = snapshot.load_closed_vset_epochs()?; + if closed_vset_epoches.is_empty() { + tracing::warn!( + target: tracing_targets::SLASHER, + "closes vset epoches not found" + ); + return Ok(()); + } + + for epoch in closed_vset_epoches { if snapshot.load_vset_report(epoch.start_session_id)?.is_some() { continue; } + tracing::info!( + target: tracing_targets::SLASHER, + vset_hash = ?epoch.vset_hash, + start_id = ?epoch.start_session_id, + "analyzing closed vset epoch" + ); + let mut session_reports = Vec::new(); for meta in snapshot.load_sessions_for_epoch(epoch.start_session_id)? { let report = match snapshot.load_session_report(meta.session_id)? { @@ -360,6 +377,7 @@ impl SlasherSharedState { report } }; + Self::log_session_report(&report); session_reports.push(report); } @@ -369,11 +387,58 @@ impl SlasherSharedState { self.config.bad_sessions_weight_threshold, ); self.storage.store_vset_report(&report)?; + Self::log_vset_report(&report); } Ok(()) } + fn log_session_report(report: &SessionPenaltyReport) { + for item in &report.validators { + tracing::info!( + target: tracing_targets::SLASHER, + session_id = ?report.session_id, + epoch_start_session_id = ?report.epoch_start_session_id, + validator_idx = item.validator_idx, + earned_points = item.earned_points, + max_points = item.max_points, + session_weight = report.session_weight, + is_bad = item.is_bad, + "scored validator in validation session", + ); + } + } + + fn log_vset_report(report: &VsetPenaltyReport) { + let bad_validator_indices = report + .validators + .iter() + .filter(|item| item.is_bad) + .map(|item| item.validator_idx) + .collect::>(); + + tracing::info!( + target: tracing_targets::SLASHER, + epoch_start_session_id = ?report.epoch_start_session_id, + vset_hash = %report.vset_hash, + bad_validator_indices = ?bad_validator_indices, + "finished scoring closed vset epoch", + ); + + for item in &report.validators { + tracing::info!( + target: tracing_targets::SLASHER, + epoch_start_session_id = ?report.epoch_start_session_id, + vset_hash = %report.vset_hash, + validator_idx = item.validator_idx, + bad_sessions_weight = item.bad_sessions_weight, + total_sessions_weight = item.total_sessions_weight, + is_bad = item.is_bad, + "computed final validator verdict in vset epoch", + ); + } + } + #[instrument(skip_all, fields(session_id = ?info.session_id))] async fn send_batches_to_contract( self: Arc, diff --git a/slasher/src/storage/mod.rs b/slasher/src/storage/mod.rs index 981952f9ed..3c0c10167e 100644 --- a/slasher/src/storage/mod.rs +++ b/slasher/src/storage/mod.rs @@ -66,6 +66,7 @@ impl SlasherStorage { next_epoch_start_session_id: None, }) } + // Cold start. Save first vset epoch None => self.store_vset_epoch(&VsetEpoch { start_session_id: current_session_id, vset_hash: current_vset_hash, From 587cd70dd026e3cddb61c9489f24ddad38c71a08 Mon Sep 17 00:00:00 2001 From: Kirill Mikheev Date: Wed, 8 Apr 2026 23:08:06 +0300 Subject: [PATCH 17/21] fix(collator): shuffle with vset_switch_round --- block-util/src/block/block_proof_stuff.rs | 15 ++++++++------- collator/src/manager/mod.rs | 5 ++--- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/block-util/src/block/block_proof_stuff.rs b/block-util/src/block/block_proof_stuff.rs index bd0cd4df4c..4f407637f0 100644 --- a/block-util/src/block/block_proof_stuff.rs +++ b/block-util/src/block/block_proof_stuff.rs @@ -388,15 +388,16 @@ impl BlockProofStuff { validator_set: &ValidatorSet, shuffle_validators: bool, ) -> Result { - let cc_seqno = self + let Some(vset_switch_round) = self .inner .proof .signatures .as_ref() - .map(|s| s.validator_info.catchain_seqno) - .unwrap_or_default(); - - ValidatorSubsetInfo::compute_standard(validator_set, cc_seqno, shuffle_validators) + .map(|s| s.consensus_info.vset_switch_round) + else { + anyhow::bail!("no `consensus_info` to compute subset from"); + }; + ValidatorSubsetInfo::compute_standard(validator_set, vset_switch_round, shuffle_validators) } } @@ -527,11 +528,11 @@ pub struct ValidatorSubsetInfo { impl ValidatorSubsetInfo { pub fn compute_standard( validator_set: &ValidatorSet, - cc_seqno: u32, + vset_switch_round: u32, shuffle_validators: bool, ) -> Result { let Some((validators, short_hash)) = - validator_set.compute_mc_subset_indexed(cc_seqno, shuffle_validators) + validator_set.compute_mc_subset_indexed(vset_switch_round, shuffle_validators) else { anyhow::bail!("failed to compute a validator subset"); }; diff --git a/collator/src/manager/mod.rs b/collator/src/manager/mod.rs index 73795a99eb..c91c6d968e 100644 --- a/collator/src/manager/mod.rs +++ b/collator/src/manager/mod.rs @@ -2352,11 +2352,11 @@ where } hash_map::Entry::Vacant(entry) => { let (subset, hash_short) = full_validators_set - .compute_mc_subset_indexed(catchain_seqno, collation_config.shuffle_mc_validators) + .compute_mc_subset_indexed(vset_switch_round, collation_config.shuffle_mc_validators) .ok_or_else(|| anyhow!( "Error calculating subset of validators for catchain session (shard_id = {}, seqno = {})", ShardIdent::MASTERCHAIN, - catchain_seqno, + vset_switch_round, ))?; let subset: FastHashMap<_, _> = subset @@ -2390,7 +2390,6 @@ where tracing::debug!( target: tracing_targets::COLLATION_MANAGER, public_key = %self.keypair.public_key, - current_session_seqno, hash_short, "Current node was not authorized to collate shard {}. Use TRACE to see subset", shard_id, From a79916caf0467e4e9e420e5a7895521c43e07ff1 Mon Sep 17 00:00:00 2001 From: Kirill Mikheev Date: Wed, 1 Apr 2026 08:46:33 +0300 Subject: [PATCH 18/21] refactor(collator): catchain seqno placement --- collator/src/collator/do_collate/finalize.rs | 118 +++++++++---------- 1 file changed, 54 insertions(+), 64 deletions(-) diff --git a/collator/src/collator/do_collate/finalize.rs b/collator/src/collator/do_collate/finalize.rs index 555ddd83eb..e72c6d381e 100644 --- a/collator/src/collator/do_collate/finalize.rs +++ b/collator/src/collator/do_collate/finalize.rs @@ -886,9 +886,8 @@ impl Phase { validator_info = session_update.apply( &mut consensus_info, - prev_state_extra.validator_info.catchain_seqno, next_session_start_round, - session_start.is_curr_switch_after_pause, + &session_start, )?; } let validator_info = validator_info.unwrap_or(ValidatorInfo { @@ -1484,10 +1483,11 @@ mod vset_update_start { is_consensus_info_overridden: bool, pub is_consensus_config_changed: bool, - pub is_curr_switch_after_pause: bool, + is_curr_switch_after_pause: bool, gen_chain_time_millis: u64, after_pause_round: u32, + catchain_seqno: u32, } impl KbNextSessionStart { @@ -1504,6 +1504,10 @@ mod vset_update_start { let after_pause_round = Self::after_pause_round(prev_processed_to_anchor, &prev_consensus_config); + let catchain_seqno = (prev_state_extra.validator_info.catchain_seqno) + .checked_add(1) + .context("catchain seqno overflow")?; + Ok(Self { is_consensus_info_overridden: { consensus_info != &prev_state_extra.consensus_info @@ -1515,6 +1519,7 @@ mod vset_update_start { gen_chain_time_millis: collation_data.get_gen_chain_time(), after_pause_round, + catchain_seqno, prev_consensus_config, }) @@ -1603,9 +1608,8 @@ mod vset_update_start { pub fn apply( &self, consensus_info: &mut ConsensusInfo, - prev_catchain_seqno: u32, next_session_start_round: u32, - is_curr_switch_after_pause: bool, + session_start: &KbNextSessionStart, ) -> Result> { let is_vset_same = *self.current_vset.repr_hash() == self.prev_vset_hash; let is_shuffle_same = self.shuffle_mc_validators == self.prev_shuffle_mc_validators; @@ -1613,10 +1617,6 @@ mod vset_update_start { return Ok(None); } - let catchain_seqno = prev_catchain_seqno - .checked_add(1) - .context("catchain seqno overflow")?; - // simultaneously update session_seqno in collation and consensus if v_(sub)_set changes; // genesis change (recovery or config) should not rotate validators by itself, so it // doesn't allow to apply scheduled v_set immediately despite it splits dag history @@ -1624,7 +1624,7 @@ mod vset_update_start { // take prev_* attributes for mempool to calculate a subset from v_set (if used); // also mempool may skip a short-lived session that ended sooner than schedule // was applied in mempool (but subset rotations should not be that short) - if !is_curr_switch_after_pause { + if !session_start.is_curr_switch_after_pause { consensus_info.prev_shuffle_mc_validators = self.prev_shuffle_mc_validators; consensus_info.prev_vset_switch_round = consensus_info.vset_switch_round; } @@ -1639,7 +1639,7 @@ mod vset_update_start { else { anyhow::bail!( "Error calculating subset of validators for next session \ - (shard_id = {}, catchain_seqno = {catchain_seqno})", + (shard_id = {}, start_round = {next_session_start_round})", ShardIdent::MASTERCHAIN, ); }; @@ -1647,7 +1647,7 @@ mod vset_update_start { Ok(Some(ValidatorInfo { validator_list_hash_short, // TODO: rename field in types - catchain_seqno, + catchain_seqno: session_start.catchain_seqno, nx_cc_updated: true, })) } @@ -1689,18 +1689,26 @@ mod vset_update_start { } } + fn random_session_start() -> KbNextSessionStart { + KbNextSessionStart { + prev_consensus_config: default_test_config().conf.consensus, + catchain_seqno: random(), + is_consensus_info_overridden: random(), + is_consensus_config_changed: random(), + is_curr_switch_after_pause: random(), + gen_chain_time_millis: random(), + after_pause_round: random(), + } + } + #[test] fn genesis_override_overcomes_config_change() { let mut cons_info = random_consensus_info(); let before = cons_info; let start = KbNextSessionStart { - prev_consensus_config: default_test_config().conf.consensus, is_consensus_info_overridden: true, - is_consensus_config_changed: random(), - is_curr_switch_after_pause: random(), - gen_chain_time_millis: random(), - after_pause_round: random(), + ..random_session_start() }; let next_session_start = start.round(&mut cons_info, random()); @@ -1714,12 +1722,10 @@ mod vset_update_start { let mut cons_info = random_consensus_info(); let start = KbNextSessionStart { - prev_consensus_config: default_test_config().conf.consensus, is_consensus_info_overridden: false, // no guard here: may overwrite ANY genesis is_consensus_config_changed: true, - is_curr_switch_after_pause: random(), gen_chain_time_millis: 50_000, - after_pause_round: random(), + ..random_session_start() }; let next_session_start = start.round(&mut cons_info, 600); @@ -1773,6 +1779,7 @@ mod vset_update_start { KbNextSessionStart::after_pause_round(processed_up_to, &cons_conf); let start_1 = KbNextSessionStart { + catchain_seqno: 10, prev_consensus_config: cons_conf.clone(), is_consensus_info_overridden: false, is_consensus_config_changed: false, @@ -1786,59 +1793,45 @@ mod vset_update_start { assert_eq!(next_1, after_pause_round); let validator_info = stub_update - .apply( - &mut cons_info, - 10, - next_1, - start_1.is_curr_switch_after_pause, - ) - .unwrap() - .unwrap(); + .apply(&mut cons_info, next_1, &start_1) + .expect("must be Ok") + .expect("must be Some"); assert_eq!(cons_info.prev_vset_switch_round, 0); assert_eq!(cons_info.vset_switch_round, after_pause_round); - assert_eq!(validator_info.catchain_seqno, 11); + assert_eq!(validator_info.catchain_seqno, start_1.catchain_seqno); // Second vset change while switch is still "applied/too close": push by full history. processed_up_to += 1; after_pause_round = KbNextSessionStart::after_pause_round(processed_up_to, &cons_conf); - let start_2 = { - let mut temp = start_1; - temp.is_curr_switch_after_pause = cons_info.vset_switch_round > after_pause_round; - temp.after_pause_round = after_pause_round; - assert!(!temp.is_curr_switch_after_pause); - temp + let start_2 = KbNextSessionStart { + is_curr_switch_after_pause: cons_info.vset_switch_round > after_pause_round, + after_pause_round, + ..start_1 }; + assert!(!start_2.is_curr_switch_after_pause); let next_2 = start_2.round(&mut cons_info, processed_up_to); assert!(next_2 > processed_up_to); assert_eq!(next_2, (next_1 + cons_conf.max_total_rounds() + 1)); - let validator_info = stub_update - .apply( - &mut cons_info, - validator_info.catchain_seqno, - next_2, - start_2.is_curr_switch_after_pause, - ) - .unwrap() - .unwrap(); + stub_update + .apply(&mut cons_info, next_2, &start_2) + .expect("must be Ok") + .expect("must be Some"); assert_eq!(cons_info.prev_vset_switch_round, next_1); assert_eq!(cons_info.vset_switch_round, next_2); - assert_eq!(validator_info.catchain_seqno, 12); // Third vset change while switch is far in the future: keep the same switch round. processed_up_to += 1; after_pause_round = KbNextSessionStart::after_pause_round(processed_up_to, &cons_conf); - let start_3 = { - let mut temp = start_2; - temp.is_curr_switch_after_pause = cons_info.vset_switch_round > after_pause_round; - temp.after_pause_round = after_pause_round; - assert!(temp.is_curr_switch_after_pause); - temp + let start_3 = KbNextSessionStart { + is_curr_switch_after_pause: cons_info.vset_switch_round > after_pause_round, + after_pause_round, + ..start_2 }; assert!(start_3.is_curr_switch_after_pause); @@ -1846,22 +1839,21 @@ mod vset_update_start { assert!(next_3 > processed_up_to); assert_eq!(next_3, next_2); - let validator_info = stub_update - .apply( - &mut cons_info, - validator_info.catchain_seqno, - next_3, - start_3.is_curr_switch_after_pause, - ) - .unwrap() - .unwrap(); + stub_update + .apply(&mut cons_info, next_3, &start_3) + .expect("must be Ok") + .expect("must be Some"); assert_eq!(cons_info.prev_vset_switch_round, next_1); assert_eq!(cons_info.vset_switch_round, next_2); - assert_eq!(validator_info.catchain_seqno, 13); } #[test] fn noop_if_v_set_unchanged() { + let start = KbNextSessionStart { + is_curr_switch_after_pause: true, + ..random_session_start() + }; + let is_shuffle = random(); let update = KbNextSessionUpdate { prev_shuffle_mc_validators: is_shuffle, @@ -1873,9 +1865,7 @@ mod vset_update_start { let mut cons_info = random_consensus_info(); let before = cons_info; - let validator_info = update - .apply(&mut cons_info, random(), random(), true) - .unwrap(); + let validator_info = update.apply(&mut cons_info, random(), &start).unwrap(); assert!(validator_info.is_none(), "{update:?} {cons_info:?}"); assert_eq!(cons_info, before, "{update:?} {cons_info:?}"); From caed98eec00806dcf0a91b52f5d4e92de3ff9350 Mon Sep 17 00:00:00 2001 From: Kirill Mikheev Date: Wed, 15 Apr 2026 19:59:53 +0300 Subject: [PATCH 19/21] feat(consensus): slasher stats output --- Cargo.lock | 1 + collator/src/collator/anchors_cache.rs | 1 + .../collator/tests/messages_reader_tests.rs | 1 + collator/src/mempool/impls/common/cache.rs | 1 + collator/src/mempool/impls/common/shuttle.rs | 1 + collator/src/mempool/impls/dump_anchors.rs | 1 + .../impls/single_node_impl/anchor_handler.rs | 1 + collator/src/mempool/impls/stub_impl.rs | 3 ++ collator/src/mempool/mod.rs | 2 ++ consensus/Cargo.toml | 3 +- consensus/src/dag/commit/mod.rs | 1 + consensus/src/engine/committer_task.rs | 36 ++++++++++++++++++- consensus/src/models/output.rs | 3 ++ consensus/src/storage/adapter_store.rs | 1 + slasher-traits/src/lib.rs | 2 ++ slasher-traits/src/mempool.rs | 9 +++++ 16 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 slasher-traits/src/mempool.rs diff --git a/Cargo.lock b/Cargo.lock index 76f52657a3..52de724fa6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4193,6 +4193,7 @@ dependencies = [ "tracing-subscriber", "tycho-crypto", "tycho-network", + "tycho-slasher-traits", "tycho-storage", "tycho-types", "tycho-util", diff --git a/collator/src/collator/anchors_cache.rs b/collator/src/collator/anchors_cache.rs index 38eadfd9ea..992dec74b8 100644 --- a/collator/src/collator/anchors_cache.rs +++ b/collator/src/collator/anchors_cache.rs @@ -343,6 +343,7 @@ mod tests { chain_time, author: PeerId([0; 32]), externals: Default::default(), + stats: None, }) } diff --git a/collator/src/collator/tests/messages_reader_tests.rs b/collator/src/collator/tests/messages_reader_tests.rs index e601ea5fc3..a9ec5fa052 100644 --- a/collator/src/collator/tests/messages_reader_tests.rs +++ b/collator/src/collator/tests/messages_reader_tests.rs @@ -1816,6 +1816,7 @@ where author: PeerId(Default::default()), chain_time: anchor_ct, externals, + stats: None, }); self.mempool.insert(anchor_id, anchor.clone()); diff --git a/collator/src/mempool/impls/common/cache.rs b/collator/src/mempool/impls/common/cache.rs index 15fa543d7e..9cc7ca653f 100644 --- a/collator/src/mempool/impls/common/cache.rs +++ b/collator/src/mempool/impls/common/cache.rs @@ -356,6 +356,7 @@ mod tests { author: PeerId(Default::default()), chain_time: id as u64, externals: vec![], + stats: None, }) } diff --git a/collator/src/mempool/impls/common/shuttle.rs b/collator/src/mempool/impls/common/shuttle.rs index 91faa60e28..fae776a7af 100644 --- a/collator/src/mempool/impls/common/shuttle.rs +++ b/collator/src/mempool/impls/common/shuttle.rs @@ -56,6 +56,7 @@ impl Shuttle { chain_time, author: *committed.anchor.author(), externals: unique_messages, + stats: committed.stats.clone(), }); metrics::counter!("tycho_mempool_msgs_unique_count") diff --git a/collator/src/mempool/impls/dump_anchors.rs b/collator/src/mempool/impls/dump_anchors.rs index efaf2f5184..0b2cb45a07 100644 --- a/collator/src/mempool/impls/dump_anchors.rs +++ b/collator/src/mempool/impls/dump_anchors.rs @@ -45,6 +45,7 @@ impl TryFrom for MempoolAnchor { author: value.author, chain_time: value.chain_time, externals, + stats: None, }) } } diff --git a/collator/src/mempool/impls/single_node_impl/anchor_handler.rs b/collator/src/mempool/impls/single_node_impl/anchor_handler.rs index 20af83b60a..cc1ec41e8b 100644 --- a/collator/src/mempool/impls/single_node_impl/anchor_handler.rs +++ b/collator/src/mempool/impls/single_node_impl/anchor_handler.rs @@ -60,6 +60,7 @@ impl SingleNodeAnchorHandler { chain_time, author: self.peer_id, externals: unique_messages, + stats: None, })) .expect("push new anchor"); diff --git a/collator/src/mempool/impls/stub_impl.rs b/collator/src/mempool/impls/stub_impl.rs index 46dea9df3a..797968bee3 100644 --- a/collator/src/mempool/impls/stub_impl.rs +++ b/collator/src/mempool/impls/stub_impl.rs @@ -443,6 +443,7 @@ pub(crate) fn make_empty_anchor( author: PeerId(Default::default()), chain_time, externals: vec![], + stats: None, }) } @@ -467,6 +468,7 @@ pub(crate) fn make_stub_anchor(id: MempoolAnchorId, prev_id: MempoolAnchorId) -> author: PeerId(Default::default()), chain_time, externals, + stats: None, } } @@ -530,6 +532,7 @@ pub(crate) fn make_anchor_from_file( author: PeerId(Default::default()), chain_time, externals, + stats: None, })) } diff --git a/collator/src/mempool/mod.rs b/collator/src/mempool/mod.rs index ae8fa230f4..4375f0578b 100644 --- a/collator/src/mempool/mod.rs +++ b/collator/src/mempool/mod.rs @@ -6,6 +6,7 @@ use anyhow::Result; use async_trait::async_trait; use bytes::Bytes; use tycho_network::PeerId; +use tycho_slasher_traits::AnchorStats; use tycho_types::models::*; use tycho_types::prelude::*; @@ -126,6 +127,7 @@ pub struct MempoolAnchor { pub author: PeerId, pub chain_time: u64, pub externals: Vec>, + pub stats: Option, } impl MempoolAnchor { diff --git a/consensus/Cargo.toml b/consensus/Cargo.toml index e33b512826..7d9593df99 100644 --- a/consensus/Cargo.toml +++ b/consensus/Cargo.toml @@ -49,8 +49,9 @@ weedb = { workspace = true } # local deps tycho-network = { workspace = true } -tycho-util = { workspace = true } +tycho-slasher-traits = { workspace = true } tycho-storage = { workspace = true } +tycho-util = { workspace = true } [dev-dependencies] humantime = { workspace = true } diff --git a/consensus/src/dag/commit/mod.rs b/consensus/src/dag/commit/mod.rs index 4339773c28..1946a861c0 100644 --- a/consensus/src/dag/commit/mod.rs +++ b/consensus/src/dag/commit/mod.rs @@ -322,6 +322,7 @@ impl Committer { proof_key: next.proof.key(), history: committed, is_executable, + stats: None, // may be set later }) } } diff --git a/consensus/src/engine/committer_task.rs b/consensus/src/engine/committer_task.rs index 8b8d8dda18..56dc437a6e 100644 --- a/consensus/src/engine/committer_task.rs +++ b/consensus/src/engine/committer_task.rs @@ -1,8 +1,10 @@ use std::mem; +use std::sync::Arc; use itertools::Itertools; use tokio::sync::mpsc; use tycho_network::PeerId; +use tycho_slasher_traits::{AnchorPeerStats, AnchorStats}; use tycho_util::FastHashMap; use tycho_util::metrics::HistogramGuard; @@ -165,7 +167,7 @@ impl State { let stats_ranges = inner.peer_schedule.atomic().stats_ranges(); - for adata in committed { + for mut adata in committed { let anchor_round = adata.anchor.round(); let (stat_map, events) = (inner.committer).remove_committed( @@ -173,6 +175,9 @@ impl State { &stats_ranges, round_ctx.conf(), )?; + if let Some(stats_slot_map) = stats_ranges.stats_slot_map(anchor_round) { + adata.stats = Some(build_dense_anchor_stats(&stat_map, stats_slot_map)); + } all_stats.push(stat_map); round_ctx.commit_metrics(&adata.anchor); @@ -196,6 +201,35 @@ impl State { } } +fn build_dense_anchor_stats( + stat_map: &FastHashMap, + stats_slot_map: &FastHashMap, +) -> AnchorStats { + let mut dense = (0..stats_slot_map.len()) + .map(|_| AnchorPeerStats { points_proven: 0 }) + .collect::>(); + + for (peer_id, stats) in stat_map { + let Some(counters) = stats.counters() else { + continue; + }; + if counters.points_proved.inner() == 0 { + continue; + } + let Some(slot_id) = stats_slot_map.get(peer_id) else { + continue; + }; + let Some(slot) = dense.get_mut(*slot_id) else { + continue; + }; + slot.points_proven = slot + .points_proven + .saturating_add(counters.points_proved.inner()); + } + + AnchorStats(Arc::<[AnchorPeerStats]>::from(dense)) +} + impl RoundCtx { fn commit_metrics(&self, anchor: &PointInfo) { metrics::counter!("tycho_mempool_commit_anchors").increment(1); diff --git a/consensus/src/models/output.rs b/consensus/src/models/output.rs index ebcc40143d..6e37047bfc 100644 --- a/consensus/src/models/output.rs +++ b/consensus/src/models/output.rs @@ -1,3 +1,5 @@ +use tycho_slasher_traits::AnchorStats; + use crate::models::{PointInfo, PointKey, Round}; pub struct AnchorData { @@ -7,6 +9,7 @@ pub struct AnchorData { pub prev_anchor: Option, pub history: Vec, pub is_executable: bool, + pub stats: Option, } pub enum MempoolOutput { diff --git a/consensus/src/storage/adapter_store.rs b/consensus/src/storage/adapter_store.rs index 479f547f28..d9ab8ec924 100644 --- a/consensus/src/storage/adapter_store.rs +++ b/consensus/src/storage/adapter_store.rs @@ -324,6 +324,7 @@ impl MempoolAdapterStore { .map(|r| r.prev()), history: keyed_vec.into_iter().map(|(_, info)| info).collect(), is_executable: false, // define later + stats: None, // not needed }); } Ok(result) diff --git a/slasher-traits/src/lib.rs b/slasher-traits/src/lib.rs index 7effcbd34c..02018e8c63 100644 --- a/slasher-traits/src/lib.rs +++ b/slasher-traits/src/lib.rs @@ -1,6 +1,8 @@ +pub use self::mempool::*; pub use self::validator::{ BlockValidationScope, NoopValidatorEventsRecorder, ReceivedSignature, ValidationSessionId, ValidatorEvents, ValidatorEventsListener, ValidatorSessionScope, }; +mod mempool; mod validator; diff --git a/slasher-traits/src/mempool.rs b/slasher-traits/src/mempool.rs new file mode 100644 index 0000000000..c7a3521ea6 --- /dev/null +++ b/slasher-traits/src/mempool.rs @@ -0,0 +1,9 @@ +use std::sync::Arc; + +#[derive(Debug, Clone)] +pub struct AnchorStats(pub Arc<[AnchorPeerStats]>); + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AnchorPeerStats { + pub points_proven: u16, +} From 5dd456dc4c555e914ef4fc4164fd5030ef408cdd Mon Sep 17 00:00:00 2001 From: Kirill Mikheev Date: Wed, 15 Apr 2026 19:42:49 +0300 Subject: [PATCH 20/21] feat(slasher): mempool stats through collator --- cli/src/node/mod.rs | 1 + collator/src/collator/mod.rs | 35 +++++++++ collator/src/manager/mod.rs | 5 ++ collator/src/validator/impls/std_impl/mod.rs | 4 +- collator/tests/collation_tests.rs | 4 + slasher-traits/src/validator.rs | 51 ++++++++++++- slasher/src/bc/mod.rs | 54 +++++++++++++- slasher/src/bc/stub_contract.rs | 78 ++++++++++++++++---- slasher/src/collector/validator_events.rs | 34 ++++++++- slasher/src/proto.tl | 12 +++ slasher/src/storage/models.rs | 49 +++++++++++- 11 files changed, 300 insertions(+), 27 deletions(-) diff --git a/cli/src/node/mod.rs b/cli/src/node/mod.rs index ec0285cabc..7c4b71f278 100644 --- a/cli/src/node/mod.rs +++ b/cli/src/node/mod.rs @@ -271,6 +271,7 @@ impl Node { CollatorStdImplFactory { wu_tuner_event_sender: Some(wu_tuner.event_sender.clone()), }, + slasher.validator_events_listener(), self.mempool_config_override.clone(), ); let collator_subcriber = CollatorStateSubscriber { diff --git a/collator/src/collator/mod.rs b/collator/src/collator/mod.rs index 97fed8fc1c..8e55821c7a 100644 --- a/collator/src/collator/mod.rs +++ b/collator/src/collator/mod.rs @@ -72,6 +72,7 @@ pub(super) mod tests; #[cfg(test)] pub(crate) use messages_reader::tests::{TestInternalMessage, TestMessageFactory}; +use tycho_slasher_traits::ValidatorEventsListener; // FACTORY @@ -82,6 +83,7 @@ pub struct CollatorContext { pub config: Arc, pub collation_session: Arc, pub zerostate_id: ZerostateId, + pub stats_recorder: Arc, pub shard_id: ShardIdent, pub prev_blocks_ids: Vec, @@ -163,6 +165,7 @@ pub struct CollatorStdImpl { collation_session: Arc, zerostate_id: ZerostateId, + stats_recorder: Arc, mq_adapter: Arc>, mpool_adapter: Arc, state_node_adapter: Arc, @@ -257,6 +260,7 @@ impl CollatorStdImpl { config, collation_session, zerostate_id, + stats_recorder, shard_id, prev_blocks_ids, mempool_config_override, @@ -279,6 +283,7 @@ impl CollatorStdImpl { config, collation_session, zerostate_id, + stats_recorder, mq_adapter, mpool_adapter, state_node_adapter, @@ -1514,6 +1519,21 @@ impl CollatorStdImpl { ) .record(elapsed_from_prev_anchor); + if let Some(stats) = &next_anchor.stats { + let (catchain_seqno, vset_switch_round) = + self.collation_session.get_validation_session_id(); + + self.stats_recorder.on_anchor_import( + tycho_slasher_traits::ValidationSessionId { + catchain_seqno, + vset_switch_round, + }, + &self.next_block_info, + next_anchor.id, + stats.clone(), + ); + } + working_state.wu_used_from_last_anchor = 0; // time between anchors @@ -1721,6 +1741,21 @@ impl CollatorStdImpl { "imported next anchor, will notify collation manager", ); + if let Some(stats) = &next_anchor.stats { + let (catchain_seqno, vset_switch_round) = + self.collation_session.get_validation_session_id(); + + self.stats_recorder.on_anchor_import( + tycho_slasher_traits::ValidationSessionId { + catchain_seqno, + vset_switch_round, + }, + &self.next_block_info, + next_anchor.id, + stats.clone(), + ); + } + // this may start master block collation or cause next anchor import let res = CollatorResult::skipped( working_state.mc_data.block_id, diff --git a/collator/src/manager/mod.rs b/collator/src/manager/mod.rs index c91c6d968e..47de71f46e 100644 --- a/collator/src/manager/mod.rs +++ b/collator/src/manager/mod.rs @@ -14,6 +14,7 @@ use tycho_block_util::state::ShardStateStuff; use tycho_core::global_config::MempoolGlobalConfig; use tycho_core::storage::{LoadStateHint, StateNotFound}; use tycho_crypto::ed25519::KeyPair; +use tycho_slasher_traits::ValidatorEventsListener; use tycho_types::models::{ BlockId, BlockIdShort, CollationConfig, GlobalCapabilities, IndexedValidatorDescription, ProcessedUptoInfo, ShardIdent, @@ -85,6 +86,7 @@ where validator: Arc, cancel_validation_runner: Mutex, + stats_recorder: Arc, active_collation_sessions: RwLock>>, active_collators: FastDashMap>>, @@ -144,6 +146,7 @@ where mpool_adapter_factory: MPF, validator: V, collator_factory: CF, + stats_recorder: Arc, mempool_config_override: Option, ) -> Arc> where @@ -178,6 +181,7 @@ where validator, cancel_validation_runner: Default::default(), + stats_recorder, active_collation_sessions: Default::default(), active_collators: Default::default(), @@ -2504,6 +2508,7 @@ where state_node_adapter: self.state_node_adapter.clone(), config: self.config.clone(), collation_session: new_session_info.clone(), + stats_recorder: self.stats_recorder.clone(), zerostate_id: *self.state_node_adapter.zerostate_id(), shard_id, prev_blocks_ids: prev_blocks_ids.clone(), diff --git a/collator/src/validator/impls/std_impl/mod.rs b/collator/src/validator/impls/std_impl/mod.rs index d32c4ad722..98be36aa76 100644 --- a/collator/src/validator/impls/std_impl/mod.rs +++ b/collator/src/validator/impls/std_impl/mod.rs @@ -77,7 +77,7 @@ impl ValidatorStdImpl { net_context: ValidatorNetworkContext, keypair: Arc, config: ValidatorStdImplConfig, - recorder: Arc, + stats_recorder: Arc, ) -> Self { Self { inner: Arc::new(Inner { @@ -85,7 +85,7 @@ impl ValidatorStdImpl { keypair, sessions: Default::default(), config, - events: ValidatorEvents::new(recorder), + events: ValidatorEvents::new(stats_recorder), }), } } diff --git a/collator/tests/collation_tests.rs b/collator/tests/collation_tests.rs index 7151ef8a28..c901c8a84c 100644 --- a/collator/tests/collation_tests.rs +++ b/collator/tests/collation_tests.rs @@ -26,6 +26,7 @@ use tycho_core::global_config::ZerostateId; use tycho_core::node::NodeKeys; use tycho_core::storage::CoreStorage; use tycho_crypto::ed25519; +use tycho_slasher_traits::NoopValidatorEventsRecorder; use tycho_storage::StorageContext; use tycho_types::models::{BlockId, BlockIdShort, ShardIdent}; @@ -216,6 +217,8 @@ fn start_collation_manager( ctx: &DumpCollationContext, dumped_anchors: Vec, ) -> RunningCollationManager { + let recorder = Arc::new(NoopValidatorEventsRecorder); + CollationManager::create( ctx.keypair.clone(), ctx.config.clone(), @@ -237,6 +240,7 @@ fn start_collation_manager( CollatorStdImplFactory { wu_tuner_event_sender: None, }, + recorder, None, ) } diff --git a/slasher-traits/src/validator.rs b/slasher-traits/src/validator.rs index 63fb9494ab..b165f1c4aa 100644 --- a/slasher-traits/src/validator.rs +++ b/slasher-traits/src/validator.rs @@ -3,9 +3,11 @@ use std::sync::Arc; use std::sync::atomic::{AtomicBool, AtomicU8, Ordering}; use indexmap::IndexMap; -use tycho_types::models::{BlockId, IndexedValidatorDescription}; +use tycho_types::models::{BlockId, BlockIdShort, IndexedValidatorDescription}; use tycho_util::FastHasherState; +use crate::AnchorStats; + // TODO: Decide how to be with this collator-defined type #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct ValidationSessionId { @@ -230,6 +232,15 @@ pub trait ValidatorEventsListener: Send + Sync + 'static { /// Called when the session is complete. fn on_session_finished(&self, session_id: ValidationSessionId); + /// Called when anchor is imported + fn on_anchor_import( + &self, + session_id: ValidationSessionId, + block_id: &BlockIdShort, + anchor_id: u32, + anchor_stats: AnchorStats, + ); + /// Called when validation is complete for a block. fn on_block_validated( &self, @@ -257,6 +268,15 @@ impl ValidatorEventsListener for NoopValidatorEventsRecorder { fn on_session_finished(&self, _session_id: ValidationSessionId) {} + fn on_anchor_import( + &self, + _session_id: ValidationSessionId, + _block_id: &BlockIdShort, + _anchor_id: u32, + _anchor_stats: AnchorStats, + ) { + } + fn on_block_validated( &self, _session_id: ValidationSessionId, @@ -288,13 +308,33 @@ macro_rules! impl_recorder_for_tuples { $(self.$n.on_session_finished(session_id);)+ } + fn on_anchor_import(&self, + session_id: ValidationSessionId, + block_id: &BlockIdShort, + anchor_id: u32, + anchor_stats: AnchorStats, + ) { + impl_recorder_for_tuples!(@call_on_anchor_import self, + session_id, + block_id, + anchor_id, + anchor_stats, + $($n)+ + ); + } + fn on_block_validated( &self, session_id: ValidationSessionId, block_id: &BlockId, signatures: Arc<[ReceivedSignature]>, ) { - impl_recorder_for_tuples!(@call_on_validated self, session_id, block_id, signatures, $($n)+); + impl_recorder_for_tuples!(@call_on_validated self, + session_id, + block_id, + signatures, + $($n)+ + ); } fn on_block_skipped(&self, session_id: ValidationSessionId, block_id: &BlockId) { @@ -303,10 +343,17 @@ macro_rules! impl_recorder_for_tuples { })* }; + (@call_on_anchor_import $self:ident, $sid:ident, $block_id:ident, $anchor_id:ident, $anchor_stats:ident, $n:tt $($rest:tt)+) => { + $self.$n.on_anchor_import($sid, $block_id, $anchor_id, $anchor_stats.clone()); + impl_recorder_for_tuples!(@call_on_anchor_import $self, $sid, $block_id, $anchor_id, $anchor_stats, $($rest)+) + }; (@call_on_validated $self:ident, $sid:ident, $block_id:ident, $signatures:ident, $n:tt $($rest:tt)+) => { $self.$n.on_block_validated($sid, $block_id, $signatures.clone()); impl_recorder_for_tuples!(@call_on_validated $self, $sid, $block_id, $signatures, $($rest)+) }; + (@call_on_anchor_import $self:ident, $sid:ident, $block_id:ident, $anchor_id:ident, $anchor_stats:ident, $n:tt) => { + $self.$n.on_anchor_import($sid, $block_id, $anchor_id, $anchor_stats); + }; (@call_on_validated $self:ident, $sid:ident, $block_id:ident, $signatures:ident, $n:tt) => { $self.$n.on_block_validated($sid, $block_id, $signatures); }; diff --git a/slasher/src/bc/mod.rs b/slasher/src/bc/mod.rs index d424d2dde4..d3f8035a45 100644 --- a/slasher/src/bc/mod.rs +++ b/slasher/src/bc/mod.rs @@ -1,10 +1,11 @@ use std::num::NonZeroU32; +use std::ops::RangeInclusive; use std::time::Duration; use anyhow::Result; use tokio::sync::oneshot; use tycho_crypto::ed25519; -use tycho_slasher_traits::{ReceivedSignature, ValidationSessionId}; +use tycho_slasher_traits::{AnchorPeerStats, AnchorStats, ReceivedSignature, ValidationSessionId}; use tycho_types::cell::{HashBytes, Lazy}; use tycho_types::models::{ BlockchainConfigParams, OwnedMessage, SignatureContext, StdAddr, Transaction, @@ -159,16 +160,29 @@ pub struct SubmitBlocksBatch { #[derive(Debug, PartialEq, Eq)] pub struct BlocksBatch { pub start_seqno: u32, + pub anchor_range: Option>, pub committed_blocks: BitSet, pub signatures_history: Box<[SignatureHistory]>, + /// sorted by `validator_idx` since mempool output + pub anchor_stats_history: Box<[AnchorStatsHistory]>, } impl BlocksBatch { pub fn new(start_seqno: u32, len: NonZeroU32, map_ids: &[u16]) -> Self { let len = len.get() as usize; + let mut anchor_stats_history = map_ids + .iter() + .map(|validator_idx| AnchorStatsHistory { + validator_idx: *validator_idx, + stats: AnchorPeerStats { points_proven: 0 }, + }) + .collect::>(); + anchor_stats_history.sort_unstable_by_key(|a| a.validator_idx); + Self { start_seqno, + anchor_range: None, committed_blocks: BitSet::with_capacity(len), signatures_history: map_ids .iter() @@ -177,11 +191,12 @@ impl BlocksBatch { bits: BitSet::with_capacity(len * 2), }) .collect::>(), + anchor_stats_history: anchor_stats_history.into_boxed_slice(), } } pub fn is_empty(&self) -> bool { - self.committed_blocks.is_zero() + self.committed_blocks.is_zero() && self.anchor_range.is_none() } pub fn start_seqno(&self) -> u32 { @@ -207,6 +222,35 @@ impl BlocksBatch { (self.start_seqno..self.seqno_after()).contains(&seqno) } + pub fn push_anchor_stats( + &mut self, + seqno: u32, + anchor_id: u32, + anchor_stats: &AnchorStats, + ) -> bool { + if !self.contains_seqno(seqno) + || (self.anchor_range.as_ref()).is_some_and(|r| r.contains(&anchor_id)) + || anchor_stats.0.len() != self.anchor_stats_history.len() + { + return false; + } + + match &mut self.anchor_range { + None => self.anchor_range = Some(anchor_id..=anchor_id), + Some(exist) => { + *exist = *exist.start()..=*exist.end().max(&anchor_id); + } + }; + + for (history, received) in std::iter::zip(&mut self.anchor_stats_history, &*anchor_stats.0) + { + history.stats.points_proven = + (history.stats.points_proven).saturating_add(received.points_proven); + } + + true + } + pub fn commit_signatures(&mut self, mut seqno: u32, signatures: &[ReceivedSignature]) -> bool { if !self.contains_seqno(seqno) || signatures.len() != self.signatures_history.len() { return false; @@ -230,3 +274,9 @@ pub struct SignatureHistory { pub validator_idx: u16, pub bits: BitSet, } + +#[derive(Debug, PartialEq, Eq)] +pub struct AnchorStatsHistory { + pub validator_idx: u16, + pub stats: AnchorPeerStats, +} diff --git a/slasher/src/bc/stub_contract.rs b/slasher/src/bc/stub_contract.rs index e3c771fc0c..e8feea0255 100644 --- a/slasher/src/bc/stub_contract.rs +++ b/slasher/src/bc/stub_contract.rs @@ -1,7 +1,7 @@ use std::num::{NonZeroU8, NonZeroU32}; -use anyhow::{Context, Result}; -use tycho_slasher_traits::ValidationSessionId; +use anyhow::{Context, Result, anyhow}; +use tycho_slasher_traits::{AnchorPeerStats, ValidationSessionId}; use tycho_types::cell::Lazy; use tycho_types::dict; use tycho_types::models::{ @@ -10,8 +10,8 @@ use tycho_types::models::{ use tycho_types::prelude::*; use super::{ - BlocksBatch, SignatureHistory, SignedMessage, SlasherContract, SlasherContractEvent, - SlasherParams, SubmitBlocksBatch, + AnchorStatsHistory, BlocksBatch, SignatureHistory, SignedMessage, SlasherContract, + SlasherContractEvent, SlasherParams, SubmitBlocksBatch, }; use crate::util::BitSet; @@ -157,30 +157,49 @@ impl BlocksBatchBc { impl<'a> Load<'a> for BlocksBatchBc { fn load_from(slice: &mut CellSlice<'a>) -> Result { let start_seqno = slice.load_u32()?; + let anchor_range = + Some(slice.load_u32()?..=slice.load_u32()?).filter(|range| *range.end() > 0); let block_count = slice.size_bits() as usize; let committed_blocks = BitSet::load_from_cs(block_count, slice)?; - let mut signatures_history = Vec::new(); + let mut zipped = Vec::new(); let dict = Dict::>::from_raw(Some(slice.load_reference_cloned()?)); for entry in dict.iter() { let (validator_idx, mut cs) = entry?; let bits = BitSet::load_from_cs(block_count * 2, &mut cs)?; + let points_proven = cs.load_u16()?; if !cs.is_empty() { return Err(tycho_types::error::Error::CellOverflow); } + zipped.push((validator_idx, bits, points_proven)); + } + + let anchor_stats_history = zipped + .iter() + .map(|(validator_idx, _, points_proven)| AnchorStatsHistory { + validator_idx: *validator_idx, + stats: AnchorPeerStats { + points_proven: *points_proven, + }, + }) + .collect::>(); - signatures_history.push(SignatureHistory { + let signatures_history = zipped + .into_iter() + .map(|(validator_idx, bits, _)| SignatureHistory { validator_idx, bits, - }); - } + }) + .collect::>(); Ok(Self(BlocksBatch { start_seqno, + anchor_range, committed_blocks, signatures_history: signatures_history.into_boxed_slice(), + anchor_stats_history: anchor_stats_history.into_boxed_slice(), })) } } @@ -194,18 +213,47 @@ impl Store for BlocksBatchBc { let batch = &self.0; builder.store_u32(batch.start_seqno)?; + if let Some(anchor_range) = self.0.anchor_range.as_ref() { + builder.store_u32(*anchor_range.start())?; + builder.store_u32(*anchor_range.end())?; + } else { + builder.store_u32(0)?; + builder.store_u32(0)?; + }; + batch.committed_blocks.store_into(builder, context)?; // A subset contains items in no particular order, // so we need to sort by them to simplify remapping to vset. - let mut entries = batch - .signatures_history - .iter() - .map(|item| (item.validator_idx, &item.bits)) - .collect::>(); - entries.sort_unstable_by_key(|(a, _)| *a); + let mut signatures = (batch.signatures_history.iter()).collect::>(); + signatures.sort_unstable_by_key(|a| a.validator_idx); + + assert_eq!( + signatures.len(), + batch.anchor_stats_history.len(), + "anchor and signature stats must have the same length, but {} != {}", + signatures.len(), + batch.anchor_stats_history.len(), + ); - let Some(dict_root) = dict::build_dict_from_sorted_iter(entries, context)? else { + let zipped = signatures + .iter() + .zip(&batch.anchor_stats_history) + .map(|(s, a)| { + if s.validator_idx == a.validator_idx { + Ok((s.validator_idx, (&s.bits, a.stats.points_proven))) + } else { + Err(anyhow!( + "expected {} got {}", + s.validator_idx, + a.validator_idx + )) + } + }) + .collect::>>() + .expect("anchor stats must share validator_idx with signature history"); + + let Some(dict_root) = dict::build_dict_from_sorted_iter(zipped, context)? else { // Subset must not be empty. return Err(tycho_types::error::Error::InvalidData); }; diff --git a/slasher/src/collector/validator_events.rs b/slasher/src/collector/validator_events.rs index 0e742bf8dc..2967c4b9d9 100644 --- a/slasher/src/collector/validator_events.rs +++ b/slasher/src/collector/validator_events.rs @@ -7,8 +7,10 @@ use anyhow::Result; use tokio::sync::mpsc; use tracing::instrument; use tycho_crypto::ed25519; -use tycho_slasher_traits::{ReceivedSignature, ValidationSessionId, ValidatorEventsListener}; -use tycho_types::models::{BlockId, IndexedValidatorDescription}; +use tycho_slasher_traits::{ + AnchorStats, ReceivedSignature, ValidationSessionId, ValidatorEventsListener, +}; +use tycho_types::models::{BlockId, BlockIdShort, IndexedValidatorDescription}; use tycho_util::{DashMapEntry, FastDashMap}; use crate::bc::BlocksBatch; @@ -183,6 +185,34 @@ impl ValidatorEventsListener for ValidatorEventsCollector { } } + #[instrument(skip_all, fields(session_id = ?session_id))] + fn on_anchor_import( + &self, + session_id: ValidationSessionId, + block_id: &BlockIdShort, + anchor_id: u32, + anchor_stats: AnchorStats, + ) { + if !block_id.is_masterchain() { + // Ignore for non-masterchain blocks (just in case). + return; + } + + tracing::debug!( + target: tracing_targets::SLASHER, + %block_id, + "on_anchor_import" + ); + let Some(mut session) = self.sessions.get_mut(&session_id) else { + tracing::warn!( + target: tracing_targets::SLASHER, + "session not found, ignoring on_anchor_import event" + ); + return; + }; + (session.current_batch).push_anchor_stats(block_id.seqno, anchor_id, &anchor_stats); + } + #[instrument(skip_all, fields(session_id = ?session_id))] fn on_block_validated( &self, diff --git a/slasher/src/proto.tl b/slasher/src/proto.tl index 8aff1ec547..03ff34598c 100644 --- a/slasher/src/proto.tl +++ b/slasher/src/proto.tl @@ -7,8 +7,11 @@ */ slasher.blocksBatch start_seqno:int + min_anchor:int + max_anchor:int committed_blocks:bitset entries:(vector slasher.signatureHistory) + anchor_stats_history:(vector slasher.anchorStatsHistory) = slasher.BlocksBatch; /** @@ -20,6 +23,15 @@ slasher.signatureHistory bits:bitset = slasher.SignatureHistory; +/** +* @param validator_idx validator index relative to the validator set +* @param points_proven occurrences of a point having the next one as a proof +*/ +slasher.anchorStatsHistory + validator_idx:int + points_proven:int + = slasher.AnchorStatsHistory; + /** * @param catchain_seqno validation session catchain seqno * @param vset_switch_round validation session vset switch round diff --git a/slasher/src/storage/models.rs b/slasher/src/storage/models.rs index d741aefecf..83d9cd988e 100644 --- a/slasher/src/storage/models.rs +++ b/slasher/src/storage/models.rs @@ -1,5 +1,5 @@ use tl_proto::{TlError, TlPacket, TlRead, TlResult, TlWrite}; -use tycho_slasher_traits::ValidationSessionId; +use tycho_slasher_traits::{AnchorPeerStats, ValidationSessionId}; use tycho_types::cell::HashBytes; use tycho_util::FastHashSet; @@ -7,6 +7,7 @@ use crate::analyzer::{ SessionMeta, SessionPenaltyReport, SessionValidatorScore, VsetEpoch, VsetPenaltyReport, VsetValidatorPenalty, }; +use crate::bc::AnchorStatsHistory; use crate::util::BitSet; use crate::{BlocksBatch, SignatureHistory}; @@ -31,25 +32,45 @@ impl TlWrite for StoredBlocksBatch { fn max_size_hint(&self) -> usize { 4 + 4 + + (4 + 4) + self.0.committed_blocks.max_size_hint() + 4 - + self - .0 - .signatures_history + + (self.0.signatures_history) .iter() .map(|item| 4 + item.bits.max_size_hint()) .sum::() + + 4 + + (4 + 4) * self.0.anchor_stats_history.len() } fn write_to(&self, packet: &mut P) { packet.write_u32(Self::TL_ID); packet.write_u32(self.0.start_seqno); + if let Some(anchor_range) = self.0.anchor_range.as_ref() { + packet.write_u32(*anchor_range.start()); + packet.write_u32(*anchor_range.end()); + } else { + packet.write_u32(0); + packet.write_u32(0); + }; + self.0.committed_blocks.write_to(packet); packet.write_u32(self.0.signatures_history.len() as u32); for item in &self.0.signatures_history { packet.write_u32(item.validator_idx as u32); item.bits.write_to(packet); } + + assert_eq!( + self.0.signatures_history.len(), + self.0.anchor_stats_history.len(), + "signature history and anchor stats must have the same length" + ); + packet.write_u32(self.0.anchor_stats_history.len() as u32); + for entry in &self.0.anchor_stats_history { + packet.write_u32(entry.validator_idx as u32); + packet.write_u32(entry.stats.points_proven as u32); + } } } @@ -62,6 +83,9 @@ impl<'tl> TlRead<'tl> for StoredBlocksBatch { } let start_seqno = u32::read_from(packet)?; + let anchor_range = Some(u32::read_from(packet)?..=u32::read_from(packet)?) + .filter(|range| *range.end() > 0); + let committed_blocks = BitSet::read_from(packet)?; let block_count = committed_blocks.len(); if start_seqno.checked_add(block_count as u32).is_none() { @@ -95,10 +119,27 @@ impl<'tl> TlRead<'tl> for StoredBlocksBatch { }); } + if u32::read_from(packet)? as usize != history_count { + return Err(TlError::InvalidData); + } + let mut anchor_stats_history = Vec::with_capacity(history_count); + for _ in 0..history_count { + let validator_idx = + u16::try_from(u32::read_from(packet)?).map_err(|_e| TlError::InvalidData)?; + let points_proven = + u16::try_from(u32::read_from(packet)?).map_err(|_e| TlError::InvalidData)?; + anchor_stats_history.push(AnchorStatsHistory { + validator_idx, + stats: AnchorPeerStats { points_proven }, + }); + } + Ok(Self(BlocksBatch { start_seqno, + anchor_range, committed_blocks, signatures_history: signatures_history.into_boxed_slice(), + anchor_stats_history: anchor_stats_history.into_boxed_slice(), })) } } From 96b61f3c31bc4c08e4a9d755783d8d9ede306732 Mon Sep 17 00:00:00 2001 From: Kirill Mikheev Date: Wed, 15 Apr 2026 01:44:05 +0300 Subject: [PATCH 21/21] feat(slasher): batches overlap allows to record anchor stats before current batch is advanced by validator --- slasher/src/bc/mod.rs | 24 +++--- slasher/src/collector/validator_events.rs | 98 +++++++++++++++++------ 2 files changed, 84 insertions(+), 38 deletions(-) diff --git a/slasher/src/bc/mod.rs b/slasher/src/bc/mod.rs index d3f8035a45..f617f8f469 100644 --- a/slasher/src/bc/mod.rs +++ b/slasher/src/bc/mod.rs @@ -1,5 +1,5 @@ use std::num::NonZeroU32; -use std::ops::RangeInclusive; +use std::ops::{Range, RangeInclusive}; use std::time::Duration; use anyhow::Result; @@ -208,6 +208,14 @@ impl BlocksBatch { .saturating_add(self.committed_blocks.len() as u32) } + pub fn seqno_range(&self) -> Range { + self.start_seqno..self.seqno_after() + } + + pub fn contains_seqno(&self, seqno: u32) -> bool { + self.seqno_range().contains(&seqno) + } + pub fn committed_block_count(&self) -> usize { (0..self.committed_blocks.len()) .filter(|offset| self.committed_blocks.get(*offset)) @@ -218,18 +226,8 @@ impl BlocksBatch { self.signatures_history.len() } - pub fn contains_seqno(&self, seqno: u32) -> bool { - (self.start_seqno..self.seqno_after()).contains(&seqno) - } - - pub fn push_anchor_stats( - &mut self, - seqno: u32, - anchor_id: u32, - anchor_stats: &AnchorStats, - ) -> bool { - if !self.contains_seqno(seqno) - || (self.anchor_range.as_ref()).is_some_and(|r| r.contains(&anchor_id)) + pub fn push_anchor_stats(&mut self, anchor_id: u32, anchor_stats: &AnchorStats) -> bool { + if (self.anchor_range.as_ref()).is_some_and(|r| r.contains(&anchor_id)) || anchor_stats.0.len() != self.anchor_stats_history.len() { return false; diff --git a/slasher/src/collector/validator_events.rs b/slasher/src/collector/validator_events.rs index 2967c4b9d9..24734b7015 100644 --- a/slasher/src/collector/validator_events.rs +++ b/slasher/src/collector/validator_events.rs @@ -42,6 +42,8 @@ struct SessionState { /// Maps each subset item with its original vset index. validator_indices: Box<[u16]>, current_batch: BlocksBatch, + /// Imported anchors can arrive one batch window before block callbacks rotate the session. + next_batch: BlocksBatch, first_seqno: u32, next_expected_seqno: u32, complete_batches: Option>, @@ -108,11 +110,8 @@ impl ValidatorEventsCollector { // TODO: Split or grow the previous batch to not discard events. if session.batch_size != batch_size { session.batch_size = batch_size; - session.current_batch = BlocksBatch::new( - session.align_seqno(session.next_expected_seqno), - batch_size, - &session.validator_indices, - ); + let next_expected_seqno = session.next_expected_seqno; + session.reset_batches(next_expected_seqno); } session.complete_batches = Some(complete_batches); @@ -147,6 +146,8 @@ impl ValidatorEventsListener for ValidatorEventsCollector { let batch_size = NonZeroU32::new(self.default_batch_size.load(Ordering::Acquire)).unwrap(); let current_batch = BlocksBatch::new(first_mc_seqno, batch_size, &validator_indices); + let next_batch = + BlocksBatch::new(current_batch.seqno_after(), batch_size, &validator_indices); let validators = Arc::<[IndexedValidatorDescription]>::from(validators); @@ -155,6 +156,7 @@ impl ValidatorEventsListener for ValidatorEventsCollector { batch_size, validator_indices, current_batch, + next_batch, first_seqno: first_mc_seqno, next_expected_seqno: first_mc_seqno, // Will be initialized later via `init_session`. @@ -210,7 +212,7 @@ impl ValidatorEventsListener for ValidatorEventsCollector { ); return; }; - (session.current_batch).push_anchor_stats(block_id.seqno, anchor_id, &anchor_stats); + session.push_anchor_stats(block_id.seqno, anchor_id, &anchor_stats); } #[instrument(skip_all, fields(session_id = ?session_id))] @@ -282,11 +284,34 @@ impl ValidatorSessionInfo { // === Session state impl === impl SessionState { + fn push_anchor_stats( + &mut self, + seqno: u32, + anchor_id: u32, + anchor_stats: &AnchorStats, + ) -> bool { + if self.current_batch.contains_seqno(seqno) { + (self.current_batch).push_anchor_stats(anchor_id, anchor_stats) + } else if self.next_batch.contains_seqno(seqno) { + (self.next_batch).push_anchor_stats(anchor_id, anchor_stats) + } else { + tracing::warn!( + target: tracing_targets::SLASHER, + anchor_id, + seqno, + current_batch_seqnos = ?self.current_batch.seqno_range(), + next_batch_seqnos = ?self.next_batch.seqno_range(), + "anchor import outside block batches" + ); + false + } + } + fn handle_block(&mut self, seqno: u32, signatures: Option<&[ReceivedSignature]>) -> bool { - let to_commit = match self.try_advance_current_batch(seqno) { + let batches = match self.try_advance_current_batch(seqno) { AdvanceBlockStatus::TooOld => return false, - AdvanceBlockStatus::Unchanged => None, - AdvanceBlockStatus::Replaced(batch) => Some(batch), + AdvanceBlockStatus::Unchanged => [None, None], + AdvanceBlockStatus::Rotated { first, second } => [Some(first), second], }; let event_type = match signatures { @@ -297,14 +322,14 @@ impl SessionState { None => "skipped", }; - if let Some(batch) = to_commit - && let Err(e) = self.commit_batch(batch) - { - tracing::error!( - target: tracing_targets::SLASHER, - event_type, - "failed to commit blocks batch: {e:?}" - ); + for (batch, ith) in batches.into_iter().flatten().zip(["1st", "2nd"]) { + if let Err(e) = self.commit_batch(batch) { + tracing::error!( + target: tracing_targets::SLASHER, + event_type, + "{ith} blocks batch failed to commit: {e:?}" + ); + } } true } @@ -316,14 +341,19 @@ impl SessionState { return AdvanceBlockStatus::Unchanged; } - let start_seqno = self.align_seqno(seqno); - let prev_batch = std::mem::replace( - &mut self.current_batch, - BlocksBatch::new(start_seqno, self.batch_size, &self.validator_indices), - ); self.next_expected_seqno = seqno + 1; - AdvanceBlockStatus::Replaced(prev_batch) + if self.next_batch.contains_seqno(seqno) { + let next = self.make_batch(self.next_batch.seqno_after()); + let current = std::mem::replace(&mut self.next_batch, next); + + AdvanceBlockStatus::Rotated { + first: std::mem::replace(&mut self.current_batch, current), + second: None, + } + } else { + self.reset_batches(seqno) + } } fn commit_batch(&self, batch: BlocksBatch) -> Result<()> { @@ -331,7 +361,8 @@ impl SessionState { } fn commit_final_batch(self) -> Result<()> { - Self::commit_batch_impl(&self.complete_batches, self.current_batch) + Self::commit_batch_impl(&self.complete_batches, self.current_batch)?; + Self::commit_batch_impl(&self.complete_batches, self.next_batch) } fn commit_batch_impl( @@ -352,6 +383,20 @@ impl SessionState { Ok(()) } + fn reset_batches(&mut self, seqno: u32) -> AdvanceBlockStatus { + let current = self.make_batch(self.align_seqno(seqno)); + let next = self.make_batch(current.seqno_after()); + + AdvanceBlockStatus::Rotated { + first: std::mem::replace(&mut self.current_batch, current), + second: Some(std::mem::replace(&mut self.next_batch, next)), + } + } + + fn make_batch(&self, start_seqno: u32) -> BlocksBatch { + BlocksBatch::new(start_seqno, self.batch_size, &self.validator_indices) + } + fn align_seqno(&self, seqno: u32) -> u32 { assert!(seqno >= self.first_seqno); @@ -367,5 +412,8 @@ impl SessionState { enum AdvanceBlockStatus { TooOld, Unchanged, - Replaced(BlocksBatch), + Rotated { + first: BlocksBatch, + second: Option, + }, }