Merge branch 'develop' into next

This resolves the conflicts and gets the workspace `Cargo.toml`s to not be
invalid. It doesn't actually get clippy to pass again yet.

Does move `crypto/dkg/src/evrf` into a new `crypto/dkg/evrf` crate (which does
not yet compile).
This commit is contained in:
Luke Parker
2025-08-23 15:04:39 -04:00
319 changed files with 4016 additions and 26990 deletions

View File

@@ -1,38 +0,0 @@
async fn handle_coordinator_msg<D: Db, N: Network, Co: Coordinator>(
txn: &mut D::Transaction<'_>,
network: &N,
coordinator: &mut Co,
tributary_mutable: &mut TributaryMutable<N, D>,
substrate_mutable: &mut SubstrateMutable<N, D>,
msg: &Message,
) {
match msg.msg.clone() {
CoordinatorMessage::Substrate(msg) => {
match msg {
messages::substrate::CoordinatorMessage::SubstrateBlock {
context,
block: substrate_block,
burns,
batches,
} => {
// Send SubstrateBlockAck, with relevant plan IDs, before we trigger the signing of these
// plans
if !tributary_mutable.signers.is_empty() {
coordinator
.send(messages::coordinator::ProcessorMessage::SubstrateBlockAck {
block: substrate_block,
plans: to_sign
.iter()
.filter_map(|signable| {
SessionDb::get(txn, signable.0.to_bytes().as_ref())
.map(|session| PlanMeta { session, id: signable.1 })
})
.collect(),
})
.await;
}
}
}
}
}
}

View File

@@ -1,148 +0,0 @@
// TODO
use core::{time::Duration, pin::Pin, future::Future};
use std::collections::HashMap;
use rand_core::OsRng;
use frost::{Participant, ThresholdKeys};
use tokio::time::timeout;
use serai_client::validator_sets::primitives::Session;
use serai_db::{DbTxn, MemDb};
use crate::{
Plan, Db,
networks::{OutputType, Output, Block, UtxoNetwork},
multisigs::{
scheduler::Scheduler,
scanner::{ScannerEvent, Scanner, ScannerHandle},
},
tests::sign,
};
async fn spend<N: UtxoNetwork, D: Db>(
db: &mut D,
network: &N,
keys: &HashMap<Participant, ThresholdKeys<N::Curve>>,
scanner: &mut ScannerHandle<N, D>,
outputs: Vec<N::Output>,
) where
<N::Scheduler as Scheduler<N>>::Addendum: From<()>,
{
let key = keys[&Participant::new(1).unwrap()].group_key();
let mut keys_txs = HashMap::new();
for (i, keys) in keys {
keys_txs.insert(
*i,
(
keys.clone(),
network
.prepare_send(
network.get_latest_block_number().await.unwrap() - N::CONFIRMATIONS,
// Send to a change output
Plan {
key,
inputs: outputs.clone(),
payments: vec![],
change: Some(N::change_address(key).unwrap()),
scheduler_addendum: ().into(),
},
0,
)
.await
.unwrap()
.tx
.unwrap(),
),
);
}
sign(network.clone(), Session(0), keys_txs).await;
for _ in 0 .. N::CONFIRMATIONS {
network.mine_block().await;
}
match timeout(Duration::from_secs(30), scanner.events.recv()).await.unwrap().unwrap() {
ScannerEvent::Block { is_retirement_block, block, outputs } => {
scanner.multisig_completed.send(false).unwrap();
assert!(!is_retirement_block);
assert_eq!(outputs.len(), 1);
// Make sure this is actually a change output
assert_eq!(outputs[0].kind(), OutputType::Change);
assert_eq!(outputs[0].key(), key);
let mut txn = db.txn();
assert_eq!(scanner.ack_block(&mut txn, block).await.1, outputs);
scanner.release_lock().await;
txn.commit();
}
ScannerEvent::Completed(_, _, _, _, _) => {
panic!("unexpectedly got eventuality completion");
}
}
}
pub async fn test_addresses<N: UtxoNetwork>(
new_network: impl Fn(MemDb) -> Pin<Box<dyn Send + Future<Output = N>>>,
) where
<N::Scheduler as Scheduler<N>>::Addendum: From<()>,
{
let mut keys = frost::tests::key_gen::<_, N::Curve>(&mut OsRng);
for keys in keys.values_mut() {
N::tweak_keys(keys);
}
let key = keys[&Participant::new(1).unwrap()].group_key();
let mut db = MemDb::new();
let network = new_network(db.clone()).await;
// Mine blocks so there's a confirmed block
for _ in 0 .. N::CONFIRMATIONS {
network.mine_block().await;
}
let (mut scanner, current_keys) = Scanner::new(network.clone(), db.clone());
assert!(current_keys.is_empty());
let mut txn = db.txn();
scanner.register_key(&mut txn, network.get_latest_block_number().await.unwrap(), key).await;
txn.commit();
for _ in 0 .. N::CONFIRMATIONS {
network.mine_block().await;
}
// Receive funds to the various addresses and make sure they're properly identified
let mut received_outputs = vec![];
for (kind, address) in [
(OutputType::External, N::external_address(&network, key).await),
(OutputType::Branch, N::branch_address(key).unwrap()),
(OutputType::Change, N::change_address(key).unwrap()),
(OutputType::Forwarded, N::forward_address(key).unwrap()),
] {
let block_id = network.test_send(address).await.id();
// Verify the Scanner picked them up
match timeout(Duration::from_secs(30), scanner.events.recv()).await.unwrap().unwrap() {
ScannerEvent::Block { is_retirement_block, block, outputs } => {
scanner.multisig_completed.send(false).unwrap();
assert!(!is_retirement_block);
assert_eq!(block, block_id);
assert_eq!(outputs.len(), 1);
assert_eq!(outputs[0].kind(), kind);
assert_eq!(outputs[0].key(), key);
let mut txn = db.txn();
assert_eq!(scanner.ack_block(&mut txn, block).await.1, outputs);
scanner.release_lock().await;
txn.commit();
received_outputs.extend(outputs);
}
ScannerEvent::Completed(_, _, _, _, _) => {
panic!("unexpectedly got eventuality completion");
}
};
}
// Spend the branch output, creating a change output and ensuring we actually get change
spend(&mut db, &network, &keys, &mut scanner, received_outputs).await;
}

View File

