Skip to content

Commit 539680d

Browse files
committed
Add blake3 object key hashing functions
1 parent 6a41509 commit 539680d

7 files changed

Lines changed: 562 additions & 1 deletion

File tree

Cargo.lock

Lines changed: 141 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,6 @@ keywords = ["storage", "distributed", "leveldb", "object-store"]
1414
categories = ["database", "network-programming"]
1515

1616
[workspace.dependencies]
17+
thiserror = "2"
18+
base64 = "0.22"
19+
blake3 = "1.8"

minikv-core/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,6 @@ keywords.workspace = true
1111
categories.workspace = true
1212

1313
[dependencies]
14+
thiserror.workspace = true
15+
base64 = { workspace = true }
16+
blake3 = { workspace = true }

minikv-core/src/hashing.rs

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
use base64::{Engine as _, engine::general_purpose::STANDARD as B64_STANDARD};
2+
3+
/// Convert a raw key into a stable, deterministic storage path for volume servers.
4+
///
5+
/// Algorithm:
6+
/// 1. Compute BLAKE3-256 hash of `key`.
7+
/// 2. Use the first two bytes as two hex directory components (fanout = 256²).
8+
/// 3. Base64-encode the raw key as the filename.
9+
///
10+
/// Format: `/%02x/%02x/<base64-key>`
11+
///
12+
/// # Stability
13+
/// Changing this function after data is stored is a breaking change.\
14+
/// Stored keys map to different paths if the algorithm is modified, requiring a full rebalance.
15+
#[must_use]
16+
pub fn key_to_path(key: &[u8]) -> String {
17+
let hash = blake3::hash(key);
18+
let b = hash.as_bytes();
19+
let b64 = B64_STANDARD.encode(key);
20+
format!("/{:02x}/{:02x}/{}", b[0], b[1], b64)
21+
}
22+
23+
/// Compute a per-volume BLAKE3 score for a key.
24+
///
25+
/// Combines the key bytes and the volume name to produce a 32-byte hash suitable
26+
/// for consistent shard placement.
27+
#[must_use]
28+
pub fn volume_score(key: &[u8], volume: &str) -> [u8; 32] {
29+
let mut hasher = blake3::Hasher::new();
30+
hasher.update(key);
31+
hasher.update(volume.as_bytes());
32+
*hasher.finalize().as_bytes()
33+
}
34+
35+
#[cfg(test)]
36+
mod tests {
37+
use super::*;
38+
39+
#[test]
40+
fn key_to_path_stability_hello() {
41+
let path = key_to_path(b"hello");
42+
assert_eq!(path, "/ea/8f/aGVsbG8=");
43+
}
44+
45+
#[test]
46+
fn key_to_path_stability_helloworld() {
47+
let path = key_to_path(b"helloworld");
48+
let parts: Vec<&str> = path.splitn(4, '/').collect();
49+
assert_eq!(parts.len(), 4);
50+
assert_eq!(parts[1].len(), 2);
51+
assert_eq!(parts[2].len(), 2);
52+
let decoded = B64_STANDARD.decode(parts[3]).unwrap();
53+
assert_eq!(decoded, b"helloworld");
54+
}
55+
56+
#[test]
57+
fn key_to_path_snapshot_helloworld() {
58+
let path = key_to_path(b"helloworld");
59+
let hash = blake3::hash(b"helloworld");
60+
let b = hash.as_bytes();
61+
let expected = format!("/{:02x}/{:02x}/aGVsbG93b3JsZA==", b[0], b[1]);
62+
assert_eq!(path, expected);
63+
}
64+
65+
#[test]
66+
fn key_to_path_empty_key() {
67+
let path = key_to_path(b"");
68+
let parts: Vec<&str> = path.splitn(4, '/').collect();
69+
assert_eq!(parts.len(), 4);
70+
}
71+
72+
#[test]
73+
fn key_to_path_binary_key() {
74+
let key: Vec<u8> = (0u8..=255).collect();
75+
let path = key_to_path(&key);
76+
let parts: Vec<&str> = path.splitn(4, '/').collect();
77+
assert_eq!(parts.len(), 4);
78+
let decoded = B64_STANDARD.decode(parts[3]).unwrap();
79+
assert_eq!(decoded, key);
80+
}
81+
}

minikv-core/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
1+
pub mod hashing;
2+
pub mod volumes;

0 commit comments

Comments
 (0)