Luke Parker 2022-08-22 12:15:14 -04:00
parent 5a1f011db8
commit 5c106cecf6
No known key found for this signature in database
GPG key ID: F9F1386DB1E119B6
9 changed files with 258 additions and 155 deletions

View file

@ -138,12 +138,9 @@ impl Rpc {
Ok(self.rpc_call::<Option<()>, HeightResponse>("get_height", None).await?.height)
}
async fn get_transactions_core(
&self,
hashes: &[[u8; 32]],
) -> Result<(Vec<Result<Transaction, RpcError>>, Vec<[u8; 32]>), RpcError> {
pub async fn get_transactions(&self, hashes: &[[u8; 32]]) -> Result<Vec<Transaction>, RpcError> {
if hashes.is_empty() {
return Ok((vec![], vec![]));
return Ok(vec![]);
}
#[derive(Deserialize, Debug)]
@ -168,50 +165,34 @@ impl Rpc {
)
.await?;
Ok((
txs
.txs
.iter()
.map(|res| {
let tx = Transaction::deserialize(&mut std::io::Cursor::new(
rpc_hex(if !res.as_hex.is_empty() { &res.as_hex } else { &res.pruned_as_hex }).unwrap(),
))
.map_err(|_| {
RpcError::InvalidTransaction(hex::decode(&res.tx_hash).unwrap().try_into().unwrap())
})?;
// https://github.com/monero-project/monero/issues/8311
if res.as_hex.is_empty() {
match tx.prefix.inputs.get(0) {
Some(Input::Gen { .. }) => (),
_ => Err(RpcError::PrunedTransaction)?,
}
}
Ok(tx)
})
.collect(),
txs.missed_tx.iter().map(|hash| hex::decode(&hash).unwrap().try_into().unwrap()).collect(),
))
}
pub async fn get_transactions(&self, hashes: &[[u8; 32]]) -> Result<Vec<Transaction>, RpcError> {
let (txs, missed) = self.get_transactions_core(hashes).await?;
if !missed.is_empty() {
Err(RpcError::TransactionsNotFound(missed))?;
if txs.missed_tx.len() != 0 {
Err(RpcError::TransactionsNotFound(
txs.missed_tx.iter().map(|hash| hex::decode(&hash).unwrap().try_into().unwrap()).collect(),
))?;
}
// This will clone several KB and is accordingly inefficient
// TODO: Optimize
txs.iter().cloned().collect::<Result<_, _>>()
}
// TODO: Remove with https://github.com/serai-dex/serai/issues/25
pub async fn get_transactions_possible(
&self,
hashes: &[[u8; 32]],
) -> Result<Vec<Transaction>, RpcError> {
let (txs, _) = self.get_transactions_core(hashes).await?;
Ok(txs.iter().cloned().filter_map(|tx| tx.ok()).collect())
txs
.txs
.iter()
.map(|res| {
let tx = Transaction::deserialize(&mut std::io::Cursor::new(
rpc_hex(if !res.as_hex.is_empty() { &res.as_hex } else { &res.pruned_as_hex }).unwrap(),
))
.map_err(|_| {
RpcError::InvalidTransaction(hex::decode(&res.tx_hash).unwrap().try_into().unwrap())
})?;
// https://github.com/monero-project/monero/issues/8311
if res.as_hex.is_empty() {
match tx.prefix.inputs.get(0) {
Some(Input::Gen { .. }) => (),
_ => Err(RpcError::PrunedTransaction)?,
}
}
Ok(tx)
})
.collect()
}
pub async fn get_block(&self, height: usize) -> Result<Block, RpcError> {
@ -238,32 +219,13 @@ impl Rpc {
)
}
async fn get_block_transactions_core(
&self,
height: usize,
possible: bool,
) -> Result<Vec<Transaction>, RpcError> {
pub async fn get_block_transactions(&self, height: usize) -> Result<Vec<Transaction>, RpcError> {
let block = self.get_block(height).await?;
let mut res = vec![block.miner_tx];
res.extend(if possible {
self.get_transactions_possible(&block.txs).await?
} else {
self.get_transactions(&block.txs).await?
});
res.extend(self.get_transactions(&block.txs).await?);
Ok(res)
}
pub async fn get_block_transactions(&self, height: usize) -> Result<Vec<Transaction>, RpcError> {
self.get_block_transactions_core(height, false).await
}
pub async fn get_block_transactions_possible(
&self,
height: usize,
) -> Result<Vec<Transaction>, RpcError> {
self.get_block_transactions_core(height, true).await
}
pub async fn get_o_indexes(&self, hash: [u8; 32]) -> Result<Vec<u64>, RpcError> {
#[derive(Serialize, Debug)]
struct Request {

View file

@ -140,8 +140,8 @@ impl Decoys {
let mut real = Vec::with_capacity(inputs.len());
let mut outputs = Vec::with_capacity(inputs.len());
for input in inputs {
real.push(rpc.get_o_indexes(input.tx).await?[usize::from(input.o)]);
outputs.push((real[real.len() - 1], [input.key, input.commitment.calculate()]));
real.push(input.global_index);
outputs.push((real[real.len() - 1], [input.output.data.key, input.commitment().calculate()]));
}
let distribution_len = {

View file

@ -8,22 +8,59 @@ use crate::{
Commitment,
serialize::{read_byte, read_u32, read_u64, read_bytes, read_scalar, read_point},
transaction::{Timelock, Transaction},
block::Block,
rpc::{Rpc, RpcError},
wallet::{PaymentId, Extra, Scanner, uniqueness, shared_key, amount_decryption, commitment_mask},
};
#[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)]
pub struct SpendableOutput {
pub struct AbsoluteId {
pub tx: [u8; 32],
pub o: u8,
}
impl AbsoluteId {
pub fn serialize(&self) -> Vec<u8> {
let mut res = Vec::with_capacity(32 + 1);
res.extend(&self.tx);
res.push(self.o);
res
}
pub fn deserialize<R: std::io::Read>(r: &mut R) -> std::io::Result<AbsoluteId> {
Ok(AbsoluteId { tx: read_bytes(r)?, o: read_byte(r)? })
}
}
#[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)]
pub struct OutputData {
pub key: EdwardsPoint,
// Absolute difference between the spend key and the key in this output, inclusive of any
// subaddress offset
// Absolute difference between the spend key and the key in this output
pub key_offset: Scalar,
pub commitment: Commitment,
}
// Metadata to know how to process this output
impl OutputData {
pub fn serialize(&self) -> Vec<u8> {
let mut res = Vec::with_capacity(32 + 32 + 40);
res.extend(self.key.compress().to_bytes());
res.extend(self.key_offset.to_bytes());
res.extend(self.commitment.mask.to_bytes());
res.extend(self.commitment.amount.to_le_bytes());
res
}
pub fn deserialize<R: std::io::Read>(r: &mut R) -> std::io::Result<OutputData> {
Ok(OutputData {
key: read_point(r)?,
key_offset: read_scalar(r)?,
commitment: Commitment::new(read_scalar(r)?, read_u64(r)?),
})
}
}
#[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)]
pub struct Metadata {
// Does not have to be an Option since the 0 subaddress is the main address
pub subaddress: (u32, u32),
// Can be an Option, as extra doesn't necessarily have a payment ID, yet all Monero TXs should
@ -35,14 +72,98 @@ pub struct SpendableOutput {
pub payment_id: [u8; 8],
}
#[derive(Zeroize, ZeroizeOnDrop)]
pub struct Timelocked(Timelock, Vec<SpendableOutput>);
impl Timelocked {
impl Metadata {
pub fn serialize(&self) -> Vec<u8> {
let mut res = Vec::with_capacity(4 + 4 + 8);
res.extend(self.subaddress.0.to_le_bytes());
res.extend(self.subaddress.1.to_le_bytes());
res.extend(self.payment_id);
res
}
pub fn deserialize<R: std::io::Read>(r: &mut R) -> std::io::Result<Metadata> {
Ok(Metadata { subaddress: (read_u32(r)?, read_u32(r)?), payment_id: read_bytes(r)? })
}
}
#[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)]
pub struct ReceivedOutput {
pub absolute: AbsoluteId,
pub data: OutputData,
pub metadata: Metadata,
}
impl ReceivedOutput {
pub fn commitment(&self) -> Commitment {
self.data.commitment.clone()
}
pub fn serialize(&self) -> Vec<u8> {
let mut serialized = self.absolute.serialize();
serialized.extend(&self.data.serialize());
serialized.extend(&self.metadata.serialize());
serialized
}
pub fn deserialize<R: std::io::Read>(r: &mut R) -> std::io::Result<ReceivedOutput> {
Ok(ReceivedOutput {
absolute: AbsoluteId::deserialize(r)?,
data: OutputData::deserialize(r)?,
metadata: Metadata::deserialize(r)?,
})
}
}
#[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)]
pub struct SpendableOutput {
pub output: ReceivedOutput,
pub global_index: u64,
}
impl SpendableOutput {
pub async fn refresh_global_index(&mut self, rpc: &Rpc) -> Result<(), RpcError> {
self.global_index =
rpc.get_o_indexes(self.output.absolute.tx).await?[usize::from(self.output.absolute.o)];
Ok(())
}
pub async fn from(rpc: &Rpc, output: ReceivedOutput) -> Result<SpendableOutput, RpcError> {
let mut output = SpendableOutput { output, global_index: 0 };
output.refresh_global_index(rpc).await?;
Ok(output)
}
pub fn commitment(&self) -> Commitment {
self.output.commitment()
}
pub fn serialize(&self) -> Vec<u8> {
let mut serialized = self.output.serialize();
serialized.extend(&self.global_index.to_le_bytes());
serialized
}
pub fn deserialize<R: std::io::Read>(r: &mut R) -> std::io::Result<SpendableOutput> {
Ok(SpendableOutput { output: ReceivedOutput::deserialize(r)?, global_index: read_u64(r)? })
}
}
#[derive(Zeroize)]
pub struct Timelocked<O: Clone + Zeroize>(Timelock, Vec<O>);
impl<O: Clone + Zeroize> Drop for Timelocked<O> {
fn drop(&mut self) {
self.0.zeroize();
self.1.zeroize();
}
}
impl<O: Clone + Zeroize> ZeroizeOnDrop for Timelocked<O> {}
impl<O: Clone + Zeroize> Timelocked<O> {
pub fn timelock(&self) -> Timelock {
self.0
}
pub fn not_locked(&self) -> Vec<SpendableOutput> {
pub fn not_locked(&self) -> Vec<O> {
if self.0 == Timelock::None {
return self.1.clone();
}
@ -50,52 +171,18 @@ impl Timelocked {
}
/// Returns None if the Timelocks aren't comparable. Returns Some(vec![]) if none are unlocked
pub fn unlocked(&self, timelock: Timelock) -> Option<Vec<SpendableOutput>> {
pub fn unlocked(&self, timelock: Timelock) -> Option<Vec<O>> {
// If the Timelocks are comparable, return the outputs if they're now unlocked
self.0.partial_cmp(&timelock).filter(|_| self.0 <= timelock).map(|_| self.1.clone())
}
pub fn ignore_timelock(&self) -> Vec<SpendableOutput> {
pub fn ignore_timelock(&self) -> Vec<O> {
self.1.clone()
}
}
impl SpendableOutput {
pub fn serialize(&self) -> Vec<u8> {
let mut res = Vec::with_capacity(32 + 1 + 32 + 32 + 40);
res.extend(&self.tx);
res.push(self.o);
res.extend(self.key.compress().to_bytes());
res.extend(self.key_offset.to_bytes());
res.extend(self.commitment.mask.to_bytes());
res.extend(self.commitment.amount.to_le_bytes());
res.extend(self.subaddress.0.to_le_bytes());
res.extend(self.subaddress.1.to_le_bytes());
res.extend(self.payment_id);
res
}
pub fn deserialize<R: std::io::Read>(r: &mut R) -> std::io::Result<SpendableOutput> {
Ok(SpendableOutput {
tx: read_bytes(r)?,
o: read_byte(r)?,
key: read_point(r)?,
key_offset: read_scalar(r)?,
commitment: Commitment::new(read_scalar(r)?, read_u64(r)?),
subaddress: (read_u32(r)?, read_u32(r)?),
payment_id: read_bytes(r)?,
})
}
}
impl Scanner {
pub fn scan(&mut self, tx: &Transaction) -> Timelocked {
pub fn scan_stateless(&mut self, tx: &Transaction) -> Timelocked<ReceivedOutput> {
let extra = Extra::deserialize(&mut Cursor::new(&tx.prefix.extra));
let keys;
let extra = if let Ok(extra) = extra {
@ -170,16 +257,16 @@ impl Scanner {
}
if commitment.amount != 0 {
res.push(SpendableOutput {
tx: tx.hash(),
o: o.try_into().unwrap(),
res.push(ReceivedOutput {
absolute: AbsoluteId { tx: tx.hash(), o: o.try_into().unwrap() },
key: output.key,
key_offset: key_offset + self.pair.subaddress(*subaddress.unwrap()),
commitment,
data: OutputData {
key: output.key,
key_offset: key_offset + self.pair.subaddress(*subaddress.unwrap()),
commitment,
},
subaddress: (0, 0),
payment_id,
metadata: Metadata { subaddress: (0, 0), payment_id },
});
if let Some(burning_bug) = self.burning_bug.as_mut() {
@ -194,4 +281,41 @@ impl Scanner {
Timelocked(tx.prefix.timelock, res)
}
pub async fn scan(
&mut self,
rpc: &Rpc,
block: &Block,
) -> Result<Vec<Timelocked<SpendableOutput>>, RpcError> {
let mut index = rpc.get_o_indexes(block.miner_tx.hash()).await?[0];
let mut txs = vec![block.miner_tx.clone()];
txs.extend(rpc.get_transactions(&block.txs).await?);
let map = |mut timelock: Timelocked<ReceivedOutput>, index| {
if timelock.1.is_empty() {
None
} else {
Some(Timelocked(
timelock.0,
timelock
.1
.drain(..)
.map(|output| SpendableOutput {
global_index: index + u64::from(output.absolute.o),
output,
})
.collect(),
))
}
};
let mut res = vec![];
for tx in txs {
if let Some(timelock) = map(self.scan_stateless(&tx), index) {
res.push(timelock);
}
index += u64::try_from(tx.prefix.outputs.len()).unwrap();
}
Ok(res)
}
}

View file

@ -129,9 +129,9 @@ async fn prepare_inputs<R: RngCore + CryptoRng>(
for (i, input) in inputs.iter().enumerate() {
signable.push((
spend + input.key_offset,
generate_key_image(spend + input.key_offset),
ClsagInput::new(input.commitment.clone(), decoys[i].clone())
spend + input.output.data.key_offset,
generate_key_image(spend + input.output.data.key_offset),
ClsagInput::new(input.commitment().clone(), decoys[i].clone())
.map_err(TransactionError::ClsagError)?,
));
@ -225,7 +225,7 @@ impl SignableTransaction {
fee_rate.calculate(Transaction::fee_weight(protocol, inputs.len(), outputs, extra));
// Make sure we have enough funds
let in_amount = inputs.iter().map(|input| input.commitment.amount).sum::<u64>();
let in_amount = inputs.iter().map(|input| input.commitment().amount).sum::<u64>();
let mut out_amount = payments.iter().map(|payment| payment.1).sum::<u64>() + fee;
if in_amount < out_amount {
Err(TransactionError::NotEnoughFunds(in_amount, out_amount))?;
@ -345,8 +345,8 @@ impl SignableTransaction {
) -> Result<Transaction, TransactionError> {
let mut images = Vec::with_capacity(self.inputs.len());
for input in &self.inputs {
let mut offset = spend + input.key_offset;
if (&offset * &ED25519_BASEPOINT_TABLE) != input.key {
let mut offset = spend + input.output.data.key_offset;
if (&offset * &ED25519_BASEPOINT_TABLE) != input.output.data.key {
Err(TransactionError::WrongPrivateKey)?;
}

View file

@ -100,11 +100,11 @@ impl SignableTransaction {
for input in &self.inputs {
// These outputs can only be spent once. Therefore, it forces all RNGs derived from this
// transcript (such as the one used to create one time keys) to be unique
transcript.append_message(b"input_hash", &input.tx);
transcript.append_message(b"input_output_index", &[input.o]);
transcript.append_message(b"input_hash", &input.output.absolute.tx);
transcript.append_message(b"input_output_index", &[input.output.absolute.o]);
// Not including this, with a doxxed list of payments, would allow brute forcing the inputs
// to determine RNG seeds and therefore the true spends
transcript.append_message(b"input_shared_key", &input.key_offset.to_bytes());
transcript.append_message(b"input_shared_key", &input.output.data.key_offset.to_bytes());
}
for payment in &self.payments {
transcript.append_message(b"payment_address", payment.0.to_string().as_bytes());
@ -116,14 +116,14 @@ impl SignableTransaction {
for (i, input) in self.inputs.iter().enumerate() {
// Check this the right set of keys
let offset = keys.offset(dalek_ff_group::Scalar(input.key_offset));
if offset.group_key().0 != input.key {
let offset = keys.offset(dalek_ff_group::Scalar(input.output.data.key_offset));
if offset.group_key().0 != input.output.data.key {
Err(TransactionError::WrongPrivateKey)?;
}
clsags.push(
AlgorithmMachine::new(
ClsagMultisig::new(transcript.clone(), input.key, inputs[i].clone())
ClsagMultisig::new(transcript.clone(), input.output.data.key, inputs[i].clone())
.map_err(TransactionError::MultisigError)?,
offset,
&included,
@ -331,7 +331,7 @@ impl SignMachine<Transaction> for TransactionSignMachine {
});
*value.3.write().unwrap() = Some(ClsagDetails::new(
ClsagInput::new(value.1.commitment.clone(), value.2).map_err(|_| {
ClsagInput::new(value.1.commitment().clone(), value.2).map_err(|_| {
panic!("Signing an input which isn't present in the ring we created for it")
})?,
mask,

View file

@ -23,7 +23,7 @@ use frost::{
use monero_serai::{
random_scalar,
wallet::{ViewPair, Scanner, address::Network, SignableTransaction},
wallet::{address::Network, ViewPair, Scanner, SpendableOutput, SignableTransaction},
};
mod rpc;
@ -98,13 +98,13 @@ async fn send_core(test: usize, multisig: bool) {
// Grab the largest output available
let output = {
let mut outputs = scanner.scan(tx.as_ref().unwrap()).ignore_timelock();
outputs.sort_by(|x, y| x.commitment.amount.cmp(&y.commitment.amount).reverse());
let mut outputs = scanner.scan_stateless(tx.as_ref().unwrap()).ignore_timelock();
outputs.sort_by(|x, y| x.commitment().amount.cmp(&y.commitment().amount).reverse());
outputs.swap_remove(0)
};
// Test creating a zero change output and a non-zero change output
amount = output.commitment.amount - u64::try_from(i).unwrap();
outputs.push(output);
amount = output.commitment().amount - u64::try_from(i).unwrap();
outputs.push(SpendableOutput::from(&rpc, output).await.unwrap());
// Test spending multiple inputs
} else if test == 1 {
@ -122,9 +122,9 @@ 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 = scanner.scan(&tx).ignore_timelock().swap_remove(0);
amount += output.commitment.amount;
let mut txs = scanner.scan(&rpc, &rpc.get_block(i).await.unwrap()).await.unwrap();
let output = txs.swap_remove(0).ignore_timelock().swap_remove(0);
amount += output.commitment().amount;
outputs.push(output);
}
}

View file

@ -53,7 +53,7 @@ pub trait Coin {
&self,
block: &Self::Block,
key: <Self::Curve as Curve>::G,
) -> Vec<Self::Output>;
) -> Result<Vec<Self::Output>, CoinError>;
async fn prepare_send(
&self,

View file

@ -8,6 +8,7 @@ use frost::{curve::Ed25519, FrostKeys};
use monero_serai::{
transaction::Transaction,
block::Block,
rpc::Rpc,
wallet::{
ViewPair, Scanner,
@ -36,11 +37,11 @@ impl OutputTrait for Output {
type Id = [u8; 32];
fn id(&self) -> Self::Id {
self.0.key.compress().to_bytes()
self.0.output.data.key.compress().to_bytes()
}
fn amount(&self) -> u64 {
self.0.commitment.amount
self.0.commitment().amount
}
fn serialize(&self) -> Vec<u8> {
@ -97,7 +98,7 @@ impl Coin for Monero {
type Fee = Fee;
type Transaction = Transaction;
type Block = Vec<Transaction>;
type Block = Block;
type Output = Output;
type SignableTransaction = SignableTransaction;
@ -125,12 +126,25 @@ impl Coin for Monero {
}
async fn get_block(&self, height: usize) -> Result<Self::Block, CoinError> {
self.rpc.get_block_transactions_possible(height).await.map_err(|_| CoinError::ConnectionError)
self.rpc.get_block(height).await.map_err(|_| CoinError::ConnectionError)
}
async fn get_outputs(&self, block: &Self::Block, key: dfg::EdwardsPoint) -> Vec<Self::Output> {
let mut scanner = self.scanner(key);
block.iter().flat_map(|tx| scanner.scan(tx).not_locked()).map(Output::from).collect()
async fn get_outputs(
&self,
block: &Self::Block,
key: dfg::EdwardsPoint,
) -> Result<Vec<Self::Output>, CoinError> {
Ok(
self
.scanner(key)
.scan(&self.rpc, block)
.await
.map_err(|_| CoinError::ConnectionError)?
.iter()
.flat_map(|outputs| outputs.not_locked())
.map(Output::from)
.collect(),
)
}
async fn prepare_send(
@ -221,10 +235,13 @@ impl Coin for Monero {
}
let outputs = Self::empty_scanner()
.scan(&self.rpc.get_block_transactions_possible(height).await.unwrap().swap_remove(0))
.scan(&self.rpc, &self.rpc.get_block(height).await.unwrap())
.await
.unwrap()
.swap_remove(0)
.ignore_timelock();
let amount = outputs[0].commitment.amount;
let amount = outputs[0].commitment().amount;
let fee = 3000000000; // TODO
let tx = MSignableTransaction::new(
self.rpc.get_protocol().await.unwrap(),

View file

@ -262,7 +262,7 @@ impl<D: CoinDb, C: Coin> Wallet<D, C> {
self
.coin
.get_outputs(&block, keys.group_key())
.await
.await?
.iter()
.cloned()
.filter(|output| self.db.add_output(output)),