@@ -1,154 +0,0 @@
// TODO
use std::collections::HashMap;
use rand_core::{RngCore, OsRng};
use ciphersuite::group::GroupEncoding;
use frost::{
curve::Ristretto,
Participant,
dkg::tests::{key_gen, clone_without},
};
use sp_application_crypto::{RuntimePublic, sr25519::Public};
use serai_db::{DbTxn, Db, MemDb};
#[rustfmt::skip]
use serai_client::{primitives::*, in_instructions::primitives::*, validator_sets::primitives::Session};
use messages::{
substrate,
coordinator::{self, SubstrateSignableId, SubstrateSignId, CoordinatorMessage},
ProcessorMessage,
};
use crate::batch_signer::BatchSigner;
#[test]
fn test_batch_signer() {
let keys = key_gen::<_, Ristretto>(&mut OsRng);
let participant_one = Participant::new(1).unwrap();
let id: u32 = 5;
let block = BlockHash([0xaa; 32]);
let batch = Batch {
network: ExternalNetworkId::Monero,
id,
block,
instructions: vec![
InInstructionWithBalance {
instruction: InInstruction::Transfer(SeraiAddress([0xbb; 32])),
balance: ExternalBalance { coin: ExternalCoin::Bitcoin, amount: Amount(1000) },
},
InInstructionWithBalance {
instruction: InInstruction::Dex(DexCall::SwapAndAddLiquidity(SeraiAddress([0xbb; 32]))),
balance: ExternalBalance { coin: ExternalCoin::Monero, amount: Amount(9999999999999999) },
},
],
};
let actual_id =
SubstrateSignId { session: Session(0), id: SubstrateSignableId::Batch(batch.id), attempt: 0 };
let mut signing_set = vec![];
while signing_set.len() < usize::from(keys.values().next().unwrap().params().t()) {
let candidate = Participant::new(
u16::try_from((OsRng.next_u64() % u64::try_from(keys.len()).unwrap()) + 1).unwrap(),
)
.unwrap();
if signing_set.contains(&candidate) {
continue;
}
signing_set.push(candidate);
}
let mut signers = HashMap::new();
let mut dbs = HashMap::new();
let mut preprocesses = HashMap::new();
for i in 1 ..= keys.len() {
let i = Participant::new(u16::try_from(i).unwrap()).unwrap();
let keys = keys.get(&i).unwrap().clone();
let mut signer = BatchSigner::<MemDb>::new(ExternalNetworkId::Monero, Session(0), vec![keys]);
let mut db = MemDb::new();
let mut txn = db.txn();
match signer.sign(&mut txn, batch.clone()).unwrap() {
// All participants should emit a preprocess
coordinator::ProcessorMessage::BatchPreprocess {
id,
block: batch_block,
preprocesses: mut these_preprocesses,
} => {
assert_eq!(id, actual_id);
assert_eq!(batch_block, block);
assert_eq!(these_preprocesses.len(), 1);
if signing_set.contains(&i) {
preprocesses.insert(i, these_preprocesses.swap_remove(0));
}
}
_ => panic!("didn't get preprocess back"),
}
txn.commit();
signers.insert(i, signer);
dbs.insert(i, db);
}
let mut shares = HashMap::new();
for i in &signing_set {
let mut txn = dbs.get_mut(i).unwrap().txn();
match signers
.get_mut(i)
.unwrap()
.handle(
&mut txn,
CoordinatorMessage::SubstratePreprocesses {
id: actual_id.clone(),
preprocesses: clone_without(&preprocesses, i),
},
)
.unwrap()
{
ProcessorMessage::Coordinator(coordinator::ProcessorMessage::SubstrateShare {
id,
shares: mut these_shares,
}) => {
assert_eq!(id, actual_id);
assert_eq!(these_shares.len(), 1);
shares.insert(*i, these_shares.swap_remove(0));
}
_ => panic!("didn't get share back"),
}
txn.commit();
}
for i in &signing_set {
let mut txn = dbs.get_mut(i).unwrap().txn();
match signers
.get_mut(i)
.unwrap()
.handle(
&mut txn,
CoordinatorMessage::SubstrateShares {
id: actual_id.clone(),
shares: clone_without(&shares, i),
},
)
.unwrap()
{
ProcessorMessage::Substrate(substrate::ProcessorMessage::SignedBatch {
batch: signed_batch,
}) => {
assert_eq!(signed_batch.batch, batch);
assert!(Public::from_raw(keys[&participant_one].group_key().to_bytes())
.verify(&batch_message(&batch), &signed_batch.signature));
}
_ => panic!("didn't get signed batch back"),
}
txn.commit();
}
}

View File

@@ -1,130 +0,0 @@
// TODO
use std::collections::HashMap;
use rand_core::{RngCore, OsRng};
use ciphersuite::group::GroupEncoding;
use frost::{
curve::Ristretto,
Participant,
dkg::tests::{key_gen, clone_without},
};
use sp_application_crypto::{RuntimePublic, sr25519::Public};
use serai_db::{DbTxn, Db, MemDb};
use serai_client::{primitives::*, validator_sets::primitives::Session};
use messages::coordinator::*;
use crate::cosigner::Cosigner;
#[test]
fn test_cosigner() {
let keys = key_gen::<_, Ristretto>(&mut OsRng);
let participant_one = Participant::new(1).unwrap();
let block_number = OsRng.next_u64();
let block = [0xaa; 32];
let actual_id = SubstrateSignId {
session: Session(0),
id: SubstrateSignableId::CosigningSubstrateBlock(block),
attempt: (OsRng.next_u64() >> 32).try_into().unwrap(),
};
let mut signing_set = vec![];
while signing_set.len() < usize::from(keys.values().next().unwrap().params().t()) {
let candidate = Participant::new(
u16::try_from((OsRng.next_u64() % u64::try_from(keys.len()).unwrap()) + 1).unwrap(),
)
.unwrap();
if signing_set.contains(&candidate) {
continue;
}
signing_set.push(candidate);
}
let mut signers = HashMap::new();
let mut dbs = HashMap::new();
let mut preprocesses = HashMap::new();
for i in 1 ..= keys.len() {
let i = Participant::new(u16::try_from(i).unwrap()).unwrap();
let keys = keys.get(&i).unwrap().clone();
let mut db = MemDb::new();
let mut txn = db.txn();
let (signer, preprocess) =
Cosigner::new(&mut txn, Session(0), vec![keys], block_number, block, actual_id.attempt)
.unwrap();
match preprocess {
// All participants should emit a preprocess
ProcessorMessage::CosignPreprocess { id, preprocesses: mut these_preprocesses } => {
assert_eq!(id, actual_id);
assert_eq!(these_preprocesses.len(), 1);
if signing_set.contains(&i) {
preprocesses.insert(i, these_preprocesses.swap_remove(0));
}
}
_ => panic!("didn't get preprocess back"),
}
txn.commit();
signers.insert(i, signer);
dbs.insert(i, db);
}
let mut shares = HashMap::new();
for i in &signing_set {
let mut txn = dbs.get_mut(i).unwrap().txn();
match signers
.get_mut(i)
.unwrap()
.handle(
&mut txn,
CoordinatorMessage::SubstratePreprocesses {
id: actual_id.clone(),
preprocesses: clone_without(&preprocesses, i),
},
)
.unwrap()
{
ProcessorMessage::SubstrateShare { id, shares: mut these_shares } => {
assert_eq!(id, actual_id);
assert_eq!(these_shares.len(), 1);
shares.insert(*i, these_shares.swap_remove(0));
}
_ => panic!("didn't get share back"),
}
txn.commit();
}
for i in &signing_set {
let mut txn = dbs.get_mut(i).unwrap().txn();
match signers
.get_mut(i)
.unwrap()
.handle(
&mut txn,
CoordinatorMessage::SubstrateShares {
id: actual_id.clone(),
shares: clone_without(&shares, i),
},
)
.unwrap()
{
ProcessorMessage::CosignedBlock { block_number, block: signed_block, signature } => {
assert_eq!(signed_block, block);
assert!(Public::from_raw(keys[&participant_one].group_key().to_bytes()).verify(
&cosign_block_msg(block_number, block),
&Signature(signature.try_into().unwrap())
));
}
_ => panic!("didn't get cosigned block back"),
}
txn.commit();
}
}

View File

