ai-agent-bitcoin-escrow 0.1.0

A Rust library for AI agents to create, manage, and execute Bitcoin escrow contracts using multisig
Documentation
//! Audit logging for escrow operations.
//!
//! This module provides comprehensive audit logging for all escrow operations,
//! enabling transparency and accountability for AI agent economic activities.

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs::{File, OpenOptions};
use std::io::{BufWriter, Write};
use std::path::PathBuf;
use std::sync::{Arc, Mutex};

use crate::error::{EscrowError, Result};
use crate::types::{EscrowId, EscrowStatus};

/// Types of audit events.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AuditEventType {
    /// Escrow contract created.
    ContractCreated,
    /// Participant joined.
    ParticipantJoined,
    /// Funds deposited.
    FundsDeposited,
    /// Condition added.
    ConditionAdded,
    /// Condition evaluated.
    ConditionEvaluated,
    /// Signature added.
    SignatureAdded,
    /// Release initiated.
    ReleaseInitiated,
    /// Funds released.
    FundsReleased,
    /// Contract cancelled.
    ContractCancelled,
    /// Dispute started.
    DisputeStarted,
    /// Dispute resolved.
    DisputeResolved,
    /// Contract expired.
    ContractExpired,
    /// AI agent decision.
    AIDecision,
    /// Oracle query.
    OracleQuery,
    /// Error occurred.
    Error,
}

/// An audit log entry.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditEntry {
    /// Unique ID for this entry.
    pub id: String,
    /// Timestamp of the event.
    pub timestamp: DateTime<Utc>,
    /// Type of event.
    pub event_type: AuditEventType,
    /// Related escrow contract ID.
    pub escrow_id: EscrowId,
    /// Actor who initiated the event.
    pub actor: String,
    /// Description of the event.
    pub description: String,
    /// Additional metadata.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub metadata: Option<HashMap<String, serde_json::Value>>,
    /// Hash of previous entry (for integrity chain).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub prev_hash: Option<String>,
    /// Hash of this entry.
    pub hash: String,
}

impl AuditEntry {
    /// Create a new audit entry.
    pub fn new(
        event_type: AuditEventType,
        escrow_id: EscrowId,
        actor: String,
        description: String,
        metadata: Option<HashMap<String, serde_json::Value>>,
        prev_hash: Option<String>,
    ) -> Self {
        let id = uuid::Uuid::new_v4().to_string();
        let timestamp = Utc::now();
        
        // Calculate hash for integrity
        let hash = Self::calculate_hash(&id, &timestamp, &event_type, &escrow_id, &actor, &description, &prev_hash);
        
        Self {
            id,
            timestamp,
            event_type,
            escrow_id,
            actor,
            description,
            metadata,
            prev_hash,
            hash,
        }
    }
    
    fn calculate_hash(
        id: &str,
        timestamp: &DateTime<Utc>,
        event_type: &AuditEventType,
        escrow_id: &EscrowId,
        actor: &str,
        description: &str,
        prev_hash: &Option<String>,
    ) -> String {
        use std::collections::hash_map::DefaultHasher;
        use std::hash::{Hash, Hasher};
        
        let mut hasher = DefaultHasher::new();
        id.hash(&mut hasher);
        timestamp.to_rfc3339().hash(&mut hasher);
        format!("{:?}", event_type).hash(&mut hasher);
        escrow_id.to_string().hash(&mut hasher);
        actor.hash(&mut hasher);
        description.hash(&mut hasher);
        prev_hash.hash(&mut hasher);
        
        format!("{:016x}", hasher.finish())
    }
}

/// Audit logger for escrow operations.
#[derive(Debug)]
pub struct AuditLogger {
    /// Path to the log file.
    path: PathBuf,
    /// Buffer for writing.
    writer: Arc<Mutex<BufWriter<File>>>,
    /// Last entry hash for chain integrity.
    last_hash: Arc<Mutex<Option<String>>>,
    /// In-memory entries for quick access.
    entries: Arc<Mutex<Vec<AuditEntry>>>,
}

impl AuditLogger {
    /// Create a new audit logger.
    pub fn new<P: Into<PathBuf>>(path: P) -> Result<Self> {
        let path = path.into();
        
        // Ensure parent directory exists
        if let Some(parent) = path.parent() {
            std::fs::create_dir_all(parent)?;
        }
        
        let file = OpenOptions::new()
            .create(true)
            .append(true)
            .open(&path)?;
        
        let writer = Arc::new(Mutex::new(BufWriter::new(file)));
        
        Ok(Self {
            path,
            writer,
            last_hash: Arc::new(Mutex::new(None)),
            entries: Arc::new(Mutex::new(Vec::new())),
        })
    }
    
