use crate::error::{ReceiptError, Result};
use crate::receipt::Receipt;
use ed25519_dalek::VerifyingKey;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReceiptChain {
receipts: Vec<Receipt>,
}
impl ReceiptChain {
#[must_use]
pub fn new() -> Self {
Self {
receipts: Vec::new(),
}
}
pub fn from_genesis(genesis: Receipt) -> Result<Self> {
if genesis.previous_receipt_hash.is_some() {
return Err(ReceiptError::InvalidReceipt(
"Genesis receipt must not have a previous hash".to_string(),
));
}
Ok(Self {
receipts: vec![genesis],
})
}
pub fn append(&mut self, receipt: Receipt) -> Result<()> {
if self.receipts.is_empty() {
if receipt.previous_receipt_hash.is_some() {
return Err(ReceiptError::InvalidChain(
"First receipt must be a genesis receipt (no previous hash)".to_string(),
));
}
} else {
let last_receipt = self
.receipts
.last()
.ok_or_else(|| ReceiptError::InvalidChain("Chain is empty".to_string()))?;
let last_hash = last_receipt.hash()?;
match &receipt.previous_receipt_hash {
Some(prev_hash) if prev_hash == &last_hash => {}
Some(prev_hash) => {
return Err(ReceiptError::HashMismatch {
expected: last_hash,
actual: prev_hash.clone(),
});
}
None => {
return Err(ReceiptError::InvalidChain(
"Non-genesis receipt must have a previous hash".to_string(),
));
}
}
}
self.receipts.push(receipt);
Ok(())
}
pub fn verify(&self, verifying_key: &VerifyingKey) -> Result<()> {
if self.receipts.is_empty() {
return Ok(());
}
let genesis = &self.receipts[0];
if genesis.previous_receipt_hash.is_some() {
return Err(ReceiptError::InvalidChain(
"Genesis receipt must not have a previous hash".to_string(),
));
}
genesis.verify(verifying_key)?;
for i in 1..self.receipts.len() {
let current = &self.receipts[i];
let previous = &self.receipts[i - 1];
current.verify(verifying_key)?;
let expected_hash = previous.hash()?;
match ¤t.previous_receipt_hash {
Some(prev_hash) if prev_hash == &expected_hash => {}
Some(prev_hash) => {
return Err(ReceiptError::HashMismatch {
expected: expected_hash,
actual: prev_hash.clone(),
});
}
None => {
return Err(ReceiptError::InvalidChain(format!(
"Receipt at index {} is missing previous hash",
i
)));
}
}
}
Ok(())
}
#[must_use]
pub fn len(&self) -> usize {
self.receipts.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.receipts.is_empty()
}
#[must_use]
pub fn receipts(&self) -> &[Receipt] {
&self.receipts
}
#[must_use]
pub fn last(&self) -> Option<&Receipt> {
self.receipts.last()
}
#[must_use]
pub fn genesis(&self) -> Option<&Receipt> {
self.receipts.first()
}
}
pub fn create_chained_receipt(
previous: &Receipt, operation_id: String, input_hashes: Vec<String>,
output_hashes: Vec<String>, signing_key: &ed25519_dalek::SigningKey,
) -> Result<Receipt> {
Receipt::new(operation_id, input_hashes, output_hashes, None)
.chain(previous)?
.sign(signing_key)
.map_err(|e| ReceiptError::InvalidReceipt(format!("Failed to sign chained receipt: {e}")))
}
impl Default for ReceiptChain {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::receipt::generate_keypair;
#[test]
fn test_empty_chain() {
let chain = ReceiptChain::new();
assert!(chain.is_empty());
assert_eq!(chain.len(), 0);
}
#[test]
fn test_genesis_chain() {
let (signing_key, _) = generate_keypair();
let genesis = Receipt::new("genesis".to_string(), vec![], vec![], None)
.sign(&signing_key)
.expect("signing failed");
let chain = ReceiptChain::from_genesis(genesis).expect("genesis creation failed");
assert!(!chain.is_empty());
assert_eq!(chain.len(), 1);
}
#[test]
fn test_genesis_with_previous_hash_fails() {
let (signing_key, _) = generate_keypair();
let invalid_genesis = Receipt::new(
"genesis".to_string(),
vec![],
vec![],
Some("previous".to_string()),
)
.sign(&signing_key)
.expect("signing failed");
let result = ReceiptChain::from_genesis(invalid_genesis);
assert!(result.is_err());
}
#[test]
fn test_chain_append() {
let (signing_key, verifying_key) = generate_keypair();
let genesis = Receipt::new("op1".to_string(), vec![], vec![], None)
.sign(&signing_key)
.expect("signing failed");
let mut chain =
ReceiptChain::from_genesis(genesis.clone()).expect("genesis creation failed");
let receipt2 = Receipt::new("op2".to_string(), vec![], vec![], None)
.chain(&genesis)
.expect("chaining failed")
.sign(&signing_key)
.expect("signing failed");
chain.append(receipt2).expect("append failed");
assert_eq!(chain.len(), 2);
assert!(chain.verify(&verifying_key).is_ok());
}
#[test]
fn test_chain_verify() {
let (signing_key, verifying_key) = generate_keypair();
let genesis = Receipt::new("op1".to_string(), vec![], vec![], None)
.sign(&signing_key)
.expect("signing failed");
let mut chain =
ReceiptChain::from_genesis(genesis.clone()).expect("genesis creation failed");
let receipt2 = Receipt::new("op2".to_string(), vec![], vec![], None)
.chain(&genesis)
.expect("chaining failed")
.sign(&signing_key)
.expect("signing failed");
let receipt3 = Receipt::new("op3".to_string(), vec![], vec![], None)
.chain(&receipt2)
.expect("chaining failed")
.sign(&signing_key)
.expect("signing failed");
chain.append(receipt2).expect("append failed");
chain.append(receipt3).expect("append failed");
assert_eq!(chain.len(), 3);
assert!(chain.verify(&verifying_key).is_ok());
}
#[test]
fn test_chain_verify_fails_with_wrong_key() {
let (signing_key, _) = generate_keypair();
let (_, wrong_key) = generate_keypair();
let genesis = Receipt::new("op1".to_string(), vec![], vec![], None)
.sign(&signing_key)
.expect("signing failed");
let chain = ReceiptChain::from_genesis(genesis).expect("genesis creation failed");
assert!(chain.verify(&wrong_key).is_err());
}
#[test]
fn test_chain_append_with_wrong_hash_fails() {
let (signing_key, _) = generate_keypair();
let genesis = Receipt::new("op1".to_string(), vec![], vec![], None)
.sign(&signing_key)
.expect("signing failed");
let mut chain = ReceiptChain::from_genesis(genesis).expect("genesis creation failed");
let wrong_receipt = Receipt::new(
"op2".to_string(),
vec![],
vec![],
Some("wrong_hash".to_string()),
)
.sign(&signing_key)
.expect("signing failed");
assert!(chain.append(wrong_receipt).is_err());
}
#[test]
fn test_chain_accessors() {
let (signing_key, _) = generate_keypair();
let genesis = Receipt::new("op1".to_string(), vec![], vec![], None)
.sign(&signing_key)
.expect("signing failed");
let chain = ReceiptChain::from_genesis(genesis.clone()).expect("genesis creation failed");
assert_eq!(chain.genesis().unwrap().operation_id, "op1");
assert_eq!(chain.last().unwrap().operation_id, "op1");
assert_eq!(chain.receipts().len(), 1);
}
}