#![expect(clippy::wildcard_enum_match_arm)]
use serde_json::Value;
use crate::{
ed25519::{Scalar, CompressedPoint},
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 compressed_point(hex: &Value) -> CompressedPoint {
CompressedPoint::from(<[u8; 32]>::try_from(hex::decode(hex.as_str().unwrap()).unwrap()).unwrap())
}
fn scalar(hex: &Value) -> Scalar {
Scalar::read(&mut hex::decode(hex.as_str().unwrap()).unwrap().as_slice()).unwrap()
}
fn compressed_point_vector(val: &Value) -> Vec<CompressedPoint> {
let mut v = vec![];
for hex in val.as_array().unwrap() {
v.push(compressed_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();
assert_eq!(tx.version(), v.tx["version"]);
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"]),
}
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, compressed_point(&key["k_image"]));
assert_eq!(key_offsets, key["key_offsets"].as_array().unwrap());
}
}
}
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 let Some(expected_view_tag) = output.view_tag {
assert_eq!(output.key, compressed_point(&outputs[i]["target"]["tagged_key"]["key"]));
let view_tag =
hex::decode(outputs[i]["target"]["tagged_key"]["view_tag"].as_str().unwrap()).unwrap();
assert_eq!(view_tag.len(), 1);
assert_eq!(expected_view_tag, view_tag[0]);
} else {
assert_eq!(output.key, compressed_point(&outputs[i]["target"]["key"]));
}
}
assert_eq!(tx.prefix().extra, v.tx["extra"].as_array().unwrap().as_slice());
match &tx {
Transaction::V1 { signatures, .. } => {
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 mut c = &tx_sig[start .. (start + 32)];
let mut s = &tx_sig[(start + 32) .. (start + 64)];
assert_eq!(sig.c, Scalar::read(&mut c).unwrap());
assert_eq!(sig.s, Scalar::read(&mut s).unwrap());
}
}
}
Transaction::V2 { proofs: None, .. } => assert_eq!(v.tx["rct_signatures"]["type"], 0),
Transaction::V2 { proofs: Some(proofs), .. } => {
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, compressed_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());
}
match &proofs.prunable {
RctPrunable::Clsag { bulletproof: _, clsags, pseudo_outs } => {
let cls = v.tx["rctsig_prunable"]["CLSAGs"].as_array().unwrap();
for (i, cl) in clsags.iter().enumerate() {
assert_eq!(cl.D, compressed_point(&cls[i]["D"]));
assert_eq!(cl.c1, scalar(&cls[i]["c1"]));
assert_eq!(cl.s, scalar_vector(&cls[i]["s"]));
}
assert_eq!(
pseudo_outs,
&compressed_point_vector(&v.tx["rctsig_prunable"]["pseudoOuts"])
);
}
_ => panic!("non-null/CLSAG test vector"),
}
}
}
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();
if let Some(sig_hash) = tx.signature_hash() {
assert_eq!(sig_hash, hex::decode(v.signature_hash.clone()).unwrap().as_slice());
} else {
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() {
#[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();
let mut rings = vec![];
for data in out_data {
let mut ring = vec![];
for out in &data {
ring.push([compressed_point(&out.key), compressed_point(&out.mask)]);
}
rings.push(ring);
}
let mut key_images = vec![];
let inputs = tx_data.tx["vin"].as_array().unwrap();
for input in inputs {
key_images.push(compressed_point(&input["key"]["k_image"]));
}
let mut pseudo_outs = vec![];
let pouts = tx_data.tx["rctsig_prunable"]["pseudoOuts"].as_array().unwrap();
for po in pouts {
pseudo_outs.push(compressed_point(po));
}
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].clone(),
&key_images[i],
&pseudo_outs[i],
&tx.signature_hash().unwrap(),
)
.unwrap();
}
}
_ => panic!("non-CLSAG test vector"),
},
_ => panic!("non-CLSAG test vector"),
}
}
#[test]
fn pruned_with_prunable() {
for tx in tx_vectors() {
let tx_bytes = hex::decode(tx.hex.clone()).unwrap();
let tx = Transaction::read(&mut tx_bytes.as_slice()).unwrap();
let (pruned, prunable) = tx.pruned_with_prunable();
assert_eq!([pruned.serialize(), prunable].concat(), tx_bytes);
}
}