mirror of
https://github.com/serai-dex/serai.git
synced 2025-01-27 04:55:57 +00:00
af86b7a499
* Remove the explicit included participants from FROST Now, whoever submits preprocesses becomes the signing set. Better separates preprocess from sign, at the cost of slightly more annoying integrations (Monero needs to now independently lagrange/offset its key images). * Support caching preprocesses Closes https://github.com/serai-dex/serai/issues/40. I *could* have added a serialization trait to Algorithm and written a ton of data to disk, while requiring Algorithm implementors also accept such work. Instead, I moved preprocess to a seeded RNG (Chacha20) which should be as secure as the regular RNG. Rebuilding from cache simply loads the previously used Chacha seed, making the Algorithm oblivious to the fact it's being rebuilt from a cache. This removes any requirements for it to be modified while guaranteeing equivalency. This builds on the last commit which delayed determining the signing set till post-preprocess acquisition. Unfortunately, that commit did force preprocess from ThresholdView to ThresholdKeys which had visible effects on Monero. Serai will actually need delayed set determination for #163, and overall, it remains better, hence it's inclusion. * Document FROST preprocess caching * Update ethereum to new FROST * Fix bug in Monero offset calculation and update processor
274 lines
8.3 KiB
Rust
274 lines
8.3 KiB
Rust
use std::sync::Mutex;
|
|
|
|
use lazy_static::lazy_static;
|
|
use rand_core::OsRng;
|
|
|
|
use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, scalar::Scalar};
|
|
|
|
use monero_serai::{
|
|
Protocol, random_scalar,
|
|
wallet::{
|
|
ViewPair,
|
|
address::{Network, AddressType, AddressMeta, MoneroAddress},
|
|
},
|
|
rpc::Rpc,
|
|
};
|
|
|
|
pub fn random_address() -> (Scalar, ViewPair, MoneroAddress) {
|
|
let spend = random_scalar(&mut OsRng);
|
|
let spend_pub = &spend * &ED25519_BASEPOINT_TABLE;
|
|
let view = random_scalar(&mut OsRng);
|
|
(
|
|
spend,
|
|
ViewPair::new(spend_pub, view),
|
|
MoneroAddress {
|
|
meta: AddressMeta::new(Network::Mainnet, AddressType::Standard),
|
|
spend: spend_pub,
|
|
view: &view * &ED25519_BASEPOINT_TABLE,
|
|
},
|
|
)
|
|
}
|
|
|
|
// TODO: Support transactions already on-chain
|
|
// TODO: Don't have a side effect of mining blocks more blocks than needed under race conditions
|
|
// TODO: mine as much as needed instead of default 10 blocks
|
|
pub async fn mine_until_unlocked(rpc: &Rpc, addr: &str, tx_hash: [u8; 32]) {
|
|
// mine until tx is in a block
|
|
let mut height = rpc.get_height().await.unwrap();
|
|
let mut found = false;
|
|
while !found {
|
|
let block = rpc.get_block(height - 1).await.unwrap();
|
|
found = match block.txs.iter().find(|&&x| x == tx_hash) {
|
|
Some(_) => true,
|
|
None => {
|
|
rpc.generate_blocks(addr, 1).await.unwrap();
|
|
height += 1;
|
|
false
|
|
}
|
|
}
|
|
}
|
|
|
|
// mine 9 more blocks to unlock the tx
|
|
rpc.generate_blocks(addr, 9).await.unwrap();
|
|
}
|
|
|
|
pub async fn rpc() -> Rpc {
|
|
let rpc = Rpc::new("http://127.0.0.1:18081".to_string()).unwrap();
|
|
|
|
// Only run once
|
|
if rpc.get_height().await.unwrap() != 1 {
|
|
return rpc;
|
|
}
|
|
|
|
let addr = MoneroAddress {
|
|
meta: AddressMeta::new(Network::Mainnet, AddressType::Standard),
|
|
spend: &random_scalar(&mut OsRng) * &ED25519_BASEPOINT_TABLE,
|
|
view: &random_scalar(&mut OsRng) * &ED25519_BASEPOINT_TABLE,
|
|
}
|
|
.to_string();
|
|
|
|
// Mine 40 blocks to ensure decoy availability
|
|
rpc.generate_blocks(&addr, 40).await.unwrap();
|
|
assert!(!matches!(rpc.get_protocol().await.unwrap(), Protocol::Unsupported(_)));
|
|
|
|
rpc
|
|
}
|
|
|
|
lazy_static! {
|
|
pub static ref SEQUENTIAL: Mutex<()> = Mutex::new(());
|
|
}
|
|
|
|
#[macro_export]
|
|
macro_rules! async_sequential {
|
|
($(async fn $name: ident() $body: block)*) => {
|
|
$(
|
|
#[tokio::test]
|
|
async fn $name() {
|
|
let guard = runner::SEQUENTIAL.lock().unwrap();
|
|
let local = tokio::task::LocalSet::new();
|
|
local.run_until(async move {
|
|
if let Err(err) = tokio::task::spawn_local(async move { $body }).await {
|
|
drop(guard);
|
|
Err(err).unwrap()
|
|
}
|
|
}).await;
|
|
}
|
|
)*
|
|
}
|
|
}
|
|
|
|
#[macro_export]
|
|
macro_rules! test {
|
|
(
|
|
$name: ident,
|
|
(
|
|
$first_tx: expr,
|
|
$first_checks: expr,
|
|
),
|
|
$((
|
|
$tx: expr,
|
|
$checks: expr,
|
|
)$(,)?),*
|
|
) => {
|
|
async_sequential! {
|
|
async fn $name() {
|
|
use core::{ops::Deref, any::Any};
|
|
use std::collections::HashSet;
|
|
#[cfg(feature = "multisig")]
|
|
use std::collections::HashMap;
|
|
|
|
use zeroize::Zeroizing;
|
|
use rand_core::OsRng;
|
|
|
|
use curve25519_dalek::constants::ED25519_BASEPOINT_TABLE;
|
|
|
|
#[cfg(feature = "multisig")]
|
|
use transcript::{Transcript, RecommendedTranscript};
|
|
#[cfg(feature = "multisig")]
|
|
use frost::{
|
|
curve::Ed25519,
|
|
tests::{THRESHOLD, key_gen},
|
|
};
|
|
|
|
use monero_serai::{
|
|
random_scalar,
|
|
wallet::{
|
|
address::Network, ViewPair, Scanner, SignableTransaction,
|
|
SignableTransactionBuilder,
|
|
},
|
|
};
|
|
|
|
use runner::{random_address, rpc, mine_until_unlocked};
|
|
|
|
type Builder = SignableTransactionBuilder;
|
|
|
|
// Run each function as both a single signer and as a multisig
|
|
for multisig in [false, true] {
|
|
// Only run the multisig variant if multisig is enabled
|
|
if multisig {
|
|
#[cfg(not(feature = "multisig"))]
|
|
continue;
|
|
}
|
|
|
|
let spend = Zeroizing::new(random_scalar(&mut OsRng));
|
|
#[cfg(feature = "multisig")]
|
|
let keys = key_gen::<_, Ed25519>(&mut OsRng);
|
|
|
|
let spend_pub = if !multisig {
|
|
spend.deref() * &ED25519_BASEPOINT_TABLE
|
|
} else {
|
|
#[cfg(not(feature = "multisig"))]
|
|
panic!("Multisig branch called without the multisig feature");
|
|
#[cfg(feature = "multisig")]
|
|
keys[&1].group_key().0
|
|
};
|
|
|
|
let view = ViewPair::new(spend_pub, random_scalar(&mut OsRng));
|
|
|
|
let rpc = rpc().await;
|
|
|
|
let (addr, miner_tx) = {
|
|
let mut scanner =
|
|
Scanner::from_view(view.clone(), Network::Mainnet, Some(HashSet::new()));
|
|
let addr = scanner.address();
|
|
|
|
// mine 60 blocks to unlock a miner tx
|
|
let start = rpc.get_height().await.unwrap();
|
|
rpc.generate_blocks(&addr.to_string(), 60).await.unwrap();
|
|
|
|
let block = rpc.get_block(start).await.unwrap();
|
|
(
|
|
addr,
|
|
scanner.scan(
|
|
&rpc,
|
|
&block
|
|
).await.unwrap().swap_remove(0).ignore_timelock().swap_remove(0)
|
|
)
|
|
};
|
|
|
|
let builder = SignableTransactionBuilder::new(
|
|
rpc.get_protocol().await.unwrap(),
|
|
rpc.get_fee().await.unwrap(),
|
|
Some(random_address().2),
|
|
);
|
|
|
|
let sign = |tx: SignableTransaction| {
|
|
let rpc = rpc.clone();
|
|
let spend = spend.clone();
|
|
#[cfg(feature = "multisig")]
|
|
let keys = keys.clone();
|
|
async move {
|
|
if !multisig {
|
|
tx.sign(&mut OsRng, &rpc, &spend).await.unwrap()
|
|
} else {
|
|
#[cfg(not(feature = "multisig"))]
|
|
panic!("Multisig branch called without the multisig feature");
|
|
#[cfg(feature = "multisig")]
|
|
{
|
|
let mut machines = HashMap::new();
|
|
for i in 1 ..= THRESHOLD {
|
|
machines.insert(
|
|
i,
|
|
tx
|
|
.clone()
|
|
.multisig(
|
|
&rpc,
|
|
keys[&i].clone(),
|
|
RecommendedTranscript::new(b"Monero Serai Test Transaction"),
|
|
rpc.get_height().await.unwrap() - 10,
|
|
)
|
|
.await
|
|
.unwrap(),
|
|
);
|
|
}
|
|
|
|
frost::tests::sign_without_caching(&mut OsRng, machines, &vec![])
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// TODO: Generate a distinct wallet for each transaction to prevent overlap
|
|
let next_addr = addr;
|
|
|
|
let temp = Box::new({
|
|
let mut builder = builder.clone();
|
|
builder.add_input(miner_tx);
|
|
let (tx, state) = ($first_tx)(rpc.clone(), builder, next_addr).await;
|
|
|
|
let signed = sign(tx).await;
|
|
rpc.publish_transaction(&signed).await.unwrap();
|
|
mine_until_unlocked(&rpc, &random_address().2.to_string(), signed.hash()).await;
|
|
let tx = rpc.get_transaction(signed.hash()).await.unwrap();
|
|
let scanner =
|
|
Scanner::from_view(view.clone(), Network::Mainnet, Some(HashSet::new()));
|
|
($first_checks)(rpc.clone(), tx, scanner, state).await
|
|
});
|
|
#[allow(unused_variables, unused_mut, unused_assignments)]
|
|
let mut carried_state: Box<dyn Any> = temp;
|
|
|
|
$(
|
|
let (tx, state) = ($tx)(
|
|
rpc.clone(),
|
|
builder.clone(),
|
|
next_addr,
|
|
*carried_state.downcast().unwrap()
|
|
).await;
|
|
|
|
let signed = sign(tx).await;
|
|
rpc.publish_transaction(&signed).await.unwrap();
|
|
mine_until_unlocked(&rpc, &random_address().2.to_string(), signed.hash()).await;
|
|
let tx = rpc.get_transaction(signed.hash()).await.unwrap();
|
|
#[allow(unused_assignments)]
|
|
{
|
|
let scanner =
|
|
Scanner::from_view(view.clone(), Network::Mainnet, Some(HashSet::new()));
|
|
carried_state =
|
|
Box::new(($checks)(rpc.clone(), tx, scanner, state).await);
|
|
}
|
|
)*
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|