Trusty: provenance library workspace (initial import)
This commit is contained in:
15
crates/adapters/trusty-memory/Cargo.toml
Normal file
15
crates/adapters/trusty-memory/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "trusty-memory"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
rust-version.workspace = true
|
||||
description = "In-memory Store backend for Trusty. Reference implementation and test harness."
|
||||
|
||||
[dependencies]
|
||||
trusty-store = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
trusty-store = { workspace = true, features = ["testing"] }
|
||||
135
crates/adapters/trusty-memory/src/lib.rs
Normal file
135
crates/adapters/trusty-memory/src/lib.rs
Normal file
@@ -0,0 +1,135 @@
|
||||
//! In-memory [`Store`](trusty_store::Store) backend for Trusty.
|
||||
//!
|
||||
//! Reference implementation of the `Store` trait. Intended for tests, fuzzing,
|
||||
//! and conformance checks. Not suitable for persistent production use -- all data
|
||||
//! is lost when the process exits.
|
||||
|
||||
#![deny(clippy::all)]
|
||||
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
|
||||
use trusty_store::{
|
||||
KvCrossCheckReport, LastRow, RowInsert, Store, StoreResult, StoredRow, TipRow, TipUpsert,
|
||||
};
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct MemoryStore {
|
||||
rows: BTreeMap<i64, StoredRow>,
|
||||
next_id: i64,
|
||||
tips_by_chain: HashMap<String, TipRow>,
|
||||
tips_by_entity: HashMap<String, TipRow>,
|
||||
}
|
||||
|
||||
impl MemoryStore {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl Store for MemoryStore {
|
||||
fn last_row(&self, entity_id: &str) -> StoreResult<Option<LastRow>> {
|
||||
let last = self
|
||||
.rows
|
||||
.values()
|
||||
.rev()
|
||||
.find(|r| r.entity_id == entity_id && r.event_type != "BREAK" && r.row_hash.is_some())
|
||||
.map(|r| LastRow {
|
||||
row_hash: r.row_hash.clone().unwrap(),
|
||||
sequence: r.sequence,
|
||||
});
|
||||
Ok(last)
|
||||
}
|
||||
|
||||
fn insert_row(&mut self, row: &RowInsert) -> StoreResult<i64> {
|
||||
self.next_id += 1;
|
||||
let id = self.next_id;
|
||||
let stored = StoredRow {
|
||||
id,
|
||||
entity_id: row.entity_id.clone(),
|
||||
chain_id: row.chain_id.clone(),
|
||||
event_type: row.event_type.clone(),
|
||||
sequence: row.sequence,
|
||||
prev_hash: row.prev_hash.clone(),
|
||||
row_hash: row.row_hash.clone(),
|
||||
anchor_hash: row.anchor_hash.clone(),
|
||||
raw_payload: row.raw_payload.clone(),
|
||||
detected_at: row.detected_at.clone(),
|
||||
received_at: row.received_at.clone(),
|
||||
source_url: row.source_url.clone(),
|
||||
extraction_method: row.extraction_method.clone(),
|
||||
};
|
||||
self.rows.insert(id, stored);
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
fn iter_chain<'a>(
|
||||
&'a self,
|
||||
entity_id: &str,
|
||||
) -> StoreResult<Box<dyn Iterator<Item = StoreResult<StoredRow>> + 'a>> {
|
||||
let entity = entity_id.to_string();
|
||||
let iter = self
|
||||
.rows
|
||||
.values()
|
||||
.filter(move |r| r.entity_id == entity)
|
||||
.cloned()
|
||||
.map(Ok);
|
||||
Ok(Box::new(iter))
|
||||
}
|
||||
|
||||
fn get_tip(&self, chain_id: &str) -> StoreResult<Option<TipRow>> {
|
||||
Ok(self.tips_by_chain.get(chain_id).cloned())
|
||||
}
|
||||
|
||||
fn get_tip_for_entity(&self, entity_id: &str) -> StoreResult<Option<TipRow>> {
|
||||
Ok(self.tips_by_entity.get(entity_id).cloned())
|
||||
}
|
||||
|
||||
fn set_tip(&mut self, tip: &TipUpsert) -> StoreResult<()> {
|
||||
let row = TipRow {
|
||||
entity_id: tip.entity_id.clone(),
|
||||
chain_id: tip.chain_id.clone(),
|
||||
current_tip_hash: tip.current_tip_hash.clone(),
|
||||
updated_at: String::new(),
|
||||
};
|
||||
self.tips_by_chain.insert(tip.chain_id.clone(), row.clone());
|
||||
self.tips_by_entity.insert(tip.entity_id.clone(), row);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn kv_cross_check(&self) -> StoreResult<KvCrossCheckReport> {
|
||||
let kv1 = self.rows.values().filter(|r| r.row_hash.is_some()).count();
|
||||
let kv2 = self.tips_by_entity.len();
|
||||
let mut mismatches: Vec<String> = Vec::new();
|
||||
for (entity, tip) in &self.tips_by_entity {
|
||||
if let Some(hash) = &tip.current_tip_hash {
|
||||
let found = self.rows.values().any(|r| {
|
||||
r.row_hash.as_deref() == Some(hash.as_str()) && r.chain_id == tip.chain_id
|
||||
});
|
||||
if !found {
|
||||
mismatches.push(format!("tip_hash_not_in_rows: {}", entity));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(KvCrossCheckReport {
|
||||
ok: mismatches.is_empty(),
|
||||
kv1_total_hashes: kv1,
|
||||
kv2_total_entities: kv2,
|
||||
entities_checked: kv2,
|
||||
mismatches,
|
||||
})
|
||||
}
|
||||
|
||||
fn with_txn(
|
||||
&mut self,
|
||||
f: &mut dyn FnMut(&mut dyn Store) -> StoreResult<()>,
|
||||
) -> StoreResult<()> {
|
||||
let snap = self.clone();
|
||||
match f(self) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(e) => {
|
||||
*self = snap;
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
8
crates/adapters/trusty-memory/tests/conformance.rs
Normal file
8
crates/adapters/trusty-memory/tests/conformance.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
use trusty_memory::MemoryStore;
|
||||
use trusty_store::testing::run_conformance;
|
||||
|
||||
#[test]
|
||||
fn memory_store_passes_conformance() {
|
||||
let mut store = MemoryStore::new();
|
||||
run_conformance(&mut store);
|
||||
}
|
||||
Reference in New Issue
Block a user