@@ -1,143 +0,0 @@
// TODO
use std::collections::HashMap;
use zeroize::Zeroizing;
use rand_core::OsRng;
use ciphersuite::{
group::{ff::Field, GroupEncoding},
Ciphersuite, Ristretto,
};
use dkg::{Participant, ThresholdParams, evrf::*};
use serai_db::{DbTxn, Db, MemDb};
use sp_application_crypto::sr25519;
use serai_client::validator_sets::primitives::{Session, KeyPair};
use messages::key_gen::*;
use crate::{
networks::Network,
key_gen::{KeyConfirmed, KeyGen},
};
const SESSION: Session = Session(1);
pub fn test_key_gen<N: Network>() {
let mut dbs = HashMap::new();
let mut substrate_evrf_keys = HashMap::new();
let mut network_evrf_keys = HashMap::new();
let mut evrf_public_keys = vec![];
let mut key_gens = HashMap::new();
for i in 1 ..= 5 {
let db = MemDb::new();
dbs.insert(i, db.clone());
let substrate_evrf_key = Zeroizing::new(
<<Ristretto as EvrfCurve>::EmbeddedCurve as Ciphersuite>::F::random(&mut OsRng),
);
substrate_evrf_keys.insert(i, substrate_evrf_key.clone());
let network_evrf_key = Zeroizing::new(
<<N::Curve as EvrfCurve>::EmbeddedCurve as Ciphersuite>::F::random(&mut OsRng),
);
network_evrf_keys.insert(i, network_evrf_key.clone());
evrf_public_keys.push((
(<<Ristretto as EvrfCurve>::EmbeddedCurve as Ciphersuite>::generator() * *substrate_evrf_key)
.to_bytes(),
(<<N::Curve as EvrfCurve>::EmbeddedCurve as Ciphersuite>::generator() * *network_evrf_key)
.to_bytes()
.as_ref()
.to_vec(),
));
key_gens
.insert(i, KeyGen::<N, MemDb>::new(db, substrate_evrf_key.clone(), network_evrf_key.clone()));
}
let mut participations = HashMap::new();
for i in 1 ..= 5 {
let key_gen = key_gens.get_mut(&i).unwrap();
let mut txn = dbs.get_mut(&i).unwrap().txn();
let mut msgs = key_gen.handle(
&mut txn,
CoordinatorMessage::GenerateKey {
session: SESSION,
threshold: 3,
evrf_public_keys: evrf_public_keys.clone(),
},
);
assert_eq!(msgs.len(), 1);
let ProcessorMessage::Participation { session, participation } = msgs.swap_remove(0) else {
panic!("didn't get a participation")
};
assert_eq!(session, SESSION);
participations.insert(i, participation);
txn.commit();
}
let mut res = None;
for i in 1 ..= 5 {
let key_gen = key_gens.get_mut(&i).unwrap();
let mut txn = dbs.get_mut(&i).unwrap().txn();
for j in 1 ..= 5 {
let mut msgs = key_gen.handle(
&mut txn,
CoordinatorMessage::Participation {
session: SESSION,
participant: Participant::new(u16::try_from(j).unwrap()).unwrap(),
participation: participations[&j].clone(),
},
);
if j != 3 {
assert!(msgs.is_empty());
}
if j == 3 {
assert_eq!(msgs.len(), 1);
let ProcessorMessage::GeneratedKeyPair { session, substrate_key, network_key } =
msgs.swap_remove(0)
else {
panic!("didn't get a generated key pair")
};
assert_eq!(session, SESSION);
if res.is_none() {
res = Some((substrate_key, network_key.clone()));
}
assert_eq!(res.as_ref().unwrap(), &(substrate_key, network_key));
}
}
txn.commit();
}
let res = res.unwrap();
for i in 1 ..= 5 {
let key_gen = key_gens.get_mut(&i).unwrap();
let mut txn = dbs.get_mut(&i).unwrap().txn();
let KeyConfirmed { mut substrate_keys, mut network_keys } = key_gen.confirm(
&mut txn,
SESSION,
&KeyPair(sr25519::Public(res.0), res.1.clone().try_into().unwrap()),
);
txn.commit();
assert_eq!(substrate_keys.len(), 1);
let substrate_keys = substrate_keys.swap_remove(0);
assert_eq!(network_keys.len(), 1);
let network_keys = network_keys.swap_remove(0);
let params =
ThresholdParams::new(3, 5, Participant::new(u16::try_from(i).unwrap()).unwrap()).unwrap();
assert_eq!(substrate_keys.params(), params);
assert_eq!(network_keys.params(), params);
assert_eq!(
(
substrate_keys.group_key().to_bytes(),
network_keys.group_key().to_bytes().as_ref().to_vec()
),
res
);
}
}

View File

