mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-09 12:49:23 +00:00
Support signing Monero TXs with multiple inputs
Remove's CLSAG's msg Rc for the msg available through AlgorithmMachine. Potentially slightly more inefficient, as it needs to be converted from a slice to a [u8; 32], yet removes a re-impl. Also removes a match for an if.
This commit is contained in:
@@ -85,16 +85,15 @@ pub struct Multisig {
|
||||
AH: (dfg::EdwardsPoint, dfg::EdwardsPoint),
|
||||
|
||||
details: Rc<RefCell<Option<Details>>>,
|
||||
msg: Rc<RefCell<Option<[u8; 32]>>>,
|
||||
|
||||
msg: Option<[u8; 32]>,
|
||||
interim: Option<Interim>
|
||||
}
|
||||
|
||||
impl Multisig {
|
||||
pub fn new(
|
||||
transcript: Transcript,
|
||||
details: Rc<RefCell<Option<Details>>>,
|
||||
msg: Rc<RefCell<Option<[u8; 32]>>>,
|
||||
details: Rc<RefCell<Option<Details>>>
|
||||
) -> Result<Multisig, MultisigError> {
|
||||
Ok(
|
||||
Multisig {
|
||||
@@ -105,8 +104,8 @@ impl Multisig {
|
||||
AH: (dfg::EdwardsPoint::identity(), dfg::EdwardsPoint::identity()),
|
||||
|
||||
details,
|
||||
msg,
|
||||
|
||||
msg: None,
|
||||
interim: None
|
||||
}
|
||||
)
|
||||
@@ -123,10 +122,6 @@ impl Multisig {
|
||||
fn mask(&self) -> Scalar {
|
||||
self.details.borrow().as_ref().unwrap().mask
|
||||
}
|
||||
|
||||
fn msg(&self) -> [u8; 32] {
|
||||
*self.msg.borrow().as_ref().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl Algorithm<Ed25519> for Multisig {
|
||||
@@ -168,7 +163,6 @@ impl Algorithm<Ed25519> for Multisig {
|
||||
self.transcript.domain_separate(b"CLSAG");
|
||||
self.input().transcript(&mut self.transcript);
|
||||
self.transcript.append_message(b"mask", &self.mask().to_bytes());
|
||||
self.transcript.append_message(b"message", &self.msg());
|
||||
}
|
||||
|
||||
let share = read_dleq(
|
||||
@@ -208,7 +202,7 @@ impl Algorithm<Ed25519> for Multisig {
|
||||
nonce_sum: dfg::EdwardsPoint,
|
||||
b: dfg::Scalar,
|
||||
nonce: dfg::Scalar,
|
||||
_: &[u8]
|
||||
msg: &[u8]
|
||||
) -> dfg::Scalar {
|
||||
// Apply the binding factor to the H variant of the nonce
|
||||
self.AH.0 += self.AH.1 * b;
|
||||
@@ -220,13 +214,15 @@ impl Algorithm<Ed25519> for Multisig {
|
||||
// input commitment masks)
|
||||
let mut rng = ChaCha12Rng::from_seed(self.transcript.rng_seed(b"decoy_responses", None));
|
||||
|
||||
self.msg = Some(msg.try_into().expect("CLSAG message should be 32-bytes"));
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
let (clsag, pseudo_out, p, c) = sign_core(
|
||||
&mut rng,
|
||||
&self.image,
|
||||
&self.input(),
|
||||
self.mask(),
|
||||
&self.msg(),
|
||||
&self.msg.as_ref().unwrap(),
|
||||
nonce_sum.0,
|
||||
self.AH.0.0
|
||||
);
|
||||
@@ -246,7 +242,13 @@ impl Algorithm<Ed25519> for Multisig {
|
||||
let interim = self.interim.as_ref().unwrap();
|
||||
let mut clsag = interim.clsag.clone();
|
||||
clsag.s[usize::from(self.input().decoys.i)] = Key { key: (sum.0 - interim.c).to_bytes() };
|
||||
if verify(&clsag, &self.input().decoys.ring, &self.image, &interim.pseudo_out, &self.msg()).is_ok() {
|
||||
if verify(
|
||||
&clsag,
|
||||
&self.input().decoys.ring,
|
||||
&self.image,
|
||||
&interim.pseudo_out,
|
||||
&self.msg.as_ref().unwrap()
|
||||
).is_ok() {
|
||||
return Some((clsag, interim.pseudo_out));
|
||||
}
|
||||
return None;
|
||||
|
||||
@@ -95,11 +95,9 @@ pub fn scan(tx: &Transaction, view: Scalar, spend: EdwardsPoint) -> Vec<Spendabl
|
||||
|
||||
let mut res = vec![];
|
||||
for o in 0 .. tx.prefix.outputs.len() {
|
||||
let output_key = match tx.prefix.outputs[o].target {
|
||||
TxOutTarget::ToScript { .. } => None,
|
||||
TxOutTarget::ToScriptHash { .. } => None,
|
||||
TxOutTarget::ToKey { key } => key.point.decompress()
|
||||
};
|
||||
let output_key = if let TxOutTarget::ToKey { key } = tx.prefix.outputs[o].target {
|
||||
key.point.decompress()
|
||||
} else { None };
|
||||
if output_key.is_none() {
|
||||
continue;
|
||||
}
|
||||
@@ -160,6 +158,7 @@ fn amount_encryption(amount: u64, key: Scalar) -> Hash8 {
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
#[derive(Clone, Debug)]
|
||||
struct Output {
|
||||
R: EdwardsPoint,
|
||||
dest: EdwardsPoint,
|
||||
@@ -200,8 +199,6 @@ async fn prepare_inputs<R: RngCore + CryptoRng>(
|
||||
spend: &Scalar,
|
||||
tx: &mut Transaction
|
||||
) -> Result<Vec<(Scalar, EdwardsPoint, clsag::Input)>, TransactionError> {
|
||||
// TODO sort inputs
|
||||
|
||||
let mut signable = Vec::with_capacity(inputs.len());
|
||||
|
||||
// Select decoys
|
||||
@@ -229,9 +226,20 @@ async fn prepare_inputs<R: RngCore + CryptoRng>(
|
||||
});
|
||||
}
|
||||
|
||||
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 (
|
||||
TxIn::ToKey{ k_image: x, ..},
|
||||
TxIn::ToKey{ k_image: y, ..}
|
||||
) = (x, y) {
|
||||
x.image.cmp(&y.image).reverse()
|
||||
} else {
|
||||
panic!("TxIn wasn't ToKey")
|
||||
});
|
||||
|
||||
Ok(signable)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SignableTransaction {
|
||||
inputs: Vec<SpendableOutput>,
|
||||
payments: Vec<(Address, u64)>,
|
||||
|
||||
@@ -29,10 +29,9 @@ pub struct TransactionMachine {
|
||||
|
||||
decoys: Vec<Decoys>,
|
||||
|
||||
our_images: Vec<EdwardsPoint>,
|
||||
images: Vec<EdwardsPoint>,
|
||||
output_masks: Option<Scalar>,
|
||||
inputs: Vec<Rc<RefCell<Option<clsag::Details>>>>,
|
||||
msg: Rc<RefCell<Option<[u8; 32]>>>,
|
||||
clsags: Vec<AlgorithmMachine<Ed25519, clsag::Multisig>>,
|
||||
|
||||
tx: Option<Transaction>
|
||||
@@ -45,14 +44,16 @@ impl SignableTransaction {
|
||||
rng: &mut R,
|
||||
rpc: &Rpc,
|
||||
height: usize,
|
||||
keys: Rc<MultisigKeys<Ed25519>>,
|
||||
keys: MultisigKeys<Ed25519>,
|
||||
included: &[usize]
|
||||
) -> Result<TransactionMachine, TransactionError> {
|
||||
let mut our_images = vec![];
|
||||
our_images.resize(self.inputs.len(), EdwardsPoint::identity());
|
||||
let mut images = vec![];
|
||||
images.resize(self.inputs.len(), EdwardsPoint::identity());
|
||||
let mut inputs = vec![];
|
||||
inputs.resize(self.inputs.len(), Rc::new(RefCell::new(None)));
|
||||
let msg = Rc::new(RefCell::new(None));
|
||||
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
|
||||
@@ -96,8 +97,7 @@ impl SignableTransaction {
|
||||
AlgorithmMachine::new(
|
||||
clsag::Multisig::new(
|
||||
transcript.clone(),
|
||||
inputs[i].clone(),
|
||||
msg.clone()
|
||||
inputs[i].clone()
|
||||
).map_err(|e| TransactionError::MultisigError(e))?,
|
||||
Rc::new(keys.offset(dalek_ff_group::Scalar(input.key_offset))),
|
||||
included
|
||||
@@ -115,10 +115,9 @@ impl SignableTransaction {
|
||||
|
||||
decoys,
|
||||
|
||||
our_images,
|
||||
images,
|
||||
output_masks: None,
|
||||
inputs,
|
||||
msg,
|
||||
clsags,
|
||||
|
||||
tx: None
|
||||
@@ -142,7 +141,7 @@ impl StateMachine for TransactionMachine {
|
||||
for (i, clsag) in self.clsags.iter_mut().enumerate() {
|
||||
let preprocess = clsag.preprocess(rng)?;
|
||||
// First 64 bytes are FROST's commitments
|
||||
self.our_images[i] += CompressedEdwardsY(preprocess[64 .. 96].try_into().unwrap()).decompress().unwrap();
|
||||
self.images[i] += CompressedEdwardsY(preprocess[64 .. 96].try_into().unwrap()).decompress().unwrap();
|
||||
serialized.extend(&preprocess);
|
||||
}
|
||||
|
||||
@@ -209,63 +208,79 @@ impl StateMachine for TransactionMachine {
|
||||
}
|
||||
|
||||
let mut rng = ChaCha12Rng::from_seed(self.transcript.rng_seed(b"pseudo_out_masks", None));
|
||||
let mut sum_pseudo_outs = Scalar::zero();
|
||||
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
|
||||
let mut image = self.our_images[c];
|
||||
for (l, serialized) in commitments.iter().enumerate().filter(|(_, s)| s.is_some()) {
|
||||
image += CompressedEdwardsY(
|
||||
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))?;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO sort inputs
|
||||
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 sum_pseudo_outs = Scalar::zero();
|
||||
while sorted.len() != 0 {
|
||||
let value = sorted.remove(0);
|
||||
|
||||
let mut mask = random_scalar(&mut rng);
|
||||
if c == (self.clsags.len() - 1) {
|
||||
if sorted.len() == 0 {
|
||||
mask = self.output_masks.unwrap() - sum_pseudo_outs;
|
||||
} else {
|
||||
sum_pseudo_outs += mask;
|
||||
}
|
||||
|
||||
self.inputs[c].replace(
|
||||
tx.prefix.inputs.push(
|
||||
TxIn::ToKey {
|
||||
amount: VarInt(0),
|
||||
key_offsets: value.1.offsets.clone(),
|
||||
k_image: KeyImage { image: Hash(value.2.compress().to_bytes()) }
|
||||
}
|
||||
);
|
||||
|
||||
value.3.replace(
|
||||
Some(
|
||||
clsag::Details::new(
|
||||
clsag::Input::new(
|
||||
self.signable.inputs[c].commitment,
|
||||
self.decoys[c].clone()
|
||||
value.0.commitment,
|
||||
value.1
|
||||
).map_err(|_| panic!("Signing an input which isn't present in the ring we created for it"))?,
|
||||
mask
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
tx.prefix.inputs.push(
|
||||
TxIn::ToKey {
|
||||
amount: VarInt(0),
|
||||
key_offsets: self.decoys[c].offsets.clone(),
|
||||
k_image: KeyImage { image: Hash(image.compress().to_bytes()) }
|
||||
}
|
||||
);
|
||||
self.clsags.push(value.4);
|
||||
commitments.push(value.5);
|
||||
}
|
||||
|
||||
self.msg.replace(Some(tx.signature_hash().unwrap().0));
|
||||
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.iter().map(
|
||||
|commitments| commitments.clone().map(
|
||||
|commitments| commitments[(c * clsag_len) .. ((c * clsag_len) + clsag_len)].to_vec()
|
||||
)
|
||||
).collect::<Vec<_>>(),
|
||||
&vec![]
|
||||
)?);
|
||||
serialized.extend(&clsag.sign(&commitments[c], &msg)?);
|
||||
}
|
||||
|
||||
Ok(serialized)
|
||||
|
||||
Reference in New Issue
Block a user