Add CRC32C checksums to WAL record format (v2), implement crash recovery with automatic truncation of corrupt records, add feature-gated group commit buffer for batched fsync under concurrent load, and implement log rotation via segment files with global offset addressing. Key changes: - Record format v2: [len:u32][crc32c:u32][blake3:32][payload:N] - recover_file() scans and truncates corrupt tail records - GroupCommitBuffer batches fsync via MPSC channel (tokio feature gate) - SegmentManager with binary search resolution and cursor-based cleanup - Journal::read() auto-refreshes segments on miss for writer/reader split - Split recovery.rs and key_codec.rs into directory modules for 500-line max Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
163 lines
6.2 KiB
Rust
163 lines
6.2 KiB
Rust
//! Signature verification tests.
|
|
//!
|
|
//! Tests for Ed25519 signature verification, rejection of invalid
|
|
//! signatures, and multi-signature validation.
|
|
|
|
use super::*;
|
|
use crate::error::IngestError;
|
|
|
|
signatures: vec![SignatureEntry {
|
|
agent_id: [1u8; 32], // Invalid: not a valid Ed25519 public key
|
|
signature: [2u8; 64], // Invalid: not a valid signature
|
|
timestamp: 1000,
|
|
}],
|
|
confidence: 0.95,
|
|
timestamp: 1000,
|
|
vector: None,
|
|
};
|
|
|
|
let mut journal = Journal::open(&wal_dir).expect("Failed to open journal");
|
|
let store = HybridStore::open(&db_dir).expect("Failed to open store");
|
|
|
|
journal.append(serialize_assertion(&assertion).expect("ser")).expect("append");
|
|
|
|
let journal = Arc::new(Mutex::new(journal));
|
|
let store = Arc::new(store);
|
|
let mut worker =
|
|
IngestWorker::new(journal, store.clone()).await.expect("Failed to create worker");
|
|
|
|
// Step should fail with InvalidSignature error
|
|
let result = worker.step().await;
|
|
assert!(result.is_err(), "Should reject invalid signature");
|
|
|
|
let err = result.unwrap_err();
|
|
assert!(
|
|
matches!(err, IngestError::InvalidSignature(_)),
|
|
"Error should be InvalidSignature, got: {:?}",
|
|
err
|
|
);
|
|
|
|
// Verify no assertion was stored
|
|
let assertions = store.scan_prefix(b"H:").await.expect("scan");
|
|
assert_eq!(assertions.len(), 0, "No assertion should be stored");
|
|
}
|
|
|
|
/// Test: Assertions with no signatures are rejected.
|
|
#[tokio::test]
|
|
async fn test_rejects_unsigned_assertion() {
|
|
let dir = tempdir().expect("Failed to create temp dir");
|
|
let wal_dir = dir.path().join("wal");
|
|
let db_dir = dir.path().join("db");
|
|
|
|
// Create an assertion with no signatures
|
|
let assertion = Assertion {
|
|
subject: "Test".to_string(),
|
|
predicate: "unsigned".to_string(),
|
|
object: ObjectValue::Text("test".to_string()),
|
|
parent_hash: None,
|
|
source_hash: [0u8; 32],
|
|
source_class: SourceClass::Expert,
|
|
visual_hash: None,
|
|
epoch: None,
|
|
source_metadata: None,
|
|
lifecycle: LifecycleStage::Proposed,
|
|
signatures: vec![], // No signatures!
|
|
confidence: 0.95,
|
|
timestamp: 1000,
|
|
vector: None,
|
|
};
|
|
|
|
let mut journal = Journal::open(&wal_dir).expect("Failed to open journal");
|
|
let store = HybridStore::open(&db_dir).expect("Failed to open store");
|
|
|
|
journal.append(serialize_assertion(&assertion).expect("ser")).expect("append");
|
|
|
|
let journal = Arc::new(Mutex::new(journal));
|
|
let store = Arc::new(store);
|
|
let mut worker =
|
|
IngestWorker::new(journal, store.clone()).await.expect("Failed to create worker");
|
|
|
|
let result = worker.step().await;
|
|
assert!(result.is_err(), "Should reject unsigned assertion");
|
|
|
|
let err = result.unwrap_err();
|
|
assert!(
|
|
matches!(err, IngestError::InvalidSignature(_)),
|
|
"Error should be InvalidSignature, got: {:?}",
|
|
err
|
|
);
|
|
}
|
|
|
|
/// Test: Multi-signature assertions require all signatures to be valid.
|
|
#[tokio::test]
|
|
async fn test_multisig_all_must_be_valid() {
|
|
let dir = tempdir().expect("Failed to create temp dir");
|
|
let wal_dir = dir.path().join("wal");
|
|
let db_dir = dir.path().join("db");
|
|
|
|
// Create an assertion with one valid and one invalid signature
|
|
let mut csprng = OsRng;
|
|
let signing_key = SigningKey::generate(&mut csprng);
|
|
let verifying_key = signing_key.verifying_key();
|
|
let message = "Test:multisig";
|
|
let valid_signature = signing_key.sign(message.as_bytes());
|
|
|
|
let assertion = Assertion {
|
|
subject: "Test".to_string(),
|
|
predicate: "multisig".to_string(),
|
|
object: ObjectValue::Text("test".to_string()),
|
|
parent_hash: None,
|
|
source_hash: [0u8; 32],
|
|
source_class: SourceClass::Expert,
|
|
visual_hash: None,
|
|
epoch: None,
|
|
source_metadata: None,
|
|
lifecycle: LifecycleStage::Proposed,
|
|
signatures: vec![
|
|
// Valid signature
|
|
SignatureEntry {
|
|
agent_id: verifying_key.to_bytes(),
|
|
signature: valid_signature.to_bytes(),
|
|
timestamp: 1000,
|
|
},
|
|
// Invalid signature
|
|
SignatureEntry { agent_id: [1u8; 32], signature: [2u8; 64], timestamp: 1001 },
|
|
],
|
|
confidence: 0.95,
|
|
timestamp: 1000,
|
|
vector: None,
|
|
};
|
|
|
|
let mut journal = Journal::open(&wal_dir).expect("Failed to open journal");
|
|
let store = HybridStore::open(&db_dir).expect("Failed to open store");
|
|
|
|
journal.append(serialize_assertion(&assertion).expect("ser")).expect("append");
|
|
|
|
let journal = Arc::new(Mutex::new(journal));
|
|
let store = Arc::new(store);
|
|
let mut worker =
|
|
IngestWorker::new(journal, store.clone()).await.expect("Failed to create worker");
|
|
|
|
let result = worker.step().await;
|
|
assert!(result.is_err(), "Should reject when any signature is invalid");
|
|
}
|
|
|
|
// ========================================================================
|
|
// PERSISTENT CURSOR TESTS
|
|
// ========================================================================
|
|
|
|
/// Test: Cursor is persisted after ingestion and restored on restart.
|
|
///
|
|
/// After ingesting records, a new worker should resume from where the
|
|
/// previous one left off instead of replaying the entire WAL.
|
|
#[tokio::test]
|
|
async fn test_cursor_persists_across_restarts() {
|
|
let dir = tempdir().expect("Failed to create temp dir");
|
|
let wal_dir = dir.path().join("wal");
|
|
let db_dir = dir.path().join("db");
|
|
|
|
let mut journal = Journal::open(&wal_dir).expect("Failed to open journal");
|
|
let store = HybridStore::open(&db_dir).expect("Failed to open store");
|
|
|
|
// Write two assertions to the WAL
|