@@ -1,443 +0,0 @@
// TODO
use dockertest::{
PullPolicy, StartPolicy, LogOptions, LogAction, LogPolicy, LogSource, Image,
TestBodySpecification, DockerOperations, DockerTest,
};
use serai_db::MemDb;
#[cfg(feature = "bitcoin")]
mod bitcoin {
use std::sync::Arc;
use rand_core::OsRng;
use frost::Participant;
use bitcoin_serai::bitcoin::{
secp256k1::{SECP256K1, SecretKey, Message},
PrivateKey, PublicKey,
hashes::{HashEngine, Hash, sha256::Hash as Sha256},
sighash::{SighashCache, EcdsaSighashType},
absolute::LockTime,
Amount as BAmount, Sequence, Script, Witness, OutPoint,
address::Address as BAddress,
transaction::{Version, Transaction, TxIn, TxOut},
Network as BNetwork, ScriptBuf,
opcodes::all::{OP_SHA256, OP_EQUALVERIFY},
};
use scale::Encode;
use sp_application_crypto::Pair;
use serai_client::{in_instructions::primitives::Shorthand, primitives::insecure_pair_from_name};
use tokio::{
time::{timeout, Duration},
sync::Mutex,
};
use super::*;
use crate::{
networks::{Network, Bitcoin, Output, OutputType, Block},
tests::scanner::new_scanner,
multisigs::scanner::ScannerEvent,
};
#[test]
fn test_dust_constant() {
struct IsTrue<const V: bool>;
trait True {}
impl True for IsTrue<true> {}
fn check<T: True>() {
core::hint::black_box(());
}
check::<IsTrue<{ Bitcoin::DUST >= bitcoin_serai::wallet::DUST }>>();
}
#[test]
fn test_receive_data_from_input() {
let docker = spawn_bitcoin();
docker.run(|ops| async move {
let btc = bitcoin(&ops).await(MemDb::new()).await;
// generate a multisig address to receive the coins
let mut keys = frost::tests::key_gen::<_, <Bitcoin as Network>::Curve>(&mut OsRng)
.remove(&Participant::new(1).unwrap())
.unwrap();
<Bitcoin as Network>::tweak_keys(&mut keys);
let group_key = keys.group_key();
let serai_btc_address = <Bitcoin as Network>::external_address(&btc, group_key).await;
// btc key pair to send from
let private_key = PrivateKey::new(SecretKey::new(&mut rand_core::OsRng), BNetwork::Regtest);
let public_key = PublicKey::from_private_key(SECP256K1, &private_key);
let main_addr = BAddress::p2pkh(public_key, BNetwork::Regtest);
// get unlocked coins
let new_block = btc.get_latest_block_number().await.unwrap() + 1;
btc
.rpc
.rpc_call::<Vec<String>>("generatetoaddress", serde_json::json!([100, main_addr]))
.await
.unwrap();
// create a scanner
let db = MemDb::new();
let mut scanner = new_scanner(&btc, &db, group_key, &Arc::new(Mutex::new(true))).await;
// make a transfer instruction & hash it for script.
let serai_address = insecure_pair_from_name("alice").public();
let message = Shorthand::transfer(None, serai_address.into()).encode();
let mut data = Sha256::engine();
data.input(&message);
// make the output script => msg_script(OP_SHA256 PUSH MSG_HASH OP_EQUALVERIFY) + any_script
let mut script = ScriptBuf::builder()
.push_opcode(OP_SHA256)
.push_slice(Sha256::from_engine(data).as_byte_array())
.push_opcode(OP_EQUALVERIFY)
.into_script();
// append a regular spend script
for i in main_addr.script_pubkey().instructions() {
script.push_instruction(i.unwrap());
}
// Create the first transaction
let tx = btc.get_block(new_block).await.unwrap().txdata.swap_remove(0);
let mut tx = Transaction {
version: Version(2),
lock_time: LockTime::ZERO,
input: vec![TxIn {
previous_output: OutPoint { txid: tx.compute_txid(), vout: 0 },
script_sig: Script::new().into(),
sequence: Sequence(u32::MAX),
witness: Witness::default(),
}],
output: vec![TxOut {
value: tx.output[0].value - BAmount::from_sat(10000),
script_pubkey: ScriptBuf::new_p2wsh(&script.wscript_hash()),
}],
};
tx.input[0].script_sig = Bitcoin::sign_btc_input_for_p2pkh(&tx, 0, &private_key);
let initial_output_value = tx.output[0].value;
// send it
btc.rpc.send_raw_transaction(&tx).await.unwrap();
// Chain a transaction spending it with the InInstruction embedded in the input
let mut tx = Transaction {
version: Version(2),
lock_time: LockTime::ZERO,
input: vec![TxIn {
previous_output: OutPoint { txid: tx.compute_txid(), vout: 0 },
script_sig: Script::new().into(),
sequence: Sequence(u32::MAX),
witness: Witness::new(),
}],
output: vec![TxOut {
value: tx.output[0].value - BAmount::from_sat(10000),
script_pubkey: serai_btc_address.into(),
}],
};
// add the witness script
// This is the standard script with an extra argument of the InInstruction
let mut sig = SECP256K1
.sign_ecdsa_low_r(
&Message::from_digest_slice(
SighashCache::new(&tx)
.p2wsh_signature_hash(0, &script, initial_output_value, EcdsaSighashType::All)
.unwrap()
.to_raw_hash()
.as_ref(),
)
.unwrap(),
&private_key.inner,
)
.serialize_der()
.to_vec();
sig.push(1);
tx.input[0].witness.push(sig);
tx.input[0].witness.push(public_key.inner.serialize());
tx.input[0].witness.push(message.clone());
tx.input[0].witness.push(script);
// Send it immediately, as Bitcoin allows mempool chaining
btc.rpc.send_raw_transaction(&tx).await.unwrap();
// Mine enough confirmations
let block_number = btc.get_latest_block_number().await.unwrap() + 1;
for _ in 0 .. <Bitcoin as Network>::CONFIRMATIONS {
btc.mine_block().await;
}
let tx_block = btc.get_block(block_number).await.unwrap();
// verify that scanner picked up the output
let outputs =
match timeout(Duration::from_secs(30), scanner.events.recv()).await.unwrap().unwrap() {
ScannerEvent::Block { is_retirement_block, block, outputs } => {
scanner.multisig_completed.send(false).unwrap();
assert!(!is_retirement_block);
assert_eq!(block, tx_block.id());
assert_eq!(outputs.len(), 1);
assert_eq!(outputs[0].kind(), OutputType::External);
outputs
}
_ => panic!("unexpectedly got eventuality completion"),
};
// verify that the amount and message are correct
assert_eq!(outputs[0].balance().amount.0, tx.output[0].value.to_sat());
assert_eq!(outputs[0].data(), message);
});
}
fn spawn_bitcoin() -> DockerTest {
serai_docker_tests::build("bitcoin".to_string());
let composition = TestBodySpecification::with_image(
Image::with_repository("serai-dev-bitcoin").pull_policy(PullPolicy::Never),
)
.set_start_policy(StartPolicy::Strict)
.set_log_options(Some(LogOptions {
action: LogAction::Forward,
policy: LogPolicy::OnError,
source: LogSource::Both,
}))
.set_publish_all_ports(true);
let mut test = DockerTest::new().with_network(dockertest::Network::Isolated);
test.provide_container(composition);
test
}
async fn bitcoin(
ops: &DockerOperations,
) -> impl Fn(MemDb) -> Pin<Box<dyn Send + Future<Output = Bitcoin>>> {
let handle = ops.handle("serai-dev-bitcoin").host_port(8332).unwrap();
let url = format!("http://serai:seraidex@{}:{}", handle.0, handle.1);
let bitcoin = Bitcoin::new(url.clone()).await;
bitcoin.fresh_chain().await;
move |_db| Box::pin(Bitcoin::new(url.clone()))
}
test_utxo_network!(
Bitcoin,
spawn_bitcoin,
bitcoin,
bitcoin_key_gen,
bitcoin_scanner,
bitcoin_no_deadlock_in_multisig_completed,
bitcoin_signer,
bitcoin_wallet,
bitcoin_addresses,
);
}
#[cfg(feature = "monero")]
mod monero {
use super::*;
use crate::networks::{Network, Monero};
fn spawn_monero() -> DockerTest {
serai_docker_tests::build("monero".to_string());
let composition = TestBodySpecification::with_image(
Image::with_repository("serai-dev-monero").pull_policy(PullPolicy::Never),
)
.set_start_policy(StartPolicy::Strict)
.set_log_options(Some(LogOptions {
action: LogAction::Forward,
policy: LogPolicy::OnError,
source: LogSource::Both,
}))
.set_publish_all_ports(true);
let mut test = DockerTest::new();
test.provide_container(composition);
test
}
async fn monero(
ops: &DockerOperations,
) -> impl Fn(MemDb) -> Pin<Box<dyn Send + Future<Output = Monero>>> {
let handle = ops.handle("serai-dev-monero").host_port(18081).unwrap();
let url = format!("http://serai:seraidex@{}:{}", handle.0, handle.1);
let monero = Monero::new(url.clone()).await;
while monero.get_latest_block_number().await.unwrap() < 150 {
monero.mine_block().await;
}
move |_db| Box::pin(Monero::new(url.clone()))
}
test_utxo_network!(
Monero,
spawn_monero,
monero,
monero_key_gen,
monero_scanner,
monero_no_deadlock_in_multisig_completed,
monero_signer,
monero_wallet,
monero_addresses,
);
}
#[cfg(feature = "ethereum")]
mod ethereum {
use super::*;
use ciphersuite::{Ciphersuite, Secp256k1};
use serai_client::validator_sets::primitives::Session;
use crate::networks::Ethereum;
fn spawn_ethereum() -> DockerTest {
serai_docker_tests::build("ethereum".to_string());
let composition = TestBodySpecification::with_image(
Image::with_repository("serai-dev-ethereum").pull_policy(PullPolicy::Never),
)
.set_start_policy(StartPolicy::Strict)
.set_log_options(Some(LogOptions {
action: LogAction::Forward,
policy: LogPolicy::OnError,
source: LogSource::Both,
}))
.set_publish_all_ports(true);
let mut test = DockerTest::new();
test.provide_container(composition);
test
}
async fn ethereum(
ops: &DockerOperations,
) -> impl Fn(MemDb) -> Pin<Box<dyn Send + Future<Output = Ethereum<MemDb>>>> {
use std::sync::Arc;
use ethereum_serai::{
alloy::{
primitives::U256,
simple_request_transport::SimpleRequest,
rpc_client::ClientBuilder,
provider::{Provider, RootProvider},
},
deployer::Deployer,
};
let handle = ops.handle("serai-dev-ethereum").host_port(8545).unwrap();
let url = format!("http://{}:{}", handle.0, handle.1);
tokio::time::sleep(core::time::Duration::from_secs(15)).await;
{
let provider = Arc::new(RootProvider::new(
ClientBuilder::default().transport(SimpleRequest::new(url.clone()), true),
));
provider.raw_request::<_, ()>("evm_setAutomine".into(), [false]).await.unwrap();
provider.raw_request::<_, ()>("anvil_mine".into(), [96]).await.unwrap();
// Perform deployment
{
// Make sure the Deployer constructor returns None, as it doesn't exist yet
assert!(Deployer::new(provider.clone()).await.unwrap().is_none());
// Deploy the Deployer
let tx = Deployer::deployment_tx();
provider
.raw_request::<_, ()>(
"anvil_setBalance".into(),
[
tx.recover_signer().unwrap().to_string(),
(U256::from(tx.tx().gas_limit) * U256::from(tx.tx().gas_price)).to_string(),
],
)
.await
.unwrap();
let (tx, sig, _) = tx.into_parts();
let mut bytes = vec![];
tx.encode_with_signature_fields(&sig, &mut bytes);
let pending_tx = provider.send_raw_transaction(&bytes).await.unwrap();
provider.raw_request::<_, ()>("anvil_mine".into(), [96]).await.unwrap();
//tokio::time::sleep(core::time::Duration::from_secs(15)).await;
let receipt = pending_tx.get_receipt().await.unwrap();
assert!(receipt.status());
let _ = Deployer::new(provider.clone())
.await
.expect("network error")
.expect("deployer wasn't deployed");
}
}
move |db| {
let url = url.clone();
Box::pin(async move {
{
let db = db.clone();
let url = url.clone();
// Spawn a task to deploy the proper Router when the time comes
tokio::spawn(async move {
let key = loop {
let Some(key) = crate::key_gen::NetworkKeyDb::get(&db, Session(0)) else {
tokio::time::sleep(core::time::Duration::from_secs(1)).await;
continue;
};
break ethereum_serai::crypto::PublicKey::new(
Secp256k1::read_G(&mut key.as_slice()).unwrap(),
)
.unwrap();
};
let provider = Arc::new(RootProvider::new(
ClientBuilder::default().transport(SimpleRequest::new(url.clone()), true),
));
let deployer = Deployer::new(provider.clone()).await.unwrap().unwrap();
let mut tx = deployer.deploy_router(&key);
tx.gas_limit = 1_000_000u64;
tx.gas_price = 1_000_000_000u64.into();
let tx = ethereum_serai::crypto::deterministically_sign(&tx);
provider
.raw_request::<_, ()>(
"anvil_setBalance".into(),
[
tx.recover_signer().unwrap().to_string(),
(U256::from(tx.tx().gas_limit) * U256::from(tx.tx().gas_price)).to_string(),
],
)
.await
.unwrap();
let (tx, sig, _) = tx.into_parts();
let mut bytes = vec![];
tx.encode_with_signature_fields(&sig, &mut bytes);
let pending_tx = provider.send_raw_transaction(&bytes).await.unwrap();
provider.raw_request::<_, ()>("anvil_mine".into(), [96]).await.unwrap();
let receipt = pending_tx.get_receipt().await.unwrap();
assert!(receipt.status());
let _router = deployer.find_router(provider.clone(), &key).await.unwrap().unwrap();
});
}
Ethereum::new(db, url.clone(), String::new()).await
})
}
}
test_network!(
Ethereum<MemDb>,
spawn_ethereum,
ethereum,
ethereum_key_gen,
ethereum_scanner,
ethereum_no_deadlock_in_multisig_completed,
ethereum_signer,
ethereum_wallet,
);
}

