5 Commits

Author SHA1 Message Date
Luke Parker
ba657e23d1 Have a public monero-rpc type be properly formatted
It was public as the raw RPC response. It's more polite to handle the
formatting in the RPC, and allows us to return a better structure.
2024-07-12 04:14:05 -04:00
Luke Parker
32c24917c4 Correct tests which should've failed to expect failures now that they fail 2024-07-12 03:09:48 -04:00
Luke Parker
4ba961b2cb Cite source for obscure wallet protocol rules 2024-07-12 02:19:21 -04:00
Luke Parker
c59be46e2f Optimize Monero BPs 2024-07-12 02:18:57 -04:00
Luke Parker
2c165e19ae Bitcoin 27.1 2024-07-12 02:18:43 -04:00
17 changed files with 172 additions and 114 deletions

View File

@@ -10,7 +10,7 @@ inputs:
bitcoin-version: bitcoin-version:
description: "Bitcoin version to download and run as a regtest node" description: "Bitcoin version to download and run as a regtest node"
required: false required: false
default: "27.0" default: "27.1"
runs: runs:
using: "composite" using: "composite"

1
Cargo.lock generated
View File

@@ -4781,7 +4781,6 @@ dependencies = [
"monero-primitives", "monero-primitives",
"rand_core", "rand_core",
"std-shims", "std-shims",
"subtle",
"thiserror", "thiserror",
"zeroize", "zeroize",
] ]

View File

