Cite source for obscure wallet protocol rules

This commit is contained in:
Luke Parker 2024-07-12 02:19:21 -04:00
parent c59be46e2f
commit 4ba961b2cb
No known key found for this signature in database
7 changed files with 50 additions and 36 deletions

View file

@ -829,16 +829,16 @@ pub trait DecoyRpc: Sync + Clone + Debug {
/// The timelock being satisfied is distinct from being free of the 10-block lock applied to all /// The timelock being satisfied is distinct from being free of the 10-block lock applied to all
/// Monero transactions. /// Monero transactions.
/// ///
/// The node is trusted for if the output is unlocked unless `fingerprintable_canonical` is set /// The node is trusted for if the output is unlocked unless `fingerprintable_deterministic` is
/// to true. If `fingerprintable_canonical` is set to true, the node's local view isn't used, yet /// set to true. If `fingerprintable_deterministic` is set to true, the node's local view isn't
/// the transaction's timelock is checked to be unlocked at the specified `height`. This offers a /// used, yet the transaction's timelock is checked to be unlocked at the specified `height`.
/// canonical decoy selection, yet is fingerprintable as time-based timelocks aren't evaluated /// This offers a deterministic decoy selection, yet is fingerprintable as time-based timelocks
/// (and considered locked, preventing their selection). /// aren't evaluated (and considered locked, preventing their selection).
async fn get_unlocked_outputs( async fn get_unlocked_outputs(
&self, &self,
indexes: &[u64], indexes: &[u64],
height: usize, height: usize,
fingerprintable_canonical: bool, fingerprintable_deterministic: bool,
) -> Result<Vec<Option<[EdwardsPoint; 2]>>, RpcError>; ) -> Result<Vec<Option<[EdwardsPoint; 2]>>, RpcError>;
} }
@ -972,12 +972,12 @@ impl<R: Rpc> DecoyRpc for R {
&self, &self,
indexes: &[u64], indexes: &[u64],
height: usize, height: usize,
fingerprintable_canonical: bool, fingerprintable_deterministic: bool,
) -> Result<Vec<Option<[EdwardsPoint; 2]>>, RpcError> { ) -> Result<Vec<Option<[EdwardsPoint; 2]>>, RpcError> {
let outs: Vec<OutputResponse> = self.get_outs(indexes).await?; let outs: Vec<OutputResponse> = self.get_outs(indexes).await?;
// Only need to fetch txs to do canonical check on timelock // Only need to fetch txs to do deterministic check on timelock
let txs = if fingerprintable_canonical { let txs = if fingerprintable_deterministic {
self self
.get_transactions( .get_transactions(
&outs.iter().map(|out| hash_hex(&out.txid)).collect::<Result<Vec<_>, _>>()?, &outs.iter().map(|out| hash_hex(&out.txid)).collect::<Result<Vec<_>, _>>()?,
@ -1005,7 +1005,7 @@ impl<R: Rpc> DecoyRpc for R {
return Ok(None); return Ok(None);
}; };
Ok(Some([key, rpc_point(&out.mask)?]).filter(|_| { Ok(Some([key, rpc_point(&out.mask)?]).filter(|_| {
if fingerprintable_canonical { if fingerprintable_deterministic {
// TODO: Are timelock blocks by height or number? // TODO: Are timelock blocks by height or number?
// TODO: This doesn't check the default timelock has been passed // TODO: This doesn't check the default timelock has been passed
Timelock::Block(height) >= txs[i].prefix().additional_timelock Timelock::Block(height) >= txs[i].prefix().additional_timelock

View file

@ -164,15 +164,19 @@ impl AddressBytes {
} }
} }
// TODO: Cite origin // https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454
// /src/cryptonote_config.h#L216-L225
// https://gist.github.com/kayabaNerve/01c50bbc35441e0bbdcee63a9d823789 for featured
const MONERO_MAINNET_BYTES: AddressBytes = match AddressBytes::new(18, 19, 42, 70) { const MONERO_MAINNET_BYTES: AddressBytes = match AddressBytes::new(18, 19, 42, 70) {
Some(bytes) => bytes, Some(bytes) => bytes,
None => panic!("mainnet byte constants conflicted"), None => panic!("mainnet byte constants conflicted"),
}; };
// https://github.com/monero-project/monero/blob/master/src/cryptonote_config.h#L277-L281
const MONERO_STAGENET_BYTES: AddressBytes = match AddressBytes::new(24, 25, 36, 86) { const MONERO_STAGENET_BYTES: AddressBytes = match AddressBytes::new(24, 25, 36, 86) {
Some(bytes) => bytes, Some(bytes) => bytes,
None => panic!("stagenet byte constants conflicted"), None => panic!("stagenet byte constants conflicted"),
}; };
// https://github.com/monero-project/monero/blob/master/src/cryptonote_config.h#L262-L266
const MONERO_TESTNET_BYTES: AddressBytes = match AddressBytes::new(53, 54, 63, 111) { const MONERO_TESTNET_BYTES: AddressBytes = match AddressBytes::new(53, 54, 63, 111) {
Some(bytes) => bytes, Some(bytes) => bytes,
None => panic!("testnet byte constants conflicted"), None => panic!("testnet byte constants conflicted"),

View file

@ -28,7 +28,7 @@ async fn select_n(
height: usize, height: usize,
real_output: u64, real_output: u64,
ring_len: usize, ring_len: usize,
fingerprintable_canonical: bool, fingerprintable_deterministic: bool,
) -> Result<Vec<(u64, [EdwardsPoint; 2])>, RpcError> { ) -> Result<Vec<(u64, [EdwardsPoint; 2])>, RpcError> {
if height < DEFAULT_LOCK_WINDOW { if height < DEFAULT_LOCK_WINDOW {
Err(RpcError::InternalError("not enough blocks to select decoys".to_string()))?; Err(RpcError::InternalError("not enough blocks to select decoys".to_string()))?;
@ -141,7 +141,7 @@ async fn select_n(
}; };
for (i, output) in rpc for (i, output) in rpc
.get_unlocked_outputs(&candidates, height, fingerprintable_canonical) .get_unlocked_outputs(&candidates, height, fingerprintable_deterministic)
.await? .await?
.iter_mut() .iter_mut()
.enumerate() .enumerate()
@ -172,8 +172,7 @@ async fn select_decoys<R: RngCore + CryptoRng>(
ring_len: usize, ring_len: usize,
height: usize, height: usize,
input: &WalletOutput, input: &WalletOutput,
// TODO: Decide "canonical" or "deterministic" (updating RPC terminology accordingly) fingerprintable_deterministic: bool,
fingerprintable_canonical: bool,
) -> Result<Decoys, RpcError> { ) -> Result<Decoys, RpcError> {
// Select all decoys for this transaction, assuming we generate a sane transaction // Select all decoys for this transaction, assuming we generate a sane transaction
// We should almost never naturally generate an insane transaction, hence why this doesn't // We should almost never naturally generate an insane transaction, hence why this doesn't
@ -184,7 +183,7 @@ async fn select_decoys<R: RngCore + CryptoRng>(
height, height,
input.relative_id.index_on_blockchain, input.relative_id.index_on_blockchain,
ring_len, ring_len,
fingerprintable_canonical, fingerprintable_deterministic,
) )
.await?; .await?;

View file

@ -204,7 +204,10 @@ impl Extra {
/// ///
/// This returns all keys specified with `PublicKey` and the first set of keys specified with /// This returns all keys specified with `PublicKey` and the first set of keys specified with
/// `PublicKeys`, so long as they're well-formed. /// `PublicKeys`, so long as they're well-formed.
// TODO: Cite this // https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c45
// /src/wallet/wallet2.cpp#L2290-L2300
// https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454
// /src/wallet/wallet2.cpp#L2337-L2340
pub fn keys(&self) -> Option<(Vec<EdwardsPoint>, Option<Vec<EdwardsPoint>>)> { pub fn keys(&self) -> Option<(Vec<EdwardsPoint>, Option<Vec<EdwardsPoint>>)> {
let mut keys = vec![]; let mut keys = vec![];
let mut additional = None; let mut additional = None;
@ -255,18 +258,24 @@ impl Extra {
pub(crate) fn new(key: EdwardsPoint, additional: Vec<EdwardsPoint>) -> Extra { pub(crate) fn new(key: EdwardsPoint, additional: Vec<EdwardsPoint>) -> Extra {
let mut res = Extra(Vec::with_capacity(3)); let mut res = Extra(Vec::with_capacity(3));
res.push(ExtraField::PublicKey(key)); // https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454
// /src/cryptonote_basic/cryptonote_format_utils.cpp#L627-L633
// We only support pushing nonces which come after these in the sort order
res.0.push(ExtraField::PublicKey(key));
if !additional.is_empty() { if !additional.is_empty() {
res.push(ExtraField::PublicKeys(additional)); res.0.push(ExtraField::PublicKeys(additional));
} }
res res
} }
pub(crate) fn push(&mut self, field: ExtraField) { pub(crate) fn push_nonce(&mut self, nonce: Vec<u8>) {
self.0.push(field); self.0.push(ExtraField::Nonce(nonce));
} }
/// Write the Extra. /// Write the Extra.
///
/// This is not of deterministic length nor length-prefixed. It should only be written to a
/// buffer which will be delimited.
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> { pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
for field in &self.0 { for field in &self.0 {
field.write(w)?; field.write(w)?;
@ -281,17 +290,19 @@ impl Extra {
buf buf
} }
// TODO: Is this supposed to silently drop trailing gibberish?
/// Read an `Extra`. /// Read an `Extra`.
///
/// This is not of deterministic length nor length-prefixed. It should only be read from a buffer
/// already delimited.
#[allow(clippy::unnecessary_wraps)] #[allow(clippy::unnecessary_wraps)]
pub fn read<R: BufRead>(r: &mut R) -> io::Result<Extra> { pub fn read<R: BufRead>(r: &mut R) -> io::Result<Extra> {
let mut res = Extra(vec![]); let mut res = Extra(vec![]);
let mut field; // Extra reads until EOF
while { // We take a BufRead so we can detect when the buffer is empty
field = ExtraField::read(r); // `fill_buf` returns the current buffer, filled if empty, only empty if the reader is
field.is_ok() // exhausted
} { while !r.fill_buf()?.is_empty() {
res.0.push(field.unwrap()); res.0.push(ExtraField::read(r)?);
} }
Ok(res) Ok(res)
} }

View file

@ -135,9 +135,8 @@ impl InternalScanner {
// This will be None if there's no additional keys, Some(None) if there's additional keys // This will be None if there's no additional keys, Some(None) if there's additional keys
// yet not one for this output (which is non-standard), and Some(Some(_)) if there's an // yet not one for this output (which is non-standard), and Some(Some(_)) if there's an
// additional key for this output // additional key for this output
// https://github.com/monero-project/monero/ // https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454
// blob/04a1e2875d6e35e27bb21497988a6c822d319c28/ // /src/cryptonote_basic/cryptonote_format_utils.cpp#L1060-L1070
// src/cryptonote_basic/cryptonote_format_utils.cpp#L1062
let additional = additional.as_ref().map(|additional| additional.get(o)); let additional = additional.as_ref().map(|additional| additional.get(o));
#[allow(clippy::manual_let_else)] #[allow(clippy::manual_let_else)]

View file

@ -13,7 +13,7 @@ use crate::{
RctProofs, RctProofs,
}, },
transaction::{Input, Output, Timelock, TransactionPrefix, Transaction}, transaction::{Input, Output, Timelock, TransactionPrefix, Transaction},
extra::{ARBITRARY_DATA_MARKER, PaymentId, ExtraField, Extra}, extra::{ARBITRARY_DATA_MARKER, PaymentId, Extra},
send::{InternalPayment, SignableTransaction, SignableTransactionWithKeyImages}, send::{InternalPayment, SignableTransaction, SignableTransactionWithKeyImages},
}; };
@ -74,7 +74,7 @@ impl SignableTransaction {
let id = (u64::from_le_bytes(id) ^ u64::from_le_bytes(*id_xor)).to_le_bytes(); let id = (u64::from_le_bytes(id) ^ u64::from_le_bytes(*id_xor)).to_le_bytes();
let mut id_vec = Vec::with_capacity(1 + 8); let mut id_vec = Vec::with_capacity(1 + 8);
PaymentId::Encrypted(id).write(&mut id_vec).unwrap(); PaymentId::Encrypted(id).write(&mut id_vec).unwrap();
extra.push(ExtraField::Nonce(id_vec)); extra.push_nonce(id_vec);
} else { } else {
// If there's no payment ID, we push a dummy (as wallet2 does) if there's only one payment // If there's no payment ID, we push a dummy (as wallet2 does) if there's only one payment
if (self.payments.len() == 2) && if (self.payments.len() == 2) &&
@ -89,7 +89,7 @@ impl SignableTransaction {
let mut id_vec = Vec::with_capacity(1 + 8); let mut id_vec = Vec::with_capacity(1 + 8);
// The dummy payment ID is [0; 8], which when xor'd with the mask, is just the mask // The dummy payment ID is [0; 8], which when xor'd with the mask, is just the mask
PaymentId::Encrypted(*payment_id_xor).write(&mut id_vec).unwrap(); PaymentId::Encrypted(*payment_id_xor).write(&mut id_vec).unwrap();
extra.push(ExtraField::Nonce(id_vec)); extra.push_nonce(id_vec);
} }
} }
@ -97,7 +97,7 @@ impl SignableTransaction {
for part in &self.data { for part in &self.data {
let mut arb = vec![ARBITRARY_DATA_MARKER]; let mut arb = vec![ARBITRARY_DATA_MARKER];
arb.extend(part); arb.extend(part);
extra.push(ExtraField::Nonce(arb)); extra.push_nonce(arb);
} }
let mut serialized = Vec::with_capacity(32 * amount_of_keys); let mut serialized = Vec::with_capacity(32 * amount_of_keys);

View file

@ -186,7 +186,8 @@ impl SignableTransaction {
let mut additional_keys_pub = vec![]; let mut additional_keys_pub = vec![];
for (additional_key, payment) in additional_keys.into_iter().zip(&self.payments) { for (additional_key, payment) in additional_keys.into_iter().zip(&self.payments) {
let addr = payment.address(); let addr = payment.address();
// TODO: Double check this against wallet2 // https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454
// /src/device/device_default.cpp#L308-L312
if addr.is_subaddress() { if addr.is_subaddress() {
additional_keys_pub.push(additional_key.deref() * addr.spend()); additional_keys_pub.push(additional_key.deref() * addr.spend());
} else { } else {