View File

@@ -1,133 +0,0 @@
// TODO
use std::sync::OnceLock;
mod key_gen;
mod scanner;
mod signer;
pub(crate) use signer::sign;
mod cosigner;
mod batch_signer;
mod wallet;
mod addresses;
// Effective Once
static INIT_LOGGER_CELL: OnceLock<()> = OnceLock::new();
fn init_logger() {
*INIT_LOGGER_CELL.get_or_init(env_logger::init)
}
#[macro_export]
macro_rules! test_network {
(
$N: ty,
$docker: ident,
$network: ident,
$key_gen: ident,
$scanner: ident,
$no_deadlock_in_multisig_completed: ident,
$signer: ident,
$wallet: ident,
) => {
use core::{pin::Pin, future::Future};
use $crate::tests::{
init_logger,
key_gen::test_key_gen,
scanner::{test_scanner, test_no_deadlock_in_multisig_completed},
signer::test_signer,
wallet::test_wallet,
};
// This doesn't interact with a node and accordingly doesn't need to be spawn one
#[tokio::test]
async fn $key_gen() {
init_logger();
test_key_gen::<$N>();
}
#[test]
fn $scanner() {
init_logger();
let docker = $docker();
docker.run(|ops| async move {
let new_network = $network(&ops).await;
test_scanner(new_network).await;
});
}
#[test]
fn $no_deadlock_in_multisig_completed() {
init_logger();
let docker = $docker();
docker.run(|ops| async move {
let new_network = $network(&ops).await;
test_no_deadlock_in_multisig_completed(new_network).await;
});
}
#[test]
fn $signer() {
init_logger();
let docker = $docker();
docker.run(|ops| async move {
let new_network = $network(&ops).await;
test_signer(new_network).await;
});
}
#[test]
fn $wallet() {
init_logger();
let docker = $docker();
docker.run(|ops| async move {
let new_network = $network(&ops).await;
test_wallet(new_network).await;
});
}
};
}
#[macro_export]
macro_rules! test_utxo_network {
(
$N: ty,
$docker: ident,
$network: ident,
$key_gen: ident,
$scanner: ident,
$no_deadlock_in_multisig_completed: ident,
$signer: ident,
$wallet: ident,
$addresses: ident,
) => {
use $crate::tests::addresses::test_addresses;
test_network!(
$N,
$docker,
$network,
$key_gen,
$scanner,
$no_deadlock_in_multisig_completed,
$signer,
$wallet,
);
#[test]
fn $addresses() {
init_logger();
let docker = $docker();
docker.run(|ops| async move {
let new_network = $network(&ops).await;
test_addresses(new_network).await;
});
}
};
}
mod literal;

View File

@@ -1,203 +0,0 @@
// TODO
use core::{pin::Pin, time::Duration, future::Future};
use std::sync::Arc;
use rand_core::OsRng;
use ciphersuite::{group::GroupEncoding, Ciphersuite};
use frost::{Participant, tests::key_gen};
use tokio::{sync::Mutex, time::timeout};
use serai_db::{DbTxn, Db, MemDb};
use serai_client::validator_sets::primitives::Session;
use crate::{
networks::{OutputType, Output, Block, Network},
key_gen::NetworkKeyDb,
multisigs::scanner::{ScannerEvent, Scanner, ScannerHandle},
};
pub async fn new_scanner<N: Network, D: Db>(
network: &N,
db: &D,
group_key: <N::Curve as Ciphersuite>::G,
first: &Arc<Mutex<bool>>,
) -> ScannerHandle<N, D> {
let activation_number = network.get_latest_block_number().await.unwrap();
let mut db = db.clone();
let (mut scanner, current_keys) = Scanner::new(network.clone(), db.clone());
let mut first = first.lock().await;
if *first {
assert!(current_keys.is_empty());
let mut txn = db.txn();
scanner.register_key(&mut txn, activation_number, group_key).await;
txn.commit();
for _ in 0 .. N::CONFIRMATIONS {
network.mine_block().await;
}
*first = false;
} else {
assert_eq!(current_keys.len(), 1);
}
scanner
}
pub async fn test_scanner<N: Network>(
new_network: impl Fn(MemDb) -> Pin<Box<dyn Send + Future<Output = N>>>,
) {
let mut keys =
frost::tests::key_gen::<_, N::Curve>(&mut OsRng).remove(&Participant::new(1).unwrap()).unwrap();
N::tweak_keys(&mut keys);
let group_key = keys.group_key();
let mut db = MemDb::new();
{
let mut txn = db.txn();
NetworkKeyDb::set(&mut txn, Session(0), &group_key.to_bytes().as_ref().to_vec());
txn.commit();
}
let network = new_network(db.clone()).await;
// Mine blocks so there's a confirmed block
for _ in 0 .. N::CONFIRMATIONS {
network.mine_block().await;
}
let first = Arc::new(Mutex::new(true));
let scanner = new_scanner(&network, &db, group_key, &first).await;
// Receive funds
let block = network.test_send(N::external_address(&network, keys.group_key()).await).await;
let block_id = block.id();
// Verify the Scanner picked them up
let verify_event = |mut scanner: ScannerHandle<N, MemDb>| async move {
let outputs =
match timeout(Duration::from_secs(30), scanner.events.recv()).await.unwrap().unwrap() {
ScannerEvent::Block { is_retirement_block, block, outputs } => {
scanner.multisig_completed.send(false).unwrap();
assert!(!is_retirement_block);
assert_eq!(block, block_id);
assert_eq!(outputs.len(), 1);
assert_eq!(outputs[0].kind(), OutputType::External);
outputs
}
ScannerEvent::Completed(_, _, _, _, _) => {
panic!("unexpectedly got eventuality completion");
}
};
(scanner, outputs)
};
let (mut scanner, outputs) = verify_event(scanner).await;
// Create a new scanner off the current DB and verify it re-emits the above events
verify_event(new_scanner(&network, &db, group_key, &first).await).await;
// Acknowledge the block
let mut cloned_db = db.clone();
let mut txn = cloned_db.txn();
assert_eq!(scanner.ack_block(&mut txn, block_id).await.1, outputs);
scanner.release_lock().await;
txn.commit();
// There should be no more events
assert!(timeout(Duration::from_secs(30), scanner.events.recv()).await.is_err());
// Create a new scanner off the current DB and make sure it also does nothing
assert!(timeout(
Duration::from_secs(30),
new_scanner(&network, &db, group_key, &first).await.events.recv()
)
.await
.is_err());
}
pub async fn test_no_deadlock_in_multisig_completed<N: Network>(
new_network: impl Fn(MemDb) -> Pin<Box<dyn Send + Future<Output = N>>>,
) {
// This test scans two blocks then acknowledges one, yet a network with one confirm won't scan
// two blocks before the first is acknowledged (due to the look-ahead limit)
if N::CONFIRMATIONS <= 1 {
return;
}
let mut db = MemDb::new();
let network = new_network(db.clone()).await;
// Mine blocks so there's a confirmed block
for _ in 0 .. N::CONFIRMATIONS {
network.mine_block().await;
}
let (mut scanner, current_keys) = Scanner::new(network.clone(), db.clone());
assert!(current_keys.is_empty());
// Register keys to cause Block events at CONFIRMATIONS (dropped since first keys),
// CONFIRMATIONS + 1, and CONFIRMATIONS + 2
for i in 0 .. 3 {
let key = {
let mut keys = key_gen(&mut OsRng);
for keys in keys.values_mut() {
N::tweak_keys(keys);
}
let key = keys[&Participant::new(1).unwrap()].group_key();
if i == 0 {
let mut txn = db.txn();
NetworkKeyDb::set(&mut txn, Session(0), &key.to_bytes().as_ref().to_vec());
txn.commit();
// Sleep for 5 seconds as setting the Network key value will trigger an async task for
// Ethereum
tokio::time::sleep(Duration::from_secs(5)).await;
}
key
};
let mut txn = db.txn();
scanner
.register_key(
&mut txn,
network.get_latest_block_number().await.unwrap() + N::CONFIRMATIONS + i,
key,
)
.await;
txn.commit();
}
for _ in 0 .. (3 * N::CONFIRMATIONS) {
network.mine_block().await;
}
// Block for the second set of keys registered
let block_id =
match timeout(Duration::from_secs(30), scanner.events.recv()).await.unwrap().unwrap() {
ScannerEvent::Block { is_retirement_block, block, outputs: _ } => {
scanner.multisig_completed.send(false).unwrap();
assert!(!is_retirement_block);
block
}
ScannerEvent::Completed(_, _, _, _, _) => {
panic!("unexpectedly got eventuality completion");
}
};
// Block for the third set of keys registered
match timeout(Duration::from_secs(30), scanner.events.recv()).await.unwrap().unwrap() {
ScannerEvent::Block { .. } => {}
ScannerEvent::Completed(_, _, _, _, _) => {
panic!("unexpectedly got eventuality completion");
}
};
// The ack_block acquisition shows the Scanner isn't maintaining the lock on its own thread after
// emitting the Block event
// TODO: This is incomplete. Also test after emitting Completed
let mut txn = db.txn();
assert_eq!(scanner.ack_block(&mut txn, block_id).await.1, vec![]);
scanner.release_lock().await;
txn.commit();
scanner.multisig_completed.send(false).unwrap();
}

