Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add more unit tests #275

Merged
merged 1 commit into from
Jul 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions coins/monero/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ monero-bulletproofs = { path = "ringct/bulletproofs", version = "0.1", default-f

hex-literal = "0.4"

[dev-dependencies]
hex = { version = "0.4", default-features = false, features = ["std"] }
serde = { version = "1", default-features = false, features = ["std", "derive"] }
serde_json = { version = "1", default-features = false, features = ["std"] }

[features]
std = [
"std-shims/std",
Expand Down
3 changes: 3 additions & 0 deletions coins/monero/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ pub mod transaction;
/// Block structs and functionality.
pub mod block;

#[cfg(test)]
mod tests;

/// The minimum amount of blocks an output is locked for.
///
/// If Monero suffered a re-organization, any transactions which selected decoys belonging to
Expand Down
11 changes: 10 additions & 1 deletion coins/monero/src/ring_signatures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,14 @@ use curve25519_dalek::{EdwardsPoint, Scalar};
use crate::{io::*, generators::hash_to_point, primitives::keccak256_to_scalar};

#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
struct Signature {
pub(crate) struct Signature {
#[cfg(test)]
pub(crate) c: Scalar,
#[cfg(test)]
pub(crate) s: Scalar,
#[cfg(not(test))]
c: Scalar,
#[cfg(not(test))]
s: Scalar,
}

Expand All @@ -32,6 +38,9 @@ impl Signature {
/// This was used by the original Cryptonote transaction protocol and was deprecated with RingCT.
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
pub struct RingSignature {
#[cfg(test)]
pub(crate) sigs: Vec<Signature>,
#[cfg(not(test))]
sigs: Vec<Signature>,
}

Expand Down
1 change: 1 addition & 0 deletions coins/monero/src/tests/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
mod transaction;
287 changes: 287 additions & 0 deletions coins/monero/src/tests/transaction.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
use curve25519_dalek::{
edwards::{CompressedEdwardsY, EdwardsPoint},
scalar::Scalar,
};

use serde_json::Value;

use crate::{
ringct::RctPrunable,
transaction::{NotPruned, Transaction, Timelock, Input},
};

const TRANSACTIONS: &str = include_str!("./vectors/transactions.json");
const CLSAG_TX: &str = include_str!("./vectors/clsag_tx.json");
const RING_DATA: &str = include_str!("./vectors/ring_data.json");

#[derive(serde::Deserialize)]
struct Vector {
id: String,
hex: String,
signature_hash: String,
tx: Value,
}

fn tx_vectors() -> Vec<Vector> {
serde_json::from_str(TRANSACTIONS).unwrap()
}

fn point(hex: &Value) -> EdwardsPoint {
CompressedEdwardsY(hex::decode(hex.as_str().unwrap()).unwrap().try_into().unwrap())
.decompress()
.unwrap()
}

fn scalar(hex: &Value) -> Scalar {
Scalar::from_canonical_bytes(hex::decode(hex.as_str().unwrap()).unwrap().try_into().unwrap())
.unwrap()
}

fn point_vector(val: &Value) -> Vec<EdwardsPoint> {
let mut v = vec![];
for hex in val.as_array().unwrap() {
v.push(point(hex));
}
v
}

fn scalar_vector(val: &Value) -> Vec<Scalar> {
let mut v = vec![];
for hex in val.as_array().unwrap() {
v.push(scalar(hex));
}
v
}

#[test]
fn parse() {
for v in tx_vectors() {
let tx =
Transaction::<NotPruned>::read(&mut hex::decode(v.hex.clone()).unwrap().as_slice()).unwrap();

// check version
assert_eq!(tx.version(), v.tx["version"]);

// check unlock time
match tx.prefix().additional_timelock {
Timelock::None => assert_eq!(0, v.tx["unlock_time"]),
Timelock::Block(h) => assert_eq!(h, v.tx["unlock_time"]),
Timelock::Time(t) => assert_eq!(t, v.tx["unlock_time"]),
}

// check inputs
let inputs = v.tx["vin"].as_array().unwrap();
assert_eq!(tx.prefix().inputs.len(), inputs.len());
for (i, input) in tx.prefix().inputs.iter().enumerate() {
match input {
Input::Gen(h) => assert_eq!(*h, inputs[i]["gen"]["height"]),
Input::ToKey { amount, key_offsets, key_image } => {
let key = &inputs[i]["key"];
assert_eq!(amount.unwrap_or(0), key["amount"]);
assert_eq!(*key_image, point(&key["k_image"]));
assert_eq!(key_offsets, key["key_offsets"].as_array().unwrap());
}
}
}

// check outputs
let outputs = v.tx["vout"].as_array().unwrap();
assert_eq!(tx.prefix().outputs.len(), outputs.len());
for (i, output) in tx.prefix().outputs.iter().enumerate() {
assert_eq!(output.amount.unwrap_or(0), outputs[i]["amount"]);
if output.view_tag.is_some() {
assert_eq!(output.key, point(&outputs[i]["target"]["tagged_key"]["key"]).compress());
let view_tag =
hex::decode(outputs[i]["target"]["tagged_key"]["view_tag"].as_str().unwrap()).unwrap();
assert_eq!(view_tag.len(), 1);
assert_eq!(output.view_tag.unwrap(), view_tag[0]);
} else {
assert_eq!(output.key, point(&outputs[i]["target"]["key"]).compress());
}
}

// check extra
assert_eq!(tx.prefix().extra, v.tx["extra"].as_array().unwrap().as_slice());

match &tx {
Transaction::V1 { signatures, .. } => {
// check signatures for v1 txs
let sigs_array = v.tx["signatures"].as_array().unwrap();
for (i, sig) in signatures.iter().enumerate() {
let tx_sig = hex::decode(sigs_array[i].as_str().unwrap()).unwrap();
for (i, sig) in sig.sigs.iter().enumerate() {
let start = i * 64;
let c: [u8; 32] = tx_sig[start .. (start + 32)].try_into().unwrap();
let s: [u8; 32] = tx_sig[(start + 32) .. (start + 64)].try_into().unwrap();
assert_eq!(sig.c, Scalar::from_canonical_bytes(c).unwrap());
assert_eq!(sig.s, Scalar::from_canonical_bytes(s).unwrap());
}
}
}
Transaction::V2 { proofs: None, .. } => assert_eq!(v.tx["rct_signatures"]["type"], 0),
Transaction::V2 { proofs: Some(proofs), .. } => {
// check rct signatures
let rct = &v.tx["rct_signatures"];
assert_eq!(u8::from(proofs.rct_type()), rct["type"]);

assert_eq!(proofs.base.fee, rct["txnFee"]);
assert_eq!(proofs.base.commitments, point_vector(&rct["outPk"]));
let ecdh_info = rct["ecdhInfo"].as_array().unwrap();
assert_eq!(proofs.base.encrypted_amounts.len(), ecdh_info.len());
for (i, ecdh) in proofs.base.encrypted_amounts.iter().enumerate() {
let mut buf = vec![];
ecdh.write(&mut buf).unwrap();
assert_eq!(buf, hex::decode(ecdh_info[i]["amount"].as_str().unwrap()).unwrap());
}

// check ringct prunable
match &proofs.prunable {
RctPrunable::Clsag { bulletproof: _, clsags, pseudo_outs } => {
// check bulletproofs
/* TODO
for (i, bp) in bulletproofs.iter().enumerate() {
match bp {
Bulletproof::Original(o) => {
let bps = v.tx["rctsig_prunable"]["bp"].as_array().unwrap();
assert_eq!(bulletproofs.len(), bps.len());
assert_eq!(o.A, point(&bps[i]["A"]));
assert_eq!(o.S, point(&bps[i]["S"]));
assert_eq!(o.T1, point(&bps[i]["T1"]));
assert_eq!(o.T2, point(&bps[i]["T2"]));
assert_eq!(o.taux, scalar(&bps[i]["taux"]));
assert_eq!(o.mu, scalar(&bps[i]["mu"]));
assert_eq!(o.L, point_vector(&bps[i]["L"]));
assert_eq!(o.R, point_vector(&bps[i]["R"]));
assert_eq!(o.a, scalar(&bps[i]["a"]));
assert_eq!(o.b, scalar(&bps[i]["b"]));
assert_eq!(o.t, scalar(&bps[i]["t"]));
}
Bulletproof::Plus(p) => {
let bps = v.tx["rctsig_prunable"]["bpp"].as_array().unwrap();
assert_eq!(bulletproofs.len(), bps.len());
assert_eq!(p.A, point(&bps[i]["A"]));
assert_eq!(p.A1, point(&bps[i]["A1"]));
assert_eq!(p.B, point(&bps[i]["B"]));
assert_eq!(p.r1, scalar(&bps[i]["r1"]));
assert_eq!(p.s1, scalar(&bps[i]["s1"]));
assert_eq!(p.d1, scalar(&bps[i]["d1"]));
assert_eq!(p.L, point_vector(&bps[i]["L"]));
assert_eq!(p.R, point_vector(&bps[i]["R"]));
}
}
}
*/

// check clsags
let cls = v.tx["rctsig_prunable"]["CLSAGs"].as_array().unwrap();
for (i, cl) in clsags.iter().enumerate() {
assert_eq!(cl.D, point(&cls[i]["D"]));
assert_eq!(cl.c1, scalar(&cls[i]["c1"]));
assert_eq!(cl.s, scalar_vector(&cls[i]["s"]));
}

// check pseudo outs
assert_eq!(pseudo_outs, &point_vector(&v.tx["rctsig_prunable"]["pseudoOuts"]));
}
// TODO: Add
_ => panic!("non-null/CLSAG test vector"),
}
}
}

// check serialized hex
let mut buf = Vec::new();
tx.write(&mut buf).unwrap();
let serialized_tx = hex::encode(&buf);
assert_eq!(serialized_tx, v.hex);
}
}

#[test]
fn signature_hash() {
for v in tx_vectors() {
let tx = Transaction::read(&mut hex::decode(v.hex.clone()).unwrap().as_slice()).unwrap();
// check for signature hashes
if let Some(sig_hash) = tx.signature_hash() {
assert_eq!(sig_hash, hex::decode(v.signature_hash.clone()).unwrap().as_slice());
} else {
// make sure it is a miner tx.
assert!(matches!(tx.prefix().inputs[0], Input::Gen(_)));
}
}
}

#[test]
fn hash() {
for v in &tx_vectors() {
let tx = Transaction::read(&mut hex::decode(v.hex.clone()).unwrap().as_slice()).unwrap();
assert_eq!(tx.hash(), hex::decode(v.id.clone()).unwrap().as_slice());
}
}

#[test]
fn clsag() {
/*
// following keys belong to the wallet that created the CLSAG_TX, and to the
// CLSAG_TX itself and here for debug purposes in case this test unexpectedly fails some day.
let view_key = "9df81dd2e369004d3737850e4f0abaf2111720f270b174acf8e08547e41afb0b";
let spend_key = "25f7339ce03a0206129c0bdd78396f80bf28183ccd16084d4ab1cbaf74f0c204";
let tx_key = "650c8038e5c6f1c533cacc1713ac27ef3ec70d7feedde0c5b37556d915b4460c";
*/

#[derive(serde::Deserialize)]
struct TxData {
hex: String,
tx: Value,
}
#[derive(serde::Deserialize)]
struct OutData {
key: Value,
mask: Value,
}
let tx_data = serde_json::from_str::<TxData>(CLSAG_TX).unwrap();
let out_data = serde_json::from_str::<Vec<Vec<OutData>>>(RING_DATA).unwrap();
let tx =
Transaction::<NotPruned>::read(&mut hex::decode(tx_data.hex).unwrap().as_slice()).unwrap();

// gather rings
let mut rings = vec![];
for data in out_data {
let mut ring = vec![];
for out in &data {
ring.push([point(&out.key), point(&out.mask)]);
}
rings.push(ring)
}

// gather key images
let mut key_images = vec![];
let inputs = tx_data.tx["vin"].as_array().unwrap();
for input in inputs {
key_images.push(point(&input["key"]["k_image"]));
}

// gather pseudo_outs
let mut pseudo_outs = vec![];
let pouts = tx_data.tx["rctsig_prunable"]["pseudoOuts"].as_array().unwrap();
for po in pouts {
pseudo_outs.push(point(po));
}

// verify clsags
match tx {
Transaction::V2 { proofs: Some(ref proofs), .. } => match &proofs.prunable {
RctPrunable::Clsag { bulletproof: _, clsags, .. } => {
for (i, cls) in clsags.iter().enumerate() {
cls
.verify(&rings[i], &key_images[i], &pseudo_outs[i], &tx.signature_hash().unwrap())
.unwrap();
}
}
// TODO: Add
_ => panic!("non-CLSAG test vector"),
},
// TODO: Add
_ => panic!("non-CLSAG test vector"),
}
}
Loading
Loading