kcode-k1-transaction-store 0.1.0

Stores immutable K1 transaction bytes in sector-aligned local files
Documentation
# Consumer contract

```rust
use std::path::Path;

pub const TX_ID_BYTES: usize = 12;
pub const SECTOR_BYTES: u64 = 4_096;
pub const INLINE_LIMIT: usize = 262_144;

#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct TxId;

impl TxId {
    pub const fn from_bytes(bytes: [u8; TX_ID_BYTES]) -> Self;
    pub const fn as_bytes(&self) -> &[u8; TX_ID_BYTES];
    pub const fn into_bytes(self) -> [u8; TX_ID_BYTES];
    pub fn for_transaction(transaction: &[u8]) -> Self;
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PutOutcome {
    Inserted(TxId),
    Duplicate(TxId),
}

#[derive(Debug)]
pub enum StoreError {
    Io(std::io::Error),
    AlreadyExists,
    InvalidStore,
    StoreFull,
    IdCollision(TxId),
    OutcomeUnknown(TxId),
    ReopenRequired,
    CorruptTransaction(TxId),
}

pub struct TransactionStore;

impl TransactionStore {
    pub fn create(root: &Path) -> Result<Self, StoreError>;
    pub fn open(root: &Path) -> Result<Self, StoreError>;
    pub fn put(&self, transaction: &[u8]) -> Result<PutOutcome, StoreError>;
    pub fn contains(&self, id: TxId) -> bool;
    pub fn get(&self, id: TxId) -> Result<Option<Vec<u8>>, StoreError>;
}

impl std::fmt::Display for StoreError;
impl std::error::Error for StoreError;
impl From<std::io::Error> for StoreError;
```

`TxId` is the first 12 bytes of SHA-256 over the exact transaction. Empty transactions are valid. An already committed equal ID is compared byte for byte: equal bytes return `Duplicate`, while different bytes return `IdCollision`. Deliberately constructing different content with the same 96-bit ID, including concurrent colliding puts, is outside this non-Byzantine store's supported domain.

The private root contains `transactions.dat`, `lookup.dat`, and `payload/<first 2>/<remaining 14>.dat`, where the payload components are the URL-safe unpadded Base64 form of the 12-byte ID. `create` materializes all 4,096 possible two-character payload shard directories and synchronizes `payload/` before creating `transactions.dat`. Shard membership never changes afterward. Inputs through 262,144 bytes are stored inline. Larger inputs use one directly written payload file and one external record; the containing shard is synchronized after a new payload file is written.

Every transaction record begins at a 4,096-byte sector. An inline record is a one-byte zero kind, little-endian `u64` length, exact bytes, and zero padding through its allocated sectors. An external record is one full sector containing a one-byte kind of one and the length. `lookup.dat` is only consecutive 16-byte entries containing a 12-byte ID and little-endian `u32` sector index. It is loaded completely into an in-memory map at open. A partial final lookup entry is discarded; transaction and payload files are not scanned or repaired.

`put` accepts only fully resident bytes. It hashes first, allocates disjoint sectors under a brief counter lock, writes positionally, synchronizes data, then directly appends and synchronizes one lookup entry before publishing the map entry. Concurrent equal transactions may leave unused records or payload bytes; final publication returns `Duplicate`. No orphan is reclaimed. A lookup-publication failure returns `OutcomeUnknown` and disables later puts through that handle with `ReopenRequired`; reopening resolves complete entries. `Inserted` is returned only after payload, transaction record, and lookup entry durability.

`get` returns `None` for an absent ID. Otherwise it allocates and returns the complete transaction, hashes it once, and returns `CorruptTransaction` for an invalid record, wrong length, missing payload, or ID mismatch. `contains` is only an in-memory lookup.

`TransactionStore` is `Send + Sync`. Concurrent calls through one instance are supported. Hashing, transaction writes, payload writes, and reads are independent; only sector allocation and final lookup publication are serialized. The package starts no threads and has no queue, batching, polling, retry, flush, close, maintenance, streaming, deletion, or compaction API. `Drop` performs no durability work. Concurrent writers through separately opened instances or processes, external file mutation, network filesystems, transactions too large for memory, and adversarial hash collisions are unsupported.

`open` performs O(number of lookup entries) sequential work and memory without reading transaction bytes. `contains` has average O(1) work and no I/O. `put` and `get` perform O(transaction length) work; `get` allocates exactly the returned transaction length, while an inline put allocates its sector-padded record and an external put does not make another transaction-sized copy. Filesystem operations have no timeout and may block with the local device. The store supports at most 2^32 transaction-file sectors, or 16 TiB.