mirror of
https://github.com/serai-dex/serai.git
synced 2025-02-02 03:06:31 +00:00
995734c960
* Add v1 ring sig verifying * allow calculating signature hash for v1 txs * add unreduced scalar type with recovery I have added this type for borromen sigs, the ee field can be a normal scalar as in the verify function the ee field is checked against a reduced scalar mean for it to verify as correct ee must be reduced * change block major/ minor versions to u8 this matches Monero I have also changed a couple varint functions to accept the `VarInt` trait * expose `serialize_hashable` on `Block` * add back MLSAG verifying functions I still need to revert the commit removing support for >1 input MLSAG FULL This adds a new rct type to separate Full and simple rct * add back support for multiple inputs for RCT FULL * comment `non_adjacent_form` function also added `#[allow(clippy::needless_range_loop)]` around a loop as without a re-write satisfying clippy without it will make the function worse. * Improve Mlsag verifying API * fix rebase errors * revert the changes on `reserialize_chain` plus other misc changes * fix no-std * Reduce the amount of rpc calls needed for `get_block_by_number`. This function was causing me problems, every now and then a node would return a block with a different number than requested. * change `serialize_hashable` to give the POW hashing blob. Monero calculates the POW hash and the block hash using *slightly* different blobs :/ * make ring_signatures public and add length check when verifying. * Misc improvements and bug fixes --------- Co-authored-by: Luke Parker <lukeparker5132@gmail.com>
268 lines
8.4 KiB
Rust
268 lines
8.4 KiB
Rust
use core::ops::Deref;
|
|
use std_shims::collections::{HashSet, HashMap};
|
|
|
|
use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
|
|
|
|
use curve25519_dalek::{
|
|
constants::ED25519_BASEPOINT_TABLE,
|
|
scalar::Scalar,
|
|
edwards::{EdwardsPoint, CompressedEdwardsY},
|
|
};
|
|
|
|
use crate::{
|
|
hash, hash_to_scalar, serialize::write_varint, ringct::EncryptedAmount, transaction::Input,
|
|
};
|
|
|
|
pub mod extra;
|
|
pub(crate) use extra::{PaymentId, ExtraField, Extra};
|
|
|
|
/// Seed creation and parsing functionality.
|
|
pub mod seed;
|
|
|
|
/// Address encoding and decoding functionality.
|
|
pub mod address;
|
|
use address::{Network, AddressType, SubaddressIndex, AddressSpec, AddressMeta, MoneroAddress};
|
|
|
|
mod scan;
|
|
pub use scan::{ReceivedOutput, SpendableOutput, Timelocked};
|
|
|
|
pub mod decoys;
|
|
pub use decoys::Decoys;
|
|
|
|
mod send;
|
|
pub use send::{FeePriority, Fee, TransactionError, Change, SignableTransaction, Eventuality};
|
|
#[cfg(feature = "std")]
|
|
pub use send::SignableTransactionBuilder;
|
|
#[cfg(feature = "multisig")]
|
|
pub(crate) use send::InternalPayment;
|
|
#[cfg(feature = "multisig")]
|
|
pub use send::TransactionMachine;
|
|
|
|
fn key_image_sort(x: &EdwardsPoint, y: &EdwardsPoint) -> core::cmp::Ordering {
|
|
x.compress().to_bytes().cmp(&y.compress().to_bytes()).reverse()
|
|
}
|
|
|
|
// https://gist.github.com/kayabaNerve/8066c13f1fe1573286ba7a2fd79f6100
|
|
pub(crate) fn uniqueness(inputs: &[Input]) -> [u8; 32] {
|
|
let mut u = b"uniqueness".to_vec();
|
|
for input in inputs {
|
|
match input {
|
|
// If Gen, this should be the only input, making this loop somewhat pointless
|
|
// This works and even if there were somehow multiple inputs, it'd be a false negative
|
|
Input::Gen(height) => {
|
|
write_varint(height, &mut u).unwrap();
|
|
}
|
|
Input::ToKey { key_image, .. } => u.extend(key_image.compress().to_bytes()),
|
|
}
|
|
}
|
|
hash(&u)
|
|
}
|
|
|
|
// Hs("view_tag" || 8Ra || o), Hs(8Ra || o), and H(8Ra || 0x8d) with uniqueness inclusion in the
|
|
// Scalar as an option
|
|
#[allow(non_snake_case)]
|
|
pub(crate) fn shared_key(
|
|
uniqueness: Option<[u8; 32]>,
|
|
ecdh: EdwardsPoint,
|
|
o: usize,
|
|
) -> (u8, Scalar, [u8; 8]) {
|
|
// 8Ra
|
|
let mut output_derivation = ecdh.mul_by_cofactor().compress().to_bytes().to_vec();
|
|
|
|
let mut payment_id_xor = [0; 8];
|
|
payment_id_xor
|
|
.copy_from_slice(&hash(&[output_derivation.as_ref(), [0x8d].as_ref()].concat())[.. 8]);
|
|
|
|
// || o
|
|
write_varint(&o, &mut output_derivation).unwrap();
|
|
|
|
let view_tag = hash(&[b"view_tag".as_ref(), &output_derivation].concat())[0];
|
|
|
|
// uniqueness ||
|
|
let shared_key = if let Some(uniqueness) = uniqueness {
|
|
[uniqueness.as_ref(), &output_derivation].concat().to_vec()
|
|
} else {
|
|
output_derivation
|
|
};
|
|
|
|
(view_tag, hash_to_scalar(&shared_key), payment_id_xor)
|
|
}
|
|
|
|
pub(crate) fn commitment_mask(shared_key: Scalar) -> Scalar {
|
|
let mut mask = b"commitment_mask".to_vec();
|
|
mask.extend(shared_key.to_bytes());
|
|
hash_to_scalar(&mask)
|
|
}
|
|
|
|
pub(crate) fn amount_encryption(amount: u64, key: Scalar) -> [u8; 8] {
|
|
let mut amount_mask = b"amount".to_vec();
|
|
amount_mask.extend(key.to_bytes());
|
|
(amount ^ u64::from_le_bytes(hash(&amount_mask)[.. 8].try_into().unwrap())).to_le_bytes()
|
|
}
|
|
|
|
// TODO: Move this under EncryptedAmount?
|
|
fn amount_decryption(amount: &EncryptedAmount, key: Scalar) -> (Scalar, u64) {
|
|
match amount {
|
|
EncryptedAmount::Original { mask, amount } => {
|
|
#[cfg(feature = "experimental")]
|
|
{
|
|
let mask_shared_sec = hash(key.as_bytes());
|
|
let mask =
|
|
Scalar::from_bytes_mod_order(*mask) - Scalar::from_bytes_mod_order(mask_shared_sec);
|
|
|
|
let amount_shared_sec = hash(&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());
|
|
|
|
(mask, amount)
|
|
}
|
|
|
|
#[cfg(not(feature = "experimental"))]
|
|
{
|
|
let _ = mask;
|
|
let _ = amount;
|
|
todo!("decrypting a legacy monero transaction's amount")
|
|
}
|
|
}
|
|
EncryptedAmount::Compact { amount } => (
|
|
commitment_mask(key),
|
|
u64::from_le_bytes(amount_encryption(u64::from_le_bytes(*amount), key)),
|
|
),
|
|
}
|
|
}
|
|
|
|
/// The private view key and public spend key, enabling scanning transactions.
|
|
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
|
|
pub struct ViewPair {
|
|
spend: EdwardsPoint,
|
|
view: Zeroizing<Scalar>,
|
|
}
|
|
|
|
impl ViewPair {
|
|
pub fn new(spend: EdwardsPoint, view: Zeroizing<Scalar>) -> ViewPair {
|
|
ViewPair { spend, view }
|
|
}
|
|
|
|
pub fn spend(&self) -> EdwardsPoint {
|
|
self.spend
|
|
}
|
|
|
|
pub fn view(&self) -> EdwardsPoint {
|
|
self.view.deref() * ED25519_BASEPOINT_TABLE
|
|
}
|
|
|
|
fn subaddress_derivation(&self, index: SubaddressIndex) -> Scalar {
|
|
hash_to_scalar(&Zeroizing::new(
|
|
[
|
|
b"SubAddr\0".as_ref(),
|
|
Zeroizing::new(self.view.to_bytes()).as_ref(),
|
|
&index.account().to_le_bytes(),
|
|
&index.address().to_le_bytes(),
|
|
]
|
|
.concat(),
|
|
))
|
|
}
|
|
|
|
fn subaddress_keys(&self, index: SubaddressIndex) -> (EdwardsPoint, EdwardsPoint) {
|
|
let scalar = self.subaddress_derivation(index);
|
|
let spend = self.spend + (&scalar * ED25519_BASEPOINT_TABLE);
|
|
let view = self.view.deref() * spend;
|
|
(spend, view)
|
|
}
|
|
|
|
/// Returns an address with the provided specification.
|
|
pub fn address(&self, network: Network, spec: AddressSpec) -> MoneroAddress {
|
|
let mut spend = self.spend;
|
|
let mut view: EdwardsPoint = self.view.deref() * ED25519_BASEPOINT_TABLE;
|
|
|
|
// construct the address meta
|
|
let meta = match spec {
|
|
AddressSpec::Standard => AddressMeta::new(network, AddressType::Standard),
|
|
AddressSpec::Integrated(payment_id) => {
|
|
AddressMeta::new(network, AddressType::Integrated(payment_id))
|
|
}
|
|
AddressSpec::Subaddress(index) => {
|
|
(spend, view) = self.subaddress_keys(index);
|
|
AddressMeta::new(network, AddressType::Subaddress)
|
|
}
|
|
AddressSpec::Featured { subaddress, payment_id, guaranteed } => {
|
|
if let Some(index) = subaddress {
|
|
(spend, view) = self.subaddress_keys(index);
|
|
}
|
|
AddressMeta::new(
|
|
network,
|
|
AddressType::Featured { subaddress: subaddress.is_some(), payment_id, guaranteed },
|
|
)
|
|
}
|
|
};
|
|
|
|
MoneroAddress::new(meta, spend, view)
|
|
}
|
|
}
|
|
|
|
/// Transaction scanner.
|
|
/// This scanner is capable of generating subaddresses, additionally scanning for them once they've
|
|
/// been explicitly generated. If the burning bug is attempted, any secondary outputs will be
|
|
/// ignored.
|
|
#[derive(Clone)]
|
|
pub struct Scanner {
|
|
pair: ViewPair,
|
|
// Also contains the spend key as None
|
|
pub(crate) subaddresses: HashMap<CompressedEdwardsY, Option<SubaddressIndex>>,
|
|
pub(crate) burning_bug: Option<HashSet<CompressedEdwardsY>>,
|
|
}
|
|
|
|
impl Zeroize for Scanner {
|
|
fn zeroize(&mut self) {
|
|
self.pair.zeroize();
|
|
|
|
// These may not be effective, unfortunately
|
|
for (mut key, mut value) in self.subaddresses.drain() {
|
|
key.zeroize();
|
|
value.zeroize();
|
|
}
|
|
if let Some(ref mut burning_bug) = self.burning_bug.take() {
|
|
for mut output in burning_bug.drain() {
|
|
output.zeroize();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Drop for Scanner {
|
|
fn drop(&mut self) {
|
|
self.zeroize();
|
|
}
|
|
}
|
|
|
|
impl ZeroizeOnDrop for Scanner {}
|
|
|
|
impl Scanner {
|
|
/// Create a Scanner from a ViewPair.
|
|
///
|
|
/// burning_bug is a HashSet of used keys, intended to prevent key reuse which would burn funds.
|
|
///
|
|
/// When an output is successfully scanned, the output key MUST be saved to disk.
|
|
///
|
|
/// When a new scanner is created, ALL saved output keys must be passed in to be secure.
|
|
///
|
|
/// If None is passed, a modified shared key derivation is used which is immune to the burning
|
|
/// bug (specifically the Guaranteed feature from Featured Addresses).
|
|
pub fn from_view(pair: ViewPair, burning_bug: Option<HashSet<CompressedEdwardsY>>) -> Scanner {
|
|
let mut subaddresses = HashMap::new();
|
|
subaddresses.insert(pair.spend.compress(), None);
|
|
Scanner { pair, subaddresses, burning_bug }
|
|
}
|
|
|
|
/// Register a subaddress.
|
|
// There used to be an address function here, yet it wasn't safe. It could generate addresses
|
|
// incompatible with the Scanner. While we could return None for that, then we have the issue
|
|
// of runtime failures to generate an address.
|
|
// Removing that API was the simplest option.
|
|
pub fn register_subaddress(&mut self, subaddress: SubaddressIndex) {
|
|
let (spend, _) = self.pair.subaddress_keys(subaddress);
|
|
self.subaddresses.insert(spend.compress(), Some(subaddress));
|
|
}
|
|
}
|