Skip to main content

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}