Merge branch 'develop' into next

This is an initial resolution of conflicts which does not work.
This commit is contained in:
Luke Parker
2025-01-30 00:56:29 -05:00
128 changed files with 1835 additions and 44261 deletions

View File

@@ -302,7 +302,8 @@ impl Extra {
// `fill_buf` returns the current buffer, filled if empty, only empty if the reader is
// exhausted
while !r.fill_buf()?.is_empty() {
res.0.push(ExtraField::read(r)?);
let Ok(field) = ExtraField::read(r) else { break };
res.0.push(field);
}
Ok(res)
}

View File

@@ -33,7 +33,7 @@ pub(crate) mod output;
pub use output::WalletOutput;
mod scan;
pub use scan::{ScanError, Scanner, GuaranteedScanner};
pub use scan::{Timelocked, ScanError, Scanner, GuaranteedScanner};
mod decoys;
pub use decoys::OutputWithDecoys;
@@ -137,15 +137,13 @@ impl SharedKeyDerivations {
fn decrypt(&self, enc_amount: &EncryptedAmount) -> Commitment {
match enc_amount {
// TODO: Add a test vector for this
EncryptedAmount::Original { mask, amount } => {
let mask_shared_sec = keccak256(self.shared_key.as_bytes());
let mask =
Scalar::from_bytes_mod_order(*mask) - Scalar::from_bytes_mod_order(mask_shared_sec);
let mask_shared_sec_scalar = keccak256_to_scalar(self.shared_key.as_bytes());
let amount_shared_sec_scalar = keccak256_to_scalar(mask_shared_sec_scalar.as_bytes());
let mask = Scalar::from_bytes_mod_order(*mask) - mask_shared_sec_scalar;
let amount_scalar = Scalar::from_bytes_mod_order(*amount) - amount_shared_sec_scalar;
let amount_shared_sec = keccak256(mask_shared_sec);
let amount_scalar =
Scalar::from_bytes_mod_order(*amount) - Scalar::from_bytes_mod_order(amount_shared_sec);
// d2b from rctTypes.cpp
let amount = u64::from_le_bytes(amount_scalar.to_bytes()[0 .. 8].try_into().unwrap());

View File

@@ -41,9 +41,9 @@ impl Timelocked {
///
/// `block` is the block number of the block the additional timelock must be satsified by.
///
/// `time` is represented in seconds since the epoch. Please note Monero uses an on-chain
/// deterministic clock for time which is subject to variance from the real world time. This time
/// argument will be evaluated against Monero's clock, not the local system's clock.
/// `time` is represented in seconds since the epoch and is in terms of Monero's on-chain clock.
/// That means outputs whose additional timelocks are statisfied by `Instant::now()` (the time
/// according to the local system clock) may still be locked due to variance with Monero's clock.
#[must_use]
pub fn additional_timelock_satisfied_by(self, block: usize, time: u64) -> Vec<WalletOutput> {
let mut res = vec![];

View File

@@ -29,6 +29,7 @@ use crate::{
};
mod tx_keys;
pub use tx_keys::TransactionKeys;
mod tx;
mod eventuality;
pub use eventuality::Eventuality;

View File

@@ -1,7 +1,7 @@
use core::ops::Deref;
use std_shims::{vec, vec::Vec};
use zeroize::Zeroizing;
use zeroize::{Zeroize, Zeroizing};
use rand_core::SeedableRng;
use rand_chacha::ChaCha20Rng;
@@ -15,28 +15,61 @@ use crate::{
send::{ChangeEnum, InternalPayment, SignableTransaction, key_image_sort},
};
fn seeded_rng(
dst: &'static [u8],
outgoing_view_key: &[u8; 32],
mut input_keys: Vec<EdwardsPoint>,
) -> ChaCha20Rng {
// Apply the DST
let mut transcript = Zeroizing::new(vec![u8::try_from(dst.len()).unwrap()]);
transcript.extend(dst);
// Bind to the outgoing view key to prevent foreign entities from rebuilding the transcript
transcript.extend(outgoing_view_key);
// We sort the inputs here to ensure a consistent order
// We use the key image sort as it's applicable and well-defined, not because these are key
// images
input_keys.sort_by(key_image_sort);
// Ensure uniqueness across transactions by binding to a use-once object
// The keys for the inputs is binding to their key images, making them use-once
for key in input_keys {
transcript.extend(key.compress().to_bytes());
}
let res = ChaCha20Rng::from_seed(keccak256(&transcript));
transcript.zeroize();
res
}
/// An iterator yielding an endless amount of ephemeral keys to use within a transaction.
///
/// This is used when sending and can be used after sending to re-derive the keys used, as
/// necessary for payment proofs.
pub struct TransactionKeys(ChaCha20Rng);
impl TransactionKeys {
/// Construct a new `TransactionKeys`.
///
/// `input_keys` is the list of keys from the outputs spent within this transaction.
pub fn new(outgoing_view_key: &Zeroizing<[u8; 32]>, input_keys: Vec<EdwardsPoint>) -> Self {
Self(seeded_rng(b"transaction_keys", outgoing_view_key, input_keys))
}
}
impl Iterator for TransactionKeys {
type Item = Zeroizing<Scalar>;
fn next(&mut self) -> Option<Self::Item> {
Some(Zeroizing::new(Scalar::random(&mut self.0)))
}
}
impl SignableTransaction {
fn input_keys(&self) -> Vec<EdwardsPoint> {
self.inputs.iter().map(OutputWithDecoys::key).collect()
}
pub(crate) fn seeded_rng(&self, dst: &'static [u8]) -> ChaCha20Rng {
// Apply the DST
let mut transcript = Zeroizing::new(vec![u8::try_from(dst.len()).unwrap()]);
transcript.extend(dst);
// Bind to the outgoing view key to prevent foreign entities from rebuilding the transcript
transcript.extend(self.outgoing_view_key.as_slice());
// Ensure uniqueness across transactions by binding to a use-once object
// The keys for the inputs is binding to their key images, making them use-once
let mut input_keys = self.inputs.iter().map(OutputWithDecoys::key).collect::<Vec<_>>();
// We sort the inputs mid-way through TX construction, so apply our own sort to ensure a
// consistent order
// We use the key image sort as it's applicable and well-defined, not because these are key
// images
input_keys.sort_by(key_image_sort);
for key in input_keys {
transcript.extend(key.compress().to_bytes());
}
ChaCha20Rng::from_seed(keccak256(&transcript))
seeded_rng(dst, &self.outgoing_view_key, self.input_keys())
}
fn has_payments_to_subaddresses(&self) -> bool {
@@ -81,14 +114,14 @@ impl SignableTransaction {
// Calculate the transaction keys used as randomness.
fn transaction_keys(&self) -> (Zeroizing<Scalar>, Vec<Zeroizing<Scalar>>) {
let mut rng = self.seeded_rng(b"transaction_keys");
let mut tx_keys = TransactionKeys::new(&self.outgoing_view_key, self.input_keys());
let tx_key = Zeroizing::new(Scalar::random(&mut rng));
let tx_key = tx_keys.next().unwrap();
let mut additional_keys = vec![];
if self.should_use_additional_keys() {
for _ in 0 .. self.payments.len() {
additional_keys.push(Zeroizing::new(Scalar::random(&mut rng)));
additional_keys.push(tx_keys.next().unwrap());
}
}
(tx_key, additional_keys)

View File

@@ -1 +1,2 @@
mod extra;
mod scan;

View File

@@ -0,0 +1,168 @@
use monero_rpc::ScannableBlock;
use crate::{
transaction::{Pruned, Transaction},
block::Block,
ViewPair, Scanner, WalletOutput,
output::{AbsoluteId, RelativeId, OutputData, Metadata},
Commitment,
PaymentId::Encrypted,
transaction::Timelock,
ringct::EncryptedAmount,
};
use zeroize::Zeroizing;
use curve25519_dalek::{Scalar, constants::ED25519_BASEPOINT_TABLE, edwards::CompressedEdwardsY};
const SPEND_KEY: &str = "ccf0ea10e1ea64354f42fa710c2b318e581969cf49046d809d1f0aadb3fc7a02";
const VIEW_KEY: &str = "a28b4b2085592881df94ee95da332c16b5bb773eb8bb74730208cbb236c73806";
#[rustfmt::skip]
const PRUNED_TX_WITH_LONG_ENCRYPTED_AMOUNT: &str = "020001020003060101cf60390bb71aa15eb24037772012d59dc68cb4b6211e1c93206db09a6c346261020002ee8ca293511571c0005e1c144e49d09b8ff03046dbafb3e064a34cb9fc1994b600029e2e5cd08c8681dbcf2ce66071467e835f7e86613fbfed3c4fb170127b94e1072c01d3ce2a622c6e06ed465f81017dd6188c3a6e3d8e65a846f9c98416da0e150a82020901c553d35e54111bd001e0bbcbf289d701ce90e309ead2b487ec1d4d8af5d649543eb99a7620f6b54e532898527be29704f050e6f06de61e5967b2ddd506b4d6d36546065d6aae156ac7bec18c99580c07867fb98cb29853edbafec91af2df605c12f9aaa81a9165625afb6649f5a652012c5ba6612351140e1fb4a8463cc765d0a9bb7d999ba35750f365c5285d77230b76c7a612784f4845812a2899f2ca6a304fee61362db59b263115c27d2ce78af6b1d9e939c1f4036c7707851f41abe6458cf1c748353e593469ebf43536a939f7";
#[rustfmt::skip]
const BLOCK: &str = "0202e8e28efe04db09e2fc4d57854786220bd33e0169ff692440d27ae3932b9219df9ab1d7260b00000000014101ff050580d0acf30e02704972eb1878e94686b62fa4c0202f3e7e3a263073bd6edd751990ea769494ee80c0fc82aa0202edac72ab7c5745d4acaa95f76a3b76e238a55743cd51efb586f968e09821788d80d0dbc3f40202f9b4cf3141aac4203a1aaed01f09326615544997d1b68964928d9aafd07e38e580a0e5b9c29101023405e3aa75b1b7adf04e8c7faa3c3d45616ae740a8b11fb7cc1555dd8b9e4c9180c0dfda8ee90602d2b78accfe1c2ae57bed4fe3385f7735a988f160ef3bbc1f9d7a0c911c26ffd92101d2d55b5066d247a97696be4a84bf70873e4f149687f57e606eb6682f11650e1701b74773bbea995079805398052da9b69244bda034b089b50e4d9151dedb59a12f";
const OUTPUT_INDEX_FOR_FIRST_RINGCT_OUTPUT: u64 = 0; // note the miner tx is a v1 tx
fn wallet_output0() -> WalletOutput {
WalletOutput {
absolute_id: AbsoluteId {
transaction: hex::decode("b74773bbea995079805398052da9b69244bda034b089b50e4d9151dedb59a12f")
.unwrap()
.try_into()
.unwrap(),
index_in_transaction: 0,
},
relative_id: RelativeId { index_on_blockchain: OUTPUT_INDEX_FOR_FIRST_RINGCT_OUTPUT },
data: OutputData {
key: CompressedEdwardsY(
hex::decode("ee8ca293511571c0005e1c144e49d09b8ff03046dbafb3e064a34cb9fc1994b6")
.unwrap()
.try_into()
.unwrap(),
)
.decompress()
.unwrap(),
key_offset: Scalar::from_canonical_bytes(
hex::decode("f1d21a76ea0bb228fbc5f0dece0597a8ffb59de7a04b29f70b7c0310446ea905")
.unwrap()
.try_into()
.unwrap(),
)
.unwrap(),
commitment: Commitment {
amount: 10000,
mask: Scalar::from_canonical_bytes(
hex::decode("05c2f142aaf3054cbff0a022f6c7cb75403fd92af0f9441c072ade3f273f7706")
.unwrap()
.try_into()
.unwrap(),
)
.unwrap(),
},
},
metadata: Metadata {
additional_timelock: Timelock::None,
subaddress: None,
payment_id: Some(Encrypted([0, 0, 0, 0, 0, 0, 0, 0])),
arbitrary_data: [].to_vec(),
},
}
}
fn wallet_output1() -> WalletOutput {
WalletOutput {
absolute_id: AbsoluteId {
transaction: hex::decode("b74773bbea995079805398052da9b69244bda034b089b50e4d9151dedb59a12f")
.unwrap()
.try_into()
.unwrap(),
index_in_transaction: 1,
},
relative_id: RelativeId { index_on_blockchain: OUTPUT_INDEX_FOR_FIRST_RINGCT_OUTPUT + 1 },
data: OutputData {
key: CompressedEdwardsY(
hex::decode("9e2e5cd08c8681dbcf2ce66071467e835f7e86613fbfed3c4fb170127b94e107")
.unwrap()
.try_into()
.unwrap(),
)
.decompress()
.unwrap(),
key_offset: Scalar::from_canonical_bytes(
hex::decode("c5189738c1cb40e68d464f1a1848a85f6ab2c09652a31849213dc0fefd212806")
.unwrap()
.try_into()
.unwrap(),
)
.unwrap(),
commitment: Commitment {
amount: 10000,
mask: Scalar::from_canonical_bytes(
hex::decode("c8922ce32cb2bf454a6b77bc91423ba7a18412b71fa39a97a2a743c1fe0bad04")
.unwrap()
.try_into()
.unwrap(),
)
.unwrap(),
},
},
metadata: Metadata {
additional_timelock: Timelock::None,
subaddress: None,
payment_id: Some(Encrypted([0, 0, 0, 0, 0, 0, 0, 0])),
arbitrary_data: [].to_vec(),
},
}
}
#[test]
fn scan_long_encrypted_amount() {
// Parse strings
let spend_key_buf = hex::decode(SPEND_KEY).unwrap();
let spend_key =
Zeroizing::new(Scalar::from_canonical_bytes(spend_key_buf.try_into().unwrap()).unwrap());
let view_key_buf = hex::decode(VIEW_KEY).unwrap();
let view_key =
Zeroizing::new(Scalar::from_canonical_bytes(view_key_buf.try_into().unwrap()).unwrap());
let tx_buf = hex::decode(PRUNED_TX_WITH_LONG_ENCRYPTED_AMOUNT).unwrap();
let tx = Transaction::<Pruned>::read::<&[u8]>(&mut tx_buf.as_ref()).unwrap();
let block_buf = hex::decode(BLOCK).unwrap();
let block = Block::read::<&[u8]>(&mut block_buf.as_ref()).unwrap();
// Confirm tx has long form encrypted amounts
match &tx {
Transaction::V2 { prefix: _, proofs } => {
let proofs = proofs.clone().unwrap();
assert_eq!(proofs.base.encrypted_amounts.len(), 2);
assert!(proofs
.base
.encrypted_amounts
.iter()
.all(|o| matches!(o, EncryptedAmount::Original { .. })));
}
_ => panic!("Unexpected tx version"),
};
// Prepare scanner
let spend_pub = &*spend_key * ED25519_BASEPOINT_TABLE;
let view: ViewPair = ViewPair::new(spend_pub, view_key).unwrap();
let mut scanner = Scanner::new(view);
// Prepare scannable block
let txs: Vec<Transaction<Pruned>> = vec![tx];
let scannable_block = ScannableBlock {
block,
transactions: txs,
output_index_for_first_ringct_output: Some(OUTPUT_INDEX_FOR_FIRST_RINGCT_OUTPUT),
};
// Scan the block
let outputs = scanner.scan(scannable_block).unwrap().not_additionally_locked();
assert_eq!(outputs.len(), 2);
assert_eq!(outputs[0], wallet_output0());
assert_eq!(outputs[1], wallet_output1());
}