1use std::io::{BufRead, BufReader, Seek, SeekFrom, Write};
16use std::path::Path;
17use std::sync::Arc;
18use std::time::Duration;
19
20use firstpass_core::{DeferredVerdict, GENESIS_HASH, Score, Trace, Verdict};
21use rusqlite::Connection;
22use tokio::sync::mpsc;
23use tokio::task::JoinHandle;
24
25use crate::config::ReceiptsMode;
26
27fn connect(db_path: impl AsRef<Path>) -> Result<Connection, StoreError> {
30 let conn = Connection::open(db_path.as_ref())?;
31 conn.busy_timeout(Duration::from_secs(5))?;
32 Ok(conn)
33}
34
35#[derive(Debug, thiserror::Error)]
37pub enum StoreError {
38 #[error("sqlite error: {0}")]
40 Sqlite(#[from] rusqlite::Error),
41 #[error("trace error: {0}")]
43 Trace(#[from] firstpass_core::Error),
44 #[error("json error: {0}")]
46 Json(#[from] serde_json::Error),
47 #[error("spill I/O error: {0}")]
49 Io(#[from] std::io::Error),
50}
51
52pub type SpillHandle = Arc<std::sync::Mutex<std::fs::File>>;
60
61pub(crate) fn spill_path(db_path: &Path) -> std::path::PathBuf {
63 let mut s = db_path.as_os_str().to_owned();
64 s.push(".spill.jsonl");
65 std::path::PathBuf::from(s)
66}
67
68pub fn append_to_spill(handle: &SpillHandle, trace: &Trace) -> Result<(), StoreError> {
77 let line = serde_json::to_string(trace)?;
78 let mut guard = handle.lock().unwrap_or_else(|e| e.into_inner());
81 writeln!(guard, "{line}")?;
82 guard.sync_data()?;
84 Ok(())
85}
86
87pub type TraceSender = mpsc::Sender<Trace>;
91
92pub const TRACE_CHANNEL_CAP: usize = 8192;
95
96pub fn open(db_path: impl AsRef<Path>) -> Result<(TraceSender, JoinHandle<()>), StoreError> {
108 let (tx, _spill, handle) = open_with_receipts(db_path, ReceiptsMode::BestEffort)?;
109 Ok((tx, handle))
110}
111
112pub fn open_with_receipts(
126 db_path: impl AsRef<Path>,
127 receipts_mode: ReceiptsMode,
128) -> Result<(TraceSender, Option<SpillHandle>, JoinHandle<()>), StoreError> {
129 let db_path = db_path.as_ref();
130 let conn = connect(db_path)?;
131 migrate(&conn)?;
132
133 let spill = if receipts_mode == ReceiptsMode::Durable {
134 let path = spill_path(db_path);
135 let file = std::fs::OpenOptions::new()
136 .create(true)
137 .append(true)
138 .read(true)
139 .open(&path)?;
140 Some(Arc::new(std::sync::Mutex::new(file)))
141 } else {
142 None
143 };
144
145 let (tx, rx) = mpsc::channel::<Trace>(TRACE_CHANNEL_CAP);
146 let spill_writer = spill.clone();
147 let handle = tokio::task::spawn_blocking(move || writer_loop(conn, rx, spill_writer));
148 Ok((tx, spill, handle))
149}
150
151fn migrate(conn: &Connection) -> Result<(), StoreError> {
152 conn.pragma_update(None, "journal_mode", "WAL")?;
155 conn.execute_batch(
156 "CREATE TABLE IF NOT EXISTS traces (
157 seq INTEGER PRIMARY KEY AUTOINCREMENT,
158 trace_id TEXT NOT NULL,
159 ts TEXT NOT NULL,
160 prev_hash TEXT NOT NULL,
161 hash TEXT NOT NULL,
162 tenant TEXT NOT NULL,
163 session TEXT NOT NULL,
164 body TEXT NOT NULL
165 );
166 CREATE INDEX IF NOT EXISTS traces_session_idx ON traces(session);
167 -- Tenant-scoped reads (ADR 0004 §D3) filter on `tenant`; index it with `seq` so a
168 -- per-tenant scan stays ordered and cheap. Existing rows keep their `tenant` value.
169 CREATE INDEX IF NOT EXISTS traces_tenant_seq_idx ON traces(tenant, seq);
170 -- Deferred verdicts live in their OWN table, keyed by trace_id. They are NEVER folded
171 -- into the sealed, hashed `traces.body`, so a late outcome can't alter a past record and
172 -- the tamper-evident chain stays valid. They are merged onto a trace only on read.
173 CREATE TABLE IF NOT EXISTS deferred_verdicts (
174 id INTEGER PRIMARY KEY AUTOINCREMENT,
175 trace_id TEXT NOT NULL,
176 gate_id TEXT NOT NULL,
177 verdict TEXT NOT NULL,
178 score REAL,
179 reported_at TEXT NOT NULL,
180 reporter TEXT NOT NULL
181 );
182 CREATE INDEX IF NOT EXISTS deferred_trace_idx ON deferred_verdicts(trace_id);",
183 )?;
184 Ok(())
185}
186
187fn write_trace(conn: &Connection, trace: &mut Trace, head: &mut String) {
190 trace.prev_hash = head.clone();
191 let hash = match trace.hash() {
192 Ok(h) => h,
193 Err(err) => {
194 tracing::error!(%err, trace_id = %trace.trace_id, "trace writer: hash failed, dropping");
195 return;
196 }
197 };
198 if let Err(err) = insert(conn, trace, &hash) {
199 tracing::error!(%err, trace_id = %trace.trace_id, "trace writer: insert failed, dropping");
200 return;
201 }
202 *head = hash;
203}
204
205fn drain_spill(conn: &Connection, spill: &SpillHandle, head: &mut String) {
212 let mut guard = spill.lock().unwrap_or_else(|e| e.into_inner());
215
216 if let Err(e) = guard.seek(SeekFrom::Start(0)) {
218 tracing::error!(%e, "drain_spill: seek failed");
219 return;
220 }
221
222 let lines: Vec<String> = {
224 let reader = BufReader::new(&*guard);
225 reader
226 .lines()
227 .filter_map(|l| match l {
228 Ok(s) if !s.trim().is_empty() => Some(s),
229 Ok(_) => None,
230 Err(e) => {
231 tracing::error!(%e, "drain_spill: read error");
232 None
233 }
234 })
235 .collect()
236 };
237
238 if lines.is_empty() {
239 return;
240 }
241
242 let n = lines.len();
243 for line in &lines {
244 let mut trace: Trace = match serde_json::from_str(line) {
245 Ok(t) => t,
246 Err(e) => {
247 tracing::error!(%e, "drain_spill: bad JSON line, skipping");
248 continue;
249 }
250 };
251 write_trace(conn, &mut trace, head);
252 }
253 tracing::info!(drained = n, "spill file drained into trace store");
254
255 if let Err(e) = guard.seek(SeekFrom::Start(0)) {
257 tracing::error!(%e, "drain_spill: post-drain seek failed");
258 return;
259 }
260 if let Err(e) = guard.set_len(0) {
261 tracing::error!(%e, "drain_spill: truncate failed — spill file may replay on next boot");
262 }
263}
264
265fn writer_loop(conn: Connection, mut rx: mpsc::Receiver<Trace>, spill: Option<SpillHandle>) {
272 let mut head = match current_head(&conn) {
273 Ok(h) => h,
274 Err(err) => {
275 tracing::error!(%err, "trace writer: failed to load chain head, stopping");
276 return;
277 }
278 };
279
280 if let Some(ref s) = spill {
282 drain_spill(&conn, s, &mut head);
283 }
284
285 loop {
286 let Some(mut trace) = rx.blocking_recv() else {
287 break;
288 };
289 write_trace(&conn, &mut trace, &mut head);
290
291 loop {
294 match rx.try_recv() {
295 Ok(mut t) => write_trace(&conn, &mut t, &mut head),
296 Err(mpsc::error::TryRecvError::Empty) => {
297 if let Some(ref s) = spill {
298 drain_spill(&conn, s, &mut head);
299 }
300 break;
301 }
302 Err(mpsc::error::TryRecvError::Disconnected) => return,
303 }
304 }
305 }
306
307 if let Some(ref s) = spill {
309 drain_spill(&conn, s, &mut head);
310 }
311}
312
313fn current_head(conn: &Connection) -> Result<String, StoreError> {
314 let mut stmt = conn.prepare("SELECT hash FROM traces ORDER BY seq DESC LIMIT 1")?;
315 let mut rows = stmt.query([])?;
316 match rows.next()? {
317 Some(row) => Ok(row.get(0)?),
318 None => Ok(GENESIS_HASH.to_owned()),
319 }
320}
321
322fn insert(conn: &Connection, trace: &Trace, hash: &str) -> Result<(), StoreError> {
323 let body = serde_json::to_string(trace)?;
324 conn.execute(
325 "INSERT INTO traces (trace_id, ts, prev_hash, hash, tenant, session, body)
326 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
327 rusqlite::params![
328 trace.trace_id.to_string(),
329 trace.ts.to_string(),
330 trace.prev_hash,
331 hash,
332 trace.tenant_id,
333 trace.session_id,
334 body,
335 ],
336 )?;
337 Ok(())
338}
339
340pub fn load_all_traces(db_path: impl AsRef<Path>) -> Result<Vec<Trace>, StoreError> {
354 let conn = connect(db_path.as_ref())?;
355 let mut stmt = conn.prepare("SELECT body FROM traces ORDER BY seq ASC")?;
356 let rows = stmt.query_map([], |row| row.get::<_, String>(0))?;
357 let mut traces = Vec::new();
358 for row in rows {
359 traces.push(serde_json::from_str(&row?)?);
360 }
361 Ok(traces)
362}
363
364pub fn load_tenant_traces(
371 db_path: impl AsRef<Path>,
372 tenant: &str,
373) -> Result<Vec<Trace>, StoreError> {
374 let conn = connect(db_path.as_ref())?;
375 let mut stmt = conn.prepare("SELECT body FROM traces WHERE tenant = ?1 ORDER BY seq ASC")?;
376 let rows = stmt.query_map([tenant], |row| row.get::<_, String>(0))?;
377 let mut traces = Vec::new();
378 for row in rows {
379 traces.push(serde_json::from_str(&row?)?);
380 }
381 Ok(traces)
382}
383
384pub fn trace_exists(
392 db_path: impl AsRef<Path>,
393 tenant: &str,
394 trace_id: &str,
395) -> Result<bool, StoreError> {
396 let conn = connect(db_path.as_ref())?;
397 let n: i64 = conn.query_row(
398 "SELECT COUNT(1) FROM traces WHERE tenant = ?1 AND trace_id = ?2",
399 [tenant, trace_id],
400 |row| row.get(0),
401 )?;
402 Ok(n > 0)
403}
404
405pub fn append_deferred(
412 db_path: impl AsRef<Path>,
413 trace_id: &str,
414 v: &DeferredVerdict,
415) -> Result<(), StoreError> {
416 let conn = connect(db_path.as_ref())?;
417 conn.execute(
418 "INSERT INTO deferred_verdicts (trace_id, gate_id, verdict, score, reported_at, reporter)
419 VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
420 rusqlite::params![
421 trace_id,
422 v.gate_id,
423 v.verdict.as_str(),
424 v.score.map(Score::value),
425 v.reported_at.to_string(),
426 v.reporter,
427 ],
428 )?;
429 Ok(())
430}
431
432pub fn load_deferred(
438 db_path: impl AsRef<Path>,
439 trace_id: &str,
440) -> Result<Vec<DeferredVerdict>, StoreError> {
441 let conn = connect(db_path.as_ref())?;
442 let mut stmt = conn.prepare(
443 "SELECT gate_id, verdict, score, reported_at, reporter
444 FROM deferred_verdicts WHERE trace_id = ?1 ORDER BY id ASC",
445 )?;
446 let rows = stmt.query_map([trace_id], |row| {
447 Ok((
448 row.get::<_, String>(0)?,
449 row.get::<_, String>(1)?,
450 row.get::<_, Option<f64>>(2)?,
451 row.get::<_, String>(3)?,
452 row.get::<_, String>(4)?,
453 ))
454 })?;
455
456 let mut out = Vec::new();
457 for row in rows {
458 let (gate_id, verdict_s, score, reported_s, reporter) = row?;
459 let verdict = match verdict_s.as_str() {
460 "pass" => Verdict::Pass,
461 "fail" => Verdict::Fail,
462 "abstain" => Verdict::Abstain,
463 other => {
464 tracing::warn!(verdict = %other, %trace_id, "skipping deferred row with bad verdict");
465 continue;
466 }
467 };
468 let Ok(reported_at) = reported_s.parse::<jiff::Timestamp>() else {
469 tracing::warn!(%trace_id, "skipping deferred row with bad timestamp");
470 continue;
471 };
472 out.push(DeferredVerdict {
473 gate_id,
474 verdict,
475 score: score.and_then(|s| Score::new(s).ok()),
476 reported_at,
477 reporter,
478 });
479 }
480 Ok(out)
481}
482
483pub fn load_trace_view(
493 db_path: impl AsRef<Path>,
494 tenant: &str,
495 trace_id: &str,
496) -> Result<Option<Trace>, StoreError> {
497 let conn = connect(db_path.as_ref())?;
498 let body: Option<String> = conn
499 .query_row(
500 "SELECT body FROM traces WHERE tenant = ?1 AND trace_id = ?2",
501 [tenant, trace_id],
502 |row| row.get(0),
503 )
504 .ok();
505 let Some(body) = body else { return Ok(None) };
506 let mut trace: Trace = serde_json::from_str(&body)?;
507 trace.deferred = load_deferred(db_path, trace_id)?;
508 Ok(Some(trace))
509}
510
511#[cfg(test)]
512mod tests {
513 use firstpass_core::{
514 Attempt, Features, FinalOutcome, GENESIS_HASH, PolicyRef, RequestInfo, ServedFrom,
515 TaskKind, Verdict, verify_chain,
516 };
517
518 use super::*;
519
520 fn sample_trace(tenant: &str, session: &str) -> Trace {
521 let attempt = Attempt {
522 rung: 0,
523 model: "claude-haiku-4-5".to_owned(),
524 provider: "anthropic".to_owned(),
525 in_tokens: 10,
526 out_tokens: 5,
527 cost_usd: 0.001,
528 latency_ms: 12,
529 gates: vec![],
530 verdict: Verdict::Pass,
531 };
532 let mut trace = Trace {
533 trace_id: uuid::Uuid::now_v7(),
534 prev_hash: GENESIS_HASH.to_owned(),
535 tenant_id: tenant.to_owned(),
536 session_id: session.to_owned(),
537 ts: jiff::Timestamp::now(),
538 mode: firstpass_core::Mode::Observe,
539 policy: PolicyRef {
540 id: "observe-passthrough@v0".to_owned(),
541 explore: false,
542 propensity: None,
543 },
544 request: RequestInfo {
545 api: "anthropic.messages".to_owned(),
546 prompt_hash: "deadbeef".to_owned(),
547 features: Features::new(TaskKind::Other),
548 },
549 attempts: vec![attempt],
550 deferred: Vec::new(),
551 final_: FinalOutcome {
552 served_rung: Some(0),
553 served_from: ServedFrom::Attempt,
554 total_cost_usd: 0.001,
555 gate_cost_usd: 0.0,
556 total_latency_ms: 12,
557 escalations: 0,
558 counterfactual_baseline_usd: 0.001,
559 savings_usd: 0.0,
560 },
561 };
562 trace.recompute_savings();
563 trace
564 }
565
566 #[tokio::test]
567 async fn writer_assigns_prev_hash_and_forms_a_valid_chain() {
568 let db_path =
569 std::env::temp_dir().join(format!("firstpass-store-test-{}.db", uuid::Uuid::now_v7()));
570 let (tx, handle) = open(&db_path).unwrap();
571
572 tx.try_send(sample_trace("tenant-a", "session-1")).unwrap();
573 tx.try_send(sample_trace("tenant-a", "session-1")).unwrap();
574 drop(tx);
575 handle.await.unwrap();
576
577 let traces = load_all_traces(&db_path).unwrap();
578 assert_eq!(traces.len(), 2);
579 assert_eq!(traces[0].prev_hash, GENESIS_HASH);
580 assert_eq!(traces[1].prev_hash, traces[0].hash().unwrap());
581 verify_chain(&traces, GENESIS_HASH).unwrap();
582
583 let _ = std::fs::remove_file(&db_path);
584 }
585
586 #[tokio::test]
590 async fn tenant_scoped_reads_never_cross_the_boundary() {
591 let db_path =
592 std::env::temp_dir().join(format!("firstpass-isolation-{}.db", uuid::Uuid::now_v7()));
593 let (tx, handle) = open(&db_path).unwrap();
594
595 let a0 = sample_trace("tenant-a", "sa-0");
597 let a1 = sample_trace("tenant-a", "sa-1");
598 let b0 = sample_trace("tenant-b", "sb-0");
599 let (a0_id, a1_id, b0_id) = (
600 a0.trace_id.to_string(),
601 a1.trace_id.to_string(),
602 b0.trace_id.to_string(),
603 );
604 tx.try_send(a0).unwrap();
605 tx.try_send(b0).unwrap();
606 tx.try_send(a1).unwrap();
607 drop(tx);
608 handle.await.unwrap();
609
610 let a_traces = load_tenant_traces(&db_path, "tenant-a").unwrap();
612 assert_eq!(a_traces.len(), 2, "A must see only A's traces");
613 assert!(a_traces.iter().all(|t| t.tenant_id == "tenant-a"));
614 let b_traces = load_tenant_traces(&db_path, "tenant-b").unwrap();
615 assert_eq!(b_traces.len(), 1, "B must see only B's trace");
616 assert!(b_traces.iter().all(|t| t.tenant_id == "tenant-b"));
617
618 assert!(trace_exists(&db_path, "tenant-a", &a0_id).unwrap());
620 assert!(!trace_exists(&db_path, "tenant-a", &b0_id).unwrap());
621 assert!(trace_exists(&db_path, "tenant-b", &b0_id).unwrap());
622 assert!(!trace_exists(&db_path, "tenant-b", &a1_id).unwrap());
623
624 assert!(
626 load_trace_view(&db_path, "tenant-a", &a0_id)
627 .unwrap()
628 .is_some()
629 );
630 assert!(
631 load_trace_view(&db_path, "tenant-a", &b0_id)
632 .unwrap()
633 .is_none()
634 );
635 assert!(
636 load_trace_view(&db_path, "tenant-b", &a0_id)
637 .unwrap()
638 .is_none()
639 );
640
641 assert!(load_tenant_traces(&db_path, "ghost").unwrap().is_empty());
643
644 let all = load_all_traces(&db_path).unwrap();
646 assert_eq!(all.len(), 3);
647 verify_chain(&all, GENESIS_HASH).unwrap();
648
649 let _ = std::fs::remove_file(&db_path);
650 }
651
652 #[tokio::test]
653 async fn deferred_verdicts_attach_on_read_without_breaking_the_chain() {
654 let db_path = std::env::temp_dir().join(format!(
655 "firstpass-deferred-test-{}.db",
656 uuid::Uuid::now_v7()
657 ));
658 let (tx, handle) = open(&db_path).unwrap();
659 let t0 = sample_trace("acme", "run-1");
660 let t1 = sample_trace("acme", "run-1");
661 let (id0, id1) = (t0.trace_id.to_string(), t1.trace_id.to_string());
662 tx.try_send(t0).unwrap();
663 tx.try_send(t1).unwrap();
664 drop(tx);
665 handle.await.unwrap();
666
667 let dv = DeferredVerdict {
669 gate_id: "tests".to_owned(),
670 verdict: Verdict::Pass,
671 score: Some(Score::new(1.0).unwrap()),
672 reported_at: jiff::Timestamp::now(),
673 reporter: "ci".to_owned(),
674 };
675 append_deferred(&db_path, &id0, &dv).unwrap();
676 assert!(trace_exists(&db_path, "acme", &id0).unwrap());
677 assert!(!trace_exists(&db_path, "acme", "no-such-trace").unwrap());
678 assert!(!trace_exists(&db_path, "other-tenant", &id0).unwrap());
680
681 let view = load_trace_view(&db_path, "acme", &id0).unwrap().unwrap();
683 assert_eq!(view.deferred.len(), 1);
684 assert_eq!(view.deferred[0].gate_id, "tests");
685 assert_eq!(view.deferred[0].verdict, Verdict::Pass);
686 assert!(
688 load_trace_view(&db_path, "acme", &id1)
689 .unwrap()
690 .unwrap()
691 .deferred
692 .is_empty()
693 );
694 assert!(
696 load_trace_view(&db_path, "other-tenant", &id0)
697 .unwrap()
698 .is_none()
699 );
700
701 let traces = load_all_traces(&db_path).unwrap();
704 assert!(
705 traces.iter().all(|t| t.deferred.is_empty()),
706 "sealed records stay deferred-free"
707 );
708 verify_chain(&traces, GENESIS_HASH).unwrap();
709
710 let _ = std::fs::remove_file(&db_path);
711 }
712
713 #[test]
717 fn durable_append_to_spill_writes_valid_json_line() {
718 let db_path =
719 std::env::temp_dir().join(format!("firstpass-spill-write-{}.db", uuid::Uuid::now_v7()));
720 let spill_p = spill_path(&db_path);
721 let file = std::fs::OpenOptions::new()
722 .create(true)
723 .append(true)
724 .read(true)
725 .open(&spill_p)
726 .unwrap();
727 let handle = Arc::new(std::sync::Mutex::new(file));
728
729 let t = sample_trace("t", "s");
730 append_to_spill(&handle, &t).unwrap();
731
732 let content = std::fs::read_to_string(&spill_p).unwrap();
733 let lines: Vec<&str> = content.lines().collect();
734 assert_eq!(lines.len(), 1, "one line per spilled trace");
735 let parsed: Trace = serde_json::from_str(lines[0]).unwrap();
736 assert_eq!(parsed.trace_id, t.trace_id);
737
738 let _ = std::fs::remove_file(&spill_p);
739 }
740
741 #[tokio::test]
744 async fn drain_on_boot_restores_spilled_traces_and_chain_is_valid() {
745 let db_path =
746 std::env::temp_dir().join(format!("firstpass-drain-boot-{}.db", uuid::Uuid::now_v7()));
747 let spill_p = spill_path(&db_path);
748
749 let t1 = sample_trace("t", "s1");
751 let t2 = sample_trace("t", "s2");
752 {
753 let file = std::fs::OpenOptions::new()
754 .create(true)
755 .append(true)
756 .read(true)
757 .open(&spill_p)
758 .unwrap();
759 let h = Arc::new(std::sync::Mutex::new(file));
760 append_to_spill(&h, &t1).unwrap();
761 append_to_spill(&h, &t2).unwrap();
762 }
763
764 let (tx, _spill, handle) = open_with_receipts(&db_path, ReceiptsMode::Durable).unwrap();
766 drop(tx); handle.await.unwrap();
768
769 let traces = load_all_traces(&db_path).unwrap();
770 assert_eq!(traces.len(), 2, "both spilled traces recovered");
771 verify_chain(&traces, GENESIS_HASH).unwrap();
772
773 let remaining = std::fs::read_to_string(&spill_p).unwrap();
775 assert!(
776 remaining.trim().is_empty(),
777 "spill file must be empty after drain"
778 );
779
780 let _ = std::fs::remove_file(&db_path);
781 let _ = std::fs::remove_file(&spill_p);
782 }
783
784 #[tokio::test]
787 async fn spilled_traces_land_before_later_channel_traces() {
788 let db_path =
789 std::env::temp_dir().join(format!("firstpass-spill-order-{}.db", uuid::Uuid::now_v7()));
790 let spill_p = spill_path(&db_path);
791
792 let spilled = sample_trace("t", "spilled");
794 {
795 let file = std::fs::OpenOptions::new()
796 .create(true)
797 .append(true)
798 .read(true)
799 .open(&spill_p)
800 .unwrap();
801 let h = Arc::new(std::sync::Mutex::new(file));
802 append_to_spill(&h, &spilled).unwrap();
803 }
804
805 let (tx, _spill, handle) = open_with_receipts(&db_path, ReceiptsMode::Durable).unwrap();
807 let channel_trace = sample_trace("t", "channel");
809 tx.try_send(channel_trace.clone()).unwrap();
810 drop(tx);
811 handle.await.unwrap();
812
813 let traces = load_all_traces(&db_path).unwrap();
814 assert_eq!(traces.len(), 2, "spilled + channel trace");
815 assert_eq!(
817 traces[0].trace_id, spilled.trace_id,
818 "spilled trace must precede channel trace"
819 );
820 assert_eq!(
821 traces[1].trace_id, channel_trace.trace_id,
822 "channel trace must follow spilled trace"
823 );
824 verify_chain(&traces, GENESIS_HASH).unwrap();
825
826 let _ = std::fs::remove_file(&db_path);
827 let _ = std::fs::remove_file(&spill_p);
828 }
829}