Implement Guaranteed Addresses

Closes https://github.com/serai-dex/serai/issues/27.

monero-rs is now solely used for Extra encoding.
This commit is contained in:
Luke Parker 2022-06-28 00:01:20 -04:00
parent 7b70baaa96
commit 7c86e4593a
No known key found for this signature in database
GPG key ID: F9F1386DB1E119B6
12 changed files with 311 additions and 117 deletions

View file

@ -7,6 +7,7 @@ authors = ["Luke Parker <lukeparker5132@gmail.com>"]
edition = "2021"
[dependencies]
hex-literal = "0.3"
lazy_static = "1"
thiserror = "1"
@ -26,6 +27,7 @@ dalek-ff-group = { path = "../../crypto/dalek-ff-group", optional = true }
transcript = { package = "flexible-transcript", path = "../../crypto/transcript", features = ["recommended"], optional = true }
frost = { package = "modular-frost", path = "../../crypto/frost", features = ["ed25519"], optional = true }
base58-monero = "1"
monero = "0.16"
hex = "0.4"

View file

@ -0,0 +1,45 @@
use hex_literal::hex;
use crate::wallet::address::{Network, AddressType, Address};
const SPEND: [u8; 32] = hex!("f8631661f6ab4e6fda310c797330d86e23a682f20d5bc8cc27b18051191f16d7");
const VIEW: [u8; 32] = hex!("4a1535063ad1fee2dabbf909d4fd9a873e29541b401f0944754e17c9a41820ce");
const STANDARD: &'static str = "4B33mFPMq6mKi7Eiyd5XuyKRVMGVZz1Rqb9ZTyGApXW5d1aT7UBDZ89ewmnWFkzJ5wPd2SFbn313vCT8a4E2Qf4KQH4pNey";
const PAYMENT_ID: [u8; 8] = hex!("b8963a57855cf73f");
const INTEGRATED: &'static str = "4Ljin4CrSNHKi7Eiyd5XuyKRVMGVZz1Rqb9ZTyGApXW5d1aT7UBDZ89ewmnWFkzJ5wPd2SFbn313vCT8a4E2Qf4KbaTH6MnpXSn88oBX35";
const SUB_SPEND: [u8; 32] = hex!("fe358188b528335ad1cfdc24a22a23988d742c882b6f19a602892eaab3c1b62b");
const SUB_VIEW: [u8; 32] = hex!("9bc2b464de90d058468522098d5610c5019c45fd1711a9517db1eea7794f5470");
const SUBADDRESS: &'static str = "8C5zHM5ud8nGC4hC2ULiBLSWx9infi8JUUmWEat4fcTf8J4H38iWYVdFmPCA9UmfLTZxD43RsyKnGEdZkoGij6csDeUnbEB";
#[test]
fn standard_address() {
let addr = Address::from_str(STANDARD, Network::Mainnet).unwrap();
assert_eq!(addr.meta.network, Network::Mainnet);
assert_eq!(addr.meta.kind, AddressType::Standard);
assert_eq!(addr.meta.guaranteed, false);
assert_eq!(addr.spend.compress().to_bytes(), SPEND);
assert_eq!(addr.view.compress().to_bytes(), VIEW);
}
#[test]
fn integrated_address() {
let addr = Address::from_str(INTEGRATED, Network::Mainnet).unwrap();
assert_eq!(addr.meta.network, Network::Mainnet);
assert_eq!(addr.meta.kind, AddressType::Integrated(PAYMENT_ID));
assert_eq!(addr.meta.guaranteed, false);
assert_eq!(addr.spend.compress().to_bytes(), SPEND);
assert_eq!(addr.view.compress().to_bytes(), VIEW);
}
#[test]
fn subaddress() {
let addr = Address::from_str(SUBADDRESS, Network::Mainnet).unwrap();
assert_eq!(addr.meta.network, Network::Mainnet);
assert_eq!(addr.meta.kind, AddressType::Subaddress);
assert_eq!(addr.meta.guaranteed, false);
assert_eq!(addr.spend.compress().to_bytes(), SUB_SPEND);
assert_eq!(addr.view.compress().to_bytes(), SUB_VIEW);
}

