// Copyright (C) 2023 Entropy Cryptography Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
//! Integration tests which log the time taken to run the protocols with the number of parties set
//! to the number of cpus available. Note that these should be run in release mode to get a realistic
//! idea of how long things take in production.
use entropy_protocol::{KeyParams, PartyId, SessionId, SigningSessionInfo, ValidatorInfo};
use futures::future;
use rand_core::OsRng;
use serial_test::serial;
use sp_core::{sr25519, Pair};
use std::time::Instant;
use subxt::utils::AccountId32;
use synedrion::{ecdsa::VerifyingKey, AuxInfo, KeyShare, ThresholdKeyShare};
use tokio::{net::TcpListener, runtime::Runtime, sync::oneshot};
use x25519_dalek::StaticSecret;
mod helpers;
use helpers::{server, ProtocolOutput};
use std::collections::BTreeSet;
#[test]
#[serial]
fn sign_protocol_with_time_logged() {
let cpus = num_cpus::get();
get_tokio_runtime(cpus).block_on(async {
test_sign_with_parties(cpus).await;
})
}
#[test]
#[serial]
fn refresh_protocol_with_time_logged() {
let cpus = num_cpus::get();
get_tokio_runtime(cpus).block_on(async {
test_refresh_with_parties(cpus).await;
})
}
#[test]
#[serial]
fn dkg_protocol_with_time_logged() {
let cpus = num_cpus::get();
get_tokio_runtime(cpus).block_on(async {
test_dkg_with_parties(cpus).await;
})
}
#[test]
#[serial]
fn t_of_n_dkg_and_sign() {
let cpus = num_cpus::get();
// For this test we need at least 3 parties
let parties = 3;
get_tokio_runtime(cpus).block_on(async {
test_dkg_and_sign_with_parties(parties).await;
})
}
async fn test_sign_with_parties(num_parties: usize) {
let (pairs, ids) = get_keypairs_and_ids(num_parties);
let keyshares = KeyShare::::new_centralized(&mut OsRng, &ids, None);
let aux_infos = AuxInfo::::new_centralized(&mut OsRng, &ids);
let verifying_key = keyshares[&PartyId::from(pairs[0].public())].verifying_key();
let parties: Vec<_> = pairs
.iter()
.map(|pair| ValidatorSecretInfo {
pair: pair.clone(),
keyshare: Some(keyshares[&PartyId::from(pair.public())].clone()),
threshold_keyshare: None,
aux_info: Some(aux_infos[&PartyId::from(pair.public())].clone()),
})
.collect();
let message_hash = [0u8; 32];
let session_id = SessionId::Sign(SigningSessionInfo {
signature_verifying_key: verifying_key.to_encoded_point(true).as_bytes().to_vec(),
message_hash,
request_author: AccountId32([0u8; 32]),
});
let threshold = parties.len();
let mut outputs = test_protocol_with_parties(parties, session_id, threshold).await;
if let ProtocolOutput::Sign(recoverable_signature) = outputs.pop().unwrap() {
// Check signature
let recovery_key_from_sig = VerifyingKey::recover_from_prehash(
&message_hash,
&recoverable_signature.signature,
recoverable_signature.recovery_id,
)
.unwrap();
assert_eq!(verifying_key, recovery_key_from_sig);
} else {
panic!("Unexpected protocol output");
}
}
async fn test_refresh_with_parties(num_parties: usize) {
let (pairs, ids) = get_keypairs_and_ids(num_parties);
let keyshares = KeyShare::::new_centralized(&mut OsRng, &ids, None);
let verifying_key = keyshares[&PartyId::from(pairs[0].public())].verifying_key();
let session_id = SessionId::Reshare {
verifying_key: verifying_key.to_encoded_point(true).as_bytes().to_vec(),
block_number: 0,
};
let parties: Vec<_> = pairs
.iter()
.map(|pair| ValidatorSecretInfo {
pair: pair.clone(),
keyshare: None,
threshold_keyshare: Some(ThresholdKeyShare::from_key_share(
&keyshares[&PartyId::from(pair.public())],
)),
aux_info: None,
})
.collect();
let threshold = parties.len();
let mut outputs = test_protocol_with_parties(parties, session_id, threshold).await;
if let ProtocolOutput::Reshare(keyshare) = outputs.pop().unwrap() {
assert!(keyshare.verifying_key() == verifying_key);
} else {
panic!("Unexpected protocol output");
}
}
async fn test_dkg_with_parties(num_parties: usize) {
let (pairs, _ids) = get_keypairs_and_ids(num_parties);
let parties: Vec<_> =
pairs.iter().map(|pair| ValidatorSecretInfo::pair_only(pair.clone())).collect();
let threshold = parties.len();
let session_id = SessionId::Dkg { block_number: 0 };
let mut outputs = test_protocol_with_parties(parties, session_id, threshold).await;
if let ProtocolOutput::Dkg(_keyshare) = outputs.pop().unwrap() {
} else {
panic!("Unexpected protocol output");
}
}
async fn test_dkg_and_sign_with_parties(num_parties: usize) {
let threshold = num_parties - 1;
if threshold < 2 {
panic!("Not enought parties to test threshold signing");
}
let (pairs, ids) = get_keypairs_and_ids(num_parties);
let dkg_parties =
pairs.iter().map(|pair| ValidatorSecretInfo::pair_only(pair.clone())).collect();
let session_id = SessionId::Dkg { block_number: 0 };
let outputs = test_protocol_with_parties(dkg_parties, session_id, threshold).await;
let signing_committee = (0..threshold)
.into_iter()
.map(|i| pairs[i].clone())
.map(|pair| ids.get(&PartyId::from(pair.public())).unwrap())
.cloned()
.collect::>();
let parties: Vec = outputs
.clone()
.into_iter()
.filter_map(|output| {
if let ProtocolOutput::Dkg((threshold_keyshare, aux_info)) = output {
let keyshare = threshold_keyshare.to_key_share(&signing_committee);
if signing_committee.contains(keyshare.owner()) {
let pair = pairs
.iter()
.find(|p| keyshare.owner() == &PartyId::new(AccountId32(p.public().0)))
.unwrap();
Some(ValidatorSecretInfo {
pair: pair.clone(),
keyshare: Some(keyshare),
threshold_keyshare: None,
aux_info: Some(aux_info),
})
} else {
None
}
} else {
panic!("Unexpected protocol output");
}
})
.collect();
let verifying_key = parties[0].keyshare.clone().unwrap().verifying_key();
let message_hash = [0u8; 32];
let session_id = SessionId::Sign(SigningSessionInfo {
signature_verifying_key: verifying_key.to_encoded_point(true).as_bytes().to_vec(),
message_hash,
request_author: AccountId32([0u8; 32]),
});
let mut outputs =
test_protocol_with_parties(parties[..threshold].to_vec(), session_id, threshold).await;
if let ProtocolOutput::Sign(recoverable_signature) = outputs.pop().unwrap() {
// Check signature
let recovery_key_from_sig = VerifyingKey::recover_from_prehash(
&message_hash,
&recoverable_signature.signature,
recoverable_signature.recovery_id,
)
.unwrap();
assert_eq!(verifying_key, recovery_key_from_sig);
} else {
panic!("Unexpected protocol output");
}
}
/// Generic test for any of the 3 protocols
async fn test_protocol_with_parties(
parties: Vec,
session_id: SessionId,
threshold: usize,
) -> Vec {
// Prepare information about each node
let mut validator_secrets = Vec::new();
let mut validators_info = Vec::new();
for i in 0..parties.len() {
// Start a TCP listener and get its socket address
let socket = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = socket.local_addr().unwrap();
let x25519_secret_key = StaticSecret::random_from_rng(OsRng);
let x25519_public_key = x25519_dalek::PublicKey::from(&x25519_secret_key).to_bytes();
validator_secrets.push(ValidatorSecretInfoWithSocket::new(
parties[i].clone(),
x25519_secret_key,
socket,
));
// Public contact information that all parties know
validators_info.push(ValidatorInfo {
tss_account: AccountId32(parties[i].pair.public().0),
x25519_public_key,
ip_address: addr.to_string(),
})
}
let now = Instant::now();
// Spawn tasks for each party
let mut results_rx = Vec::new();
for _ in 0..parties.len() {
// Channel used to return the resulting signature
let (tx, rx) = oneshot::channel();
results_rx.push(rx);
let secret = validator_secrets.pop().unwrap();
let validators_info_clone = validators_info.clone();
let session_id_clone = session_id.clone();
tokio::spawn(async move {
let result = server(
secret.socket,
validators_info_clone,
secret.pair,
secret.x25519_secret_key,
session_id_clone,
secret.keyshare,
secret.threshold_keyshare,
secret.aux_info,
threshold,
)
.await;
if !tx.is_closed() {
tx.send(result).unwrap();
}
});
}
let results =
future::join_all(results_rx).await.into_iter().map(|r| r.unwrap().unwrap()).collect();
println!("Got protocol results with {} parties in {:?}", parties.len(), now.elapsed());
results
}
/// Details of an individual party
#[derive(Clone)]
struct ValidatorSecretInfo {
pair: sr25519::Pair,
keyshare: Option>,
threshold_keyshare: Option>,
aux_info: Option>,
}
impl ValidatorSecretInfo {
fn pair_only(pair: sr25519::Pair) -> Self {
ValidatorSecretInfo { pair, keyshare: None, threshold_keyshare: None, aux_info: None }
}
}
/// Full details of an individual party, with a socket
struct ValidatorSecretInfoWithSocket {
pair: sr25519::Pair,
keyshare: Option>,
threshold_keyshare: Option>,
aux_info: Option>,
x25519_secret_key: StaticSecret,
socket: TcpListener,
}
impl ValidatorSecretInfoWithSocket {
fn new(
secret_info: ValidatorSecretInfo,
x25519_secret_key: StaticSecret,
socket: TcpListener,
) -> Self {
Self {
pair: secret_info.pair,
keyshare: secret_info.keyshare,
threshold_keyshare: secret_info.threshold_keyshare,
aux_info: secret_info.aux_info,
x25519_secret_key,
socket,
}
}
}
/// Helper to get the async runtime used for these tests
fn get_tokio_runtime(num_cpus: usize) -> Runtime {
tokio::runtime::Builder::new_multi_thread()
.worker_threads(num_cpus)
.enable_all()
.build()
.unwrap()
}
/// Generate keypair and make PartyId from public key
fn get_keypairs_and_ids(num_parties: usize) -> (Vec, BTreeSet) {
let pairs = (0..num_parties).map(|_| sr25519::Pair::generate().0).collect::>();
let ids = pairs
.iter()
.map(|pair| PartyId::new(AccountId32(pair.public().0)))
.collect::>();
(pairs, ids)
}