From 424bcc3173d2169e2cc485dee17a658692beac65 Mon Sep 17 00:00:00 2001 From: Jakob Meier Date: Thu, 2 Apr 2026 21:47:44 +0200 Subject: [PATCH 1/3] test: MPC + stream component test Create a MockStream that connects to the MPC fixture setup. This lets us test the indexer stream <-> MPC glue. --- chain-signatures/contract-sol/src/lib.rs | 1 + chain-signatures/node/src/rpc.rs | 11 +- chain-signatures/node/src/stream/mod.rs | 1 + chain-signatures/node/src/stream/ops.rs | 1 + chain-signatures/node/src/util/mod.rs | 14 ++ integration-tests/src/mpc_fixture/builder.rs | 30 +++- .../src/mpc_fixture/fixture_interface.rs | 2 + .../src/mpc_fixture/fixture_tasks.rs | 37 ++++- .../src/mpc_fixture/mock_stream.rs | 140 ++++++++++++++++++ integration-tests/src/mpc_fixture/mod.rs | 1 + integration-tests/tests/cases/helpers.rs | 13 ++ integration-tests/tests/cases/mod.rs | 1 + integration-tests/tests/cases/mpc.rs | 16 +- .../tests/cases/mpc_with_stream.rs | 47 ++++++ 14 files changed, 286 insertions(+), 29 deletions(-) create mode 100644 integration-tests/src/mpc_fixture/mock_stream.rs create mode 100644 integration-tests/tests/cases/mpc_with_stream.rs diff --git a/chain-signatures/contract-sol/src/lib.rs b/chain-signatures/contract-sol/src/lib.rs index 26706e5bf..f54c1a0fd 100644 --- a/chain-signatures/contract-sol/src/lib.rs +++ b/chain-signatures/contract-sol/src/lib.rs @@ -196,6 +196,7 @@ pub struct SignatureRespondedEvent { } #[event] +#[derive(Clone)] pub struct RespondBidirectionalEvent { pub request_id: [u8; 32], pub responder: Pubkey, diff --git a/chain-signatures/node/src/rpc.rs b/chain-signatures/node/src/rpc.rs index 7ee4b881a..33c417d25 100644 --- a/chain-signatures/node/src/rpc.rs +++ b/chain-signatures/node/src/rpc.rs @@ -1600,8 +1600,6 @@ use signet_program::accounts::Respond as SolanaRespondAccount; use signet_program::accounts::RespondBidirectional as SolanaRespondBidirectionalAccount; use signet_program::instruction::Respond as SolanaRespond; use signet_program::instruction::RespondBidirectional as SolanaRespondBidirectional; -use signet_program::AffinePoint as SolanaContractAffinePoint; -use signet_program::Signature as SolanaContractSignature; use solana_sdk::signature::Signer as SolanaSigner; async fn try_publish_sol( sol: &SolanaClient, @@ -1614,14 +1612,7 @@ async fn try_publish_sol( let sign_id = action.indexed.id; let request_ids = vec![action.indexed.id.request_id]; let big_r = signature.big_r.to_encoded_point(false); - let signature = SolanaContractSignature { - big_r: SolanaContractAffinePoint { - x: big_r.as_bytes()[1..33].try_into().unwrap(), - y: big_r.as_bytes()[33..65].try_into().unwrap(), - }, - s: signature.s.to_bytes().into(), - recovery_id: signature.recovery_id, - }; + let signature = crate::util::mpc_to_sol_signature(signature, big_r); tracing::debug!( ?sign_id, diff --git a/chain-signatures/node/src/stream/mod.rs b/chain-signatures/node/src/stream/mod.rs index b549964c6..bd1875139 100644 --- a/chain-signatures/node/src/stream/mod.rs +++ b/chain-signatures/node/src/stream/mod.rs @@ -24,6 +24,7 @@ pub fn channel() -> (mpsc::Sender, mpsc::Receiver) { /// Unified event produced by a chain stream #[allow(clippy::large_enum_variant)] +#[derive(Clone)] pub enum ChainEvent { SignRequest(IndexedSignRequest), Respond(SignatureRespondedEvent), diff --git a/chain-signatures/node/src/stream/ops.rs b/chain-signatures/node/src/stream/ops.rs index f00b580c9..20d99745a 100644 --- a/chain-signatures/node/src/stream/ops.rs +++ b/chain-signatures/node/src/stream/ops.rs @@ -136,6 +136,7 @@ impl SignBidirectionalEvent { } } +#[derive(Clone)] pub enum RespondBidirectionalEvent { Solana(signet_program::RespondBidirectionalEvent), Hydration(HydrationRespondBidirectionalEvent), diff --git a/chain-signatures/node/src/util/mod.rs b/chain-signatures/node/src/util/mod.rs index bce4a5e9d..9b839ec07 100644 --- a/chain-signatures/node/src/util/mod.rs +++ b/chain-signatures/node/src/util/mod.rs @@ -71,6 +71,20 @@ impl AffinePointExt for AffinePoint { } } +pub fn mpc_to_sol_signature( + signature: &mpc_primitives::Signature, + big_r: k256::EncodedPoint, +) -> signet_program::Signature { + signet_program::Signature { + big_r: signet_program::AffinePoint { + x: big_r.as_bytes()[1..33].try_into().unwrap(), + y: big_r.as_bytes()[33..65].try_into().unwrap(), + }, + s: signature.s.to_bytes().into(), + recovery_id: signature.recovery_id, + } +} + pub fn is_elapsed_longer_than_timeout(timestamp_sec: u64, timeout: u64) -> bool { if let LocalResult::Single(msg_timestamp) = Utc.timestamp_opt(timestamp_sec as i64, 0) { let timeout = Duration::from_millis(timeout); diff --git a/integration-tests/src/mpc_fixture/builder.rs b/integration-tests/src/mpc_fixture/builder.rs index b14db0622..c4702359f 100644 --- a/integration-tests/src/mpc_fixture/builder.rs +++ b/integration-tests/src/mpc_fixture/builder.rs @@ -7,6 +7,7 @@ use crate::mpc_fixture::fixture_tasks::MessageFilter; use crate::mpc_fixture::input::FixtureInput; use crate::mpc_fixture::message_collector::CollectMessages; use crate::mpc_fixture::mock_governance::MockGovernance; +use crate::mpc_fixture::mock_stream::MockStream; use crate::mpc_fixture::{fixture_tasks, MpcFixture, MpcFixtureNode}; use cait_sith::protocol::Participant; use mpc_contract::config::{ @@ -54,6 +55,7 @@ struct MpcFixtureNodeBuilder { config: Config, messaging: NodeMessagingBuilder, key_info: Option, + mock_streams: Vec, } /// Config options for the test setup. @@ -388,6 +390,17 @@ impl MpcFixtureBuilder { .with_node_min_triples(0) .with_node_min_presignatures(0) } + + /// Add a mock stream to all nodes. + /// + /// Each node will have a independent deep-clone of the provided stream. + /// Events are thus delivered to all nodes. + pub async fn with_mock_stream(mut self, stream: MockStream) -> Self { + for node in &mut self.prepared_nodes { + node.mock_streams.push(stream.deep_clone().await) + } + self + } } impl MpcFixtureNodeBuilder { @@ -435,6 +448,7 @@ impl MpcFixtureNodeBuilder { config, messaging, key_info: None, + mock_streams: vec![], } } @@ -496,10 +510,20 @@ impl MpcFixtureNodeBuilder { me: account_id.clone(), protocol_state_tx, }, - context.contract_state, + context.contract_state.clone(), mesh_rx.clone(), )); + let backlog = Backlog::new(); + + fixture_tasks::start_mock_stream_tasks( + &self.mock_streams, + sign_tx.clone(), + backlog.clone(), + context.contract_state, + &mesh_rx, + ); + // handle outbox messages manually, we want them before they are // encrypted and we want to send them directly to other node's inboxes let _mock_network_handle = fixture_tasks::test_mock_network( @@ -510,6 +534,7 @@ impl MpcFixtureNodeBuilder { mesh_tx.clone(), config_tx.clone(), self.messaging.filter, + self.mock_streams.clone(), ); let mut node = MpcFixtureNode { @@ -519,9 +544,10 @@ impl MpcFixtureNodeBuilder { config: config_tx, sign_tx, msg_channel: self.messaging.channel, + mock_streams: self.mock_streams, triple_storage, presignature_storage, - backlog: Backlog::new(), + backlog, web_handle: None, }; diff --git a/integration-tests/src/mpc_fixture/fixture_interface.rs b/integration-tests/src/mpc_fixture/fixture_interface.rs index 02ea6ed0d..b263ebee8 100644 --- a/integration-tests/src/mpc_fixture/fixture_interface.rs +++ b/integration-tests/src/mpc_fixture/fixture_interface.rs @@ -3,6 +3,7 @@ use crate::containers::Redis; use crate::mpc_fixture::message_collector::{CollectMessages, MessagePrinter}; +use crate::mpc_fixture::mock_stream::MockStream; use cait_sith::protocol::Participant; use mpc_node::backlog::Backlog; use mpc_node::config::Config; @@ -33,6 +34,7 @@ pub struct MpcFixtureNode { pub sign_tx: mpsc::Sender, pub msg_channel: MessageChannel, + pub mock_streams: Vec, pub triple_storage: TripleStorage, pub presignature_storage: PresignatureStorage, diff --git a/integration-tests/src/mpc_fixture/fixture_tasks.rs b/integration-tests/src/mpc_fixture/fixture_tasks.rs index c740897e4..9576e2587 100644 --- a/integration-tests/src/mpc_fixture/fixture_tasks.rs +++ b/integration-tests/src/mpc_fixture/fixture_tasks.rs @@ -2,20 +2,26 @@ //! passing between nodes and updates to the governance smart contract. use crate::mpc_fixture::fixture_interface::SharedOutput; +use crate::mpc_fixture::mock_stream::MockStream; use cait_sith::protocol::Participant; use mpc_keys::hpke::Ciphered; +use mpc_node::backlog::Backlog; use mpc_node::config::Config; use mpc_node::mesh::MeshState; +use mpc_node::node_client::NodeClient; use mpc_node::protocol::message::{MessageOutbox, SendMessage, SignedMessage}; -use mpc_node::rpc::RpcAction; +use mpc_node::protocol::Sign; +use mpc_node::rpc::{ContractStateWatcher, RpcAction}; +use mpc_node::stream::run_stream; use std::collections::HashMap; use std::sync::Arc; -use tokio::sync::mpsc::{Receiver, Sender}; +use tokio::sync::mpsc::{self, Receiver, Sender}; use tokio::sync::watch; use tokio::task::JoinHandle; pub type MessageFilter = Box bool + Send>; +#[allow(clippy::too_many_arguments)] pub(super) fn test_mock_network( routing_table: HashMap>, shared_output: &SharedOutput, @@ -24,6 +30,7 @@ pub(super) fn test_mock_network( mesh: watch::Sender, config: watch::Sender, mut filter: MessageFilter, + mock_streams: Vec, ) -> JoinHandle<()> { let msg_log = Arc::clone(&shared_output.msg_log); let rpc_actions = Arc::clone(&shared_output.rpc_actions); @@ -67,7 +74,7 @@ pub(super) fn test_mock_network( } Some(rpc) = rpc_rx.recv() => { - let action_str = match rpc { + let action_str = match &rpc { RpcAction::Publish(publish_action) => { format!( "RpcAction::Publish({:?})", @@ -78,6 +85,10 @@ pub(super) fn test_mock_network( tracing::info!(target: "mock_network", ?action_str, "Received RPC action"); let mut actions_log = rpc_actions.lock().await; actions_log.insert(action_str); + let block = [rpc]; + for stream in &mock_streams { + stream.rpc_actions(&block).await; + } } else => { @@ -89,3 +100,23 @@ pub(super) fn test_mock_network( tracing::info!(target: "mock_network", "Test mock network task exited"); }) } + +pub(super) fn start_mock_stream_tasks( + mock_streams: &[MockStream], + sign_tx: mpsc::Sender, + backlog: Backlog, + contract_watcher: ContractStateWatcher, + mesh_state: &watch::Receiver, +) { + for stream in mock_streams { + tokio::spawn(run_stream( + stream.clone(), + sign_tx.clone(), + backlog.clone(), + contract_watcher.clone(), + mesh_state.clone(), + // Only used for backlog recovery - not implemented in component tests yet + NodeClient::new(&Default::default()), + )); + } +} diff --git a/integration-tests/src/mpc_fixture/mock_stream.rs b/integration-tests/src/mpc_fixture/mock_stream.rs new file mode 100644 index 000000000..19b4912ff --- /dev/null +++ b/integration-tests/src/mpc_fixture/mock_stream.rs @@ -0,0 +1,140 @@ +use elliptic_curve::sec1::ToEncodedPoint; +use mpc_node::protocol::{IndexedSignRequest, SignKind}; +use mpc_node::rpc::RpcAction; +use mpc_node::stream::{ChainEvent, ChainStream}; +use mpc_primitives::Chain; +use solana_sdk::pubkey::Pubkey; +use std::sync::Arc; +use std::time::Duration; +use tokio::sync::Mutex; + +#[derive(Default, Clone)] +pub struct MockStream { + inner: Arc>, +} + +#[derive(Default)] +pub struct InnerMockStream { + block_height: u64, + /// Events for blocks >= `block_height`, not ready to be published, yet. + future_blocks: Vec>, + /// Events already produced < `block_height` but not yet consumed by + /// `next_event()`. + pending_events: Vec, +} + +impl ChainStream for MockStream { + const CHAIN: Chain = Chain::Solana; + + async fn start(&mut self) { + let mut guard = self.inner.lock().await; + guard.pending_events.push(ChainEvent::CatchupCompleted); + } + + async fn next_event(&mut self) -> Option { + loop { + let mut guard = self.inner.lock().await; + let out = guard.pending_events.pop(); + if out.is_some() { + return out; + } + // TODO: would be better to avoid sleep by awaiting new data + tokio::time::sleep(Duration::from_millis(10)).await; + } + } +} + +impl MockStream { + pub async fn deep_clone(&self) -> Self { + let guard = self.inner.lock().await; + let cloned = InnerMockStream { + block_height: guard.block_height, + future_blocks: guard.future_blocks.clone(), + pending_events: guard.pending_events.clone(), + }; + Self { + inner: Arc::new(Mutex::new(cloned)), + } + } + + pub async fn progress_block_height(&self, steps: usize) { + let mut guard = self.inner.lock().await; + guard.progress_block_height(steps) + } + + pub async fn sign_requests(&self, requests: &[IndexedSignRequest]) { + let mut guard = self.inner.lock().await; + guard.sign_requests(requests) + } + + pub async fn rpc_actions(&self, actions: &[RpcAction]) { + let mut guard = self.inner.lock().await; + guard.rpc_actions(actions) + } +} + +impl InnerMockStream { + /// Move events from future blocks tp pending blocks. + pub fn progress_block_height(&mut self, steps: usize) { + let checked_steps = steps.min(self.future_blocks.len()); + for mut block in self.future_blocks.drain(0..checked_steps) { + self.pending_events.append(&mut block); + self.pending_events + .push(ChainEvent::Block(self.block_height)); + self.block_height += 1; + } + } + + /// Add a future block that contains signature requesting events. + pub fn sign_requests(&mut self, requests: &[IndexedSignRequest]) { + let mut block = Vec::new(); + + for request in requests { + // Skip events for other chains + if request.chain != MockStream::CHAIN { + continue; + } + + block.push(ChainEvent::SignRequest(request.clone())) + } + + self.future_blocks.push(block); + } + + /// Add a future block that contains events corresponding to the provided rpc actions. + pub fn rpc_actions(&mut self, actions: &[RpcAction]) { + let mut block = Vec::new(); + + for action in actions { + let RpcAction::Publish(publish_action) = action; + + // Skip events for other chains + if publish_action.indexed.chain != MockStream::CHAIN { + continue; + } + + // for now, the mock stream only converts signature RPC actions to chain events + if !matches!(publish_action.indexed.kind, SignKind::Sign,) { + tracing::warn!( + kind=?publish_action.indexed.kind, + "kind not yet supported in test framework", + ); + continue; + } + + // type conversions that would usually happen in RPC publishing -> Solana contract -> CPI event library + let big_r = publish_action.signature.big_r.to_encoded_point(false); + let sol_event = signet_program::SignatureRespondedEvent { + request_id: publish_action.indexed.id.request_id, + responder: Pubkey::new_unique(), + signature: mpc_node::util::mpc_to_sol_signature(&publish_action.signature, big_r), + }; + + let respond_event = mpc_node::stream::ops::SignatureRespondedEvent::Solana(sol_event); + + block.push(ChainEvent::Respond(respond_event)); + } + + self.future_blocks.push(block); + } +} diff --git a/integration-tests/src/mpc_fixture/mod.rs b/integration-tests/src/mpc_fixture/mod.rs index c867bfb5a..ec8f91e27 100644 --- a/integration-tests/src/mpc_fixture/mod.rs +++ b/integration-tests/src/mpc_fixture/mod.rs @@ -8,6 +8,7 @@ pub mod fixture_tasks; pub mod input; pub mod message_collector; pub mod mock_governance; +pub mod mock_stream; pub use builder::MpcFixtureBuilder; pub use fixture_interface::{MpcFixture, MpcFixtureNode}; diff --git a/integration-tests/tests/cases/helpers.rs b/integration-tests/tests/cases/helpers.rs index aa6bb6ab2..6414d8af7 100644 --- a/integration-tests/tests/cases/helpers.rs +++ b/integration-tests/tests/cases/helpers.rs @@ -7,6 +7,7 @@ use mpc_node::protocol::presignature::Presignature; use mpc_node::protocol::triple::Triple; use mpc_node::storage::triple_storage::TriplePair; use mpc_node::storage::{PresignatureStorage, TripleStorage}; +use mpc_primitives::{SignArgs, LATEST_MPC_KEY_VERSION}; pub(crate) fn dummy_presignature(id: u64) -> Presignature { dummy_presignature_with_holders(id, vec![Participant::from(1), Participant::from(2)]) @@ -133,3 +134,15 @@ pub(crate) async fn assert_presig_owned_state( ); } } + +pub fn test_sign_arg(seed: u8) -> SignArgs { + let mut entropy = [1; 32]; + entropy[0] = seed; + SignArgs { + entropy, + epsilon: k256::Scalar::default(), + payload: k256::Scalar::default(), + path: "test".to_owned(), + key_version: LATEST_MPC_KEY_VERSION, + } +} diff --git a/integration-tests/tests/cases/mod.rs b/integration-tests/tests/cases/mod.rs index 441b5b77f..153b0d71b 100644 --- a/integration-tests/tests/cases/mod.rs +++ b/integration-tests/tests/cases/mod.rs @@ -21,6 +21,7 @@ pub mod ethereum; pub mod ethereum_stream; pub mod helpers; pub mod mpc; +pub mod mpc_with_stream; pub mod nightly; pub mod solana; pub mod solana_stream; diff --git a/integration-tests/tests/cases/mpc.rs b/integration-tests/tests/cases/mpc.rs index 3d9d8c343..7a8c67ba9 100644 --- a/integration-tests/tests/cases/mpc.rs +++ b/integration-tests/tests/cases/mpc.rs @@ -5,7 +5,7 @@ use integration_tests::mpc_fixture::MpcFixtureBuilder; use mpc_node::protocol::presignature::Presignature; use mpc_node::protocol::{Chain, IndexedSignRequest, ProtocolState, Sign}; use mpc_node::storage::triple_storage::TriplePair; -use mpc_primitives::{SignArgs, SignId, LATEST_MPC_KEY_VERSION}; +use mpc_primitives::SignId; use test_log::test; use tokio::sync::oneshot; use tokio::sync::Mutex; @@ -217,24 +217,12 @@ async fn test_basic_sign() { fn sign_request(seed: u8) -> Sign { Sign::Request(IndexedSignRequest::sign( SignId::new([seed; 32]), - sign_arg(seed), + super::helpers::test_sign_arg(seed), Chain::NEAR, 0, )) } -fn sign_arg(seed: u8) -> SignArgs { - let mut entropy = [1; 32]; - entropy[0] = seed; - SignArgs { - entropy, - epsilon: k256::Scalar::default(), - payload: k256::Scalar::default(), - path: "test".to_owned(), - key_version: LATEST_MPC_KEY_VERSION, - } -} - /// drop the first 20 presignature messages on each node and see if the system /// can recover #[test(tokio::test(flavor = "multi_thread"))] diff --git a/integration-tests/tests/cases/mpc_with_stream.rs b/integration-tests/tests/cases/mpc_with_stream.rs new file mode 100644 index 000000000..0e207249b --- /dev/null +++ b/integration-tests/tests/cases/mpc_with_stream.rs @@ -0,0 +1,47 @@ +//! Component test that combine the MPC network combined with a chain stream as input and output. + +use integration_tests::mpc_fixture::{mock_stream::MockStream, MpcFixtureBuilder}; +use mpc_node::protocol::IndexedSignRequest; +use mpc_primitives::{Chain, SignId}; +use std::time::Duration; +use test_log::test; + +fn sign_request(seed: u8) -> IndexedSignRequest { + IndexedSignRequest::sign( + SignId::new([seed; 32]), + super::helpers::test_sign_arg(seed), + Chain::Solana, + 0, + ) +} + +#[test(tokio::test(flavor = "multi_thread"))] +async fn test_sign() { + let network = MpcFixtureBuilder::default() + .only_generate_signatures() + .with_mock_stream(MockStream::default()) + .await + .build() + .await; + + tracing::info!("sending requests now"); + let request = [sign_request(0)]; + network[0].mock_streams[0].sign_requests(&request).await; + network[1].mock_streams[0].sign_requests(&request).await; + network[2].mock_streams[0].sign_requests(&request).await; + + network[0].mock_streams[0].progress_block_height(1).await; + network[1].mock_streams[0].progress_block_height(1).await; + network[2].mock_streams[0].progress_block_height(1).await; + + let timeout = Duration::from_secs(10); + + let actions = network.assert_actions(1, timeout).await; + + assert_eq!(actions.len(), 1); + let action_str = actions.iter().next().unwrap(); + assert!( + action_str.contains("RpcAction::Publish"), + "unexpected rpc action {action_str}" + ); +} From f3980aa66f14b9c47a7757a122e1e3aa66f5c1ea Mon Sep 17 00:00:00 2001 From: Jakob Meier Date: Thu, 2 Apr 2026 22:39:19 +0200 Subject: [PATCH 2/3] WIP: channel contention test --- chain-signatures/node/src/cli.rs | 2 +- .../node/src/protocol/message/sub.rs | 2 +- .../src/mpc_fixture/fixture_interface.rs | 2 +- .../tests/cases/mpc_with_stream.rs | 52 +++++++++++++++++-- 4 files changed, 52 insertions(+), 6 deletions(-) diff --git a/chain-signatures/node/src/cli.rs b/chain-signatures/node/src/cli.rs index 5db1f40c9..eee89050d 100644 --- a/chain-signatures/node/src/cli.rs +++ b/chain-signatures/node/src/cli.rs @@ -214,7 +214,7 @@ pub async fn run(cmd: Cli) -> anyhow::Result<()> { ); crate::metrics::nodes::CONFIGURATION_DIGEST.set(digest); - let (sign_tx, sign_rx) = mpsc::channel(16384); + let (sign_tx, sign_rx) = mpsc::channel(if cfg!(test) { 1 } else { 16384 }); let gcp_service = GcpService::init(&account_id, &storage_options).await?; let key_storage = diff --git a/chain-signatures/node/src/protocol/message/sub.rs b/chain-signatures/node/src/protocol/message/sub.rs index e947dea18..73bbdf0ef 100644 --- a/chain-signatures/node/src/protocol/message/sub.rs +++ b/chain-signatures/node/src/protocol/message/sub.rs @@ -12,7 +12,7 @@ use crate::protocol::presignature::{FullPresignatureId, PresignatureId}; use crate::protocol::triple::TripleId; /// This should be enough to hold a few messages in the inbox. -pub const MAX_MESSAGE_SUB_CHANNEL_SIZE: usize = 4 * 1024; +pub const MAX_MESSAGE_SUB_CHANNEL_SIZE: usize = if cfg!(test) { 1 } else { 4 * 1024 }; pub enum SubscribeId { Generating, diff --git a/integration-tests/src/mpc_fixture/fixture_interface.rs b/integration-tests/src/mpc_fixture/fixture_interface.rs index b263ebee8..290ee21a3 100644 --- a/integration-tests/src/mpc_fixture/fixture_interface.rs +++ b/integration-tests/src/mpc_fixture/fixture_interface.rs @@ -126,7 +126,7 @@ impl MpcFixture { let actions: tokio::sync::MutexGuard<'_, HashSet> = self.output.rpc_actions.lock().await; - tracing::info!("All published RPC actions:"); + tracing::info!(count = actions.len(), "All published RPC actions:"); for action in actions.iter() { tracing::info!("{action}"); } diff --git a/integration-tests/tests/cases/mpc_with_stream.rs b/integration-tests/tests/cases/mpc_with_stream.rs index 0e207249b..2b2d62c3c 100644 --- a/integration-tests/tests/cases/mpc_with_stream.rs +++ b/integration-tests/tests/cases/mpc_with_stream.rs @@ -6,10 +6,11 @@ use mpc_primitives::{Chain, SignId}; use std::time::Duration; use test_log::test; -fn sign_request(seed: u8) -> IndexedSignRequest { +fn sign_request(seed: u16) -> IndexedSignRequest { + let bytes = [seed.to_be_bytes()[0], seed.to_be_bytes()[1]].repeat(16); IndexedSignRequest::sign( - SignId::new([seed; 32]), - super::helpers::test_sign_arg(seed), + SignId::new(bytes.try_into().unwrap()), + super::helpers::test_sign_arg(seed.to_be_bytes()[0]), Chain::Solana, 0, ) @@ -45,3 +46,48 @@ async fn test_sign() { "unexpected rpc action {action_str}" ); } + +// WIP/TODO: This should fill up some channels and assert the effect of it. +// Right now, it just runs through all presignatures and then gets stuck as there are no Ps left. +#[test(tokio::test(flavor = "multi_thread"))] +async fn test_channel_contention() { + let network = MpcFixtureBuilder::default() + .only_generate_signatures() + .with_mock_stream(MockStream::default()) + .await + .build() + .await; + + // send requests in batches of 50 per block + for outer in 0..1000 { + let requests = (0..50) + .map(|inner| sign_request(outer * 50 + inner)) + .collect::>(); + + tracing::info!(outer, "sending request now"); + network[0].mock_streams[0].sign_requests(&requests).await; + network[1].mock_streams[0].sign_requests(&requests).await; + network[2].mock_streams[0].sign_requests(&requests).await; + } + network[0].mock_streams[0] + .progress_block_height(50_000) + .await; + network[1].mock_streams[0] + .progress_block_height(50_000) + .await; + network[2].mock_streams[0] + .progress_block_height(50_000) + .await; + + // there are only enough presignatures for 75 signatures in the fixture + let actions = network.assert_actions(75, Duration::from_secs(10)).await; + + assert_eq!(actions.len(), 75); + let action_str = actions.iter().next().unwrap(); + assert!( + action_str.contains("RpcAction::Publish"), + "unexpected rpc action {action_str}" + ); + + // TODO: check the system is still operative +} From b24050f9d0426240b89a588caf0a98ad1e15c1f7 Mon Sep 17 00:00:00 2001 From: Jakob Meier Date: Thu, 9 Apr 2026 19:15:03 +0200 Subject: [PATCH 3/3] wip: contention problem found but not with channel capacity? --- chain-signatures/node/src/cli.rs | 2 +- .../node/src/protocol/message/sub.rs | 16 +++- integration-tests/tests/cases/helpers.rs | 5 +- .../tests/cases/mpc_with_stream.rs | 88 +++++++++++++------ 4 files changed, 77 insertions(+), 34 deletions(-) diff --git a/chain-signatures/node/src/cli.rs b/chain-signatures/node/src/cli.rs index eee89050d..5db1f40c9 100644 --- a/chain-signatures/node/src/cli.rs +++ b/chain-signatures/node/src/cli.rs @@ -214,7 +214,7 @@ pub async fn run(cmd: Cli) -> anyhow::Result<()> { ); crate::metrics::nodes::CONFIGURATION_DIGEST.set(digest); - let (sign_tx, sign_rx) = mpsc::channel(if cfg!(test) { 1 } else { 16384 }); + let (sign_tx, sign_rx) = mpsc::channel(16384); let gcp_service = GcpService::init(&account_id, &storage_options).await?; let key_storage = diff --git a/chain-signatures/node/src/protocol/message/sub.rs b/chain-signatures/node/src/protocol/message/sub.rs index 73bbdf0ef..64c647060 100644 --- a/chain-signatures/node/src/protocol/message/sub.rs +++ b/chain-signatures/node/src/protocol/message/sub.rs @@ -12,7 +12,7 @@ use crate::protocol::presignature::{FullPresignatureId, PresignatureId}; use crate::protocol::triple::TripleId; /// This should be enough to hold a few messages in the inbox. -pub const MAX_MESSAGE_SUB_CHANNEL_SIZE: usize = if cfg!(test) { 1 } else { 4 * 1024 }; +pub const MAX_MESSAGE_SUB_CHANNEL_SIZE: usize = 4 * 1024; pub enum SubscribeId { Generating, @@ -110,8 +110,18 @@ impl Subscriber { pub async fn send(&self, msg: T) -> Result<(), mpsc::error::SendError> { match self { - Self::Subscribed(tx) => tx.send(msg).await, - Self::Unsubscribed(tx, _) => tx.send(msg).await, + Self::Subscribed(tx) => { + let cap = tx.capacity(); + let max_cap = tx.max_capacity(); + tracing::warn!("Sending to subscribed, capacity {cap}/{max_cap}"); + tx.send(msg).await + } + Self::Unsubscribed(tx, _) => { + let cap = tx.capacity(); + let max_cap = tx.max_capacity(); + tracing::warn!("Sending to unsubscribed, capacity {cap}/{max_cap}"); + tx.send(msg).await + } Self::Unknown => Ok(()), } } diff --git a/integration-tests/tests/cases/helpers.rs b/integration-tests/tests/cases/helpers.rs index 6414d8af7..fffb01a69 100644 --- a/integration-tests/tests/cases/helpers.rs +++ b/integration-tests/tests/cases/helpers.rs @@ -135,9 +135,10 @@ pub(crate) async fn assert_presig_owned_state( } } -pub fn test_sign_arg(seed: u8) -> SignArgs { +pub fn test_sign_arg(seed: impl Into) -> SignArgs { + let seed = seed.into(); let mut entropy = [1; 32]; - entropy[0] = seed; + entropy[0..4].copy_from_slice(&seed.to_be_bytes()); SignArgs { entropy, epsilon: k256::Scalar::default(), diff --git a/integration-tests/tests/cases/mpc_with_stream.rs b/integration-tests/tests/cases/mpc_with_stream.rs index 2b2d62c3c..968f2740b 100644 --- a/integration-tests/tests/cases/mpc_with_stream.rs +++ b/integration-tests/tests/cases/mpc_with_stream.rs @@ -6,11 +6,17 @@ use mpc_primitives::{Chain, SignId}; use std::time::Duration; use test_log::test; -fn sign_request(seed: u16) -> IndexedSignRequest { - let bytes = [seed.to_be_bytes()[0], seed.to_be_bytes()[1]].repeat(16); +fn sign_request(seed: u32) -> IndexedSignRequest { + let bytes = [ + seed.to_be_bytes()[0], + seed.to_be_bytes()[1], + seed.to_be_bytes()[2], + seed.to_be_bytes()[3], + ] + .repeat(8); IndexedSignRequest::sign( SignId::new(bytes.try_into().unwrap()), - super::helpers::test_sign_arg(seed.to_be_bytes()[0]), + super::helpers::test_sign_arg(seed), Chain::Solana, 0, ) @@ -27,6 +33,7 @@ async fn test_sign() { tracing::info!("sending requests now"); let request = [sign_request(0)]; + // TODO: abstraction to send to all network[0].mock_streams[0].sign_requests(&request).await; network[1].mock_streams[0].sign_requests(&request).await; network[2].mock_streams[0].sign_requests(&request).await; @@ -47,10 +54,11 @@ async fn test_sign() { ); } -// WIP/TODO: This should fill up some channels and assert the effect of it. -// Right now, it just runs through all presignatures and then gets stuck as there are no Ps left. -#[test(tokio::test(flavor = "multi_thread"))] -async fn test_channel_contention() { +async fn check_channel_contention( + num_blocks: usize, + req_per_block: usize, + expected_signatures: usize, +) { let network = MpcFixtureBuilder::default() .only_generate_signatures() .with_mock_stream(MockStream::default()) @@ -58,36 +66,60 @@ async fn test_channel_contention() { .build() .await; - // send requests in batches of 50 per block - for outer in 0..1000 { - let requests = (0..50) - .map(|inner| sign_request(outer * 50 + inner)) + let num_nodes = 3; + for outer in 0..(num_blocks as u16) { + let requests = (0..req_per_block) + .map(|inner| sign_request(outer as u32 * req_per_block as u32 + inner as u32)) .collect::>(); - tracing::info!(outer, "sending request now"); - network[0].mock_streams[0].sign_requests(&requests).await; - network[1].mock_streams[0].sign_requests(&requests).await; - network[2].mock_streams[0].sign_requests(&requests).await; + for i in 0..num_nodes { + network[i].mock_streams[0].sign_requests(&requests).await; + } + } + + for i in 0..num_nodes { + network[i].mock_streams[0] + .progress_block_height(num_blocks) + .await; } - network[0].mock_streams[0] - .progress_block_height(50_000) - .await; - network[1].mock_streams[0] - .progress_block_height(50_000) - .await; - network[2].mock_streams[0] - .progress_block_height(50_000) - .await; - // there are only enough presignatures for 75 signatures in the fixture - let actions = network.assert_actions(75, Duration::from_secs(10)).await; + let actions = network + .assert_actions(expected_signatures, Duration::from_secs(60)) + .await; - assert_eq!(actions.len(), 75); + assert_eq!(actions.len(), expected_signatures); let action_str = actions.iter().next().unwrap(); assert!( action_str.contains("RpcAction::Publish"), "unexpected rpc action {action_str}" ); +} + +// WIP: up to 50 seems to work fine + +#[test(tokio::test(flavor = "multi_thread"))] +async fn test_channel_contention_ok_a() { + check_channel_contention(1, 50, 50).await; +} +#[test(tokio::test(flavor = "multi_thread"))] +async fn test_channel_contention_ok_b() { + check_channel_contention(5, 10, 50).await; +} + +// WIP: proof that there are enough Ps for more signatures for more than 50 +#[test(tokio::test(flavor = "multi_thread"))] +async fn test_channel_contention_show_limit() { + check_channel_contention(6, 50, 75).await; +} - // TODO: check the system is still operative +// TODO: find out why it stops at 50 signatures +#[should_panic(expected = "should produce enough signatures")] +#[test(tokio::test(flavor = "multi_thread"))] +async fn test_channel_contention_nok_a() { + check_channel_contention(1, 51, 51).await; +} +#[should_panic(expected = "should produce enough signatures")] +#[test(tokio::test(flavor = "multi_thread"))] +async fn test_channel_contention_nok_b() { + check_channel_contention(6, 10, 60).await; }