View File

@@ -1,246 +0,0 @@
// TODO
use core::{pin::Pin, future::Future};
use std::collections::HashMap;
use rand_core::{RngCore, OsRng};
use ciphersuite::group::GroupEncoding;
use frost::{
Participant, ThresholdKeys,
dkg::tests::{key_gen, clone_without},
};
use serai_db::{DbTxn, Db, MemDb};
use serai_client::{
primitives::{ExternalNetworkId, ExternalCoin, Amount, ExternalBalance},
validator_sets::primitives::Session,
};
use messages::sign::*;
use crate::{
Payment,
networks::{Output, Transaction, Eventuality, Network},
key_gen::NetworkKeyDb,
multisigs::scheduler::Scheduler,
signer::Signer,
};
#[allow(clippy::type_complexity)]
pub async fn sign<N: Network>(
network: N,
session: Session,
mut keys_txs: HashMap<
Participant,
(ThresholdKeys<N::Curve>, (N::SignableTransaction, N::Eventuality)),
>,
) -> <N::Eventuality as Eventuality>::Claim {
let actual_id = SignId { session, id: [0xaa; 32], attempt: 0 };
let mut keys = HashMap::new();
let mut txs = HashMap::new();
for (i, (these_keys, this_tx)) in keys_txs.drain() {
keys.insert(i, these_keys);
txs.insert(i, this_tx);
}
let mut signers = HashMap::new();
let mut dbs = HashMap::new();
let mut t = 0;
for i in 1 ..= keys.len() {
let i = Participant::new(u16::try_from(i).unwrap()).unwrap();
let keys = keys.remove(&i).unwrap();
t = keys.params().t();
signers.insert(i, Signer::<_, MemDb>::new(network.clone(), Session(0), vec![keys]));
dbs.insert(i, MemDb::new());
}
drop(keys);
let mut signing_set = vec![];
while signing_set.len() < usize::from(t) {
let candidate = Participant::new(
u16::try_from((OsRng.next_u64() % u64::try_from(signers.len()).unwrap()) + 1).unwrap(),
)
.unwrap();
if signing_set.contains(&candidate) {
continue;
}
signing_set.push(candidate);
}
let mut preprocesses = HashMap::new();
let mut eventuality = None;
for i in 1 ..= signers.len() {
let i = Participant::new(u16::try_from(i).unwrap()).unwrap();
let (tx, this_eventuality) = txs.remove(&i).unwrap();
let mut txn = dbs.get_mut(&i).unwrap().txn();
match signers
.get_mut(&i)
.unwrap()
.sign_transaction(&mut txn, actual_id.id, tx, &this_eventuality)
.await
{
// All participants should emit a preprocess
Some(ProcessorMessage::Preprocess { id, preprocesses: mut these_preprocesses }) => {
assert_eq!(id, actual_id);
assert_eq!(these_preprocesses.len(), 1);
if signing_set.contains(&i) {
preprocesses.insert(i, these_preprocesses.swap_remove(0));
}
}
_ => panic!("didn't get preprocess back"),
}
txn.commit();
if eventuality.is_none() {
eventuality = Some(this_eventuality.clone());
}
assert_eq!(eventuality, Some(this_eventuality));
}
let mut shares = HashMap::new();
for i in &signing_set {
let mut txn = dbs.get_mut(i).unwrap().txn();
match signers
.get_mut(i)
.unwrap()
.handle(
&mut txn,
CoordinatorMessage::Preprocesses {
id: actual_id.clone(),
preprocesses: clone_without(&preprocesses, i),
},
)
.await
.unwrap()
{
ProcessorMessage::Share { id, shares: mut these_shares } => {
assert_eq!(id, actual_id);
assert_eq!(these_shares.len(), 1);
shares.insert(*i, these_shares.swap_remove(0));
}
_ => panic!("didn't get share back"),
}
txn.commit();
}
let mut tx_id = None;
for i in &signing_set {
let mut txn = dbs.get_mut(i).unwrap().txn();
match signers
.get_mut(i)
.unwrap()
.handle(
&mut txn,
CoordinatorMessage::Shares { id: actual_id.clone(), shares: clone_without(&shares, i) },
)
.await
.unwrap()
{
ProcessorMessage::Completed { session, id, tx } => {
assert_eq!(session, Session(0));
assert_eq!(id, actual_id.id);
if tx_id.is_none() {
tx_id = Some(tx.clone());
}
assert_eq!(tx_id, Some(tx));
}
_ => panic!("didn't get TX back"),
}
txn.commit();
}
let mut typed_claim = <N::Eventuality as Eventuality>::Claim::default();
typed_claim.as_mut().copy_from_slice(tx_id.unwrap().as_ref());
assert!(network.check_eventuality_by_claim(&eventuality.unwrap(), &typed_claim).await);
typed_claim
}
pub async fn test_signer<N: Network>(
new_network: impl Fn(MemDb) -> Pin<Box<dyn Send + Future<Output = N>>>,
) {
let mut keys = key_gen(&mut OsRng);
for keys in keys.values_mut() {
N::tweak_keys(keys);
}
let key = keys[&Participant::new(1).unwrap()].group_key();
let mut db = MemDb::new();
{
let mut txn = db.txn();
NetworkKeyDb::set(&mut txn, Session(0), &key.to_bytes().as_ref().to_vec());
txn.commit();
}
let network = new_network(db.clone()).await;
let outputs = network
.get_outputs(&network.test_send(N::external_address(&network, key).await).await, key)
.await;
let sync_block = network.get_latest_block_number().await.unwrap() - N::CONFIRMATIONS;
let amount = (2 * N::DUST) + 1000;
let plan = {
let mut txn = db.txn();
let mut scheduler = N::Scheduler::new::<MemDb>(&mut txn, key, N::NETWORK);
let payments = vec![Payment {
address: N::external_address(&network, key).await,
balance: ExternalBalance {
coin: match N::NETWORK {
ExternalNetworkId::Bitcoin => ExternalCoin::Bitcoin,
ExternalNetworkId::Ethereum => ExternalCoin::Ether,
ExternalNetworkId::Monero => ExternalCoin::Monero,
},
amount: Amount(amount),
},
}];
let mut plans = scheduler.schedule::<MemDb>(&mut txn, outputs.clone(), payments, key, false);
assert_eq!(plans.len(), 1);
plans.swap_remove(0)
};
let mut keys_txs = HashMap::new();
let mut eventualities = vec![];
for (i, keys) in keys.drain() {
let (signable, eventuality) =
network.prepare_send(sync_block, plan.clone(), 0).await.unwrap().tx.unwrap();
eventualities.push(eventuality.clone());
keys_txs.insert(i, (keys, (signable, eventuality)));
}
let claim = sign(network.clone(), Session(0), keys_txs).await;
// Mine a block, and scan it, to ensure that the TX actually made it on chain
network.mine_block().await;
let block_number = network.get_latest_block_number().await.unwrap();
let tx = network.get_transaction_by_eventuality(block_number, &eventualities[0]).await;
let outputs = network
.get_outputs(
&network.get_block(network.get_latest_block_number().await.unwrap()).await.unwrap(),
key,
)
.await;
// Don't run if Ethereum as the received output will revert by the contract
// (and therefore not actually exist)
if N::NETWORK != ExternalNetworkId::Ethereum {
assert_eq!(outputs.len(), 1 + usize::from(u8::from(plan.change.is_some())));
// Adjust the amount for the fees
let amount = amount - tx.fee(&network).await;
if plan.change.is_some() {
// Check either output since Monero will randomize its output order
assert!(
(outputs[0].balance().amount.0 == amount) || (outputs[1].balance().amount.0 == amount)
);
} else {
assert!(outputs[0].balance().amount.0 == amount);
}
}
// Check the eventualities pass
for eventuality in eventualities {
let completion = network.confirm_completion(&eventuality, &claim).await.unwrap().unwrap();
assert_eq!(N::Eventuality::claim(&completion), claim);
}
}

