use crate::error::{ReceiptError, Result};
use chrono::{DateTime, Utc};
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Receipt {
pub operation_id: String,
pub timestamp: DateTime<Utc>,
pub input_hashes: Vec<String>,
pub output_hashes: Vec<String>,
pub signature: String,
pub previous_receipt_hash: Option<String>,
}
impl Receipt {
#[must_use]
pub fn new(
operation_id: String, input_hashes: Vec<String>, output_hashes: Vec<String>,
previous_receipt_hash: Option<String>,
) -> Self {
Self {
operation_id,
timestamp: Utc::now(),
input_hashes,
output_hashes,
signature: String::new(),
previous_receipt_hash,
}
}
pub fn sign(mut self, signing_key: &SigningKey) -> Result<Self> {
let message = self.signing_message()?;
let signature = signing_key.sign(&message);
self.signature = hex::encode(signature.to_bytes());
Ok(self)
}
pub fn verify(&self, verifying_key: &VerifyingKey) -> Result<()> {
let message = self.signing_message()?;
let signature_bytes =
hex::decode(&self.signature).map_err(|_| ReceiptError::InvalidSignature)?;
let signature =
Signature::from_slice(&signature_bytes).map_err(|_| ReceiptError::InvalidSignature)?;
verifying_key
.verify(&message, &signature)
.map_err(|_| ReceiptError::InvalidSignature)
}
pub fn hash(&self) -> Result<String> {
let json = serde_json::to_string(self)?;
let mut hasher = Sha256::new();
hasher.update(json.as_bytes());
Ok(hex::encode(hasher.finalize()))
}
pub fn chain(mut self, previous_receipt: &Receipt) -> Result<Self> {
self.previous_receipt_hash = Some(previous_receipt.hash()?);
Ok(self)
}
fn signing_message(&self) -> Result<Vec<u8>> {
let mut data = Self {
signature: String::new(),
..self.clone()
};
data.signature = String::new();
let json = serde_json::to_string(&data)?;
Ok(json.into_bytes())
}
}
#[must_use]
pub fn generate_keypair() -> (SigningKey, VerifyingKey) {
let mut rng = rand::thread_rng();
let signing_key = SigningKey::generate(&mut rng);
let verifying_key = signing_key.verifying_key();
(signing_key, verifying_key)
}
#[must_use]
pub fn hash_data(data: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(data);
hex::encode(hasher.finalize())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_receipt_creation() {
let receipt = Receipt::new(
"test-op".to_string(),
vec!["input1".to_string()],
vec!["output1".to_string()],
None,
);
assert_eq!(receipt.operation_id, "test-op");
assert_eq!(receipt.input_hashes.len(), 1);
assert_eq!(receipt.output_hashes.len(), 1);
assert!(receipt.previous_receipt_hash.is_none());
}
#[test]
fn test_receipt_signing_and_verification() {
let (signing_key, verifying_key) = generate_keypair();
let receipt = Receipt::new(
"test-op".to_string(),
vec!["input1".to_string()],
vec!["output1".to_string()],
None,
);
let signed_receipt = receipt.sign(&signing_key).expect("signing failed");
assert!(!signed_receipt.signature.is_empty());
assert!(signed_receipt.verify(&verifying_key).is_ok());
}
#[test]
fn test_receipt_verification_fails_with_wrong_key() {
let (signing_key, _) = generate_keypair();
let (_, wrong_key) = generate_keypair();
let receipt = Receipt::new(
"test-op".to_string(),
vec!["input1".to_string()],
vec!["output1".to_string()],
None,
);
let signed_receipt = receipt.sign(&signing_key).expect("signing failed");
assert!(signed_receipt.verify(&wrong_key).is_err());
}
#[test]
fn test_receipt_hash() {
let receipt = Receipt::new(
"test-op".to_string(),
vec!["input1".to_string()],
vec!["output1".to_string()],
None,
);
let hash1 = receipt.hash().expect("hashing failed");
let hash2 = receipt.hash().expect("hashing failed");
assert_eq!(hash1, hash2);
assert_eq!(hash1.len(), 64); }
#[test]
fn test_receipt_chain() {
let (signing_key, _) = generate_keypair();
let receipt1 = Receipt::new(
"op1".to_string(),
vec!["input1".to_string()],
vec!["output1".to_string()],
None,
)
.sign(&signing_key)
.expect("signing failed");
let receipt2 = Receipt::new(
"op2".to_string(),
vec!["input2".to_string()],
vec!["output2".to_string()],
None,
)
.chain(&receipt1)
.expect("chaining failed")
.sign(&signing_key)
.expect("signing failed");
assert!(receipt2.previous_receipt_hash.is_some());
assert_eq!(
receipt2.previous_receipt_hash.as_ref().unwrap(),
&receipt1.hash().expect("hashing failed")
);
}
#[test]
fn test_hash_data() {
let data = b"test data";
let hash1 = hash_data(data);
let hash2 = hash_data(data);
assert_eq!(hash1, hash2);
assert_eq!(hash1.len(), 64);
}
}