extern crate alloc;
use alloc::vec::Vec;
use serde::{Deserialize, Serialize};
use aethel_core::signing::purpose;
use aethel_core::signing::Identity;
use crate::error::VaultError;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Receipt {
pub auth_hash: [u8; 32],
pub amount: u128,
pub dst: [u8; 20],
pub ts: u64,
pub chain_id: u64,
}
impl Receipt {
fn to_bytes(&self) -> Result<Vec<u8>, VaultError> {
bincode::serialize(self).map_err(|_| VaultError::SerializationError)
}
pub fn sign(&self, identity: &Identity) -> Result<SignedReceipt, VaultError> {
let bytes = self.to_bytes()?;
let signature = identity
.sign_with_purpose(purpose::VAULT_SETTLEMENT_RECEIPT_V1, &bytes)
.map_err(VaultError::from)?;
Ok(SignedReceipt {
receipt: self.clone(),
signer_pk: identity.public_key(),
signature,
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SignedReceipt {
pub receipt: Receipt,
pub signer_pk: Vec<u8>,
pub signature: Vec<u8>,
}
impl SignedReceipt {
pub fn verify(&self) -> Result<bool, VaultError> {
let bytes = self.receipt.to_bytes()?;
aethel_core::signing::verify_with_purpose(
&self.signer_pk,
purpose::VAULT_SETTLEMENT_RECEIPT_V1,
&bytes,
&self.signature,
)
.map_err(VaultError::from)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_receipt() -> Receipt {
Receipt {
auth_hash: [0x11u8; 32],
amount: 500_000,
dst: [0x22u8; 20],
ts: 1_700_000_000,
chain_id: 8453,
}
}
#[test]
fn receipt_round_trips_and_verifies() {
let identity = Identity::generate(&[0x77u8; 32]).expect("generate");
let receipt = sample_receipt();
let signed = receipt.sign(&identity).expect("sign");
assert_eq!(signed.verify(), Ok(true));
}
#[test]
fn tampered_amount_fails_verification() {
let identity = Identity::generate(&[0x77u8; 32]).expect("generate");
let receipt = sample_receipt();
let mut signed = receipt.sign(&identity).expect("sign");
signed.receipt.amount += 1;
assert_eq!(signed.verify(), Ok(false));
}
#[test]
fn receipt_signed_by_another_identity_fails() {
let identity = Identity::generate(&[0x77u8; 32]).expect("generate");
let other = Identity::generate(&[0x88u8; 32]).expect("generate");
let receipt = sample_receipt();
let mut signed = receipt.sign(&identity).expect("sign");
signed.signer_pk = other.public_key();
assert_eq!(signed.verify(), Ok(false));
}
#[test]
fn receipt_signature_does_not_verify_under_the_spend_intent_purpose() {
let identity = Identity::generate(&[0x77u8; 32]).expect("generate");
let receipt = sample_receipt();
let signed = receipt.sign(&identity).expect("sign");
let bytes = bincode::serialize(&signed.receipt).unwrap();
assert_eq!(
aethel_core::signing::verify_with_purpose(
&signed.signer_pk,
purpose::VAULT_SPEND_INTENT_V1,
&bytes,
&signed.signature,
),
Ok(false),
"a settlement-receipt signature verified under the spend-intent purpose"
);
}
#[test]
fn a_spend_intent_signature_does_not_verify_as_a_receipt() {
let identity = Identity::generate(&[0x77u8; 32]).expect("generate");
let receipt = sample_receipt();
let bytes = bincode::serialize(&receipt).unwrap();
let spend_intent_sig = identity
.sign_with_purpose(purpose::VAULT_SPEND_INTENT_V1, &bytes)
.expect("sign");
let signed = SignedReceipt {
receipt,
signer_pk: identity.public_key(),
signature: spend_intent_sig,
};
assert_eq!(
signed.verify(),
Ok(false),
"a spend-intent signature verified as a settlement receipt"
);
}
#[test]
fn no_receipt_is_produced_unless_requested() {
let receipt = sample_receipt();
let _ = receipt;
}
}