View File

@@ -1,203 +0,0 @@
// TODO
use core::{time::Duration, pin::Pin, future::Future};
use std::collections::HashMap;
use rand_core::OsRng;
use ciphersuite::group::GroupEncoding;
use frost::{Participant, dkg::tests::key_gen};
use tokio::time::timeout;
use serai_db::{DbTxn, Db, MemDb};
use serai_client::{
primitives::{ExternalNetworkId, ExternalCoin, Amount, ExternalBalance},
validator_sets::primitives::Session,
};
use crate::{
Payment, Plan,
networks::{Output, Transaction, Eventuality, Block, Network},
key_gen::NetworkKeyDb,
multisigs::{
scanner::{ScannerEvent, Scanner},
scheduler::{self, Scheduler},
},
tests::sign,
};
// Tests the Scanner, Scheduler, and Signer together
pub async fn test_wallet<N: Network>(
new_network: impl Fn(MemDb) -> Pin<Box<dyn Send + Future<Output = N>>>,
) {
let mut keys = key_gen(&mut OsRng);
for keys in keys.values_mut() {
N::tweak_keys(keys);
}
let key = keys[&Participant::new(1).unwrap()].group_key();
let mut db = MemDb::new();
{
let mut txn = db.txn();
NetworkKeyDb::set(&mut txn, Session(0), &key.to_bytes().as_ref().to_vec());
txn.commit();
}
let network = new_network(db.clone()).await;
// Mine blocks so there's a confirmed block
for _ in 0 .. N::CONFIRMATIONS {
network.mine_block().await;
}
let (mut scanner, current_keys) = Scanner::new(network.clone(), db.clone());
assert!(current_keys.is_empty());
let (block_id, outputs) = {
let mut txn = db.txn();
scanner.register_key(&mut txn, network.get_latest_block_number().await.unwrap(), key).await;
txn.commit();
for _ in 0 .. N::CONFIRMATIONS {
network.mine_block().await;
}
let block = network.test_send(N::external_address(&network, key).await).await;
let block_id = block.id();
match timeout(Duration::from_secs(30), scanner.events.recv()).await.unwrap().unwrap() {
ScannerEvent::Block { is_retirement_block, block, outputs } => {
scanner.multisig_completed.send(false).unwrap();
assert!(!is_retirement_block);
assert_eq!(block, block_id);
assert_eq!(outputs.len(), 1);
(block_id, outputs)
}
ScannerEvent::Completed(_, _, _, _, _) => {
panic!("unexpectedly got eventuality completion");
}
}
};
let mut txn = db.txn();
assert_eq!(scanner.ack_block(&mut txn, block_id.clone()).await.1, outputs);
scanner.release_lock().await;
txn.commit();
let mut txn = db.txn();
let mut scheduler = N::Scheduler::new::<MemDb>(&mut txn, key, N::NETWORK);
let amount = 2 * N::DUST;
let plans = scheduler.schedule::<MemDb>(
&mut txn,
outputs.clone(),
vec![Payment {
address: N::external_address(&network, key).await,
balance: ExternalBalance {
coin: match N::NETWORK {
ExternalNetworkId::Bitcoin => ExternalCoin::Bitcoin,
ExternalNetworkId::Ethereum => ExternalCoin::Ether,
ExternalNetworkId::Monero => ExternalCoin::Monero,
},
amount: Amount(amount),
},
}],
key,
false,
);
txn.commit();
assert_eq!(plans.len(), 1);
assert_eq!(plans[0].key, key);
if std::any::TypeId::of::<N::Scheduler>() ==
std::any::TypeId::of::<scheduler::smart_contract::Scheduler<N>>()
{
assert_eq!(plans[0].inputs, vec![]);
} else {
assert_eq!(plans[0].inputs, outputs);
}
assert_eq!(
plans[0].payments,
vec![Payment {
address: N::external_address(&network, key).await,
balance: ExternalBalance {
coin: match N::NETWORK {
ExternalNetworkId::Bitcoin => ExternalCoin::Bitcoin,
ExternalNetworkId::Ethereum => ExternalCoin::Ether,
ExternalNetworkId::Monero => ExternalCoin::Monero,
},
amount: Amount(amount),
}
}]
);
assert_eq!(plans[0].change, N::change_address(key));
{
let mut buf = vec![];
plans[0].write(&mut buf).unwrap();
assert_eq!(plans[0], Plan::<N>::read::<&[u8]>(&mut buf.as_ref()).unwrap());
}
// Execute the plan
let mut keys_txs = HashMap::new();
let mut eventualities = vec![];
for (i, keys) in keys.drain() {
let (signable, eventuality) = network
.prepare_send(network.get_block_number(&block_id).await, plans[0].clone(), 0)
.await
.unwrap()
.tx
.unwrap();
eventualities.push(eventuality.clone());
keys_txs.insert(i, (keys, (signable, eventuality)));
}
let claim = sign(network.clone(), Session(0), keys_txs).await;
network.mine_block().await;
let block_number = network.get_latest_block_number().await.unwrap();
let tx = network.get_transaction_by_eventuality(block_number, &eventualities[0]).await;
let block = network.get_block(block_number).await.unwrap();
let outputs = network.get_outputs(&block, key).await;
// Don't run if Ethereum as the received output will revert by the contract
// (and therefore not actually exist)
if N::NETWORK != ExternalNetworkId::Ethereum {
assert_eq!(outputs.len(), 1 + usize::from(u8::from(plans[0].change.is_some())));
// Adjust the amount for the fees
let amount = amount - tx.fee(&network).await;
if plans[0].change.is_some() {
// Check either output since Monero will randomize its output order
assert!(
(outputs[0].balance().amount.0 == amount) || (outputs[1].balance().amount.0 == amount)
);
} else {
assert!(outputs[0].balance().amount.0 == amount);
}
}
for eventuality in eventualities {
let completion = network.confirm_completion(&eventuality, &claim).await.unwrap().unwrap();
assert_eq!(N::Eventuality::claim(&completion), claim);
}
for _ in 1 .. N::CONFIRMATIONS {
network.mine_block().await;
}
if N::NETWORK != ExternalNetworkId::Ethereum {
match timeout(Duration::from_secs(30), scanner.events.recv()).await.unwrap().unwrap() {
ScannerEvent::Block { is_retirement_block, block: block_id, outputs: these_outputs } => {
scanner.multisig_completed.send(false).unwrap();
assert!(!is_retirement_block);
assert_eq!(block_id, block.id());
assert_eq!(these_outputs, outputs);
}
ScannerEvent::Completed(_, _, _, _, _) => {
panic!("unexpectedly got eventuality completion");
}
}
// Check the Scanner DB can reload the outputs
let mut txn = db.txn();
assert_eq!(scanner.ack_block(&mut txn, block.id()).await.1, outputs);
scanner.release_lock().await;
txn.commit();
}
}