View file

@ -1 +1,2 @@
mod clsag;
mod address;

View file

@ -0,0 +1,152 @@
use std::string::ToString;
use thiserror::Error;
use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, edwards::{EdwardsPoint, CompressedEdwardsY}};
use base58_monero::base58::{encode_check, decode_check};
use crate::wallet::ViewPair;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Network {
Mainnet,
Testnet,
Stagenet
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum AddressType {
Standard,
Integrated([u8; 8]),
Subaddress
}
impl AddressType {
fn network_bytes(network: Network) -> (u8, u8, u8) {
match network {
Network::Mainnet => (18, 19, 42),
Network::Testnet => (53, 54, 63),
Network::Stagenet => (24, 25, 36)
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct AddressMeta {
pub network: Network,
pub kind: AddressType,
pub guaranteed: bool
}
#[derive(Clone, Error, Debug)]
pub enum AddressError {
#[error("invalid address byte")]
InvalidByte,
#[error("invalid address encoding")]
InvalidEncoding,
#[error("invalid length")]
InvalidLength,
#[error("different network than expected")]
DifferentNetwork,
#[error("invalid key")]
InvalidKey
}
impl AddressMeta {
fn to_byte(&self) -> u8 {
let bytes = AddressType::network_bytes(self.network);
let byte = match self.kind {
AddressType::Standard => bytes.0,
AddressType::Integrated(_) => bytes.1,
AddressType::Subaddress => bytes.2
};
byte | (if self.guaranteed { 1 << 7 } else { 0 })
}
// Returns an incomplete type in the case of Integrated addresses
fn from_byte(byte: u8) -> Result<AddressMeta, AddressError> {
let actual = byte & 0b01111111;
let guaranteed = (byte >> 7) == 1;
let mut meta = None;
for network in [Network::Mainnet, Network::Testnet, Network::Stagenet] {
let (standard, integrated, subaddress) = AddressType::network_bytes(network);
if let Some(kind) = match actual {
_ if actual == standard => Some(AddressType::Standard),
_ if actual == integrated => Some(AddressType::Integrated([0; 8])),
_ if actual == subaddress => Some(AddressType::Subaddress),
_ => None
} {
meta = Some(AddressMeta { network, kind, guaranteed });
break;
}
}
meta.ok_or(AddressError::InvalidByte)
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Address {
pub meta: AddressMeta,
pub spend: EdwardsPoint,
pub view: EdwardsPoint
}
impl ViewPair {
pub fn address(&self, network: Network, kind: AddressType, guaranteed: bool) -> Address {
Address {
meta: AddressMeta {
network,
kind,
guaranteed
},
spend: self.spend,
view: &self.view * &ED25519_BASEPOINT_TABLE
}
}
}
impl ToString for Address {
fn to_string(&self) -> String {
let mut data = vec![self.meta.to_byte()];
data.extend(self.spend.compress().to_bytes());
data.extend(self.view.compress().to_bytes());
if let AddressType::Integrated(id) = self.meta.kind {
data.extend(id);
}
encode_check(&data).unwrap()
}
}
impl Address {
pub fn from_str(s: &str, network: Network) -> Result<Self, AddressError> {
let raw = decode_check(s).map_err(|_| AddressError::InvalidEncoding)?;
if raw.len() == 1 {
Err(AddressError::InvalidLength)?;
}
let mut meta = AddressMeta::from_byte(raw[0])?;
if meta.network != network {
Err(AddressError::DifferentNetwork)?;
}
let len = match meta.kind {
AddressType::Standard | AddressType::Subaddress => 65,
AddressType::Integrated(_) => 73
};
if raw.len() != len {
Err(AddressError::InvalidLength)?;
}
let spend = CompressedEdwardsY(raw[1 .. 33].try_into().unwrap()).decompress().ok_or(AddressError::InvalidKey)?;
let view = CompressedEdwardsY(raw[33 .. 65].try_into().unwrap()).decompress().ok_or(AddressError::InvalidKey)?;
if let AddressType::Integrated(ref mut payment_id) = meta.kind {
payment_id.copy_from_slice(&raw[65 .. 73]);
}
Ok(Address { meta, spend, view })
}
}

View file

@ -6,6 +6,8 @@ use crate::{
transaction::Input
};
pub mod address;
mod scan;
pub use scan::SpendableOutput;
@ -23,7 +25,7 @@ fn key_image_sort(x: &EdwardsPoint, y: &EdwardsPoint) -> std::cmp::Ordering {
// https://github.com/monero-project/research-lab/issues/103
pub(crate) fn uniqueness(inputs: &[Input]) -> [u8; 32] {
let mut u = b"domain_separator".to_vec();
let mut u = b"uniqueness".to_vec();
for input in inputs {
match input {
// If Gen, this should be the only input, making this loop somewhat pointless
@ -63,3 +65,9 @@ pub(crate) fn commitment_mask(shared_key: Scalar) -> Scalar {
mask.extend(shared_key.to_bytes());
hash_to_scalar(&mask)
}
#[derive(Clone, Copy)]
pub struct ViewPair {
pub spend: EdwardsPoint,
pub view: Scalar
}

View file

@ -12,7 +12,7 @@ use crate::{
Commitment,
serialize::{write_varint, read_32, read_scalar, read_point},
transaction::{Timelock, Transaction},
wallet::{uniqueness, shared_key, amount_decryption, commitment_mask}
wallet::{ViewPair, uniqueness, shared_key, amount_decryption, commitment_mask}
};
#[derive(Clone, PartialEq, Debug)]
@ -55,8 +55,8 @@ impl SpendableOutput {
impl Transaction {
pub fn scan(
&self,
view: Scalar,
spend: EdwardsPoint
view: ViewPair,
guaranteed: bool
) -> (Vec<SpendableOutput>, Timelock) {
let mut extra = vec![];
write_varint(&u64::try_from(self.prefix.extra.len()).unwrap(), &mut extra).unwrap();
@ -82,52 +82,53 @@ impl Transaction {
for (o, output) in self.prefix.outputs.iter().enumerate() {
// TODO: This may be replaceable by pubkeys[o]
for pubkey in &pubkeys {
let key_offset = shared_key(
Some(uniqueness(&self.prefix.inputs)).filter(|_| guaranteed),
view.view,
pubkey,
o
);
// P - shared == spend
if (output.key - (&key_offset * &ED25519_BASEPOINT_TABLE)) != view.spend {
continue;
}
// Since we've found an output to us, get its amount
let mut commitment = Commitment::zero();
// P - shared == spend
let matches = |shared_key| (output.key - (&shared_key * &ED25519_BASEPOINT_TABLE)) == spend;
let test = |shared_key| Some(shared_key).filter(|shared_key| matches(*shared_key));
// Miner transaction
if output.amount != 0 {
commitment.amount = output.amount;
// Regular transaction
} else {
let amount = match self.rct_signatures.base.ecdh_info.get(o) {
Some(amount) => amount_decryption(*amount, key_offset),
// This should never happen, yet it may be possible with miner transactions?
// Using get just decreases the possibility of a panic and lets us move on in that case
None => break
};
// Get the traditional shared key and unique shared key, testing if either matches for this output
let traditional = test(shared_key(None, view, pubkey, o));
let unique = test(shared_key(Some(uniqueness(&self.prefix.inputs)), view, pubkey, o));
// If either matches, grab it and decode the amount
if let Some(key_offset) = traditional.or(unique) {
// Miner transaction
if output.amount != 0 {
commitment.amount = output.amount;
// Regular transaction
} else {
let amount = match self.rct_signatures.base.ecdh_info.get(o) {
Some(amount) => amount_decryption(*amount, key_offset),
// This should never happen, yet it may be possible with miner transactions?
// Using get just decreases the possibility of a panic and lets us move on in that case
None => continue
};
// Rebuild the commitment to verify it
commitment = Commitment::new(commitment_mask(key_offset), amount);
// If this is a malicious commitment, move to the next output
// Any other R value will calculate to a different spend key and are therefore ignorable
if Some(&commitment.calculate()) != self.rct_signatures.base.commitments.get(o) {
break;
}
// Rebuild the commitment to verify it
commitment = Commitment::new(commitment_mask(key_offset), amount);
// If this is a malicious commitment, move to the next output
// Any other R value will calculate to a different spend key and are therefore ignorable
if Some(&commitment.calculate()) != self.rct_signatures.base.commitments.get(o) {
break;
}
if commitment.amount != 0 {
res.push(SpendableOutput {
tx: self.hash(),
o: o.try_into().unwrap(),
key: output.key,
key_offset,
commitment
});
}
// Break to prevent public keys from being included multiple times, triggering multiple
// inclusions of the same output
break;
}
if commitment.amount != 0 {
res.push(SpendableOutput {
tx: self.hash(),
o: o.try_into().unwrap(),
key: output.key,
key_offset,
commitment
});
}
// Break to prevent public keys from being included multiple times, triggering multiple
// inclusions of the same output
break;
}
}

View file

@ -9,11 +9,7 @@ use curve25519_dalek::{
edwards::EdwardsPoint
};
use monero::{
consensus::Encodable,
util::{key::PublicKey, address::{AddressType, Address}},
blockdata::transaction::SubField
};
use monero::{consensus::Encodable, PublicKey, blockdata::transaction::SubField};
#[cfg(feature = "multisig")]
use frost::FrostError;
@ -29,7 +25,10 @@ use crate::{
},
transaction::{Input, Output, Timelock, TransactionPrefix, Transaction},
rpc::{Rpc, RpcError},
wallet::{SpendableOutput, Decoys, key_image_sort, uniqueness, shared_key, commitment_mask, amount_encryption}
wallet::{
address::{AddressType, Address}, SpendableOutput, Decoys,
key_image_sort, uniqueness, shared_key, commitment_mask, amount_encryption
}
};
#[cfg(feature = "multisig")]
use crate::frost::MultisigError;
@ -52,23 +51,23 @@ impl SendOutput {
fn new<R: RngCore + CryptoRng>(
rng: &mut R,
unique: [u8; 32],
output: (Address, u64, bool),
output: (Address, u64),
o: usize
) -> SendOutput {
let r = random_scalar(rng);
let shared_key = shared_key(
Some(unique).filter(|_| output.2),
Some(unique).filter(|_| output.0.meta.guaranteed),
r,
&output.0.public_view.point.decompress().expect("SendOutput::new requires valid addresses"),
&output.0.view,
o
);
let spend = output.0.public_spend.point.decompress().expect("SendOutput::new requires valid addresses");
let spend = output.0.spend;
SendOutput {
R: match output.0.addr_type {
R: match output.0.meta.kind {
AddressType::Standard => &r * &ED25519_BASEPOINT_TABLE,
AddressType::SubAddress => &r * spend,
AddressType::Integrated(_) => panic!("SendOutput::new doesn't support Integrated addresses")
AddressType::Integrated(_) => unimplemented!("SendOutput::new doesn't support Integrated addresses"),
AddressType::Subaddress => &r * spend
},
dest: ((&shared_key * &ED25519_BASEPOINT_TABLE) + spend),
commitment: Commitment::new(commitment_mask(shared_key), output.1),
@ -169,7 +168,7 @@ impl Fee {
#[derive(Clone, PartialEq, Debug)]
pub struct SignableTransaction {
inputs: Vec<SpendableOutput>,
payments: Vec<(Address, u64, bool)>,
payments: Vec<(Address, u64)>,
outputs: Vec<SendOutput>,
fee: u64
}
@ -177,23 +176,16 @@ pub struct SignableTransaction {
impl SignableTransaction {
pub fn new(
inputs: Vec<SpendableOutput>,
payments: Vec<(Address, u64)>,
mut payments: Vec<(Address, u64)>,
change_address: Option<Address>,
fee_rate: Fee
) -> Result<SignableTransaction, TransactionError> {
// Make sure all addresses are valid
let test = |addr: Address| {
if !(
addr.public_view.point.decompress().is_some() &&
addr.public_spend.point.decompress().is_some()
) {
Err(TransactionError::InvalidAddress)?;
}
match addr.addr_type {
match addr.meta.kind {
AddressType::Standard => Ok(()),
AddressType::Integrated(..) => Err(TransactionError::InvalidAddress),
AddressType::SubAddress => Ok(())
AddressType::Subaddress => Ok(())
}
};
@ -250,11 +242,8 @@ impl SignableTransaction {
Err(TransactionError::TooManyOutputs)?;
}
let mut payments = payments.iter().map(|(address, amount)| (*address, *amount, false)).collect::<Vec<_>>();
if change {
// Always use a unique key image for the change output
// TODO: Make this a config option
payments.push((change_address.unwrap(), in_amount - out_amount, true));
payments.push((change_address.unwrap(), in_amount - out_amount));
}
Ok(

View file

@ -94,9 +94,8 @@ impl SignableTransaction {
transcript.append_message(b"input_shared_key", &input.key_offset.to_bytes());
}
for payment in &self.payments {
transcript.append_message(b"payment_address", &payment.0.as_bytes());
transcript.append_message(b"payment_address", &payment.0.to_string().as_bytes());
transcript.append_message(b"payment_amount", &payment.1.to_le_bytes());
transcript.append_message(b"payment_unique", &(if payment.2 { [1] } else { [0] }));
}
// Sort included before cloning it around

View file

@ -18,12 +18,7 @@ use transcript::RecommendedTranscript;
#[cfg(feature = "multisig")]
use frost::{curve::Ed25519, tests::{THRESHOLD, key_gen, sign}};
use monero::{
network::Network,
util::{key::PublicKey, address::Address}
};
use monero_serai::{random_scalar, wallet::SignableTransaction};
use monero_serai::{random_scalar, wallet::{ViewPair, address::{Network, AddressType}, SignableTransaction}};
mod rpc;
use crate::rpc::{rpc, mine_block};
@ -73,11 +68,8 @@ async fn send_core(test: usize, multisig: bool) {
}
}
let addr = Address::standard(
Network::Mainnet,
PublicKey { point: spend_pub.compress() },
PublicKey { point: (&view * &ED25519_BASEPOINT_TABLE).compress() }
);
let view_pair = ViewPair { view, spend: spend_pub };
let addr = view_pair.address(Network::Mainnet, AddressType::Standard, false);
let fee = rpc.get_fee().await.unwrap();
@ -99,7 +91,7 @@ async fn send_core(test: usize, multisig: bool) {
// Grab the largest output available
let output = {
let mut outputs = tx.as_ref().unwrap().scan(view, spend_pub).0;
let mut outputs = tx.as_ref().unwrap().scan(view_pair, false).0;
outputs.sort_by(|x, y| x.commitment.amount.cmp(&y.commitment.amount).reverse());
outputs.swap_remove(0)
};
@ -124,7 +116,7 @@ async fn send_core(test: usize, multisig: bool) {
for i in (start + 1) .. (start + 9) {
let tx = rpc.get_block_transactions(i).await.unwrap().swap_remove(0);
let output = tx.scan(view, spend_pub).0.swap_remove(0);
let output = tx.scan(view_pair, false).0.swap_remove(0);
amount += output.commitment.amount;
outputs.push(output);
}

View file

@ -77,7 +77,7 @@ pub trait Coin {
) -> Result<(Vec<u8>, Vec<<Self::Output as Output>::Id>), CoinError>;
#[cfg(test)]
async fn mine_block(&self, address: Self::Address);
async fn mine_block(&self);
#[cfg(test)]
async fn test_send(&self, key: Self::Address);

View file

@ -2,17 +2,19 @@ use std::sync::Arc;
use async_trait::async_trait;
use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, scalar::Scalar};
use curve25519_dalek::scalar::Scalar;
use dalek_ff_group as dfg;
use transcript::RecommendedTranscript;
use frost::{curve::Ed25519, MultisigKeys};
use monero::{PublicKey, network::Network, util::address::Address};
use monero_serai::{
transaction::{Timelock, Transaction},
rpc::Rpc,
wallet::{Fee, SpendableOutput, SignableTransaction as MSignableTransaction, TransactionMachine}
wallet::{
ViewPair, address::{Network, AddressType, Address},
Fee, SpendableOutput, SignableTransaction as MSignableTransaction, TransactionMachine
}
};
use crate::{coin::{CoinError, Output as OutputTrait, Coin}, view_key};
@ -59,18 +61,28 @@ pub struct SignableTransaction(
#[derive(Clone, Debug)]
pub struct Monero {
pub(crate) rpc: Rpc,
view: Scalar,
view_pub: PublicKey
view: Scalar
}
impl Monero {
pub fn new(url: String) -> Monero {
let view = view_key::<Monero>(0).0;
Monero {
rpc: Rpc::new(url),
view,
view_pub: PublicKey { point: (&view * &ED25519_BASEPOINT_TABLE).compress() }
}
Monero { rpc: Rpc::new(url), view }
}
fn view_pair(&self, spend: dfg::EdwardsPoint) -> ViewPair {
ViewPair { spend: spend.0, view: self.view }
}
#[cfg(test)]
fn empty_view_pair(&self) -> ViewPair {
use group::Group;
self.view_pair(dfg::EdwardsPoint::generator())
}
#[cfg(test)]
fn empty_address(&self) -> Address {
self.empty_view_pair().address(Network::Mainnet, AddressType::Standard, false)
}
}
@ -100,7 +112,7 @@ impl Coin for Monero {
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)
self.view_pair(key).address(Network::Mainnet, AddressType::Standard, true)
}
async fn get_height(&self) -> Result<usize, CoinError> {
@ -115,7 +127,7 @@ impl Coin for Monero {
block
.iter()
.flat_map(|tx| {
let (outputs, timelock) = tx.scan(self.view, key.0);
let (outputs, timelock) = tx.scan(self.view_pair(key), true);
if timelock == Timelock::None {
outputs
} else {
@ -178,13 +190,13 @@ impl Coin for Monero {
}
#[cfg(test)]
async fn mine_block(&self, address: Self::Address) {
async fn mine_block(&self) {
#[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(),
"wallet_address": self.empty_address().to_string(),
"amount_of_blocks": 10
},
}))).await.unwrap();
@ -192,31 +204,28 @@ impl Coin for Monero {
#[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;
self.mine_block().await;
for _ in 0 .. 7 {
self.mine_block(temp).await;
self.mine_block().await;
}
let outputs = self.rpc
.get_block_transactions_possible(height).await.unwrap()
.swap_remove(0).scan(self.view, dfg::EdwardsPoint::generator().0).0;
.swap_remove(0).scan(self.empty_view_pair(), false).0;
let amount = outputs[0].commitment.amount;
let fee = 1000000000; // TODO
let tx = MSignableTransaction::new(
outputs,
vec![(address, amount - fee)],
Some(temp),
Some(self.empty_address()),
self.rpc.get_fee().await.unwrap()
).unwrap().sign(&mut OsRng, &self.rpc, &Scalar::one()).await.unwrap();
self.rpc.publish_transaction(&tx).await.unwrap();
self.mine_block(temp).await;
self.mine_block().await;
}
}

View file

@ -4,10 +4,6 @@ use async_trait::async_trait;
use rand::rngs::OsRng;
use group::Group;
use frost::curve::Curve;
use crate::{NetworkError, Network, coin::{Coin, Monero}, wallet::{WalletKeys, MemCoinDb, Wallet}};
#[derive(Clone)]
@ -55,7 +51,7 @@ impl Network for LocalNetwork {
async fn test_send<C: Coin + Clone>(coin: C, fee: C::Fee) {
// Mine a block so there's a confirmed height
coin.mine_block(coin.address(<C::Curve as Curve>::G::generator())).await;
coin.mine_block().await;
let height = coin.get_height().await.unwrap();
let mut keys = frost::tests::key_gen::<_, C::Curve>(&mut OsRng);
@ -74,7 +70,7 @@ async fn test_send<C: Coin + Clone>(coin: C, fee: C::Fee) {
// Get the chain to a height where blocks have sufficient confirmations
while (height + C::CONFIRMATIONS) > coin.get_height().await.unwrap() {
coin.mine_block(coin.address(<C::Curve as Curve>::G::generator())).await;
coin.mine_block().await;
}
for wallet in wallets.iter_mut() {