| Crates.io | bitcoinleveldb-cache |
| lib.rs | bitcoinleveldb-cache |
| version | 0.1.19 |
| created_at | 2023-01-18 04:46:28.430134+00 |
| updated_at | 2025-12-01 16:58:01.978712+00 |
| description | Low-level, Bitcoin Core–compatible in-memory cache for LevelDB-style storage, providing capacity-bounded LRU eviction, explicit charge-based memory accounting, and opaque pointer values with custom deleters for FFI-heavy systems. |
| homepage | |
| repository | https://github.com/klebs6/bitcoin-rs |
| max_upload_size | |
| id | 761508 |
| size | 225,313 |
A faithful, test-focused Rust port of the cache subsystem used by Bitcoin Core's LevelDB fork. It provides an internal, concurrency-safe caching layer with explicit control over memory charge, eviction, and lifecycle semantics, suitable for embedding under higher-level storage engines.
bitcoinleveldb-cache exposes a low-level cache abstraction that mimics the behavior and semantics of the original C++ LevelDB/Bitcoin cache implementation. The primary design goals are:
charge for each value.*mut c_void) for maximal flexibility and interoperability with FFI-heavy or legacy code.The crate is primarily concerned with providing the cache core that Bitcoin's LevelDB adapter depends upon. It is not a general high-level Rust cache with ownership-safe value types; instead, it mirrors the original memory and pointer semantics used by Bitcoin's tests, including custom deleters and test fixtures.
The cache API is specified as a trait composition. Any type implementing all the following traits automatically implements the marker trait CacheInterface:
CacheInsertCacheLookupCacheReleaseCacheValueCacheEraseCacheNewIdCachePruneCacheTotalChargeThis enables alternative cache implementations (e.g., scan-resistant or different eviction policies) while maintaining a common API boundary.
pub trait CacheInsert {
fn insert(
&mut self,
key_: &Slice,
value: *mut c_void,
charge: usize,
deleter: CacheDeleterFn,
) -> *mut CacheHandle;
}
pub trait CacheLookup {
fn lookup(&mut self, key_: &Slice) -> *mut CacheHandle;
}
pub trait CacheRelease {
fn release(&mut self, handle: *mut CacheHandle);
}
pub trait CacheValue {
fn value(&mut self, handle: *mut CacheHandle) -> *mut c_void;
}
pub trait CacheErase {
fn erase(&mut self, key_: &Slice);
}
pub trait CacheNewId {
fn new_id(&mut self) -> u64;
}
pub trait CachePrune {
fn prune(&mut self);
}
pub trait CacheTotalCharge {
fn total_charge(&self) -> usize;
}
pub type CacheDeleterFn = fn(key_: &Slice, value: *mut c_void);
CacheInterface is then defined as:
impl<T> CacheInterface for T where
T: CacheInsert
+ CacheLookup
+ CacheRelease
+ CacheValue
+ CacheErase
+ CacheNewId
+ CachePrune
+ CacheTotalCharge {}
The main production implementation is the Cache type backed by a CacheRep struct. It is instantiated via:
pub fn new_lru_cache(capacity: usize) -> *mut Cache
capacity is expressed in abstract charge units, not entry count.charge contributes to the aggregate usage of the cache.usage > capacity, entries are evicted according to an approximate least-recently-used policy.Internally:
Cache stores Rc<RefCell<CacheRep>>, making it cloneable and internally synchronized at the Rust level.CacheRep manages a HashMap<Vec<u8>, *mut CacheHandle> that maps serialized keys to handle pointers.clock (u64) and last_use timestamps inside each CacheHandle. Candidates with refs == 1 and the oldest last_use are selected for eviction.Each cache lookup/insert returns an opaque pointer to a CacheHandle:
#[derive(Getters, Setters, Builder)]
pub struct CacheHandle {
key: Vec<u8>,
value: *mut c_void,
deleter: CacheDeleterFn,
charge: usize,
refs: u32,
in_cache: bool,
last_use: u64,
}
Important invariants:
refs counts strong references, including the internal cache reference if in_cache == true.CacheRelease::release when done with a handle, decrementing refs.refs reaches 0, the following occur:
deleter is invoked with the Slice-view of key and the stored value pointer.CacheHandle itself is deallocated.This closely replicates the original C++ pointer-based design while making it explicit in Rust. Misuse (forgetting release or double-releasing) will generally show up as incorrect reference counts and log messages.
A number of helpers are provided for converting between integer keys and the Slice/byte representation used internally:
pub fn encode_key(k: i32) -> Vec<u8>;
pub fn decode_key(k: &Slice) -> i32;
PutFixed32.decode_key asserts that the key size is exactly 4 bytes and then reverses the encoding.For values, two helpers exist primarily for tests:
pub fn decode_value(v: *mut c_void) -> i32;
pub fn encode_value(v: uintptr_t) -> *mut c_void;
decode_value expects a pointer to a CacheTestValue struct, which is test-only payload carrying both the owning fixture and a logical i32 value.
The cache never assumes ownership of your values beyond calling the deleter you provide at insertion time. This enables complex lifetime management across FFI boundaries.
In tests, the deleter is:
impl CacheTest {
pub fn deleter(key_: &Slice, v: *mut c_void) {
// Cast `v` to `Box<CacheTestValue>`, record deleted key/value on fixture,
// then drop the box exactly once.
}
}
In production use, you will typically wrap your data structures in a Box and provide a deleter that reconstructs the box, performs any side effects, and then allows Rust to drop the object.
[dependencies]
bitcoinleveldb-cache = "0.1.19"
use bitcoinleveldb_cache::{new_lru_cache, CacheInterface, CacheInsert, CacheLookup, CacheRelease, CacheValue, CacheErase};
use std::ffi::c_void;
fn example_usage() {
// Capacity in arbitrary charge units; choose according to your accounting model.
let capacity: usize = 1024 * 1024; // e.g. 1 MiB equivalent
// Allocate a new LRU cache on the heap.
let cache_ptr = unsafe { new_lru_cache(capacity) };
// SAFETY: `cache_ptr` is owned here; ensure you eventually reconstruct the Box and drop it.
let cache: &mut bitcoinleveldb_cache::Cache = unsafe { &mut *cache_ptr };
// Application-specific: encode a key into bytes then into the `Slice` type used by the cache.
let key_bytes = bitcoinleveldb_cache::encode_key(42);
let key_slice = bitcoinleveldb_cache::Slice::from_ptr_len(key_bytes.as_ptr(), key_bytes.len());
// Suppose we have some heap-allocated payload.
let payload = Box::new(MyPayload { /* ... */ });
let payload_ptr: *mut c_void = Box::into_raw(payload) as *mut c_void;
// Define a deleter that recovers the Box and drops it.
fn my_deleter(_key: &bitcoinleveldb_cache::Slice, value: *mut c_void) {
if value.is_null() {
return;
}
unsafe {
let _: Box<MyPayload> = Box::from_raw(value as *mut MyPayload);
// Dropped on scope exit.
}
}
// Insert into the cache with an explicit charge.
let charge = 128usize; // e.g. approximate size in bytes
let handle = cache.insert(&key_slice, payload_ptr, charge, my_deleter);
// Immediately release our reference if we only want the cache to own it.
cache.release(handle);
// Later: look up the entry.
let lookup_handle = cache.lookup(&key_slice);
if !lookup_handle.is_null() {
let value_ptr = cache.value(lookup_handle);
// SAFETY: You must know the actual type; here we re-cast to `MyPayload`.
let payload_ref: &mut MyPayload = unsafe { &mut *(value_ptr as *mut MyPayload) };
// Use `payload_ref`...
// Release when done.
cache.release(lookup_handle);
}
// When completely finished with the cache, reconstruct the Box and drop it.
unsafe {
let boxed_cache = Box::from_raw(cache_ptr);
drop(boxed_cache); // triggers Cache::drop, which clears entries and calls deleters
}
}
struct MyPayload {
// your data
}
Mathematically, you can think of the cache as maintaining:
The invariant is that ideally ( U \leq C ). When an insert causes ( U > C ), the cache repeatedly selects eviction candidates according to an approximate LRU order until ( U \leq C ) or no evictable entries remain.
Eviction candidates must satisfy:
in_cache == true, i.e., the cache still owns a reference.refs == 1, meaning no other external user currently holds the handle.Among such candidates, the one with smallest last_use (oldest) is removed first, implementing a discrete-time LRU policy.
This behavior matches a widely used pattern in storage engines: approximate LRU policies are usually sufficient to avoid pathological reuse patterns while keeping implementation complexity low.
The CachePrune trait allows manual compaction of entries that are not actively pinned by clients:
use bitcoinleveldb_cache::CachePrune;
fn prune(cache: &mut bitcoinleveldb_cache::Cache) {
cache.prune();
}
prune will remove all entries where in_cache == true and refs == 1, freeing associated memory and invoking deleters. This is particularly useful for tests and memory-pressure simulations.
You can inspect the total accumulated charge at any time:
use bitcoinleveldb_cache::CacheTotalCharge;
fn current_usage(cache: &bitcoinleveldb_cache::Cache) -> usize {
cache.total_charge()
}
This is typically used by higher layers to monitor memory consumption and tune capacity or charge models.
The crate includes a dedicated test harness with the following auxiliary types:
CacheTestValue — an internal payload type storing:
fixture: *mut CacheTestvalue: i32CacheTest — a fixture that owns a cache pointer and records deleted keys/values.
These are used to validate the correctness of deletion, eviction, and reference counting by:
CacheTestValues into the cache.Utility methods such as CacheTest::insert, CacheTest::lookup, CacheTest::erase, and CacheTest::insert_and_return_handle are provided to reproduce the original C++ test matrix as closely as possible. Production users should not rely on these types, but they are illustrative of correct integration with the low-level API.
This crate intentionally operates at a low level:
*mut c_void).lookup / release pairs.*mut c_void is unsafe and must be validated by the caller.When embedding in a larger Rust system, consider:
charge as the approximate heap footprint (e.g., length of byte buffers, plus metadata) to keep eviction behavior aligned with real memory usage.In FFI contexts (e.g., when interoperating with C or C++ code derived from Bitcoin Core), this crate's API is intentionally close to the original patterns and should integrate straightforwardly.
The implementation uses the log crate macros (trace!, debug!, info!, warn!, error!) extensively to describe internal state transitions:
in_cache flags.For meaningful diagnostics during integration or testing, configure a logger (e.g., env_logger, tracing-log) in your binary or test harness.
bitcoin-rsThis crate lives within the https://github.com/klebs6/bitcoin-rs repository and is primarily designed to support the LevelDB-esque storage components there. It aims for behavioral fidelity with the upstream Bitcoin Core cache implementation while exposing a Rust interface suitable for systems-level work on UTXO sets, block indices, and related structures.
This crate is licensed under the MIT license, consistent with the broader bitcoin-rs project.
See the repository for the full license text and attribution details.