| Crates.io | bitcoinleveldb-skiplist |
| lib.rs | bitcoinleveldb-skiplist |
| version | 0.1.19 |
| created_at | 2023-01-18 10:34:21.31732+00 |
| updated_at | 2025-12-01 17:55:12.005326+00 |
| description | Arena-backed LevelDB-style skip list for Copy keys with custom comparators, providing lock-free read-friendly ordered storage for internal database and storage engine use. |
| homepage | |
| repository | https://github.com/klebs6/bitcoin-rs |
| max_upload_size | |
| id | 761634 |
| size | 242,033 |
A faithful, arena-backed translation of LevelDB's internal SkipList<Key, Comparator> to safe(ish) Rust, intended for use inside high‑throughput storage engines such as LevelDB clones, LSM‑trees, or log‑structured key–value stores.
This crate packages the skip list used by the bitcoinleveldb components of bitcoin-rs. It mirrors the invariants, complexity properties, and memory layout of the original C++ implementation closely enough to be used as a drop‑in internal building block.
Arena, matching LevelDB's custom allocator usage.#[repr(C)] node layout with a flexible tail for next pointers, equivalent to a C++ FAM (flexible array member).SkipListComparator<K> trait that returns < 0, 0, or > 0, and a blanket impl for Fn(&K, &K) -> i32 + Send + Sync.Acquire/Release/Relaxed) to permit concurrent readers in typical LevelDB usage patterns.SkipListIterator exposes seek, seek_to_first, seek_to_last, next, and prev, plus valid/key.RandomHeight() with branching factor 4, as in the original LevelDB skip list.This crate intentionally caters to consumers who already understand LevelDB's concurrency model and are willing to work near raw pointers and arenas for maximal control.
A skip list is an ordered probabilistic data structure that supports search, insertion, and membership queries with expected logarithmic time complexity, comparable to balanced binary search trees, but usually easier to implement and more cache‑friendly.
C: SkipListComparator<K>.h ∈ [1, H_max].0 forms a sorted singly linked list of all nodes.Heights are chosen independently with geometric distribution: with branching factor 4, each additional level is present with probability 1/4, up to SkipListMaxHeight. This yields an expected complexity of O(log n) for search and insertion, with high probability bounds similar to balanced trees.
The design follows LevelDB's single writer + multiple lock‑free readers model:
Release stores to publish new nodes.Acquire loads, observing a consistent, fully‑initialized view of any node they reach.If you follow the original LevelDB pattern (single mutable owner for insertions, many readers with only shared references), the atomic semantics in this crate are designed to be correct.
SkipListComparator<K>pub trait SkipListComparator<K> {
/// Compare two keys. Return < 0 if a < b, 0 if a == b, > 0 if a > b.
fn compare(&self, a: &K, b: &K) -> i32;
}
This trait abstracts key ordering. Semantics mirror LevelDB:
K.SkipList assume the comparator is consistent and transitive.A blanket implementation is provided for any function or closure of type Fn(&K, &K) -> i32 + Send + Sync, allowing you to use simple lambdas as comparators.
SkipListNode<K>#[repr(C)]
pub struct SkipListNode<K>
where
K: Copy + Default,
{
key: K,
next: [AtomicPtr<SkipListNode<K>>; 1],
}
This internal node struct is arranged so that, when allocated via the arena, it can reserve additional trailing storage for higher levels' next pointers (like a flexible array member). You do not normally interact with SkipListNode directly, but its methods are:
key_ref(&self) -> &K: Borrow the stored key.next(level: i32) -> *mut Self: Load next pointer at a level with Acquire semantics.set_next(level: i32, node: *mut Self): Store next with Release semantics.no_barrier_next(level: i32), no_barrier_set_next(level: i32, node): Same, but with Relaxed orderings.These are used internally by the SkipList to maintain lock‑free traversal guarantees.
SkipList<K, C>#[derive(Getters, MutGetters)]
#[getset(get = "pub", get_mut = "pub")]
pub struct SkipList<K, C>
where
K: Debug + Copy + Default,
C: SkipListComparator<K>,
{
compare: C,
arena: *mut Arena,
head: *mut SkipListNode<K>,
max_height: AtomicI32,
rnd: Random,
}
Key constraints:
K: Copy + Default: The key must be trivially copyable; Default is used for the dummy head node.K: Debug: Only required for logging, assertions, and tracing.C: SkipListComparator<K>: Provides ordering.Important methods:
pub fn new(compare: C, arena: *mut Arena) -> Selfpub fn insert(&mut self, key: K)pub fn contains(&self, key: &K) -> boolpub fn get_max_height(&self) -> i32Low‑level navigation primitives (intended for internal / advanced users):
find_greater_or_equal(&self, key: &K, prev: Option<&mut [*mut SkipListNode<K>]>) -> *mut SkipListNode<K>find_less_than(&self, key: &K) -> *mut SkipListNode<K>find_last(&self) -> *mut SkipListNode<K>SkipListIterator<'a, K, C>pub struct SkipListIterator<'a, K, C>
where
K: Debug + Copy + Default,
C: SkipListComparator<K>,
{
list: &'a SkipList<K, C>,
node: *mut SkipListNode<K>,
}
This iterator is intentionally copyable and holds:
Methods mirror LevelDB's iterator interface:
pub fn new(list: &'a SkipList<K, C>) -> Selfpub fn valid(&self) -> boolpub fn key(&self) -> Kpub fn next(&mut self)pub fn prev(&mut self)pub fn seek(&mut self, target: &K)pub fn seek_to_first(&mut self)pub fn seek_to_last(&mut self)In your Cargo.toml:
[dependencies]
bitcoinleveldb-skiplist = "0.1.19"
You must provide:
Arena (from the surrounding bitcoinleveldb or storage engine implementation).C, usually as a closure.use bitcoinleveldb_skiplist::{SkipList, SkipListComparator};
// Pseudocode: obtain a mutable pointer to an Arena from your system.
let arena: *mut Arena = obtain_arena();
// Comparator: ascending `u64` keys.
let cmp = |a: &u64, b: &u64| {
if *a < *b { -1 } else if *a > *b { 1 } else { 0 }
};
let mut list: SkipList<u64, _> = SkipList::new(cmp, arena);
list.insert(10);
list.insert(20);
list.insert(15);
assert!(list.contains(&10));
assert!(!list.contains(&42));
Note that Arena is not defined in this crate; it is taken as a raw pointer so that the skip list can integrate with an external allocator with custom alignment and lifetime guarantees. This matches LevelDB's design: the arena is long‑lived and freed only when the entire structure is discarded.
let mut it = bitcoinleveldb_skiplist::SkipListIterator::new(&list);
// Forward iteration from the smallest key
it.seek_to_first();
while it.valid() {
let key = it.key();
// process key...
it.next();
}
// Seek and scan from an arbitrary key
let mut it = bitcoinleveldb_skiplist::SkipListIterator::new(&list);
it.seek(&15);
if it.valid() {
assert!(it.key() >= 15);
}
// Reverse traversal
it.seek_to_last();
while it.valid() {
let key = it.key();
// process key from high to low...
it.prev();
}
The iterator intentionally uses raw pointers and assert!s to catch misuse, rather than returning Option<K> from every operation. You must respect the preconditions documented in the method comments:
key, next, prev require valid() == true.Keys must be Copy + Default + Debug. A typical pattern is to use integer or fixed‑size types (e.g., hashes, pointers, or small structs).
Example with a fixed‑size 32‑byte key:
#[derive(Copy, Clone, Default, Debug, Eq, PartialEq)]
pub struct Key32([u8; 32]);
impl Key32 {
fn cmp(&self, other: &Self) -> i32 {
for (a, b) in self.0.iter().zip(other.0.iter()) {
if a < b { return -1; }
if a > b { return 1; }
}
0
}
}
let arena: *mut Arena = obtain_arena();
let cmp = |a: &Key32, b: &Key32| a.cmp(b);
let mut list: SkipList<Key32, _> = SkipList::new(cmp, arena);
let key = Key32([0; 32]);
list.insert(key);
assert!(list.contains(&key));
This crate operates close to the metal:
SkipList and SkipListIterator store raw pointers into an external arena.SkipList and all iterators.arena: *mut Arena supplied to SkipList::new must remain valid and must not be freed or repurposed while any skip list node is in use.a, b, c, the comparator must be transitive and antisymmetric. Breaking this assumption invalidates search and insertion logic.insert operations from a single thread or otherwise serialize writes logically; concurrent writers are not supported by the current algorithm.SkipList while traversing; they must not mutate the data structure.Violating these invariants can lead to undefined behavior due to the use of unsafe code and raw pointers.
The skip list uses the classical geometric tower height distribution with branching factor 4. Under standard probabilistic assumptions:
find_greater_or_equal, contains: O(log n) expected time.insert: O(log n) expected time, dominated by search for insertion points.The arena allocator reduces per‑node allocation overhead, improving spatial locality and cache performance. The skip list is well‑suited to workloads where:
bitcoin-rsThis crate lives in the bitcoin-rs repository and is primarily intended as an internal building block for the LevelDB‑like storage layer used by that project. Nonetheless, it is packaged as a separate crate so it can be reused by other storage engines or experimental systems that require:
SkipList behavior.When integrating into other systems, you will typically:
Arena abstraction comparable to the one used in bitcoin-rs.K that satisfies Copy + Default + Debug.This crate is licensed under the MIT license.
See the repository for full license text and any additional notices.