mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-08 20:29:23 +00:00
Acknowledge Substrate's ordering and move to a multi-key wallet setup
This commit is contained in:
@@ -21,4 +21,5 @@ monero = { version = "0.16", features = ["experimental"] }
|
|||||||
monero-serai = { path = "../coins/monero", features = ["multisig"] }
|
monero-serai = { path = "../coins/monero", features = ["multisig"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
rand = "0.8"
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
|||||||
@@ -1,24 +1,30 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use rand_core::{RngCore, CryptoRng};
|
use rand_core::{RngCore, CryptoRng};
|
||||||
|
|
||||||
use curve25519_dalek::scalar::Scalar;
|
use curve25519_dalek::{scalar::Scalar, edwards::CompressedEdwardsY};
|
||||||
|
|
||||||
use dalek_ff_group as dfg;
|
use dalek_ff_group as dfg;
|
||||||
use frost::MultisigKeys;
|
use frost::MultisigKeys;
|
||||||
|
|
||||||
use monero::util::address::Address;
|
use monero::util::address::Address;
|
||||||
use monero_serai::{frost::Ed25519, rpc::Rpc, wallet::{SpendableOutput, SignableTransaction}};
|
use monero_serai::{
|
||||||
|
frost::Ed25519,
|
||||||
|
transaction::Transaction,
|
||||||
|
rpc::Rpc,
|
||||||
|
wallet::{SpendableOutput, SignableTransaction}
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{Output as OutputTrait, CoinError, Coin, view_key};
|
use crate::{Output as OutputTrait, CoinError, Coin, view_key};
|
||||||
|
|
||||||
pub struct Output(SpendableOutput);
|
pub struct Output(SpendableOutput);
|
||||||
impl OutputTrait for Output {
|
impl OutputTrait for Output {
|
||||||
// If Monero ever does support more than 255 outputs at once, which it could, this u8 could be a
|
// While we could use (tx, o), using the key ensures we won't be susceptible to the burning bug.
|
||||||
// u16 which serializes as little endian, dropping the last byte if empty, without conflict
|
// While the Monero library offers a variant which allows senders to ensure their TXs have unique
|
||||||
type Id = ([u8; 32], u8);
|
// output keys, Serai can still be targeted using the classic burning bug
|
||||||
|
type Id = CompressedEdwardsY;
|
||||||
|
|
||||||
fn id(&self) -> Self::Id {
|
fn id(&self) -> Self::Id {
|
||||||
(self.0.tx, self.0.o.try_into().unwrap())
|
self.0.key.compress()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn amount(&self) -> u64 {
|
fn amount(&self) -> u64 {
|
||||||
@@ -59,34 +65,32 @@ impl Coin for Monero {
|
|||||||
type Curve = Ed25519;
|
type Curve = Ed25519;
|
||||||
|
|
||||||
type Output = Output;
|
type Output = Output;
|
||||||
|
type Block = Vec<Transaction>;
|
||||||
type SignableTransaction = SignableTransaction;
|
type SignableTransaction = SignableTransaction;
|
||||||
|
|
||||||
type Address = Address;
|
type Address = Address;
|
||||||
|
|
||||||
fn id() -> &'static [u8] { b"Monero" }
|
fn id() -> &'static [u8] { b"Monero" }
|
||||||
async fn confirmations() -> usize { 10 }
|
fn confirmations() -> usize { 10 }
|
||||||
// Testnet TX bb4d188a4c571f2f0de70dca9d475abc19078c10ffa8def26dd4f63ce1bcfd79 uses 146 inputs
|
// Testnet TX bb4d188a4c571f2f0de70dca9d475abc19078c10ffa8def26dd4f63ce1bcfd79 uses 146 inputs
|
||||||
// while using less than 100kb of space, albeit with just 2 outputs (though outputs share a BP)
|
// while using less than 100kb of space, albeit with just 2 outputs (though outputs share a BP)
|
||||||
// The TX size limit is half the contextual median block weight, where said weight is >= 300,000
|
// The TX size limit is half the contextual median block weight, where said weight is >= 300,000
|
||||||
// This means any TX which fits into 150kb will be accepted by Monero
|
// This means any TX which fits into 150kb will be accepted by Monero
|
||||||
// 128, even with 16 outputs, should fit into 100kb. Further efficiency by 192 may be viable
|
// 128, even with 16 outputs, should fit into 100kb. Further efficiency by 192 may be viable
|
||||||
// TODO: Get hard numbers and tune
|
// TODO: Get hard numbers and tune
|
||||||
async fn max_inputs() -> usize { 128 }
|
fn max_inputs() -> usize { 128 }
|
||||||
async fn max_outputs() -> usize { 16 }
|
fn max_outputs() -> usize { 16 }
|
||||||
|
|
||||||
async fn get_height(&self) -> Result<usize, CoinError> {
|
async fn get_height(&self) -> Result<usize, CoinError> {
|
||||||
self.rpc.get_height().await.map_err(|_| CoinError::ConnectionError)
|
self.rpc.get_height().await.map_err(|_| CoinError::ConnectionError)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_outputs_in_block(
|
async fn get_block(&self, height: usize) -> Result<Self::Block, CoinError> {
|
||||||
&self,
|
self.rpc.get_block_transactions_possible(height).await.map_err(|_| CoinError::ConnectionError)
|
||||||
height: usize,
|
}
|
||||||
key: dfg::EdwardsPoint
|
|
||||||
) -> Result<Vec<Self::Output>, CoinError> {
|
async fn get_outputs(&self, block: &Self::Block, key: dfg::EdwardsPoint) -> Vec<Self::Output> {
|
||||||
Ok(
|
block.iter().flat_map(|tx| tx.scan(self.view, key.0)).map(Output::from).collect()
|
||||||
self.rpc.get_block_transactions_possible(height).await.map_err(|_| CoinError::ConnectionError)?
|
|
||||||
.iter().flat_map(|tx| tx.scan(self.view, key.0)).map(Output::from).collect()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn prepare_send<R: RngCore + CryptoRng>(
|
async fn prepare_send<R: RngCore + CryptoRng>(
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ mod wallet;
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
trait Output: Sized {
|
pub trait Output: Sized {
|
||||||
type Id;
|
type Id;
|
||||||
|
|
||||||
fn id(&self) -> Self::Id;
|
fn id(&self) -> Self::Id;
|
||||||
@@ -25,31 +25,33 @@ trait Output: Sized {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Error, Debug)]
|
#[derive(Clone, Error, Debug)]
|
||||||
enum CoinError {
|
pub enum CoinError {
|
||||||
#[error("failed to connect to coin daemon")]
|
#[error("failed to connect to coin daemon")]
|
||||||
ConnectionError
|
ConnectionError
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
trait Coin {
|
pub trait Coin {
|
||||||
type Curve: Curve;
|
type Curve: Curve;
|
||||||
|
|
||||||
type Output: Output;
|
type Output: Output;
|
||||||
|
type Block;
|
||||||
type SignableTransaction;
|
type SignableTransaction;
|
||||||
|
|
||||||
type Address: Send;
|
type Address: Send;
|
||||||
|
|
||||||
fn id() -> &'static [u8];
|
fn id() -> &'static [u8];
|
||||||
async fn confirmations() -> usize;
|
fn confirmations() -> usize;
|
||||||
async fn max_inputs() -> usize;
|
fn max_inputs() -> usize;
|
||||||
async fn max_outputs() -> usize;
|
fn max_outputs() -> usize;
|
||||||
|
|
||||||
async fn get_height(&self) -> Result<usize, CoinError>;
|
async fn get_height(&self) -> Result<usize, CoinError>;
|
||||||
async fn get_outputs_in_block(
|
async fn get_block(&self, height: usize) -> Result<Self::Block, CoinError>;
|
||||||
|
async fn get_outputs(
|
||||||
&self,
|
&self,
|
||||||
height: usize,
|
block: &Self::Block,
|
||||||
key: <Self::Curve as Curve>::G
|
key: <Self::Curve as Curve>::G
|
||||||
) -> Result<Vec<Self::Output>, CoinError>;
|
) -> Vec<Self::Output>;
|
||||||
|
|
||||||
async fn prepare_send<R: RngCore + CryptoRng>(
|
async fn prepare_send<R: RngCore + CryptoRng>(
|
||||||
&self,
|
&self,
|
||||||
@@ -73,6 +75,6 @@ trait Coin {
|
|||||||
// Takes an index, k, for more modern privacy protocols which use multiple view keys
|
// Takes an index, k, for more modern privacy protocols which use multiple view keys
|
||||||
// Doesn't run Curve::hash_to_F, instead returning the hash object, due to hash_to_F being a FROST
|
// Doesn't run Curve::hash_to_F, instead returning the hash object, due to hash_to_F being a FROST
|
||||||
// definition instead of a wide reduction from a hash object
|
// definition instead of a wide reduction from a hash object
|
||||||
fn view_key<C: Coin>(k: u64) -> Blake2b512 {
|
pub fn view_key<C: Coin>(k: u64) -> Blake2b512 {
|
||||||
Blake2b512::new().chain(b"Serai DEX View Key").chain(C::id()).chain(k.to_le_bytes())
|
Blake2b512::new().chain(b"Serai DEX View Key").chain(C::id()).chain(k.to_le_bytes())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,16 @@
|
|||||||
use crate::{Coin, coins::monero::Monero};
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use rand::rngs::OsRng;
|
||||||
|
|
||||||
|
use crate::{Coin, coins::monero::Monero, wallet::{WalletKeys, Wallet}};
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test() {
|
async fn test() {
|
||||||
println!("{}", Monero::new("http://127.0.0.1:18081".to_string()).get_height().await.unwrap());
|
let monero = Monero::new("http://127.0.0.1:18081".to_string());
|
||||||
|
println!("{}", monero.get_height().await.unwrap());
|
||||||
|
let mut keys = frost::tests::key_gen::<_, <Monero as Coin>::Curve>(&mut OsRng);
|
||||||
|
let mut wallet = Wallet::new(monero);
|
||||||
|
wallet.acknowledge_height(0, 0);
|
||||||
|
wallet.add_keys(&WalletKeys::new(Rc::try_unwrap(keys.remove(&1).take().unwrap()).unwrap(), 0));
|
||||||
|
dbg!(0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +1,102 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use frost::{Curve, MultisigKeys};
|
use frost::{Curve, MultisigKeys};
|
||||||
|
|
||||||
use crate::Coin;
|
use crate::{CoinError, Coin};
|
||||||
|
|
||||||
struct Wallet<C: Coin> {
|
pub struct WalletKeys<C: Curve> {
|
||||||
keys: MultisigKeys<C::Curve>,
|
keys: MultisigKeys<C>,
|
||||||
|
creation_height: usize
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C: Curve> WalletKeys<C> {
|
||||||
|
pub fn new(keys: MultisigKeys<C>, creation_height: usize) -> WalletKeys<C> {
|
||||||
|
WalletKeys { keys, creation_height }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bind this key to a specific network by applying an additive offset
|
||||||
|
// While it would be fine to just C::id(), including the group key creates distinct
|
||||||
|
// offsets instead of static offsets. Under a statically offset system, a BTC key could
|
||||||
|
// have X subtracted to find the potential group key, and then have Y added to find the
|
||||||
|
// potential ETH group key. While this shouldn't be an issue, as this isn't a private
|
||||||
|
// system, there are potentially other benefits to binding this to a specific group key
|
||||||
|
// It's no longer possible to influence group key gen to key cancel without breaking the hash
|
||||||
|
// function, although that degree of influence means key gen is broken already
|
||||||
|
fn bind(&self, chain: &[u8]) -> MultisigKeys<C> {
|
||||||
|
self.keys.offset(
|
||||||
|
C::hash_to_F(
|
||||||
|
&[
|
||||||
|
b"Serai Processor Wallet",
|
||||||
|
chain,
|
||||||
|
&C::G_to_bytes(&self.keys.group_key())
|
||||||
|
].concat()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CoinDb {
|
||||||
|
// Height this coin has been scanned to
|
||||||
|
scanned_height: usize,
|
||||||
|
// Acknowledged height for a given canonical height
|
||||||
|
acknowledged_heights: HashMap<usize, usize>
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Wallet<C: Coin> {
|
||||||
|
db: CoinDb,
|
||||||
|
coin: C,
|
||||||
|
keys: Vec<MultisigKeys<C::Curve>>,
|
||||||
|
pending: Vec<(usize, MultisigKeys<C::Curve>)>,
|
||||||
outputs: Vec<C::Output>
|
outputs: Vec<C::Output>
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<C: Coin> Wallet<C> {
|
impl<C: Coin> Wallet<C> {
|
||||||
fn new(keys: &MultisigKeys<C::Curve>) -> Wallet<C> {
|
pub fn new(coin: C) -> Wallet<C> {
|
||||||
Wallet {
|
Wallet {
|
||||||
keys: keys.offset(
|
db: CoinDb {
|
||||||
C::Curve::hash_to_F(
|
scanned_height: 0,
|
||||||
// Use distinct keys on each network by applying an additive offset
|
acknowledged_heights: HashMap::new(),
|
||||||
// While it would be fine to just C::id(), including the group key creates distinct
|
},
|
||||||
// offsets instead of static offsets. Under a statically offset system, a BTC key could
|
|
||||||
// have X subtracted to find the potential group key, and then have Y added to find the
|
|
||||||
// potential BCH group key. While this shouldn't be an issue, as this isn't a private
|
|
||||||
// system, there are potentially other benefits to binding this to a specific group key
|
|
||||||
&[b"Serai Processor Wallet", C::id(), &C::Curve::G_to_bytes(&keys.group_key())].concat()
|
|
||||||
)
|
|
||||||
),
|
|
||||||
|
|
||||||
|
coin,
|
||||||
|
|
||||||
|
keys: vec![],
|
||||||
|
pending: vec![],
|
||||||
outputs: vec![]
|
outputs: vec![]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn poll() { todo!() }
|
pub fn scanned_height(&self) -> usize { self.db.scanned_height }
|
||||||
|
pub fn acknowledge_height(&mut self, canonical: usize, height: usize) {
|
||||||
|
debug_assert!(!self.db.acknowledged_heights.contains_key(&canonical));
|
||||||
|
self.db.acknowledged_heights.insert(canonical, height);
|
||||||
|
}
|
||||||
|
pub fn acknowledged_height(&self, canonical: usize) -> usize {
|
||||||
|
self.db.acknowledged_heights[&canonical]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_keys(&mut self, keys: &WalletKeys<C::Curve>) {
|
||||||
|
// Doesn't use +1 as this is height, not block index, and poll moves by block index
|
||||||
|
self.pending.push((self.acknowledged_height(keys.creation_height), keys.bind(C::id())));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn poll(&mut self) -> Result<(), CoinError> {
|
||||||
|
let confirmed_height = self.coin.get_height().await? - C::confirmations();
|
||||||
|
for h in self.scanned_height() .. confirmed_height {
|
||||||
|
let mut k = 0;
|
||||||
|
while k < self.pending.len() {
|
||||||
|
if h == self.pending[k].0 {
|
||||||
|
self.keys.push(self.pending.swap_remove(k).1);
|
||||||
|
} else {
|
||||||
|
k += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let block = self.coin.get_block(h).await?;
|
||||||
|
for keys in &self.keys {
|
||||||
|
let outputs = self.coin.get_outputs(&block, keys.group_key());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user