mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-09 04:39:24 +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:
@@ -109,16 +109,15 @@ fn clsag_multisig() -> Result<(), MultisigError> {
|
||||
).unwrap(),
|
||||
mask_sum
|
||||
)
|
||||
))),
|
||||
Rc::new(RefCell::new(Some([1; 32])))
|
||||
)))
|
||||
).unwrap(),
|
||||
keys[i - 1].clone(),
|
||||
Rc::new(keys[i - 1].clone()),
|
||||
&(1 ..= THRESHOLD).collect::<Vec<usize>>()
|
||||
).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
let mut signatures = sign(&mut machines, keys);
|
||||
let mut signatures = sign(&mut machines, &[1; 32]);
|
||||
let signature = signatures.swap_remove(0);
|
||||
for s in 0 .. (t - 1) {
|
||||
// Verify the commitments and the non-decoy s scalar are identical to every other signature
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
#![cfg(feature = "multisig")]
|
||||
|
||||
use std::rc::Rc;
|
||||
|
||||
use rand::rngs::OsRng;
|
||||
|
||||
use ff::Field;
|
||||
@@ -17,7 +15,7 @@ use monero_serai::frost::Ed25519;
|
||||
pub const THRESHOLD: usize = 3;
|
||||
pub const PARTICIPANTS: usize = 5;
|
||||
|
||||
pub fn generate_keys() -> (Vec<Rc<MultisigKeys<Ed25519>>>, Scalar) {
|
||||
pub fn generate_keys() -> (Vec<MultisigKeys<Ed25519>>, Scalar) {
|
||||
let mut params = vec![];
|
||||
let mut machines = vec![];
|
||||
let mut commitments = vec![vec![]];
|
||||
@@ -54,7 +52,7 @@ pub fn generate_keys() -> (Vec<Rc<MultisigKeys<Ed25519>>>, Scalar) {
|
||||
our_secret_shares.extend(
|
||||
secret_shares.iter().map(|shares| shares[i].clone()).collect::<Vec<Vec<u8>>>()
|
||||
);
|
||||
keys.push(Rc::new(machines[i - 1].complete(our_secret_shares).unwrap().clone()));
|
||||
keys.push(machines[i - 1].complete(our_secret_shares).unwrap().clone());
|
||||
}
|
||||
|
||||
let mut group_private = Scalar::zero();
|
||||
@@ -70,12 +68,8 @@ pub fn generate_keys() -> (Vec<Rc<MultisigKeys<Ed25519>>>, Scalar) {
|
||||
}
|
||||
|
||||
#[allow(dead_code)] // Currently has some false positive
|
||||
pub fn sign<S, M: sign::StateMachine<Signature = S>>(
|
||||
machines: &mut Vec<M>,
|
||||
keys: Vec<Rc<MultisigKeys<Ed25519>>>
|
||||
) -> Vec<S> {
|
||||
pub fn sign<S, M: sign::StateMachine<Signature = S>>(machines: &mut Vec<M>, msg: &[u8]) -> Vec<S> {
|
||||
assert!(machines.len() >= THRESHOLD);
|
||||
assert!(keys.len() >= machines.len());
|
||||
|
||||
let mut commitments = Vec::with_capacity(PARTICIPANTS + 1);
|
||||
commitments.resize(PARTICIPANTS + 1, None);
|
||||
@@ -93,7 +87,7 @@ pub fn sign<S, M: sign::StateMachine<Signature = S>>(
|
||||
.enumerate()
|
||||
.map(|(idx, value)| if idx == i { None } else { value.to_owned() })
|
||||
.collect::<Vec<Option<Vec<u8>>>>(),
|
||||
&vec![]
|
||||
msg
|
||||
).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,25 +1,62 @@
|
||||
use std::sync::Mutex;
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
use rand::rngs::OsRng;
|
||||
|
||||
#[cfg(feature = "multisig")]
|
||||
use blake2::{digest::Update, Digest, Blake2b512};
|
||||
|
||||
use curve25519_dalek::constants::ED25519_BASEPOINT_TABLE;
|
||||
#[cfg(feature = "multisig")]
|
||||
use dalek_ff_group::Scalar;
|
||||
|
||||
use monero::{
|
||||
network::Network,
|
||||
util::{key::PublicKey, address::Address}
|
||||
};
|
||||
#[cfg(feature = "multisig")]
|
||||
use monero::cryptonote::hash::Hashable;
|
||||
|
||||
use monero_serai::{random_scalar, transaction::{self, SignableTransaction}};
|
||||
|
||||
mod rpc;
|
||||
use crate::rpc::{rpc, mine_block};
|
||||
|
||||
#[tokio::test]
|
||||
pub async fn send() {
|
||||
#[cfg(feature = "multisig")]
|
||||
mod frost;
|
||||
#[cfg(feature = "multisig")]
|
||||
use crate::frost::{THRESHOLD, generate_keys, sign};
|
||||
|
||||
lazy_static! {
|
||||
static ref SEQUENTIAL: Mutex<()> = Mutex::new(());
|
||||
}
|
||||
|
||||
pub async fn send_core(test: usize, multisig: bool) {
|
||||
let _guard = SEQUENTIAL.lock().unwrap();
|
||||
let rpc = rpc().await;
|
||||
|
||||
// Generate an address
|
||||
let view = random_scalar(&mut OsRng);
|
||||
let spend = random_scalar(&mut OsRng);
|
||||
let spend_pub = &spend * &ED25519_BASEPOINT_TABLE;
|
||||
#[allow(unused_mut)]
|
||||
let mut view = random_scalar(&mut OsRng);
|
||||
#[allow(unused_mut)]
|
||||
let mut spend_pub = &spend * &ED25519_BASEPOINT_TABLE;
|
||||
|
||||
#[cfg(feature = "multisig")]
|
||||
let (keys, _) = generate_keys();
|
||||
#[cfg(feature = "multisig")]
|
||||
let t = keys[0].params().t();
|
||||
|
||||
if multisig {
|
||||
#[cfg(not(feature = "multisig"))]
|
||||
panic!("Running a multisig test without the multisig feature");
|
||||
#[cfg(feature = "multisig")]
|
||||
{
|
||||
view = Scalar::from_hash(Blake2b512::new().chain("Monero Serai Transaction Test")).0;
|
||||
spend_pub = keys[0].group_key().0;
|
||||
}
|
||||
}
|
||||
|
||||
let addr = Address::standard(
|
||||
Network::Mainnet,
|
||||
@@ -27,26 +64,99 @@ pub async fn send() {
|
||||
PublicKey { point: (&view * &ED25519_BASEPOINT_TABLE).compress() }
|
||||
);
|
||||
|
||||
// TODO
|
||||
let fee_per_byte = 50000000;
|
||||
let fee = fee_per_byte * 2000;
|
||||
|
||||
let mut tx;
|
||||
let mut output;
|
||||
let mut amount;
|
||||
for i in 0 .. 2 {
|
||||
let start = rpc.get_height().await.unwrap();
|
||||
for _ in 0 .. 7 {
|
||||
mine_block(&rpc, &addr.to_string()).await.unwrap();
|
||||
let start = rpc.get_height().await.unwrap();
|
||||
for _ in 0 .. 7 {
|
||||
mine_block(&rpc, &addr.to_string()).await.unwrap();
|
||||
}
|
||||
|
||||
let mut tx = None;
|
||||
// Allow tests to test variable transactions
|
||||
for i in 0 .. [2, 1][test] {
|
||||
let mut outputs = vec![];
|
||||
let mut amount = 0;
|
||||
// Test spending both a miner output and a normal output
|
||||
if test == 0 {
|
||||
if i == 0 {
|
||||
tx = Some(rpc.get_block_transactions(start).await.unwrap().swap_remove(0));
|
||||
}
|
||||
|
||||
let output = transaction::scan(tx.as_ref().unwrap(), view, spend_pub).swap_remove(0);
|
||||
// Test creating a zero change output and a non-zero change output
|
||||
amount = output.commitment.amount - u64::try_from(i).unwrap();
|
||||
outputs.push(output);
|
||||
|
||||
// Test spending multiple inputs
|
||||
} else if test == 1 {
|
||||
if i != 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
for i in (start + 1) .. (start + 9) {
|
||||
let tx = rpc.get_block_transactions(i).await.unwrap().swap_remove(0);
|
||||
let output = transaction::scan(&tx, view, spend_pub).swap_remove(0);
|
||||
amount += output.commitment.amount;
|
||||
outputs.push(output);
|
||||
}
|
||||
}
|
||||
|
||||
// Test both a miner output and a normal output
|
||||
tx = rpc.get_block_transactions(start).await.unwrap().swap_remove(i);
|
||||
output = transaction::scan(&tx, view, spend_pub).swap_remove(0);
|
||||
// Test creating a zero change output and a non-zero change output
|
||||
amount = output.commitment.amount - fee - u64::try_from(i).unwrap();
|
||||
let tx = SignableTransaction::new(
|
||||
vec![output], vec![(addr, amount)], addr, fee_per_byte
|
||||
).unwrap().sign(&mut OsRng, &rpc, &spend).await.unwrap();
|
||||
rpc.publish_transaction(&tx).await.unwrap();
|
||||
let mut signable = SignableTransaction::new(
|
||||
outputs, vec![(addr, amount - fee)], addr, fee_per_byte
|
||||
).unwrap();
|
||||
|
||||
if !multisig {
|
||||
tx = Some(signable.sign(&mut OsRng, &rpc, &spend).await.unwrap());
|
||||
} else {
|
||||
#[cfg(feature = "multisig")]
|
||||
{
|
||||
let mut machines = Vec::with_capacity(t);
|
||||
for i in 1 ..= t {
|
||||
machines.push(
|
||||
signable.clone().multisig(
|
||||
b"Monero Serai Test Transaction".to_vec(),
|
||||
&mut OsRng,
|
||||
&rpc,
|
||||
rpc.get_height().await.unwrap() - 10,
|
||||
keys[i - 1].clone(),
|
||||
&(1 ..= THRESHOLD).collect::<Vec<usize>>()
|
||||
).await.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
let mut txs = sign(&mut machines, &vec![]);
|
||||
for s in 0 .. (t - 1) {
|
||||
assert_eq!(txs[s].hash(), txs[0].hash());
|
||||
}
|
||||
tx = Some(txs.swap_remove(0));
|
||||
}
|
||||
}
|
||||
|
||||
rpc.publish_transaction(tx.as_ref().unwrap()).await.unwrap();
|
||||
mine_block(&rpc, &addr.to_string()).await.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
pub async fn send_single_input() {
|
||||
send_core(0, false).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
pub async fn send_multiple_inputs() {
|
||||
send_core(1, false).await;
|
||||
}
|
||||
|
||||
#[cfg(feature = "multisig")]
|
||||
#[tokio::test]
|
||||
pub async fn multisig_send_single_input() {
|
||||
send_core(0, true).await;
|
||||
}
|
||||
|
||||
#[cfg(feature = "multisig")]
|
||||
#[tokio::test]
|
||||
pub async fn multisig_send_multiple_inputs() {
|
||||
send_core(1, true).await;
|
||||
}
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
#![cfg(feature = "multisig")]
|
||||
|
||||
use rand::rngs::OsRng;
|
||||
|
||||
use blake2::{digest::Update, Digest, Blake2b512};
|
||||
|
||||
use curve25519_dalek::constants::ED25519_BASEPOINT_TABLE;
|
||||
use dalek_ff_group::Scalar;
|
||||
|
||||
use monero::{
|
||||
cryptonote::hash::Hashable,
|
||||
network::Network,
|
||||
util::{key::PublicKey, address::Address}
|
||||
};
|
||||
|
||||
use monero_serai::transaction::{self, SignableTransaction};
|
||||
|
||||
mod rpc;
|
||||
use crate::rpc::{rpc, mine_block};
|
||||
|
||||
mod frost;
|
||||
use crate::frost::{THRESHOLD, generate_keys, sign};
|
||||
|
||||
#[tokio::test]
|
||||
pub async fn send_multisig() {
|
||||
let rpc = rpc().await;
|
||||
|
||||
let fee_per_byte = 50000000;
|
||||
let fee = fee_per_byte * 2000;
|
||||
|
||||
let (keys, _) = generate_keys();
|
||||
let t = keys[0].params().t();
|
||||
|
||||
// Generate an address
|
||||
let view = Scalar::from_hash(Blake2b512::new().chain("Monero Serai Transaction Test")).0;
|
||||
let spend = keys[0].group_key().0;
|
||||
let addr = Address::standard(
|
||||
Network::Mainnet,
|
||||
PublicKey { point: spend.compress() },
|
||||
PublicKey { point: (&view * &ED25519_BASEPOINT_TABLE).compress() }
|
||||
);
|
||||
|
||||
// Mine blocks to that address
|
||||
let start = rpc.get_height().await.unwrap();
|
||||
for _ in 0 .. 7 {
|
||||
mine_block(&rpc, &addr.to_string()).await.unwrap();
|
||||
}
|
||||
|
||||
// Get the input TX
|
||||
let tx = rpc.get_block_transactions(start).await.unwrap().swap_remove(0);
|
||||
let output = transaction::scan(&tx, view, spend).swap_remove(0);
|
||||
let amount = output.commitment.amount - fee;
|
||||
|
||||
let mut machines = Vec::with_capacity(t);
|
||||
for i in 1 ..= t {
|
||||
machines.push(
|
||||
SignableTransaction::new(
|
||||
vec![output.clone()], vec![(addr, amount)], addr, fee_per_byte
|
||||
).unwrap().multisig(
|
||||
b"Monero Serai Test Transaction".to_vec(),
|
||||
&mut OsRng,
|
||||
&rpc,
|
||||
rpc.get_height().await.unwrap() - 10,
|
||||
keys[i - 1].clone(),
|
||||
&(1 ..= THRESHOLD).collect::<Vec<usize>>()
|
||||
).await.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
let txs = sign(&mut machines, keys);
|
||||
for s in 0 .. (t - 1) {
|
||||
assert_eq!(txs[s].hash(), txs[0].hash());
|
||||
}
|
||||
rpc.publish_transaction(&txs[0]).await.unwrap();
|
||||
}
|
||||
Reference in New Issue
Block a user