Replace "coin" with "network"

The Processor's coins folder referred to the networks it could process, as did
its Coin trait. This, and other similar cases throughout the codebase, have now
been corrected.

Also corrects dated documentation for a key pair is confirmed under the
validator-sets pallet.
This commit is contained in:
Luke Parker
2023-07-30 16:11:30 -04:00
parent 2815046b21
commit 9f143a9742
40 changed files with 551 additions and 532 deletions

View File

@@ -0,0 +1,643 @@
use std::{time::Duration, collections::HashMap, io};
use async_trait::async_trait;
use zeroize::Zeroizing;
use rand_core::SeedableRng;
use rand_chacha::ChaCha20Rng;
use transcript::{Transcript, RecommendedTranscript};
use ciphersuite::group::{ff::Field, Group};
use dalek_ff_group::{Scalar, EdwardsPoint};
use frost::{curve::Ed25519, ThresholdKeys};
use monero_serai::{
Protocol,
transaction::Transaction,
block::Block,
rpc::{RpcError, HttpRpc, Rpc},
wallet::{
ViewPair, Scanner,
address::{Network as MoneroNetwork, SubaddressIndex, AddressSpec},
Fee, SpendableOutput, Change, Decoys, TransactionError,
SignableTransaction as MSignableTransaction, Eventuality, TransactionMachine,
},
};
use tokio::time::sleep;
pub use serai_client::{
primitives::{MAX_DATA_LEN, Coin as SeraiCoin, NetworkId, Amount, Balance},
networks::monero::Address,
};
use crate::{
Payment, Plan, additional_key,
networks::{
NetworkError, Block as BlockTrait, OutputType, Output as OutputTrait,
Transaction as TransactionTrait, Eventuality as EventualityTrait, EventualitiesTracker,
PostFeeBranch, Network, drop_branches, amortize_fee,
},
};
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct Output(SpendableOutput, Vec<u8>);
const EXTERNAL_SUBADDRESS: Option<SubaddressIndex> = SubaddressIndex::new(0, 0);
const BRANCH_SUBADDRESS: Option<SubaddressIndex> = SubaddressIndex::new(1, 0);
const CHANGE_SUBADDRESS: Option<SubaddressIndex> = SubaddressIndex::new(2, 0);
impl OutputTrait for Output {
// While we could use (tx, o), using the key ensures we won't be susceptible to the burning bug.
// While we already are immune, thanks to using featured address, this doesn't hurt and is
// technically more efficient.
type Id = [u8; 32];
fn kind(&self) -> OutputType {
match self.0.output.metadata.subaddress {
EXTERNAL_SUBADDRESS => OutputType::External,
BRANCH_SUBADDRESS => OutputType::Branch,
CHANGE_SUBADDRESS => OutputType::Change,
_ => panic!("unrecognized address was scanned for"),
}
}
fn id(&self) -> Self::Id {
self.0.output.data.key.compress().to_bytes()
}
fn balance(&self) -> Balance {
Balance { coin: SeraiCoin::Monero, amount: Amount(self.0.commitment().amount) }
}
fn data(&self) -> &[u8] {
&self.1
}
fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
self.0.write(writer)?;
writer.write_all(&u16::try_from(self.1.len()).unwrap().to_le_bytes())?;
writer.write_all(&self.1)?;
Ok(())
}
fn read<R: io::Read>(reader: &mut R) -> io::Result<Self> {
let output = SpendableOutput::read(reader)?;
let mut data_len = [0; 2];
reader.read_exact(&mut data_len)?;
let mut data = vec![0; usize::from(u16::from_le_bytes(data_len))];
reader.read_exact(&mut data)?;
Ok(Output(output, data))
}
}
#[async_trait]
impl TransactionTrait<Monero> for Transaction {
type Id = [u8; 32];
fn id(&self) -> Self::Id {
self.hash()
}
fn serialize(&self) -> Vec<u8> {
self.serialize()
}
#[cfg(test)]
async fn fee(&self, _: &Monero) -> u64 {
self.rct_signatures.base.fee
}
}
impl EventualityTrait for Eventuality {
// Use the TX extra to look up potential matches
// While anyone can forge this, a transaction with distinct outputs won't actually match
// Extra includess the one time keys which are derived from the plan ID, so a collision here is a
// hash collision
fn lookup(&self) -> Vec<u8> {
self.extra().to_vec()
}
fn read<R: io::Read>(reader: &mut R) -> io::Result<Self> {
Eventuality::read(reader)
}
fn serialize(&self) -> Vec<u8> {
self.serialize()
}
}
#[derive(Clone, Debug)]
pub struct SignableTransaction {
keys: ThresholdKeys<Ed25519>,
transcript: RecommendedTranscript,
actual: MSignableTransaction,
}
impl BlockTrait<Monero> for Block {
type Id = [u8; 32];
fn id(&self) -> Self::Id {
self.hash()
}
fn parent(&self) -> Self::Id {
self.header.previous
}
fn time(&self) -> u64 {
self.header.timestamp
}
fn median_fee(&self) -> Fee {
// TODO
Fee { per_weight: 10000000, mask: 10000 }
}
}
#[derive(Clone, Debug)]
pub struct Monero {
rpc: Rpc<HttpRpc>,
}
// Shim required for testing/debugging purposes due to generic arguments also necessitating trait
// bounds
impl PartialEq for Monero {
fn eq(&self, _: &Self) -> bool {
true
}
}
impl Eq for Monero {}
impl Monero {
pub fn new(url: String) -> Monero {
Monero { rpc: HttpRpc::new(url).unwrap() }
}
fn view_pair(spend: EdwardsPoint) -> ViewPair {
ViewPair::new(spend.0, Zeroizing::new(additional_key::<Monero>(0).0))
}
fn address_internal(spend: EdwardsPoint, subaddress: Option<SubaddressIndex>) -> Address {
Address::new(Self::view_pair(spend).address(
MoneroNetwork::Mainnet,
AddressSpec::Featured { subaddress, payment_id: None, guaranteed: true },
))
.unwrap()
}
fn scanner(spend: EdwardsPoint) -> Scanner {
let mut scanner = Scanner::from_view(Self::view_pair(spend), None);
debug_assert!(EXTERNAL_SUBADDRESS.is_none());
scanner.register_subaddress(BRANCH_SUBADDRESS.unwrap());
scanner.register_subaddress(CHANGE_SUBADDRESS.unwrap());
scanner
}
#[cfg(test)]
fn test_view_pair() -> ViewPair {
ViewPair::new(*EdwardsPoint::generator(), Zeroizing::new(Scalar::ONE.0))
}
#[cfg(test)]
fn test_scanner() -> Scanner {
Scanner::from_view(Self::test_view_pair(), Some(std::collections::HashSet::new()))
}
#[cfg(test)]
fn test_address() -> Address {
Address::new(Self::test_view_pair().address(MoneroNetwork::Mainnet, AddressSpec::Standard))
.unwrap()
}
}
#[async_trait]
impl Network for Monero {
type Curve = Ed25519;
type Fee = Fee;
type Transaction = Transaction;
type Block = Block;
type Output = Output;
type SignableTransaction = SignableTransaction;
type Eventuality = Eventuality;
type TransactionMachine = TransactionMachine;
type Address = Address;
const NETWORK: NetworkId = NetworkId::Monero;
const ID: &'static str = "Monero";
const CONFIRMATIONS: usize = 10;
// wallet2 will not create a transaction larger than 100kb, and Monero won't relay a transaction
// larger than 150kb. This fits within the 100kb mark
// Technically, it can be ~124, yet a small bit of buffer is appreciated
// TODO: Test creating a TX this big
const MAX_INPUTS: usize = 120;
const MAX_OUTPUTS: usize = 16;
// 0.01 XMR
const DUST: u64 = 10000000000;
// Monero doesn't require/benefit from tweaking
fn tweak_keys(_: &mut ThresholdKeys<Self::Curve>) {}
fn address(key: EdwardsPoint) -> Self::Address {
Self::address_internal(key, EXTERNAL_SUBADDRESS)
}
fn branch_address(key: EdwardsPoint) -> Self::Address {
Self::address_internal(key, BRANCH_SUBADDRESS)
}
async fn get_latest_block_number(&self) -> Result<usize, NetworkError> {
// Monero defines height as chain length, so subtract 1 for block number
Ok(self.rpc.get_height().await.map_err(|_| NetworkError::ConnectionError)? - 1)
}
async fn get_block(&self, number: usize) -> Result<Self::Block, NetworkError> {
Ok(
self
.rpc
.get_block(
self.rpc.get_block_hash(number).await.map_err(|_| NetworkError::ConnectionError)?,
)
.await
.map_err(|_| NetworkError::ConnectionError)?,
)
}
async fn get_outputs(
&self,
block: &Block,
key: EdwardsPoint,
) -> Result<Vec<Self::Output>, NetworkError> {
let mut txs = Self::scanner(key)
.scan(&self.rpc, block)
.await
.map_err(|_| NetworkError::ConnectionError)?
.iter()
.filter_map(|outputs| Some(outputs.not_locked()).filter(|outputs| !outputs.is_empty()))
.collect::<Vec<_>>();
// This should be pointless as we shouldn't be able to scan for any other subaddress
// This just ensures nothing invalid makes it through
for tx_outputs in &txs {
for output in tx_outputs {
assert!([EXTERNAL_SUBADDRESS, BRANCH_SUBADDRESS, CHANGE_SUBADDRESS]
.contains(&output.output.metadata.subaddress));
}
}
let mut outputs = Vec::with_capacity(txs.len());
for mut tx_outputs in txs.drain(..) {
for output in tx_outputs.drain(..) {
let mut data = output.arbitrary_data().get(0).cloned().unwrap_or(vec![]);
// The Output serialization code above uses u16 to represent length
data.truncate(u16::MAX.into());
// Monero data segments should be <= 255 already, and MAX_DATA_LEN is currently 512
// This just allows either Monero to change, or MAX_DATA_LEN to change, without introducing
// complicationso
data.truncate(MAX_DATA_LEN.try_into().unwrap());
outputs.push(Output(output, data));
}
}
Ok(outputs)
}
async fn get_eventuality_completions(
&self,
eventualities: &mut EventualitiesTracker<Eventuality>,
block: &Block,
) -> HashMap<[u8; 32], [u8; 32]> {
let mut res = HashMap::new();
if eventualities.map.is_empty() {
return res;
}
async fn check_block(
network: &Monero,
eventualities: &mut EventualitiesTracker<Eventuality>,
block: &Block,
res: &mut HashMap<[u8; 32], [u8; 32]>,
) {
for hash in &block.txs {
let tx = {
let mut tx;
while {
tx = network.get_transaction(hash).await;
tx.is_err()
} {
log::error!("couldn't get transaction {}: {}", hex::encode(hash), tx.err().unwrap());
sleep(Duration::from_secs(60)).await;
}
tx.unwrap()
};
if let Some((_, eventuality)) = eventualities.map.get(&tx.prefix.extra) {
if eventuality.matches(&tx) {
res.insert(eventualities.map.remove(&tx.prefix.extra).unwrap().0, tx.hash());
}
}
}
eventualities.block_number += 1;
assert_eq!(eventualities.block_number, block.number());
}
for block_num in (eventualities.block_number + 1) .. block.number() {
let block = {
let mut block;
while {
block = self.get_block(block_num).await;
block.is_err()
} {
log::error!("couldn't get block {}: {}", block_num, block.err().unwrap());
sleep(Duration::from_secs(60)).await;
}
block.unwrap()
};
check_block(self, eventualities, &block, &mut res).await;
}
// Also check the current block
check_block(self, eventualities, block, &mut res).await;
assert_eq!(eventualities.block_number, block.number());
res
}
async fn prepare_send(
&self,
keys: ThresholdKeys<Ed25519>,
block_number: usize,
mut plan: Plan<Self>,
fee: Fee,
) -> Result<(Option<(SignableTransaction, Eventuality)>, Vec<PostFeeBranch>), NetworkError> {
// Sanity check this has at least one output planned
assert!((!plan.payments.is_empty()) || plan.change.is_some());
// Get the protocol for the specified block number
// For now, this should just be v16, the latest deployed protocol, since there's no upcoming
// hard fork to be mindful of
let get_protocol = || Protocol::v16;
#[cfg(not(test))]
let protocol = get_protocol();
// If this is a test, we won't be using a mainnet node and need a distinct protocol
// determination
// Just use whatever the node expects
#[cfg(test)]
let protocol = self.rpc.get_protocol().await.unwrap();
// Hedge against the above codegen failing by having an always included runtime check
if !cfg!(test) {
assert_eq!(protocol, get_protocol());
}
// Check a fork hasn't occurred which this processor hasn't been updated for
assert_eq!(protocol, self.rpc.get_protocol().await.map_err(|_| NetworkError::ConnectionError)?);
let spendable_outputs = plan.inputs.iter().cloned().map(|input| input.0).collect::<Vec<_>>();
let mut transcript = plan.transcript();
// All signers need to select the same decoys
// All signers use the same height and a seeded RNG to make sure they do so.
let decoys = Decoys::select(
&mut ChaCha20Rng::from_seed(transcript.rng_seed(b"decoys")),
&self.rpc,
protocol.ring_len(),
block_number + 1,
&spendable_outputs,
)
.await
.map_err(|_| NetworkError::ConnectionError)
.unwrap();
let inputs = spendable_outputs.into_iter().zip(decoys.into_iter()).collect::<Vec<_>>();
let signable = |mut plan: Plan<Self>, tx_fee: Option<_>| {
// Monero requires at least two outputs
// If we only have one output planned, add a dummy payment
let outputs = plan.payments.len() + usize::from(u8::from(plan.change.is_some()));
if outputs == 0 {
return Ok(None);
} else if outputs == 1 {
plan.payments.push(Payment {
address: Address::new(
ViewPair::new(EdwardsPoint::generator().0, Zeroizing::new(Scalar::ONE.0))
.address(MoneroNetwork::Mainnet, AddressSpec::Standard),
)
.unwrap(),
amount: 0,
data: None,
});
}
let mut payments = vec![];
for payment in &plan.payments {
// If we're solely estimating the fee, don't actually specify an amount
// This won't affect the fee calculation yet will ensure we don't hit an out of funds error
payments.push((
payment.address.clone().into(),
if tx_fee.is_none() { 0 } else { payment.amount },
));
}
match MSignableTransaction::new(
protocol,
// Use the plan ID as the r_seed
// This perfectly binds the plan while simultaneously allowing verifying the plan was
// executed with no additional communication
Some(Zeroizing::new(plan.id())),
inputs.clone(),
payments,
plan.change.map(|key| {
Change::fingerprintable(Self::address_internal(key, CHANGE_SUBADDRESS).into())
}),
vec![],
fee,
) {
Ok(signable) => Ok(Some(signable)),
Err(e) => match e {
TransactionError::MultiplePaymentIds => {
panic!("multiple payment IDs despite not supporting integrated addresses");
}
TransactionError::NoInputs |
TransactionError::NoOutputs |
TransactionError::InvalidDecoyQuantity |
TransactionError::NoChange |
TransactionError::TooManyOutputs |
TransactionError::TooMuchData |
TransactionError::TooLargeTransaction |
TransactionError::WrongPrivateKey => {
panic!("created an Monero invalid transaction: {e}");
}
TransactionError::ClsagError(_) |
TransactionError::InvalidTransaction(_) |
TransactionError::FrostError(_) => {
panic!("supposedly unreachable (at this time) Monero error: {e}");
}
TransactionError::NotEnoughFunds { inputs, outputs, fee } => {
if let Some(tx_fee) = tx_fee {
panic!(
"{}. in: {inputs}, out: {outputs}, fee: {fee}, prior estimated fee: {tx_fee}",
"didn't have enough funds for a Monero TX",
);
} else {
Ok(None)
}
}
TransactionError::RpcError(e) => {
log::error!("RpcError when preparing transaction: {e:?}");
Err(NetworkError::ConnectionError)
}
},
}
};
let tx_fee = match signable(plan.clone(), None)? {
Some(tx) => tx.fee(),
None => return Ok((None, drop_branches(&plan))),
};
let branch_outputs = amortize_fee(&mut plan, tx_fee);
let signable = SignableTransaction {
keys,
transcript,
actual: match signable(plan, Some(tx_fee))? {
Some(signable) => signable,
None => return Ok((None, branch_outputs)),
},
};
let eventuality = signable.actual.eventuality().unwrap();
Ok((Some((signable, eventuality)), branch_outputs))
}
async fn attempt_send(
&self,
transaction: SignableTransaction,
) -> Result<Self::TransactionMachine, NetworkError> {
match transaction.actual.clone().multisig(transaction.keys.clone(), transaction.transcript) {
Ok(machine) => Ok(machine),
Err(e) => panic!("failed to create a multisig machine for TX: {e}"),
}
}
async fn publish_transaction(&self, tx: &Self::Transaction) -> Result<(), NetworkError> {
match self.rpc.publish_transaction(tx).await {
Ok(_) => Ok(()),
Err(RpcError::ConnectionError) => Err(NetworkError::ConnectionError)?,
// TODO: Distinguish already in pool vs double spend (other signing attempt succeeded) vs
// invalid transaction
Err(e) => panic!("failed to publish TX {}: {e}", hex::encode(tx.hash())),
}
}
async fn get_transaction(&self, id: &[u8; 32]) -> Result<Transaction, NetworkError> {
self.rpc.get_transaction(*id).await.map_err(|_| NetworkError::ConnectionError)
}
fn confirm_completion(&self, eventuality: &Eventuality, tx: &Transaction) -> bool {
eventuality.matches(tx)
}
#[cfg(test)]
async fn get_block_number(&self, id: &[u8; 32]) -> usize {
self.rpc.get_block(*id).await.unwrap().number()
}
#[cfg(test)]
async fn get_fee(&self) -> Self::Fee {
use monero_serai::wallet::FeePriority;
self.rpc.get_fee(self.rpc.get_protocol().await.unwrap(), FeePriority::Low).await.unwrap()
}
#[cfg(test)]
async fn mine_block(&self) {
// https://github.com/serai-dex/serai/issues/198
sleep(std::time::Duration::from_millis(100)).await;
#[derive(serde::Deserialize, Debug)]
struct EmptyResponse {}
let _: EmptyResponse = self
.rpc
.rpc_call(
"json_rpc",
Some(serde_json::json!({
"method": "generateblocks",
"params": {
"wallet_address": Self::test_address().to_string(),
"amount_of_blocks": 1
},
})),
)
.await
.unwrap();
}
#[cfg(test)]
async fn test_send(&self, address: Self::Address) -> Block {
use zeroize::Zeroizing;
use rand_core::OsRng;
use monero_serai::wallet::FeePriority;
let new_block = self.get_latest_block_number().await.unwrap() + 1;
for _ in 0 .. 80 {
self.mine_block().await;
}
let outputs = Self::test_scanner()
.scan(&self.rpc, &self.rpc.get_block_by_number(new_block).await.unwrap())
.await
.unwrap()
.swap_remove(0)
.ignore_timelock();
let amount = outputs[0].commitment().amount;
// The dust should always be sufficient for the fee
let fee = Monero::DUST;
let protocol = self.rpc.get_protocol().await.unwrap();
let decoys = Decoys::select(
&mut OsRng,
&self.rpc,
protocol.ring_len(),
self.rpc.get_height().await.unwrap() - 1,
&outputs,
)
.await
.unwrap();
let inputs = outputs.into_iter().zip(decoys.into_iter()).collect::<Vec<_>>();
let tx = MSignableTransaction::new(
protocol,
None,
inputs,
vec![(address.into(), amount - fee)],
Some(Change::fingerprintable(Self::test_address().into())),
vec![],
self.rpc.get_fee(protocol, FeePriority::Low).await.unwrap(),
)
.unwrap()
.sign(&mut OsRng, &Zeroizing::new(Scalar::ONE.0))
.await
.unwrap();
let block = self.get_latest_block_number().await.unwrap() + 1;
self.rpc.publish_transaction(&tx).await.unwrap();
for _ in 0 .. 10 {
self.mine_block().await;
}
self.get_block(block).await.unwrap()
}
}