From 5b0a6aa0a53395decda8fcbe0f6b7291eabc9b64 Mon Sep 17 00:00:00 2001 From: Ivan Kalinin Date: Thu, 17 Jul 2025 10:29:48 +0200 Subject: [PATCH 01/31] feat(slasher): add slasher crates --- Cargo.lock | 26 ++ Cargo.toml | 6 +- cli/Cargo.toml | 3 +- 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, 1132 insertions(+), 41 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 1152cd60f7..2de584e75e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4151,6 +4151,7 @@ dependencies = [ "tycho-crypto", "tycho-network", "tycho-rpc", + "tycho-slasher", "tycho-storage", "tycho-types", "tycho-util", @@ -4202,6 +4203,7 @@ dependencies = [ "tycho-crypto", "tycho-executor", "tycho-network", + "tycho-slasher-traits", "tycho-storage", "tycho-types", "tycho-util", @@ -4489,6 +4491,30 @@ dependencies = [ "tycho-util", ] +[[package]] +name = "tycho-slasher" +version = "0.3.10" +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.10" +dependencies = [ + "tycho-types", +] + [[package]] name = "tycho-storage" version = "0.3.10" diff --git a/Cargo.toml b/Cargo.toml index 954a9e8b79..369770ee19 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] @@ -170,6 +172,8 @@ tycho-core = { path = "./core", version = "0.3.10" } tycho-network = { path = "./network", version = "0.3.10" } tycho-rpc-subscriptions = { path = "./rpc-subscriptions", version = "0.3.10" } tycho-rpc = { path = "./rpc", version = "0.3.10" } +tycho-slasher = { path = "./slasher", version = "0.3.10" } +tycho-slasher-traits = { path = "./slasher-traits", version = "0.3.10" } tycho-storage = { path = "./storage", version = "0.3.10" } tycho-storage-traits = { path = "./storage-traits", version = "0.3.10" } tycho-util = { path = "./util", version = "0.3.10" } diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 14de85b5fe..3c17bb4274 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -61,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 b5cf478db8..60b00e3327 100644 --- a/cli/src/node/mod.rs +++ b/cli/src/node/mod.rs @@ -218,6 +218,9 @@ impl Node { let message_queue_adapter = MessageQueueAdapterStdImpl::new(queue); message_queue_adapter.recover_after_restart(&mc_state)?; + // NOTE: Stub + let slasher = tycho_slasher::Slasher::new(base.keypair.clone()); + let validator = ValidatorStdImpl::new( ValidatorNetworkContext { network: base.network.clone(), @@ -227,6 +230,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 7d4c5e0d96..28b53ac91a 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 39fdc58c0b..501253cbf1 100755 --- a/scripts/gen-dashboard.py +++ b/scripts/gen-dashboard.py @@ -3076,6 +3076,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 b07959d8a57dc96d5f83d742259f8f8883cb9366 Mon Sep 17 00:00:00 2001 From: Ivan Kalinin Date: Tue, 25 Nov 2025 14:47:37 +0100 Subject: [PATCH 02/31] feat(collator): propagate original vset item index --- Cargo.lock | 4 + block-util/src/block/block_proof_stuff.rs | 6 +- collator/src/manager/state_update_handler.rs | 12 +- collator/src/manager/utils.rs | 9 +- .../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(+), 243 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 2de584e75e..e074d34d0a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4497,11 +4497,15 @@ version = "0.3.10" 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/state_update_handler.rs b/collator/src/manager/state_update_handler.rs index 5790de33d2..840906a0b2 100644 --- a/collator/src/manager/state_update_handler.rs +++ b/collator/src/manager/state_update_handler.rs @@ -5,7 +5,7 @@ use ahash::HashMapExt; use anyhow::{Context, Result, anyhow}; use tokio::sync::Notify; use tycho_block_util::block::{ValidatorSubsetInfo, calc_next_block_id_short}; -use tycho_types::models::{BlockId, GlobalCapabilities, ShardIdent, ValidatorDescription}; +use tycho_types::models::{BlockId, GlobalCapabilities, IndexedValidatorDescription, ShardIdent}; use tycho_util::futures::JoinTask; use tycho_util::metrics::HistogramGuard; use tycho_util::{DashMapEntry, FastHashMap, FastHashSet}; @@ -195,13 +195,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, @@ -210,7 +212,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..fa20847242 100644 --- a/collator/src/manager/utils.rs +++ b/collator/src/manager/utils.rs @@ -1,11 +1,10 @@ use tycho_crypto::ed25519::{KeyPair, PublicKey}; -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,9 +15,9 @@ 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>, + _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 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 9e52d79fc49e9170112a4658f6575fcf0a7d8d9c Mon Sep 17 00:00:00 2001 From: Ivan Kalinin Date: Fri, 28 Nov 2025 13:01:39 +0100 Subject: [PATCH 03/31] 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 78179327a0be7c16ccc27832cd43ba8a16965c87 Mon Sep 17 00:00:00 2001 From: Ivan Kalinin Date: Wed, 24 Dec 2025 12:33:01 +0100 Subject: [PATCH 04/31] 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 e074d34d0a..5aa2da6169 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4502,7 +4502,9 @@ dependencies = [ "metrics", "parking_lot", "scopeguard", + "serde", "tokio", + "tokio-util", "tracing", "tycho-block-util", "tycho-core", @@ -4516,7 +4518,9 @@ dependencies = [ name = "tycho-slasher-traits" version = "0.3.10" 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 fa5157debe98ddebcbb5ec1308282c9ddaa9d2fc Mon Sep 17 00:00:00 2001 From: Ivan Kalinin Date: Thu, 25 Dec 2025 18:35:21 +0100 Subject: [PATCH 05/31] feat(slasher): add stub contract --- cli/src/node/config.rs | 5 ++ cli/src/node/mod.rs | 17 +++++- .../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 ++++++++++------ 16 files changed, 274 insertions(+), 53 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 60b00e3327..69c0ab8e28 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, }) @@ -219,7 +222,12 @@ impl Node { message_queue_adapter.recover_after_restart(&mc_state)?; // 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 { @@ -349,7 +357,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/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 1bdc74fb5d3fb130e5ad35ca9988a8d3ad8234bd Mon Sep 17 00:00:00 2001 From: Ivan Kalinin Date: Thu, 15 Jan 2026 15:53:08 +0100 Subject: [PATCH 06/31] 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 5aa2da6169..3fd4f0390e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4503,6 +4503,7 @@ dependencies = [ "parking_lot", "scopeguard", "serde", + "tl-proto", "tokio", "tokio-util", "tracing", @@ -4510,8 +4511,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 69c0ab8e28..f167b88b5c 100644 --- a/cli/src/node/mod.rs +++ b/cli/src/node/mod.rs @@ -226,8 +226,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 0564982c9f83f835b06343e3f7ba9e7c94227a78 Mon Sep 17 00:00:00 2001 From: Ivan Kalinin Date: Mon, 2 Feb 2026 14:43:21 +0100 Subject: [PATCH 07/31] 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 28b53ac91a..7d4c5e0d96 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 0ce93e05d107616aea0e23dca0353f4779a94c9e Mon Sep 17 00:00:00 2001 From: MrWad3r Date: Tue, 24 Feb 2026 20:00:59 +0100 Subject: [PATCH 08/31] chore(core): change ValidationSessionId signature --- collator/src/manager/state_update_handler.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/state_update_handler.rs b/collator/src/manager/state_update_handler.rs index 840906a0b2..e572cfeb74 100644 --- a/collator/src/manager/state_update_handler.rs +++ b/collator/src/manager/state_update_handler.rs @@ -181,7 +181,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()?; @@ -203,11 +205,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 @@ -262,7 +264,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 { @@ -302,7 +305,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))), ); } @@ -313,7 +316,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 54bdb52a11..ad22cb9623 100644 --- a/collator/src/types.rs +++ b/collator/src/types.rs @@ -364,39 +364,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 { @@ -411,7 +420,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 fd6473f27f57861b6a8d352a9ce4b29fcdd42673 Mon Sep 17 00:00:00 2001 From: MrWad3r Date: Tue, 10 Mar 2026 18:41:12 +0100 Subject: [PATCH 09/31] 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 ad22cb9623..b32a4b847f 100644 --- a/collator/src/types.rs +++ b/collator/src/types.rs @@ -398,7 +398,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 bacd043558a897d8afc48be5492f515ff573ee98 Mon Sep 17 00:00:00 2001 From: MrWad3r Date: Wed, 11 Mar 2026 13:16:55 +0100 Subject: [PATCH 10/31] 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 2d6d18f085a14296edfc0f6376f4d152879b1002 Mon Sep 17 00:00:00 2001 From: MrWad3r Date: Fri, 20 Mar 2026 14:26:19 +0100 Subject: [PATCH 11/31] chore(contract): bump dependencies --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index 369770ee19..2538e808c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -182,6 +182,7 @@ tycho-wu-tuner = { path = "./wu-tuner", version = "0.3.10" } [patch.crates-io] # patches here +tycho-types = { git = "https://github.com/broxus/tycho-types.git", rev = "ce1f6fb7e755f7de1d9df612b2417e9155be9e7e" } [workspace.lints.rust] future_incompatible = "warn" From 11d7d5bf64137d8075e8c9331a4a14c626004202 Mon Sep 17 00:00:00 2001 From: MrWad3r Date: Tue, 17 Mar 2026 12:19:11 +0100 Subject: [PATCH 12/31] 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 2f05425766..8d659466d0 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 2527f614a07fba75ee06d4c51d89cc1e5e7500a0 Mon Sep 17 00:00:00 2001 From: MrWad3r Date: Tue, 17 Mar 2026 13:00:13 +0100 Subject: [PATCH 13/31] 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 1d2f432688042ea69582641b8f9b4994b0ae20f0 Mon Sep 17 00:00:00 2001 From: MrWad3r Date: Tue, 17 Mar 2026 16:17:32 +0100 Subject: [PATCH 14/31] 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 afae077efc0457bc954c5f32a154a3b293a43b91 Mon Sep 17 00:00:00 2001 From: MrWad3r Date: Thu, 26 Mar 2026 11:40:38 +0100 Subject: [PATCH 15/31] 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 8d659466d0..d73feca3ea 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 6d20b082fd11324927cb20c1dd4f5e652136efdb Mon Sep 17 00:00:00 2001 From: MrWad3r Date: Fri, 27 Mar 2026 16:31:39 +0100 Subject: [PATCH 16/31] 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 3fd4f0390e..d3d7ac12c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4560,8 +4560,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", @@ -4594,8 +4593,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", @@ -4606,8 +4604,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 2538e808c3..87e18e8b58 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -182,7 +182,7 @@ tycho-wu-tuner = { path = "./wu-tuner", version = "0.3.10" } [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 1150fe317d3504fb652c99c696d14a893326c184 Mon Sep 17 00:00:00 2001 From: Kirill Mikheev Date: Wed, 8 Apr 2026 23:08:06 +0300 Subject: [PATCH 17/31] fix(collator): shuffle with vset_switch_round --- block-util/src/block/block_proof_stuff.rs | 15 ++++++++------- collator/src/manager/state_update_handler.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/state_update_handler.rs b/collator/src/manager/state_update_handler.rs index e572cfeb74..585bfea0e6 100644 --- a/collator/src/manager/state_update_handler.rs +++ b/collator/src/manager/state_update_handler.rs @@ -205,11 +205,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 @@ -243,7 +243,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 f42003dafdd4e71aafd5fc8a19c067daf9e017da Mon Sep 17 00:00:00 2001 From: Ivan Kalinin Date: Wed, 13 May 2026 14:32:19 +0200 Subject: [PATCH 18/31] feat(slasher): only update slasher state on config change --- Cargo.lock | 1 + cli/Cargo.toml | 1 + cli/src/node/mod.rs | 14 +- collator/src/collator/do_collate/finalize.rs | 2 +- slasher-traits/src/validator.rs | 31 ++- slasher/src/bc/mod.rs | 40 +--- slasher/src/collector/validator_events.rs | 45 +--- slasher/src/lib.rs | 228 +++++++++---------- slasher/src/tracing_targets.rs | 1 - 9 files changed, 166 insertions(+), 197 deletions(-) delete mode 100644 slasher/src/tracing_targets.rs diff --git a/Cargo.lock b/Cargo.lock index d3d7ac12c8..2667d35ae3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4152,6 +4152,7 @@ dependencies = [ "tycho-network", "tycho-rpc", "tycho-slasher", + "tycho-slasher-traits", "tycho-storage", "tycho-types", "tycho-util", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 3c17bb4274..dbde1ff2fc 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -67,6 +67,7 @@ tycho-core = { workspace = true, features = ["cli"] } tycho-network = { workspace = true } tycho-rpc = { workspace = true, features = ["http2"] } tycho-slasher = { workspace = true } +tycho-slasher-traits = { workspace = true } tycho-storage = { workspace = true } tycho-util = { workspace = true, features = ["cli"] } tycho-wu-tuner = { workspace = true } diff --git a/cli/src/node/mod.rs b/cli/src/node/mod.rs index f167b88b5c..23b4f5a49a 100644 --- a/cli/src/node/mod.rs +++ b/cli/src/node/mod.rs @@ -178,9 +178,9 @@ impl Node { .await .context("failed to load mc zerostate on run")?; - { - let config = mc_state.config_params()?; - let current_validator_set = config.get_current_validator_set()?; + let blockchain_config = mc_state.config_params()?; + let validator_session_id = { + let current_validator_set = blockchain_config.get_current_validator_set()?; base.validator_resolver() .update_validator_set(¤t_validator_set); let v_set_len = current_validator_set.list.len(); @@ -188,7 +188,9 @@ impl Node { is_single_node == (v_set_len == 1), "cannot start with v_set_len={v_set_len} and single_node={is_single_node}" ); - } + + tycho_slasher_traits::ValidationSessionId::from(mc_state.state_extra()?) + }; // Create mempool adapter let mempool_adapter = self.rpc_mempool_adapter.inner.clone(); @@ -221,13 +223,15 @@ impl Node { let message_queue_adapter = MessageQueueAdapterStdImpl::new(queue); message_queue_adapter.recover_after_restart(&mc_state)?; - // NOTE: Stub let slasher = tycho_slasher::Slasher::new( base.keypair.clone(), tycho_slasher::StubSlasherContract, base.blockchain_rpc_client.clone(), &base.storage_context, self.slasher_config, + mc_state.as_ref().global_id, + blockchain_config, + validator_session_id, ) .context("failed to create slasher")?; diff --git a/collator/src/collator/do_collate/finalize.rs b/collator/src/collator/do_collate/finalize.rs index d73feca3ea..efc1ba585c 100644 --- a/collator/src/collator/do_collate/finalize.rs +++ b/collator/src/collator/do_collate/finalize.rs @@ -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 = {}, catchain_seqno = {catchain_seqno})", ShardIdent::MASTERCHAIN, ); }; diff --git a/slasher-traits/src/validator.rs b/slasher-traits/src/validator.rs index 63fb9494ab..74cf94376d 100644 --- a/slasher-traits/src/validator.rs +++ b/slasher-traits/src/validator.rs @@ -3,7 +3,7 @@ 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, IndexedValidatorDescription, McStateExtra}; use tycho_util::FastHasherState; // TODO: Decide how to be with this collator-defined type @@ -13,19 +13,17 @@ pub struct ValidationSessionId { pub vset_switch_round: u32, } -// TEMP -impl From<(u32, u32)> for ValidationSessionId { - #[inline] - fn from(value: (u32, u32)) -> Self { - Self { - // seqno: value.0, - catchain_seqno: value.0, - vset_switch_round: value.1, +impl From<&McStateExtra> for ValidationSessionId { + fn from(value: &McStateExtra) -> Self { + let catchain_seqno = value.validator_info.catchain_seqno; + let vset_switch_round = value.consensus_info.vset_switch_round; + ValidationSessionId { + vset_switch_round, + catchain_seqno, } } } -// TEMP impl Ord for ValidationSessionId { #[inline] fn cmp(&self, other: &Self) -> std::cmp::Ordering { @@ -34,7 +32,6 @@ impl Ord for ValidationSessionId { } } -// TEMP impl PartialOrd for ValidationSessionId { #[inline] fn partial_cmp(&self, other: &Self) -> Option { @@ -42,6 +39,18 @@ impl PartialOrd for ValidationSessionId { } } +// TEMP +impl From<(u32, u32)> for ValidationSessionId { + #[inline] + fn from(value: (u32, u32)) -> Self { + Self { + // seqno: value.0, + catchain_seqno: value.0, + vset_switch_round: value.1, + } + } +} + pub struct ValidatorEvents { listener: Arc, } diff --git a/slasher/src/bc/mod.rs b/slasher/src/bc/mod.rs index d424d2dde4..069f1d2958 100644 --- a/slasher/src/bc/mod.rs +++ b/slasher/src/bc/mod.rs @@ -12,7 +12,6 @@ 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; @@ -75,7 +74,7 @@ impl ContractSubscription { &self, msg_hash: &HashBytes, expire_at: u32, - ) -> Result> { + ) -> Result> { use dashmap::mapref::entry::Entry; let (tx, rx) = oneshot::channel(); @@ -99,48 +98,33 @@ impl ContractSubscription { 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(); + pending.tx.send(MessageDelivered { tx_hash: *tx_hash }).ok(); return Ok(true); } Ok(false) } pub fn cleanup_expired_messages(&self, now_sec: u32) { - 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(); - } - } - + 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!( - target: tracing_targets::SLASHER, - dropped, - "dropped pending messages" - ); + tracing::warn!(dropped, "dropped pending messages"); } } } struct PendingMessage { expire_at: u32, - tx: oneshot::Sender, + tx: oneshot::Sender, } #[derive(Debug, Clone, Copy)] -pub enum MessageDeliveryStatus { - Sent { tx_hash: HashBytes }, - Expired, +pub struct MessageDelivered { + pub tx_hash: HashBytes, } // TODO: Add mempool batches or votes here diff --git a/slasher/src/collector/validator_events.rs b/slasher/src/collector/validator_events.rs index 0e742bf8dc..b8859a862d 100644 --- a/slasher/src/collector/validator_events.rs +++ b/slasher/src/collector/validator_events.rs @@ -12,7 +12,6 @@ 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; @@ -79,7 +78,6 @@ impl ValidatorEventsCollector { && let Some(info) = items.pop_front() { tracing::warn!( - target: tracing_targets::SLASHER, session_id = ?info.session_id, "session info dropped from init queue" ); @@ -132,11 +130,7 @@ impl ValidatorEventsListener for ValidatorEventsCollector { own_validator_idx: u16, validators: &[IndexedValidatorDescription], ) { - tracing::debug!( - target: tracing_targets::SLASHER, - first_mc_seqno, - "on_session_started" - ); + tracing::debug!(first_mc_seqno, "on_session_started"); let validator_indices = validators .iter() @@ -166,20 +160,17 @@ impl ValidatorEventsListener for ValidatorEventsCollector { validators, }); } else { - tracing::warn!(target: tracing_targets::SLASHER, "duplicate session"); + tracing::warn!("duplicate session"); } } #[instrument(skip_all, fields(session_id = ?session_id))] fn on_session_finished(&self, session_id: ValidationSessionId) { - tracing::debug!(target: tracing_targets::SLASHER, "on_session_finished"); + tracing::debug!("on_session_finished"); if let Some((_, session)) = self.sessions.remove(&session_id) && let Err(e) = session.commit_final_batch() { - tracing::warn!( - target: tracing_targets::SLASHER, - "failed to commit blocks batch on finish: {e:?}" - ); + tracing::warn!("failed to commit blocks batch on finish: {e:?}"); } } @@ -195,16 +186,9 @@ impl ValidatorEventsListener for ValidatorEventsCollector { return; } - tracing::debug!( - target: tracing_targets::SLASHER, - %block_id, - "on_block_validated" - ); + tracing::debug!(%block_id, "on_block_validated"); let Some(mut session) = self.sessions.get_mut(&session_id) else { - tracing::warn!( - target: tracing_targets::SLASHER, - "session not found, ignoring on_block_validated event" - ); + tracing::warn!("session not found, ignoring on_block_validated event"); return; }; session.handle_block(block_id.seqno, Some(signatures.as_ref())); @@ -217,16 +201,9 @@ impl ValidatorEventsListener for ValidatorEventsCollector { return; } - tracing::debug!( - target: tracing_targets::SLASHER, - %block_id, - "on_block_skipped" - ); + tracing::debug!(%block_id, "on_block_skipped"); let Some(mut session) = self.sessions.get_mut(&session_id) else { - tracing::warn!( - target: tracing_targets::SLASHER, - "session not found, ignoring on_block_skipped event" - ); + tracing::warn!("session not found, ignoring on_block_skipped event"); return; }; session.handle_block(block_id.seqno, None); @@ -270,11 +247,7 @@ impl SessionState { 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:?}" - ); + tracing::error!(event_type, "failed to commit blocks batch: {e:?}"); } true } diff --git a/slasher/src/lib.rs b/slasher/src/lib.rs index f8a6d69e9b..f192bca507 100644 --- a/slasher/src/lib.rs +++ b/slasher/src/lib.rs @@ -15,7 +15,7 @@ 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_types::models::{BlockchainConfig, SignatureContext, StdAddr}; use tycho_util::config::PartialConfig; use tycho_util::futures::JoinTask; use tycho_util::serde_helpers; @@ -23,8 +23,9 @@ use tycho_util::serde_helpers; pub use self::analyzer::{ SessionPenaltyReport, SessionValidatorScore, VsetPenaltyReport, VsetValidatorPenalty, }; +use self::bc::SlasherParams; pub use self::bc::{ - BlocksBatch, ContractSubscription, EncodeBlocksBatchMessage, MessageDeliveryStatus, + BlocksBatch, ContractSubscription, EncodeBlocksBatchMessage, MessageDelivered, SignatureHistory, SignedMessage, SlasherContract, StubSlasherContract, }; use self::collector::{ValidatorEventsCollector, ValidatorSessionInfo}; @@ -41,7 +42,6 @@ pub mod collector { mod bc; mod storage; -mod tracing_targets; mod util; #[derive(Debug, Clone, Serialize, Deserialize, PartialConfig)] @@ -88,17 +88,39 @@ pub struct Slasher { } impl Slasher { + #[allow(clippy::too_many_arguments)] pub fn new( node_keys: Arc, contract: C, blockchain_rpc_client: BlockchainRpcClient, storage_context: &StorageContext, config: SlasherConfig, + global_id: i32, + blockchain_config: &BlockchainConfig, + known_session_id: ValidationSessionId, ) -> Result { let storage = SlasherStorage::open(storage_context).context("failed to open slasher storage")?; - let collector = Arc::new(ValidatorEventsCollector::new(contract.default_batch_size())); + let slasher_params = contract + .find_params(blockchain_config) + .context("failed to find slasher params")?; + + let subscription = match &slasher_params { + Some(slasher_params) => { + let slasher_address = StdAddr::new_masterchain(slasher_params.address); + tracing::info!(%slasher_address, ?slasher_params, "slasher initialized"); + Some(Arc::new(ContractSubscription::new(&slasher_address))) + } + None => None, + }; + + let collector = Arc::new(ValidatorEventsCollector::new( + slasher_params + .as_ref() + .map_or(contract.default_batch_size(), |p| p.blocks_batch_size), + )); + let global = blockchain_config.get_global_version()?; Ok(Self { validator_events_collector: collector, @@ -106,16 +128,16 @@ impl Slasher { config, node_keys, contract: Box::new(contract), - subscription: ArcSwapOption::empty(), + subscription: ArcSwapOption::new(subscription), blockchain_rpc_client, storage, - known_session_id: AtomicValidationSessionId::new(ValidationSessionId { - vset_switch_round: 0, - catchain_seqno: 0, - }), - signature_context: ArcSwap::new(Arc::new(SignatureContext { - global_id: 0, - capabilities: Default::default(), + known_session_id: AtomicValidationSessionId::new(known_session_id), + parsed_config: ArcSwap::new(Arc::new(ParsedConfig { + signature_context: SignatureContext { + global_id, + capabilities: global.capabilities, + }, + slasher_params, })), }), cancellation_token: Default::default(), @@ -135,50 +157,74 @@ impl Slasher { let this = self.shared.as_ref(); 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, + // Apply config changes when needed. + if state_extra.after_key_block { + let global = state_extra.config.get_global_version()?; + let slasher_params = this + .contract + .find_params(&state_extra.config) + .context("failed to find slasher params")?; + + if let Some(slasher_params) = &slasher_params { + self.validator_events_collector + .set_default_batch_size(slasher_params.blocks_batch_size); + } + + let slasher_address = slasher_params + .as_ref() + .map(|p| StdAddr::new_masterchain(p.address)); + + // Update parsed config. + self.shared.parsed_config.store(Arc::new(ParsedConfig { + signature_context: SignatureContext { + global_id: cx.block.as_ref().global_id, + capabilities: global.capabilities, + }, + slasher_params, })); - // Check config updates (TODO: do it only when config changes) - let Some(slasher_params) = this - .contract - .find_params(&state_extra.config) - .context("failed to find slasher params")? - else { + // Update subscription if changed. + match (this.subscription.load_full(), &slasher_address) { + // Slasher has been disabled. + (_subscription, None) => { + // TODO: Notify subscription that it is no longer needed. + this.subscription.store(None); + } + // Slasher address unchanged. + (Some(s), Some(slasher_address)) if s.address() == slasher_address => {} + // Slasher address has changed. + (_, Some(slasher_address)) => { + tracing::info!(%slasher_address, "slasher address changed"); + this.subscription + .store(Some(Arc::new(ContractSubscription::new(slasher_address)))); + } + } + } + + // Prepare slasher handler context. + let Some(slasher_params) = this.parsed_config.load().slasher_params.clone() else { + // Slasher disabled. return Ok(()); }; - self.validator_events_collector - .set_default_batch_size(slasher_params.blocks_batch_size); - let slasher_address = StdAddr::new_masterchain(slasher_params.address); - - let catchain_seqno = state_extra.validator_info.catchain_seqno; - let vset_switch_round = state_extra.consensus_info.vset_switch_round; - - let current_session_id = ValidationSessionId { - vset_switch_round, - catchain_seqno, + let Some(subscription) = this.subscription.load_full() else { + return Ok(()); }; + + let current_session_id = ValidationSessionId::from(state_extra); 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_vset_hash = %current_vset_hash, + %current_vset_hash, ); - // TODO: Add metrics. if current_session_id != this.known_session_id.load() { + // TODO: Add metrics. tracing::info!( - target: tracing_targets::SLASHER, old_session_id = ?this.known_session_id.load(), ?current_session_id, "slasher observed validation session change", @@ -188,45 +234,29 @@ impl Slasher { this.storage .update_current_vset_epoch(current_session_id, current_vset_hash)?; - // Handle subscription - let subscription = match this.subscription.load_full() { - Some(s) if s.address() == &slasher_address => s, - _ => { - 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 - } - }; - + // Update subscription state. 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)? { + if let Some((_, account_block)) = extra.get(slasher_params.address)? { for entry in account_block.transactions.iter() { let (_, _, tx) = entry?; let tx_hash = tx.repr_hash(); 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", ); - let matched_own_message = subscription.handle_account_transaction(tx_hash, &tx)?; + let 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, @@ -235,12 +265,8 @@ impl Slasher { 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" - } + is_own = own_message, + "blocks batch submitted", ); // TODO: Move into blocking. @@ -250,7 +276,6 @@ impl Slasher { &submitted.blocks_batch, )? { tracing::warn!( - target: tracing_targets::SLASHER, session_id = ?submitted.session_id, current_vset_hash = %current_vset_hash, "ignoring observed blocks batch without known epoch" @@ -261,7 +286,6 @@ impl Slasher { }, Ok(None) => {} Err(e) => tracing::warn!( - target: tracing_targets::SLASHER, %tx_hash, "failed to parse slasher event: {e:?}" ), @@ -269,24 +293,18 @@ impl Slasher { } } + // Trigger reporting. self.shared.analyze_closed_vset_epochs()?; + // Start session handlers. while let Some(session_info) = self .validator_events_collector .pop_session_to_init(mc_seqno) { let session_id = session_info.session_id; - tracing::info!( - target: tracing_targets::SLASHER, - ?session_id, - "found session to init" - ); + tracing::info!(?session_id, "found session to init"); if !session_info.can_participate(&this.node_keys.public_key) { - tracing::info!( - target: tracing_targets::SLASHER, - ?session_id, - "skipping session" - ); + tracing::info!(?session_id, "skipping session"); continue; } @@ -296,11 +314,7 @@ impl Slasher { slasher_params.blocks_batch_size, tx, ) { - tracing::warn!( - target: tracing_targets::SLASHER, - ?session_id, - "session removed before init" - ); + tracing::warn!(?session_id, "session removed before init"); continue; } @@ -338,7 +352,12 @@ struct SlasherSharedState { blockchain_rpc_client: BlockchainRpcClient, storage: SlasherStorage, known_session_id: AtomicValidationSessionId, - signature_context: ArcSwap, + parsed_config: ArcSwap, +} + +struct ParsedConfig { + signature_context: SignatureContext, + slasher_params: Option, } impl SlasherSharedState { @@ -346,10 +365,7 @@ impl SlasherSharedState { let snapshot = self.storage.snapshot(); 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" - ); + tracing::warn!("closes vset epoches not found"); return Ok(()); } @@ -359,7 +375,6 @@ impl SlasherSharedState { } tracing::info!( - target: tracing_targets::SLASHER, vset_hash = ?epoch.vset_hash, start_id = ?epoch.start_session_id, "analyzing closed vset epoch" @@ -396,7 +411,6 @@ impl SlasherSharedState { 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, @@ -418,7 +432,6 @@ impl SlasherSharedState { .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, @@ -427,7 +440,6 @@ impl SlasherSharedState { 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, @@ -445,8 +457,8 @@ impl SlasherSharedState { info: ValidatorSessionInfo, mut rx: collector::BlocksBatchRx, ) { - tracing::info!(target: tracing_targets::SLASHER, "started"); - scopeguard::defer!(tracing::info!(target: tracing_targets::SLASHER, "finished")); + tracing::info!("started"); + scopeguard::defer!(tracing::info!("finished")); let mut send_task = None; @@ -455,10 +467,7 @@ impl SlasherSharedState { && let Some(timeout) = self.config.prev_delivery_timeout && tokio::time::timeout(timeout, send_task).await.is_err() { - tracing::warn!( - target: tracing_targets::SLASHER, - "timeout on waiting for the previous batch to be delivered" - ); + tracing::warn!("timeout on waiting for the previous batch to be delivered"); } send_task = Some(JoinTask::new(self.clone().deliver_batch_message( @@ -477,16 +486,17 @@ impl SlasherSharedState { ) { loop { let Some(subscription) = self.subscription.load_full() else { - tracing::warn!(target: tracing_targets::SLASHER, "no slasher contract subscription"); + tracing::warn!("no slasher contract subscription"); break; }; + let signature_context = self.parsed_config.load().signature_context; let params = EncodeBlocksBatchMessage { address: subscription.address(), session_id, batch: &batch, validator_idx, - signature_context: **self.signature_context.load(), + signature_context, keypair: &self.node_keys, ttl: self.config.message_ttl, }; @@ -494,10 +504,7 @@ impl SlasherSharedState { let signed = match self.contract.encode_blocks_batch_message(¶ms) { Ok(signed) => signed, Err(e) => { - tracing::error!( - target: tracing_targets::SLASHER, - "failed to encode batch message: {e:?}" - ); + tracing::error!("failed to encode batch message: {e:?}"); return; } }; @@ -507,7 +514,6 @@ 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, @@ -525,9 +531,8 @@ impl SlasherSharedState { drop(boc); match res.await { - Ok(MessageDeliveryStatus::Sent { tx_hash }) => { + Ok(MessageDelivered { tx_hash }) => { tracing::info!( - target: tracing_targets::SLASHER, %tx_hash, session_id = ?params.session_id, validator_idx = params.validator_idx, @@ -540,20 +545,13 @@ impl SlasherSharedState { ); return; } - Ok(MessageDeliveryStatus::Expired) => { + Err(_) => { // TODO: Execute transaction locally to guess the reason. - tracing::warn!( - target: tracing_targets::SLASHER, - "batch message expired" - ); + tracing::warn!("batch message expired"); } - Err(_) => return, } } - Err(e) => tracing::warn!( - target: tracing_targets::SLASHER, - "failed to track message: {e:?}" - ), + Err(e) => tracing::warn!("failed to track message: {e:?}"), } tokio::time::sleep(self.config.message_retry_interval).await; diff --git a/slasher/src/tracing_targets.rs b/slasher/src/tracing_targets.rs deleted file mode 100644 index afd2daf608..0000000000 --- a/slasher/src/tracing_targets.rs +++ /dev/null @@ -1 +0,0 @@ -pub const SLASHER: &str = "slasher"; From a59f19874da29150ca9119a688f068074917aaf4 Mon Sep 17 00:00:00 2001 From: Ivan Kalinin Date: Fri, 15 May 2026 11:59:43 +0200 Subject: [PATCH 19/31] wip: simplify slasher storage --- cli/src/node/mod.rs | 6 +- contracts/src/slasher-stub.tolk | 2 - slasher/src/bc/mod.rs | 1 - slasher/src/bc/stub_contract.rs | 10 - slasher/src/lib.rs | 190 ++++++-------- slasher/src/proto.tl | 81 +----- slasher/src/storage/db.rs | 84 +----- slasher/src/storage/mod.rs | 451 ++++++++------------------------ slasher/src/storage/models.rs | 308 +--------------------- 9 files changed, 219 insertions(+), 914 deletions(-) diff --git a/cli/src/node/mod.rs b/cli/src/node/mod.rs index 23b4f5a49a..05b009ce79 100644 --- a/cli/src/node/mod.rs +++ b/cli/src/node/mod.rs @@ -178,9 +178,9 @@ impl Node { .await .context("failed to load mc zerostate on run")?; - let blockchain_config = mc_state.config_params()?; + let config = mc_state.config_params()?; let validator_session_id = { - let current_validator_set = blockchain_config.get_current_validator_set()?; + let current_validator_set = config.get_current_validator_set()?; base.validator_resolver() .update_validator_set(¤t_validator_set); let v_set_len = current_validator_set.list.len(); @@ -230,7 +230,7 @@ impl Node { &base.storage_context, self.slasher_config, mc_state.as_ref().global_id, - blockchain_config, + config, validator_session_id, ) .context("failed to create slasher")?; diff --git a/contracts/src/slasher-stub.tolk b/contracts/src/slasher-stub.tolk index f0a965ac58..6c363d7700 100644 --- a/contracts/src/slasher-stub.tolk +++ b/contracts/src/slasher-stub.tolk @@ -63,8 +63,6 @@ fun onExternalMessage(inMsg: slice) { 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/slasher/src/bc/mod.rs b/slasher/src/bc/mod.rs index 069f1d2958..b19414707b 100644 --- a/slasher/src/bc/mod.rs +++ b/slasher/src/bc/mod.rs @@ -135,7 +135,6 @@ pub enum SlasherContractEvent { #[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 e3c771fc0c..ebea4f3224 100644 --- a/slasher/src/bc/stub_contract.rs +++ b/slasher/src/bc/stub_contract.rs @@ -1,7 +1,6 @@ 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::{ @@ -64,8 +63,6 @@ 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()? @@ -121,12 +118,6 @@ 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)?; @@ -136,7 +127,6 @@ 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 f192bca507..c812e07200 100644 --- a/slasher/src/lib.rs +++ b/slasher/src/lib.rs @@ -1,3 +1,4 @@ +use std::collections::BTreeMap; use std::sync::Arc; use std::time::Duration; @@ -15,6 +16,7 @@ use tycho_crypto::ed25519; use tycho_slasher_traits::{ValidationSessionId, ValidatorEventsListener}; use tycho_storage::StorageContext; use tycho_types::boc::Boc; +use tycho_types::cell::HashBytes; use tycho_types::models::{BlockchainConfig, SignatureContext, StdAddr}; use tycho_util::config::PartialConfig; use tycho_util::futures::JoinTask; @@ -102,6 +104,10 @@ impl Slasher { let storage = SlasherStorage::open(storage_context).context("failed to open slasher storage")?; + let current_vset_hash = *blockchain_config + .get_current_validator_set_raw()? + .repr_hash(); + let slasher_params = contract .find_params(blockchain_config) .context("failed to find slasher params")?; @@ -122,6 +128,8 @@ impl Slasher { )); let global = blockchain_config.get_global_version()?; + // TODO: Spawn previous unsubmitted reports. + Ok(Self { validator_events_collector: collector, shared: Arc::new(SlasherSharedState { @@ -133,6 +141,7 @@ impl Slasher { storage, known_session_id: AtomicValidationSessionId::new(known_session_id), parsed_config: ArcSwap::new(Arc::new(ParsedConfig { + current_vset_hash, signature_context: SignatureContext { global_id, capabilities: global.capabilities, @@ -157,8 +166,19 @@ impl Slasher { let this = self.shared.as_ref(); let state_extra = cx.state.state_extra()?; + let current_vset_hash = *state_extra + .config + .get_current_validator_set_raw()? + .repr_hash(); + // Apply config changes when needed. + let mut vset_to_complete = None; if state_extra.after_key_block { + let known_vset_hash = self.shared.parsed_config.load().current_vset_hash; + if current_vset_hash != known_vset_hash { + vset_to_complete = Some(known_vset_hash); + } + let global = state_extra.config.get_global_version()?; let slasher_params = this .contract @@ -176,6 +196,7 @@ impl Slasher { // Update parsed config. self.shared.parsed_config.store(Arc::new(ParsedConfig { + current_vset_hash, signature_context: SignatureContext { global_id: cx.block.as_ref().global_id, capabilities: global.capabilities, @@ -201,6 +222,23 @@ impl Slasher { } } + // Sync session id. + let current_session_id = ValidationSessionId::from(state_extra); + let session_changed = current_session_id != this.known_session_id.load(); + if session_changed { + // TODO: Add metrics. + 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); + this.storage + .store_vset_session(current_session_id, ¤t_vset_hash, mc_seqno); + } + + // TODO: Cleanup stored vset sessions. + // Prepare slasher handler context. let Some(slasher_params) = this.parsed_config.load().slasher_params.clone() else { // Slasher disabled. @@ -210,33 +248,13 @@ impl Slasher { return Ok(()); }; - let current_session_id = ValidationSessionId::from(state_extra); - let current_vset_hash = *state_extra - .config - .get_current_validator_set_raw()? - .repr_hash(); - tracing::trace!( ?slasher_params, ?current_session_id, %current_vset_hash, ); - if current_session_id != this.known_session_id.load() { - // TODO: Add metrics. - 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); - } - this.storage - .update_current_vset_epoch(current_session_id, current_vset_hash)?; - - // Update subscription state. - subscription.cleanup_expired_messages(cx.block.load_info()?.gen_utime); - + // TODO: Move into blocking. let extra = cx.block.load_extra()?.account_blocks.load()?; if let Some((_, account_block)) = extra.get(slasher_params.address)? { for entry in account_block.transactions.iter() { @@ -258,7 +276,7 @@ impl Slasher { let batch = &submitted.blocks_batch; tracing::info!( %tx_hash, - session_id = ?submitted.session_id, + %current_vset_hash, validator_idx = submitted.validator_idx, batch_start_seqno = batch.start_seqno(), batch_seqno_after = batch.seqno_after(), @@ -269,18 +287,11 @@ impl Slasher { "blocks batch submitted", ); - // TODO: Move into blocking. - if !this.storage.store_blocks_batch( - submitted.session_id, + this.storage.store_blocks_batch( + ¤t_vset_hash, submitted.validator_idx, &submitted.blocks_batch, - )? { - tracing::warn!( - session_id = ?submitted.session_id, - current_vset_hash = %current_vset_hash, - "ignoring observed blocks batch without known epoch" - ); - } + )?; tokio::task::yield_now().await; } }, @@ -293,8 +304,16 @@ impl Slasher { } } + // Update subscription state. + subscription.cleanup_expired_messages(cx.block.load_info()?.gen_utime); + // Trigger reporting. - self.shared.analyze_closed_vset_epochs()?; + if let Some(vset_hash) = vset_to_complete + && let Some(last_seqno) = mc_seqno.checked_sub(1) + { + self.shared + .complete_vset(&vset_hash, last_seqno, &slasher_params)?; + } // Start session handlers. while let Some(session_info) = self @@ -356,101 +375,48 @@ struct SlasherSharedState { } struct ParsedConfig { + current_vset_hash: HashBytes, signature_context: SignatureContext, slasher_params: Option, } impl SlasherSharedState { - fn analyze_closed_vset_epochs(&self) -> Result<()> { + fn complete_vset( + &self, + vset_hash: &HashBytes, + last_seqno: u32, + _params: &SlasherParams, + ) -> Result<()> { + // Session should containt at least this amount of blocks to do some reporting. + // TODO: Move into `SlasherParams`. + const MIN_VSET_LENGTH: u32 = 1000; + let snapshot = self.storage.snapshot(); - let closed_vset_epoches = snapshot.load_closed_vset_epochs()?; - if closed_vset_epoches.is_empty() { - tracing::warn!("closes vset epoches not found"); + + // Compute vset block range. + let session_ids = snapshot.load_vset_sessions(vset_hash)?; + let start_seqno = session_ids + .iter() + .map(|item| item.start_seqno) + .min() + .unwrap_or(u32::MAX); + let vset_len = last_seqno.saturating_sub(start_seqno); + if vset_len < MIN_VSET_LENGTH { + tracing::warn!(%vset_hash, vset_len, "too short vset"); return Ok(()); } - for epoch in closed_vset_epoches { - if snapshot.load_vset_report(epoch.start_session_id)?.is_some() { - continue; - } - - tracing::info!( - vset_hash = ?epoch.vset_hash, - start_id = ?epoch.start_session_id, - "analyzing closed vset epoch" - ); + // Build a matrix from all known block batches. - 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 - } - }; - Self::log_session_report(&report); - session_reports.push(report); - } + for item in snapshot.iter_block_batches(vset_hash) { + let (validator_idx, batch) = item?; - let report = analyzer::analyze_vset_epoch( - &epoch, - &session_reports, - self.config.bad_sessions_weight_threshold, - ); - self.storage.store_vset_report(&report)?; - Self::log_vset_report(&report); + // todo } Ok(()) } - fn log_session_report(report: &SessionPenaltyReport) { - for item in &report.validators { - tracing::info!( - 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!( - 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!( - 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/proto.tl b/slasher/src/proto.tl index 8aff1ec547..c8cd713665 100644 --- a/slasher/src/proto.tl +++ b/slasher/src/proto.tl @@ -20,80 +20,17 @@ slasher.signatureHistory bits:bitset = slasher.SignatureHistory; -/** -* @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 - 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 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.sessionValidatorScore +slasher.report + pubkey:int256 validator_idx:int - 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; + entries:(vector slasher.ReportEntry) + = slasher.Report; -/** -* @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 +slasher.reportEntry 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; + good_score:int + bad_score:int + punish:bool + = slasher.ReportEntry; bitset length:int data:bytes = BitSet; diff --git a/slasher/src/storage/db.rs b/slasher/src/storage/db.rs index 542d1f313a..7cfb03b5c9 100644 --- a/slasher/src/storage/db.rs +++ b/slasher/src/storage/db.rs @@ -30,11 +30,8 @@ impl WithMigrations for SlasherTables { weedb::tables! { pub struct SlasherTables { pub state: tables::State, - pub vset_epochs: tables::VsetEpochs, - pub session_meta: tables::SessionMeta, + pub vset_sessions: tables::VsetSessions, pub block_batches: tables::BlockBatches, - pub session_reports: tables::SessionReports, - pub vset_reports: tables::VsetReports, } } @@ -62,53 +59,34 @@ pub mod tables { } } - /// Stores validator-set epochs keyed by their start validation session. - pub struct VsetEpochs; + /// Maps validator sessions to vset. + /// + /// - Key: `session_id: (catchain_seqno u32 BE, vset_switch_round u32 BE)` + /// - Value: `vset_hash: 32 bytes, start_seqno: u32 LE` + pub struct VsetSessions; - impl VsetEpochs { + impl VsetSessions { pub const KEY_LEN: usize = 4 + 4; + pub const VALUE_LEN: usize = 32 + 4; } - impl ColumnFamily for VsetEpochs { - const NAME: &'static str = "vset_epochs"; + impl ColumnFamily for VsetSessions { + const NAME: &'static str = "vset_sessions"; } - impl ColumnFamilyOptions for VsetEpochs { + impl ColumnFamilyOptions for VsetSessions { 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 session metadata grouped by epoch. - pub struct SessionMeta; - - impl SessionMeta { - pub const KEY_LEN: usize = VsetEpochs::KEY_LEN + 4 + 4; - } - - 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); - - opts.set_optimize_filters_for_hits(true); - optimize_for_point_lookup(opts, ctx); } } /// Block batches submitted by validators - /// - Key: `session_id: (catchain_seqno u32 BE, vset_switch_round u32 BE), validator_idx: u16 BE, start_block: u32 BE` + /// - Key: `vset_hash: 32 bytes, 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; + pub const KEY_LEN: usize = 32 + 2 + 4; } impl ColumnFamily for BlockBatches { @@ -120,40 +98,4 @@ 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 3c0c10167e..2d130bacb0 100644 --- a/slasher/src/storage/mod.rs +++ b/slasher/src/storage/mod.rs @@ -1,20 +1,14 @@ use std::sync::Arc; -use anyhow::{Context, Result}; +use anyhow::Result; use tycho_slasher_traits::ValidationSessionId; use tycho_storage::StorageContext; use tycho_types::cell::HashBytes; -use weedb::OwnedSnapshot; +use weedb::{OwnedSnapshot, rocksdb}; use self::db::{SlasherDb, tables}; -use self::models::{ - StoredBlocksBatch, StoredSessionMeta, StoredSessionPenaltyReport, StoredVsetEpoch, - StoredVsetPenaltyReport, -}; +use self::models::StoredBlocksBatch; use crate::BlocksBatch; -use crate::analyzer::{ - ObservedBlocksBatch, SessionMeta, SessionPenaltyReport, VsetEpoch, VsetPenaltyReport, -}; pub mod db; pub mod models; @@ -43,185 +37,28 @@ 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, - }) - } - // Cold start. Save first vset epoch - 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, - observer_validator_idx: u16, + vset_hash: &HashBytes, + validator_idx: u16, batch: &BlocksBatch, - ) -> 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)?; - - 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<()> { - 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 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, - epoch: &VsetEpoch, - session_id: &ValidationSessionId, ) -> Result<()> { - self.delete_session_report(*session_id)?; - self.delete_vset_report(epoch.start_session_id)?; + let key = block_batches_key(vset_hash, validator_idx, batch.start_seqno); + let value = tl_proto::serialize(StoredBlocksBatch::wrap(batch)); + self.inner.db.block_batches.insert(key, value)?; 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( + pub fn store_vset_session( &self, - cf: &impl weedb::rocksdb::AsColumnFamilyRef, - key: &[u8], - write_config: &weedb::rocksdb::WriteOptions, + session_id: ValidationSessionId, + vset_hash: &HashBytes, + start_seqno: u32, ) -> 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() - .raw_iterator_cf_opt(&cf, read_config); - iter.seek_to_last(); - - let epoch = match iter.item() { - Some((key, value)) => Some(parse_vset_epoch(key, value)?), - None => { - iter.status()?; - None - } - }; - Ok(epoch) - } - - 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) + let value = session_value(vset_hash, start_seqno); + self.inner.db.vset_sessions.insert(key, value)?; + Ok(()) } } @@ -236,196 +73,136 @@ pub struct SlasherStorageSnapshot { } impl SlasherStorageSnapshot { - 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_first(); - - 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_vset_sessions(&self, vset_hash: &HashBytes) -> Result> { + let mut iter = self.snapshot.raw_iterator_cf_opt( + &self.db.vset_sessions.cf(), + self.db.vset_sessions.new_read_config(), + ); + iter.seek_to_last(); - 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(prefix.as_slice()); - - let mut items = Vec::new(); - while let Some((key, value)) = iter.item() { - if &key[..tables::VsetEpochs::KEY_LEN] != prefix.as_slice() { + let mut result = Vec::new(); + let mut vset_seen = false; + loop { + let (key, value) = match iter.item() { + Some(item) => item, + None => { + iter.status()?; + break; + } + }; + + let session_vset_hash = HashBytes::from_slice(&value[0..32]); + if &session_vset_hash == vset_hash { + vset_seen = true; + let session_id = 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()), + }; + + let start_seqno = u32::from_le_bytes(value[32..36].try_into().unwrap()); + + result.push(VsetSession { + id: session_id, + start_seqno, + }); + } else if vset_seen { break; } - items.push(parse_session_meta(key, value)?); - iter.next(); + iter.prev(); } - iter.status()?; - Ok(items) + Ok(result) } - pub fn load_observed_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()); + pub fn iter_block_batches(&self, vset_hash: &HashBytes) -> BlockBatchesIter<'_> { + let mut readopts = self.db.block_batches.new_read_config(); + readopts.set_snapshot(&self.snapshot); + let mut key = block_batches_key(vset_hash, 0, 0); + readopts.set_iterate_lower_bound(key); + key[32..].fill(0xff); + readopts.set_iterate_upper_bound(key); - let cf = table.cf(); - let mut iter = self.db.rocksdb().raw_iterator_cf_opt(&cf, read_config); - iter.seek(prefix.as_slice()); + let mut raw = self + .db + .rocksdb() + .raw_iterator_cf_opt(&self.db.block_batches.cf(), readopts); + raw.seek_to_first(); - let mut items = Vec::new(); - while let Some((key, value)) = iter.item() { - if &key[..tables::SessionReports::KEY_LEN] != prefix.as_slice() { - break; - } + BlockBatchesIter { raw, broken: false } + } +} - let batch = tl_proto::deserialize::(value) - .context("failed to deserialize slasher blocks batch")? - .0; - items.push(ObservedBlocksBatch { - observer_validator_idx: parse_observer_validator_idx(key), - batch, - }); - iter.next(); - } - iter.status()?; +pub struct BlockBatchesIter<'a> { + raw: rocksdb::DBRawIterator<'a>, + broken: bool, +} - Ok(items) - } +impl Iterator for BlockBatchesIter<'_> { + type Item = Result<(u16, BlocksBatch)>; - 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()); + fn next(&mut self) -> Option { + if self.broken { + return None; + } - 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 (key, value) = match self.raw.item() { + Some(item) => item, + None => match self.raw.status() { + Ok(()) => return None, + Err(e) => { + self.broken = true; + return Some(Err(e.into())); + } + }, }; - let report = tl_proto::deserialize::(value.as_ref()) - .context("failed to deserialize slasher session report")? - .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 validator_idx = u16::from_be_bytes(key[32..34].try_into().unwrap()); + let batch = match tl_proto::deserialize(value) { + Ok(StoredBlocksBatch(batch)) => batch, + Err(e) => { + let start_seqno = u32::from_be_bytes(key[34..48].try_into().unwrap()); + self.broken = true; + return Some(Err(anyhow::anyhow!( + "invalid stored blocks batch \ + (validator_idx={validator_idx}, start_seqno={start_seqno}): {e:?}" + ))); + } }; - let report = tl_proto::deserialize::(value.as_ref()) - .context("failed to deserialize slasher vset report")? - .0; - Ok(Some(report)) + self.raw.next(); + Some(Ok((validator_idx, batch))) } } -fn session_key(session_id: ValidationSessionId) -> [u8; tables::SessionReports::KEY_LEN] { - let mut key = [0u8; tables::SessionReports::KEY_LEN]; +#[derive(Debug, Clone)] +pub struct VsetSession { + pub id: ValidationSessionId, + pub start_seqno: u32, +} + +fn session_key(session_id: ValidationSessionId) -> [u8; tables::VsetSessions::KEY_LEN] { + let mut key = [0u8; tables::VsetSessions::KEY_LEN]; 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 session_value(vset_hash: &HashBytes, start_seqno: u32) -> [u8; tables::VsetSessions::VALUE_LEN] { + let mut value = [0u8; tables::VsetSessions::VALUE_LEN]; + value[0..32].copy_from_slice(vset_hash.as_slice()); + value[32..36].copy_from_slice(&start_seqno.to_le_bytes()); + value } fn block_batches_key( - session_id: ValidationSessionId, - observer_validator_idx: u16, + vset_hash: &HashBytes, + validator_idx: u16, start_seqno: u32, ) -> [u8; tables::BlockBatches::KEY_LEN] { let mut key = [0u8; tables::BlockBatches::KEY_LEN]; - 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[0..32].copy_from_slice(vset_hash.as_slice()); + key[32..34].copy_from_slice(&validator_idx.to_be_bytes()); + key[34..38].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[..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 d741aefecf..9b0a0985bf 100644 --- a/slasher/src/storage/models.rs +++ b/slasher/src/storage/models.rs @@ -1,15 +1,11 @@ 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, SignatureHistory}; +// === StoredBlocksBatch === + #[repr(transparent)] pub struct StoredBlocksBatch(pub BlocksBatch); @@ -102,303 +98,3 @@ 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); - -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 + 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.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)); - } - } -} - -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 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 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); - } - - validators.push(SessionValidatorScore { - validator_idx, - earned_points: u64::read_from(packet)?, - max_points: u64::read_from(packet)?, - is_bad: u32::read_from(packet)? != 0, - }); - } - - Ok(Self(SessionPenaltyReport { - session_id, - epoch_start_session_id, - session_weight, - validators: validators.into_boxed_slice(), - })) - } -} - -#[repr(transparent)] -pub struct StoredVsetPenaltyReport(pub VsetPenaltyReport); - -impl StoredVsetPenaltyReport { - pub const TL_ID: u32 = tl_proto::id!("slasher.vsetPenaltyReport", scheme = "proto.tl"); - - #[inline] - pub const fn wrap(inner: &VsetPenaltyReport) -> &Self { - // SAFETY: `StoredVsetPenaltyReport` has the same layout as `VsetPenaltyReport`. - unsafe { &*(inner as *const VsetPenaltyReport).cast::() } - } -} - -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); - } - - 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 7df9b158fd748a07397adc972cef44d653664ae3 Mon Sep 17 00:00:00 2001 From: Ivan Kalinin Date: Fri, 22 May 2026 12:26:47 +0200 Subject: [PATCH 20/31] feat(slasher): compute accusations in `complete_vset` --- slasher/src/analyzer.rs | 375 +++++++++++++++++++++---------------- slasher/src/lib.rs | 112 +++++------ slasher/src/storage/mod.rs | 2 + 3 files changed, 276 insertions(+), 213 deletions(-) diff --git a/slasher/src/analyzer.rs b/slasher/src/analyzer.rs index 22ec105d2f..2022e23ea6 100644 --- a/slasher/src/analyzer.rs +++ b/slasher/src/analyzer.rs @@ -1,188 +1,247 @@ -use std::collections::{BTreeMap, BTreeSet}; +use anyhow::Result; + +use crate::ParsedVset; +use crate::bc::SlasherParams; +use crate::storage::SlasherStorageSnapshot; + +// TODO: Move these constants into config. + +// Session should contain at least this amount of blocks to do some reporting. +const MIN_VSET_LENGTH: u32 = 1000; +// At least this number of samples must be collected to accuse someone. +const MIN_SAMPLES: u64 = 100; +// At least this number of malformed batches must be collected to accuse someone. +const MIN_MALFORMED: u64 = 5; +// We treat the node as slow if its block rate is this times the median rate. +const SLOW_FACTOR: f64 = 0.5; +// See https://en.wikipedia.org/wiki/Z-test +const Z_95: f64 = 1.96; + +#[tracing::instrument(skip_all, fields(vset_hash = %vset.hash))] +pub fn analyze_vset( + snapshot: SlasherStorageSnapshot, + vset: &ParsedVset, + last_seqno: u32, + own_validator_idx: usize, + _params: &SlasherParams, +) -> Result> { + let vset_hash = &vset.hash; + let vset = &vset.vset; + + // Compute vset block range. + let session_ids = snapshot.load_vset_sessions(vset_hash)?; + let start_seqno = session_ids + .iter() + .map(|item| item.start_seqno) + .min() + .unwrap_or(u32::MAX); + let vset_len = last_seqno.saturating_sub(start_seqno); + if vset_len < MIN_VSET_LENGTH { + tracing::warn!(vset_len, "too short vset"); + return Ok(Vec::new()); + } -use tycho_slasher_traits::ValidationSessionId; -use tycho_types::cell::HashBytes; + let n = vset.list.len(); + if n <= 1 { + tracing::warn!(n, "not enough nodes in vset"); + return Ok(Vec::new()); + } -use crate::BlocksBatch; + let mut scores = vec![vec![Score::default(); n]; n]; + let mut observed = vec![Observed::default(); n]; + + let max_weight = vset + .list + .iter() + .take(vset.main.get() as usize) + .map(|v| v.weight) + .sum::(); + let weight_threshold = max_weight.saturating_mul(2) / 3 + 1; + + // Build a matrix from all known block batches. + let mut weight_per_block = Vec::with_capacity(100); + for item in snapshot.iter_block_batches(vset_hash) { + let (observer, batch) = item?; + let observer = observer as usize; + // NOTE: This is a hard error because we must not store invalid batches. + anyhow::ensure!(observer < n, "invalid validator idx: idx={observer}, n={n}"); + let observer_weight = vset.list[observer].weight; + + let block_count = batch.committed_blocks.len(); + + weight_per_block.clear(); + weight_per_block.resize(block_count, 0u64); + + let mut malformed = false; + + // Count weight of valid signatures per committed column (block). + for history in &batch.signatures_history { + let other = history.validator_idx as usize; + if other >= n || other == observer { + malformed = true; + tracing::warn!( + observer, + history_entry_idx = other, + n, + reason = "invalid_history_entry", + "malformed batch", + ); + continue; + } -#[derive(Debug, PartialEq, Eq)] -pub struct ObservedBlocksBatch { - pub observer_validator_idx: u16, - pub batch: BlocksBatch, -} + let weight = vset.list[other].weight; + for block in 0..block_count { + if !batch.committed_blocks.get(block) { + // Ignore blocks which observer did not collate. + continue; + } -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SessionMeta { - pub session_id: ValidationSessionId, - pub epoch_start_session_id: ValidationSessionId, - pub validator_indices: Vec, -} + let valid_bit = block * 2 + 1; + if history.bits.get(valid_bit) { + weight_per_block[block] += weight; + } + } + } -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct VsetEpoch { - pub start_session_id: ValidationSessionId, - pub vset_hash: HashBytes, - pub next_epoch_start_session_id: Option, -} + // Count samples and adjust weight per block. + for weight in &mut weight_per_block { + if *weight == 0 { + continue; + } -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SessionPenaltyReport { - pub session_id: ValidationSessionId, - pub epoch_start_session_id: ValidationSessionId, - pub session_weight: u32, - pub validators: Box<[SessionValidatorScore]>, -} + *weight += observer_weight; + if *weight >= weight_threshold { + observed[observer].samples += 1; + } else { + // TODO: Should we treat this as malformed? + } + } -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SessionValidatorScore { - pub validator_idx: u16, - pub earned_points: u64, - pub max_points: u64, - pub is_bad: bool, -} + // Update scores. + for history in &batch.signatures_history { + let other = history.validator_idx as usize; + if other >= n || other == observer { + continue; + } -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct VsetPenaltyReport { - pub epoch_start_session_id: ValidationSessionId, - pub vset_hash: HashBytes, - pub validators: Box<[VsetValidatorPenalty]>, -} + for (block, weight) in weight_per_block.iter().enumerate() { + if *weight < weight_threshold { + continue; + } -#[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, -} + let scores = &mut scores[observer][other]; + scores.invalid_signatures += history.bits.get(block * 2) as u64; + scores.valid_signatures += history.bits.get(block * 2 + 1) as u64; + } + } -pub fn analyze_session( - meta: &SessionMeta, - batches: &[ObservedBlocksBatch], -) -> SessionPenaltyReport { - let mut committed_blocks = BTreeSet::new(); - let mut observed_validators = BTreeSet::new(); - // observer -> observed : points * session weight - let mut validator_points = BTreeMap::<(u16, u16), u64>::new(); + // Update malformed + if malformed { + observed[observer].malformed += 1; + } + } - for item in batches { - observed_validators.insert(item.observer_validator_idx); + // Finally reduce scores and observations into accusations. + let mut accusation_weights = vec![0; n]; + let mut rates = Vec::with_capacity(n - 1); + for (observer, (observed, scores)) in std::iter::zip(&observed, &scores).enumerate() { + if observed.samples < MIN_SAMPLES { + continue; + } + let observer_weight = vset.list[observer].weight; - for offset in 0..item.batch.committed_blocks.len() { - if !item.batch.committed_blocks.get(offset) { - continue; - } + // Compute the rate of valid signatures from other nodes. + rates.clear(); + rates.extend(scores.iter().enumerate().filter_map(|(i, score)| { + (i != observer).then(|| score.valid_signatures as f64 / observed.samples as f64) + })); + rates.sort_by(|a, b| a.total_cmp(b)); - committed_blocks.insert(item.batch.start_seqno + offset as u32); + let baseline = rates[rates.len() / 2]; + let slow_threshold = baseline * SLOW_FACTOR; - 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); + tracing::debug!(baseline, slow_threshold, "computed valid signature rates"); - if has_invalid_signature && has_valid_signature { + for (other, score) in scores.iter().enumerate() { + if other == observer { + continue; + } + + let rate = wilson_upper_bound(score.valid_signatures, observed.samples, Z_95); + if rate <= slow_threshold { + tracing::debug!(observer, other, "accusation found"); + if other == own_validator_idx { 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, + own_validator_idx, + observer, + "our node may be accused by {}", + vset.list[observer].public_key ); - continue; - } - - if has_valid_signature { - *validator_points - .entry((item.observer_validator_idx, history.validator_idx)) - .or_default() += 1; } + accusation_weights[other] += observer_weight; } } } - let session_weight = committed_blocks.len() as u64; - - let mut validator_indices = meta.validator_indices.clone(); - validator_indices.sort(); - validator_indices.dedup(); - - let validators = validator_indices - .into_iter() - .map(|validator_idx| { - 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_validators - .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, - earned_points, - max_points: max_session_points, - is_bad: max_session_points > 0 - && earned_points.saturating_mul(2) < max_session_points, + tracing::debug!( + %vset_hash, + ?accusation_weights, + weight_threshold, + "computed accusation weights" + ); + + let accusations = std::iter::zip(observed, accusation_weights) + .enumerate() + .filter_map(|(idx, (observed, weight))| { + let should_accuse = weight >= weight_threshold || observed.malformed >= MIN_MALFORMED; + if idx == own_validator_idx { + tracing::warn!( + own_validator_idx, + weight, + weight_threshold, + "our node is considered bad" + ); + return None; } + + should_accuse.then_some(idx as u16) }) - .collect::>() - .into_boxed_slice(); - - SessionPenaltyReport { - session_id: meta.session_id, - epoch_start_session_id: meta.epoch_start_session_id, - session_weight: session_weight as u32, - validators, - } + .collect::>(); + + Ok(accusations) } -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); - } - } - } +#[derive(Default, Clone, Copy)] +struct Score { + valid_signatures: u64, + invalid_signatures: u64, +} - for item in validators.values_mut() { - item.is_bad = item.bad_sessions_weight > bad_sessions_weight_threshold; - } +#[derive(Default, Clone, Copy)] +struct Observed { + samples: u64, + malformed: u64, +} + +// NOTE: We don't really need an exact determenism in decisions so we can +// use floating point math here. If its a concert, this can be rewritten +// to fixed point. +fn wilson_upper_bound(hits: u64, samples: u64, z: f64) -> f64 { + debug_assert!(hits <= samples); + debug_assert!(z.is_finite() && z >= 0.0); - VsetPenaltyReport { - epoch_start_session_id: epoch.start_session_id, - vset_hash: epoch.vset_hash, - validators: validators - .into_values() - .collect::>() - .into_boxed_slice(), + if samples == 0 { + return 1.0; } + + let samples_f = samples as f64; + let hits_f = hits.min(samples) as f64; + let p = hits_f / samples_f; + + let z2 = z * z; + let denom = 1.0 + z2 / samples_f; + let center = p + z2 / (2.0 * samples_f); + let margin = z * (p * (1.0 - p) / samples_f + z2 / (4.0 * samples_f * samples_f)).sqrt(); + + ((center + margin) / denom).clamp(0.0, 1.0) } diff --git a/slasher/src/lib.rs b/slasher/src/lib.rs index c812e07200..02cda659c1 100644 --- a/slasher/src/lib.rs +++ b/slasher/src/lib.rs @@ -1,4 +1,3 @@ -use std::collections::BTreeMap; use std::sync::Arc; use std::time::Duration; @@ -15,16 +14,12 @@ 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::cell::HashBytes; -use tycho_types::models::{BlockchainConfig, SignatureContext, StdAddr}; +use tycho_types::models::{BlockchainConfig, SignatureContext, StdAddr, ValidatorSet}; +use tycho_types::prelude::*; use tycho_util::config::PartialConfig; use tycho_util::futures::JoinTask; use tycho_util::serde_helpers; -pub use self::analyzer::{ - SessionPenaltyReport, SessionValidatorScore, VsetPenaltyReport, VsetValidatorPenalty, -}; use self::bc::SlasherParams; pub use self::bc::{ BlocksBatch, ContractSubscription, EncodeBlocksBatchMessage, MessageDelivered, @@ -104,9 +99,9 @@ impl Slasher { let storage = SlasherStorage::open(storage_context).context("failed to open slasher storage")?; - let current_vset_hash = *blockchain_config - .get_current_validator_set_raw()? - .repr_hash(); + let current_vset = Arc::new(ParsedVset::from_raw( + blockchain_config.get_current_validator_set_raw()?, + )?); let slasher_params = contract .find_params(blockchain_config) @@ -141,7 +136,7 @@ impl Slasher { storage, known_session_id: AtomicValidationSessionId::new(known_session_id), parsed_config: ArcSwap::new(Arc::new(ParsedConfig { - current_vset_hash, + current_vset, signature_context: SignatureContext { global_id, capabilities: global.capabilities, @@ -166,18 +161,19 @@ impl Slasher { let this = self.shared.as_ref(); let state_extra = cx.state.state_extra()?; - let current_vset_hash = *state_extra - .config - .get_current_validator_set_raw()? - .repr_hash(); + let current_vset_raw = state_extra.config.get_current_validator_set_raw()?; + let current_vset_hash = *current_vset_raw.repr_hash(); // Apply config changes when needed. let mut vset_to_complete = None; if state_extra.after_key_block { - let known_vset_hash = self.shared.parsed_config.load().current_vset_hash; - if current_vset_hash != known_vset_hash { - vset_to_complete = Some(known_vset_hash); - } + let known_vset = self.shared.parsed_config.load().current_vset.clone(); + let current_vset = if current_vset_hash == known_vset.hash { + known_vset + } else { + vset_to_complete = Some(known_vset); + Arc::new(ParsedVset::from_raw(current_vset_raw)?) + }; let global = state_extra.config.get_global_version()?; let slasher_params = this @@ -196,7 +192,7 @@ impl Slasher { // Update parsed config. self.shared.parsed_config.store(Arc::new(ParsedConfig { - current_vset_hash, + current_vset, signature_context: SignatureContext { global_id: cx.block.as_ref().global_id, capabilities: global.capabilities, @@ -234,7 +230,7 @@ impl Slasher { ); this.known_session_id.set(current_session_id); this.storage - .store_vset_session(current_session_id, ¤t_vset_hash, mc_seqno); + .store_vset_session(current_session_id, ¤t_vset_hash, mc_seqno)?; } // TODO: Cleanup stored vset sessions. @@ -308,11 +304,11 @@ impl Slasher { subscription.cleanup_expired_messages(cx.block.load_info()?.gen_utime); // Trigger reporting. - if let Some(vset_hash) = vset_to_complete + if let Some(vset) = vset_to_complete && let Some(last_seqno) = mc_seqno.checked_sub(1) { self.shared - .complete_vset(&vset_hash, last_seqno, &slasher_params)?; + .complete_vset(&vset, last_seqno, &slasher_params)?; } // Start session handlers. @@ -374,45 +370,30 @@ struct SlasherSharedState { parsed_config: ArcSwap, } -struct ParsedConfig { - current_vset_hash: HashBytes, - signature_context: SignatureContext, - slasher_params: Option, -} - impl SlasherSharedState { fn complete_vset( &self, - vset_hash: &HashBytes, + vset: &ParsedVset, last_seqno: u32, - _params: &SlasherParams, + params: &SlasherParams, ) -> Result<()> { - // Session should containt at least this amount of blocks to do some reporting. - // TODO: Move into `SlasherParams`. - const MIN_VSET_LENGTH: u32 = 1000; - - let snapshot = self.storage.snapshot(); - - // Compute vset block range. - let session_ids = snapshot.load_vset_sessions(vset_hash)?; - let start_seqno = session_ids - .iter() - .map(|item| item.start_seqno) - .min() - .unwrap_or(u32::MAX); - let vset_len = last_seqno.saturating_sub(start_seqno); - if vset_len < MIN_VSET_LENGTH { - tracing::warn!(%vset_hash, vset_len, "too short vset"); + let Some(own_validator_idx) = + vset.vset.list.iter().position(|item| { + item.public_key.as_array() == self.node_keys.public_key.as_bytes() + }) + else { + tracing::warn!(vset_hash = %vset.hash, "not in a validator set"); return Ok(()); - } - - // Build a matrix from all known block batches. - - for item in snapshot.iter_block_batches(vset_hash) { - let (validator_idx, batch) = item?; + }; - // todo - } + let accusations = analyzer::analyze_vset( + self.storage.snapshot(), + vset, + last_seqno, + own_validator_idx, + params, + )?; + tracing::warn!("slasher accusations: {accusations:?}"); Ok(()) } @@ -524,3 +505,24 @@ impl SlasherSharedState { } } } + +struct ParsedConfig { + current_vset: Arc, + signature_context: SignatureContext, + slasher_params: Option, +} + +struct ParsedVset { + hash: HashBytes, + vset: ValidatorSet, +} + +impl ParsedVset { + fn from_raw(raw: Cell) -> Result { + let vset = raw.parse::()?; + Ok(Self { + hash: *raw.repr_hash(), + vset, + }) + } +} diff --git a/slasher/src/storage/mod.rs b/slasher/src/storage/mod.rs index 2d130bacb0..d7f889b926 100644 --- a/slasher/src/storage/mod.rs +++ b/slasher/src/storage/mod.rs @@ -177,6 +177,8 @@ impl Iterator for BlockBatchesIter<'_> { #[derive(Debug, Clone)] pub struct VsetSession { + // NOTE: Might be needed later. + #[expect(unused)] pub id: ValidationSessionId, pub start_seqno: u32, } From 3fe51e555897cb32ddfaffff38ccff774c8f0d51 Mon Sep 17 00:00:00 2001 From: Ivan Kalinin Date: Thu, 28 May 2026 16:43:33 +0200 Subject: [PATCH 21/31] feat(slasher): check vset hash in slasher config --- Cargo.lock | 9 +- Cargo.toml | 1 - cli/src/cmd/tools/gen_zerostate.rs | 4 - cli/src/node/mod.rs | 21 +- collator/src/manager/state_update_handler.rs | 9 +- .../src/validator/impls/std_impl/session.rs | 1 + collator/src/validator/mod.rs | 2 + collator/tests/validator_tests.rs | 3 + ...genSlasherStub.ts => genSlasherAccount.ts} | 12 +- .../src/{slasher-stub.tolk => slasher.tolk} | 46 +- contracts/tests/Slasher.spec.ts | 23 +- ...sherStub.compile.ts => Slasher.compile.ts} | 2 +- .../wrappers/{SlasherStub.ts => Slasher.ts} | 14 +- contracts/wrappers/util.ts | 4 + contracts/yarn.lock | 1112 ++++------------- scripts/build-contracts.sh | 2 +- slasher-traits/src/validator.rs | 22 +- slasher/src/analyzer.rs | 62 +- .../src/bc/{stub_contract.rs => contract.rs} | 24 +- slasher/src/bc/mod.rs | 5 +- slasher/src/collector/validator_events.rs | 5 + slasher/src/lib.rs | 94 +- slasher/src/proto.tl | 25 +- slasher/src/storage/db.rs | 30 +- slasher/src/storage/mod.rs | 196 ++- slasher/src/storage/models.rs | 70 ++ 26 files changed, 730 insertions(+), 1068 deletions(-) rename contracts/scripts/{genSlasherStub.ts => genSlasherAccount.ts} (85%) rename contracts/src/{slasher-stub.tolk => slasher.tolk} (79%) rename contracts/wrappers/{SlasherStub.compile.ts => Slasher.compile.ts} (83%) rename contracts/wrappers/{SlasherStub.ts => Slasher.ts} (84%) rename slasher/src/bc/{stub_contract.rs => contract.rs} (93%) diff --git a/Cargo.lock b/Cargo.lock index 2667d35ae3..a3dbe24dae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4561,7 +4561,8 @@ dependencies = [ [[package]] name = "tycho-types" version = "0.3.4" -source = "git+https://github.com/broxus/tycho-types.git?rev=aeea4e8d007e8a64439d3d923a3752fc823b2256#aeea4e8d007e8a64439d3d923a3752fc823b2256" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ebf3e9cb2b0e515adc25e4b30f46bd70cbd4d67edfaca7e3440c0ab7405086" dependencies = [ "ahash", "anyhow", @@ -4594,7 +4595,8 @@ dependencies = [ [[package]] name = "tycho-types-abi-proc" version = "0.3.0" -source = "git+https://github.com/broxus/tycho-types.git?rev=aeea4e8d007e8a64439d3d923a3752fc823b2256#aeea4e8d007e8a64439d3d923a3752fc823b2256" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c813c08a03554252747f9e5e88485d9af4c30077394a1c3bb6d774ddca56b07" dependencies = [ "anyhow", "proc-macro2", @@ -4605,7 +4607,8 @@ dependencies = [ [[package]] name = "tycho-types-proc" version = "0.3.0" -source = "git+https://github.com/broxus/tycho-types.git?rev=aeea4e8d007e8a64439d3d923a3752fc823b2256#aeea4e8d007e8a64439d3d923a3752fc823b2256" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad05cf4ab89631f8c11d85c3aa80f781502440f75361d251f866e0d76ae9d31" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 87e18e8b58..369770ee19 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -182,7 +182,6 @@ tycho-wu-tuner = { path = "./wu-tuner", version = "0.3.10" } [patch.crates-io] # patches here -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 906e0b7e26..32d31ba907 100644 --- a/cli/src/cmd/tools/gen_zerostate.rs +++ b/cli/src/cmd/tools/gen_zerostate.rs @@ -211,10 +211,6 @@ impl ZerostateConfig { fundamental_addresses.set(minter_address, ())?; } - if let Some(slasher_params) = self.params.get::()? { - fundamental_addresses.set(slasher_params.address, ())?; - } - self.params.set::(&fundamental_addresses)?; } diff --git a/cli/src/node/mod.rs b/cli/src/node/mod.rs index 05b009ce79..ca1266266e 100644 --- a/cli/src/node/mod.rs +++ b/cli/src/node/mod.rs @@ -178,19 +178,16 @@ impl Node { .await .context("failed to load mc zerostate on run")?; - let config = mc_state.config_params()?; - let validator_session_id = { - let current_validator_set = config.get_current_validator_set()?; + { + let current_validator_set = mc_state.config_params()?.get_current_validator_set()?; base.validator_resolver() .update_validator_set(¤t_validator_set); - let v_set_len = current_validator_set.list.len(); + let vset_len = current_validator_set.list.len(); anyhow::ensure!( - is_single_node == (v_set_len == 1), - "cannot start with v_set_len={v_set_len} and single_node={is_single_node}" + is_single_node == (vset_len == 1), + "cannot start with vset_len={vset_len} and single_node={is_single_node}" ); - - tycho_slasher_traits::ValidationSessionId::from(mc_state.state_extra()?) - }; + } // Create mempool adapter let mempool_adapter = self.rpc_mempool_adapter.inner.clone(); @@ -225,13 +222,11 @@ impl Node { let slasher = tycho_slasher::Slasher::new( base.keypair.clone(), - tycho_slasher::StubSlasherContract, + tycho_slasher::StdSlasherContract, base.blockchain_rpc_client.clone(), &base.storage_context, self.slasher_config, - mc_state.as_ref().global_id, - config, - validator_session_id, + &mc_state, ) .context("failed to create slasher")?; diff --git a/collator/src/manager/state_update_handler.rs b/collator/src/manager/state_update_handler.rs index 585bfea0e6..11fe14882c 100644 --- a/collator/src/manager/state_update_handler.rs +++ b/collator/src/manager/state_update_handler.rs @@ -5,7 +5,10 @@ use ahash::HashMapExt; use anyhow::{Context, Result, anyhow}; use tokio::sync::Notify; use tycho_block_util::block::{ValidatorSubsetInfo, calc_next_block_id_short}; -use tycho_types::models::{BlockId, GlobalCapabilities, IndexedValidatorDescription, ShardIdent}; +use tycho_block_util::config::BlockchainConfigExt; +use tycho_types::models::{ + BlockId, GlobalCapabilities, IndexedValidatorDescription, ShardIdent, ValidatorSet, +}; use tycho_util::futures::JoinTask; use tycho_util::metrics::HistogramGuard; use tycho_util::{DashMapEntry, FastHashMap, FastHashSet}; @@ -186,7 +189,8 @@ where 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()?; + let raw_validators_set = mc_data.config.get_current_validator_set_raw()?; + let full_validators_set = raw_validators_set.parse::()?; tracing::trace!(target: tracing_targets::COLLATION_MANAGER, "full_validators_set: since={}, until={}, main={}, total_weight={}, list={:?}", full_validators_set.utime_since, full_validators_set.utime_until, @@ -409,6 +413,7 @@ where shard_ident: shard_id, session_id: new_session_info.get_validation_session_id(), start_block_seqno: next_block_id_short.seqno, + vset_hash: raw_validators_set.repr_hash(), validators: &new_session_info.collators().validators, })?; } diff --git a/collator/src/validator/impls/std_impl/session.rs b/collator/src/validator/impls/std_impl/session.rs index 7ac2d9c09d..72edd41a00 100644 --- a/collator/src/validator/impls/std_impl/session.rs +++ b/collator/src/validator/impls/std_impl/session.rs @@ -89,6 +89,7 @@ impl ValidatorSession { info.session_id.into(), info.start_block_seqno, own_validator_idx, + info.vset_hash, info.validators, ); diff --git a/collator/src/validator/mod.rs b/collator/src/validator/mod.rs index d2003ee251..077fc4697b 100644 --- a/collator/src/validator/mod.rs +++ b/collator/src/validator/mod.rs @@ -4,6 +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::cell::HashBytes; use tycho_types::models::{BlockId, BlockIdShort, IndexedValidatorDescription, ShardIdent}; use tycho_util::FastHashMap; @@ -75,6 +76,7 @@ pub struct AddSession<'a> { pub shard_ident: ShardIdent, pub start_block_seqno: u32, pub session_id: ValidationSessionId, + pub vset_hash: &'a HashBytes, pub validators: &'a [IndexedValidatorDescription], } diff --git a/collator/tests/validator_tests.rs b/collator/tests/validator_tests.rs index 1cb278a03e..b4a82c61d4 100644 --- a/collator/tests/validator_tests.rs +++ b/collator/tests/validator_tests.rs @@ -140,6 +140,7 @@ async fn validator_signatures_match() -> Result<()> { shard_ident: zerostate_id.shard, session_id, start_block_seqno: block_id.seqno, + vset_hash: &HashBytes::ZERO, validators: &validators, })?; } @@ -256,6 +257,7 @@ async fn malicious_validators_are_ignored() -> Result<()> { shard_ident: zerostate_id.shard, session_id, start_block_seqno: block_id.seqno, + vset_hash: &HashBytes::ZERO, validators: &validators, })?; } @@ -392,6 +394,7 @@ async fn network_gets_stuck_without_signatures() -> Result<()> { shard_ident: zerostate_id.shard, session_id, start_block_seqno: block_id.seqno, + vset_hash: &HashBytes::ZERO, validators: &validators, })?; } diff --git a/contracts/scripts/genSlasherStub.ts b/contracts/scripts/genSlasherAccount.ts similarity index 85% rename from contracts/scripts/genSlasherStub.ts rename to contracts/scripts/genSlasherAccount.ts index c54001f5e3..0f4be81cd9 100644 --- a/contracts/scripts/genSlasherStub.ts +++ b/contracts/scripts/genSlasherAccount.ts @@ -1,6 +1,6 @@ import arg from "arg"; import { address, beginCell, storeAccount, toNano } from "@ton/core"; -import { storeSlasherStubData } from "../wrappers/SlasherStub"; +import { storeSlasherData } from "../wrappers/Slasher"; import { compile } from "@ton/blueprint"; async function main() { @@ -12,14 +12,14 @@ async function main() { throw new Error("`--balance` option is missing"); } - const code = await compile("SlasherStub"); + const code = await compile("Slasher"); const account = beginCell() .storeBit(true) .store( storeAccount({ addr: address( - "-1:0000000000000000000000000000000000000000000000000000000000000000" + "-1:0000000000000000000000000000000000000000000000000000000000000000", ), storage: { balance: { @@ -32,9 +32,9 @@ async function main() { code, data: beginCell() .store( - storeSlasherStubData({ + storeSlasherData({ updatedAtMs: 0n, - }) + }), ) .endCell(), }, @@ -48,7 +48,7 @@ async function main() { lastPaid: 0, storageExtra: null, }, - }) + }), ) .endCell(); console.log(account.toBoc().toString("base64")); diff --git a/contracts/src/slasher-stub.tolk b/contracts/src/slasher.tolk similarity index 79% rename from contracts/src/slasher-stub.tolk rename to contracts/src/slasher.tolk index 6c363d7700..fa152387e0 100644 --- a/contracts/src/slasher-stub.tolk +++ b/contracts/src/slasher.tolk @@ -2,10 +2,14 @@ import "@stdlib/gas-payments" import "@stdlib/tvm-dicts" import "lib/config-params" +const SLASHER_OP_SEND_BLOCKS_BATCH = stringCrc32("op::slasher_send_blocks_batch") +const SLASHER_OP_VOTE = stringCrc32("op::slasher_vote") + const ERROR_INVALID_SIGNATURE = 40 const ERROR_VALIDATOR_NOT_FOUND = 50 const ERROR_REPLAY_PROTECTION = 52 const ERROR_MESSAGE_EXPIRED = 57 +const ERROR_UNKNOWN_OP = 60 const ERROR_INVALID_BLOCKS_BATCH = 100 const ERROR_NO_SLASHER_CONFIG = 101 const ERROR_NO_PREV_BLOCK_ID = 102 @@ -63,13 +67,43 @@ fun onExternalMessage(inMsg: slice) { val signedBody = inMsg; val createdAtMs = inMsg.loadUint(64); val expireAtSec = inMsg.loadUint(32); - val validatorIdx = inMsg.loadUint(16); - val batch = inMsg.loadRef(); - inMsg.assertEnd(); + + val op = inMsg.loadUint(32); + match (op) { + SLASHER_OP_SEND_BLOCKS_BATCH => { + handleBlocksBatchMessage( + signature, + signedBody, + createdAtMs, + expireAtSec, + inMsg, + ) + } + // TODO: Handle `SLASHER_OP_VOTE` + else => { + throw(ERROR_UNKNOWN_OP) + } + } +} + +fun handleBlocksBatchMessage( + signature: slice, + signedBody: slice, + createdAtMs: int, + expireAtSec: int, + rest: slice +) { + val vsetHash = rest.loadUint(256); + val validatorIdx = rest.loadUint(16); + val batch = rest.loadRef(); + rest.assertEnd(); assert(blockchain.now() <= expireAtSec, ERROR_MESSAGE_EXPIRED); val toSign = beginCell().storeSlice(signedBody).endCell(); - val vset = lazy ValidatorSet.fromCell(blockchain.configParam(PARAM_IDX_CURRENT_VSET)!); + val vsetRaw = blockchain.configParam(PARAM_IDX_CURRENT_VSET)!; + assert(vsetRaw.hash() == vsetHash, ERROR_VALIDATOR_NOT_FOUND); + + val vset = lazy ValidatorSet.fromCell(vsetRaw); var (validatorCs, validatorFound) = vset.list.uDictGet(16, validatorIdx); assert(validatorFound, ERROR_VALIDATOR_NOT_FOUND); val validatorPubkey = ValidatorDescr.readPubkeyOnly(validatorCs!); @@ -89,6 +123,8 @@ fun onExternalMessage(inMsg: slice) { ), ERROR_INVALID_BLOCKS_BATCH); var data = Storage.load(); + + // TODO: `updatedAtMs` must be separate for all validators. assert(createdAtMs > (data.updatedAtMs - REPLAY_OFFSET_MS) && createdAtMs <= (blockchain.now() + FUTURE_OFFSET_SEC) * 1000, ERROR_REPLAY_PROTECTION); @@ -106,7 +142,7 @@ struct ValidateBlocksBatchParams { fun validateBlocksBatch(batch: slice, params: ValidateBlocksBatchParams): bool { val startSeqno = batch.loadUint(32); - if (startSeqno + params.batchSize >= params.mcSeqno) { + if (startSeqno + params.batchSize > params.mcSeqno) { // Batch contains blocks that were not produced yet. return false; } diff --git a/contracts/tests/Slasher.spec.ts b/contracts/tests/Slasher.spec.ts index 4ed660a7d8..fa217eeaf5 100644 --- a/contracts/tests/Slasher.spec.ts +++ b/contracts/tests/Slasher.spec.ts @@ -18,10 +18,11 @@ import { import { TychoExecutor } from "@tychosdk/emulator"; import { PARAM_IDX_SLASHER_PARAMS, - SlasherStub, + Slasher, storeSlasherParams, - storeSlasherStubData, -} from "../wrappers/SlasherStub"; + storeSlasherData, + SLASHER_OP_SEND_BLOCKS_BATCH, +} from "../wrappers/Slasher"; import { bufferToBigInt, ConfigParams, @@ -45,6 +46,7 @@ describe("Slasher", () => { let blockchain: Blockchain; let slasher: SmartContract; let keypair: KeyPair; + let currentVsetHash: Buffer; beforeAll(async () => { keypair = await getSecureRandomBytes(32).then(keyPairFromSeed); @@ -77,6 +79,7 @@ describe("Slasher", () => { adnlAddr: null, }); params.setCurrentVset(vset); + currentVsetHash = params.getCurrentVsetHash()!; const fundamentalAddresses = Dictionary.load( Dictionary.Keys.Buffer(32), @@ -88,7 +91,7 @@ describe("Slasher", () => { config = params.toCell(); - code = await compile("SlasherStub", { debugInfo: true }); + code = await compile("Slasher", { debugInfo: true }); executor = await TychoExecutor.create(); }); @@ -106,7 +109,7 @@ describe("Slasher", () => { code, data: beginCell() .store( - storeSlasherStubData({ + storeSlasherData({ updatedAtMs: 0n, }), ) @@ -132,15 +135,13 @@ 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(catchainSeqno, 32) - .storeUint(vsetSwitchRound, 32) + .storeUint(SLASHER_OP_SEND_BLOCKS_BATCH, 32) + .storeBuffer(currentVsetHash, 32) .storeUint(validatorIdx, 16) .storeRef(SAMPLE_BLOCKS_BATCH) .endCell(); @@ -188,7 +189,5 @@ describe("Slasher", () => { }); function getters(blockchain: Blockchain, slasher: SmartContract) { - return blockchain.openContract( - SlasherStub.createFromAddress(slasher.address), - ); + return blockchain.openContract(Slasher.createFromAddress(slasher.address)); } diff --git a/contracts/wrappers/SlasherStub.compile.ts b/contracts/wrappers/Slasher.compile.ts similarity index 83% rename from contracts/wrappers/SlasherStub.compile.ts rename to contracts/wrappers/Slasher.compile.ts index 44aa7aeb51..1af774f5a9 100644 --- a/contracts/wrappers/SlasherStub.compile.ts +++ b/contracts/wrappers/Slasher.compile.ts @@ -2,7 +2,7 @@ import { CompilerConfig } from "@ton/blueprint"; export const compile: CompilerConfig = { lang: "tolk", - entrypoint: "src/slasher-stub.tolk", + entrypoint: "src/slasher.tolk", withStackComments: true, withSrcLineComments: true, experimentalOptions: "", diff --git a/contracts/wrappers/SlasherStub.ts b/contracts/wrappers/Slasher.ts similarity index 84% rename from contracts/wrappers/SlasherStub.ts rename to contracts/wrappers/Slasher.ts index 1d7e54c16f..9eeca67041 100644 --- a/contracts/wrappers/SlasherStub.ts +++ b/contracts/wrappers/Slasher.ts @@ -8,6 +8,8 @@ import { } from "@ton/core"; import { UnknownTagError } from "./util"; +export const SLASHER_OP_SEND_BLOCKS_BATCH = 0x60e2ac7f; + export const PARAM_IDX_SLASHER_PARAMS = 666; const SLASHER_PARAMS_TAG = 0x01; @@ -38,32 +40,30 @@ export function storeSlasherParams( }; } -export type SlasherStubData = { +export type SlasherData = { updatedAtMs: bigint; }; -export function loadSlasherStubData(cs: Slice): SlasherStubData { +export function loadSlasherData(cs: Slice): SlasherData { return { updatedAtMs: cs.loadUintBig(64), }; } -export function storeSlasherStubData( - s: SlasherStubData, -): (builder: Builder) => void { +export function storeSlasherData(s: SlasherData): (builder: Builder) => void { return (builder) => { builder.storeUint(s.updatedAtMs, 64); }; } -export class SlasherStub implements Contract { +export class Slasher implements Contract { constructor( readonly address: Address, readonly init?: { code: Cell; data: Cell }, ) {} static createFromAddress(address: Address) { - return new SlasherStub(address); + return new Slasher(address); } async isBlocksBatchValid( diff --git a/contracts/wrappers/util.ts b/contracts/wrappers/util.ts index 490cc3e340..45f4f3cfb6 100644 --- a/contracts/wrappers/util.ts +++ b/contracts/wrappers/util.ts @@ -143,6 +143,10 @@ export class ConfigParams { }; } + getCurrentVsetHash(): Buffer | undefined { + return this.getRaw(34)?.hash(); + } + getCurrentVset(): ValidatorSet | null { return this.getVsetParam(34); } diff --git a/contracts/yarn.lock b/contracts/yarn.lock index 9ab77505ce..f219cd00bd 100644 --- a/contracts/yarn.lock +++ b/contracts/yarn.lock @@ -15,7 +15,7 @@ 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.12.13", "@babel/code-frame@^7.27.1": +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.27.1": version "7.27.1" resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz" integrity sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg== @@ -29,7 +29,7 @@ resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz" integrity sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw== -"@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": +"@babel/core@^7.23.9", "@babel/core@^7.27.4": version "7.28.3" resolved "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz" integrity sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ== @@ -50,7 +50,7 @@ json5 "^2.2.3" semver "^6.3.1" -"@babel/generator@^7.27.5", "@babel/generator@^7.28.3", "@babel/generator@^7.7.2": +"@babel/generator@^7.27.5", "@babel/generator@^7.28.3": version "7.28.3" resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz" integrity sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw== @@ -122,7 +122,7 @@ "@babel/template" "^7.27.2" "@babel/types" "^7.28.2" -"@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": +"@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": version "7.28.3" resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz" integrity sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA== @@ -178,7 +178,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-jsx@^7.27.1", "@babel/plugin-syntax-jsx@^7.7.2": +"@babel/plugin-syntax-jsx@^7.27.1": version "7.27.1" resolved "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz" integrity sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w== @@ -241,14 +241,14 @@ dependencies: "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-syntax-typescript@^7.27.1", "@babel/plugin-syntax-typescript@^7.7.2": +"@babel/plugin-syntax-typescript@^7.27.1": version "7.27.1" 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.3.3": +"@babel/template@^7.27.2": version "7.27.2" resolved "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz" integrity sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw== @@ -270,7 +270,7 @@ "@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.3.3": +"@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": version "7.28.2" resolved "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz" integrity sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ== @@ -290,6 +290,28 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" +"@emnapi/core@^1.4.3": + version "1.10.0" + resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.10.0.tgz#380ccc8f2412ea22d1d972df7f8ee23a3b9c7467" + integrity sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw== + dependencies: + "@emnapi/wasi-threads" "1.2.1" + tslib "^2.4.0" + +"@emnapi/runtime@^1.4.3": + version "1.10.0" + resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.10.0.tgz#4b260c0d3534204e98c6110b8db1a987d26ec87c" + integrity sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA== + dependencies: + tslib "^2.4.0" + +"@emnapi/wasi-threads@1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz#28fed21a1ba1ce797c44a070abc94d42f3ae8548" + integrity sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w== + dependencies: + tslib "^2.4.0" + "@inquirer/external-editor@^1.0.0": version "1.0.1" resolved "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.1.tgz" @@ -333,18 +355,6 @@ 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.npmjs.org/@jest/console/-/console-30.0.5.tgz" @@ -357,40 +367,6 @@ 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.npmjs.org/@jest/core/-/core-30.0.5.tgz" @@ -430,16 +406,6 @@ 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.npmjs.org/@jest/environment/-/environment-30.0.5.tgz" @@ -450,13 +416,6 @@ "@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.npmjs.org/@jest/expect-utils/-/expect-utils-30.0.5.tgz" @@ -464,14 +423,6 @@ 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.npmjs.org/@jest/expect/-/expect-30.0.5.tgz" @@ -480,18 +431,6 @@ 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.npmjs.org/@jest/fake-timers/-/fake-timers-30.0.5.tgz" @@ -509,7 +448,7 @@ resolved "https://registry.npmjs.org/@jest/get-type/-/get-type-30.0.1.tgz" integrity sha512-AyYdemXCptSRFirI5EPazNxyPwAL0jXt3zceFjaj8NFiKP9pOi0bfXonf6qkf82z2t3QWPeLCWWw4stPBzctLw== -"@jest/globals@*", "@jest/globals@30.0.5": +"@jest/globals@30.0.5": version "30.0.5" resolved "https://registry.npmjs.org/@jest/globals/-/globals-30.0.5.tgz" integrity sha512-7oEJT19WW4oe6HR7oLRvHxwlJk2gev0U9px3ufs8sX9PoD1Eza68KF0/tlN7X0dq/WVsBScXQGgCldA1V9Y/jA== @@ -519,16 +458,6 @@ "@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.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz" @@ -537,36 +466,6 @@ "@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.npmjs.org/@jest/reporters/-/reporters-30.0.5.tgz" @@ -596,13 +495,6 @@ 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.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz" @@ -620,15 +512,6 @@ 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.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz" @@ -638,16 +521,6 @@ 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.npmjs.org/@jest/test-result/-/test-result-30.0.5.tgz" @@ -658,16 +531,6 @@ "@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.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.0.5.tgz" @@ -678,7 +541,7 @@ jest-haste-map "30.0.5" slash "^3.0.0" -"@jest/transform@^29.0.0 || ^30.0.0", "@jest/transform@30.0.5": +"@jest/transform@30.0.5": version "30.0.5" resolved "https://registry.npmjs.org/@jest/transform/-/transform-30.0.5.tgz" integrity sha512-Vk8amLQCmuZyy6GbBht1Jfo9RSdBtg7Lks+B0PecnjI8J+PCLQPGh7uI8Q/2wwpW2gLdiAfiHNsmekKlywULqg== @@ -699,28 +562,7 @@ slash "^3.0.0" write-file-atomic "^5.0.1" -"@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": +"@jest/types@30.0.5": version "30.0.5" resolved "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz" integrity sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ== @@ -733,18 +575,6 @@ "@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.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz" @@ -763,14 +593,6 @@ 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.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz" @@ -779,6 +601,14 @@ "@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.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" + "@multiformats/murmur3@^1.0.3": version "1.1.3" resolved "https://registry.npmjs.org/@multiformats/murmur3/-/murmur3-1.1.3.tgz" @@ -787,6 +617,15 @@ 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.npmjs.org/@noble/ed25519/-/ed25519-1.7.5.tgz" @@ -860,30 +699,18 @@ 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.npmjs.org/@sinclair/typebox/-/typebox-0.34.40.tgz" integrity sha512-gwBNIP8ZAYev/ORDWW0QvxdwPXwxBtLsdsJgSc7eDIRt8ubP+rxUBzPsrwnu16fgEF8Bx4lh/+mvQvJzcTM6Kw== -"@sinonjs/commons@^3.0.0", "@sinonjs/commons@^3.0.1": +"@sinonjs/commons@^3.0.1": version "3.0.1" 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.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz" @@ -891,7 +718,7 @@ dependencies: "@sinonjs/commons" "^3.0.1" -"@tact-lang/compiler@>=1.6.5", "@tact-lang/compiler@1.6.13": +"@tact-lang/compiler@1.6.13": version "1.6.13" resolved "https://registry.npmjs.org/@tact-lang/compiler/-/compiler-1.6.13.tgz" integrity sha512-lrgT/kCgC+nuppB4zPSDCAcLQ6EauTJ3NEBX4prEBBRmJ8aexYAfFUfVayskZw96JrDjReNIhnD8dG/yU0Fk+w== @@ -959,22 +786,22 @@ ton-lite-client "^3.1.1" ts-node "^10.9.1" -"@ton/core@^0.61.0": - version "0.61.0" - resolved "https://registry.npmjs.org/@ton/core/-/core-0.61.0.tgz" - 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/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": +"@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.60.1": - version "0.60.1" - resolved "https://registry.npmjs.org/@ton/core/-/core-0.60.1.tgz" - 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" @@ -985,7 +812,7 @@ 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.npmjs.org/@ton/crypto/-/crypto-3.3.0.tgz" integrity sha512-/A6CYGgA/H36OZ9BbTaGerKtzWp50rg67ZCH2oIjV1NcrBaCK9Z343M+CxedvM7Haf3f/Ee9EhxyeTp0GKMUpA== @@ -994,15 +821,7 @@ jssha "3.2.0" tweetnacl "1.0.3" -"@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": +"@ton/sandbox@>=0.39.0": version "0.41.0" resolved "https://registry.npmjs.org/@ton/sandbox/-/sandbox-0.41.0.tgz" integrity sha512-+WRWiHfm62xQebVt6BvLb2UhVphpBHCwSby8R5vP9llzdVck+XEs+p4csIkZBh6gRQsy1Xomzh1PpgZS5XVE3A== @@ -1013,21 +832,29 @@ table "^6.9.0" ton-assembly "0.6.1" -"@ton/test-utils@>=0.11.0", "@ton/test-utils@>=0.7.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/test-utils@>=0.11.0": version "0.11.0" 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@>=0.13.0", "@ton/tolk-js@>=1.0.0": +"@ton/tolk-js@>=1.0.0": version "1.0.0" 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", "@ton/ton@>=15.2.1 <16.0.0": +"@ton/ton@>=15.2.1 <16.0.0": version "15.3.1" resolved "https://registry.npmjs.org/@ton/ton/-/ton-15.3.1.tgz" integrity sha512-+UuvbE0o0VIU/0W90STO+emRIDr3Vs39LdbX5ySm/Ra+RQJSiH0KX6TDOFqWDmD2Wzk4/zw21KwSiZ6Xjk8zlw== @@ -1069,7 +896,7 @@ "@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.npmjs.org/@tonstudio/parser-runtime/-/parser-runtime-0.0.1.tgz" integrity sha512-5s4fLkXWxa4SAd7QGGvJXe13GakEo0J3VF5dUI/i3A//bGZxMwCp1FcnbErpNs3y0LcAZoXE5FCUnDowDQptqw== @@ -1094,6 +921,13 @@ 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.2" + resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.2.tgz#12b3a1b33db1f9cad4ddff1f604ab7dd00bf464e" + integrity sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg== + dependencies: + tslib "^2.4.0" + "@tychosdk/emulator@^0.2.6": version "0.2.6" resolved "https://registry.npmjs.org/@tychosdk/emulator/-/emulator-0.2.6.tgz" @@ -1102,7 +936,7 @@ axios "^1.8.4" zod "^3.24.2" -"@types/babel__core@^7.1.14", "@types/babel__core@^7.20.5": +"@types/babel__core@^7.20.5": version "7.20.5" resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz" integrity sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA== @@ -1128,7 +962,7 @@ "@babel/parser" "^7.1.0" "@babel/types" "^7.0.0" -"@types/babel__traverse@*", "@types/babel__traverse@^7.0.6": +"@types/babel__traverse@*": version "7.28.0" resolved "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz" integrity sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q== @@ -1142,14 +976,7 @@ dependencies: "@types/node" "*" -"@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": +"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.1", "@types/istanbul-lib-coverage@^2.0.6": version "2.0.6" 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== @@ -1161,7 +988,7 @@ dependencies: "@types/istanbul-lib-coverage" "*" -"@types/istanbul-reports@^3.0.0", "@types/istanbul-reports@^3.0.4": +"@types/istanbul-reports@^3.0.4": version "3.0.4" resolved "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz" integrity sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ== @@ -1181,7 +1008,7 @@ resolved "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz" integrity sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA== -"@types/node@*", "@types/node@^22.15.32", "@types/node@>=13.7.0", "@types/node@>=18": +"@types/node@*", "@types/node@>=13.7.0", "@types/node@^22.15.32": version "22.17.2" resolved "https://registry.npmjs.org/@types/node/-/node-22.17.2.tgz" integrity sha512-gL6z5N9Jm9mhY+U2KXZpteb+09zyffliRkZyZOHODGATyC5B1Jt/7TzuuiLkFsSUMLbS1OLmlj/E+/3KF4Q/4w== @@ -1193,7 +1020,7 @@ resolved "https://registry.npmjs.org/@types/pegjs/-/pegjs-0.10.6.tgz" integrity sha512-eLYXDbZWXh2uxf+w8sXS8d6KSoXTswfps6fvCUuVAGN8eRpfe7h9eSRydxiSJvo9Bf+GzifsDOr9TMQlmJdmkw== -"@types/stack-utils@^2.0.0", "@types/stack-utils@^2.0.3": +"@types/stack-utils@^2.0.3": version "2.0.3" resolved "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz" integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw== @@ -1203,7 +1030,7 @@ 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.8": +"@types/yargs@^17.0.33": version "17.0.33" resolved "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz" integrity sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA== @@ -1215,6 +1042,71 @@ 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.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz" @@ -1225,6 +1117,28 @@ 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.npmjs.org/@vscode/debugadapter/-/debugadapter-1.68.0.tgz" @@ -1301,11 +1215,6 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: 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.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz" @@ -1316,7 +1225,7 @@ ansi-styles@^6.1.0: resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz" integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== -anymatch@^3.0.3, anymatch@^3.1.3: +anymatch@^3.1.3: version "3.1.3" resolved "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz" integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== @@ -1360,7 +1269,7 @@ axios@^1.6.7, axios@^1.7.7, axios@^1.8.4: form-data "^4.0.4" proxy-from-env "^1.1.0" -"babel-jest@^29.0.0 || ^30.0.0", babel-jest@30.0.5: +babel-jest@30.0.5: version "30.0.5" resolved "https://registry.npmjs.org/babel-jest/-/babel-jest-30.0.5.tgz" integrity sha512-mRijnKimhGDMsizTvBTWotwNpzrkHr+VvZUQBof2AufXKB8NXrL1W69TG20EvOz7aevx6FTJIaBuBkYxS8zolg== @@ -1373,30 +1282,6 @@ axios@^1.6.7, axios@^1.7.7, axios@^1.8.4: 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.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.0.tgz" @@ -1408,16 +1293,6 @@ 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.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.0.1.tgz" @@ -1427,7 +1302,7 @@ babel-plugin-jest-hoist@30.0.1: "@babel/types" "^7.27.3" "@types/babel__core" "^7.20.5" -babel-preset-current-node-syntax@^1.0.0, babel-preset-current-node-syntax@^1.1.0: +babel-preset-current-node-syntax@^1.1.0: version "1.2.0" 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== @@ -1448,14 +1323,6 @@ babel-preset-current-node-syntax@^1.0.0, 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.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.0.1.tgz" @@ -1533,7 +1400,7 @@ braces@^3.0.3: dependencies: fill-range "^7.1.1" -browserslist@^4.24.0, "browserslist@>= 4.21.0": +browserslist@^4.24.0: version "4.25.3" resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.25.3.tgz" integrity sha512-cDGv1kkDI4/0e5yON9yM5G/0A5u8sf5TnmdX5C9qHzI9PPu++sQ9zjm1k9NiOrf3riY4OkK0zSGqfvJyJsgCBQ== @@ -1591,7 +1458,7 @@ call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: es-errors "^1.3.0" function-bind "^1.1.2" -callsites@^3.0.0, callsites@^3.1.0: +callsites@^3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== @@ -1601,11 +1468,6 @@ camelcase@^5.3.1: 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.npmjs.org/camelcase/-/camelcase-6.3.0.tgz" @@ -1621,7 +1483,7 @@ case-shift@^2.5.3: resolved "https://registry.npmjs.org/case-shift/-/case-shift-2.5.3.tgz" integrity sha512-6SdS9W5xu82Kj1Z6f14h0zsbWTdXGtD0RftPNnqbAFFqqlzX1SMFi1E1NDIBg5LC2m9EYWWPUV80nTb3iu2e6Q== -chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2: +chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2: version "4.1.2" resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -1639,21 +1501,11 @@ chardet@^2.1.0: 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.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.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.1.0.tgz" @@ -1695,7 +1547,7 @@ co@^4.6.0: resolved "https://registry.npmjs.org/co/-/co-4.6.0.tgz" integrity sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ== -collect-v8-coverage@^1.0.0, collect-v8-coverage@^1.0.2: +collect-v8-coverage@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz" integrity sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q== @@ -1739,19 +1591,6 @@ crc-32@^1.2.2: 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.npmjs.org/create-require/-/create-require-1.1.1.tgz" @@ -1778,12 +1617,12 @@ debug@^4.1.0, debug@^4.1.1, debug@^4.3.1: dependencies: ms "^2.1.3" -dedent@^1.0.0, dedent@^1.6.0: +dedent@^1.6.0: version "1.6.0" resolved "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz" integrity sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA== -deepmerge@^4.2.2, deepmerge@^4.3.1: +deepmerge@^4.3.1: version "4.3.1" resolved "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz" integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== @@ -1800,16 +1639,11 @@ delayed-stream@~1.0.0: resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== -detect-newline@^3.0.0, detect-newline@^3.1.0: +detect-newline@^3.1.0: version "3.1.0" 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.npmjs.org/diff/-/diff-4.0.2.tgz" @@ -1923,7 +1757,7 @@ eventsource@^2.0.2: resolved "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz" integrity sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA== -execa@^5.0.0, execa@^5.1.1: +execa@^5.1.1: version "5.1.1" resolved "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz" integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== @@ -1943,23 +1777,7 @@ exit-x@^0.2.2: resolved "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz" integrity sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ== -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: +expect@30.0.5, expect@^30.0.0: version "30.0.5" resolved "https://registry.npmjs.org/expect/-/expect-30.0.5.tgz" integrity sha512-P0te2pt+hHI5qLJkIR+iMvS+lYUZml8rKKsohVHAGY+uClp9XVbdyYNJOIjSRpHVp8s8YqxJCiHUkSYZGr8rtQ== @@ -1976,7 +1794,7 @@ fast-deep-equal@^3.1.3: 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.1.0, fast-json-stable-stringify@2.x: +fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== @@ -1986,7 +1804,7 @@ fast-uri@^3.0.1: resolved "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz" integrity sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw== -fb-watchman@^2.0.0, fb-watchman@^2.0.2: +fb-watchman@^2.0.2: version "2.0.2" resolved "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz" integrity sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA== @@ -2049,6 +1867,11 @@ fs.realpath@^1.0.0: 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.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" @@ -2110,18 +1933,6 @@ 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.npmjs.org/glob/-/glob-7.2.3.tgz" @@ -2150,7 +1961,7 @@ gopd@^1.2.0: 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.9: +graceful-fs@^4.2.11: version "4.2.11" resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -2221,7 +2032,7 @@ ieee754@^1.1.13, ieee754@^1.2.1: resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== -import-local@^3.0.2, import-local@^3.2.0: +import-local@^3.2.0: version "3.2.0" resolved "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz" integrity sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA== @@ -2242,7 +2053,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@^2.0.3, inherits@^2.0.4, inherits@2: +inherits@2, inherits@^2.0.3, inherits@^2.0.4: version "2.0.4" resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -2315,19 +2126,12 @@ is-arrayish@^0.2.1: 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.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz" integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== -is-generator-fn@^2.0.0, is-generator-fn@^2.1.0: +is-generator-fn@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz" integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== @@ -2372,17 +2176,6 @@ istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: 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.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz" @@ -2403,15 +2196,6 @@ istanbul-lib-report@^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.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz" @@ -2475,15 +2259,6 @@ jackspeak@^3.1.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.npmjs.org/jest-changed-files/-/jest-changed-files-30.0.5.tgz" @@ -2493,32 +2268,6 @@ jest-changed-files@30.0.5: 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.npmjs.org/jest-circus/-/jest-circus-30.0.5.tgz" @@ -2545,23 +2294,6 @@ 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.npmjs.org/jest-cli/-/jest-cli-30.0.5.tgz" @@ -2578,34 +2310,6 @@ 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.npmjs.org/jest-config/-/jest-config-30.0.5.tgz" @@ -2636,16 +2340,6 @@ 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.npmjs.org/jest-diff/-/jest-diff-30.0.5.tgz" @@ -2656,13 +2350,6 @@ 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.npmjs.org/jest-docblock/-/jest-docblock-30.0.1.tgz" @@ -2670,17 +2357,6 @@ jest-docblock@30.0.1: 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.npmjs.org/jest-each/-/jest-each-30.0.5.tgz" @@ -2692,18 +2368,6 @@ 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.npmjs.org/jest-environment-node/-/jest-environment-node-30.0.5.tgz" @@ -2717,30 +2381,6 @@ 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.npmjs.org/jest-haste-map/-/jest-haste-map-30.0.5.tgz" @@ -2759,14 +2399,6 @@ 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.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.0.5.tgz" @@ -2775,16 +2407,6 @@ jest-leak-detector@30.0.5: "@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.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.0.5.tgz" @@ -2795,21 +2417,6 @@ 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.npmjs.org/jest-message-util/-/jest-message-util-30.0.5.tgz" @@ -2825,15 +2432,6 @@ 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.npmjs.org/jest-mock/-/jest-mock-30.0.5.tgz" @@ -2843,29 +2441,16 @@ jest-mock@30.0.5: "@types/node" "*" jest-util "30.0.5" -jest-pnp-resolver@^1.2.2, jest-pnp-resolver@^1.2.3: +jest-pnp-resolver@^1.2.3: version "1.2.3" 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.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.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.0.5.tgz" @@ -2874,7 +2459,7 @@ jest-resolve-dependencies@30.0.5: jest-regex-util "30.0.1" jest-snapshot "30.0.5" -jest-resolve@*, jest-resolve@30.0.5: +jest-resolve@30.0.5: version "30.0.5" resolved "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.0.5.tgz" integrity sha512-d+DjBQ1tIhdz91B79mywH5yYu76bZuE96sSbxj8MkjWVx5WNdt1deEFRONVL4UkKLSrAbMkdhb24XN691yDRHg== @@ -2888,48 +2473,6 @@ jest-resolve@*, 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.npmjs.org/jest-runner/-/jest-runner-30.0.5.tgz" @@ -2958,34 +2501,6 @@ 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.npmjs.org/jest-runtime/-/jest-runtime-30.0.5.tgz" @@ -3014,32 +2529,6 @@ 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.npmjs.org/jest-snapshot/-/jest-snapshot-30.0.5.tgz" @@ -3067,7 +2556,7 @@ jest-snapshot@30.0.5: semver "^7.7.2" synckit "^0.11.8" -"jest-util@^29.0.0 || ^30.0.0", jest-util@30.0.5: +jest-util@30.0.5: version "30.0.5" resolved "https://registry.npmjs.org/jest-util/-/jest-util-30.0.5.tgz" integrity sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g== @@ -3079,30 +2568,6 @@ jest-snapshot@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.npmjs.org/jest-validate/-/jest-validate-30.0.5.tgz" @@ -3115,20 +2580,6 @@ 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.npmjs.org/jest-watcher/-/jest-watcher-30.0.5.tgz" @@ -3143,16 +2594,6 @@ 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.npmjs.org/jest-worker/-/jest-worker-30.0.5.tgz" @@ -3164,7 +2605,7 @@ jest-worker@30.0.5: merge-stream "^2.0.0" supports-color "^8.1.1" -"jest@^29.0.0 || ^30.0.0", "jest@^29.5.0 || ^30.0.5", jest@^30.0.0: +jest@^30.0.0: version "30.0.5" resolved "https://registry.npmjs.org/jest/-/jest-30.0.5.tgz" integrity sha512-y2mfcJywuTUkvLm2Lp1/pFX8kTgMO5yyQGq/Sk/n2mN7XWYp4JsCZ/QXW34M8YScgk8bPZlREH04f6blPnoHnQ== @@ -3174,16 +2615,6 @@ jest-worker@30.0.5: 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.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" @@ -3222,11 +2653,6 @@ jssha@3.2.0: 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.npmjs.org/leven/-/leven-3.1.0.tgz" @@ -3272,11 +2698,6 @@ long@^4.0.0: 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.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz" @@ -3289,6 +2710,11 @@ lru-cache@^5.1.1: dependencies: yallist "^3.0.2" +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== + make-dir@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz" @@ -3325,7 +2751,7 @@ merge-stream@^2.0.0: resolved "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz" integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== -micromatch@^4.0.4, micromatch@^4.0.8: +micromatch@^4.0.8: version "4.0.8" resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz" integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== @@ -3540,11 +2966,6 @@ path-normalize@^6.0.13: 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.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz" @@ -3563,7 +2984,7 @@ picocolors@^1.1.1: resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz" integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== -picomatch@^2.0.4, picomatch@^2.2.3, picomatch@^2.3.1: +picomatch@^2.0.4, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== @@ -3573,7 +2994,7 @@ picomatch@^4.0.2: resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz" integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q== -pirates@^4.0.4, pirates@^4.0.7: +pirates@^4.0.7: version "4.0.7" resolved "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz" integrity sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA== @@ -3585,16 +3006,7 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.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: +pretty-format@30.0.5, pretty-format@^30.0.0: version "30.0.5" resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz" integrity sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw== @@ -3603,14 +3015,6 @@ pretty-format@^30.0.0, pretty-format@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.npmjs.org/protobufjs/-/protobufjs-6.11.4.tgz" @@ -3635,11 +3039,6 @@ proxy-from-env@^1.1.0: 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.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz" @@ -3662,7 +3061,7 @@ rabin-wasm@^0.1.4: node-fetch "^2.6.1" readable-stream "^3.6.0" -react-is@^18.0.0, react-is@^18.3.1: +react-is@^18.3.1: version "18.3.1" resolved "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz" integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== @@ -3698,20 +3097,6 @@ resolve-from@^5.0.0: 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.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz" @@ -3742,22 +3127,12 @@ safe-buffer@~5.2.0: resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -semver@^6.3.0, semver@^6.3.1: +semver@^6.3.1: version "6.3.1" resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -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.npmjs.org/semver/-/semver-7.7.2.tgz" - integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA== - -semver@^7.7.2: +semver@^7.5.3, semver@^7.5.4, 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== @@ -3774,7 +3149,7 @@ shebang-regex@^3.0.0: 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.7: +signal-exit@^3.0.2, signal-exit@^3.0.3: version "3.0.7" resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== @@ -3784,11 +3159,6 @@ signal-exit@^4.0.1: 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.npmjs.org/slash/-/slash-3.0.0.tgz" @@ -3826,21 +3196,14 @@ sprintf-js@~1.0.2: resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz" integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== -stack-utils@^2.0.3, stack-utils@^2.0.6: +stack-utils@^2.0.6: version "2.0.6" 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_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: +string-length@^4.0.2: version "4.0.2" resolved "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz" integrity sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ== @@ -3875,6 +3238,13 @@ string-width@^5.0.1, string-width@^5.1.2: emoji-regex "^9.2.2" strip-ansi "^7.0.1" +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" + "strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" @@ -3918,13 +3288,6 @@ supports-color@^7.1.0: 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.npmjs.org/supports-color/-/supports-color-8.1.1.tgz" @@ -3932,11 +3295,6 @@ supports-color@^8.1.1: 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.npmjs.org/symbol.inspect/-/symbol.inspect-1.0.1.tgz" @@ -4049,7 +3407,7 @@ 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@>=9.0.0: +ts-node@^10.9.1, ts-node@^10.9.2: version "10.9.2" resolved "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz" integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== @@ -4068,7 +3426,7 @@ ts-node@^10.9.1, ts-node@^10.9.2, ts-node@>=9.0.0: v8-compile-cache-lib "^3.0.1" yn "3.1.1" -tslib@^2.1.0: +tslib@^2.1.0, tslib@^2.4.0: version "2.8.1" resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== @@ -4078,7 +3436,7 @@ tweetnacl-util@^0.15.1: 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.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz" integrity sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw== @@ -4098,7 +3456,7 @@ type-fest@^4.41.0: resolved "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz" integrity sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA== -typescript@^5.9.2, typescript@>=2.7, "typescript@>=4.3 <6": +typescript@^5.9.2: version "5.9.2" resolved "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz" integrity sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A== @@ -4254,14 +3612,6 @@ wrappy@1: 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.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz" @@ -4270,7 +3620,7 @@ write-file-atomic@^5.0.1: imurmurhash "^0.1.4" signal-exit "^4.0.1" -ws@*, ws@^8.8.1: +ws@^8.8.1: version "8.18.3" resolved "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz" integrity sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg== @@ -4295,7 +3645,7 @@ yargs-parser@^21.1.1: resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz" integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== -yargs@^17.3.1, yargs@^17.7.2: +yargs@^17.7.2: version "17.7.2" resolved "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz" integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== diff --git a/scripts/build-contracts.sh b/scripts/build-contracts.sh index 91d3ad33eb..321a34bd25 100755 --- a/scripts/build-contracts.sh +++ b/scripts/build-contracts.sh @@ -19,4 +19,4 @@ yarn build --all copy_code Elector copy_code ElectorPoA copy_code Config -copy_code SlasherStub +copy_code Slasher diff --git a/slasher-traits/src/validator.rs b/slasher-traits/src/validator.rs index 74cf94376d..5ba2284e80 100644 --- a/slasher-traits/src/validator.rs +++ b/slasher-traits/src/validator.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use std::sync::atomic::{AtomicBool, AtomicU8, Ordering}; use indexmap::IndexMap; +use tycho_types::cell::HashBytes; use tycho_types::models::{BlockId, IndexedValidatorDescription, McStateExtra}; use tycho_util::FastHasherState; @@ -65,10 +66,16 @@ impl ValidatorEvents { session_id: ValidationSessionId, first_mc_seqno: u32, own_validator_idx: u16, + vset_hash: &HashBytes, validators: &[IndexedValidatorDescription], ) -> ValidatorSessionScope { - self.listener - .on_session_started(session_id, first_mc_seqno, own_validator_idx, validators); + self.listener.on_session_started( + session_id, + first_mc_seqno, + own_validator_idx, + vset_hash, + validators, + ); let mut remap = IndexMap::::with_capacity_and_hasher( validators.len(), @@ -233,6 +240,7 @@ pub trait ValidatorEventsListener: Send + Sync + 'static { session_id: ValidationSessionId, first_mc_seqno: u32, own_validator_idx: u16, + vset_hash: &HashBytes, validators: &[IndexedValidatorDescription], ); @@ -260,6 +268,7 @@ impl ValidatorEventsListener for NoopValidatorEventsRecorder { _session_id: ValidationSessionId, _first_mc_seqno: u32, _own_validator_idx: u16, + _vset_hash: &HashBytes, _validators: &[IndexedValidatorDescription], ) { } @@ -288,9 +297,16 @@ macro_rules! impl_recorder_for_tuples { session_id: ValidationSessionId, first_mc_seqno: u32, own_validator_idx: u16, + vset_hash: &HashBytes, validators: &[IndexedValidatorDescription], ) { - $(self.$n.on_session_started(session_id, first_mc_seqno, own_validator_idx, validators);)+ + $(self.$n.on_session_started( + session_id, + first_mc_seqno, + own_validator_idx, + vset_hash, + validators + );)+ } fn on_session_finished(&self, session_id: ValidationSessionId) { diff --git a/slasher/src/analyzer.rs b/slasher/src/analyzer.rs index 2022e23ea6..af6a4e31ee 100644 --- a/slasher/src/analyzer.rs +++ b/slasher/src/analyzer.rs @@ -1,19 +1,8 @@ use anyhow::Result; -use crate::ParsedVset; -use crate::bc::SlasherParams; use crate::storage::SlasherStorageSnapshot; +use crate::{ParsedVset, SlasherConfig}; -// TODO: Move these constants into config. - -// Session should contain at least this amount of blocks to do some reporting. -const MIN_VSET_LENGTH: u32 = 1000; -// At least this number of samples must be collected to accuse someone. -const MIN_SAMPLES: u64 = 100; -// At least this number of malformed batches must be collected to accuse someone. -const MIN_MALFORMED: u64 = 5; -// We treat the node as slow if its block rate is this times the median rate. -const SLOW_FACTOR: f64 = 0.5; // See https://en.wikipedia.org/wiki/Z-test const Z_95: f64 = 1.96; @@ -23,25 +12,24 @@ pub fn analyze_vset( vset: &ParsedVset, last_seqno: u32, own_validator_idx: usize, - _params: &SlasherParams, + config: &SlasherConfig, ) -> Result> { let vset_hash = &vset.hash; let vset = &vset.vset; // Compute vset block range. - let session_ids = snapshot.load_vset_sessions(vset_hash)?; - let start_seqno = session_ids - .iter() - .map(|item| item.start_seqno) - .min() - .unwrap_or(u32::MAX); - let vset_len = last_seqno.saturating_sub(start_seqno); - if vset_len < MIN_VSET_LENGTH { + let mut start_seqno = u32::MAX; + for item in snapshot.iter_sessions(vset_hash) { + let session = item?; + start_seqno = std::cmp::min(start_seqno, session.start_seqno); + } + let vset_len = last_seqno.saturating_add(1).saturating_sub(start_seqno); + if vset_len < config.vset_len_threshold { tracing::warn!(vset_len, "too short vset"); return Ok(Vec::new()); } - let n = vset.list.len(); + let n = vset.list.len().min(vset.main.get() as _); if n <= 1 { tracing::warn!(n, "not enough nodes in vset"); return Ok(Vec::new()); @@ -50,12 +38,7 @@ pub fn analyze_vset( let mut scores = vec![vec![Score::default(); n]; n]; let mut observed = vec![Observed::default(); n]; - let max_weight = vset - .list - .iter() - .take(vset.main.get() as usize) - .map(|v| v.weight) - .sum::(); + let max_weight = vset.list.iter().take(n).map(|v| v.weight).sum::(); let weight_threshold = max_weight.saturating_mul(2) / 3 + 1; // Build a matrix from all known block batches. @@ -90,7 +73,7 @@ pub fn analyze_vset( } let weight = vset.list[other].weight; - for block in 0..block_count { + for (block, total_block_weight) in weight_per_block.iter_mut().enumerate() { if !batch.committed_blocks.get(block) { // Ignore blocks which observer did not collate. continue; @@ -98,7 +81,7 @@ pub fn analyze_vset( let valid_bit = block * 2 + 1; if history.bits.get(valid_bit) { - weight_per_block[block] += weight; + *total_block_weight += weight; } } } @@ -145,20 +128,24 @@ pub fn analyze_vset( let mut accusation_weights = vec![0; n]; let mut rates = Vec::with_capacity(n - 1); for (observer, (observed, scores)) in std::iter::zip(&observed, &scores).enumerate() { - if observed.samples < MIN_SAMPLES { + if observed.samples < config.block_samples_threshold { continue; } let observer_weight = vset.list[observer].weight; // Compute the rate of valid signatures from other nodes. rates.clear(); - rates.extend(scores.iter().enumerate().filter_map(|(i, score)| { - (i != observer).then(|| score.valid_signatures as f64 / observed.samples as f64) - })); + rates.extend( + scores + .iter() + .enumerate() + .filter(|(i, _)| *i != observer) + .map(|(_, score)| score.valid_signatures as f64 / observed.samples as f64), + ); rates.sort_by(|a, b| a.total_cmp(b)); let baseline = rates[rates.len() / 2]; - let slow_threshold = baseline * SLOW_FACTOR; + let slow_threshold = baseline * config.slow_node_factor; tracing::debug!(baseline, slow_threshold, "computed valid signature rates"); @@ -193,8 +180,9 @@ pub fn analyze_vset( let accusations = std::iter::zip(observed, accusation_weights) .enumerate() .filter_map(|(idx, (observed, weight))| { - let should_accuse = weight >= weight_threshold || observed.malformed >= MIN_MALFORMED; - if idx == own_validator_idx { + let should_accuse = weight >= weight_threshold + || observed.malformed >= config.malformed_samples_threshold; + if should_accuse && idx == own_validator_idx { tracing::warn!( own_validator_idx, weight, diff --git a/slasher/src/bc/stub_contract.rs b/slasher/src/bc/contract.rs similarity index 93% rename from slasher/src/bc/stub_contract.rs rename to slasher/src/bc/contract.rs index ebea4f3224..7db733e8f5 100644 --- a/slasher/src/bc/stub_contract.rs +++ b/slasher/src/bc/contract.rs @@ -23,27 +23,31 @@ use crate::util::BitSet; /// ``` #[derive(Debug, Store, Load)] #[tlb(tag = "#01")] -pub struct StubSlasherParams { +pub struct StdSlasherParams { pub address: HashBytes, pub blocks_batch_size: NonZeroU8, } -impl StubSlasherParams { +impl StdSlasherParams { pub const IDX: u32 = 666; } -pub struct StubSlasherContract; +pub struct StdSlasherContract; -impl SlasherContract for StubSlasherContract { +impl StdSlasherContract { + const OP_SEND_BLOCKS_BATCH: u32 = 0x60e2ac7f; +} + +impl SlasherContract for StdSlasherContract { fn default_batch_size(&self) -> NonZeroU32 { NonZeroU32::new(10).unwrap() } fn find_params(&self, config: &BlockchainConfigParams) -> Result> { - let Some(raw) = config.get_raw_cell_ref(StubSlasherParams::IDX)? else { + let Some(raw) = config.get_raw_cell_ref(StdSlasherParams::IDX)? else { return Ok(None); }; - let params = raw.parse::()?; + let params = raw.parse::()?; Ok(Some(SlasherParams { address: params.address, blocks_batch_size: params.blocks_batch_size.into(), @@ -63,6 +67,8 @@ impl SlasherContract for StubSlasherContract { let mut b = CellBuilder::new(); b.store_u64(now)?; b.store_u32(expire_at)?; + b.store_u32(Self::OP_SEND_BLOCKS_BATCH)?; + b.store_u256(¶ms.vset_hash)?; b.store_u16(params.validator_idx)?; b.store_reference(cell)?; b.build()? @@ -118,6 +124,12 @@ impl SlasherContract for StubSlasherContract { // TODO: Add message op let mut body = msg.body; body.skip_first(512 + 64 + 32, 0)?; + let op = body.load_u32()?; + if op != Self::OP_SEND_BLOCKS_BATCH { + return Ok(None); + } + + body.skip_first(256, 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)?; diff --git a/slasher/src/bc/mod.rs b/slasher/src/bc/mod.rs index b19414707b..5d1c9203d1 100644 --- a/slasher/src/bc/mod.rs +++ b/slasher/src/bc/mod.rs @@ -11,16 +11,17 @@ use tycho_types::models::{ }; use tycho_util::FastDashMap; -pub use self::stub_contract::StubSlasherContract; +pub use self::contract::StdSlasherContract; use crate::util::BitSet; -mod stub_contract; +mod contract; #[derive(Clone, Copy)] pub struct EncodeBlocksBatchMessage<'a> { pub address: &'a StdAddr, pub session_id: ValidationSessionId, pub batch: &'a BlocksBatch, + pub vset_hash: &'a HashBytes, pub validator_idx: u16, pub signature_context: SignatureContext, pub keypair: &'a ed25519::KeyPair, diff --git a/slasher/src/collector/validator_events.rs b/slasher/src/collector/validator_events.rs index b8859a862d..9f0e4767f5 100644 --- a/slasher/src/collector/validator_events.rs +++ b/slasher/src/collector/validator_events.rs @@ -9,6 +9,7 @@ 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; @@ -28,6 +29,7 @@ pub struct ValidatorEventsCollector { #[derive(Debug, Clone)] pub struct ValidatorSessionInfo { + pub vset_hash: HashBytes, pub session_id: ValidationSessionId, pub first_mc_seqno: u32, pub own_validator_idx: u16, @@ -128,6 +130,7 @@ impl ValidatorEventsListener for ValidatorEventsCollector { session_id: ValidationSessionId, first_mc_seqno: u32, own_validator_idx: u16, + vset_hash: &HashBytes, validators: &[IndexedValidatorDescription], ) { tracing::debug!(first_mc_seqno, "on_session_started"); @@ -154,6 +157,7 @@ impl ValidatorEventsListener for ValidatorEventsCollector { }); self.push_session_to_init(ValidatorSessionInfo { + vset_hash: *vset_hash, session_id, first_mc_seqno, own_validator_idx, @@ -286,6 +290,7 @@ impl SessionState { } let Some(tx) = complete_batches else { + // Something is really broken if ~100 blocks were not enough to initialize session. anyhow::bail!("not initialized"); }; diff --git a/slasher/src/lib.rs b/slasher/src/lib.rs index 02cda659c1..8aa87441eb 100644 --- a/slasher/src/lib.rs +++ b/slasher/src/lib.rs @@ -9,12 +9,13 @@ use tokio::sync::mpsc; use tokio_util::sync::CancellationToken; use tracing::instrument; use tycho_block_util::config::BlockchainConfigExt; +use tycho_block_util::state::ShardStateStuff; 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::models::{BlockchainConfig, SignatureContext, StdAddr, ValidatorSet}; +use tycho_types::models::{SignatureContext, StdAddr, ValidatorSet}; use tycho_types::prelude::*; use tycho_util::config::PartialConfig; use tycho_util::futures::JoinTask; @@ -23,10 +24,11 @@ use tycho_util::serde_helpers; use self::bc::SlasherParams; pub use self::bc::{ BlocksBatch, ContractSubscription, EncodeBlocksBatchMessage, MessageDelivered, - SignatureHistory, SignedMessage, SlasherContract, StubSlasherContract, + SignatureHistory, SignedMessage, SlasherContract, StdSlasherContract, }; use self::collector::{ValidatorEventsCollector, ValidatorSessionInfo}; use self::storage::SlasherStorage; +use self::storage::models::{StoredVsetInfo, StoredVsetReport}; use self::util::AtomicValidationSessionId; mod analyzer; @@ -61,10 +63,26 @@ pub struct SlasherConfig { #[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. + /// Validator set round must contain at least this amount of blocks + /// to run analyzer. /// - /// Default: `100` - pub bad_sessions_weight_threshold: u64, + /// Default: `1000` + pub vset_len_threshold: u32, + + // At least this number of block samples must be collected to accuse someone. + // + // Default: `100` + pub block_samples_threshold: u64, + + /// At least this number of malformed batches must be collected to accuse someone. + /// + /// Default: `5` + pub malformed_samples_threshold: u64, + + /// We treat the node as slow if its block rate is this times the median rate. + /// + /// Default: `0.5` + pub slow_node_factor: f64, } impl Default for SlasherConfig { @@ -73,7 +91,10 @@ 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: 100, + vset_len_threshold: 1000, + block_samples_threshold: 100, + malformed_samples_threshold: 5, + slow_node_factor: 0.5, } } } @@ -85,17 +106,25 @@ pub struct Slasher { } impl Slasher { - #[allow(clippy::too_many_arguments)] pub fn new( node_keys: Arc, contract: C, blockchain_rpc_client: BlockchainRpcClient, storage_context: &StorageContext, config: SlasherConfig, - global_id: i32, - blockchain_config: &BlockchainConfig, - known_session_id: ValidationSessionId, + last_mc_state: &ShardStateStuff, ) -> Result { + anyhow::ensure!( + last_mc_state.block_id().is_masterchain(), + "slasher init requires masterchain state" + ); + + let global_id = last_mc_state.as_ref().global_id; + + let state_extra = last_mc_state.state_extra()?; + let known_session_id = tycho_slasher_traits::ValidationSessionId::from(state_extra); + let blockchain_config = &state_extra.config; + let storage = SlasherStorage::open(storage_context).context("failed to open slasher storage")?; @@ -123,6 +152,16 @@ impl Slasher { )); let global = blockchain_config.get_global_version()?; + if !storage.contains_vset_info(¤t_vset.hash)? { + let start_seqno = last_mc_state.block_id().seqno; + storage.store_vset_session(¤t_vset.hash, known_session_id, start_seqno)?; + storage.begin_vset(¤t_vset.hash, &StoredVsetInfo { + prev_vset_hash: [0; 32], + first_session_id: known_session_id, + start_seqno, + })?; + } + // TODO: Spawn previous unsubmitted reports. Ok(Self { @@ -230,10 +269,22 @@ impl Slasher { ); this.known_session_id.set(current_session_id); this.storage - .store_vset_session(current_session_id, ¤t_vset_hash, mc_seqno)?; + .store_vset_session(¤t_vset_hash, current_session_id, mc_seqno)?; } - // TODO: Cleanup stored vset sessions. + if let Some(prev_vset) = &vset_to_complete { + anyhow::ensure!( + session_changed, + "validation session must change when validation set changes" + ); + self.shared + .storage + .begin_vset(¤t_vset_hash, &StoredVsetInfo { + prev_vset_hash: prev_vset.hash.0, + first_session_id: current_session_id, + start_seqno: mc_seqno, + })?; + } // Prepare slasher handler context. let Some(slasher_params) = this.parsed_config.load().slasher_params.clone() else { @@ -241,6 +292,7 @@ impl Slasher { return Ok(()); }; let Some(subscription) = this.subscription.load_full() else { + // Probably unreachable branch, but means the same - no subscription no slashing. return Ok(()); }; @@ -375,7 +427,7 @@ impl SlasherSharedState { &self, vset: &ParsedVset, last_seqno: u32, - params: &SlasherParams, + _params: &SlasherParams, ) -> Result<()> { let Some(own_validator_idx) = vset.vset.list.iter().position(|item| { @@ -391,10 +443,17 @@ impl SlasherSharedState { vset, last_seqno, own_validator_idx, - params, + &self.config, )?; tracing::warn!("slasher accusations: {accusations:?}"); + self.storage + .store_vset_report(&vset.hash, &StoredVsetReport { + public_key: *self.node_keys.public_key.as_bytes(), + accusations, + })?; + + // TODO: Spawn vote sender. Ok(()) } @@ -409,6 +468,10 @@ impl SlasherSharedState { let mut send_task = None; + // NOTE: `send_task` will be cancelled when `rx` returns `None`. + // This is intentional - block batches are quite small and there + // is no requirement for them to be delivered. The last batch + // will definitly be cancelled and there is no problem in this. while let Some(batch) = rx.recv().await { if let Some(send_task) = send_task.take() && let Some(timeout) = self.config.prev_delivery_timeout @@ -419,6 +482,7 @@ impl SlasherSharedState { send_task = Some(JoinTask::new(self.clone().deliver_batch_message( info.session_id, + info.vset_hash, info.own_validator_idx, batch, ))); @@ -428,6 +492,7 @@ impl SlasherSharedState { async fn deliver_batch_message( self: Arc, session_id: ValidationSessionId, + vset_hash: HashBytes, validator_idx: u16, batch: BlocksBatch, ) { @@ -442,6 +507,7 @@ impl SlasherSharedState { address: subscription.address(), session_id, batch: &batch, + vset_hash: &vset_hash, validator_idx, signature_context, keypair: &self.node_keys, diff --git a/slasher/src/proto.tl b/slasher/src/proto.tl index c8cd713665..97caf47d26 100644 --- a/slasher/src/proto.tl +++ b/slasher/src/proto.tl @@ -20,17 +20,20 @@ slasher.signatureHistory bits:bitset = slasher.SignatureHistory; -slasher.report - pubkey:int256 - validator_idx:int - entries:(vector slasher.ReportEntry) - = slasher.Report; +slasher.storedVsetInfo + prev_vset_hash:int256 + first_session_id:slasher.validationSessionId + start_seqno:int + = slasher.StoredVsetInfo; -slasher.reportEntry - validator_idx:int - good_score:int - bad_score:int - punish:bool - = slasher.ReportEntry; +slasher.storedVsetReport + public_key:int256 + accusations:(vector int) + = slasher.StoredVsetReport; + +slasher.validationSessionId + catchain_seqno:int + vset_switch_round:int + = slasher.ValidationSessionId; bitset length:int data:bytes = BitSet; diff --git a/slasher/src/storage/db.rs b/slasher/src/storage/db.rs index 7cfb03b5c9..a68c220652 100644 --- a/slasher/src/storage/db.rs +++ b/slasher/src/storage/db.rs @@ -30,6 +30,7 @@ impl WithMigrations for SlasherTables { weedb::tables! { pub struct SlasherTables { pub state: tables::State, + pub vset_state: tables::VsetState, pub vset_sessions: tables::VsetSessions, pub block_batches: tables::BlockBatches, } @@ -59,15 +60,36 @@ pub mod tables { } } + /// Info about validator sets. + /// + /// - Key: `vset_hash: 32 bytes, key: u8`. + pub struct VsetState; + + impl VsetState { + pub const KEY_LEN: usize = 32 + 1; + + pub const KEY_TYPE_INFO: u8 = 0; + pub const KEY_TYPE_REPORT: u8 = 1; + } + + impl ColumnFamily for VsetState { + const NAME: &'static str = "vset_state"; + } + + impl ColumnFamilyOptions for VsetState { + fn options(opts: &mut Options, ctx: &mut TableContext) { + default_block_based_table_factory(opts, ctx); + } + } + /// Maps validator sessions to vset. /// - /// - Key: `session_id: (catchain_seqno u32 BE, vset_switch_round u32 BE)` - /// - Value: `vset_hash: 32 bytes, start_seqno: u32 LE` + /// - Key: `vset_hash: 32 bytes, session_id: (catchain_seqno u32 BE, vset_switch_round u32 BE)` + /// - Value: `start_seqno: u32 LE` pub struct VsetSessions; impl VsetSessions { - pub const KEY_LEN: usize = 4 + 4; - pub const VALUE_LEN: usize = 32 + 4; + pub const KEY_LEN: usize = 32 + 4 + 4; } impl ColumnFamily for VsetSessions { diff --git a/slasher/src/storage/mod.rs b/slasher/src/storage/mod.rs index d7f889b926..0413cbb98c 100644 --- a/slasher/src/storage/mod.rs +++ b/slasher/src/storage/mod.rs @@ -7,7 +7,7 @@ use tycho_types::cell::HashBytes; use weedb::{OwnedSnapshot, rocksdb}; use self::db::{SlasherDb, tables}; -use self::models::StoredBlocksBatch; +use self::models::{StoredBlocksBatch, StoredVsetInfo, StoredVsetReport}; use crate::BlocksBatch; pub mod db; @@ -42,7 +42,7 @@ impl SlasherStorage { vset_hash: &HashBytes, validator_idx: u16, batch: &BlocksBatch, - ) -> Result<()> { + ) -> Result<(), rocksdb::Error> { let key = block_batches_key(vset_hash, validator_idx, batch.start_seqno); let value = tl_proto::serialize(StoredBlocksBatch::wrap(batch)); self.inner.db.block_batches.insert(key, value)?; @@ -51,15 +51,80 @@ impl SlasherStorage { pub fn store_vset_session( &self, - session_id: ValidationSessionId, vset_hash: &HashBytes, + session_id: ValidationSessionId, start_seqno: u32, - ) -> Result<()> { - let key = session_key(session_id); - let value = session_value(vset_hash, start_seqno); - self.inner.db.vset_sessions.insert(key, value)?; + ) -> Result<(), rocksdb::Error> { + let key = vset_session_key(vset_hash, session_id); + self.inner + .db + .vset_sessions + .insert(key, start_seqno.to_le_bytes())?; + Ok(()) + } + + pub fn load_vset_info(&self, vset_hash: &HashBytes) -> Result> { + let table = &self.inner.db.vset_state; + match table.get(vset_state_key(vset_hash, tables::VsetState::KEY_TYPE_INFO))? { + Some(data) => Ok(Some(tl_proto::deserialize::(&data)?)), + None => Ok(None), + } + } + + pub fn contains_vset_info(&self, vset_hash: &HashBytes) -> Result { + self.inner + .db + .vset_state + .contains_key(vset_state_key(vset_hash, tables::VsetState::KEY_TYPE_INFO)) + } + + pub fn begin_vset(&self, vset_hash: &HashBytes, info: &StoredVsetInfo) -> Result<()> { + let db = &self.inner.db; + + let mut batch = rocksdb::WriteBatch::new(); + + // Remove vset before the previous validator set. + if let Some(prev_vset) = self.load_vset_info(HashBytes::wrap(&info.prev_vset_hash))? { + let vset_to_remove = HashBytes::wrap(&prev_vset.prev_vset_hash); + + batch.delete_range_cf( + &db.state.cf(), + vset_state_key(vset_to_remove, 0), + vset_state_key(vset_to_remove, u8::MAX), + ); + batch.delete_range_cf( + &db.vset_sessions.cf(), + vset_session_key(vset_to_remove, ZERO_SESSION), + vset_session_key(vset_to_remove, MAX_SESSION), + ); + batch.delete_range_cf( + &db.block_batches.cf(), + block_batches_key(vset_to_remove, 0, 0), + block_batches_key(vset_to_remove, u16::MAX, u32::MAX), + ); + } + + batch.put_cf( + &db.state.cf(), + vset_state_key(vset_hash, tables::VsetState::KEY_TYPE_INFO), + tl_proto::serialize(info), + ); + + db.rocksdb() + .write_opt(batch, db.vset_state.write_config())?; Ok(()) } + + pub fn store_vset_report( + &self, + vset_hash: &HashBytes, + report: &StoredVsetReport, + ) -> Result<(), rocksdb::Error> { + self.inner.db.vset_state.insert( + vset_state_key(vset_hash, tables::VsetState::KEY_TYPE_REPORT), + tl_proto::serialize(report), + ) + } } struct Inner { @@ -73,46 +138,19 @@ pub struct SlasherStorageSnapshot { } impl SlasherStorageSnapshot { - pub fn load_vset_sessions(&self, vset_hash: &HashBytes) -> Result> { - let mut iter = self.snapshot.raw_iterator_cf_opt( - &self.db.vset_sessions.cf(), - self.db.vset_sessions.new_read_config(), - ); - iter.seek_to_last(); - - let mut result = Vec::new(); - let mut vset_seen = false; - loop { - let (key, value) = match iter.item() { - Some(item) => item, - None => { - iter.status()?; - break; - } - }; - - let session_vset_hash = HashBytes::from_slice(&value[0..32]); - if &session_vset_hash == vset_hash { - vset_seen = true; - let session_id = 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()), - }; - - let start_seqno = u32::from_le_bytes(value[32..36].try_into().unwrap()); - - result.push(VsetSession { - id: session_id, - start_seqno, - }); - } else if vset_seen { - break; - } + pub fn iter_sessions(&self, vset_hash: &HashBytes) -> VsetSessionsIter<'_> { + let mut readopts = self.db.vset_sessions.new_read_config(); + readopts.set_snapshot(&self.snapshot); + readopts.set_iterate_lower_bound(vset_session_key(vset_hash, ZERO_SESSION)); + readopts.set_iterate_upper_bound(vset_session_key(vset_hash, MAX_SESSION)); - iter.prev(); - } + let mut raw = self + .db + .rocksdb() + .raw_iterator_cf_opt(&self.db.vset_sessions.cf(), readopts); + raw.seek_to_first(); - Ok(result) + VsetSessionsIter { raw, broken: false } } pub fn iter_block_batches(&self, vset_hash: &HashBytes) -> BlockBatchesIter<'_> { @@ -133,6 +171,41 @@ impl SlasherStorageSnapshot { } } +pub struct VsetSessionsIter<'a> { + raw: rocksdb::DBRawIterator<'a>, + broken: bool, +} + +impl Iterator for VsetSessionsIter<'_> { + type Item = Result; + + fn next(&mut self) -> Option { + if self.broken { + return None; + } + + let (key, value) = match self.raw.item() { + Some(item) => item, + None => match self.raw.status() { + Ok(()) => return None, + Err(e) => { + self.broken = true; + return Some(Err(e.into())); + } + }, + }; + + let id = ValidationSessionId { + catchain_seqno: u32::from_be_bytes(key[32..36].try_into().unwrap()), + vset_switch_round: u32::from_be_bytes(key[36..40].try_into().unwrap()), + }; + let start_seqno = u32::from_le_bytes(value[0..4].try_into().unwrap()); + + self.raw.next(); + Some(Ok(VsetSession { id, start_seqno })) + } +} + pub struct BlockBatchesIter<'a> { raw: rocksdb::DBRawIterator<'a>, broken: bool, @@ -161,7 +234,7 @@ impl Iterator for BlockBatchesIter<'_> { let batch = match tl_proto::deserialize(value) { Ok(StoredBlocksBatch(batch)) => batch, Err(e) => { - let start_seqno = u32::from_be_bytes(key[34..48].try_into().unwrap()); + let start_seqno = u32::from_be_bytes(key[34..38].try_into().unwrap()); self.broken = true; return Some(Err(anyhow::anyhow!( "invalid stored blocks batch \ @@ -183,18 +256,22 @@ pub struct VsetSession { pub start_seqno: u32, } -fn session_key(session_id: ValidationSessionId) -> [u8; tables::VsetSessions::KEY_LEN] { - let mut key = [0u8; tables::VsetSessions::KEY_LEN]; - 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()); +fn vset_state_key(vset_hash: &HashBytes, ty: u8) -> [u8; tables::VsetState::KEY_LEN] { + let mut key = [0u8; tables::VsetState::KEY_LEN]; + key[..32].copy_from_slice(vset_hash.as_slice()); + key[32] = ty; key } -fn session_value(vset_hash: &HashBytes, start_seqno: u32) -> [u8; tables::VsetSessions::VALUE_LEN] { - let mut value = [0u8; tables::VsetSessions::VALUE_LEN]; - value[0..32].copy_from_slice(vset_hash.as_slice()); - value[32..36].copy_from_slice(&start_seqno.to_le_bytes()); - value +fn vset_session_key( + vset_hash: &HashBytes, + session_id: ValidationSessionId, +) -> [u8; tables::VsetSessions::KEY_LEN] { + let mut key = [0u8; tables::VsetSessions::KEY_LEN]; + key[0..32].copy_from_slice(vset_hash.as_slice()); + key[32..36].copy_from_slice(&session_id.catchain_seqno.to_be_bytes()); + key[36..40].copy_from_slice(&session_id.vset_switch_round.to_be_bytes()); + key } fn block_batches_key( @@ -208,3 +285,12 @@ fn block_batches_key( key[34..38].copy_from_slice(&start_seqno.to_be_bytes()); key } + +const ZERO_SESSION: ValidationSessionId = ValidationSessionId { + catchain_seqno: 0, + vset_switch_round: 0, +}; +const MAX_SESSION: ValidationSessionId = ValidationSessionId { + catchain_seqno: u32::MAX, + vset_switch_round: u32::MAX, +}; diff --git a/slasher/src/storage/models.rs b/slasher/src/storage/models.rs index 9b0a0985bf..191a94b874 100644 --- a/slasher/src/storage/models.rs +++ b/slasher/src/storage/models.rs @@ -1,9 +1,29 @@ 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}; +// === Vset State Stuff === + +#[derive(Debug, Clone, Copy, TlRead, TlWrite)] +#[tl(boxed, id = "slasher.storedVsetInfo", scheme = "proto.tl")] +pub struct StoredVsetInfo { + pub prev_vset_hash: [u8; 32], + #[tl(with = "tl_session_id")] + pub first_session_id: ValidationSessionId, + pub start_seqno: u32, +} + +#[derive(Debug, Clone, TlRead, TlWrite)] +#[tl(boxed, id = "slasher.storedVsetReport", scheme = "proto.tl")] +pub struct StoredVsetReport { + pub public_key: [u8; 32], + #[tl(with = "tl_accusations")] + pub accusations: Vec, +} + // === StoredBlocksBatch === #[repr(transparent)] @@ -98,3 +118,53 @@ impl<'tl> TlRead<'tl> for StoredBlocksBatch { })) } } + +// === Stuff === + +mod tl_session_id { + use super::*; + + pub fn size_hint(_: &ValidationSessionId) -> usize { + 4 + 4 + } + + pub fn write(value: &ValidationSessionId, packet: &mut T) { + packet.write_u32(value.catchain_seqno); + packet.write_u32(value.vset_switch_round); + } + + pub fn read(packet: &mut &[u8]) -> TlResult { + Ok(ValidationSessionId { + catchain_seqno: u32::read_from(packet)?, + vset_switch_round: u32::read_from(packet)?, + }) + } +} + +mod tl_accusations { + use super::*; + + pub fn size_hint(value: &[u16]) -> usize { + 4 + 4 * value.len() + } + + pub fn write(value: &[u16], packet: &mut T) { + packet.write_u32(value.len() as u32); + for item in value { + packet.write_u32(*item as u32); + } + } + + pub fn read(packet: &mut &[u8]) -> TlResult> { + let len = u32::read_from(packet)? as usize; + let mut result = Vec::with_capacity(len); + for _ in 0..len { + let idx = u32::read_from(packet)?; + if idx > u16::MAX as u32 { + return Err(TlError::InvalidData); + } + result.push(idx as u16); + } + Ok(result) + } +} From b8353a6f9f3c5c933fcfd976cc61ce422245800c Mon Sep 17 00:00:00 2001 From: Ivan Kalinin Date: Mon, 1 Jun 2026 23:07:22 +0200 Subject: [PATCH 22/31] fix(slasher): handle strange analyzer cases --- collator/src/collator/do_collate/finalize.rs | 8 ++++---- scripts/gen-network.sh | 21 ++++++++++++++++---- scripts/requirements.txt | 2 +- slasher/src/analyzer.rs | 12 +++++++++-- slasher/src/bc/contract.rs | 3 ++- slasher/src/bc/mod.rs | 1 + slasher/src/lib.rs | 10 +++------- slasher/src/storage/mod.rs | 6 ++---- util/src/cli/logger.rs | 6 +----- 9 files changed, 41 insertions(+), 28 deletions(-) diff --git a/collator/src/collator/do_collate/finalize.rs b/collator/src/collator/do_collate/finalize.rs index efc1ba585c..3c5bbe6041 100644 --- a/collator/src/collator/do_collate/finalize.rs +++ b/collator/src/collator/do_collate/finalize.rs @@ -770,10 +770,10 @@ impl Phase { value_flow, created_by: self.state.collation_data.created_by, queue_diff_aug: queue_diff.build(&new_block_id), - consensus_info: new_mc_data.as_ref().map_or_else( - || self.state.mc_data.consensus_info, - |mcd| mcd.consensus_info, - ), + // Proof signatures describe the session that signed this block. + // A key block may update the next session into its resulting state, + // so use pre-block consensus info here. + consensus_info: self.state.mc_data.consensus_info, processed_upto, }); diff --git a/scripts/gen-network.sh b/scripts/gen-network.sh index c944e10cb7..79b6c5273a 100755 --- a/scripts/gen-network.sh +++ b/scripts/gen-network.sh @@ -106,10 +106,23 @@ do global_config=$(echo "${global_config}" | jq ".bootstrap_peers += [${dht_entry}]") - node_config=$(echo "${node_config}" | jq ".port = ${node_port} | .storage.root_dir = \"${storage_root_dir}\"") - node_config=$(echo "${node_config}" | jq "if .rpc.listen_addr? then .rpc.listen_addr = \"${rpc_listen_addr}\" else . end") - node_config=$(echo "${node_config}" | jq "if .metrics.listen_addr? then .metrics.listen_addr = \"${metrics_listen_addr}\" else . end") - node_config=$(echo "${node_config}" | jq "if .control.socket_path? then .control.socket_path = \"${control_socket_path}\" else . end") + node_config=$(echo "${node_config}" | jq " + .port = ${node_port} + | .storage.root_dir = \"${storage_root_dir}\" + | if .rpc.listen_addr? then .rpc.listen_addr = \"${rpc_listen_addr}\" else . end + | if .metrics.listen_addr? then .metrics.listen_addr = \"${metrics_listen_addr}\" else . end + | if .control.socket_path? then .control.socket_path = \"${control_socket_path}\" else . end + | if all(.logger.outputs[]; .type != \"File\") then .logger.outputs += [{ + "type": \"File\", + "human_readable": true, + }] else . end + | .logger.outputs |= map( + if .type == \"File\" then + .dir=\"$base_dir\" + | .file_prefix=\"node${i}.log\" + else . + end) + ") echo "${node_config}" > "${base_dir}/config${i}.json" elections_config=$( diff --git a/scripts/requirements.txt b/scripts/requirements.txt index cf0bcfe186..ae3a3220d0 100644 --- a/scripts/requirements.txt +++ b/scripts/requirements.txt @@ -1,4 +1,4 @@ git+https://github.com/broxus/grafana-builder.git@376e405ac6620a76598dcdf81e7b6ff10aa702eb#egg=dashboard-builder grafanalib==0.7.1 -nekoton==0.1.23 +nekoton==0.1.24 ruff==0.7.3 diff --git a/slasher/src/analyzer.rs b/slasher/src/analyzer.rs index af6a4e31ee..3f28b5b1bf 100644 --- a/slasher/src/analyzer.rs +++ b/slasher/src/analyzer.rs @@ -60,7 +60,7 @@ pub fn analyze_vset( // Count weight of valid signatures per committed column (block). for history in &batch.signatures_history { let other = history.validator_idx as usize; - if other >= n || other == observer { + if other >= n { malformed = true; tracing::warn!( observer, @@ -70,6 +70,9 @@ pub fn analyze_vset( "malformed batch", ); continue; + } else if other == observer { + // Self signatures are not used. + continue; } let weight = vset.list[other].weight; @@ -147,7 +150,12 @@ pub fn analyze_vset( let baseline = rates[rates.len() / 2]; let slow_threshold = baseline * config.slow_node_factor; - tracing::debug!(baseline, slow_threshold, "computed valid signature rates"); + tracing::debug!( + observer, + baseline, + slow_threshold, + "computed valid signature rates" + ); for (other, score) in scores.iter().enumerate() { if other == observer { diff --git a/slasher/src/bc/contract.rs b/slasher/src/bc/contract.rs index 7db733e8f5..2c9dba6f78 100644 --- a/slasher/src/bc/contract.rs +++ b/slasher/src/bc/contract.rs @@ -129,7 +129,7 @@ impl SlasherContract for StdSlasherContract { return Ok(None); } - body.skip_first(256, 0)?; + let vset_hash = body.load_u256()?; let validator_idx = body.load_u16()?; let mut batch_cs = body.load_reference_as_slice()?; let BlocksBatchBc(blocks_batch) = <_>::load_from(&mut batch_cs)?; @@ -139,6 +139,7 @@ impl SlasherContract for StdSlasherContract { Ok(Some(SlasherContractEvent::SubmitBlocksBatch( SubmitBlocksBatch { + vset_hash, validator_idx, blocks_batch, }, diff --git a/slasher/src/bc/mod.rs b/slasher/src/bc/mod.rs index 5d1c9203d1..6eb251146b 100644 --- a/slasher/src/bc/mod.rs +++ b/slasher/src/bc/mod.rs @@ -136,6 +136,7 @@ pub enum SlasherContractEvent { #[derive(Debug, PartialEq, Eq)] pub struct SubmitBlocksBatch { + pub vset_hash: HashBytes, pub validator_idx: u16, pub blocks_batch: BlocksBatch, } diff --git a/slasher/src/lib.rs b/slasher/src/lib.rs index 8aa87441eb..b4726f0312 100644 --- a/slasher/src/lib.rs +++ b/slasher/src/lib.rs @@ -296,11 +296,7 @@ impl Slasher { return Ok(()); }; - tracing::trace!( - ?slasher_params, - ?current_session_id, - %current_vset_hash, - ); + tracing::trace!(?slasher_params, ?current_session_id); // TODO: Move into blocking. let extra = cx.block.load_extra()?.account_blocks.load()?; @@ -324,7 +320,7 @@ impl Slasher { let batch = &submitted.blocks_batch; tracing::info!( %tx_hash, - %current_vset_hash, + vset_hash = %submitted.vset_hash, validator_idx = submitted.validator_idx, batch_start_seqno = batch.start_seqno(), batch_seqno_after = batch.seqno_after(), @@ -336,7 +332,7 @@ impl Slasher { ); this.storage.store_blocks_batch( - ¤t_vset_hash, + &submitted.vset_hash, submitted.validator_idx, &submitted.blocks_batch, )?; diff --git a/slasher/src/storage/mod.rs b/slasher/src/storage/mod.rs index 0413cbb98c..08e01b8dcc 100644 --- a/slasher/src/storage/mod.rs +++ b/slasher/src/storage/mod.rs @@ -156,10 +156,8 @@ impl SlasherStorageSnapshot { pub fn iter_block_batches(&self, vset_hash: &HashBytes) -> BlockBatchesIter<'_> { let mut readopts = self.db.block_batches.new_read_config(); readopts.set_snapshot(&self.snapshot); - let mut key = block_batches_key(vset_hash, 0, 0); - readopts.set_iterate_lower_bound(key); - key[32..].fill(0xff); - readopts.set_iterate_upper_bound(key); + readopts.set_iterate_lower_bound(block_batches_key(vset_hash, 0, 0)); + readopts.set_iterate_upper_bound(block_batches_key(vset_hash, u16::MAX, u32::MAX)); let mut raw = self .db diff --git a/util/src/cli/logger.rs b/util/src/cli/logger.rs index 8ec3d54bf9..e9556f8acc 100644 --- a/util/src/cli/logger.rs +++ b/util/src/cli/logger.rs @@ -182,11 +182,7 @@ impl LoggerFileOutput { .build(&self.dir)?; Ok(match self.resolved_format() { - LogFormat::Human => fmt::layer() - .without_time() - .with_ansi(false) - .with_writer(writer) - .boxed(), + LogFormat::Human => fmt::layer().with_ansi(false).with_writer(writer).boxed(), LogFormat::Json | LogFormat::Auto => { tracing_stackdriver::layer().with_writer(writer).boxed() } From 7257b50fc9fc94d2a1e2afe27718e13433916c3e Mon Sep 17 00:00:00 2001 From: Ivan Kalinin Date: Wed, 3 Jun 2026 18:34:35 +0200 Subject: [PATCH 23/31] feat(slasher): add slasher metrics --- scripts/gen-dashboard.py | 70 +++++++++++++++++++ slasher/src/bc/mod.rs | 13 ++++ slasher/src/collector/validator_events.rs | 17 ++++- slasher/src/lib.rs | 82 ++++++++++++++++++++--- 4 files changed, 172 insertions(+), 10 deletions(-) diff --git a/scripts/gen-dashboard.py b/scripts/gen-dashboard.py index 501253cbf1..3dba4d7d6f 100755 --- a/scripts/gen-dashboard.py +++ b/scripts/gen-dashboard.py @@ -3095,6 +3095,65 @@ def validator() -> RowPanel: return create_row("Validator", metrics) +def slasher() -> RowPanel: + metrics = [ + timeseries_panel( + targets=[ + target( + 'tycho_slasher_blocks_batch_size{instance=~"$instance"} and \ + on(instance, job) tycho_slasher_enabled{instance=~"$instance"} == 1', + legend_format="{{instance}}", + ) + ], + title="Slasher blocks batch size", + unit=UNITS.NUMBER_FORMAT, + ), + create_gauge_panel("tycho_slasher_active_sessions", "Slasher active sessions"), + create_gauge_panel( + "tycho_slasher_session_init_queue_len", "Slasher session init queue" + ), + create_gauge_panel( + "tycho_slasher_batch_delivery_tasks", "Slasher batch delivery tasks" + ), + create_gauge_panel( + "tycho_slasher_pending_messages", "Slasher pending messages" + ), + create_counter_panel( + "tycho_slasher_blocks_batch_send_attempts_total", + "Blocks batch send attempts", + ), + create_counter_panel( + "tycho_slasher_blocks_batch_delivery_time_count", + "Delivered block batches", + ), + create_counter_panel( + "tycho_slasher_blocks_batch_errors_total", "Expired block batches" + ), + create_counter_panel( + "tycho_slasher_blocks_batches_submitted_total", + "Slasher submitted batches", + by_labels=["instance", "origin"], + legend_format="{{instance}} {{origin}}", + ), + create_gauge_panel("tycho_slasher_vset_reports_total", "Slasher vset reports"), + create_gauge_panel( + "tycho_slasher_accusations_total", + "Slasher accusations", + labels=['instance=~"$instance"', 'pubkey=~"$pubkey"'], + legend_format="{{instance}} {{pubkey}}", + legend_placement="bottom", + ), + create_heatmap_panel( + "tycho_slasher_handle_state_time", "Slasher handle state time" + ), + create_heatmap_panel( + "tycho_slasher_blocks_batch_delivery_time", + "Slasher blocks batch delivery time", + ), + ] + return create_row("Slasher", metrics) + + def mempool_rounds() -> RowPanel: metrics = [ create_gauge_panel( @@ -3972,6 +4031,16 @@ def templates() -> Templating: include_all=True, all_value=".*", ), + template( + name="pubkey", + query="label_values(tycho_slasher_accusations_total, pubkey)", + data_source="${source}", + hide=0, + regex=None, + multi=True, + include_all=True, + all_value=".*", + ), template( name="remote_addr", query="label_values(tycho_network_connection_rtt_ms, addr)", @@ -4016,6 +4085,7 @@ def templates() -> Templating: collator_misc_operations_metrics(), collator_commit_block_metrics(), validator(), + slasher(), mempool_rounds(), mempool_payload_rates(), mempool_engine_rates(), diff --git a/slasher/src/bc/mod.rs b/slasher/src/bc/mod.rs index 6eb251146b..4ed0ff0c50 100644 --- a/slasher/src/bc/mod.rs +++ b/slasher/src/bc/mod.rs @@ -16,6 +16,8 @@ use crate::util::BitSet; mod contract; +const METRIC_PENDING_MESSAGES: &str = "tycho_slasher_pending_messages"; + #[derive(Clone, Copy)] pub struct EncodeBlocksBatchMessage<'a> { pub address: &'a StdAddr, @@ -82,6 +84,7 @@ impl ContractSubscription { match self.pending_messages.entry(*msg_hash) { Entry::Vacant(entry) => { entry.insert(PendingMessage { expire_at, tx }); + self.report_pending_messages(); Ok(rx) } Entry::Occupied(_) => anyhow::bail!("duplicate external message: {msg_hash}"), @@ -100,6 +103,7 @@ impl ContractSubscription { if let Some((_, pending)) = self.pending_messages.remove(msg_hash) { pending.tx.send(MessageDelivered { tx_hash: *tx_hash }).ok(); + self.report_pending_messages(); return Ok(true); } Ok(false) @@ -115,6 +119,15 @@ impl ContractSubscription { if dropped > 0 { tracing::warn!(dropped, "dropped pending messages"); } + self.report_pending_messages(); + } + + pub fn reset_pending_messages_metrics() { + metrics::gauge!(METRIC_PENDING_MESSAGES).set(0); + } + + pub fn report_pending_messages(&self) { + metrics::gauge!(METRIC_PENDING_MESSAGES).set(self.pending_messages.len() as f64); } } diff --git a/slasher/src/collector/validator_events.rs b/slasher/src/collector/validator_events.rs index 9f0e4767f5..3cbbe38c59 100644 --- a/slasher/src/collector/validator_events.rs +++ b/slasher/src/collector/validator_events.rs @@ -15,6 +15,8 @@ use tycho_util::{DashMapEntry, FastDashMap}; use crate::bc::BlocksBatch; const INIT_QUEUE_CAPACITY: usize = 3; +const METRIC_ACTIVE_SESSIONS: &str = "tycho_slasher_active_sessions"; +const METRIC_SESSION_INIT_QUEUE_LEN: &str = "tycho_slasher_session_init_queue_len"; pub trait BlockBatchesStore { fn known_batch_size(&self) -> AtomicU32; @@ -55,6 +57,8 @@ 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)); + metrics::gauge!(METRIC_ACTIVE_SESSIONS).set(0); + metrics::gauge!(METRIC_SESSION_INIT_QUEUE_LEN).set(0); Self { default_batch_size: AtomicU32::new(default_batch_size.get()), @@ -71,7 +75,9 @@ impl ValidatorEventsCollector { { return None; } - queue.pop_front() + let result = queue.pop_front(); + metrics::gauge!(METRIC_SESSION_INIT_QUEUE_LEN).set(queue.len() as f64); + result } fn push_session_to_init(&self, info: ValidatorSessionInfo) { @@ -85,6 +91,7 @@ impl ValidatorEventsCollector { ); } items.push_back(info); + metrics::gauge!(METRIC_SESSION_INIT_QUEUE_LEN).set(items.len() as f64); } pub fn set_default_batch_size(&self, batch_size: NonZeroU32) { @@ -119,7 +126,11 @@ impl ValidatorEventsCollector { } pub fn skip_session(&self, session_id: ValidationSessionId) -> bool { - self.sessions.remove(&session_id).is_some() + let removed = self.sessions.remove(&session_id).is_some(); + if removed { + metrics::gauge!(METRIC_ACTIVE_SESSIONS).set(self.sessions.len() as f64); + } + removed } } @@ -163,6 +174,7 @@ impl ValidatorEventsListener for ValidatorEventsCollector { own_validator_idx, validators, }); + metrics::gauge!(METRIC_ACTIVE_SESSIONS).set(self.sessions.len() as f64); } else { tracing::warn!("duplicate session"); } @@ -176,6 +188,7 @@ impl ValidatorEventsListener for ValidatorEventsCollector { { tracing::warn!("failed to commit blocks batch on finish: {e:?}"); } + metrics::gauge!(METRIC_ACTIVE_SESSIONS).set(self.sessions.len() as f64); } #[instrument(skip_all, fields(session_id = ?session_id))] diff --git a/slasher/src/lib.rs b/slasher/src/lib.rs index b4726f0312..59bd706786 100644 --- a/slasher/src/lib.rs +++ b/slasher/src/lib.rs @@ -1,5 +1,5 @@ use std::sync::Arc; -use std::time::Duration; +use std::time::{Duration, Instant}; use anyhow::{Context, Result}; use arc_swap::{ArcSwap, ArcSwapOption}; @@ -19,6 +19,7 @@ use tycho_types::models::{SignatureContext, StdAddr, ValidatorSet}; use tycho_types::prelude::*; use tycho_util::config::PartialConfig; use tycho_util::futures::JoinTask; +use tycho_util::metrics::{GaugeGuard, HistogramGuard}; use tycho_util::serde_helpers; use self::bc::SlasherParams; @@ -43,6 +44,18 @@ mod bc; mod storage; mod util; +const METRIC_ENABLED: &str = "tycho_slasher_enabled"; +const METRIC_BLOCKS_BATCH_SIZE: &str = "tycho_slasher_blocks_batch_size"; +const METRIC_HANDLE_STATE_TIME: &str = "tycho_slasher_handle_state_time"; +const METRIC_BATCH_DELIVERY_TASKS: &str = "tycho_slasher_batch_delivery_tasks"; +const METRIC_BLOCKS_BATCH_SEND_ATTEMPTS_TOTAL: &str = + "tycho_slasher_blocks_batch_send_attempts_total"; +const METRIC_BLOCKS_BATCH_ERRORS_TOTAL: &str = "tycho_slasher_blocks_batch_errors_total"; +const METRIC_BLOCKS_BATCH_DELIVERY_TIME: &str = "tycho_slasher_blocks_batch_delivery_time"; +const METRIC_BLOCKS_BATCHES_SUBMITTED_TOTAL: &str = "tycho_slasher_blocks_batches_submitted_total"; +const METRIC_VSET_REPORTS_TOTAL: &str = "tycho_slasher_vset_reports_total"; +const METRIC_ACCUSATIONS_TOTAL: &str = "tycho_slasher_accusations_total"; + #[derive(Debug, Clone, Serialize, Deserialize, PartialConfig)] #[serde(default)] pub struct SlasherConfig { @@ -135,6 +148,8 @@ impl Slasher { let slasher_params = contract .find_params(blockchain_config) .context("failed to find slasher params")?; + report_config_metrics(slasher_params.as_ref()); + metrics::gauge!(METRIC_BATCH_DELIVERY_TASKS).set(0); let subscription = match &slasher_params { Some(slasher_params) => { @@ -145,6 +160,11 @@ impl Slasher { None => None, }; + match &subscription { + Some(subscription) => subscription.report_pending_messages(), + None => ContractSubscription::reset_pending_messages_metrics(), + } + let collector = Arc::new(ValidatorEventsCollector::new( slasher_params .as_ref() @@ -195,6 +215,7 @@ impl Slasher { if !cx.block.id().is_masterchain() { return Ok(()); } + let _state_timer = HistogramGuard::begin(METRIC_HANDLE_STATE_TIME); let mc_seqno = cx.block.id().seqno; let this = self.shared.as_ref(); @@ -219,6 +240,7 @@ impl Slasher { .contract .find_params(&state_extra.config) .context("failed to find slasher params")?; + report_config_metrics(slasher_params.as_ref()); if let Some(slasher_params) = &slasher_params { self.validator_events_collector @@ -245,14 +267,16 @@ impl Slasher { (_subscription, None) => { // TODO: Notify subscription that it is no longer needed. this.subscription.store(None); + ContractSubscription::reset_pending_messages_metrics(); } // Slasher address unchanged. (Some(s), Some(slasher_address)) if s.address() == slasher_address => {} // Slasher address has changed. (_, Some(slasher_address)) => { tracing::info!(%slasher_address, "slasher address changed"); - this.subscription - .store(Some(Arc::new(ContractSubscription::new(slasher_address)))); + let subscription = Arc::new(ContractSubscription::new(slasher_address)); + this.subscription.store(Some(subscription.clone())); + subscription.report_pending_messages(); } } } @@ -318,6 +342,7 @@ impl Slasher { Ok(Some(event)) => match event { bc::SlasherContractEvent::SubmitBlocksBatch(submitted) => { let batch = &submitted.blocks_batch; + tracing::info!( %tx_hash, vset_hash = %submitted.vset_hash, @@ -331,6 +356,15 @@ impl Slasher { "blocks batch submitted", ); + // NOTE: Might increment batches twice on restart, + // but it's better to keep this simple. + let origin = if own_message { "own" } else { "external" }; + metrics::counter!( + METRIC_BLOCKS_BATCHES_SUBMITTED_TOTAL, + "origin" => origin, + ) + .increment(1); + this.storage.store_blocks_batch( &submitted.vset_hash, submitted.validator_idx, @@ -340,10 +374,12 @@ impl Slasher { } }, Ok(None) => {} - Err(e) => tracing::warn!( - %tx_hash, - "failed to parse slasher event: {e:?}" - ), + Err(e) => { + tracing::warn!( + %tx_hash, + "failed to parse slasher event: {e:?}" + ); + } } } } @@ -443,6 +479,17 @@ impl SlasherSharedState { )?; tracing::warn!("slasher accusations: {accusations:?}"); + metrics::counter!(METRIC_VSET_REPORTS_TOTAL).increment(1); + for &validator_idx in &accusations { + if let Some(desc) = vset.vset.list.get(validator_idx as usize) { + metrics::counter!( + METRIC_ACCUSATIONS_TOTAL, + "pubkey" => desc.public_key.to_string(), + ) + .increment(1); + } + } + self.storage .store_vset_report(&vset.hash, &StoredVsetReport { public_key: *self.node_keys.public_key.as_bytes(), @@ -492,8 +539,13 @@ impl SlasherSharedState { validator_idx: u16, batch: BlocksBatch, ) { + let _delivery_task = GaugeGuard::increment(METRIC_BATCH_DELIVERY_TASKS, 1.0); + loop { + metrics::counter!(METRIC_BLOCKS_BATCH_SEND_ATTEMPTS_TOTAL).increment(1); + let Some(subscription) = self.subscription.load_full() else { + metrics::counter!(METRIC_BLOCKS_BATCH_ERRORS_TOTAL).increment(1); tracing::warn!("no slasher contract subscription"); break; }; @@ -513,6 +565,7 @@ impl SlasherSharedState { let signed = match self.contract.encode_blocks_batch_message(¶ms) { Ok(signed) => signed, Err(e) => { + metrics::counter!(METRIC_BLOCKS_BATCH_ERRORS_TOTAL).increment(1); tracing::error!("failed to encode batch message: {e:?}"); return; } @@ -522,6 +575,7 @@ impl SlasherSharedState { match subscription.track_message(&msg_hash, signed.expire_at) { Ok(res) => { + let delivery_started_at = Instant::now(); tracing::info!( %msg_hash, address = %params.address, @@ -541,6 +595,8 @@ impl SlasherSharedState { match res.await { Ok(MessageDelivered { tx_hash }) => { + metrics::histogram!(METRIC_BLOCKS_BATCH_DELIVERY_TIME) + .record(delivery_started_at.elapsed()); tracing::info!( %tx_hash, session_id = ?params.session_id, @@ -555,12 +611,16 @@ impl SlasherSharedState { return; } Err(_) => { + metrics::counter!(METRIC_BLOCKS_BATCH_ERRORS_TOTAL).increment(1); // TODO: Execute transaction locally to guess the reason. tracing::warn!("batch message expired"); } } } - Err(e) => tracing::warn!("failed to track message: {e:?}"), + Err(e) => { + metrics::counter!(METRIC_BLOCKS_BATCH_ERRORS_TOTAL).increment(1); + tracing::warn!("failed to track message: {e:?}"); + } } tokio::time::sleep(self.config.message_retry_interval).await; @@ -588,3 +648,9 @@ impl ParsedVset { }) } } + +fn report_config_metrics(slasher_params: Option<&SlasherParams>) { + metrics::gauge!(METRIC_ENABLED).set(slasher_params.is_some() as u8); + metrics::gauge!(METRIC_BLOCKS_BATCH_SIZE) + .set(slasher_params.map_or(0, |params| params.blocks_batch_size.get())); +} From 4337f8ab5ec0bd4d7dff9a29d30dbc78cac129a2 Mon Sep 17 00:00:00 2001 From: Ivan Kalinin Date: Thu, 4 Jun 2026 13:26:41 +0200 Subject: [PATCH 24/31] fix(slasher): use proper cf for slasher state --- slasher/src/storage/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/slasher/src/storage/mod.rs b/slasher/src/storage/mod.rs index 08e01b8dcc..f2fb092ff2 100644 --- a/slasher/src/storage/mod.rs +++ b/slasher/src/storage/mod.rs @@ -88,7 +88,7 @@ impl SlasherStorage { let vset_to_remove = HashBytes::wrap(&prev_vset.prev_vset_hash); batch.delete_range_cf( - &db.state.cf(), + &db.vset_state.cf(), vset_state_key(vset_to_remove, 0), vset_state_key(vset_to_remove, u8::MAX), ); @@ -105,7 +105,7 @@ impl SlasherStorage { } batch.put_cf( - &db.state.cf(), + &db.vset_state.cf(), vset_state_key(vset_hash, tables::VsetState::KEY_TYPE_INFO), tl_proto::serialize(info), ); From 971cb79c46f747fa4986f017aac0130c967c09dd Mon Sep 17 00:00:00 2001 From: Ivan Kalinin Date: Fri, 5 Jun 2026 17:22:59 +0200 Subject: [PATCH 25/31] feat(contracts): rework slasher replay protection --- contracts/scripts/genSlasherAccount.ts | 16 +++- contracts/src/slasher.tolk | 118 +++++++++++++++++-------- contracts/tests/Slasher.spec.ts | 109 ++++++++++++++++++----- contracts/tsconfig.json | 3 +- contracts/wrappers/Slasher.ts | 48 +++++++++- contracts/yarn.lock | 6 +- slasher/src/bc/contract.rs | 4 +- 7 files changed, 235 insertions(+), 69 deletions(-) diff --git a/contracts/scripts/genSlasherAccount.ts b/contracts/scripts/genSlasherAccount.ts index 0f4be81cd9..0c9fc7f79d 100644 --- a/contracts/scripts/genSlasherAccount.ts +++ b/contracts/scripts/genSlasherAccount.ts @@ -1,5 +1,11 @@ import arg from "arg"; -import { address, beginCell, storeAccount, toNano } from "@ton/core"; +import { + address, + beginCell, + Dictionary, + storeAccount, + toNano, +} from "@ton/core"; import { storeSlasherData } from "../wrappers/Slasher"; import { compile } from "@ton/blueprint"; @@ -30,10 +36,16 @@ async function main() { type: "active", state: { code, + special: { + tick: true, + tock: false, + }, data: beginCell() .store( storeSlasherData({ - updatedAtMs: 0n, + currentVsetHash: Buffer.alloc(32), + validatorCount: 0, + sentBatches: Dictionary.empty(), }), ) .endCell(), diff --git a/contracts/src/slasher.tolk b/contracts/src/slasher.tolk index fa152387e0..6f5f29672e 100644 --- a/contracts/src/slasher.tolk +++ b/contracts/src/slasher.tolk @@ -7,18 +7,19 @@ const SLASHER_OP_VOTE = stringCrc32("op::slasher_vote") const ERROR_INVALID_SIGNATURE = 40 const ERROR_VALIDATOR_NOT_FOUND = 50 -const ERROR_REPLAY_PROTECTION = 52 const ERROR_MESSAGE_EXPIRED = 57 const ERROR_UNKNOWN_OP = 60 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 - struct Storage { - updatedAtMs: uint64 + currentVsetHash: uint256 + validatorCount: uint16 + /// Per-participant replay protection. + /// + /// uint16 (validatorIdx) => `ValidatorState` + sentBatches: dict, } fun Storage.load(): Storage { @@ -29,6 +30,16 @@ fun Storage.save(self) { contract.setData(self.toCell()); } +struct ValidatorState { + pubkey: uint256 + minSeqno: uint32 +} + +/// Create a new builder with this value. +fun ValidatorState.toBuilder(self): builder { + return beginCell().storeAny(self); +} + // // === Slasher Config param === // @@ -46,26 +57,61 @@ get fun is_blocks_batch_valid(batch: cell, mcSeqno: int): bool { val params = loadSlasherParams(); val vset = lazy ValidatorSet.fromCell(blockchain.configParam(PARAM_IDX_CURRENT_VSET)!); val validatorCount = min(vset.total, vset.main); - return validateBlocksBatch( + val nextSeqno = validateBlocksBatch( batch.beginParse(), { + minSeqno: 0, batchSize: params.blocksBatchSize, mcSeqno, validatorCount, } ); + return nextSeqno != null; } // // === Logic === // +fun onRunTickTock(_isTock: bool) { + val vsetRaw = blockchain.configParam(PARAM_IDX_CURRENT_VSET)!; + val data = lazy Storage.load(); + val currentVsetHash = vsetRaw.hash(); + if (data.currentVsetHash == currentVsetHash) { + return; + } + + val vset = lazy ValidatorSet.fromCell(vsetRaw); + val validatorCount = min(vset.total, vset.main); + val lastValidatorIdx = validatorCount - 1; + + var sentBatches = createEmptyDict(); + + var iterNext = -1; + do { + val (validatorIdx, cs, found) = vset.list.uDictGetNext(16, iterNext); + if (found) { + iterNext = validatorIdx!; + val state = ValidatorState { + pubkey: ValidatorDescr.readPubkeyOnly(cs!), + minSeqno: 0, + }; + sentBatches.uDictSetBuilder(16, validatorIdx!, state.toBuilder()); + } + } while (found && validatorIdx! < lastValidatorIdx); + + Storage { + currentVsetHash, + validatorCount, + sentBatches, + }.save(); +} + 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 op = inMsg.loadUint(32); @@ -74,7 +120,6 @@ fun onExternalMessage(inMsg: slice) { handleBlocksBatchMessage( signature, signedBody, - createdAtMs, expireAtSec, inMsg, ) @@ -89,7 +134,6 @@ fun onExternalMessage(inMsg: slice) { fun handleBlocksBatchMessage( signature: slice, signedBody: slice, - createdAtMs: int, expireAtSec: int, rest: slice ) { @@ -100,61 +144,61 @@ fun handleBlocksBatchMessage( assert(blockchain.now() <= expireAtSec, ERROR_MESSAGE_EXPIRED); val toSign = beginCell().storeSlice(signedBody).endCell(); - val vsetRaw = blockchain.configParam(PARAM_IDX_CURRENT_VSET)!; - assert(vsetRaw.hash() == vsetHash, ERROR_VALIDATOR_NOT_FOUND); - val vset = lazy ValidatorSet.fromCell(vsetRaw); - 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); + var data = Storage.load(); + assert(vsetHash == data.currentVsetHash, ERROR_VALIDATOR_NOT_FOUND); - val validatorCount = min(vset.total, vset.main); + val (validatorCs, validatorFound) = data.sentBatches.uDictGet(16, validatorIdx); + assert(validatorFound, ERROR_VALIDATOR_NOT_FOUND); + var validatorState = ValidatorState.fromSlice(validatorCs!); + assert(isSignatureValid(toSign.hash(), signature, validatorState.pubkey), ERROR_INVALID_SIGNATURE); val batchSize = loadSlasherParams().blocksBatchSize; val mcSeqno = blockchain.prevMcSeqno() + 1; - assert(validateBlocksBatch( + val nextSeqno = validateBlocksBatch( batch.beginParse(), { + minSeqno: validatorState.minSeqno, batchSize, - validatorCount, + validatorCount: data.validatorCount, mcSeqno, } - ), ERROR_INVALID_BLOCKS_BATCH); - - var data = Storage.load(); - - // TODO: `updatedAtMs` must be separate for all validators. - assert(createdAtMs > (data.updatedAtMs - REPLAY_OFFSET_MS) && - createdAtMs <= (blockchain.now() + FUTURE_OFFSET_SEC) * 1000, ERROR_REPLAY_PROTECTION); + ); + assert(nextSeqno != null, ERROR_INVALID_BLOCKS_BATCH); - data.updatedAtMs = max(createdAtMs, data.updatedAtMs); + validatorState.minSeqno = nextSeqno; + data.sentBatches.uDictSetBuilder(16, validatorIdx, validatorState.toBuilder()); data.save(); acceptExternalMessage(); } struct ValidateBlocksBatchParams { + minSeqno: int batchSize: int mcSeqno: int validatorCount: int } -fun validateBlocksBatch(batch: slice, params: ValidateBlocksBatchParams): bool { +fun validateBlocksBatch(batch: slice, params: ValidateBlocksBatchParams): int? { val startSeqno = batch.loadUint(32); - if (startSeqno + params.batchSize > params.mcSeqno) { + if ( + // Repeated batch. + startSeqno < params.minSeqno // Batch contains blocks that were not produced yet. - return false; - } - if (startSeqno + params.batchSize * 2 < params.mcSeqno) { + || startSeqno + params.batchSize > params.mcSeqno // Batch contains too old blocks. - return false; + || startSeqno + params.batchSize * 2 < params.mcSeqno + ) { + return null; } + val nextSeqno = startSeqno + params.batchSize; + batch.skipBits(params.batchSize); val history = batch.loadRef() as dict; if (!batch.isEmpty()) { - return false; + return null; } var iterNext = -1; @@ -164,17 +208,17 @@ fun validateBlocksBatch(batch: slice, params: ValidateBlocksBatchParams): bool { iterNext = validatorIdx!; if (validatorIdx! >= params.validatorCount) { - return false; + return null; } val (csBits, csRefs) = cs!.remainingBitsAndRefsCount(); if (csBits != params.batchSize * 2 || csRefs != 0) { - return false; + return null; } } } while (found); - return true; + return nextSeqno; } fun loadSlasherParams(): SlasherParams { diff --git a/contracts/tests/Slasher.spec.ts b/contracts/tests/Slasher.spec.ts index fa217eeaf5..bbb7008857 100644 --- a/contracts/tests/Slasher.spec.ts +++ b/contracts/tests/Slasher.spec.ts @@ -8,7 +8,7 @@ import { Dictionary, toNano, } from "@ton/core"; -import { Blockchain, createShardAccount, SmartContract } from "@ton/sandbox"; +import { Blockchain, EmulationError, SmartContract } from "@ton/sandbox"; import { getSecureRandomBytes, KeyPair, @@ -22,12 +22,12 @@ import { storeSlasherParams, storeSlasherData, SLASHER_OP_SEND_BLOCKS_BATCH, + loadSlasherData, } from "../wrappers/Slasher"; import { bufferToBigInt, ConfigParams, makeStubValidatorSet, - ValidatorDescrValue, } from "../wrappers/util"; const SLASHER_ADDR = address( @@ -39,6 +39,8 @@ const SAMPLE_BLOCKS_BATCH = Cell.fromBase64( "te6ccgEBCAEAMAABCwAAAObYYAECAswFAgIBIAQDAAfRCgDAAAdpRQBgAgEgBwYAB2UFAGAAB/SKAMA=", ); +const FIRST_MC_SEQNO = 241; + describe("Slasher", () => { let config: Cell; let code: Cell; @@ -101,22 +103,45 @@ describe("Slasher", () => { executor, }); - await blockchain.setShardAccount( - SLASHER_ADDR, - createShardAccount({ - address: SLASHER_ADDR, - balance: toNano(500), - code, - data: beginCell() - .store( - storeSlasherData({ - updatedAtMs: 0n, - }), - ) - .endCell(), - workchain: -1, - }), - ); + await blockchain.setShardAccount(SLASHER_ADDR, { + account: { + addr: SLASHER_ADDR, + storage: { + lastTransLt: 0n, + balance: { coins: toNano(500) }, + state: { + type: "active", + state: { + code, + data: beginCell() + .store( + storeSlasherData({ + currentVsetHash: Buffer.alloc(32), + validatorCount: 0, + sentBatches: Dictionary.empty(), + }), + ) + .endCell(), + special: { + tick: true, + tock: false, + }, + }, + }, + }, + storageStats: { + used: { + cells: 0n, + bits: 0n, + }, + lastPaid: 0, + duePayment: null, + storageExtra: null, + }, + }, + lastTransactionLt: 0n, + lastTransactionHash: 0n, + }); slasher = await blockchain.getContract(SLASHER_ADDR); }); @@ -124,7 +149,7 @@ describe("Slasher", () => { it("should accept valid blocks batch", async () => { const { isValid } = await getters(blockchain, slasher).isBlocksBatchValid({ blocksBatch: SAMPLE_BLOCKS_BATCH, - mcSeqno: 241, + mcSeqno: FIRST_MC_SEQNO, }); expect(isValid).toBe(true); }); @@ -138,7 +163,6 @@ describe("Slasher", () => { const validatorIdx = 0; const bodyToSign = beginCell() - .storeUint(nowMs, 64) .storeUint(expireAt, 32) .storeUint(SLASHER_OP_SEND_BLOCKS_BATCH, 32) .storeBuffer(currentVsetHash, 32) @@ -162,7 +186,7 @@ describe("Slasher", () => { { workchain: -1, shard: 1n << 63n, - seqno: 241, + seqno: FIRST_MC_SEQNO, rootHash: Buffer.alloc(32), fileHash: Buffer.alloc(32), }, @@ -175,6 +199,19 @@ describe("Slasher", () => { fileHash: Buffer.alloc(32), }, }; + + await slasher.runTickTock("tick"); + + // Check slasher state to have an updated vset + { + const state = slasher.accountState; + assert(state?.type === "active"); + + const slasherData = loadSlasherData(state.state.data!.asSlice()); + expect(slasherData.currentVsetHash).toEqual(currentVsetHash); + expect(slasherData.validatorCount).toBe(13); + } + const tx = await slasher.receiveMessage({ info: { type: "external-in", @@ -185,6 +222,36 @@ describe("Slasher", () => { }); assert(tx.description.type === "generic"); expect(tx.description.aborted).toBe(false); + + { + const state = slasher.accountState; + assert(state?.type === "active"); + + const slasherData = loadSlasherData(state.state.data!.asSlice()); + expect(slasherData.currentVsetHash).toEqual(currentVsetHash); + expect(slasherData.validatorCount).toBe(13); + + const ourState = slasherData.sentBatches.get(0)!; + expect(ourState.pubkey).toEqual(keypair.publicKey); + + const firstSeqno = SAMPLE_BLOCKS_BATCH.asSlice().loadUint(32); + expect(ourState.minSeqno).toBe(firstSeqno + BLOCKS_BATCH_SIZE); + } + + try { + await slasher.receiveMessage({ + info: { + type: "external-in", + dest: SLASHER_ADDR, + importFee: 0n, + }, + body, + }); + assert(false, "tx must fail"); + } catch (error: any) { + assert(error instanceof EmulationError); + expect(error.exitCode).toBe(100); + } }); }); diff --git a/contracts/tsconfig.json b/contracts/tsconfig.json index 8a5861b163..7bc40d32b2 100644 --- a/contracts/tsconfig.json +++ b/contracts/tsconfig.json @@ -7,6 +7,7 @@ "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, - "skipLibCheck": true + "skipLibCheck": true, + "types": ["jest"] } } diff --git a/contracts/wrappers/Slasher.ts b/contracts/wrappers/Slasher.ts index 9eeca67041..e8253f4c3c 100644 --- a/contracts/wrappers/Slasher.ts +++ b/contracts/wrappers/Slasher.ts @@ -4,6 +4,8 @@ import { Cell, Contract, ContractProvider, + Dictionary, + DictionaryValue, Slice, } from "@ton/core"; import { UnknownTagError } from "./util"; @@ -41,18 +43,58 @@ export function storeSlasherParams( } export type SlasherData = { - updatedAtMs: bigint; + currentVsetHash: Buffer; + validatorCount: number; + sentBatches: Dictionary; }; export function loadSlasherData(cs: Slice): SlasherData { return { - updatedAtMs: cs.loadUintBig(64), + currentVsetHash: cs.loadBuffer(32), + validatorCount: cs.loadUint(16), + sentBatches: cs.loadDict( + Dictionary.Keys.Uint(16), + SlasherValidatorStateValue, + ), }; } export function storeSlasherData(s: SlasherData): (builder: Builder) => void { return (builder) => { - builder.storeUint(s.updatedAtMs, 64); + builder.storeBuffer(s.currentVsetHash, 32); + builder.storeUint(s.validatorCount, 16); + builder.storeDict(s.sentBatches); + }; +} + +export const SlasherValidatorStateValue: DictionaryValue = + { + serialize: (src, builder) => builder.store(storeSlasherValidatorState(src)), + parse: (cs) => { + const res = loadSlasherValidatorState(cs); + cs.endParse(); + return res; + }, + }; + +export type SlasherValidatorState = { + pubkey: Buffer; + minSeqno: number; +}; + +export function loadSlasherValidatorState(cs: Slice): SlasherValidatorState { + return { + pubkey: cs.loadBuffer(32), + minSeqno: cs.loadUint(32), + }; +} + +export function storeSlasherValidatorState( + s: SlasherValidatorState, +): (builder: Builder) => void { + return (builder) => { + builder.storeBuffer(s.pubkey, 32); + builder.storeUint(s.minSeqno, 32); }; } diff --git a/contracts/yarn.lock b/contracts/yarn.lock index f219cd00bd..e662a31a3b 100644 --- a/contracts/yarn.lock +++ b/contracts/yarn.lock @@ -2004,9 +2004,9 @@ has-tostringtag@^1.0.2: has-symbols "^1.0.3" hasown@^2.0.2: - version "2.0.2" - resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz" - integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + version "2.0.4" + resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz" + integrity sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A== dependencies: function-bind "^1.1.2" diff --git a/slasher/src/bc/contract.rs b/slasher/src/bc/contract.rs index 2c9dba6f78..b900d5aa78 100644 --- a/slasher/src/bc/contract.rs +++ b/slasher/src/bc/contract.rs @@ -65,7 +65,6 @@ impl SlasherContract for StdSlasherContract { 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_u32(Self::OP_SEND_BLOCKS_BATCH)?; b.store_u256(¶ms.vset_hash)?; @@ -110,6 +109,7 @@ impl SlasherContract for StdSlasherContract { { break 'check; } + // Skip all failed transactions. return Ok(None); }; @@ -123,7 +123,7 @@ impl SlasherContract for StdSlasherContract { // TODO: Add message op let mut body = msg.body; - body.skip_first(512 + 64 + 32, 0)?; + body.skip_first(512 + 32, 0)?; let op = body.load_u32()?; if op != Self::OP_SEND_BLOCKS_BATCH { return Ok(None); From f1b0b1fb1e826abaea8c5c7fbe0ed4547bfbb45b Mon Sep 17 00:00:00 2001 From: Ivan Kalinin Date: Tue, 9 Jun 2026 14:17:13 +0200 Subject: [PATCH 26/31] fix(slasher): review fixes --- collator/tests/validator_tests.rs | 109 ------------------ slasher/src/lib.rs | 179 +++++++++++++++--------------- 2 files changed, 89 insertions(+), 199 deletions(-) diff --git a/collator/tests/validator_tests.rs b/collator/tests/validator_tests.rs index b4a82c61d4..d637f72007 100644 --- a/collator/tests/validator_tests.rs +++ b/collator/tests/validator_tests.rs @@ -174,39 +174,6 @@ async fn validator_signatures_match() -> Result<()> { 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))?; @@ -325,38 +292,6 @@ 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; } } @@ -430,7 +365,6 @@ async fn network_gets_stuck_without_signatures() -> Result<()> { } } - // let range = block_id.as_short_id()..=block_id.as_short_id(); tokio::select! { _ = good_validators.next() => { panic!("good validator completed block"); @@ -440,49 +374,6 @@ 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/slasher/src/lib.rs b/slasher/src/lib.rs index 59bd706786..26beb33364 100644 --- a/slasher/src/lib.rs +++ b/slasher/src/lib.rs @@ -322,109 +322,108 @@ impl Slasher { tracing::trace!(?slasher_params, ?current_session_id); - // TODO: Move into blocking. - let extra = cx.block.load_extra()?.account_blocks.load()?; - if let Some((_, account_block)) = extra.get(slasher_params.address)? { - 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", - ); - - let 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; + let block = cx.block.clone(); + let shared = self.shared.clone(); + let collector = self.validator_events_collector.clone(); + let cancellation_token = self.cancellation_token.clone(); + tokio::task::spawn_blocking(move || { + let extra = block.load_extra()?.account_blocks.load()?; + if let Some((_, account_block)) = extra.get(slasher_params.address)? { + 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", + ); - tracing::info!( + let own_message = subscription.handle_account_transaction(tx_hash, &tx)?; + + match shared.contract.decode_event(&tx) { + Ok(Some(event)) => match event { + bc::SlasherContractEvent::SubmitBlocksBatch(submitted) => { + let batch = &submitted.blocks_batch; + + tracing::info!( + %tx_hash, + vset_hash = %submitted.vset_hash, + 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(), + is_own = own_message, + "blocks batch submitted", + ); + + // NOTE: Might increment batches twice on restart, + // but it's better to keep this simple. + let origin = if own_message { "own" } else { "external" }; + metrics::counter!( + METRIC_BLOCKS_BATCHES_SUBMITTED_TOTAL, + "origin" => origin, + ) + .increment(1); + + shared.storage.store_blocks_batch( + &submitted.vset_hash, + submitted.validator_idx, + &submitted.blocks_batch, + )?; + } + }, + Ok(None) => {} + Err(e) => { + tracing::warn!( %tx_hash, - vset_hash = %submitted.vset_hash, - 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(), - is_own = own_message, - "blocks batch submitted", + "failed to parse slasher event: {e:?}" ); - - // NOTE: Might increment batches twice on restart, - // but it's better to keep this simple. - let origin = if own_message { "own" } else { "external" }; - metrics::counter!( - METRIC_BLOCKS_BATCHES_SUBMITTED_TOTAL, - "origin" => origin, - ) - .increment(1); - - this.storage.store_blocks_batch( - &submitted.vset_hash, - 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:?}" - ); } } } - } - - // Update subscription state. - subscription.cleanup_expired_messages(cx.block.load_info()?.gen_utime); - // Trigger reporting. - if let Some(vset) = vset_to_complete - && let Some(last_seqno) = mc_seqno.checked_sub(1) - { - self.shared - .complete_vset(&vset, last_seqno, &slasher_params)?; - } + // Update subscription state. + subscription.cleanup_expired_messages(block.load_info()?.gen_utime); - // Start session handlers. - 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; + // Trigger reporting. + if let Some(vset) = vset_to_complete + && let Some(last_seqno) = mc_seqno.checked_sub(1) + { + shared.complete_vset(&vset, last_seqno, &slasher_params)?; } - let (tx, rx) = mpsc::unbounded_channel::(); - if !self.validator_events_collector.init_session( - session_id, - slasher_params.blocks_batch_size, - tx, - ) { - tracing::warn!(?session_id, "session removed before init"); - continue; - } + // Start session handlers. + while let Some(session_info) = 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(&shared.node_keys.public_key) { + tracing::info!(?session_id, "skipping session"); + 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)), - ); - } + let (tx, rx) = mpsc::unbounded_channel::(); + if !collector.init_session(session_id, slasher_params.blocks_batch_size, tx) { + tracing::warn!(?session_id, "session removed before init"); + continue; + } - Ok(()) + let token = cancellation_token.clone(); + let shared = shared.clone(); + tokio::task::spawn( + token.run_until_cancelled_owned( + shared.send_batches_to_contract(session_info, rx), + ), + ); + } + + Ok(()) + }) + .await? } } From 6fb79fd8b0d4e5a9026d2186331472282a349a34 Mon Sep 17 00:00:00 2001 From: Ivan Kalinin Date: Mon, 22 Jun 2026 12:53:59 +0200 Subject: [PATCH 27/31] feat(collator): reuse `CreateStats` feature for vset migration --- block-util/src/block/block_proof_stuff.rs | 47 +++++++--- block-util/src/block/mod.rs | 2 +- collator/Cargo.toml | 1 - collator/src/collator/do_collate/finalize.rs | 90 +------------------- collator/src/collator/do_collate/mod.rs | 5 -- collator/src/collator/types.rs | 19 ----- collator/src/manager/blocks_cache.rs | 10 --- collator/src/manager/state_update_handler.rs | 54 +++++++----- collator/src/types.rs | 2 - 9 files changed, 70 insertions(+), 160 deletions(-) diff --git a/block-util/src/block/block_proof_stuff.rs b/block-util/src/block/block_proof_stuff.rs index 4f407637f0..fef5aebc0a 100644 --- a/block-util/src/block/block_proof_stuff.rs +++ b/block-util/src/block/block_proof_stuff.rs @@ -348,7 +348,8 @@ impl BlockProofStuff { (validator_set, shuffle_validators) }; - self.calc_validators_subset_standard(&validator_set, shuffle_validators) + let mode = ValidatorSubsetMode::from_capabilities(block_info.gen_software.capabilities); + self.calc_validators_subset_standard(&validator_set, mode, shuffle_validators) } fn process_prev_key_block_proof( @@ -380,24 +381,24 @@ impl BlockProofStuff { (validator_set, shuffle_validators) }; - self.calc_validators_subset_standard(&validator_set, shuffle_validators) + let caps = prev_key_block_info.gen_software.capabilities; + let mode = ValidatorSubsetMode::from_capabilities(caps); + self.calc_validators_subset_standard(&validator_set, mode, shuffle_validators) } fn calc_validators_subset_standard( &self, validator_set: &ValidatorSet, + mode: ValidatorSubsetMode, shuffle_validators: bool, ) -> Result { - let Some(vset_switch_round) = self - .inner - .proof - .signatures - .as_ref() - .map(|s| s.consensus_info.vset_switch_round) - else { - anyhow::bail!("no `consensus_info` to compute subset from"); + let Some(vset_nonce) = self.inner.proof.signatures.as_ref().map(|s| match mode { + ValidatorSubsetMode::Original => s.validator_info.catchain_seqno, + ValidatorSubsetMode::BySwitchRound => s.consensus_info.vset_switch_round, + }) else { + anyhow::bail!("no signatures info to compute subset from"); }; - ValidatorSubsetInfo::compute_standard(validator_set, vset_switch_round, shuffle_validators) + ValidatorSubsetInfo::compute_standard(validator_set, vset_nonce, shuffle_validators) } } @@ -528,11 +529,11 @@ pub struct ValidatorSubsetInfo { impl ValidatorSubsetInfo { pub fn compute_standard( validator_set: &ValidatorSet, - vset_switch_round: u32, + vset_nonce: u32, shuffle_validators: bool, ) -> Result { let Some((validators, short_hash)) = - validator_set.compute_mc_subset_indexed(vset_switch_round, shuffle_validators) + validator_set.compute_mc_subset_indexed(vset_nonce, shuffle_validators) else { anyhow::bail!("failed to compute a validator subset"); }; @@ -544,6 +545,26 @@ impl ValidatorSubsetInfo { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ValidatorSubsetMode { + Original, + BySwitchRound, +} + +impl ValidatorSubsetMode { + pub fn from_capabilities(caps: GlobalCapabilities) -> Self { + // NOTE: We are using this capability because it should be disabled for + // Tycho-based networks, but for some reason it is enabled everywhere. + // By relying on this capability we can do two things at once during + // migration. + if caps.contains(GlobalCapability::CapCreateStatsEnabled) { + Self::Original + } else { + Self::BySwitchRound + } + } +} + // TODO: Move into `types`. pub struct AlwaysInclude; diff --git a/block-util/src/block/mod.rs b/block-util/src/block/mod.rs index 9923abf079..099ad14af6 100644 --- a/block-util/src/block/mod.rs +++ b/block-util/src/block/mod.rs @@ -3,7 +3,7 @@ use tycho_types::models::ShardIdent; pub use self::block_id_ext::{BlockIdExt, BlockIdRelation, calc_next_block_id_short}; pub use self::block_proof_stuff::{ - AlwaysInclude, BlockProofStuff, BlockProofStuffAug, ValidatorSubsetInfo, + AlwaysInclude, BlockProofStuff, BlockProofStuffAug, ValidatorSubsetInfo, ValidatorSubsetMode, check_with_master_state, check_with_prev_key_block_proof, }; pub use self::block_stuff::{BlockStuff, BlockStuffAug}; diff --git a/collator/Cargo.toml b/collator/Cargo.toml index a09876787f..5750a2b3eb 100644 --- a/collator/Cargo.toml +++ b/collator/Cargo.toml @@ -90,7 +90,6 @@ tycho-util = { workspace = true, features = ["test"] } [features] default = [] test = ["tycho-block-util/test", "tycho-storage/test", "tycho-core/test", "tycho-consensus/test"] -block-creator-stats = [] bench-helpers = [] [lints] diff --git a/collator/src/collator/do_collate/finalize.rs b/collator/src/collator/do_collate/finalize.rs index 3c5bbe6041..34163a34d2 100644 --- a/collator/src/collator/do_collate/finalize.rs +++ b/collator/src/collator/do_collate/finalize.rs @@ -941,27 +941,7 @@ impl Phase { global_balance.try_sub_assign(&collation_data.value_flow.burned)?; global_balance.try_add_assign(&collation_data.shard_fees.root_extra().create)?; - // 9. update block creator stats - #[cfg(not(feature = "block-creator-stats"))] - let block_create_stats = None; - #[cfg(feature = "block-creator-stats")] - let block_create_stats = if prev_state_extra - .config - .get_global_version()? - .capabilities - .contains(GlobalCapability::CapCreateStatsEnabled) - { - let mut stats = prev_state_extra - .block_create_stats - .clone() - .unwrap_or_default(); - Self::update_block_creator_stats(collation_data, &mut stats)?; - Some(stats) - } else { - None - }; - - // 10. pack new McStateExtra + // 9. pack new McStateExtra let mc_state_extra = McStateExtra { shards, config, @@ -970,7 +950,7 @@ impl Phase { prev_blocks, after_key_block: is_key_block, last_key_block, - block_create_stats, + block_create_stats: None, global_balance, }; @@ -1202,72 +1182,6 @@ impl Phase { .map_err(Into::into) } - #[cfg(feature = "block-creator-stats")] - fn update_block_creator_stats( - collation_data: &BlockCollationData, - block_create_stats: &mut Dict, - ) -> Result<()> { - let mut mc_updated = false; - for (creator, count) in &collation_data.block_create_count { - let shard_scaled = count << 32; - let total_mc = if collation_data.created_by == *creator { - mc_updated = true; - 1 - } else { - 0 - }; - - block_create_stats.set(creator, CreatorStats { - mc_blocks: BlockCounters { - updated_at: collation_data.gen_utime, - total: total_mc, - cnt2048: total_mc, - cnt65536: total_mc, - }, - shard_blocks: BlockCounters { - updated_at: collation_data.gen_utime, - total: *count, - cnt2048: shard_scaled, - cnt65536: shard_scaled, - }, - })?; - } - if !mc_updated { - block_create_stats.set(collation_data.created_by, CreatorStats { - mc_blocks: BlockCounters { - updated_at: collation_data.gen_utime, - total: 1, - cnt2048: 1, - cnt65536: 1, - }, - shard_blocks: BlockCounters { - updated_at: collation_data.gen_utime, - total: 0, - cnt2048: 0, - cnt65536: 0, - }, - })?; - } - - let default_shard_blocks_count = collation_data.block_create_count.values().sum(); - block_create_stats.set(HashBytes::default(), CreatorStats { - mc_blocks: BlockCounters { - updated_at: collation_data.gen_utime, - total: 1, - cnt2048: 1, - cnt65536: 1, - }, - shard_blocks: BlockCounters { - updated_at: collation_data.gen_utime, - total: default_shard_blocks_count, - cnt2048: default_shard_blocks_count << 32, - cnt65536: default_shard_blocks_count << 32, - }, - })?; - // TODO: prune CreatorStats https://github.com/ton-blockchain/ton/blob/master/validator/impl/collator.cpp#L4191 - Ok(()) - } - fn check_value_flow( value_flow: &ValueFlow, collation_data: &BlockCollationData, diff --git a/collator/src/collator/do_collate/mod.rs b/collator/src/collator/do_collate/mod.rs index bb26dc83aa..333516797a 100644 --- a/collator/src/collator/do_collate/mod.rs +++ b/collator/src/collator/do_collate/mod.rs @@ -930,8 +930,6 @@ impl CollatorStdImpl { processed_to_anchor_id, value_flow, proof_funds, - #[cfg(feature = "block-creator-stats")] - creators, processed_to_by_partitions, } = top_block_descr; @@ -996,9 +994,6 @@ impl CollatorStdImpl { block_id, processed_to_by_partitions, }); - - #[cfg(feature = "block-creator-stats")] - collation_data_builder.register_shard_block_creators(creators)?; } // filling mc_processed_to_by_partitions diff --git a/collator/src/collator/types.rs b/collator/src/collator/types.rs index bb9c78799f..85f296dbe2 100644 --- a/collator/src/collator/types.rs +++ b/collator/src/collator/types.rs @@ -320,8 +320,6 @@ pub(super) struct BlockCollationDataBuilder { pub shard_fees: ShardFees, pub min_ref_mc_seqno: u32, pub rand_seed: HashBytes, - #[cfg(feature = "block-creator-stats")] - pub block_create_count: FastHashMap, pub created_by: HashBytes, pub top_shard_blocks: Vec, @@ -352,8 +350,6 @@ impl BlockCollationDataBuilder { shard_fees: Default::default(), min_ref_mc_seqno, rand_seed, - #[cfg(feature = "block-creator-stats")] - block_create_count: Default::default(), created_by, shards: None, top_shard_blocks: vec![], @@ -397,17 +393,6 @@ impl BlockCollationDataBuilder { Ok(()) } - #[cfg(feature = "block-creator-stats")] - pub fn register_shard_block_creators(&mut self, creators: Vec) -> Result<()> { - for creator in creators { - self.block_create_count - .entry(creator) - .and_modify(|count| *count += 1) - .or_insert(1); - } - Ok(()) - } - pub fn build(self, start_lt: u64, block_limits: BlockLimits) -> BlockCollationData { let block_limit = BlockLimitStats::new(block_limits, start_lt); BlockCollationData { @@ -450,8 +435,6 @@ impl BlockCollationDataBuilder { recover_create_msg: None, mempool_config_override: self.mempool_config_override, consensus_config_changed: None, - #[cfg(feature = "block-creator-stats")] - block_create_count: self.block_create_count, diff_tail_len: 0, mc_shards_processed_to_by_partitions: self.mc_shards_processed_to_by_partitions, } @@ -533,8 +516,6 @@ pub(super) struct BlockCollationData { /// `None` - if it is a shard block or not a key master block. pub consensus_config_changed: Option, - #[cfg(feature = "block-creator-stats")] - pub block_create_count: FastHashMap, pub diff_tail_len: u32, } diff --git a/collator/src/manager/blocks_cache.rs b/collator/src/manager/blocks_cache.rs index 62a8dd3e3b..036a7dc0b8 100644 --- a/collator/src/manager/blocks_cache.rs +++ b/collator/src/manager/blocks_cache.rs @@ -82,8 +82,6 @@ impl BlocksCache { processed_to_anchor_id: additional_info.processed_to_anchor_id, value_flow: std::mem::take(&mut shard_cache.data.value_flow), proof_funds: std::mem::take(&mut shard_cache.data.proof_funds), - #[cfg(feature = "block-creator-stats")] - creators: std::mem::take(&mut shard_cache.data.creators), processed_to_by_partitions, }); break; @@ -1151,8 +1149,6 @@ impl BlocksCacheData for MasterBlocksCacheData { struct ShardBlocksCacheData { value_flow: ValueFlow, proof_funds: ShardFeeCreated, - #[cfg(feature = "block-creator-stats")] - creators: Vec, } impl ShardBlocksCacheData { @@ -1166,18 +1162,12 @@ impl ShardBlocksCacheData { .create .try_add_assign(&candidate.value_flow.created)?; - #[cfg(feature = "block-creator-stats")] - self.creators.push(candidate.created_by); - Ok(()) } fn reset_top_shard_block_additional_info(&mut self) { self.value_flow = Default::default(); self.proof_funds = Default::default(); - - #[cfg(feature = "block-creator-stats")] - self.creators.clear(); } } diff --git a/collator/src/manager/state_update_handler.rs b/collator/src/manager/state_update_handler.rs index 11fe14882c..1ff2c0c9f3 100644 --- a/collator/src/manager/state_update_handler.rs +++ b/collator/src/manager/state_update_handler.rs @@ -4,10 +4,11 @@ use std::sync::Arc; use ahash::HashMapExt; use anyhow::{Context, Result, anyhow}; use tokio::sync::Notify; -use tycho_block_util::block::{ValidatorSubsetInfo, calc_next_block_id_short}; +use tycho_block_util::block::{ValidatorSubsetInfo, ValidatorSubsetMode, calc_next_block_id_short}; use tycho_block_util::config::BlockchainConfigExt; use tycho_types::models::{ - BlockId, GlobalCapabilities, IndexedValidatorDescription, ShardIdent, ValidatorSet, + BlockId, GlobalCapabilities, GlobalVersion, IndexedValidatorDescription, ShardIdent, + ValidatorSet, }; use tycho_util::futures::JoinTask; use tycho_util::metrics::HistogramGuard; @@ -120,19 +121,21 @@ where let mut collator_tasks = vec![]; if !matches!(mode, ProcessMcStateUpdateMode::SkipProcess) { - let block_global = mc_data.config.get_global_version()?; - if self.config.supported_block_version >= block_global.version - && block_global + let global = mc_data.config.get_global_version()?; + if self.config.supported_block_version >= global.version + && global .capabilities .is_subset_of(self.config.supported_capabilities) { - collator_tasks = self.refresh_collation_sessions(mc_data, mode).await?; + collator_tasks = self + .refresh_collation_sessions(mc_data, mode, global) + .await?; } else { tracing::warn!(target: tracing_targets::COLLATION_MANAGER, collator_supported_block_version = self.config.supported_block_version, - mc_block_version = block_global.version, + mc_block_version = global.version, collator_supported_capabilities = ?self.config.supported_capabilities, - mc_block_capabilities = ?block_global.capabilities, + mc_block_capabilities = ?global.capabilities, "Refresh collation sessions is skipped: collator does not support mc block version or capabilities", ); } @@ -149,6 +152,7 @@ where &self, mc_data: Arc, mode: ProcessMcStateUpdateMode, + global: GlobalVersion, ) -> Result>> { tracing::debug!(target: tracing_targets::COLLATION_MANAGER, "Start refresh collation sessions by mc state ({})...", @@ -200,23 +204,27 @@ where let collation_config = mc_data.config.get_collation_config()?; 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(); - Result::<_>::Ok((subset.clone(), *hash_short)) - } hash_map::Entry::Vacant(entry) => { + let vset_mode = ValidatorSubsetMode::from_capabilities(global.capabilities); + let vset_nonce = match vset_mode { + ValidatorSubsetMode::Original => catchain_seqno, + ValidatorSubsetMode::BySwitchRound => vset_switch_round, + }; + let (subset, hash_short) = full_validators_set - .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, + .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 = {}, vset_nonce = {vset_nonce})", + ShardIdent::MASTERCHAIN, + ) + })?; - let subset: FastHashMap<_, _> = subset + let subset: FastHashMap<[u8; 32], IndexedValidatorDescription> = subset .into_iter() .map(|vldr| (vldr.desc.public_key.into(), vldr)) .collect(); @@ -225,6 +233,10 @@ where entry.insert((subset.clone(), hash_short)); Ok((subset, hash_short)) } + hash_map::Entry::Occupied(entry) => { + let (subset, hash_short) = entry.get(); + Result::<_>::Ok((subset.clone(), *hash_short)) + } }; // detect sessions and collators to start and to finish diff --git a/collator/src/types.rs b/collator/src/types.rs index b32a4b847f..22f9f23048 100644 --- a/collator/src/types.rs +++ b/collator/src/types.rs @@ -452,8 +452,6 @@ pub struct TopBlockDescription { pub processed_to_anchor_id: u32, pub value_flow: ValueFlow, pub proof_funds: ShardFeeCreated, - #[cfg(feature = "block-creator-stats")] - pub creators: Vec, pub processed_to_by_partitions: ProcessedToByPartitions, } From 07e7bb552665a4a57ccdd09977c4b1be51b482fb Mon Sep 17 00:00:00 2001 From: Kirill Mikheev Date: Tue, 23 Jun 2026 00:25:42 +0300 Subject: [PATCH 28/31] fix(collator): catchain seqno compatibility --- block-util/src/block/block_proof_stuff.rs | 35 ++-- block-util/src/block/mod.rs | 2 +- collator/src/collator/do_collate/finalize.rs | 159 +++++++++---------- collator/src/manager/state_update_handler.rs | 22 +-- 4 files changed, 108 insertions(+), 110 deletions(-) diff --git a/block-util/src/block/block_proof_stuff.rs b/block-util/src/block/block_proof_stuff.rs index fef5aebc0a..0a0743709e 100644 --- a/block-util/src/block/block_proof_stuff.rs +++ b/block-util/src/block/block_proof_stuff.rs @@ -348,7 +348,7 @@ impl BlockProofStuff { (validator_set, shuffle_validators) }; - let mode = ValidatorSubsetMode::from_capabilities(block_info.gen_software.capabilities); + let mode = CatchainSeqnoMode::from_capabilities(block_info.gen_software.capabilities); self.calc_validators_subset_standard(&validator_set, mode, shuffle_validators) } @@ -382,23 +382,32 @@ impl BlockProofStuff { }; let caps = prev_key_block_info.gen_software.capabilities; - let mode = ValidatorSubsetMode::from_capabilities(caps); + let mode = CatchainSeqnoMode::from_capabilities(caps); self.calc_validators_subset_standard(&validator_set, mode, shuffle_validators) } + /// NOTE: Despite the `mode` the only nonce used for subsets shuffling is + /// `vset_switch_round`. Subset mode only controls from where we extract it. fn calc_validators_subset_standard( &self, validator_set: &ValidatorSet, - mode: ValidatorSubsetMode, + mode: CatchainSeqnoMode, shuffle_validators: bool, ) -> Result { - let Some(vset_nonce) = self.inner.proof.signatures.as_ref().map(|s| match mode { - ValidatorSubsetMode::Original => s.validator_info.catchain_seqno, - ValidatorSubsetMode::BySwitchRound => s.consensus_info.vset_switch_round, + let Some(switch_round) = self.inner.proof.signatures.as_ref().map(|s| match mode { + // Originally (before https://github.com/broxus/tycho/pull/1098) `catchain_seqno` + // was initialized with `vset_switch_round`. However, we were reading this round + // "indirectly" from `validator_info` and didn't notice that `consensus_info` was + // set from the **next** state. So for old blocks/proofs we must continue to read + // switch round this way. + CatchainSeqnoMode::Original => s.validator_info.catchain_seqno, + // This mode can only be enabled on a new version of the node, so `consensus_info` + // is the same which was used to produce this block. + CatchainSeqnoMode::Sequential => s.consensus_info.vset_switch_round, }) else { anyhow::bail!("no signatures info to compute subset from"); }; - ValidatorSubsetInfo::compute_standard(validator_set, vset_nonce, shuffle_validators) + ValidatorSubsetInfo::compute_standard(validator_set, switch_round, shuffle_validators) } } @@ -529,11 +538,11 @@ pub struct ValidatorSubsetInfo { impl ValidatorSubsetInfo { pub fn compute_standard( validator_set: &ValidatorSet, - vset_nonce: u32, + vset_switch_round: u32, shuffle_validators: bool, ) -> Result { let Some((validators, short_hash)) = - validator_set.compute_mc_subset_indexed(vset_nonce, shuffle_validators) + validator_set.compute_mc_subset_indexed(vset_switch_round, shuffle_validators) else { anyhow::bail!("failed to compute a validator subset"); }; @@ -546,12 +555,12 @@ impl ValidatorSubsetInfo { } #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ValidatorSubsetMode { +pub enum CatchainSeqnoMode { Original, - BySwitchRound, + Sequential, } -impl ValidatorSubsetMode { +impl CatchainSeqnoMode { pub fn from_capabilities(caps: GlobalCapabilities) -> Self { // NOTE: We are using this capability because it should be disabled for // Tycho-based networks, but for some reason it is enabled everywhere. @@ -560,7 +569,7 @@ impl ValidatorSubsetMode { if caps.contains(GlobalCapability::CapCreateStatsEnabled) { Self::Original } else { - Self::BySwitchRound + Self::Sequential } } } diff --git a/block-util/src/block/mod.rs b/block-util/src/block/mod.rs index 099ad14af6..ad85d73402 100644 --- a/block-util/src/block/mod.rs +++ b/block-util/src/block/mod.rs @@ -3,7 +3,7 @@ use tycho_types::models::ShardIdent; pub use self::block_id_ext::{BlockIdExt, BlockIdRelation, calc_next_block_id_short}; pub use self::block_proof_stuff::{ - AlwaysInclude, BlockProofStuff, BlockProofStuffAug, ValidatorSubsetInfo, ValidatorSubsetMode, + AlwaysInclude, BlockProofStuff, BlockProofStuffAug, CatchainSeqnoMode, ValidatorSubsetInfo, check_with_master_state, check_with_prev_key_block_proof, }; pub use self::block_stuff::{BlockStuff, BlockStuffAug}; diff --git a/collator/src/collator/do_collate/finalize.rs b/collator/src/collator/do_collate/finalize.rs index 34163a34d2..55efccf77a 100644 --- a/collator/src/collator/do_collate/finalize.rs +++ b/collator/src/collator/do_collate/finalize.rs @@ -872,10 +872,10 @@ impl Phase { collation_data, prev_processed_to_anchor, prev_state_extra, - prev_config, - &config, + prev_config.get_consensus_config()?, + &config.get_consensus_config()?, &consensus_info, - )?; + ); // remember if consensus config changed collation_data.consensus_config_changed = @@ -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 { @@ -1390,15 +1389,18 @@ fn create_merkle_update( } mod vset_update_start { + use tycho_block_util::block::CatchainSeqnoMode; + use super::*; #[cfg_attr(test, derive(Clone, Debug))] pub struct KbNextSessionStart { prev_consensus_config: ConsensusConfig, + prev_catchain_seqno: u32, 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, @@ -1409,29 +1411,24 @@ mod vset_update_start { collation_data: &BlockCollationData, prev_processed_to_anchor: MempoolAnchorId, prev_state_extra: &McStateExtra, - prev_config: &BlockchainConfig, - config: &BlockchainConfig, + prev_consensus_config: ConsensusConfig, + consensus_config: &ConsensusConfig, consensus_info: &ConsensusInfo, - ) -> Result { - let prev_consensus_config = prev_config.get_consensus_config()?; - + ) -> Self { let after_pause_round = Self::after_pause_round(prev_processed_to_anchor, &prev_consensus_config); - Ok(Self { - is_consensus_info_overridden: { - consensus_info != &prev_state_extra.consensus_info - }, - is_consensus_config_changed: { - prev_consensus_config != config.get_consensus_config()? - }, + Self { + is_consensus_info_overridden: consensus_info != &prev_state_extra.consensus_info, + is_consensus_config_changed: prev_consensus_config != *consensus_config, is_curr_switch_after_pause: consensus_info.vset_switch_round > after_pause_round, gen_chain_time_millis: collation_data.get_gen_chain_time(), after_pause_round, prev_consensus_config, - }) + prev_catchain_seqno: prev_state_extra.validator_info.catchain_seqno, + } } /// `prev_processed_to_anchor` is a round in the ending session, after which @@ -1495,6 +1492,7 @@ mod vset_update_start { prev_shuffle_mc_validators: bool, prev_vset_hash: HashBytes, + catchain_seqno_mode: CatchainSeqnoMode, shuffle_mc_validators: bool, current_vset: Cell, } @@ -1509,6 +1507,9 @@ mod vset_update_start { }, prev_vset_hash: *prev_config.get_current_validator_set_raw()?.repr_hash(), + catchain_seqno_mode: CatchainSeqnoMode::from_capabilities( + config.get_global_version()?.capabilities, + ), shuffle_mc_validators: config.get_collation_config()?.shuffle_mc_validators, current_vset: config.get_current_validator_set_raw()?, }) @@ -1517,9 +1518,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; @@ -1527,10 +1527,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 @@ -1538,7 +1534,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; } @@ -1552,12 +1548,20 @@ mod vset_update_start { .compute_mc_subset(next_session_start_round, self.shuffle_mc_validators) else { anyhow::bail!( - "Error calculating subset of validators for next session \ - (shard_id = {}, catchain_seqno = {catchain_seqno})", + "Error calculating subset of validators for next session: \ + shard_id = {}, start_round = {next_session_start_round}", ShardIdent::MASTERCHAIN, ); }; + let catchain_seqno = match self.catchain_seqno_mode { + CatchainSeqnoMode::Original => next_session_start_round, + CatchainSeqnoMode::Sequential => session_start + .prev_catchain_seqno + .checked_add(1) + .context("catchain seqno overflow")?, + }; + Ok(Some(ValidatorInfo { validator_list_hash_short, // TODO: rename field in types @@ -1603,18 +1607,26 @@ mod vset_update_start { } } + fn random_session_start() -> KbNextSessionStart { + KbNextSessionStart { + prev_consensus_config: default_test_config().conf.consensus, + prev_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()); @@ -1628,12 +1640,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); @@ -1674,6 +1684,7 @@ mod vset_update_start { let stub_update = KbNextSessionUpdate { prev_shuffle_mc_validators: false, prev_vset_hash: HashBytes([0; _]), + catchain_seqno_mode: CatchainSeqnoMode::Sequential, shuffle_mc_validators: false, current_vset: v_set_cell(), }; @@ -1687,6 +1698,7 @@ mod vset_update_start { KbNextSessionStart::after_pause_round(processed_up_to, &cons_conf); let start_1 = KbNextSessionStart { + prev_catchain_seqno: 10, prev_consensus_config: cons_conf.clone(), is_consensus_info_overridden: false, is_consensus_config_changed: false, @@ -1700,59 +1712,48 @@ 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.prev_catchain_seqno + 1 + ); // 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); @@ -1760,26 +1761,26 @@ 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, prev_vset_hash: *v_set_cell().repr_hash(), + catchain_seqno_mode: CatchainSeqnoMode::Original, shuffle_mc_validators: is_shuffle, current_vset: v_set_cell(), }; @@ -1787,9 +1788,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:?}"); diff --git a/collator/src/manager/state_update_handler.rs b/collator/src/manager/state_update_handler.rs index 1ff2c0c9f3..c6f46ece76 100644 --- a/collator/src/manager/state_update_handler.rs +++ b/collator/src/manager/state_update_handler.rs @@ -4,11 +4,10 @@ use std::sync::Arc; use ahash::HashMapExt; use anyhow::{Context, Result, anyhow}; use tokio::sync::Notify; -use tycho_block_util::block::{ValidatorSubsetInfo, ValidatorSubsetMode, calc_next_block_id_short}; +use tycho_block_util::block::{ValidatorSubsetInfo, calc_next_block_id_short}; use tycho_block_util::config::BlockchainConfigExt; use tycho_types::models::{ - BlockId, GlobalCapabilities, GlobalVersion, IndexedValidatorDescription, ShardIdent, - ValidatorSet, + BlockId, GlobalCapabilities, IndexedValidatorDescription, ShardIdent, ValidatorSet, }; use tycho_util::futures::JoinTask; use tycho_util::metrics::HistogramGuard; @@ -127,9 +126,7 @@ where .capabilities .is_subset_of(self.config.supported_capabilities) { - collator_tasks = self - .refresh_collation_sessions(mc_data, mode, global) - .await?; + collator_tasks = self.refresh_collation_sessions(mc_data, mode).await?; } else { tracing::warn!(target: tracing_targets::COLLATION_MANAGER, collator_supported_block_version = self.config.supported_block_version, @@ -152,7 +149,6 @@ where &self, mc_data: Arc, mode: ProcessMcStateUpdateMode, - global: GlobalVersion, ) -> Result>> { tracing::debug!(target: tracing_targets::COLLATION_MANAGER, "Start refresh collation sessions by mc state ({})...", @@ -205,12 +201,6 @@ where let mut subset_cache = FastHashMap::new(); let mut get_validator_subset = |shard_id| match subset_cache.entry(shard_id) { hash_map::Entry::Vacant(entry) => { - let vset_mode = ValidatorSubsetMode::from_capabilities(global.capabilities); - let vset_nonce = match vset_mode { - ValidatorSubsetMode::Original => catchain_seqno, - ValidatorSubsetMode::BySwitchRound => vset_switch_round, - }; - let (subset, hash_short) = full_validators_set .compute_mc_subset_indexed( vset_switch_round, @@ -219,7 +209,7 @@ where .ok_or_else(|| { anyhow!( "Error calculating subset of validators for catchain session \ - (shard_id = {}, vset_nonce = {vset_nonce})", + (shard_id = {}, vset_switch_round = {vset_switch_round})", ShardIdent::MASTERCHAIN, ) })?; @@ -331,8 +321,8 @@ where let new_session_info = Arc::new(CollationSessionInfo::new( shard_id, - validation_session_id.0, - validation_session_id.1, + catchain_seqno, + vset_switch_round, ValidatorSubsetInfo { validators: subset.values().cloned().collect(), short_hash: hash_short, From e08a631ddd345216bc4d09ef3317746571b4271f Mon Sep 17 00:00:00 2001 From: Ivan Kalinin Date: Wed, 24 Jun 2026 15:01:02 +0200 Subject: [PATCH 29/31] fix(block-util): use capabilities from the checked block to get mode --- block-util/src/block/block_proof_stuff.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/block-util/src/block/block_proof_stuff.rs b/block-util/src/block/block_proof_stuff.rs index 0a0743709e..b8219c6284 100644 --- a/block-util/src/block/block_proof_stuff.rs +++ b/block-util/src/block/block_proof_stuff.rs @@ -355,6 +355,7 @@ impl BlockProofStuff { fn process_prev_key_block_proof( &self, prev_key_block_proof: &BlockProofStuff, + checked_block_info: &BlockInfo, ) -> Result { let (virt_key_block, prev_key_block_info) = prev_key_block_proof.pre_check_block_proof()?; @@ -381,7 +382,10 @@ impl BlockProofStuff { (validator_set, shuffle_validators) }; - let caps = prev_key_block_info.gen_software.capabilities; + // NOTE: `prev_key_block_info` is unused here. We only need the previous + // key block to get the current validator set. All other stuff, like + // vset switch round or gen capabilities, must be used from the current block. + let caps = checked_block_info.gen_software.capabilities; let mode = CatchainSeqnoMode::from_capabilities(caps); self.calc_validators_subset_standard(&validator_set, mode, shuffle_validators) } @@ -474,7 +478,7 @@ pub fn check_with_prev_key_block_proof( proof_id.seqno, ); - let subset = proof.process_prev_key_block_proof(prev_key_block_proof)?; + let subset = proof.process_prev_key_block_proof(prev_key_block_proof, virt_block_info)?; if virt_block_info.key_block { pre_check_key_block_proof(virt_block)?; From 288f06a7ea9ffcb903c34733c24f913d43833983 Mon Sep 17 00:00:00 2001 From: Ivan Kalinin Date: Wed, 24 Jun 2026 15:14:52 +0200 Subject: [PATCH 30/31] fix(slasher): apply updated batch size to all running sessions --- slasher/src/collector/validator_events.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/slasher/src/collector/validator_events.rs b/slasher/src/collector/validator_events.rs index 3cbbe38c59..4fa734507c 100644 --- a/slasher/src/collector/validator_events.rs +++ b/slasher/src/collector/validator_events.rs @@ -97,6 +97,18 @@ impl ValidatorEventsCollector { pub fn set_default_batch_size(&self, batch_size: NonZeroU32) { self.default_batch_size .store(batch_size.get(), Ordering::Release); + + for mut session in self.sessions.iter_mut() { + // 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, + ); + } + } } pub fn init_session( From 70e67a79a723ab2dae48c281b964b8bb853e4d37 Mon Sep 17 00:00:00 2001 From: Ivan Kalinin Date: Fri, 26 Jun 2026 15:35:40 +0200 Subject: [PATCH 31/31] chore(scripts): cleanup unused metrics --- scripts/gen-dashboard.py | 70 ++++++++++++++++------------------------ 1 file changed, 28 insertions(+), 42 deletions(-) diff --git a/scripts/gen-dashboard.py b/scripts/gen-dashboard.py index 3dba4d7d6f..dbff385422 100755 --- a/scripts/gen-dashboard.py +++ b/scripts/gen-dashboard.py @@ -1,38 +1,39 @@ #!/usr/bin/env python3 import sys from dataclasses import dataclass -from typing import Union, List, Literal +from typing import List, Literal, Union from dashboard_builder import ( - Layout, - timeseries_panel, - target, - template, + DATASOURCE, Expr, + Layout, Stat, - expr_sum_rate, - expr_sum_increase, expr_aggr_func, expr_avg, + expr_max, + expr_operator, + expr_sum_increase, + expr_sum_rate, heatmap_panel, + target, + template, + timeseries_panel, yaxis, - expr_operator, - expr_max, - DATASOURCE, ) -from grafanalib import formatunits as UNITS, _gen +from grafanalib import _gen +from grafanalib import formatunits as UNITS from grafanalib.core import ( - Dashboard, - Templating, - Template, + GRAPH_TOOLTIP_MODE_SHARED_CROSSHAIR, Annotations, - RowPanel, - Panel, + Dashboard, HeatmapColor, - Tooltip, - GRAPH_TOOLTIP_MODE_SHARED_CROSSHAIR, - Target, + Panel, + RowPanel, Table, + Target, + Template, + Templating, + Tooltip, ) SHORT_FORMAT = "short" @@ -1112,35 +1113,35 @@ def storage() -> RowPanel: title="load_cell path ratio", targets=[ target( - f'100 * ({nursery_rate("tycho_storage_load_cell_cache_hit_total")}) / clamp_min({nursery_rate("tycho_storage_load_cell_total")}, 1e-9)', + f"100 * ({nursery_rate('tycho_storage_load_cell_cache_hit_total')}) / clamp_min({nursery_rate('tycho_storage_load_cell_total')}, 1e-9)", legend_format="{{instance}} tree cache hit", ), target( - f'100 * ({nursery_rate("tycho_storage_load_cell_raw_cache_hit_total")}) / clamp_min({nursery_rate("tycho_storage_load_cell_total")}, 1e-9)', + f"100 * ({nursery_rate('tycho_storage_load_cell_raw_cache_hit_total')}) / clamp_min({nursery_rate('tycho_storage_load_cell_total')}, 1e-9)", legend_format="{{instance}} raw cache hit", ), target( - f'100 * ({nursery_rate("tycho_storage_load_cell_raw_cache_miss_total")}) / clamp_min({nursery_rate("tycho_storage_load_cell_total")}, 1e-9)', + f"100 * ({nursery_rate('tycho_storage_load_cell_raw_cache_miss_total')}) / clamp_min({nursery_rate('tycho_storage_load_cell_total')}, 1e-9)", legend_format="{{instance}} raw cache miss", ), target( - f'100 * ({nursery_rate("tycho_storage_load_cell_nursery_checked_total")}) / clamp_min({nursery_rate("tycho_storage_load_cell_total")}, 1e-9)', + f"100 * ({nursery_rate('tycho_storage_load_cell_nursery_checked_total')}) / clamp_min({nursery_rate('tycho_storage_load_cell_total')}, 1e-9)", legend_format="{{instance}} nursery checked", ), target( - f'100 * ({nursery_rate("tycho_storage_load_cell_nursery_hit_total")}) / clamp_min({nursery_rate("tycho_storage_load_cell_total")}, 1e-9)', + f"100 * ({nursery_rate('tycho_storage_load_cell_nursery_hit_total')}) / clamp_min({nursery_rate('tycho_storage_load_cell_total')}, 1e-9)", legend_format="{{instance}} nursery hit", ), target( - f'100 * clamp_min(({nursery_rate("tycho_storage_load_cell_nursery_checked_total")}) - ({nursery_rate("tycho_storage_load_cell_nursery_hit_total")}), 0) / clamp_min({nursery_rate("tycho_storage_load_cell_total")}, 1e-9)', + f"100 * clamp_min(({nursery_rate('tycho_storage_load_cell_nursery_checked_total')}) - ({nursery_rate('tycho_storage_load_cell_nursery_hit_total')}), 0) / clamp_min({nursery_rate('tycho_storage_load_cell_total')}, 1e-9)", legend_format="{{instance}} nursery miss", ), target( - f'100 * ({nursery_rate("tycho_storage_load_cell_raw_hit_total")}) / clamp_min({nursery_rate("tycho_storage_load_cell_total")}, 1e-9)', + f"100 * ({nursery_rate('tycho_storage_load_cell_raw_hit_total')}) / clamp_min({nursery_rate('tycho_storage_load_cell_total')}, 1e-9)", legend_format="{{instance}} raw fallback hit", ), target( - f'100 * ({nursery_rate("tycho_storage_load_cell_raw_miss_total")}) / clamp_min({nursery_rate("tycho_storage_load_cell_total")}, 1e-9)', + f"100 * ({nursery_rate('tycho_storage_load_cell_raw_miss_total')}) / clamp_min({nursery_rate('tycho_storage_load_cell_total')}, 1e-9)", legend_format="{{instance}} raw miss", ), ], @@ -3076,21 +3077,6 @@ 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)