inferadb_ledger_types/types/block.rs
1//! Block, transaction, entity, relationship, and write result types for the ledger chain.
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6use super::{ClientId, NodeId, OrganizationId, Region, TxId, VaultId, WriteStatus};
7use crate::hash::Hash;
8
9// ============================================================================
10// Block Structures
11// ============================================================================
12
13/// Block header containing cryptographic chain metadata.
14///
15/// Block headers are hashed with a fixed 148-byte encoding:
16/// height (8) + organization (8) + vault (8) + previous_hash (32) + tx_merkle_root (32)
17/// + state_root (32) + timestamp_secs (8) + timestamp_nanos (4) + term (8) + committed_index (8)
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, bon::Builder)]
19pub struct BlockHeader {
20 /// Block height (0 for genesis).
21 pub height: u64,
22 /// Organization owning this vault.
23 #[builder(into)]
24 pub organization: OrganizationId,
25 /// Vault identifier within the organization.
26 #[builder(into)]
27 pub vault: VaultId,
28 /// Hash of the previous block (ZERO_HASH for genesis).
29 pub previous_hash: Hash,
30 /// Merkle root of transactions in this block.
31 pub tx_merkle_root: Hash,
32 /// State root after applying all transactions.
33 pub state_root: Hash,
34 /// Block creation timestamp.
35 pub timestamp: DateTime<Utc>,
36 /// Raft term when this block was committed.
37 pub term: u64,
38 /// Raft committed index for this block.
39 pub committed_index: u64,
40}
41
42/// Client-facing block containing a header and transactions for a single vault.
43///
44/// Clients receive and verify these blocks. Each vault maintains its own
45/// independent chain for cryptographic isolation.
46#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
47pub struct VaultBlock {
48 /// Block header with chain metadata (includes organization, vault).
49 pub header: BlockHeader,
50 /// Transactions in this block.
51 pub transactions: Vec<Transaction>,
52}
53
54impl VaultBlock {
55 /// Returns the organization that owns this vault block.
56 #[inline]
57 pub fn organization(&self) -> OrganizationId {
58 self.header.organization
59 }
60
61 /// Returns the vault identifier for this block.
62 #[inline]
63 pub fn vault(&self) -> VaultId {
64 self.header.vault
65 }
66
67 /// Returns the block height in the vault chain.
68 #[inline]
69 pub fn height(&self) -> u64 {
70 self.header.height
71 }
72}
73
74/// Internal region block stored on disk, containing entries for multiple vaults.
75///
76/// Multiple vaults share a single Raft group. Region blocks are the physical
77/// unit of Raft replication; clients never see them directly.
78#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
79pub struct RegionBlock {
80 /// Region this block belongs to.
81 pub region: Region,
82 /// Monotonic region-level height.
83 pub region_height: u64,
84 /// Hash linking to previous region block.
85 pub previous_region_hash: Hash,
86 /// Entries for each vault modified in this block.
87 pub vault_entries: Vec<VaultEntry>,
88 /// Block creation timestamp.
89 pub timestamp: DateTime<Utc>,
90 /// Raft leader that committed this block.
91 pub leader_id: NodeId,
92 /// Raft term when committed.
93 pub term: u64,
94 /// Raft committed log index.
95 pub committed_index: u64,
96}
97
98/// Per-vault entry within a RegionBlock.
99#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
100pub struct VaultEntry {
101 /// Organization owning this vault.
102 pub organization: OrganizationId,
103 /// Vault identifier.
104 pub vault: VaultId,
105 /// Per-vault height (independent of shard height).
106 pub vault_height: u64,
107 /// Hash of previous vault block.
108 pub previous_vault_hash: Hash,
109 /// Transactions for this vault.
110 pub transactions: Vec<Transaction>,
111 /// Merkle root of transactions.
112 pub tx_merkle_root: Hash,
113 /// State root after applying transactions.
114 pub state_root: Hash,
115}
116
117/// Accumulated cryptographic commitment for a range of blocks.
118///
119/// Proves snapshot lineage without requiring full block replay.
120/// Enables verification continuity even after transaction body compaction.
121#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
122pub struct ChainCommitment {
123 /// Sequential hash chain of all block headers in range.
124 /// Ensures header ordering is preserved and any tampering invalidates chain.
125 pub accumulated_header_hash: Hash,
126
127 /// Merkle root of state_roots in range.
128 /// Enables O(log n) proofs that a specific state_root was in the range.
129 pub state_root_accumulator: Hash,
130
131 /// Start height of this commitment (inclusive).
132 /// 0 for genesis, or previous_snapshot_height + 1.
133 pub from_height: u64,
134
135 /// End height of this commitment (inclusive).
136 /// This is the snapshot's block height.
137 pub to_height: u64,
138}
139
140impl RegionBlock {
141 /// Converts this [`RegionBlock`] to a region-level [`BlockHeader`] for chain commitment
142 /// computation.
143 ///
144 /// Aggregates vault entry Merkle roots into a single header, enabling
145 /// [`ChainCommitment`] computation over the region chain for snapshot verification.
146 pub fn to_region_header(&self) -> BlockHeader {
147 use crate::merkle::merkle_root;
148
149 let (tx_merkle_root, state_root) = if self.vault_entries.is_empty() {
150 (crate::EMPTY_HASH, crate::EMPTY_HASH)
151 } else {
152 let tx_roots: Vec<_> = self.vault_entries.iter().map(|e| e.tx_merkle_root).collect();
153 let state_roots: Vec<_> = self.vault_entries.iter().map(|e| e.state_root).collect();
154 (merkle_root(&tx_roots), merkle_root(&state_roots))
155 };
156
157 BlockHeader {
158 height: self.region_height,
159 organization: OrganizationId::new(0), // Region-level aggregate, not vault-specific
160 vault: VaultId::new(0),
161 previous_hash: self.previous_region_hash,
162 tx_merkle_root,
163 state_root,
164 timestamp: self.timestamp,
165 term: self.term,
166 committed_index: self.committed_index,
167 }
168 }
169
170 /// Extracts a standalone VaultBlock for client verification.
171 ///
172 /// Clients verify per-vault chains and never see [`RegionBlock`] directly.
173 /// Requires organization, vault, and vault height to uniquely identify
174 /// the entry since multiple organizations can share a region.
175 pub fn extract_vault_block(
176 &self,
177 organization: OrganizationId,
178 vault: VaultId,
179 vault_height: u64,
180 ) -> Option<VaultBlock> {
181 self.vault_entries
182 .iter()
183 .find(|e| {
184 e.organization == organization && e.vault == vault && e.vault_height == vault_height
185 })
186 .map(|e| VaultBlock {
187 header: BlockHeader {
188 height: e.vault_height,
189 organization: e.organization,
190 vault: e.vault,
191 previous_hash: e.previous_vault_hash,
192 tx_merkle_root: e.tx_merkle_root,
193 state_root: e.state_root,
194 timestamp: self.timestamp,
195 term: self.term,
196 committed_index: self.committed_index,
197 },
198 transactions: e.transactions.clone(),
199 })
200 }
201}
202
203// ============================================================================
204// Transaction Structures
205// ============================================================================
206
207/// Error during transaction validation.
208#[derive(Debug, snafu::Snafu)]
209#[snafu(visibility(pub))]
210pub enum TransactionValidationError {
211 /// Operations list is empty.
212 #[snafu(display("Transaction must contain at least one operation"))]
213 EmptyOperations,
214
215 /// Sequence number must be positive.
216 #[snafu(display("Transaction sequence must be positive (got 0)"))]
217 ZeroSequence,
218}
219
220/// Transaction containing one or more operations.
221///
222/// Use the builder pattern to construct transactions with validation:
223/// ```no_run
224/// # use inferadb_ledger_types::types::{Transaction, Operation};
225/// # use chrono::Utc;
226/// let tx = Transaction::builder()
227/// .id([1u8; 16])
228/// .client_id("client-123")
229/// .sequence(1)
230/// .operations(vec![Operation::CreateRelationship {
231/// resource: "doc:1".into(),
232/// relation: "owner".into(),
233/// subject: "user:alice".into(),
234/// }])
235/// .timestamp(Utc::now())
236/// .build()
237/// .expect("valid transaction");
238/// ```
239#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
240pub struct Transaction {
241 /// Unique transaction identifier.
242 pub id: TxId,
243 /// Client identifier for idempotency.
244 pub client_id: ClientId,
245 /// Monotonic sequence number per client.
246 pub sequence: u64,
247 /// Operations to apply atomically.
248 pub operations: Vec<Operation>,
249 /// Transaction submission timestamp.
250 pub timestamp: DateTime<Utc>,
251}
252
253#[bon::bon]
254impl Transaction {
255 /// Creates a new transaction with validation.
256 ///
257 /// # Errors
258 ///
259 /// Returns an error if:
260 /// - `operations` is empty
261 /// - `sequence` is zero
262 #[builder]
263 pub fn new(
264 id: TxId,
265 #[builder(into)] client_id: ClientId,
266 sequence: u64,
267 operations: Vec<Operation>,
268 timestamp: DateTime<Utc>,
269 ) -> Result<Self, TransactionValidationError> {
270 snafu::ensure!(!operations.is_empty(), EmptyOperationsSnafu);
271 snafu::ensure!(sequence > 0, ZeroSequenceSnafu);
272
273 Ok(Self { id, client_id, sequence, operations, timestamp })
274 }
275}
276
277/// Mutation operations that can be applied to vault state.
278#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
279pub enum Operation {
280 /// Creates a relationship tuple.
281 CreateRelationship {
282 /// Resource identifier (e.g., "document:123").
283 resource: String,
284 /// Relation name (e.g., "viewer", "editor").
285 relation: String,
286 /// Subject identifier (e.g., "user:456").
287 subject: String,
288 },
289 /// Deletes a relationship tuple.
290 DeleteRelationship {
291 /// Resource identifier.
292 resource: String,
293 /// Relation name.
294 relation: String,
295 /// Subject identifier.
296 subject: String,
297 },
298 /// Sets an entity value with optional condition and expiration.
299 SetEntity {
300 /// Entity key.
301 key: String,
302 /// Entity value (opaque bytes).
303 value: Vec<u8>,
304 /// Optional write condition.
305 condition: Option<SetCondition>,
306 /// Optional Unix timestamp for expiration. `None` means the entry never expires.
307 expires_at: Option<u64>,
308 },
309 /// Deletes an entity.
310 DeleteEntity {
311 /// Entity key to delete.
312 key: String,
313 },
314 /// Expires an entity (GC-initiated, distinct from DeleteEntity for audit).
315 ExpireEntity {
316 /// Entity key that expired.
317 key: String,
318 /// Unix timestamp when expiration occurred.
319 expired_at: u64,
320 },
321}
322
323/// Conditional write predicates for compare-and-swap operations.
324#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
325pub enum SetCondition {
326 /// Key must not exist (0x01).
327 MustNotExist,
328 /// Key must exist (0x02).
329 MustExist,
330 /// Key version must equal specified value (0x03).
331 VersionEquals(u64),
332 /// Key value must equal specified bytes (0x04).
333 ValueEquals(Vec<u8>),
334}
335
336impl SetCondition {
337 /// Returns the condition type byte for encoding.
338 pub fn type_byte(&self) -> u8 {
339 match self {
340 SetCondition::MustNotExist => 0x01,
341 SetCondition::MustExist => 0x02,
342 SetCondition::VersionEquals(_) => 0x03,
343 SetCondition::ValueEquals(_) => 0x04,
344 }
345 }
346}
347
348// ============================================================================
349// Entity Structures
350// ============================================================================
351
352/// Key-value record stored per-vault in the B-tree.
353///
354/// Each entity has a unique key within its vault, an opaque value,
355/// optional TTL expiration, and a monotonic version for optimistic concurrency.
356#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
357pub struct Entity {
358 /// Unique key within the vault, conforming to the validation character whitelist.
359 pub key: Vec<u8>,
360 /// Opaque value bytes. Interpretation is application-defined.
361 pub value: Vec<u8>,
362 /// Unix timestamp for expiration. A value of 0 means the entry never expires.
363 pub expires_at: u64,
364 /// Block height when this entity was last modified.
365 pub version: u64,
366}
367
368/// Relationship tuple (resource, relation, subject).
369#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
370pub struct Relationship {
371 /// Resource identifier (e.g., "doc:123").
372 pub resource: String,
373 /// Relation name (e.g., "viewer").
374 pub relation: String,
375 /// Subject identifier (e.g., "user:alice").
376 pub subject: String,
377}
378
379impl Relationship {
380 /// Creates a new authorization tuple linking a resource, relation, and subject.
381 pub fn new(
382 resource: impl Into<String>,
383 relation: impl Into<String>,
384 subject: impl Into<String>,
385 ) -> Self {
386 Self { resource: resource.into(), relation: relation.into(), subject: subject.into() }
387 }
388
389 /// Encodes relationship as a canonical string key.
390 pub fn to_key(&self) -> String {
391 format!("rel:{}#{}@{}", self.resource, self.relation, self.subject)
392 }
393}
394
395// ============================================================================
396// Write Result
397// ============================================================================
398
399/// Aggregate result of a committed write request, including per-operation statuses.
400#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
401pub struct WriteResult {
402 /// Block height where the write was committed.
403 pub block_height: u64,
404 /// Block hash.
405 pub block_hash: Hash,
406 /// Status of each operation.
407 pub statuses: Vec<WriteStatus>,
408}