use crate::common; use e2e_tests::{CLUSTER_WAIT_TIMEOUT, metrics}; use near_mpc_contract_interface::types::{Curve, DomainConfig, DomainPurpose}; use rand::{RngCore, SeedableRng}; fn must_get_payload_for_domain(domain: &DomainConfig, rng: &mut impl RngCore) -> serde_json::Value { match Curve::from(domain.protocol) { Curve::Secp256k1 => common::generate_ecdsa_payload(rng), Curve::Edwards25519 => common::generate_eddsa_payload(rng), c => panic!("unsupported curve in test: {c:?}"), } } /// When a sign request can't be answered (because too many participants are /// down), the contract calls `fail_on_timeout` and each alive node's indexer /// bumps `mpc_num_timeouts_indexed`. This test stands up a 2-of-2 cluster, /// kills one node, submits a sign request, and verifies the surviving node /// observes the timeout. #[tokio::test] #[expect(non_snake_case)] async fn timeout_metric__should_increment_when_signature_times_out() { // given — 2-of-2 cluster (one node down ⇒ signing impossible) let mut rng = rand::rngs::StdRng::seed_from_u64(0); let (mut cluster, running) = common::must_setup_cluster(common::TIMEOUT_METRIC_PORT_SEED, |c| { c.num_nodes = 2; c.threshold = 2; }) .await; let domain = running .domains .domains .iter() .find(|d| matches!(d.purpose, DomainPurpose::Sign)) .expect("cluster must have a signable domain"); // when — kill node 0, then submit a request no one can answer cluster.kill_nodes(&[0]).expect("failed to kill node 0"); let payload = must_get_payload_for_domain(domain, &mut rng); // then — node 1's indexer must observe fail_on_timeout. We can't `.await` // the second sign request: the contract's yield + auto-timeout completes // long after the JSON-RPC server's per-call timeout, so the user-facing // future is doomed to return Err from near_kit retries before the on-chain // outcome materializes. Race the call against the metric — once node 1's // indexer bumps the counter the test passes; the sign-request future is // cancelled (the tx is already on chain by then). tokio::select! { res = common::wait_metric_on_nodes( &cluster, &[1], metrics::TIMEOUTS_INDEXED, |v| v >= 1, // note: we would prefer to have strict equality, but we use inequality, in // case the send below has retry mechanism. C.f. // https://github.com/near/mpc/pull/3211#discussion_r3233189801 CLUSTER_WAIT_TIMEOUT, ) => res.unwrap_or_else(|_| panic!("{} did not reach 1 on node 1", metrics::TIMEOUTS_INDEXED)), _ = cluster.send_sign_request(domain.id, payload, cluster.default_user_account()) => panic!( "sign request future returned before timeout metric — test wiring is wrong \ (request unexpectedly succeeded or near_kit retries exhausted before \ the on-chain timeout)" ), } }