@@ -22,7 +22,6 @@ thiserror = { version = "1", default-features = false, optional = true }
rand_core = { version = "0.6", default-features = false } rand_core = { version = "0.6", default-features = false }
zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] } zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] }
subtle = { version = "^2.4", default-features = false }
# Cryptographic dependencies # Cryptographic dependencies
curve25519-dalek = { version = "4", default-features = false, features = ["alloc", "zeroize"] } curve25519-dalek = { version = "4", default-features = false, features = ["alloc", "zeroize"] }
@@ -47,7 +46,6 @@ std = [
"rand_core/std", "rand_core/std",
"zeroize/std", "zeroize/std",
"subtle/std",
"monero-io/std", "monero-io/std",
"monero-generators/std", "monero-generators/std",

View File

@@ -167,19 +167,18 @@ impl<'a> AggregateRangeStatement<'a> {
let (y, z) = Self::transcript_A_S(transcript, A, S); let (y, z) = Self::transcript_A_S(transcript, A, S);
transcript = z; transcript = z;
let z = ScalarVector::powers(z, 3 + padded_pow_of_2);
let twos = ScalarVector::powers(Scalar::from(2u8), COMMITMENT_BITS); let twos = ScalarVector::powers(Scalar::from(2u8), COMMITMENT_BITS);
let l = [aL - z, sL]; let l = [aL - z[1], sL];
let y_pow_n = ScalarVector::powers(y, aR.len()); let y_pow_n = ScalarVector::powers(y, aR.len());
let mut r = [((aR + z) * &y_pow_n), sR * &y_pow_n]; let mut r = [((aR + z[1]) * &y_pow_n), sR * &y_pow_n];
{ {
let mut z_current = z * z;
for j in 0 .. padded_pow_of_2 { for j in 0 .. padded_pow_of_2 {
for i in 0 .. COMMITMENT_BITS { for i in 0 .. COMMITMENT_BITS {
r[0].0[(j * COMMITMENT_BITS) + i] += z_current * twos[i]; r[0].0[(j * COMMITMENT_BITS) + i] += z[2 + j] * twos[i];
} }
z_current *= z;
} }
} }
let t1 = (l[0].clone().inner_product(&r[1])) + (r[0].clone().inner_product(&l[1])); let t1 = (l[0].clone().inner_product(&r[1])) + (r[0].clone().inner_product(&l[1]));
@@ -216,10 +215,8 @@ impl<'a> AggregateRangeStatement<'a> {
let t_hat = l.clone().inner_product(&r); let t_hat = l.clone().inner_product(&r);
let mut tau_x = ((tau_2 * x) + tau_1) * x; let mut tau_x = ((tau_2 * x) + tau_1) * x;
{ {
let mut z_current = z * z; for (i, commitment) in witness.commitments.iter().enumerate() {
for commitment in &witness.commitments { tau_x += z[2 + i] * commitment.mask;
tau_x += z_current * commitment.mask;
z_current *= z;
} }
} }
let mu = alpha + (rho * x); let mu = alpha + (rho * x);
@@ -268,6 +265,7 @@ impl<'a> AggregateRangeStatement<'a> {
let (y, z) = Self::transcript_A_S(transcript, proof.A, proof.S); let (y, z) = Self::transcript_A_S(transcript, proof.A, proof.S);
transcript = z; transcript = z;
let z = ScalarVector::powers(z, 3 + padded_pow_of_2);
transcript = Self::transcript_T12(transcript, proof.T1, proof.T2); transcript = Self::transcript_T12(transcript, proof.T1, proof.T2);
let x = transcript; let x = transcript;
transcript = Self::transcript_tau_x_mu_t_hat(transcript, proof.tau_x, proof.mu, proof.t_hat); transcript = Self::transcript_tau_x_mu_t_hat(transcript, proof.tau_x, proof.mu, proof.t_hat);
@@ -293,18 +291,14 @@ impl<'a> AggregateRangeStatement<'a> {
// These will now sum to 0 if equal // These will now sum to 0 if equal
let weight = -weight; let weight = -weight;
verifier.0.h += weight * (z - (z * z)) * y_pow_n.sum(); verifier.0.h += weight * (z[1] - (z[2])) * y_pow_n.sum();
let mut z_current = z * z; for (i, commitment) in commitments.iter().enumerate() {
for commitment in &commitments { verifier.0.other.push((weight * z[2 + i], *commitment));
verifier.0.other.push((weight * z_current, *commitment));
z_current *= z;
} }
let mut z_current = z * z * z; for i in 0 .. padded_pow_of_2 {
for _ in 0 .. padded_pow_of_2 { verifier.0.h -= weight * z[3 + i] * twos.clone().sum();
verifier.0.h -= weight * z_current * twos.clone().sum();
z_current *= z;
} }
verifier.0.other.push((weight * x, proof.T1)); verifier.0.other.push((weight * x, proof.T1));
verifier.0.other.push((weight * (x * x), proof.T2)); verifier.0.other.push((weight * (x * x), proof.T2));
@@ -315,22 +309,23 @@ impl<'a> AggregateRangeStatement<'a> {
// 66 // 66
verifier.0.other.push((ip_weight, proof.A)); verifier.0.other.push((ip_weight, proof.A));
verifier.0.other.push((ip_weight * x, proof.S)); verifier.0.other.push((ip_weight * x, proof.S));
// TODO: g_sum // We can replace these with a g_sum, h_sum scalar in the batch verifier
// It'd trade `2 * ip_rows` scalar additions (per proof) for one scalar addition and an
// additional term in the MSM
let ip_z = ip_weight * z[1];
for i in 0 .. ip_rows { for i in 0 .. ip_rows {
verifier.0.g_bold[i] += ip_weight * -z; verifier.0.h_bold[i] += ip_z;
} }
// TODO: h_sum let neg_ip_z = -ip_z;
for i in 0 .. ip_rows { for i in 0 .. ip_rows {
verifier.0.h_bold[i] += ip_weight * z; verifier.0.g_bold[i] += neg_ip_z;
} }
{ {
let mut z_current = z * z;
for j in 0 .. padded_pow_of_2 { for j in 0 .. padded_pow_of_2 {
for i in 0 .. COMMITMENT_BITS { for i in 0 .. COMMITMENT_BITS {
let full_i = (j * COMMITMENT_BITS) + i; let full_i = (j * COMMITMENT_BITS) + i;
verifier.0.h_bold[full_i] += ip_weight * y_inv_pow_n[full_i] * z_current * twos[i]; verifier.0.h_bold[full_i] += ip_weight * y_inv_pow_n[full_i] * z[2 + j] * twos[i];
} }
z_current *= z;
} }
} }
verifier.0.h += ip_weight * x_ip * proof.t_hat; verifier.0.h += ip_weight * x_ip * proof.t_hat;

View File

@@ -166,6 +166,17 @@ impl WipStatement {
let mut g_bold = PointVector(g_bold); let mut g_bold = PointVector(g_bold);
let mut h_bold = PointVector(h_bold); let mut h_bold = PointVector(h_bold);
let mut y_inv = {
let mut i = 1;
let mut to_invert = vec![];
while i < g_bold.len() {
to_invert.push(y[i - 1]);
i *= 2;
}
Scalar::batch_invert(&mut to_invert);
to_invert
};
// Check P has the expected relationship // Check P has the expected relationship
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
{ {
@@ -219,8 +230,7 @@ impl WipStatement {
let c_l = a1.clone().weighted_inner_product(&b2, &y); let c_l = a1.clone().weighted_inner_product(&b2, &y);
let c_r = (a2.clone() * y_n_hat).weighted_inner_product(&b1, &y); let c_r = (a2.clone() * y_n_hat).weighted_inner_product(&b1, &y);
// TODO: Calculate these with a batch inversion let y_inv_n_hat = y_inv.pop().unwrap();
let y_inv_n_hat = y_n_hat.invert();
let mut L_terms = (a1.clone() * y_inv_n_hat) let mut L_terms = (a1.clone() * y_inv_n_hat)
.0 .0

View File

@@ -102,3 +102,26 @@ async fn test_decoy_rpc() {
rpc.get_output_distribution(1 .. 0).await.unwrap_err(); rpc.get_output_distribution(1 .. 0).await.unwrap_err();
} }
} }
// This test passes yet requires a mainnet node, which we don't have reliable access to in CI.
/*
#[tokio::test]
async fn test_zero_out_tx_o_indexes() {
use monero_rpc::Rpc;
let rpc = SimpleRequestRpc::new("https://node.sethforprivacy.com".to_string()).await.unwrap();
assert_eq!(
rpc
.get_o_indexes(
hex::decode("17ce4c8feeb82a6d6adaa8a89724b32bf4456f6909c7f84c8ce3ee9ebba19163")
.unwrap()
.try_into()
.unwrap()
)
.await
.unwrap(),
Vec::<u64>::new()
);
}
*/

View File

@@ -19,7 +19,7 @@ use zeroize::Zeroize;
use async_trait::async_trait; use async_trait::async_trait;
use curve25519_dalek::edwards::EdwardsPoint; use curve25519_dalek::edwards::{CompressedEdwardsY, EdwardsPoint};
use serde::{Serialize, Deserialize, de::DeserializeOwned}; use serde::{Serialize, Deserialize, de::DeserializeOwned};
use serde_json::{Value, json}; use serde_json::{Value, json};
@@ -28,6 +28,7 @@ use monero_serai::{
io::*, io::*,
transaction::{Input, Timelock, Transaction}, transaction::{Input, Timelock, Transaction},
block::Block, block::Block,
DEFAULT_LOCK_WINDOW,
}; };
use monero_address::Address; use monero_address::Address;
@@ -188,19 +189,24 @@ struct TransactionsResponse {
txs: Vec<TransactionResponse>, txs: Vec<TransactionResponse>,
} }
/// The response to an output query. /// The response to an query for the information of a RingCT output.
#[derive(Debug, Deserialize)] #[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct OutputResponse { pub struct OutputInformation {
/// The height of the block this output was added to the chain in. /// The block number of the block this output was added to the chain in.
///
/// This is equivalent to he height of the blockchain at the time the block was added.
pub height: usize, pub height: usize,
/// If the output is unlocked, per the node's local view. /// If the output is unlocked, per the node's local view.
pub unlocked: bool, pub unlocked: bool,
/// The output's key. /// The output's key.
pub key: String, ///
/// This is a CompressedEdwardsY, not an EdwardsPoint, as it may be invalid. CompressedEdwardsY
/// only asserts validity on decompression and allows representing compressed types.
pub key: CompressedEdwardsY,
/// The output's commitment. /// The output's commitment.
pub mask: String, pub commitment: EdwardsPoint,
/// The transaction which created this output. /// The transaction which created this output.
pub txid: String, pub transaction: [u8; 32],
} }
fn rpc_hex(value: &str) -> Result<Vec<u8>, RpcError> { fn rpc_hex(value: &str) -> Result<Vec<u8>, RpcError> {
@@ -497,7 +503,6 @@ pub trait Rpc: Sync + Clone + Debug {
/// This may be manipulated to unsafe levels and MUST be sanity checked. /// This may be manipulated to unsafe levels and MUST be sanity checked.
/// ///
/// This MUST NOT be expected to be deterministic in any way. /// This MUST NOT be expected to be deterministic in any way.
// TODO: Take a sanity check argument
async fn get_fee_rate(&self, priority: FeePriority) -> Result<FeeRate, RpcError> { async fn get_fee_rate(&self, priority: FeePriority) -> Result<FeeRate, RpcError> {
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct FeeResponse { struct FeeResponse {
@@ -788,7 +793,6 @@ pub trait Rpc: Sync + Clone + Debug {
} }
// If the Vec was empty, it would've been omitted, hence the unwrap_or // If the Vec was empty, it would've been omitted, hence the unwrap_or
// TODO: Test against a 0-output TX, such as the ones found in block 202612
Ok(res.unwrap_or(vec![])) Ok(res.unwrap_or(vec![]))
}; };
@@ -821,7 +825,7 @@ pub trait DecoyRpc: Sync + Clone + Debug {
) -> Result<Vec<u64>, RpcError>; ) -> Result<Vec<u64>, RpcError>;
/// Get the specified outputs from the RingCT (zero-amount) pool. /// Get the specified outputs from the RingCT (zero-amount) pool.
async fn get_outs(&self, indexes: &[u64]) -> Result<Vec<OutputResponse>, RpcError>; async fn get_outs(&self, indexes: &[u64]) -> Result<Vec<OutputInformation>, RpcError>;
/// Get the specified outputs from the RingCT (zero-amount) pool, but only return them if their /// Get the specified outputs from the RingCT (zero-amount) pool, but only return them if their
/// timelock has been satisfied. /// timelock has been satisfied.
@@ -829,16 +833,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>;
} }
@@ -941,7 +945,16 @@ impl<R: Rpc> DecoyRpc for R {
Ok(distribution) Ok(distribution)
} }
async fn get_outs(&self, indexes: &[u64]) -> Result<Vec<OutputResponse>, RpcError> { async fn get_outs(&self, indexes: &[u64]) -> Result<Vec<OutputInformation>, RpcError> {
#[derive(Debug, Deserialize)]
struct OutputResponse {
height: usize,
unlocked: bool,
key: String,
mask: String,
txid: String,
}
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct OutsResponse { struct OutsResponse {
status: String, status: String,
@@ -965,24 +978,38 @@ impl<R: Rpc> DecoyRpc for R {
Err(RpcError::InvalidNode("bad response to get_outs".to_string()))?; Err(RpcError::InvalidNode("bad response to get_outs".to_string()))?;
} }
Ok(res.outs) Ok(
res
.outs
.into_iter()
.map(|output| {
Ok(OutputInformation {
height: output.height,
unlocked: output.unlocked,
key: CompressedEdwardsY(
rpc_hex(&output.key)?
.try_into()
.map_err(|_| RpcError::InvalidNode("output key wasn't 32 bytes".to_string()))?,
),
commitment: rpc_point(&output.mask)?,
transaction: hash_hex(&output.txid)?,
})
})
.collect::<Result<Vec<_>, RpcError>>()?,
)
} }
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> {
let outs: Vec<OutputResponse> = self.get_outs(indexes).await?; let outs = 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(&outs.iter().map(|out| out.transaction).collect::<Vec<_>>()).await?
.get_transactions(
&outs.iter().map(|out| hash_hex(&out.txid)).collect::<Result<Vec<_>, _>>()?,
)
.await?
} else { } else {
vec![] vec![]
}; };
@@ -996,19 +1023,20 @@ impl<R: Rpc> DecoyRpc for R {
// decoy // decoy
// Only valid keys can be used in CLSAG proofs, hence the need for re-selection, yet // Only valid keys can be used in CLSAG proofs, hence the need for re-selection, yet
// invalid keys may honestly exist on the blockchain // invalid keys may honestly exist on the blockchain
// Only a recent hard fork checked output keys were valid points let Some(key) = out.key.decompress() else {
let Some(key) = decompress_point(
rpc_hex(&out.key)?
.try_into()
.map_err(|_| RpcError::InvalidNode("non-32-byte point".to_string()))?,
) else {
return Ok(None); return Ok(None);
}; };
Ok(Some([key, rpc_point(&out.mask)?]).filter(|_| { Ok(Some([key, out.commitment]).filter(|_| {
if fingerprintable_canonical { if fingerprintable_deterministic {
// TODO: Are timelock blocks by height or number? // https://github.com/monero-project/monero/blob
// TODO: This doesn't check the default timelock has been passed // /cc73fe71162d564ffda8e549b79a350bca53c454/src/cryptonote_core/blockchain.cpp#L90
Timelock::Block(height) >= txs[i].prefix().additional_timelock const ACCEPTED_TIMELOCK_DELTA: usize = 1;
// https://github.com/monero-project/monero/blob
// /cc73fe71162d564ffda8e549b79a350bca53c454/src/cryptonote_core/blockchain.cpp#L3836
((out.height + DEFAULT_LOCK_WINDOW) <= height) &&
(Timelock::Block(height - 1 + ACCEPTED_TIMELOCK_DELTA) >=
txs[i].prefix().additional_timelock)
} else { } else {
out.unlocked out.unlocked
} }

View File

@@ -65,7 +65,6 @@ impl AddressType {
} }
/// The payment ID within this address. /// The payment ID within this address.
// TODO: wallet-core PaymentId? TX extra crate imported here?
pub fn payment_id(&self) -> Option<[u8; 8]> { pub fn payment_id(&self) -> Option<[u8; 8]> {
if let AddressType::LegacyIntegrated(id) = self { if let AddressType::LegacyIntegrated(id) = self {
Some(*id) Some(*id)
@@ -164,15 +163,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

@@ -102,7 +102,6 @@ impl SharedKeyDerivations {
} }
// H(8Ra || 0x8d) // H(8Ra || 0x8d)
// TODO: Make this itself a PaymentId
#[allow(clippy::needless_pass_by_value)] #[allow(clippy::needless_pass_by_value)]
fn payment_id_xor(ecdh: Zeroizing<EdwardsPoint>) -> [u8; 8] { fn payment_id_xor(ecdh: Zeroizing<EdwardsPoint>) -> [u8; 8] {
// 8Ra // 8Ra

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 {

View File

@@ -105,15 +105,13 @@ fn padding_only_max_size() {
#[test] #[test]
fn padding_only_exceed_max_size() { fn padding_only_exceed_max_size() {
let buf: Vec<u8> = vec![0; MAX_TX_EXTRA_PADDING_COUNT + 1]; let buf: Vec<u8> = vec![0; MAX_TX_EXTRA_PADDING_COUNT + 1];
let extra = Extra::read::<&[u8]>(&mut buf.as_ref()).unwrap(); Extra::read::<&[u8]>(&mut buf.as_ref()).unwrap_err();
assert!(extra.0.is_empty());
} }
#[test] #[test]
fn invalid_padding_only() { fn invalid_padding_only() {
let buf: Vec<u8> = vec![0, 42]; let buf: Vec<u8> = vec![0, 42];
let extra = Extra::read::<&[u8]>(&mut buf.as_ref()).unwrap(); Extra::read::<&[u8]>(&mut buf.as_ref()).unwrap_err();
assert!(extra.0.is_empty());
} }
#[test] #[test]
@@ -137,8 +135,7 @@ fn extra_nonce_only_wrong_size() {
let mut buf: Vec<u8> = vec![0; 20]; let mut buf: Vec<u8> = vec![0; 20];
buf[0] = 2; buf[0] = 2;
buf[1] = 255; buf[1] = 255;
let extra = Extra::read::<&[u8]>(&mut buf.as_ref()).unwrap(); Extra::read::<&[u8]>(&mut buf.as_ref()).unwrap_err();
assert!(extra.0.is_empty());
} }
#[test] #[test]
@@ -158,8 +155,7 @@ fn pub_key_and_padding() {
fn pub_key_and_invalid_padding() { fn pub_key_and_invalid_padding() {
let mut buf: Vec<u8> = PUB_KEY_BYTES.to_vec(); let mut buf: Vec<u8> = PUB_KEY_BYTES.to_vec();
buf.extend([0, 1]); buf.extend([0, 1]);
let extra = Extra::read::<&[u8]>(&mut buf.as_ref()).unwrap(); Extra::read::<&[u8]>(&mut buf.as_ref()).unwrap_err();
assert_eq!(extra.0, vec![ExtraField::PublicKey(pub_key())]);
} }
#[test] #[test]
@@ -185,8 +181,7 @@ fn extra_mysterious_minergate_only_wrong_size() {
let mut buf: Vec<u8> = vec![0; 20]; let mut buf: Vec<u8> = vec![0; 20];
buf[0] = 222; buf[0] = 222;
buf[1] = 255; buf[1] = 255;
let extra = Extra::read::<&[u8]>(&mut buf.as_ref()).unwrap(); Extra::read::<&[u8]>(&mut buf.as_ref()).unwrap_err();
assert!(extra.0.is_empty());
} }
#[test] #[test]

View File

@@ -2,7 +2,7 @@ use monero_simple_request_rpc::SimpleRequestRpc;
use monero_wallet::{ use monero_wallet::{
DEFAULT_LOCK_WINDOW, DEFAULT_LOCK_WINDOW,
transaction::Transaction, transaction::Transaction,
rpc::{OutputResponse, Rpc, DecoyRpc}, rpc::{Rpc, DecoyRpc},
WalletOutput, WalletOutput,
}; };
@@ -54,8 +54,7 @@ test!(
let most_recent_o_index = rpc.get_o_indexes(tx.hash()).await.unwrap().pop().unwrap(); let most_recent_o_index = rpc.get_o_indexes(tx.hash()).await.unwrap().pop().unwrap();
// Make sure output from tx1 is in the block in which it unlocks // Make sure output from tx1 is in the block in which it unlocks
let out_tx1: OutputResponse = let out_tx1 = rpc.get_outs(&[most_recent_o_index]).await.unwrap().swap_remove(0);
rpc.get_outs(&[most_recent_o_index]).await.unwrap().swap_remove(0);
assert_eq!(out_tx1.height, height - DEFAULT_LOCK_WINDOW); assert_eq!(out_tx1.height, height - DEFAULT_LOCK_WINDOW);
assert!(out_tx1.unlocked); assert!(out_tx1.unlocked);
@@ -133,8 +132,7 @@ test!(
let most_recent_o_index = rpc.get_o_indexes(tx.hash()).await.unwrap().pop().unwrap(); let most_recent_o_index = rpc.get_o_indexes(tx.hash()).await.unwrap().pop().unwrap();
// Make sure output from tx1 is in the block in which it unlocks // Make sure output from tx1 is in the block in which it unlocks
let out_tx1: OutputResponse = let out_tx1 = rpc.get_outs(&[most_recent_o_index]).await.unwrap().swap_remove(0);
rpc.get_outs(&[most_recent_o_index]).await.unwrap().swap_remove(0);
assert_eq!(out_tx1.height, height - DEFAULT_LOCK_WINDOW); assert_eq!(out_tx1.height, height - DEFAULT_LOCK_WINDOW);
assert!(out_tx1.unlocked); assert!(out_tx1.unlocked);

View File

@@ -7,7 +7,7 @@ pub fn bitcoin(orchestration_path: &Path, network: Network) {
const DOWNLOAD_BITCOIN: &str = r#" const DOWNLOAD_BITCOIN: &str = r#"
FROM alpine:latest as bitcoin FROM alpine:latest as bitcoin
ENV BITCOIN_VERSION=27.0 ENV BITCOIN_VERSION=27.1
RUN apk --no-cache add git gnupg RUN apk --no-cache add git gnupg