mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-08 12:19:24 +00:00
Implement Guaranteed Addresses
Closes https://github.com/serai-dex/serai/issues/27. monero-rs is now solely used for Extra encoding.
This commit is contained in:
@@ -7,6 +7,7 @@ authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
hex-literal = "0.3"
|
||||||
lazy_static = "1"
|
lazy_static = "1"
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
|
|
||||||
@@ -26,6 +27,7 @@ dalek-ff-group = { path = "../../crypto/dalek-ff-group", optional = true }
|
|||||||
transcript = { package = "flexible-transcript", path = "../../crypto/transcript", features = ["recommended"], optional = true }
|
transcript = { package = "flexible-transcript", path = "../../crypto/transcript", features = ["recommended"], optional = true }
|
||||||
frost = { package = "modular-frost", path = "../../crypto/frost", features = ["ed25519"], optional = true }
|
frost = { package = "modular-frost", path = "../../crypto/frost", features = ["ed25519"], optional = true }
|
||||||
|
|
||||||
|
base58-monero = "1"
|
||||||
monero = "0.16"
|
monero = "0.16"
|
||||||
|
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
|
|||||||
45
coins/monero/src/tests/address.rs
Normal file
45
coins/monero/src/tests/address.rs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
use hex_literal::hex;
|
||||||
|
|
||||||
|
use crate::wallet::address::{Network, AddressType, Address};
|
||||||
|
|
||||||
|
const SPEND: [u8; 32] = hex!("f8631661f6ab4e6fda310c797330d86e23a682f20d5bc8cc27b18051191f16d7");
|
||||||
|
const VIEW: [u8; 32] = hex!("4a1535063ad1fee2dabbf909d4fd9a873e29541b401f0944754e17c9a41820ce");
|
||||||
|
|
||||||
|
const STANDARD: &'static str = "4B33mFPMq6mKi7Eiyd5XuyKRVMGVZz1Rqb9ZTyGApXW5d1aT7UBDZ89ewmnWFkzJ5wPd2SFbn313vCT8a4E2Qf4KQH4pNey";
|
||||||
|
|
||||||
|
const PAYMENT_ID: [u8; 8] = hex!("b8963a57855cf73f");
|
||||||
|
const INTEGRATED: &'static str = "4Ljin4CrSNHKi7Eiyd5XuyKRVMGVZz1Rqb9ZTyGApXW5d1aT7UBDZ89ewmnWFkzJ5wPd2SFbn313vCT8a4E2Qf4KbaTH6MnpXSn88oBX35";
|
||||||
|
|
||||||
|
const SUB_SPEND: [u8; 32] = hex!("fe358188b528335ad1cfdc24a22a23988d742c882b6f19a602892eaab3c1b62b");
|
||||||
|
const SUB_VIEW: [u8; 32] = hex!("9bc2b464de90d058468522098d5610c5019c45fd1711a9517db1eea7794f5470");
|
||||||
|
const SUBADDRESS: &'static str = "8C5zHM5ud8nGC4hC2ULiBLSWx9infi8JUUmWEat4fcTf8J4H38iWYVdFmPCA9UmfLTZxD43RsyKnGEdZkoGij6csDeUnbEB";
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn standard_address() {
|
||||||
|
let addr = Address::from_str(STANDARD, Network::Mainnet).unwrap();
|
||||||
|
assert_eq!(addr.meta.network, Network::Mainnet);
|
||||||
|
assert_eq!(addr.meta.kind, AddressType::Standard);
|
||||||
|
assert_eq!(addr.meta.guaranteed, false);
|
||||||
|
assert_eq!(addr.spend.compress().to_bytes(), SPEND);
|
||||||
|
assert_eq!(addr.view.compress().to_bytes(), VIEW);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn integrated_address() {
|
||||||
|
let addr = Address::from_str(INTEGRATED, Network::Mainnet).unwrap();
|
||||||
|
assert_eq!(addr.meta.network, Network::Mainnet);
|
||||||
|
assert_eq!(addr.meta.kind, AddressType::Integrated(PAYMENT_ID));
|
||||||
|
assert_eq!(addr.meta.guaranteed, false);
|
||||||
|
assert_eq!(addr.spend.compress().to_bytes(), SPEND);
|
||||||
|
assert_eq!(addr.view.compress().to_bytes(), VIEW);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn subaddress() {
|
||||||
|
let addr = Address::from_str(SUBADDRESS, Network::Mainnet).unwrap();
|
||||||
|
assert_eq!(addr.meta.network, Network::Mainnet);
|
||||||
|
assert_eq!(addr.meta.kind, AddressType::Subaddress);
|
||||||
|
assert_eq!(addr.meta.guaranteed, false);
|
||||||
|
assert_eq!(addr.spend.compress().to_bytes(), SUB_SPEND);
|
||||||
|
assert_eq!(addr.view.compress().to_bytes(), SUB_VIEW);
|
||||||
|
}
|
||||||
@@ -1 +1,2 @@
|
|||||||
mod clsag;
|
mod clsag;
|
||||||
|
mod address;
|
||||||
|
|||||||
152
coins/monero/src/wallet/address.rs
Normal file
152
coins/monero/src/wallet/address.rs
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
use std::string::ToString;
|
||||||
|
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, edwards::{EdwardsPoint, CompressedEdwardsY}};
|
||||||
|
|
||||||
|
use base58_monero::base58::{encode_check, decode_check};
|
||||||
|
|
||||||
|
use crate::wallet::ViewPair;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
|
pub enum Network {
|
||||||
|
Mainnet,
|
||||||
|
Testnet,
|
||||||
|
Stagenet
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
|
pub enum AddressType {
|
||||||
|
Standard,
|
||||||
|
Integrated([u8; 8]),
|
||||||
|
Subaddress
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AddressType {
|
||||||
|
fn network_bytes(network: Network) -> (u8, u8, u8) {
|
||||||
|
match network {
|
||||||
|
Network::Mainnet => (18, 19, 42),
|
||||||
|
Network::Testnet => (53, 54, 63),
|
||||||
|
Network::Stagenet => (24, 25, 36)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
|
pub struct AddressMeta {
|
||||||
|
pub network: Network,
|
||||||
|
pub kind: AddressType,
|
||||||
|
pub guaranteed: bool
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Error, Debug)]
|
||||||
|
pub enum AddressError {
|
||||||
|
#[error("invalid address byte")]
|
||||||
|
InvalidByte,
|
||||||
|
#[error("invalid address encoding")]
|
||||||
|
InvalidEncoding,
|
||||||
|
#[error("invalid length")]
|
||||||
|
InvalidLength,
|
||||||
|
#[error("different network than expected")]
|
||||||
|
DifferentNetwork,
|
||||||
|
#[error("invalid key")]
|
||||||
|
InvalidKey
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AddressMeta {
|
||||||
|
fn to_byte(&self) -> u8 {
|
||||||
|
let bytes = AddressType::network_bytes(self.network);
|
||||||
|
let byte = match self.kind {
|
||||||
|
AddressType::Standard => bytes.0,
|
||||||
|
AddressType::Integrated(_) => bytes.1,
|
||||||
|
AddressType::Subaddress => bytes.2
|
||||||
|
};
|
||||||
|
byte | (if self.guaranteed { 1 << 7 } else { 0 })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns an incomplete type in the case of Integrated addresses
|
||||||
|
fn from_byte(byte: u8) -> Result<AddressMeta, AddressError> {
|
||||||
|
let actual = byte & 0b01111111;
|
||||||
|
let guaranteed = (byte >> 7) == 1;
|
||||||
|
|
||||||
|
let mut meta = None;
|
||||||
|
for network in [Network::Mainnet, Network::Testnet, Network::Stagenet] {
|
||||||
|
let (standard, integrated, subaddress) = AddressType::network_bytes(network);
|
||||||
|
if let Some(kind) = match actual {
|
||||||
|
_ if actual == standard => Some(AddressType::Standard),
|
||||||
|
_ if actual == integrated => Some(AddressType::Integrated([0; 8])),
|
||||||
|
_ if actual == subaddress => Some(AddressType::Subaddress),
|
||||||
|
_ => None
|
||||||
|
} {
|
||||||
|
meta = Some(AddressMeta { network, kind, guaranteed });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
meta.ok_or(AddressError::InvalidByte)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
|
pub struct Address {
|
||||||
|
pub meta: AddressMeta,
|
||||||
|
pub spend: EdwardsPoint,
|
||||||
|
pub view: EdwardsPoint
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ViewPair {
|
||||||
|
pub fn address(&self, network: Network, kind: AddressType, guaranteed: bool) -> Address {
|
||||||
|
Address {
|
||||||
|
meta: AddressMeta {
|
||||||
|
network,
|
||||||
|
kind,
|
||||||
|
guaranteed
|
||||||
|
},
|
||||||
|
spend: self.spend,
|
||||||
|
view: &self.view * &ED25519_BASEPOINT_TABLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToString for Address {
|
||||||
|
fn to_string(&self) -> String {
|
||||||
|
let mut data = vec![self.meta.to_byte()];
|
||||||
|
data.extend(self.spend.compress().to_bytes());
|
||||||
|
data.extend(self.view.compress().to_bytes());
|
||||||
|
if let AddressType::Integrated(id) = self.meta.kind {
|
||||||
|
data.extend(id);
|
||||||
|
}
|
||||||
|
encode_check(&data).unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Address {
|
||||||
|
pub fn from_str(s: &str, network: Network) -> Result<Self, AddressError> {
|
||||||
|
let raw = decode_check(s).map_err(|_| AddressError::InvalidEncoding)?;
|
||||||
|
if raw.len() == 1 {
|
||||||
|
Err(AddressError::InvalidLength)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut meta = AddressMeta::from_byte(raw[0])?;
|
||||||
|
if meta.network != network {
|
||||||
|
Err(AddressError::DifferentNetwork)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let len = match meta.kind {
|
||||||
|
AddressType::Standard | AddressType::Subaddress => 65,
|
||||||
|
AddressType::Integrated(_) => 73
|
||||||
|
};
|
||||||
|
if raw.len() != len {
|
||||||
|
Err(AddressError::InvalidLength)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let spend = CompressedEdwardsY(raw[1 .. 33].try_into().unwrap()).decompress().ok_or(AddressError::InvalidKey)?;
|
||||||
|
let view = CompressedEdwardsY(raw[33 .. 65].try_into().unwrap()).decompress().ok_or(AddressError::InvalidKey)?;
|
||||||
|
|
||||||
|
if let AddressType::Integrated(ref mut payment_id) = meta.kind {
|
||||||
|
payment_id.copy_from_slice(&raw[65 .. 73]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Address { meta, spend, view })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,8 @@ use crate::{
|
|||||||
transaction::Input
|
transaction::Input
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub mod address;
|
||||||
|
|
||||||
mod scan;
|
mod scan;
|
||||||
pub use scan::SpendableOutput;
|
pub use scan::SpendableOutput;
|
||||||
|
|
||||||
@@ -23,7 +25,7 @@ fn key_image_sort(x: &EdwardsPoint, y: &EdwardsPoint) -> std::cmp::Ordering {
|
|||||||
|
|
||||||
// https://github.com/monero-project/research-lab/issues/103
|
// https://github.com/monero-project/research-lab/issues/103
|
||||||
pub(crate) fn uniqueness(inputs: &[Input]) -> [u8; 32] {
|
pub(crate) fn uniqueness(inputs: &[Input]) -> [u8; 32] {
|
||||||
let mut u = b"domain_separator".to_vec();
|
let mut u = b"uniqueness".to_vec();
|
||||||
for input in inputs {
|
for input in inputs {
|
||||||
match input {
|
match input {
|
||||||
// If Gen, this should be the only input, making this loop somewhat pointless
|
// If Gen, this should be the only input, making this loop somewhat pointless
|
||||||
@@ -63,3 +65,9 @@ pub(crate) fn commitment_mask(shared_key: Scalar) -> Scalar {
|
|||||||
mask.extend(shared_key.to_bytes());
|
mask.extend(shared_key.to_bytes());
|
||||||
hash_to_scalar(&mask)
|
hash_to_scalar(&mask)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct ViewPair {
|
||||||
|
pub spend: EdwardsPoint,
|
||||||
|
pub view: Scalar
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ use crate::{
|
|||||||
Commitment,
|
Commitment,
|
||||||
serialize::{write_varint, read_32, read_scalar, read_point},
|
serialize::{write_varint, read_32, read_scalar, read_point},
|
||||||
transaction::{Timelock, Transaction},
|
transaction::{Timelock, Transaction},
|
||||||
wallet::{uniqueness, shared_key, amount_decryption, commitment_mask}
|
wallet::{ViewPair, uniqueness, shared_key, amount_decryption, commitment_mask}
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Debug)]
|
#[derive(Clone, PartialEq, Debug)]
|
||||||
@@ -55,8 +55,8 @@ impl SpendableOutput {
|
|||||||
impl Transaction {
|
impl Transaction {
|
||||||
pub fn scan(
|
pub fn scan(
|
||||||
&self,
|
&self,
|
||||||
view: Scalar,
|
view: ViewPair,
|
||||||
spend: EdwardsPoint
|
guaranteed: bool
|
||||||
) -> (Vec<SpendableOutput>, Timelock) {
|
) -> (Vec<SpendableOutput>, Timelock) {
|
||||||
let mut extra = vec![];
|
let mut extra = vec![];
|
||||||
write_varint(&u64::try_from(self.prefix.extra.len()).unwrap(), &mut extra).unwrap();
|
write_varint(&u64::try_from(self.prefix.extra.len()).unwrap(), &mut extra).unwrap();
|
||||||
@@ -82,52 +82,53 @@ impl Transaction {
|
|||||||
for (o, output) in self.prefix.outputs.iter().enumerate() {
|
for (o, output) in self.prefix.outputs.iter().enumerate() {
|
||||||
// TODO: This may be replaceable by pubkeys[o]
|
// TODO: This may be replaceable by pubkeys[o]
|
||||||
for pubkey in &pubkeys {
|
for pubkey in &pubkeys {
|
||||||
|
let key_offset = shared_key(
|
||||||
|
Some(uniqueness(&self.prefix.inputs)).filter(|_| guaranteed),
|
||||||
|
view.view,
|
||||||
|
pubkey,
|
||||||
|
o
|
||||||
|
);
|
||||||
|
// P - shared == spend
|
||||||
|
if (output.key - (&key_offset * &ED25519_BASEPOINT_TABLE)) != view.spend {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since we've found an output to us, get its amount
|
||||||
let mut commitment = Commitment::zero();
|
let mut commitment = Commitment::zero();
|
||||||
|
|
||||||
// P - shared == spend
|
// Miner transaction
|
||||||
let matches = |shared_key| (output.key - (&shared_key * &ED25519_BASEPOINT_TABLE)) == spend;
|
if output.amount != 0 {
|
||||||
let test = |shared_key| Some(shared_key).filter(|shared_key| matches(*shared_key));
|
commitment.amount = output.amount;
|
||||||
|
// Regular transaction
|
||||||
|
} else {
|
||||||
|
let amount = match self.rct_signatures.base.ecdh_info.get(o) {
|
||||||
|
Some(amount) => amount_decryption(*amount, key_offset),
|
||||||
|
// This should never happen, yet it may be possible with miner transactions?
|
||||||
|
// Using get just decreases the possibility of a panic and lets us move on in that case
|
||||||
|
None => break
|
||||||
|
};
|
||||||
|
|
||||||
// Get the traditional shared key and unique shared key, testing if either matches for this output
|
// Rebuild the commitment to verify it
|
||||||
let traditional = test(shared_key(None, view, pubkey, o));
|
commitment = Commitment::new(commitment_mask(key_offset), amount);
|
||||||
let unique = test(shared_key(Some(uniqueness(&self.prefix.inputs)), view, pubkey, o));
|
// If this is a malicious commitment, move to the next output
|
||||||
|
// Any other R value will calculate to a different spend key and are therefore ignorable
|
||||||
// If either matches, grab it and decode the amount
|
if Some(&commitment.calculate()) != self.rct_signatures.base.commitments.get(o) {
|
||||||
if let Some(key_offset) = traditional.or(unique) {
|
break;
|
||||||
// Miner transaction
|
|
||||||
if output.amount != 0 {
|
|
||||||
commitment.amount = output.amount;
|
|
||||||
// Regular transaction
|
|
||||||
} else {
|
|
||||||
let amount = match self.rct_signatures.base.ecdh_info.get(o) {
|
|
||||||
Some(amount) => amount_decryption(*amount, key_offset),
|
|
||||||
// This should never happen, yet it may be possible with miner transactions?
|
|
||||||
// Using get just decreases the possibility of a panic and lets us move on in that case
|
|
||||||
None => continue
|
|
||||||
};
|
|
||||||
|
|
||||||
// Rebuild the commitment to verify it
|
|
||||||
commitment = Commitment::new(commitment_mask(key_offset), amount);
|
|
||||||
// If this is a malicious commitment, move to the next output
|
|
||||||
// Any other R value will calculate to a different spend key and are therefore ignorable
|
|
||||||
if Some(&commitment.calculate()) != self.rct_signatures.base.commitments.get(o) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if commitment.amount != 0 {
|
|
||||||
res.push(SpendableOutput {
|
|
||||||
tx: self.hash(),
|
|
||||||
o: o.try_into().unwrap(),
|
|
||||||
key: output.key,
|
|
||||||
key_offset,
|
|
||||||
commitment
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Break to prevent public keys from being included multiple times, triggering multiple
|
|
||||||
// inclusions of the same output
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if commitment.amount != 0 {
|
||||||
|
res.push(SpendableOutput {
|
||||||
|
tx: self.hash(),
|
||||||
|
o: o.try_into().unwrap(),
|
||||||
|
key: output.key,
|
||||||
|
key_offset,
|
||||||
|
commitment
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Break to prevent public keys from being included multiple times, triggering multiple
|
||||||
|
// inclusions of the same output
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,11 +9,7 @@ use curve25519_dalek::{
|
|||||||
edwards::EdwardsPoint
|
edwards::EdwardsPoint
|
||||||
};
|
};
|
||||||
|
|
||||||
use monero::{
|
use monero::{consensus::Encodable, PublicKey, blockdata::transaction::SubField};
|
||||||
consensus::Encodable,
|
|
||||||
util::{key::PublicKey, address::{AddressType, Address}},
|
|
||||||
blockdata::transaction::SubField
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(feature = "multisig")]
|
#[cfg(feature = "multisig")]
|
||||||
use frost::FrostError;
|
use frost::FrostError;
|
||||||
@@ -29,7 +25,10 @@ use crate::{
|
|||||||
},
|
},
|
||||||
transaction::{Input, Output, Timelock, TransactionPrefix, Transaction},
|
transaction::{Input, Output, Timelock, TransactionPrefix, Transaction},
|
||||||
rpc::{Rpc, RpcError},
|
rpc::{Rpc, RpcError},
|
||||||
wallet::{SpendableOutput, Decoys, key_image_sort, uniqueness, shared_key, commitment_mask, amount_encryption}
|
wallet::{
|
||||||
|
address::{AddressType, Address}, SpendableOutput, Decoys,
|
||||||
|
key_image_sort, uniqueness, shared_key, commitment_mask, amount_encryption
|
||||||
|
}
|
||||||
};
|
};
|
||||||
#[cfg(feature = "multisig")]
|
#[cfg(feature = "multisig")]
|
||||||
use crate::frost::MultisigError;
|
use crate::frost::MultisigError;
|
||||||
@@ -52,23 +51,23 @@ impl SendOutput {
|
|||||||
fn new<R: RngCore + CryptoRng>(
|
fn new<R: RngCore + CryptoRng>(
|
||||||
rng: &mut R,
|
rng: &mut R,
|
||||||
unique: [u8; 32],
|
unique: [u8; 32],
|
||||||
output: (Address, u64, bool),
|
output: (Address, u64),
|
||||||
o: usize
|
o: usize
|
||||||
) -> SendOutput {
|
) -> SendOutput {
|
||||||
let r = random_scalar(rng);
|
let r = random_scalar(rng);
|
||||||
let shared_key = shared_key(
|
let shared_key = shared_key(
|
||||||
Some(unique).filter(|_| output.2),
|
Some(unique).filter(|_| output.0.meta.guaranteed),
|
||||||
r,
|
r,
|
||||||
&output.0.public_view.point.decompress().expect("SendOutput::new requires valid addresses"),
|
&output.0.view,
|
||||||
o
|
o
|
||||||
);
|
);
|
||||||
|
|
||||||
let spend = output.0.public_spend.point.decompress().expect("SendOutput::new requires valid addresses");
|
let spend = output.0.spend;
|
||||||
SendOutput {
|
SendOutput {
|
||||||
R: match output.0.addr_type {
|
R: match output.0.meta.kind {
|
||||||
AddressType::Standard => &r * &ED25519_BASEPOINT_TABLE,
|
AddressType::Standard => &r * &ED25519_BASEPOINT_TABLE,
|
||||||
AddressType::SubAddress => &r * spend,
|
AddressType::Integrated(_) => unimplemented!("SendOutput::new doesn't support Integrated addresses"),
|
||||||
AddressType::Integrated(_) => panic!("SendOutput::new doesn't support Integrated addresses")
|
AddressType::Subaddress => &r * spend
|
||||||
},
|
},
|
||||||
dest: ((&shared_key * &ED25519_BASEPOINT_TABLE) + spend),
|
dest: ((&shared_key * &ED25519_BASEPOINT_TABLE) + spend),
|
||||||
commitment: Commitment::new(commitment_mask(shared_key), output.1),
|
commitment: Commitment::new(commitment_mask(shared_key), output.1),
|
||||||
@@ -169,7 +168,7 @@ impl Fee {
|
|||||||
#[derive(Clone, PartialEq, Debug)]
|
#[derive(Clone, PartialEq, Debug)]
|
||||||
pub struct SignableTransaction {
|
pub struct SignableTransaction {
|
||||||
inputs: Vec<SpendableOutput>,
|
inputs: Vec<SpendableOutput>,
|
||||||
payments: Vec<(Address, u64, bool)>,
|
payments: Vec<(Address, u64)>,
|
||||||
outputs: Vec<SendOutput>,
|
outputs: Vec<SendOutput>,
|
||||||
fee: u64
|
fee: u64
|
||||||
}
|
}
|
||||||
@@ -177,23 +176,16 @@ pub struct SignableTransaction {
|
|||||||
impl SignableTransaction {
|
impl SignableTransaction {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
inputs: Vec<SpendableOutput>,
|
inputs: Vec<SpendableOutput>,
|
||||||
payments: Vec<(Address, u64)>,
|
mut payments: Vec<(Address, u64)>,
|
||||||
change_address: Option<Address>,
|
change_address: Option<Address>,
|
||||||
fee_rate: Fee
|
fee_rate: Fee
|
||||||
) -> Result<SignableTransaction, TransactionError> {
|
) -> Result<SignableTransaction, TransactionError> {
|
||||||
// Make sure all addresses are valid
|
// Make sure all addresses are valid
|
||||||
let test = |addr: Address| {
|
let test = |addr: Address| {
|
||||||
if !(
|
match addr.meta.kind {
|
||||||
addr.public_view.point.decompress().is_some() &&
|
|
||||||
addr.public_spend.point.decompress().is_some()
|
|
||||||
) {
|
|
||||||
Err(TransactionError::InvalidAddress)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
match addr.addr_type {
|
|
||||||
AddressType::Standard => Ok(()),
|
AddressType::Standard => Ok(()),
|
||||||
AddressType::Integrated(..) => Err(TransactionError::InvalidAddress),
|
AddressType::Integrated(..) => Err(TransactionError::InvalidAddress),
|
||||||
AddressType::SubAddress => Ok(())
|
AddressType::Subaddress => Ok(())
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -250,11 +242,8 @@ impl SignableTransaction {
|
|||||||
Err(TransactionError::TooManyOutputs)?;
|
Err(TransactionError::TooManyOutputs)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut payments = payments.iter().map(|(address, amount)| (*address, *amount, false)).collect::<Vec<_>>();
|
|
||||||
if change {
|
if change {
|
||||||
// Always use a unique key image for the change output
|
payments.push((change_address.unwrap(), in_amount - out_amount));
|
||||||
// TODO: Make this a config option
|
|
||||||
payments.push((change_address.unwrap(), in_amount - out_amount, true));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(
|
Ok(
|
||||||
|
|||||||
@@ -94,9 +94,8 @@ impl SignableTransaction {
|
|||||||
transcript.append_message(b"input_shared_key", &input.key_offset.to_bytes());
|
transcript.append_message(b"input_shared_key", &input.key_offset.to_bytes());
|
||||||
}
|
}
|
||||||
for payment in &self.payments {
|
for payment in &self.payments {
|
||||||
transcript.append_message(b"payment_address", &payment.0.as_bytes());
|
transcript.append_message(b"payment_address", &payment.0.to_string().as_bytes());
|
||||||
transcript.append_message(b"payment_amount", &payment.1.to_le_bytes());
|
transcript.append_message(b"payment_amount", &payment.1.to_le_bytes());
|
||||||
transcript.append_message(b"payment_unique", &(if payment.2 { [1] } else { [0] }));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort included before cloning it around
|
// Sort included before cloning it around
|
||||||
|
|||||||
@@ -18,12 +18,7 @@ use transcript::RecommendedTranscript;
|
|||||||
#[cfg(feature = "multisig")]
|
#[cfg(feature = "multisig")]
|
||||||
use frost::{curve::Ed25519, tests::{THRESHOLD, key_gen, sign}};
|
use frost::{curve::Ed25519, tests::{THRESHOLD, key_gen, sign}};
|
||||||
|
|
||||||
use monero::{
|
use monero_serai::{random_scalar, wallet::{ViewPair, address::{Network, AddressType}, SignableTransaction}};
|
||||||
network::Network,
|
|
||||||
util::{key::PublicKey, address::Address}
|
|
||||||
};
|
|
||||||
|
|
||||||
use monero_serai::{random_scalar, wallet::SignableTransaction};
|
|
||||||
|
|
||||||
mod rpc;
|
mod rpc;
|
||||||
use crate::rpc::{rpc, mine_block};
|
use crate::rpc::{rpc, mine_block};
|
||||||
@@ -73,11 +68,8 @@ async fn send_core(test: usize, multisig: bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let addr = Address::standard(
|
let view_pair = ViewPair { view, spend: spend_pub };
|
||||||
Network::Mainnet,
|
let addr = view_pair.address(Network::Mainnet, AddressType::Standard, false);
|
||||||
PublicKey { point: spend_pub.compress() },
|
|
||||||
PublicKey { point: (&view * &ED25519_BASEPOINT_TABLE).compress() }
|
|
||||||
);
|
|
||||||
|
|
||||||
let fee = rpc.get_fee().await.unwrap();
|
let fee = rpc.get_fee().await.unwrap();
|
||||||
|
|
||||||
@@ -99,7 +91,7 @@ async fn send_core(test: usize, multisig: bool) {
|
|||||||
|
|
||||||
// Grab the largest output available
|
// Grab the largest output available
|
||||||
let output = {
|
let output = {
|
||||||
let mut outputs = tx.as_ref().unwrap().scan(view, spend_pub).0;
|
let mut outputs = tx.as_ref().unwrap().scan(view_pair, false).0;
|
||||||
outputs.sort_by(|x, y| x.commitment.amount.cmp(&y.commitment.amount).reverse());
|
outputs.sort_by(|x, y| x.commitment.amount.cmp(&y.commitment.amount).reverse());
|
||||||
outputs.swap_remove(0)
|
outputs.swap_remove(0)
|
||||||
};
|
};
|
||||||
@@ -124,7 +116,7 @@ async fn send_core(test: usize, multisig: bool) {
|
|||||||
|
|
||||||
for i in (start + 1) .. (start + 9) {
|
for i in (start + 1) .. (start + 9) {
|
||||||
let tx = rpc.get_block_transactions(i).await.unwrap().swap_remove(0);
|
let tx = rpc.get_block_transactions(i).await.unwrap().swap_remove(0);
|
||||||
let output = tx.scan(view, spend_pub).0.swap_remove(0);
|
let output = tx.scan(view_pair, false).0.swap_remove(0);
|
||||||
amount += output.commitment.amount;
|
amount += output.commitment.amount;
|
||||||
outputs.push(output);
|
outputs.push(output);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ pub trait Coin {
|
|||||||
) -> Result<(Vec<u8>, Vec<<Self::Output as Output>::Id>), CoinError>;
|
) -> Result<(Vec<u8>, Vec<<Self::Output as Output>::Id>), CoinError>;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
async fn mine_block(&self, address: Self::Address);
|
async fn mine_block(&self);
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
async fn test_send(&self, key: Self::Address);
|
async fn test_send(&self, key: Self::Address);
|
||||||
|
|||||||
@@ -2,17 +2,19 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
|
||||||
use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, scalar::Scalar};
|
use curve25519_dalek::scalar::Scalar;
|
||||||
|
|
||||||
use dalek_ff_group as dfg;
|
use dalek_ff_group as dfg;
|
||||||
use transcript::RecommendedTranscript;
|
use transcript::RecommendedTranscript;
|
||||||
use frost::{curve::Ed25519, MultisigKeys};
|
use frost::{curve::Ed25519, MultisigKeys};
|
||||||
|
|
||||||
use monero::{PublicKey, network::Network, util::address::Address};
|
|
||||||
use monero_serai::{
|
use monero_serai::{
|
||||||
transaction::{Timelock, Transaction},
|
transaction::{Timelock, Transaction},
|
||||||
rpc::Rpc,
|
rpc::Rpc,
|
||||||
wallet::{Fee, SpendableOutput, SignableTransaction as MSignableTransaction, TransactionMachine}
|
wallet::{
|
||||||
|
ViewPair, address::{Network, AddressType, Address},
|
||||||
|
Fee, SpendableOutput, SignableTransaction as MSignableTransaction, TransactionMachine
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{coin::{CoinError, Output as OutputTrait, Coin}, view_key};
|
use crate::{coin::{CoinError, Output as OutputTrait, Coin}, view_key};
|
||||||
@@ -59,18 +61,28 @@ pub struct SignableTransaction(
|
|||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Monero {
|
pub struct Monero {
|
||||||
pub(crate) rpc: Rpc,
|
pub(crate) rpc: Rpc,
|
||||||
view: Scalar,
|
view: Scalar
|
||||||
view_pub: PublicKey
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Monero {
|
impl Monero {
|
||||||
pub fn new(url: String) -> Monero {
|
pub fn new(url: String) -> Monero {
|
||||||
let view = view_key::<Monero>(0).0;
|
let view = view_key::<Monero>(0).0;
|
||||||
Monero {
|
Monero { rpc: Rpc::new(url), view }
|
||||||
rpc: Rpc::new(url),
|
}
|
||||||
view,
|
|
||||||
view_pub: PublicKey { point: (&view * &ED25519_BASEPOINT_TABLE).compress() }
|
fn view_pair(&self, spend: dfg::EdwardsPoint) -> ViewPair {
|
||||||
}
|
ViewPair { spend: spend.0, view: self.view }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
fn empty_view_pair(&self) -> ViewPair {
|
||||||
|
use group::Group;
|
||||||
|
self.view_pair(dfg::EdwardsPoint::generator())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
fn empty_address(&self) -> Address {
|
||||||
|
self.empty_view_pair().address(Network::Mainnet, AddressType::Standard, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,7 +112,7 @@ impl Coin for Monero {
|
|||||||
const MAX_OUTPUTS: usize = 16;
|
const MAX_OUTPUTS: usize = 16;
|
||||||
|
|
||||||
fn address(&self, key: dfg::EdwardsPoint) -> Self::Address {
|
fn address(&self, key: dfg::EdwardsPoint) -> Self::Address {
|
||||||
Address::standard(Network::Mainnet, PublicKey { point: key.compress().0 }, self.view_pub)
|
self.view_pair(key).address(Network::Mainnet, AddressType::Standard, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_height(&self) -> Result<usize, CoinError> {
|
async fn get_height(&self) -> Result<usize, CoinError> {
|
||||||
@@ -115,7 +127,7 @@ impl Coin for Monero {
|
|||||||
block
|
block
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|tx| {
|
.flat_map(|tx| {
|
||||||
let (outputs, timelock) = tx.scan(self.view, key.0);
|
let (outputs, timelock) = tx.scan(self.view_pair(key), true);
|
||||||
if timelock == Timelock::None {
|
if timelock == Timelock::None {
|
||||||
outputs
|
outputs
|
||||||
} else {
|
} else {
|
||||||
@@ -178,13 +190,13 @@ impl Coin for Monero {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
async fn mine_block(&self, address: Self::Address) {
|
async fn mine_block(&self) {
|
||||||
#[derive(serde::Deserialize, Debug)]
|
#[derive(serde::Deserialize, Debug)]
|
||||||
struct EmptyResponse {}
|
struct EmptyResponse {}
|
||||||
let _: EmptyResponse = self.rpc.rpc_call("json_rpc", Some(serde_json::json!({
|
let _: EmptyResponse = self.rpc.rpc_call("json_rpc", Some(serde_json::json!({
|
||||||
"method": "generateblocks",
|
"method": "generateblocks",
|
||||||
"params": {
|
"params": {
|
||||||
"wallet_address": address.to_string(),
|
"wallet_address": self.empty_address().to_string(),
|
||||||
"amount_of_blocks": 10
|
"amount_of_blocks": 10
|
||||||
},
|
},
|
||||||
}))).await.unwrap();
|
}))).await.unwrap();
|
||||||
@@ -192,31 +204,28 @@ impl Coin for Monero {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
async fn test_send(&self, address: Self::Address) {
|
async fn test_send(&self, address: Self::Address) {
|
||||||
use group::Group;
|
|
||||||
|
|
||||||
use rand::rngs::OsRng;
|
use rand::rngs::OsRng;
|
||||||
|
|
||||||
let height = self.get_height().await.unwrap();
|
let height = self.get_height().await.unwrap();
|
||||||
|
|
||||||
let temp = self.address(dfg::EdwardsPoint::generator());
|
self.mine_block().await;
|
||||||
self.mine_block(temp).await;
|
|
||||||
for _ in 0 .. 7 {
|
for _ in 0 .. 7 {
|
||||||
self.mine_block(temp).await;
|
self.mine_block().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
let outputs = self.rpc
|
let outputs = self.rpc
|
||||||
.get_block_transactions_possible(height).await.unwrap()
|
.get_block_transactions_possible(height).await.unwrap()
|
||||||
.swap_remove(0).scan(self.view, dfg::EdwardsPoint::generator().0).0;
|
.swap_remove(0).scan(self.empty_view_pair(), false).0;
|
||||||
|
|
||||||
let amount = outputs[0].commitment.amount;
|
let amount = outputs[0].commitment.amount;
|
||||||
let fee = 1000000000; // TODO
|
let fee = 1000000000; // TODO
|
||||||
let tx = MSignableTransaction::new(
|
let tx = MSignableTransaction::new(
|
||||||
outputs,
|
outputs,
|
||||||
vec![(address, amount - fee)],
|
vec![(address, amount - fee)],
|
||||||
Some(temp),
|
Some(self.empty_address()),
|
||||||
self.rpc.get_fee().await.unwrap()
|
self.rpc.get_fee().await.unwrap()
|
||||||
).unwrap().sign(&mut OsRng, &self.rpc, &Scalar::one()).await.unwrap();
|
).unwrap().sign(&mut OsRng, &self.rpc, &Scalar::one()).await.unwrap();
|
||||||
self.rpc.publish_transaction(&tx).await.unwrap();
|
self.rpc.publish_transaction(&tx).await.unwrap();
|
||||||
self.mine_block(temp).await;
|
self.mine_block().await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,6 @@ use async_trait::async_trait;
|
|||||||
|
|
||||||
use rand::rngs::OsRng;
|
use rand::rngs::OsRng;
|
||||||
|
|
||||||
use group::Group;
|
|
||||||
|
|
||||||
use frost::curve::Curve;
|
|
||||||
|
|
||||||
use crate::{NetworkError, Network, coin::{Coin, Monero}, wallet::{WalletKeys, MemCoinDb, Wallet}};
|
use crate::{NetworkError, Network, coin::{Coin, Monero}, wallet::{WalletKeys, MemCoinDb, Wallet}};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -55,7 +51,7 @@ impl Network for LocalNetwork {
|
|||||||
|
|
||||||
async fn test_send<C: Coin + Clone>(coin: C, fee: C::Fee) {
|
async fn test_send<C: Coin + Clone>(coin: C, fee: C::Fee) {
|
||||||
// Mine a block so there's a confirmed height
|
// Mine a block so there's a confirmed height
|
||||||
coin.mine_block(coin.address(<C::Curve as Curve>::G::generator())).await;
|
coin.mine_block().await;
|
||||||
let height = coin.get_height().await.unwrap();
|
let height = coin.get_height().await.unwrap();
|
||||||
|
|
||||||
let mut keys = frost::tests::key_gen::<_, C::Curve>(&mut OsRng);
|
let mut keys = frost::tests::key_gen::<_, C::Curve>(&mut OsRng);
|
||||||
@@ -74,7 +70,7 @@ async fn test_send<C: Coin + Clone>(coin: C, fee: C::Fee) {
|
|||||||
|
|
||||||
// Get the chain to a height where blocks have sufficient confirmations
|
// Get the chain to a height where blocks have sufficient confirmations
|
||||||
while (height + C::CONFIRMATIONS) > coin.get_height().await.unwrap() {
|
while (height + C::CONFIRMATIONS) > coin.get_height().await.unwrap() {
|
||||||
coin.mine_block(coin.address(<C::Curve as Curve>::G::generator())).await;
|
coin.mine_block().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
for wallet in wallets.iter_mut() {
|
for wallet in wallets.iter_mut() {
|
||||||
|
|||||||
Reference in New Issue
Block a user