mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-11 05:29:25 +00:00
Remove monero-rs types
Still missing an updated RPC file. Restructures the library as it makes sense
This commit is contained in:
198
coins/monero/src/wallet/decoys.rs
Normal file
198
coins/monero/src/wallet/decoys.rs
Normal file
@@ -0,0 +1,198 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
use rand_core::{RngCore, CryptoRng};
|
||||
use rand_distr::{Distribution, Gamma};
|
||||
|
||||
use curve25519_dalek::edwards::EdwardsPoint;
|
||||
|
||||
use crate::{wallet::SpendableOutput, rpc::{RpcError, Rpc}};
|
||||
|
||||
const LOCK_WINDOW: usize = 10;
|
||||
const MATURITY: u64 = 60;
|
||||
const RECENT_WINDOW: usize = 15;
|
||||
const BLOCK_TIME: usize = 120;
|
||||
const BLOCKS_PER_YEAR: usize = 365 * 24 * 60 * 60 / BLOCK_TIME;
|
||||
const TIP_APPLICATION: f64 = (LOCK_WINDOW * BLOCK_TIME) as f64;
|
||||
|
||||
const DECOYS: usize = 11;
|
||||
|
||||
lazy_static! {
|
||||
static ref GAMMA: Gamma<f64> = Gamma::new(19.28, 1.0 / 1.61).unwrap();
|
||||
}
|
||||
|
||||
async fn select_n<R: RngCore + CryptoRng>(
|
||||
rng: &mut R,
|
||||
rpc: &Rpc,
|
||||
height: usize,
|
||||
distribution: &[u64],
|
||||
high: u64,
|
||||
per_second: f64,
|
||||
used: &mut HashSet<u64>,
|
||||
count: usize
|
||||
) -> Result<Vec<(u64, [EdwardsPoint; 2])>, RpcError> {
|
||||
// Panic if not enough decoys are available
|
||||
// TODO: Simply create a TX with less than the target amount
|
||||
if (high - MATURITY) < u64::try_from(DECOYS).unwrap() {
|
||||
panic!("Not enough decoys available");
|
||||
}
|
||||
|
||||
let mut confirmed = Vec::with_capacity(count);
|
||||
while confirmed.len() != count {
|
||||
let remaining = count - confirmed.len();
|
||||
let mut candidates = Vec::with_capacity(remaining);
|
||||
while candidates.len() != remaining {
|
||||
// Use a gamma distribution
|
||||
let mut age = GAMMA.sample(rng).exp();
|
||||
if age > TIP_APPLICATION {
|
||||
age -= TIP_APPLICATION;
|
||||
} else {
|
||||
// f64 does not have try_from available, which is why these are written with `as`
|
||||
age = (rng.next_u64() % u64::try_from(RECENT_WINDOW * BLOCK_TIME).unwrap()) as f64;
|
||||
}
|
||||
|
||||
let o = (age * per_second) as u64;
|
||||
if o < high {
|
||||
let i = distribution.partition_point(|s| *s < (high - 1 - o));
|
||||
let prev = i.saturating_sub(1);
|
||||
let n = distribution[i] - distribution[prev];
|
||||
if n != 0 {
|
||||
let o = distribution[prev] + (rng.next_u64() % n);
|
||||
if !used.contains(&o) {
|
||||
// It will either actually be used, or is unusable and this prevents trying it again
|
||||
used.insert(o);
|
||||
candidates.push(o);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let outputs = rpc.get_outputs(&candidates, height).await?;
|
||||
for i in 0 .. outputs.len() {
|
||||
if let Some(output) = outputs[i] {
|
||||
confirmed.push((candidates[i], output));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(confirmed)
|
||||
}
|
||||
|
||||
fn offset(decoys: &[u64]) -> Vec<u64> {
|
||||
let mut res = vec![decoys[0]];
|
||||
res.resize(decoys.len(), 0);
|
||||
for m in (1 .. decoys.len()).rev() {
|
||||
res[m] = decoys[m] - decoys[m - 1];
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Decoys {
|
||||
pub i: u8,
|
||||
pub offsets: Vec<u64>,
|
||||
pub ring: Vec<[EdwardsPoint; 2]>
|
||||
}
|
||||
|
||||
impl Decoys {
|
||||
pub fn len(&self) -> usize {
|
||||
self.offsets.len()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn select<R: RngCore + CryptoRng>(
|
||||
rng: &mut R,
|
||||
rpc: &Rpc,
|
||||
height: usize,
|
||||
inputs: &[SpendableOutput]
|
||||
) -> Result<Vec<Decoys>, RpcError> {
|
||||
// Convert the inputs in question to the raw output data
|
||||
let mut outputs = Vec::with_capacity(inputs.len());
|
||||
for input in inputs {
|
||||
outputs.push((
|
||||
rpc.get_o_indexes(input.tx).await?[input.o],
|
||||
[input.key, input.commitment.calculate()]
|
||||
));
|
||||
}
|
||||
|
||||
let distribution = rpc.get_output_distribution(height).await?;
|
||||
let high = distribution[distribution.len() - 1];
|
||||
let per_second = {
|
||||
let blocks = distribution.len().min(BLOCKS_PER_YEAR);
|
||||
let outputs = high - distribution[distribution.len().saturating_sub(blocks + 1)];
|
||||
(outputs as f64) / ((blocks * BLOCK_TIME) as f64)
|
||||
};
|
||||
|
||||
let mut used = HashSet::<u64>::new();
|
||||
for o in &outputs {
|
||||
used.insert(o.0);
|
||||
}
|
||||
|
||||
let mut res = Vec::with_capacity(inputs.len());
|
||||
for (i, o) in outputs.iter().enumerate() {
|
||||
// If there's only the target amount of decoys available, remove the index of the output we're spending
|
||||
// So we don't infinite loop while ignoring it
|
||||
// TODO: If we're spending 2 outputs of a possible 11 outputs, this will still fail
|
||||
used.remove(&o.0);
|
||||
|
||||
// Select the full amount of ring members in decoys, instead of just the actual decoys, in order
|
||||
// to increase sample size
|
||||
let mut decoys = select_n(rng, rpc, height, &distribution, high, per_second, &mut used, DECOYS).await?;
|
||||
decoys.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
|
||||
// Add back this output
|
||||
used.insert(o.0);
|
||||
|
||||
// Make sure the TX passes the sanity check that the median output is within the last 40%
|
||||
// This actually checks the median is within the last third, a slightly more aggressive boundary,
|
||||
// as the height used in this calculation will be slightly under the height this is sanity
|
||||
// checked against
|
||||
let target_median = high * 2 / 3;
|
||||
|
||||
// Sanity checks are only run when 1000 outputs are available
|
||||
// We run this check whenever it's possible to satisfy
|
||||
// This means we need the middle possible decoy to be above the target_median
|
||||
// TODO: This will break if timelocks are used other than maturity on very small chains/chains
|
||||
// of any size which use timelocks extremely frequently, as it'll try to satisfy an impossible
|
||||
// condition
|
||||
// Reduce target_median by each timelocked output found?
|
||||
if (high - MATURITY) >= target_median {
|
||||
while decoys[DECOYS / 2].0 < target_median {
|
||||
// If it's not, update the bottom half with new values to ensure the median only moves up
|
||||
for m in 0 .. DECOYS / 2 {
|
||||
// We could not remove this, saving CPU time and removing low values as possibilities, yet
|
||||
// it'd increase the amount of decoys required to create this transaction and some banned
|
||||
// outputs may be the best options
|
||||
used.remove(&decoys[m].0);
|
||||
}
|
||||
|
||||
decoys.splice(
|
||||
0 .. DECOYS / 2,
|
||||
select_n(rng, rpc, height, &distribution, high, per_second, &mut used, DECOYS / 2).await?
|
||||
);
|
||||
decoys.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
}
|
||||
}
|
||||
|
||||
// Replace the closest selected decoy with the actual
|
||||
let mut replace = 0;
|
||||
let mut distance = u64::MAX;
|
||||
for m in 0 .. decoys.len() {
|
||||
let diff = decoys[m].0.abs_diff(o.0);
|
||||
if diff < distance {
|
||||
replace = m;
|
||||
distance = diff;
|
||||
}
|
||||
}
|
||||
|
||||
decoys[replace] = outputs[i];
|
||||
res.push(Decoys {
|
||||
i: u8::try_from(replace).unwrap(),
|
||||
offsets: offset(&decoys.iter().map(|output| output.0).collect::<Vec<_>>()),
|
||||
ring: decoys.iter().map(|output| output.1).collect()
|
||||
});
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
59
coins/monero/src/wallet/mod.rs
Normal file
59
coins/monero/src/wallet/mod.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
use curve25519_dalek::{scalar::Scalar, edwards::EdwardsPoint};
|
||||
|
||||
use crate::{
|
||||
hash, hash_to_scalar,
|
||||
serialize::write_varint,
|
||||
transaction::Input
|
||||
};
|
||||
|
||||
mod scan;
|
||||
pub use scan::SpendableOutput;
|
||||
|
||||
pub(crate) mod decoys;
|
||||
pub(crate) use decoys::Decoys;
|
||||
|
||||
mod send;
|
||||
pub use send::{TransactionError, SignableTransaction};
|
||||
|
||||
// https://github.com/monero-project/research-lab/issues/103
|
||||
pub(crate) fn uniqueness(inputs: &[Input]) -> [u8; 32] {
|
||||
let mut u = b"domain_separator".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).try_into().unwrap(), &mut u).unwrap(); },
|
||||
Input::ToKey { key_image, .. } => u.extend(key_image.compress().to_bytes())
|
||||
}
|
||||
}
|
||||
hash(&u)
|
||||
}
|
||||
|
||||
// Hs(8Ra || o) with https://github.com/monero-project/research-lab/issues/103 as an option
|
||||
#[allow(non_snake_case)]
|
||||
pub(crate) fn shared_key(uniqueness: Option<[u8; 32]>, s: Scalar, P: &EdwardsPoint, o: usize) -> Scalar {
|
||||
// uniqueness
|
||||
let mut shared = uniqueness.map_or(vec![], |uniqueness| uniqueness.to_vec());
|
||||
// || 8Ra
|
||||
shared.extend((s * P).mul_by_cofactor().compress().to_bytes().to_vec());
|
||||
// || o
|
||||
write_varint(&o.try_into().unwrap(), &mut shared).unwrap();
|
||||
// Hs()
|
||||
hash_to_scalar(&shared)
|
||||
}
|
||||
|
||||
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)[0 .. 8].try_into().unwrap())).to_le_bytes()
|
||||
}
|
||||
|
||||
fn amount_decryption(amount: [u8; 8], key: Scalar) -> u64 {
|
||||
u64::from_le_bytes(amount_encryption(u64::from_le_bytes(amount), key))
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
317
coins/monero/src/wallet/multisig.rs
Normal file
317
coins/monero/src/wallet/multisig.rs
Normal file
@@ -0,0 +1,317 @@
|
||||
use std::{rc::Rc, cell::RefCell};
|
||||
|
||||
use rand_core::{RngCore, CryptoRng, SeedableRng};
|
||||
use rand_chacha::ChaCha12Rng;
|
||||
|
||||
use curve25519_dalek::{traits::Identity, scalar::Scalar, edwards::{EdwardsPoint, CompressedEdwardsY}};
|
||||
|
||||
use monero::{
|
||||
Hash, VarInt,
|
||||
util::ringct::Key,
|
||||
blockdata::transaction::{KeyImage, TxIn, Transaction}
|
||||
};
|
||||
|
||||
use transcript::Transcript as TranscriptTrait;
|
||||
use frost::{FrostError, MultisigKeys, MultisigParams, sign::{State, StateMachine, AlgorithmMachine}};
|
||||
|
||||
use crate::{
|
||||
frost::{Transcript, Ed25519},
|
||||
random_scalar, bulletproofs::Bulletproofs, clsag,
|
||||
rpc::Rpc,
|
||||
wallet::{TransactionError, SignableTransaction, decoys::{self, Decoys}}
|
||||
};
|
||||
|
||||
pub struct TransactionMachine {
|
||||
leader: bool,
|
||||
signable: SignableTransaction,
|
||||
transcript: Transcript,
|
||||
|
||||
decoys: Vec<Decoys>,
|
||||
|
||||
images: Vec<EdwardsPoint>,
|
||||
output_masks: Option<Scalar>,
|
||||
inputs: Vec<Rc<RefCell<Option<ClsagDetails>>>>,
|
||||
clsags: Vec<AlgorithmMachine<Ed25519, ClsagMultisig>>,
|
||||
|
||||
tx: Option<Transaction>
|
||||
}
|
||||
|
||||
impl SignableTransaction {
|
||||
pub async fn multisig<R: RngCore + CryptoRng>(
|
||||
mut self,
|
||||
label: Vec<u8>,
|
||||
rng: &mut R,
|
||||
rpc: &Rpc,
|
||||
height: usize,
|
||||
keys: MultisigKeys<Ed25519>,
|
||||
included: &[usize]
|
||||
) -> Result<TransactionMachine, TransactionError> {
|
||||
let mut images = vec![];
|
||||
images.resize(self.inputs.len(), EdwardsPoint::identity());
|
||||
let mut inputs = vec![];
|
||||
for _ in 0 .. self.inputs.len() {
|
||||
// Doesn't resize as that will use a single Rc for the entire Vec
|
||||
inputs.push(Rc::new(RefCell::new(None)));
|
||||
}
|
||||
let mut clsags = vec![];
|
||||
|
||||
// Create a RNG out of the input shared keys, which either requires the view key or being every
|
||||
// sender, and the payments (address and amount), which a passive adversary may be able to know
|
||||
// depending on how these transactions are coordinated
|
||||
|
||||
let mut transcript = Transcript::new(label);
|
||||
// Also include the spend_key as below only the key offset is included, so this confirms the sum product
|
||||
// Useful as confirming the sum product confirms the key image, further guaranteeing the one time
|
||||
// properties noted below
|
||||
transcript.append_message(b"spend_key", &keys.group_key().0.compress().to_bytes());
|
||||
for input in &self.inputs {
|
||||
// These outputs can only be spent once. Therefore, it forces all RNGs derived from this
|
||||
// transcript (such as the one used to create one time keys) to be unique
|
||||
transcript.append_message(b"input_hash", &input.tx.0);
|
||||
transcript.append_message(b"input_output_index", &u16::try_from(input.o).unwrap().to_le_bytes());
|
||||
// Not including this, with a doxxed list of payments, would allow brute forcing the inputs
|
||||
// to determine RNG seeds and therefore the true spends
|
||||
transcript.append_message(b"input_shared_key", &input.key_offset.to_bytes());
|
||||
}
|
||||
for payment in &self.payments {
|
||||
transcript.append_message(b"payment_address", &payment.0.as_bytes());
|
||||
transcript.append_message(b"payment_amount", &payment.1.to_le_bytes());
|
||||
}
|
||||
transcript.append_message(b"change", &self.change.as_bytes());
|
||||
|
||||
// Select decoys
|
||||
// Ideally, this would be done post entropy, instead of now, yet doing so would require sign
|
||||
// to be async which isn't feasible. This should be suitably competent though
|
||||
// While this inability means we can immediately create the input, moving it out of the
|
||||
// Rc RefCell, keeping it within an Rc RefCell keeps our options flexible
|
||||
let decoys = decoys::select(
|
||||
&mut ChaCha12Rng::from_seed(transcript.rng_seed(b"decoys", None)),
|
||||
rpc,
|
||||
height,
|
||||
&self.inputs
|
||||
).await.map_err(|e| TransactionError::RpcError(e))?;
|
||||
|
||||
for (i, input) in self.inputs.iter().enumerate() {
|
||||
clsags.push(
|
||||
AlgorithmMachine::new(
|
||||
ClsagMultisig::new(
|
||||
transcript.clone(),
|
||||
inputs[i].clone()
|
||||
).map_err(|e| TransactionError::MultisigError(e))?,
|
||||
Rc::new(keys.offset(dalek_ff_group::Scalar(input.key_offset))),
|
||||
included
|
||||
).map_err(|e| TransactionError::FrostError(e))?
|
||||
);
|
||||
}
|
||||
|
||||
// Verify these outputs by a dummy prep
|
||||
self.prepare_outputs(rng, None)?;
|
||||
|
||||
Ok(TransactionMachine {
|
||||
leader: keys.params().i() == included[0],
|
||||
signable: self,
|
||||
transcript,
|
||||
|
||||
decoys,
|
||||
|
||||
images,
|
||||
output_masks: None,
|
||||
inputs,
|
||||
clsags,
|
||||
|
||||
tx: None
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl StateMachine for TransactionMachine {
|
||||
type Signature = Transaction;
|
||||
|
||||
fn preprocess<R: RngCore + CryptoRng>(
|
||||
&mut self,
|
||||
rng: &mut R
|
||||
) -> Result<Vec<u8>, FrostError> {
|
||||
if self.state() != State::Fresh {
|
||||
Err(FrostError::InvalidSignTransition(State::Fresh, self.state()))?;
|
||||
}
|
||||
|
||||
// Iterate over each CLSAG calling preprocess
|
||||
let mut serialized = vec![];
|
||||
for (i, clsag) in self.clsags.iter_mut().enumerate() {
|
||||
let preprocess = clsag.preprocess(rng)?;
|
||||
// First 64 bytes are FROST's commitments
|
||||
self.images[i] += CompressedEdwardsY(preprocess[64 .. 96].try_into().unwrap()).decompress().unwrap();
|
||||
serialized.extend(&preprocess);
|
||||
}
|
||||
|
||||
if self.leader {
|
||||
let mut entropy = [0; 32];
|
||||
rng.fill_bytes(&mut entropy);
|
||||
serialized.extend(&entropy);
|
||||
|
||||
let mut rng = ChaCha12Rng::from_seed(self.transcript.rng_seed(b"tx_keys", Some(entropy)));
|
||||
// Safe to unwrap thanks to the dummy prepare
|
||||
let (commitments, output_masks) = self.signable.prepare_outputs(&mut rng, None).unwrap();
|
||||
self.output_masks = Some(output_masks);
|
||||
|
||||
let bp = Bulletproofs::new(&commitments).unwrap();
|
||||
serialized.extend(&bp.serialize());
|
||||
|
||||
let tx = self.signable.prepare_transaction(&commitments, bp);
|
||||
self.tx = Some(tx);
|
||||
}
|
||||
|
||||
Ok(serialized)
|
||||
}
|
||||
|
||||
fn sign(
|
||||
&mut self,
|
||||
commitments: &[Option<Vec<u8>>],
|
||||
_: &[u8]
|
||||
) -> Result<Vec<u8>, FrostError> {
|
||||
if self.state() != State::Preprocessed {
|
||||
Err(FrostError::InvalidSignTransition(State::Preprocessed, self.state()))?;
|
||||
}
|
||||
|
||||
// FROST commitments, image, commitments, and their proofs
|
||||
let clsag_len = 64 + ClsagMultisig::serialized_len();
|
||||
let clsag_lens = clsag_len * self.clsags.len();
|
||||
|
||||
// Split out the prep and update the TX
|
||||
let mut tx;
|
||||
if self.leader {
|
||||
tx = self.tx.take().unwrap();
|
||||
} else {
|
||||
let (l, prep) = commitments.iter().enumerate().filter(|(_, prep)| prep.is_some()).next()
|
||||
.ok_or(FrostError::InternalError("no participants".to_string()))?;
|
||||
let prep = prep.as_ref().unwrap();
|
||||
|
||||
// Not invalid outputs due to doing a dummy prep as leader
|
||||
let (commitments, output_masks) = self.signable.prepare_outputs(
|
||||
&mut ChaCha12Rng::from_seed(
|
||||
self.transcript.rng_seed(
|
||||
b"tx_keys",
|
||||
Some(prep[clsag_lens .. (clsag_lens + 32)].try_into().map_err(|_| FrostError::InvalidShare(l))?)
|
||||
)
|
||||
),
|
||||
None
|
||||
).map_err(|_| FrostError::InvalidShare(l))?;
|
||||
self.output_masks.replace(output_masks);
|
||||
|
||||
// Verify the provided bulletproofs if not leader
|
||||
let bp = Bulletproofs::deserialize(
|
||||
&mut std::io::Cursor::new(&prep[(clsag_lens + 32) .. prep.len()])
|
||||
).map_err(|_| FrostError::InvalidShare(l))?;
|
||||
if !bp.verify(&commitments.iter().map(|c| c.calculate()).collect::<Vec<EdwardsPoint>>()) {
|
||||
Err(FrostError::InvalidShare(l))?;
|
||||
}
|
||||
|
||||
tx = self.signable.prepare_transaction(&commitments, bp);
|
||||
}
|
||||
|
||||
for c in 0 .. self.clsags.len() {
|
||||
// Calculate the key images in order to update the TX
|
||||
// Multisig will parse/calculate/validate this as needed, yet doing so here as well provides
|
||||
// the easiest API overall
|
||||
for (l, serialized) in commitments.iter().enumerate().filter(|(_, s)| s.is_some()) {
|
||||
self.images[c] += CompressedEdwardsY(
|
||||
serialized.as_ref().unwrap()[((c * clsag_len) + 64) .. ((c * clsag_len) + 96)]
|
||||
.try_into().map_err(|_| FrostError::InvalidCommitment(l))?
|
||||
).decompress().ok_or(FrostError::InvalidCommitment(l))?;
|
||||
}
|
||||
}
|
||||
|
||||
let mut commitments = (0 .. self.inputs.len()).map(|c| commitments.iter().map(
|
||||
|commitments| commitments.clone().map(
|
||||
|commitments| commitments[(c * clsag_len) .. ((c * clsag_len) + clsag_len)].to_vec()
|
||||
)
|
||||
).collect::<Vec<_>>()).collect::<Vec<_>>();
|
||||
|
||||
let mut sorted = Vec::with_capacity(self.decoys.len());
|
||||
while self.decoys.len() != 0 {
|
||||
sorted.push((
|
||||
self.signable.inputs.swap_remove(0),
|
||||
self.decoys.swap_remove(0),
|
||||
self.images.swap_remove(0),
|
||||
self.inputs.swap_remove(0),
|
||||
self.clsags.swap_remove(0),
|
||||
commitments.swap_remove(0)
|
||||
));
|
||||
}
|
||||
sorted.sort_by(|x, y| x.2.compress().to_bytes().cmp(&y.2.compress().to_bytes()).reverse());
|
||||
|
||||
let mut rng = ChaCha12Rng::from_seed(self.transcript.rng_seed(b"pseudo_out_masks", None));
|
||||
let mut sum_pseudo_outs = Scalar::zero();
|
||||
while sorted.len() != 0 {
|
||||
let value = sorted.remove(0);
|
||||
|
||||
let mut mask = random_scalar(&mut rng);
|
||||
if sorted.len() == 0 {
|
||||
mask = self.output_masks.unwrap() - sum_pseudo_outs;
|
||||
} else {
|
||||
sum_pseudo_outs += mask;
|
||||
}
|
||||
|
||||
tx.prefix.inputs.push(
|
||||
Input::ToKey {
|
||||
amount: VarInt(0),
|
||||
key_offsets: value.1.offsets.clone().iter().map(|x| VarInt(*x)).collect(),
|
||||
k_image: KeyImage { image: Hash(value.2.compress().to_bytes()) }
|
||||
}
|
||||
);
|
||||
|
||||
value.3.replace(
|
||||
Some(
|
||||
ClsagDetails::new(
|
||||
clsag::Input::new(
|
||||
value.0.commitment,
|
||||
value.1
|
||||
).map_err(|_| panic!("Signing an input which isn't present in the ring we created for it"))?,
|
||||
mask
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
self.clsags.push(value.4);
|
||||
commitments.push(value.5);
|
||||
}
|
||||
|
||||
let msg = tx.signature_hash().unwrap().0;
|
||||
self.tx = Some(tx);
|
||||
|
||||
// Iterate over each CLSAG calling sign
|
||||
let mut serialized = Vec::with_capacity(self.clsags.len() * 32);
|
||||
for (c, clsag) in self.clsags.iter_mut().enumerate() {
|
||||
serialized.extend(&clsag.sign(&commitments[c], &msg)?);
|
||||
}
|
||||
|
||||
Ok(serialized)
|
||||
}
|
||||
|
||||
fn complete(&mut self, shares: &[Option<Vec<u8>>]) -> Result<Transaction, FrostError> {
|
||||
if self.state() != State::Signed {
|
||||
Err(FrostError::InvalidSignTransition(State::Signed, self.state()))?;
|
||||
}
|
||||
|
||||
let mut tx = self.tx.take().unwrap();
|
||||
let mut prunable = tx.rct_signatures.p.unwrap();
|
||||
for (c, clsag) in self.clsags.iter_mut().enumerate() {
|
||||
let (clsag, pseudo_out) = clsag.complete(&shares.iter().map(
|
||||
|share| share.clone().map(|share| share[(c * 32) .. ((c * 32) + 32)].to_vec())
|
||||
).collect::<Vec<_>>())?;
|
||||
prunable.Clsags.push(clsag);
|
||||
prunable.pseudo_outs.push(pseudo_out.compress().to_bytes());
|
||||
}
|
||||
tx.rct_signatures.p = Some(prunable);
|
||||
|
||||
Ok(tx)
|
||||
}
|
||||
|
||||
fn multisig_params(&self) -> MultisigParams {
|
||||
self.clsags[0].multisig_params()
|
||||
}
|
||||
|
||||
fn state(&self) -> State {
|
||||
self.clsags[0].state()
|
||||
}
|
||||
}
|
||||
103
coins/monero/src/wallet/scan.rs
Normal file
103
coins/monero/src/wallet/scan.rs
Normal file
@@ -0,0 +1,103 @@
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use curve25519_dalek::{
|
||||
constants::ED25519_BASEPOINT_TABLE,
|
||||
scalar::Scalar,
|
||||
edwards::EdwardsPoint
|
||||
};
|
||||
|
||||
use monero::{consensus::deserialize, blockdata::transaction::ExtraField};
|
||||
|
||||
use crate::{
|
||||
Commitment,
|
||||
serialize::write_varint,
|
||||
transaction::Transaction,
|
||||
wallet::{uniqueness, shared_key, amount_decryption, commitment_mask}
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SpendableOutput {
|
||||
pub tx: [u8; 32],
|
||||
pub o: usize,
|
||||
pub key: EdwardsPoint,
|
||||
pub key_offset: Scalar,
|
||||
pub commitment: Commitment
|
||||
}
|
||||
|
||||
// TODO: Enable disabling one of the shared key derivations and solely using one
|
||||
// Change outputs currently always use unique derivations, so that must also be corrected
|
||||
impl Transaction {
|
||||
pub fn scan(
|
||||
&self,
|
||||
view: Scalar,
|
||||
spend: EdwardsPoint
|
||||
) -> Vec<SpendableOutput> {
|
||||
let mut extra = vec![];
|
||||
write_varint(&u64::try_from(self.prefix.extra.len()).unwrap(), &mut extra).unwrap();
|
||||
extra.extend(&self.prefix.extra);
|
||||
let extra = deserialize::<ExtraField>(&extra);
|
||||
|
||||
let pubkeys: Vec<EdwardsPoint>;
|
||||
if let Ok(extra) = extra {
|
||||
let mut m_pubkeys = vec![];
|
||||
if let Some(key) = extra.tx_pubkey() {
|
||||
m_pubkeys.push(key);
|
||||
}
|
||||
if let Some(keys) = extra.tx_additional_pubkeys() {
|
||||
m_pubkeys.extend(&keys);
|
||||
}
|
||||
|
||||
pubkeys = m_pubkeys.iter().map(|key| key.point.decompress()).filter_map(|key| key).collect();
|
||||
} else {
|
||||
return vec![];
|
||||
};
|
||||
|
||||
let mut res = vec![];
|
||||
for (o, output) in self.prefix.outputs.iter().enumerate() {
|
||||
// TODO: This may be replaceable by pubkeys[o]
|
||||
for pubkey in &pubkeys {
|
||||
let mut commitment = Commitment::zero();
|
||||
|
||||
// P - shared == spend
|
||||
let matches = |shared_key| (output.key - (&shared_key * &ED25519_BASEPOINT_TABLE)) == spend;
|
||||
let test = |shared_key| Some(shared_key).filter(|shared_key| matches(*shared_key));
|
||||
|
||||
// Get the traditional shared key and unique shared key, testing if either matches for this output
|
||||
let traditional = test(shared_key(None, view, pubkey, o));
|
||||
let unique = test(shared_key(Some(uniqueness(&self.prefix.inputs)), view, pubkey, o));
|
||||
|
||||
// If either matches, grab it and decode the amount
|
||||
if let Some(key_offset) = traditional.or(unique) {
|
||||
// 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, key: output.key, key_offset, commitment });
|
||||
}
|
||||
// Break to prevent public keys from being included multiple times, triggering multiple
|
||||
// inclusions of the same output
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
res
|
||||
}
|
||||
}
|
||||
319
coins/monero/src/wallet/send/mod.rs
Normal file
319
coins/monero/src/wallet/send/mod.rs
Normal file
@@ -0,0 +1,319 @@
|
||||
use thiserror::Error;
|
||||
|
||||
use rand_core::{RngCore, CryptoRng};
|
||||
use rand::seq::SliceRandom;
|
||||
|
||||
use curve25519_dalek::{
|
||||
constants::ED25519_BASEPOINT_TABLE,
|
||||
scalar::Scalar,
|
||||
edwards::EdwardsPoint
|
||||
};
|
||||
|
||||
use monero::{
|
||||
consensus::Encodable,
|
||||
util::{key::PublicKey, address::Address},
|
||||
blockdata::transaction::SubField
|
||||
};
|
||||
|
||||
#[cfg(feature = "multisig")]
|
||||
use frost::FrostError;
|
||||
|
||||
use crate::{
|
||||
Commitment,
|
||||
random_scalar,
|
||||
generate_key_image, bulletproofs::Bulletproofs, clsag::{ClsagError, ClsagInput, Clsag},
|
||||
rpc::{Rpc, RpcError},
|
||||
transaction::*,
|
||||
wallet::{uniqueness, shared_key, commitment_mask, amount_encryption, SpendableOutput, Decoys}
|
||||
};
|
||||
#[cfg(feature = "multisig")]
|
||||
use crate::frost::MultisigError;
|
||||
|
||||
#[cfg(feature = "multisig")]
|
||||
mod multisig;
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
#[derive(Clone, Debug)]
|
||||
struct SendOutput {
|
||||
R: EdwardsPoint,
|
||||
dest: EdwardsPoint,
|
||||
mask: Scalar,
|
||||
amount: [u8; 8]
|
||||
}
|
||||
|
||||
impl SendOutput {
|
||||
fn new<R: RngCore + CryptoRng>(
|
||||
rng: &mut R,
|
||||
unique: Option<[u8; 32]>,
|
||||
output: (Address, u64),
|
||||
o: usize
|
||||
) -> Result<SendOutput, TransactionError> {
|
||||
let r = random_scalar(rng);
|
||||
let shared_key = shared_key(
|
||||
unique,
|
||||
r,
|
||||
&output.0.public_view.point.decompress().ok_or(TransactionError::InvalidAddress)?,
|
||||
o
|
||||
);
|
||||
|
||||
Ok(
|
||||
SendOutput {
|
||||
R: &r * &ED25519_BASEPOINT_TABLE,
|
||||
dest: (
|
||||
(&shared_key * &ED25519_BASEPOINT_TABLE) +
|
||||
output.0.public_spend.point.decompress().ok_or(TransactionError::InvalidAddress)?
|
||||
),
|
||||
mask: commitment_mask(shared_key),
|
||||
amount: amount_encryption(output.1, shared_key)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum TransactionError {
|
||||
#[error("no inputs")]
|
||||
NoInputs,
|
||||
#[error("no outputs")]
|
||||
NoOutputs,
|
||||
#[error("too many outputs")]
|
||||
TooManyOutputs,
|
||||
#[error("not enough funds (in {0}, out {1})")]
|
||||
NotEnoughFunds(u64, u64),
|
||||
#[error("invalid address")]
|
||||
InvalidAddress,
|
||||
#[error("rpc error ({0})")]
|
||||
RpcError(RpcError),
|
||||
#[error("clsag error ({0})")]
|
||||
ClsagError(ClsagError),
|
||||
#[error("invalid transaction ({0})")]
|
||||
InvalidTransaction(RpcError),
|
||||
#[cfg(feature = "multisig")]
|
||||
#[error("frost error {0}")]
|
||||
FrostError(FrostError),
|
||||
#[cfg(feature = "multisig")]
|
||||
#[error("multisig error {0}")]
|
||||
MultisigError(MultisigError)
|
||||
}
|
||||
|
||||
async fn prepare_inputs<R: RngCore + CryptoRng>(
|
||||
rng: &mut R,
|
||||
rpc: &Rpc,
|
||||
inputs: &[SpendableOutput],
|
||||
spend: &Scalar,
|
||||
tx: &mut Transaction
|
||||
) -> Result<Vec<(Scalar, EdwardsPoint, ClsagInput)>, TransactionError> {
|
||||
let mut signable = Vec::with_capacity(inputs.len());
|
||||
|
||||
// Select decoys
|
||||
let decoys = Decoys::select(
|
||||
rng,
|
||||
rpc,
|
||||
rpc.get_height().await.map_err(|e| TransactionError::RpcError(e))? - 10,
|
||||
inputs
|
||||
).await.map_err(|e| TransactionError::RpcError(e))?;
|
||||
|
||||
for (i, input) in inputs.iter().enumerate() {
|
||||
signable.push((
|
||||
spend + input.key_offset,
|
||||
generate_key_image(&(spend + input.key_offset)),
|
||||
ClsagInput::new(
|
||||
input.commitment,
|
||||
decoys[i].clone()
|
||||
).map_err(|e| TransactionError::ClsagError(e))?
|
||||
));
|
||||
|
||||
tx.prefix.inputs.push(Input::ToKey {
|
||||
amount: 0,
|
||||
key_offsets: decoys[i].offsets.clone(),
|
||||
key_image: signable[i].1
|
||||
});
|
||||
}
|
||||
|
||||
signable.sort_by(|x, y| x.1.compress().to_bytes().cmp(&y.1.compress().to_bytes()).reverse());
|
||||
tx.prefix.inputs.sort_by(|x, y| if let (
|
||||
Input::ToKey { key_image: x, ..},
|
||||
Input::ToKey { key_image: y, ..}
|
||||
) = (x, y) {
|
||||
x.compress().to_bytes().cmp(&y.compress().to_bytes()).reverse()
|
||||
} else {
|
||||
panic!("Input wasn't ToKey")
|
||||
});
|
||||
|
||||
Ok(signable)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SignableTransaction {
|
||||
inputs: Vec<SpendableOutput>,
|
||||
payments: Vec<(Address, u64)>,
|
||||
change: Address,
|
||||
fee_per_byte: u64,
|
||||
|
||||
fee: u64,
|
||||
outputs: Vec<SendOutput>
|
||||
}
|
||||
|
||||
impl SignableTransaction {
|
||||
pub fn new(
|
||||
inputs: Vec<SpendableOutput>,
|
||||
payments: Vec<(Address, u64)>,
|
||||
change: Address,
|
||||
fee_per_byte: u64
|
||||
) -> Result<SignableTransaction, TransactionError> {
|
||||
if inputs.len() == 0 {
|
||||
Err(TransactionError::NoInputs)?;
|
||||
}
|
||||
if payments.len() == 0 {
|
||||
Err(TransactionError::NoOutputs)?;
|
||||
}
|
||||
|
||||
Ok(
|
||||
SignableTransaction {
|
||||
inputs,
|
||||
payments,
|
||||
change,
|
||||
fee_per_byte,
|
||||
|
||||
fee: 0,
|
||||
outputs: vec![]
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fn prepare_outputs<R: RngCore + CryptoRng>(
|
||||
&mut self,
|
||||
rng: &mut R,
|
||||
uniqueness: Option<[u8; 32]>
|
||||
) -> Result<(Vec<Commitment>, Scalar), TransactionError> {
|
||||
self.fee = self.fee_per_byte * 2000; // TODO
|
||||
|
||||
// TODO TX MAX SIZE
|
||||
|
||||
// Make sure we have enough funds
|
||||
let in_amount = self.inputs.iter().map(|input| input.commitment.amount).sum();
|
||||
let out_amount = self.fee + self.payments.iter().map(|payment| payment.1).sum::<u64>();
|
||||
if in_amount < out_amount {
|
||||
Err(TransactionError::NotEnoughFunds(in_amount, out_amount))?;
|
||||
}
|
||||
|
||||
let mut temp_outputs = Vec::with_capacity(self.payments.len() + 1);
|
||||
// Add the payments to the outputs
|
||||
for payment in &self.payments {
|
||||
temp_outputs.push((None, (payment.0, payment.1)));
|
||||
}
|
||||
// Ideally, the change output would always have uniqueness, as we control this wallet software
|
||||
// Unfortunately, if this is used with multisig, doing so would add an extra round due to the
|
||||
// fact Bulletproofs use a leader protocol reliant on this shared key before the first round of
|
||||
// communication. Making the change output unique would require Bulletproofs not be a leader
|
||||
// protocol, using a seeded random
|
||||
// There is a vector where the multisig participants leak the output key they're about to send
|
||||
// to, and someone could use that key, forcing some funds to be burnt accordingly if they win
|
||||
// the race. Any multisig wallet, with this current setup, must only keep change keys in context
|
||||
// accordingly, preferably as soon as they are proposed, even before they appear as confirmed
|
||||
// Using another source of uniqueness would also be possible, yet it'd make scanning a tri-key
|
||||
// system (currently dual for the simpler API, yet would be dual even with a more complex API
|
||||
// under this decision)
|
||||
// TODO after https://github.com/serai-dex/serai/issues/2
|
||||
temp_outputs.push((uniqueness, (self.change, in_amount - out_amount)));
|
||||
|
||||
// Shuffle the outputs
|
||||
temp_outputs.shuffle(rng);
|
||||
|
||||
// Actually create the outputs
|
||||
self.outputs = Vec::with_capacity(temp_outputs.len());
|
||||
let mut commitments = Vec::with_capacity(temp_outputs.len());
|
||||
let mut mask_sum = Scalar::zero();
|
||||
for (o, output) in temp_outputs.iter().enumerate() {
|
||||
self.outputs.push(SendOutput::new(rng, output.0, output.1, o)?);
|
||||
commitments.push(Commitment::new(self.outputs[o].mask, output.1.1));
|
||||
mask_sum += self.outputs[o].mask;
|
||||
}
|
||||
|
||||
Ok((commitments, mask_sum))
|
||||
}
|
||||
|
||||
fn prepare_transaction(
|
||||
&self,
|
||||
commitments: &[Commitment],
|
||||
bp: Bulletproofs
|
||||
) -> Transaction {
|
||||
// Create the TX extra
|
||||
let mut extra = vec![];
|
||||
SubField::TxPublicKey(
|
||||
PublicKey { point: self.outputs[0].R.compress() }
|
||||
).consensus_encode(&mut extra).unwrap();
|
||||
SubField::AdditionalPublickKey(
|
||||
self.outputs[1 .. self.outputs.len()].iter().map(|output| PublicKey { point: output.R.compress() }).collect()
|
||||
).consensus_encode(&mut extra).unwrap();
|
||||
|
||||
// Format it for monero-rs
|
||||
let mut tx_outputs = Vec::with_capacity(self.outputs.len());
|
||||
let mut ecdh_info = Vec::with_capacity(self.outputs.len());
|
||||
for o in 0 .. self.outputs.len() {
|
||||
tx_outputs.push(Output {
|
||||
amount: 0,
|
||||
key: self.outputs[o].dest,
|
||||
tag: None
|
||||
});
|
||||
ecdh_info.push(self.outputs[o].amount);
|
||||
}
|
||||
|
||||
Transaction {
|
||||
prefix: TransactionPrefix {
|
||||
version: 2,
|
||||
unlock_time: 0,
|
||||
inputs: vec![],
|
||||
outputs: tx_outputs,
|
||||
extra
|
||||
},
|
||||
rct_signatures: RctSignatures {
|
||||
base: RctBase {
|
||||
fee: self.fee,
|
||||
ecdh_info,
|
||||
commitments: commitments.iter().map(|commitment| commitment.calculate()).collect()
|
||||
},
|
||||
prunable: RctPrunable::Clsag {
|
||||
bulletproofs: vec![bp],
|
||||
clsags: vec![],
|
||||
pseudo_outs: vec![]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn sign<R: RngCore + CryptoRng>(
|
||||
&mut self,
|
||||
rng: &mut R,
|
||||
rpc: &Rpc,
|
||||
spend: &Scalar
|
||||
) -> Result<Transaction, TransactionError> {
|
||||
let (commitments, mask_sum) = self.prepare_outputs(
|
||||
rng,
|
||||
Some(
|
||||
uniqueness(
|
||||
&self.inputs.iter().map(|input| Input::ToKey {
|
||||
amount: 0,
|
||||
key_offsets: vec![],
|
||||
key_image: generate_key_image(&(spend + input.key_offset))
|
||||
}).collect::<Vec<_>>()
|
||||
)
|
||||
)
|
||||
)?;
|
||||
|
||||
let mut tx = self.prepare_transaction(&commitments, Bulletproofs::new(&commitments)?);
|
||||
|
||||
let signable = prepare_inputs(rng, rpc, &self.inputs, spend, &mut tx).await?;
|
||||
|
||||
let clsag_pairs = Clsag::sign(rng, &signable, mask_sum, tx.signature_hash());
|
||||
match tx.rct_signatures.prunable {
|
||||
RctPrunable::Null => panic!("Signing for RctPrunable::Null"),
|
||||
RctPrunable::Clsag { ref mut clsags, ref mut pseudo_outs, .. } => {
|
||||
clsags.append(&mut clsag_pairs.iter().map(|clsag| clsag.0.clone()).collect::<Vec<_>>());
|
||||
pseudo_outs.append(&mut clsag_pairs.iter().map(|clsag| clsag.1.clone()).collect::<Vec<_>>());
|
||||
}
|
||||
}
|
||||
Ok(tx)
|
||||
}
|
||||
}
|
||||
314
coins/monero/src/wallet/send/multisig.rs
Normal file
314
coins/monero/src/wallet/send/multisig.rs
Normal file
@@ -0,0 +1,314 @@
|
||||
use std::{rc::Rc, cell::RefCell};
|
||||
|
||||
use rand_core::{RngCore, CryptoRng, SeedableRng};
|
||||
use rand_chacha::ChaCha12Rng;
|
||||
|
||||
use curve25519_dalek::{traits::Identity, scalar::Scalar, edwards::{EdwardsPoint, CompressedEdwardsY}};
|
||||
|
||||
use transcript::Transcript as TranscriptTrait;
|
||||
use frost::{FrostError, MultisigKeys, MultisigParams, sign::{State, StateMachine, AlgorithmMachine}};
|
||||
|
||||
use crate::{
|
||||
frost::{Transcript, Ed25519},
|
||||
random_scalar, bulletproofs::Bulletproofs, clsag::{ClsagInput, ClsagDetails, ClsagMultisig},
|
||||
rpc::Rpc,
|
||||
transaction::{Input, RctPrunable, Transaction},
|
||||
wallet::{TransactionError, SignableTransaction, Decoys}
|
||||
};
|
||||
|
||||
pub struct TransactionMachine {
|
||||
leader: bool,
|
||||
signable: SignableTransaction,
|
||||
transcript: Transcript,
|
||||
|
||||
decoys: Vec<Decoys>,
|
||||
|
||||
images: Vec<EdwardsPoint>,
|
||||
output_masks: Option<Scalar>,
|
||||
inputs: Vec<Rc<RefCell<Option<ClsagDetails>>>>,
|
||||
clsags: Vec<AlgorithmMachine<Ed25519, ClsagMultisig>>,
|
||||
|
||||
tx: Option<Transaction>
|
||||
}
|
||||
|
||||
impl SignableTransaction {
|
||||
pub async fn multisig<R: RngCore + CryptoRng>(
|
||||
mut self,
|
||||
label: Vec<u8>,
|
||||
rng: &mut R,
|
||||
rpc: &Rpc,
|
||||
height: usize,
|
||||
keys: MultisigKeys<Ed25519>,
|
||||
included: &[usize]
|
||||
) -> Result<TransactionMachine, TransactionError> {
|
||||
let mut images = vec![];
|
||||
images.resize(self.inputs.len(), EdwardsPoint::identity());
|
||||
let mut inputs = vec![];
|
||||
for _ in 0 .. self.inputs.len() {
|
||||
// Doesn't resize as that will use a single Rc for the entire Vec
|
||||
inputs.push(Rc::new(RefCell::new(None)));
|
||||
}
|
||||
let mut clsags = vec![];
|
||||
|
||||
// Create a RNG out of the input shared keys, which either requires the view key or being every
|
||||
// sender, and the payments (address and amount), which a passive adversary may be able to know
|
||||
// depending on how these transactions are coordinated
|
||||
|
||||
let mut transcript = Transcript::new(label);
|
||||
// Also include the spend_key as below only the key offset is included, so this confirms the sum product
|
||||
// Useful as confirming the sum product confirms the key image, further guaranteeing the one time
|
||||
// properties noted below
|
||||
transcript.append_message(b"spend_key", &keys.group_key().0.compress().to_bytes());
|
||||
for input in &self.inputs {
|
||||
// These outputs can only be spent once. Therefore, it forces all RNGs derived from this
|
||||
// transcript (such as the one used to create one time keys) to be unique
|
||||
transcript.append_message(b"input_hash", &input.tx);
|
||||
transcript.append_message(b"input_output_index", &u16::try_from(input.o).unwrap().to_le_bytes());
|
||||
// Not including this, with a doxxed list of payments, would allow brute forcing the inputs
|
||||
// to determine RNG seeds and therefore the true spends
|
||||
transcript.append_message(b"input_shared_key", &input.key_offset.to_bytes());
|
||||
}
|
||||
for payment in &self.payments {
|
||||
transcript.append_message(b"payment_address", &payment.0.as_bytes());
|
||||
transcript.append_message(b"payment_amount", &payment.1.to_le_bytes());
|
||||
}
|
||||
transcript.append_message(b"change", &self.change.as_bytes());
|
||||
|
||||
// Select decoys
|
||||
// Ideally, this would be done post entropy, instead of now, yet doing so would require sign
|
||||
// to be async which isn't feasible. This should be suitably competent though
|
||||
// While this inability means we can immediately create the input, moving it out of the
|
||||
// Rc RefCell, keeping it within an Rc RefCell keeps our options flexible
|
||||
let decoys = Decoys::select(
|
||||
&mut ChaCha12Rng::from_seed(transcript.rng_seed(b"decoys", None)),
|
||||
rpc,
|
||||
height,
|
||||
&self.inputs
|
||||
).await.map_err(|e| TransactionError::RpcError(e))?;
|
||||
|
||||
for (i, input) in self.inputs.iter().enumerate() {
|
||||
clsags.push(
|
||||
AlgorithmMachine::new(
|
||||
ClsagMultisig::new(
|
||||
transcript.clone(),
|
||||
inputs[i].clone()
|
||||
).map_err(|e| TransactionError::MultisigError(e))?,
|
||||
Rc::new(keys.offset(dalek_ff_group::Scalar(input.key_offset))),
|
||||
included
|
||||
).map_err(|e| TransactionError::FrostError(e))?
|
||||
);
|
||||
}
|
||||
|
||||
// Verify these outputs by a dummy prep
|
||||
self.prepare_outputs(rng, None)?;
|
||||
|
||||
Ok(TransactionMachine {
|
||||
leader: keys.params().i() == included[0],
|
||||
signable: self,
|
||||
transcript,
|
||||
|
||||
decoys,
|
||||
|
||||
images,
|
||||
output_masks: None,
|
||||
inputs,
|
||||
clsags,
|
||||
|
||||
tx: None
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl StateMachine for TransactionMachine {
|
||||
type Signature = Transaction;
|
||||
|
||||
fn preprocess<R: RngCore + CryptoRng>(
|
||||
&mut self,
|
||||
rng: &mut R
|
||||
) -> Result<Vec<u8>, FrostError> {
|
||||
if self.state() != State::Fresh {
|
||||
Err(FrostError::InvalidSignTransition(State::Fresh, self.state()))?;
|
||||
}
|
||||
|
||||
// Iterate over each CLSAG calling preprocess
|
||||
let mut serialized = vec![];
|
||||
for (i, clsag) in self.clsags.iter_mut().enumerate() {
|
||||
let preprocess = clsag.preprocess(rng)?;
|
||||
// First 64 bytes are FROST's commitments
|
||||
self.images[i] += CompressedEdwardsY(preprocess[64 .. 96].try_into().unwrap()).decompress().unwrap();
|
||||
serialized.extend(&preprocess);
|
||||
}
|
||||
|
||||
if self.leader {
|
||||
let mut entropy = [0; 32];
|
||||
rng.fill_bytes(&mut entropy);
|
||||
serialized.extend(&entropy);
|
||||
|
||||
let mut rng = ChaCha12Rng::from_seed(self.transcript.rng_seed(b"tx_keys", Some(entropy)));
|
||||
// Safe to unwrap thanks to the dummy prepare
|
||||
let (commitments, output_masks) = self.signable.prepare_outputs(&mut rng, None).unwrap();
|
||||
self.output_masks = Some(output_masks);
|
||||
|
||||
let bp = Bulletproofs::new(&commitments).unwrap();
|
||||
bp.serialize(&mut serialized).unwrap();
|
||||
|
||||
let tx = self.signable.prepare_transaction(&commitments, bp);
|
||||
self.tx = Some(tx);
|
||||
}
|
||||
|
||||
Ok(serialized)
|
||||
}
|
||||
|
||||
fn sign(
|
||||
&mut self,
|
||||
commitments: &[Option<Vec<u8>>],
|
||||
_: &[u8]
|
||||
) -> Result<Vec<u8>, FrostError> {
|
||||
if self.state() != State::Preprocessed {
|
||||
Err(FrostError::InvalidSignTransition(State::Preprocessed, self.state()))?;
|
||||
}
|
||||
|
||||
// FROST commitments, image, commitments, and their proofs
|
||||
let clsag_len = 64 + ClsagMultisig::serialized_len();
|
||||
let clsag_lens = clsag_len * self.clsags.len();
|
||||
|
||||
// Split out the prep and update the TX
|
||||
let mut tx;
|
||||
if self.leader {
|
||||
tx = self.tx.take().unwrap();
|
||||
} else {
|
||||
let (l, prep) = commitments.iter().enumerate().filter(|(_, prep)| prep.is_some()).next()
|
||||
.ok_or(FrostError::InternalError("no participants".to_string()))?;
|
||||
let prep = prep.as_ref().unwrap();
|
||||
|
||||
// Not invalid outputs due to doing a dummy prep as leader
|
||||
let (commitments, output_masks) = self.signable.prepare_outputs(
|
||||
&mut ChaCha12Rng::from_seed(
|
||||
self.transcript.rng_seed(
|
||||
b"tx_keys",
|
||||
Some(prep[clsag_lens .. (clsag_lens + 32)].try_into().map_err(|_| FrostError::InvalidShare(l))?)
|
||||
)
|
||||
),
|
||||
None
|
||||
).map_err(|_| FrostError::InvalidShare(l))?;
|
||||
self.output_masks.replace(output_masks);
|
||||
|
||||
// Verify the provided bulletproofs if not leader
|
||||
let bp = Bulletproofs::deserialize(
|
||||
&mut std::io::Cursor::new(&prep[(clsag_lens + 32) .. prep.len()])
|
||||
).map_err(|_| FrostError::InvalidShare(l))?;
|
||||
if !bp.verify(&commitments.iter().map(|c| c.calculate()).collect::<Vec<EdwardsPoint>>()) {
|
||||
Err(FrostError::InvalidShare(l))?;
|
||||
}
|
||||
|
||||
tx = self.signable.prepare_transaction(&commitments, bp);
|
||||
}
|
||||
|
||||
for c in 0 .. self.clsags.len() {
|
||||
// Calculate the key images in order to update the TX
|
||||
// Multisig will parse/calculate/validate this as needed, yet doing so here as well provides
|
||||
// the easiest API overall
|
||||
for (l, serialized) in commitments.iter().enumerate().filter(|(_, s)| s.is_some()) {
|
||||
self.images[c] += CompressedEdwardsY(
|
||||
serialized.as_ref().unwrap()[((c * clsag_len) + 64) .. ((c * clsag_len) + 96)]
|
||||
.try_into().map_err(|_| FrostError::InvalidCommitment(l))?
|
||||
).decompress().ok_or(FrostError::InvalidCommitment(l))?;
|
||||
}
|
||||
}
|
||||
|
||||
let mut commitments = (0 .. self.inputs.len()).map(|c| commitments.iter().map(
|
||||
|commitments| commitments.clone().map(
|
||||
|commitments| commitments[(c * clsag_len) .. ((c * clsag_len) + clsag_len)].to_vec()
|
||||
)
|
||||
).collect::<Vec<_>>()).collect::<Vec<_>>();
|
||||
|
||||
let mut sorted = Vec::with_capacity(self.decoys.len());
|
||||
while self.decoys.len() != 0 {
|
||||
sorted.push((
|
||||
self.signable.inputs.swap_remove(0),
|
||||
self.decoys.swap_remove(0),
|
||||
self.images.swap_remove(0),
|
||||
self.inputs.swap_remove(0),
|
||||
self.clsags.swap_remove(0),
|
||||
commitments.swap_remove(0)
|
||||
));
|
||||
}
|
||||
sorted.sort_by(|x, y| x.2.compress().to_bytes().cmp(&y.2.compress().to_bytes()).reverse());
|
||||
|
||||
let mut rng = ChaCha12Rng::from_seed(self.transcript.rng_seed(b"pseudo_out_masks", None));
|
||||
let mut sum_pseudo_outs = Scalar::zero();
|
||||
while sorted.len() != 0 {
|
||||
let value = sorted.remove(0);
|
||||
|
||||
let mut mask = random_scalar(&mut rng);
|
||||
if sorted.len() == 0 {
|
||||
mask = self.output_masks.unwrap() - sum_pseudo_outs;
|
||||
} else {
|
||||
sum_pseudo_outs += mask;
|
||||
}
|
||||
|
||||
tx.prefix.inputs.push(
|
||||
Input::ToKey {
|
||||
amount: 0,
|
||||
key_offsets: value.1.offsets.clone(),
|
||||
key_image: value.2
|
||||
}
|
||||
);
|
||||
|
||||
value.3.replace(
|
||||
Some(
|
||||
ClsagDetails::new(
|
||||
ClsagInput::new(
|
||||
value.0.commitment,
|
||||
value.1
|
||||
).map_err(|_| panic!("Signing an input which isn't present in the ring we created for it"))?,
|
||||
mask
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
self.clsags.push(value.4);
|
||||
commitments.push(value.5);
|
||||
}
|
||||
|
||||
let msg = tx.signature_hash();
|
||||
self.tx = Some(tx);
|
||||
|
||||
// Iterate over each CLSAG calling sign
|
||||
let mut serialized = Vec::with_capacity(self.clsags.len() * 32);
|
||||
for (c, clsag) in self.clsags.iter_mut().enumerate() {
|
||||
serialized.extend(&clsag.sign(&commitments[c], &msg)?);
|
||||
}
|
||||
|
||||
Ok(serialized)
|
||||
}
|
||||
|
||||
fn complete(&mut self, shares: &[Option<Vec<u8>>]) -> Result<Transaction, FrostError> {
|
||||
if self.state() != State::Signed {
|
||||
Err(FrostError::InvalidSignTransition(State::Signed, self.state()))?;
|
||||
}
|
||||
|
||||
let mut tx = self.tx.take().unwrap();
|
||||
match tx.rct_signatures.prunable {
|
||||
RctPrunable::Null => panic!("Signing for RctPrunable::Null"),
|
||||
RctPrunable::Clsag { ref mut clsags, ref mut pseudo_outs, .. } => {
|
||||
for (c, clsag) in self.clsags.iter_mut().enumerate() {
|
||||
let (clsag, pseudo_out) = clsag.complete(&shares.iter().map(
|
||||
|share| share.clone().map(|share| share[(c * 32) .. ((c * 32) + 32)].to_vec())
|
||||
).collect::<Vec<_>>())?;
|
||||
clsags.push(clsag);
|
||||
pseudo_outs.push(pseudo_out);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(tx)
|
||||
}
|
||||
|
||||
fn multisig_params(&self) -> MultisigParams {
|
||||
self.clsags[0].multisig_params()
|
||||
}
|
||||
|
||||
fn state(&self) -> State {
|
||||
self.clsags[0].state()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user