View File

@@ -25,7 +25,7 @@ scale = { package = "parity-scale-codec", version = "3", default-features = fals
borsh = { version = "1", default-features = false, features = ["std", "derive", "de_strict_order"] }
ciphersuite = { path = "../../crypto/ciphersuite", default-features = false, features = ["std"] }
dkg = { path = "../../crypto/dkg", default-features = false, features = ["std", "evrf-ristretto"] }
dkg = { package = "dkg-evrf", path = "../../crypto/dkg/evrf", default-features = false, features = ["std", "ristretto"] }
serai-client = { path = "../../substrate/client", default-features = false }
serai-cosign = { path = "../../coordinator/cosign" }

View File

@@ -24,8 +24,8 @@ hex = { version = "0.4", default-features = false, features = ["std"] }
scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["std"] }
borsh = { version = "1", default-features = false, features = ["std", "derive", "de_strict_order"] }
ciphersuite = { path = "../../crypto/ciphersuite", default-features = false, features = ["std", "secp256k1"] }
dkg = { path = "../../crypto/dkg", default-features = false, features = ["std", "evrf-secp256k1"] }
ciphersuite = { path = "../../crypto/ciphersuite", default-features = false, features = ["std"] }
dkg = { package = "dkg-evrf", path = "../../crypto/dkg/evrf", default-features = false, features = ["std", "secp256k1"] }
frost = { package = "modular-frost", path = "../../crypto/frost", default-features = false }
secp256k1 = { version = "0.29", default-features = false, features = ["std", "global-context", "rand-std"] }

View File

@@ -25,8 +25,8 @@ hex = { version = "0.4", default-features = false, features = ["std"] }
scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["std"] }
borsh = { version = "1", default-features = false, features = ["std", "derive", "de_strict_order"] }
ciphersuite = { path = "../../crypto/ciphersuite", default-features = false, features = ["std", "secp256k1"] }
dkg = { path = "../../crypto/dkg", default-features = false, features = ["std", "evrf-secp256k1"] }
ciphersuite = { path = "../../crypto/ciphersuite", default-features = false, features = ["std"] }
dkg = { package = "dkg-evrf", path = "../../crypto/dkg/evrf", default-features = false, features = ["std", "secp256k1"] }
frost = { package = "modular-frost", path = "../../crypto/frost", default-features = false, features = ["secp256k1"] }
k256 = { version = "^0.13.1", default-features = false, features = ["std"] }

View File

@@ -23,7 +23,7 @@ workspace = true
[dependencies]
rand_core = { version = "0.6", default-features = false, features = ["std", "getrandom"] }
frost = { package = "modular-frost", path = "../../crypto/frost", version = "^0.8.1", default-features = false }
frost = { package = "modular-frost", path = "../../crypto/frost", default-features = false }
serai-validator-sets-primitives = { path = "../../substrate/validator-sets/primitives", default-features = false, features = ["std"] }

View File

@@ -31,9 +31,10 @@ rand_chacha = { version = "0.3", default-features = false, features = ["std"] }
# Cryptography
blake2 = { version = "0.10", default-features = false, features = ["std"] }
transcript = { package = "flexible-transcript", path = "../../crypto/transcript", default-features = false, features = ["std"] }
ec-divisors = { package = "ec-divisors", path = "../../crypto/evrf/divisors", default-features = false }
ciphersuite = { path = "../../crypto/ciphersuite", default-features = false, features = ["std", "ristretto"] }
dkg = { package = "dkg", path = "../../crypto/dkg", default-features = false, features = ["std", "evrf-ristretto"] }
ec-divisors = { git = "https://github.com/kayabaNerve/monero-oxide", rev = "b6dd1a9ff7ac6b96eb7cb488a4501fd1f6f2dd1e", default-features = false }
ciphersuite = { path = "../../crypto/ciphersuite", default-features = false, features = ["std"] }
dalek-ff-group = { path = "../../crypto/dalek-ff-group", default-features = false, features = ["std"] }
dkg = { package = "dkg-evrf", path = "../../crypto/dkg/evrf", default-features = false, features = ["std", "ristretto"] }
# Substrate
serai-validator-sets-primitives = { path = "../../substrate/validator-sets/primitives", default-features = false, features = ["std"] }

View File

@@ -26,12 +26,12 @@ scale = { package = "parity-scale-codec", version = "3", default-features = fals
borsh = { version = "1", default-features = false, features = ["std", "derive", "de_strict_order"] }
dalek-ff-group = { path = "../../crypto/dalek-ff-group", default-features = false, features = ["std"] }
ciphersuite = { path = "../../crypto/ciphersuite", default-features = false, features = ["std", "ed25519"] }
dkg = { path = "../../crypto/dkg", default-features = false, features = ["std", "evrf-ed25519"] }
ciphersuite = { path = "../../crypto/ciphersuite", default-features = false, features = ["std"] }
dkg = { package = "dkg-evrf", path = "../../crypto/dkg/evrf", default-features = false, features = ["std", "ed25519"] }
frost = { package = "modular-frost", path = "../../crypto/frost", default-features = false }
monero-wallet = { path = "../../networks/monero/wallet", default-features = false, features = ["std", "multisig"] }
monero-simple-request-rpc = { path = "../../networks/monero/rpc/simple-request", default-features = false }
monero-wallet = { git = "https://github.com/kayabaNerve/monero-oxide", rev = "b6dd1a9ff7ac6b96eb7cb488a4501fd1f6f2dd1e", default-features = false, features = ["std", "multisig"] }
monero-simple-request-rpc = { git = "https://github.com/kayabaNerve/monero-oxide", rev = "b6dd1a9ff7ac6b96eb7cb488a4501fd1f6f2dd1e", default-features = false }
serai-client = { path = "../../substrate/client", default-features = false, features = ["monero"] }