use crate::types::ProtocolError;
use qudag_crypto::{ml_dsa::MlDsa65, signature::{Signature, SignatureError}};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use thiserror::Error;
use tracing::{debug, error, info};
#[derive(Debug, Error)]
pub enum TransactionError {
#[error("Invalid transaction format")]
InvalidFormat,
#[error("Insufficient balance")]
InsufficientBalance,
#[error("Invalid signature")]
InvalidSignature,
#[error("Double spending detected")]
DoubleSpending,
#[error("Transaction not found")]
NotFound,
#[error("Validation failed: {0}")]
ValidationFailed(String),
#[error("Crypto error: {0}")]
CryptoError(String),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TransactionInput {
pub prev_tx_hash: [u8; 32],
pub prev_output_index: u32,
pub signature_script: Vec<u8>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TransactionOutput {
pub amount: u64,
pub recipient: [u8; 32],
pub locking_script: Vec<u8>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Transaction {
pub version: u32,
pub inputs: Vec<TransactionInput>,
pub outputs: Vec<TransactionOutput>,
pub timestamp: u64,
pub nonce: u64,
pub fee: u64,
pub signature: Vec<u8>,
}
impl Transaction {
pub fn new(
inputs: Vec<TransactionInput>,
outputs: Vec<TransactionOutput>,
fee: u64,
) -> Self {
Self {
version: 1,
inputs,
outputs,
timestamp: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs(),
nonce: 0,
fee,
signature: Vec::new(),
}
}
pub fn hash(&self) -> [u8; 32] {
use sha3::{Digest, Sha3_256};
let mut hasher = Sha3_256::new();
hasher.update(self.version.to_le_bytes());
for input in &self.inputs {
hasher.update(input.prev_tx_hash);
hasher.update(input.prev_output_index.to_le_bytes());
hasher.update(&input.signature_script);
}
for output in &self.outputs {
hasher.update(output.amount.to_le_bytes());
hasher.update(output.recipient);
hasher.update(&output.locking_script);
}
hasher.update(self.timestamp.to_le_bytes());
hasher.update(self.nonce.to_le_bytes());
hasher.update(self.fee.to_le_bytes());
hasher.finalize().into()
}
pub fn sign(&mut self, private_key: &[u8]) -> Result<(), TransactionError> {
let hash = self.hash();
let ml_dsa = MlDsa65::new()
.map_err(|e| TransactionError::CryptoError(e.to_string()))?;
self.signature = ml_dsa.sign(private_key, &hash)
.map_err(|e| TransactionError::CryptoError(e.to_string()))?;
Ok(())
}
pub fn verify_signature(&self, public_key: &[u8]) -> Result<bool, TransactionError> {
if self.signature.is_empty() {
return Ok(false);
}
let hash = self.hash();
let ml_dsa = MlDsa65::new()
.map_err(|e| TransactionError::CryptoError(e.to_string()))?;
ml_dsa.verify(public_key, &hash, &self.signature)
.map_err(|e| TransactionError::CryptoError(e.to_string()))
}
pub fn total_input_amount(&self, utxo_set: &UTXOSet) -> Result<u64, TransactionError> {
let mut total = 0u64;
for input in &self.inputs {
if let Some(utxo) = utxo_set.get_utxo(&input.prev_tx_hash, input.prev_output_index) {
total = total.checked_add(utxo.amount)
.ok_or(TransactionError::ValidationFailed("Integer overflow".to_string()))?;
} else {
return Err(TransactionError::ValidationFailed("UTXO not found".to_string()));
}
}
Ok(total)
}
pub fn total_output_amount(&self) -> Result<u64, TransactionError> {
let mut total = 0u64;
for output in &self.outputs {
total = total.checked_add(output.amount)
.ok_or(TransactionError::ValidationFailed("Integer overflow".to_string()))?;
}
Ok(total)
}
pub fn validate(&self, utxo_set: &UTXOSet, public_key: &[u8]) -> Result<(), TransactionError> {
if self.inputs.is_empty() {
return Err(TransactionError::ValidationFailed("No inputs".to_string()));
}
if self.outputs.is_empty() {
return Err(TransactionError::ValidationFailed("No outputs".to_string()));
}
if !self.verify_signature(public_key)? {
return Err(TransactionError::InvalidSignature);
}
let input_amount = self.total_input_amount(utxo_set)?;
let output_amount = self.total_output_amount()?;
if input_amount < output_amount + self.fee {
return Err(TransactionError::InsufficientBalance);
}
for input in &self.inputs {
if utxo_set.is_spent(&input.prev_tx_hash, input.prev_output_index) {
return Err(TransactionError::DoubleSpending);
}
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct UTXO {
pub tx_hash: [u8; 32],
pub output_index: u32,
pub amount: u64,
pub recipient: [u8; 32],
pub locking_script: Vec<u8>,
pub block_height: u64,
}
#[derive(Debug, Clone)]
pub struct UTXOSet {
utxos: HashMap<([u8; 32], u32), UTXO>,
spent: HashMap<([u8; 32], u32), bool>,
}
impl UTXOSet {
pub fn new() -> Self {
Self {
utxos: HashMap::new(),
spent: HashMap::new(),
}
}
pub fn add_utxo(&mut self, utxo: UTXO) {
let key = (utxo.tx_hash, utxo.output_index);
self.utxos.insert(key, utxo);
self.spent.remove(&key); }
pub fn get_utxo(&self, tx_hash: &[u8; 32], output_index: u32) -> Option<&UTXO> {
let key = (*tx_hash, output_index);
if self.spent.get(&key).copied().unwrap_or(false) {
None
} else {
self.utxos.get(&key)
}
}
pub fn spend_utxo(&mut self, tx_hash: &[u8; 32], output_index: u32) {
let key = (*tx_hash, output_index);
self.spent.insert(key, true);
}
pub fn is_spent(&self, tx_hash: &[u8; 32], output_index: u32) -> bool {
let key = (*tx_hash, output_index);
self.spent.get(&key).copied().unwrap_or(false)
}
pub fn get_balance(&self, address: &[u8; 32]) -> u64 {
self.utxos
.values()
.filter(|utxo| !self.is_spent(&utxo.tx_hash, utxo.output_index))
.filter(|utxo| utxo.recipient == *address)
.map(|utxo| utxo.amount)
.sum()
}
pub fn get_utxos_for_address(&self, address: &[u8; 32]) -> Vec<&UTXO> {
self.utxos
.values()
.filter(|utxo| !self.is_spent(&utxo.tx_hash, utxo.output_index))
.filter(|utxo| utxo.recipient == *address)
.collect()
}
}
impl Default for UTXOSet {
fn default() -> Self {
Self::new()
}
}
pub struct TransactionProcessor {
utxo_set: UTXOSet,
pending_txs: HashMap<[u8; 32], Transaction>,
tx_pool: Vec<Transaction>,
}
impl TransactionProcessor {
pub fn new() -> Self {
Self {
utxo_set: UTXOSet::new(),
pending_txs: HashMap::new(),
tx_pool: Vec::new(),
}
}
pub fn add_transaction(&mut self, tx: Transaction, public_key: &[u8]) -> Result<(), TransactionError> {
tx.validate(&self.utxo_set, public_key)?;
let tx_hash = tx.hash();
self.tx_pool.push(tx.clone());
self.pending_txs.insert(tx_hash, tx);
info!("Added transaction to pool: {:?}", hex::encode(tx_hash));
Ok(())
}
pub fn process_transaction(&mut self, tx_hash: &[u8; 32]) -> Result<(), TransactionError> {
let tx = self.pending_txs.remove(tx_hash)
.ok_or(TransactionError::NotFound)?;
for input in &tx.inputs {
self.utxo_set.spend_utxo(&input.prev_tx_hash, input.prev_output_index);
}
for (index, output) in tx.outputs.iter().enumerate() {
let utxo = UTXO {
tx_hash: *tx_hash,
output_index: index as u32,
amount: output.amount,
recipient: output.recipient,
locking_script: output.locking_script.clone(),
block_height: 0, };
self.utxo_set.add_utxo(utxo);
}
self.tx_pool.retain(|pool_tx| pool_tx.hash() != *tx_hash);
info!("Processed transaction: {:?}", hex::encode(tx_hash));
Ok(())
}
pub fn get_pending_transactions(&self) -> Vec<&Transaction> {
self.tx_pool.iter().collect()
}
pub fn get_utxo_set(&self) -> &UTXOSet {
&self.utxo_set
}
pub fn get_balance(&self, address: &[u8; 32]) -> u64 {
self.utxo_set.get_balance(address)
}
}
impl Default for TransactionProcessor {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use qudag_crypto::ml_dsa::MlDsa65;
#[test]
fn test_transaction_creation() {
let input = TransactionInput {
prev_tx_hash: [1; 32],
prev_output_index: 0,
signature_script: vec![],
};
let output = TransactionOutput {
amount: 100,
recipient: [2; 32],
locking_script: vec![],
};
let tx = Transaction::new(vec![input], vec![output], 1);
assert_eq!(tx.version, 1);
assert_eq!(tx.inputs.len(), 1);
assert_eq!(tx.outputs.len(), 1);
assert_eq!(tx.fee, 1);
}
#[test]
fn test_transaction_hash() {
let input = TransactionInput {
prev_tx_hash: [1; 32],
prev_output_index: 0,
signature_script: vec![],
};
let output = TransactionOutput {
amount: 100,
recipient: [2; 32],
locking_script: vec![],
};
let tx1 = Transaction::new(vec![input.clone()], vec![output.clone()], 1);
let tx2 = Transaction::new(vec![input], vec![output], 1);
let hash1 = tx1.hash();
let hash2 = tx1.hash();
assert_eq!(hash1, hash2);
}
#[tokio::test]
async fn test_transaction_signing() {
let ml_dsa = MlDsa65::new().unwrap();
let (pk, sk) = ml_dsa.keygen().unwrap();
let input = TransactionInput {
prev_tx_hash: [1; 32],
prev_output_index: 0,
signature_script: vec![],
};
let output = TransactionOutput {
amount: 100,
recipient: [2; 32],
locking_script: vec![],
};
let mut tx = Transaction::new(vec![input], vec![output], 1);
tx.sign(sk.as_bytes()).unwrap();
assert!(!tx.signature.is_empty());
assert!(tx.verify_signature(pk.as_bytes()).unwrap());
}
#[test]
fn test_utxo_set() {
let mut utxo_set = UTXOSet::new();
let utxo = UTXO {
tx_hash: [1; 32],
output_index: 0,
amount: 100,
recipient: [2; 32],
locking_script: vec![],
block_height: 1,
};
utxo_set.add_utxo(utxo.clone());
assert_eq!(utxo_set.get_balance(&[2; 32]), 100);
utxo_set.spend_utxo(&[1; 32], 0);
assert_eq!(utxo_set.get_balance(&[2; 32]), 0);
assert!(utxo_set.is_spent(&[1; 32], 0));
}
#[tokio::test]
async fn test_transaction_processor() {
let mut processor = TransactionProcessor::new();
let ml_dsa = MlDsa65::new().unwrap();
let (pk, sk) = ml_dsa.keygen().unwrap();
let genesis_utxo = UTXO {
tx_hash: [0; 32],
output_index: 0,
amount: 1000,
recipient: [1; 32],
locking_script: vec![],
block_height: 0,
};
processor.utxo_set.add_utxo(genesis_utxo);
let input = TransactionInput {
prev_tx_hash: [0; 32],
prev_output_index: 0,
signature_script: vec![],
};
let output = TransactionOutput {
amount: 900,
recipient: [2; 32],
locking_script: vec![],
};
let mut tx = Transaction::new(vec![input], vec![output], 100);
tx.sign(sk.as_bytes()).unwrap();
processor.add_transaction(tx.clone(), pk.as_bytes()).unwrap();
let tx_hash = tx.hash();
processor.process_transaction(&tx_hash).unwrap();
assert_eq!(processor.get_balance(&[1; 32]), 0); assert_eq!(processor.get_balance(&[2; 32]), 900); }
}