    /// Create an in-memory audit logger (for testing).
    pub fn in_memory() -> Self {
        Self {
            path: PathBuf::from(":memory:"),
            writer: Arc::new(Mutex::new(BufWriter::new(
                OpenOptions::new()
                    .write(true)
                    .open("/dev/null")
                    .unwrap()
            ))),
            last_hash: Arc::new(Mutex::new(None)),
            entries: Arc::new(Mutex::new(Vec::new())),
        }
    }
    
    /// Log an audit event.
    pub fn log(
        &self,
        event_type: AuditEventType,
        escrow_id: EscrowId,
        actor: String,
        description: String,
        metadata: Option<HashMap<String, serde_json::Value>>,
    ) -> Result<AuditEntry> {
        let prev_hash = self.last_hash.lock().unwrap().clone();
        
        let entry = AuditEntry::new(
            event_type,
            escrow_id,
            actor,
            description,
            metadata,
            prev_hash,
        );
        
        // Update last hash
        *self.last_hash.lock().unwrap() = Some(entry.hash.clone());
        
        // Store in memory
        self.entries.lock().unwrap().push(entry.clone());
        
        // Write to file
        self.write_entry(&entry)?;
        
        Ok(entry)
    }
    
    fn write_entry(&self, entry: &AuditEntry) -> Result<()> {
        let json = serde_json::to_string(entry)?;
        let mut writer = self.writer.lock().unwrap();
        writeln!(writer, "{}", json)?;
        writer.flush()?;
        Ok(())
    }
    
    /// Get all entries for a specific escrow.
    pub fn get_entries(&self, escrow_id: &EscrowId) -> Vec<AuditEntry> {
        self.entries
            .lock()
            .unwrap()
            .iter()
            .filter(|e| &e.escrow_id == escrow_id)
            .cloned()
            .collect()
    }
    
    /// Get all entries.
    pub fn get_all_entries(&self) -> Vec<AuditEntry> {
        self.entries.lock().unwrap().clone()
    }
    
    /// Verify the integrity of the audit log chain.
    pub fn verify_integrity(&self) -> Result<bool> {
        let entries = self.entries.lock().unwrap();
        
        for (i, entry) in entries.iter().enumerate() {
            // Verify hash
            let expected_hash = AuditEntry::calculate_hash(
                &entry.id,
                &entry.timestamp,
                &entry.event_type,
                &entry.escrow_id,
                &entry.actor,
                &entry.description,
                &entry.prev_hash,
            );
            
            if entry.hash != expected_hash {
                return Ok(false);
            }
            
            // Verify chain
            if i > 0 {
                let prev_entry = &entries[i - 1];
                if entry.prev_hash.as_ref() != Some(&prev_entry.hash) {
                    return Ok(false);
                }
            }
        }
        
        Ok(true)
    }
    
    /// Export audit log to JSON.
    pub fn export_json(&self) -> Result<String> {
        let entries = self.entries.lock().unwrap();
        Ok(serde_json::to_string_pretty(&*entries)?)
    }
    
    /// Export audit log to a file.
    pub fn export_to_file<P: Into<PathBuf>>(&self, path: P) -> Result<()> {
        let path = path.into();
        let json = self.export_json()?;
        std::fs::write(&path, json)?;
        Ok(())
    }
    
    /// Get the log file path.
    pub fn path(&self) -> &PathBuf {
        &self.path
    }
}

/// Convenience function to create a default audit logger.
pub fn default_audit_logger() -> Result<AuditLogger> {
    AuditLogger::new("/tmp/escrow_audit.log")
}

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_audit_entry_creation() {
        let entry = AuditEntry::new(
            AuditEventType::ContractCreated,
            EscrowId::new(),
            "agent-1".to_string(),
            "Contract created".to_string(),
            None,
            None,
        );
        
        assert!(!entry.id.is_empty());
        assert!(!entry.hash.is_empty());
    }
    
    #[test]
    fn test_audit_logger() {
        let logger = AuditLogger::in_memory();
        let escrow_id = EscrowId::new();
        
        logger.log(
            AuditEventType::ContractCreated,
            escrow_id.clone(),
            "agent-1".to_string(),
            "Contract created".to_string(),
            None,
        ).unwrap();
        
        let entries = logger.get_entries(&escrow_id);
        assert_eq!(entries.len(), 1);
    }
    
    #[test]
    fn test_integrity_chain() {
        let logger = AuditLogger::in_memory();
        let escrow_id = EscrowId::new();
        
        logger.log(
            AuditEventType::ContractCreated,
            escrow_id.clone(),
            "agent-1".to_string(),
            "Contract created".to_string(),
            None,
        ).unwrap();
        
        logger.log(
            AuditEventType::FundsDeposited,
            escrow_id.clone(),
            "agent-1".to_string(),
            "Funds deposited".to_string(),
            None,
        ).unwrap();
        
        assert!(logger.verify_integrity().unwrap());
    }
}