mirror of
https://github.com/serai-dex/serai.git
synced 2025-03-12 09:26:51 +00:00
Successfully get processor to send a transaction out
Modifies FROST behavior so group_key has the offset applied regardless of if view was called. The unaltered secret_share and verification_shares (as they have differing values depending on the signing set) are no longer publicly accessible.
This commit is contained in:
parent
714ce68deb
commit
27751d8d98
8 changed files with 255 additions and 62 deletions
|
@ -34,6 +34,7 @@ pub enum RpcError {
|
|||
InvalidTransaction([u8; 32])
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Rpc(String);
|
||||
|
||||
fn rpc_hex(value: &str) -> Result<Vec<u8>, RpcError> {
|
||||
|
|
|
@ -75,7 +75,6 @@ impl SendOutput {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Clone, Error, Debug)]
|
||||
pub enum TransactionError {
|
||||
#[error("no inputs")]
|
||||
|
|
|
@ -268,6 +268,7 @@ impl<C: Curve> MultisigKeys<C> {
|
|||
// Enables schemes like Monero's subaddresses which have a per-subaddress offset and then a
|
||||
// one-time-key offset
|
||||
res.offset = Some(offset + res.offset.unwrap_or(C::F::zero()));
|
||||
res.group_key += C::GENERATOR_TABLE * offset;
|
||||
res
|
||||
}
|
||||
|
||||
|
@ -275,7 +276,7 @@ impl<C: Curve> MultisigKeys<C> {
|
|||
self.params
|
||||
}
|
||||
|
||||
pub fn secret_share(&self) -> C::F {
|
||||
fn secret_share(&self) -> C::F {
|
||||
self.secret_share
|
||||
}
|
||||
|
||||
|
@ -283,7 +284,7 @@ impl<C: Curve> MultisigKeys<C> {
|
|||
self.group_key
|
||||
}
|
||||
|
||||
pub fn verification_shares(&self) -> HashMap<u16, C::G> {
|
||||
fn verification_shares(&self) -> HashMap<u16, C::G> {
|
||||
self.verification_shares.clone()
|
||||
}
|
||||
|
||||
|
@ -297,7 +298,7 @@ impl<C: Curve> MultisigKeys<C> {
|
|||
let offset_share = offset * C::F::from(included.len().try_into().unwrap()).invert().unwrap();
|
||||
|
||||
Ok(MultisigView {
|
||||
group_key: self.group_key + (C::GENERATOR_TABLE * offset),
|
||||
group_key: self.group_key,
|
||||
secret_share: secret_share + offset_share,
|
||||
verification_shares: self.verification_shares.iter().map(
|
||||
|(l, share)| (
|
||||
|
|
|
@ -11,6 +11,10 @@ async-trait = "0.1"
|
|||
rand_core = "0.6"
|
||||
thiserror = "1"
|
||||
|
||||
hex = "0.4"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
|
||||
curve25519-dalek = { version = "3", features = ["std"] }
|
||||
blake2 = "0.10"
|
||||
|
||||
|
@ -22,5 +26,7 @@ monero = { version = "0.16", features = ["experimental"] }
|
|||
monero-serai = { path = "../coins/monero", features = ["multisig"] }
|
||||
|
||||
[dev-dependencies]
|
||||
group = "0.12"
|
||||
rand = "0.8"
|
||||
futures = "0.3"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
|
|
|
@ -1,16 +1,12 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use rand_core::{RngCore, CryptoRng};
|
||||
use rand_core::OsRng;
|
||||
|
||||
use curve25519_dalek::{
|
||||
constants::ED25519_BASEPOINT_TABLE,
|
||||
scalar::Scalar,
|
||||
edwards::CompressedEdwardsY
|
||||
};
|
||||
use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, scalar::Scalar};
|
||||
|
||||
use dalek_ff_group as dfg;
|
||||
use frost::MultisigKeys;
|
||||
use frost::{MultisigKeys, sign::StateMachine};
|
||||
|
||||
use monero::{PublicKey, network::Network, util::address::Address};
|
||||
use monero_serai::{
|
||||
|
@ -20,9 +16,15 @@ use monero_serai::{
|
|||
wallet::{SpendableOutput, SignableTransaction as MSignableTransaction}
|
||||
};
|
||||
|
||||
use crate::{Transcript, Output as OutputTrait, CoinError, Coin, view_key};
|
||||
use crate::{
|
||||
Transcript,
|
||||
CoinError, SignError,
|
||||
Network as NetworkTrait,
|
||||
Output as OutputTrait, Coin,
|
||||
view_key
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Output(SpendableOutput);
|
||||
impl OutputTrait for Output {
|
||||
// While we could use (tx, o), using the key ensures we won't be susceptible to the burning bug.
|
||||
|
@ -53,6 +55,7 @@ impl From<SpendableOutput> for Output {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SignableTransaction(
|
||||
Arc<MultisigKeys<Ed25519>>,
|
||||
Transcript,
|
||||
|
@ -60,10 +63,11 @@ pub struct SignableTransaction(
|
|||
MSignableTransaction
|
||||
);
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Monero {
|
||||
rpc: Rpc,
|
||||
view: Scalar,
|
||||
view_pub: CompressedEdwardsY
|
||||
view_pub: PublicKey
|
||||
}
|
||||
|
||||
impl Monero {
|
||||
|
@ -72,7 +76,7 @@ impl Monero {
|
|||
Monero {
|
||||
rpc: Rpc::new(url),
|
||||
view,
|
||||
view_pub: (&view * &ED25519_BASEPOINT_TABLE).compress()
|
||||
view_pub: PublicKey { point: (&view * &ED25519_BASEPOINT_TABLE).compress() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -98,6 +102,10 @@ impl Coin for Monero {
|
|||
const MAX_INPUTS: usize = 128;
|
||||
const MAX_OUTPUTS: usize = 16;
|
||||
|
||||
fn address(&self, key: dfg::EdwardsPoint) -> Self::Address {
|
||||
Address::standard(Network::Mainnet, PublicKey { point: key.compress().0 }, self.view_pub)
|
||||
}
|
||||
|
||||
async fn get_height(&self) -> Result<usize, CoinError> {
|
||||
self.rpc.get_height().await.map_err(|_| CoinError::ConnectionError)
|
||||
}
|
||||
|
@ -129,7 +137,7 @@ impl Coin for Monero {
|
|||
mut inputs: Vec<Output>,
|
||||
payments: &[(Address, u64)]
|
||||
) -> Result<SignableTransaction, CoinError> {
|
||||
let spend = keys.group_key().0.compress();
|
||||
let spend = keys.group_key();
|
||||
Ok(
|
||||
SignableTransaction(
|
||||
keys,
|
||||
|
@ -138,40 +146,86 @@ impl Coin for Monero {
|
|||
MSignableTransaction::new(
|
||||
inputs.drain(..).map(|input| input.0).collect(),
|
||||
payments.to_vec(),
|
||||
Address::standard(
|
||||
Network::Mainnet,
|
||||
PublicKey { point: spend },
|
||||
PublicKey { point: self.view_pub }
|
||||
),
|
||||
100000000
|
||||
self.address(spend),
|
||||
100000000 // TODO
|
||||
).map_err(|_| CoinError::ConnectionError)?
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
async fn attempt_send<R: RngCore + CryptoRng + std::marker::Send>(
|
||||
async fn attempt_send<N: NetworkTrait>(
|
||||
&self,
|
||||
rng: &mut R,
|
||||
network: &mut N,
|
||||
transaction: SignableTransaction,
|
||||
included: &[u16]
|
||||
) -> Result<(Vec<u8>, Vec<<Self::Output as OutputTrait>::Id>), CoinError> {
|
||||
let attempt = transaction.3.clone().multisig(
|
||||
rng,
|
||||
) -> Result<(Vec<u8>, Vec<<Self::Output as OutputTrait>::Id>), SignError> {
|
||||
let mut attempt = transaction.3.clone().multisig(
|
||||
&mut OsRng,
|
||||
&self.rpc,
|
||||
(*transaction.0).clone(),
|
||||
transaction.1.clone(),
|
||||
transaction.2,
|
||||
included.to_vec()
|
||||
).await.map_err(|_| CoinError::ConnectionError)?;
|
||||
).await.map_err(|_| SignError::CoinError(CoinError::ConnectionError))?;
|
||||
|
||||
/*
|
||||
let tx = None;
|
||||
self.rpc.publish_transaction(tx).await.map_err(|_| CoinError::ConnectionError)?;
|
||||
Ok(
|
||||
let commitments = network.round(
|
||||
attempt.preprocess(&mut OsRng).unwrap()
|
||||
).await.map_err(|e| SignError::NetworkError(e))?;
|
||||
let shares = network.round(
|
||||
attempt.sign(commitments, b"").map_err(|e| SignError::FrostError(e))?
|
||||
).await.map_err(|e| SignError::NetworkError(e))?;
|
||||
let tx = attempt.complete(shares).map_err(|e| SignError::FrostError(e))?;
|
||||
|
||||
self.rpc.publish_transaction(
|
||||
&tx
|
||||
).await.map_err(|_| SignError::CoinError(CoinError::ConnectionError))?;
|
||||
|
||||
Ok((
|
||||
tx.hash().to_vec(),
|
||||
tx.outputs.iter().map(|output| output.key.compress().to_bytes().collect())
|
||||
)
|
||||
*/
|
||||
Ok((vec![], vec![]))
|
||||
tx.prefix.outputs.iter().map(|output| output.key.compress().to_bytes()).collect()
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
async fn mine_block(&self, address: Self::Address) {
|
||||
#[derive(serde::Deserialize, Debug)]
|
||||
struct EmptyResponse {}
|
||||
let _: EmptyResponse = self.rpc.rpc_call("json_rpc", Some(serde_json::json!({
|
||||
"method": "generateblocks",
|
||||
"params": {
|
||||
"wallet_address": address.to_string(),
|
||||
"amount_of_blocks": 10
|
||||
},
|
||||
}))).await.unwrap();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
async fn test_send(&self, address: Self::Address) {
|
||||
use group::Group;
|
||||
|
||||
use rand::rngs::OsRng;
|
||||
|
||||
let height = self.get_height().await.unwrap();
|
||||
|
||||
let temp = self.address(dfg::EdwardsPoint::generator());
|
||||
self.mine_block(temp).await;
|
||||
for _ in 0 .. 7 {
|
||||
self.mine_block(temp).await;
|
||||
}
|
||||
|
||||
let outputs = self.rpc
|
||||
.get_block_transactions_possible(height).await.unwrap()
|
||||
.swap_remove(0).scan(self.view, dfg::EdwardsPoint::generator().0).0;
|
||||
|
||||
let amount = outputs[0].commitment.amount;
|
||||
let fee = 1000000000; // TODO
|
||||
let tx = MSignableTransaction::new(
|
||||
outputs,
|
||||
vec![(address, amount - fee)],
|
||||
temp,
|
||||
fee / 2000
|
||||
).unwrap().sign(&mut OsRng, &self.rpc, &Scalar::one()).await.unwrap();
|
||||
self.rpc.publish_transaction(&tx).await.unwrap();
|
||||
self.mine_block(temp).await;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
use std::{marker::Send, sync::Arc};
|
||||
use std::{marker::Send, sync::Arc, collections::HashMap};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use thiserror::Error;
|
||||
use rand_core::{RngCore, CryptoRng};
|
||||
|
||||
use frost::{Curve, MultisigKeys};
|
||||
use frost::{Curve, FrostError, MultisigKeys};
|
||||
|
||||
pub(crate) use monero_serai::frost::Transcript;
|
||||
|
||||
|
@ -14,6 +13,30 @@ mod wallet;
|
|||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
#[derive(Clone, Error, Debug)]
|
||||
pub enum CoinError {
|
||||
#[error("failed to connect to coin daemon")]
|
||||
ConnectionError
|
||||
}
|
||||
|
||||
#[derive(Clone, Error, Debug)]
|
||||
pub enum NetworkError {}
|
||||
|
||||
#[derive(Clone, Error, Debug)]
|
||||
pub enum SignError {
|
||||
#[error("coin had an error {0}")]
|
||||
CoinError(CoinError),
|
||||
#[error("network had an error {0}")]
|
||||
NetworkError(NetworkError),
|
||||
#[error("FROST had an error {0}")]
|
||||
FrostError(FrostError)
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait Network: Send {
|
||||
async fn round(&mut self, data: Vec<u8>) -> Result<HashMap<u16, Vec<u8>>, NetworkError>;
|
||||
}
|
||||
|
||||
pub trait Output: Sized + Clone {
|
||||
type Id: AsRef<[u8]>;
|
||||
|
||||
|
@ -24,12 +47,6 @@ pub trait Output: Sized + Clone {
|
|||
fn deserialize<R: std::io::Read>(reader: &mut R) -> std::io::Result<Self>;
|
||||
}
|
||||
|
||||
#[derive(Clone, Error, Debug)]
|
||||
pub enum CoinError {
|
||||
#[error("failed to connect to coin daemon")]
|
||||
ConnectionError
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait Coin {
|
||||
type Curve: Curve;
|
||||
|
@ -43,7 +60,10 @@ pub trait Coin {
|
|||
const ID: &'static [u8];
|
||||
const CONFIRMATIONS: usize;
|
||||
const MAX_INPUTS: usize;
|
||||
const MAX_OUTPUTS: usize;
|
||||
const MAX_OUTPUTS: usize; // TODO: Decide if this includes change or not
|
||||
|
||||
// Doesn't have to take self, enables some level of caching which is pleasant
|
||||
fn address(&self, key: <Self::Curve as Curve>::G) -> Self::Address;
|
||||
|
||||
async fn get_height(&self) -> Result<usize, CoinError>;
|
||||
async fn get_block(&self, height: usize) -> Result<Self::Block, CoinError>;
|
||||
|
@ -62,12 +82,18 @@ pub trait Coin {
|
|||
payments: &[(Self::Address, u64)]
|
||||
) -> Result<Self::SignableTransaction, CoinError>;
|
||||
|
||||
async fn attempt_send<R: RngCore + CryptoRng + Send>(
|
||||
async fn attempt_send<N: Network>(
|
||||
&self,
|
||||
rng: &mut R,
|
||||
network: &mut N,
|
||||
transaction: Self::SignableTransaction,
|
||||
included: &[u16]
|
||||
) -> Result<(Vec<u8>, Vec<<Self::Output as Output>::Id>), CoinError>;
|
||||
) -> Result<(Vec<u8>, Vec<<Self::Output as Output>::Id>), SignError>;
|
||||
|
||||
#[cfg(test)]
|
||||
async fn mine_block(&self, address: Self::Address);
|
||||
|
||||
#[cfg(test)]
|
||||
async fn test_send(&self, key: Self::Address);
|
||||
}
|
||||
|
||||
// Generate a static view key for a given chain in a globally consistent manner
|
||||
|
|
|
@ -1,16 +1,107 @@
|
|||
use std::sync::Arc;
|
||||
use std::{sync::{Arc, RwLock}, collections::HashMap};
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use rand::rngs::OsRng;
|
||||
|
||||
use crate::{Coin, coins::monero::Monero, wallet::{WalletKeys, MemCoinDb, Wallet}};
|
||||
use group::Group;
|
||||
|
||||
use crate::{
|
||||
NetworkError, Network,
|
||||
Coin, coins::monero::Monero,
|
||||
wallet::{WalletKeys, MemCoinDb, Wallet}
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
struct LocalNetwork {
|
||||
i: u16,
|
||||
size: u16,
|
||||
round: usize,
|
||||
rounds: Arc<RwLock<Vec<HashMap<u16, Vec<u8>>>>>
|
||||
}
|
||||
|
||||
impl LocalNetwork {
|
||||
fn new(size: u16) -> Vec<LocalNetwork> {
|
||||
let rounds = Arc::new(RwLock::new(vec![]));
|
||||
let mut res = vec![];
|
||||
for i in 1 ..= size {
|
||||
res.push(LocalNetwork { i, size, round: 0, rounds: rounds.clone() });
|
||||
}
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Network for LocalNetwork {
|
||||
async fn round(&mut self, data: Vec<u8>) -> Result<HashMap<u16, Vec<u8>>, NetworkError> {
|
||||
{
|
||||
let mut rounds = self.rounds.write().unwrap();
|
||||
if rounds.len() == self.round {
|
||||
rounds.push(HashMap::new());
|
||||
}
|
||||
rounds[self.round].insert(self.i, data);
|
||||
}
|
||||
|
||||
while {
|
||||
let read = self.rounds.try_read().unwrap();
|
||||
read[self.round].len() != usize::from(self.size)
|
||||
} {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
|
||||
let res = self.rounds.try_read().unwrap()[self.round].clone();
|
||||
self.round += 1;
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test() {
|
||||
let monero = Monero::new("http://127.0.0.1:18081".to_string());
|
||||
println!("{}", monero.get_height().await.unwrap());
|
||||
// Mine a block so there's a confirmed height
|
||||
monero.mine_block(monero.address(dalek_ff_group::EdwardsPoint::generator())).await;
|
||||
let height = monero.get_height().await.unwrap();
|
||||
|
||||
let mut networks = LocalNetwork::new(3);
|
||||
|
||||
let mut keys = frost::tests::key_gen::<_, <Monero as Coin>::Curve>(&mut OsRng);
|
||||
let mut wallet = Wallet::new(MemCoinDb::new(), monero);
|
||||
wallet.acknowledge_height(0, 0);
|
||||
wallet.add_keys(&WalletKeys::new(Arc::try_unwrap(keys.remove(&1).take().unwrap()).unwrap(), 0));
|
||||
dbg!(0);
|
||||
let mut wallets = vec![];
|
||||
for i in 1 ..= 3 {
|
||||
let mut wallet = Wallet::new(MemCoinDb::new(), monero.clone());
|
||||
wallet.acknowledge_height(0, height);
|
||||
wallet.add_keys(
|
||||
&WalletKeys::new(Arc::try_unwrap(keys.remove(&i).take().unwrap()).unwrap(), 0)
|
||||
);
|
||||
wallets.push(wallet);
|
||||
}
|
||||
|
||||
// Get the chain to a height where blocks have sufficient confirmations
|
||||
while (height + Monero::CONFIRMATIONS) > monero.get_height().await.unwrap() {
|
||||
monero.mine_block(monero.address(dalek_ff_group::EdwardsPoint::generator())).await;
|
||||
}
|
||||
|
||||
for wallet in wallets.iter_mut() {
|
||||
// Poll to activate the keys
|
||||
wallet.poll().await.unwrap();
|
||||
}
|
||||
|
||||
monero.test_send(wallets[0].address()).await;
|
||||
|
||||
let mut futures = vec![];
|
||||
for (i, network) in networks.iter_mut().enumerate() {
|
||||
let wallet = &mut wallets[i];
|
||||
wallet.poll().await.unwrap();
|
||||
|
||||
let height = monero.get_height().await.unwrap();
|
||||
wallet.acknowledge_height(1, height - 10);
|
||||
let signable = wallet.prepare_sends(
|
||||
1,
|
||||
vec![(wallet.address(), 10000000000)]
|
||||
).await.unwrap().1.swap_remove(0);
|
||||
futures.push(monero.attempt_send(network, signable, &[1, 2, 3]));
|
||||
}
|
||||
println!(
|
||||
"{:?}",
|
||||
hex::encode(futures::future::join_all(futures).await.swap_remove(0).unwrap().0)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -121,6 +121,9 @@ impl<D: CoinDb, C: Coin> Wallet<D, C> {
|
|||
pub fn scanned_height(&self) -> usize { self.db.scanned_height() }
|
||||
pub fn acknowledge_height(&mut self, canonical: usize, height: usize) {
|
||||
self.db.acknowledge_height(canonical, height);
|
||||
if height > self.db.scanned_height() {
|
||||
self.db.scanned_to_height(height);
|
||||
}
|
||||
}
|
||||
pub fn acknowledged_height(&self, canonical: usize) -> usize {
|
||||
self.db.acknowledged_height(canonical)
|
||||
|
@ -131,17 +134,25 @@ impl<D: CoinDb, C: Coin> Wallet<D, C> {
|
|||
self.pending.push((self.acknowledged_height(keys.creation_height), keys.bind(C::ID)));
|
||||
}
|
||||
|
||||
pub fn address(&self) -> C::Address {
|
||||
self.coin.address(self.keys[self.keys.len() - 1].0.group_key())
|
||||
}
|
||||
|
||||
pub async fn poll(&mut self) -> Result<(), CoinError> {
|
||||
let confirmed_height = self.coin.get_height().await? - C::CONFIRMATIONS;
|
||||
for height in self.scanned_height() .. confirmed_height {
|
||||
if self.coin.get_height().await? < C::CONFIRMATIONS {
|
||||
return Ok(());
|
||||
}
|
||||
let confirmed_block = self.coin.get_height().await? - C::CONFIRMATIONS;
|
||||
|
||||
for b in self.scanned_height() ..= confirmed_block {
|
||||
// If any keys activated at this height, shift them over
|
||||
{
|
||||
let mut k = 0;
|
||||
while k < self.pending.len() {
|
||||
// TODO
|
||||
//if height < self.pending[k].0 {
|
||||
//} else if height == self.pending[k].0 {
|
||||
if height <= self.pending[k].0 {
|
||||
//if b < self.pending[k].0 {
|
||||
//} else if b == self.pending[k].0 {
|
||||
if b <= self.pending[k].0 {
|
||||
self.keys.push((Arc::new(self.pending.swap_remove(k).1), vec![]));
|
||||
} else {
|
||||
k += 1;
|
||||
|
@ -149,7 +160,7 @@ impl<D: CoinDb, C: Coin> Wallet<D, C> {
|
|||
}
|
||||
}
|
||||
|
||||
let block = self.coin.get_block(height).await?;
|
||||
let block = self.coin.get_block(b).await?;
|
||||
for (keys, outputs) in self.keys.iter_mut() {
|
||||
outputs.extend(
|
||||
self.coin.get_outputs(&block, keys.group_key()).await.iter().cloned().filter(
|
||||
|
@ -157,7 +168,11 @@ impl<D: CoinDb, C: Coin> Wallet<D, C> {
|
|||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Blocks are zero-indexed while heights aren't
|
||||
self.db.scanned_to_height(b + 1);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue