Urkel Tree: Cryptographically Provable Key-Value Store
Urkel Tree is an optimized, cryptographically provable key-value store implemented as a base-2 merkelized trie, specifically designed for the Handshake protocol. It significantly outperforms alternatives like Ethereum's base-16 trie by operating as its own database, storing nodes in flat, append-only files for snapshotting and crash consistency. The current stable version is 1.0.3, with releases focusing on stability, minor bug fixes, and internal optimizations rather than rapid feature additions. Key differentiators include its architectural simplicity (only two node types), compact internal node size (76 bytes), and extremely small proof sizes (32 bytes per sibling node), which are crucial for maintaining proof sizes under 1KB even with millions of leaves. It offers fully transactional capabilities and inherently provides history independence and non-destruction properties. A critical usage requirement is that Urkel must be used with uniformly distributed keys, typically achieved through hashing. While compaction is available, it is currently inefficient and manual, with future C implementation planned for optimization.
Common errors
-
Could not verify proof: %s.
cause The cryptographic proof generated for a key-value pair failed verification against the tree's root, key, hash function, or bit depth.fixCheck that the `root`, `key`, `hash` function (e.g., BLAKE2b), and `bits` (key size) passed to `proof.verify()` exactly match those used during tree creation and snapshot generation. Also, ensure the key being proved actually exists or its absence is correctly expected. -
TypeError: require is not a function
cause Attempting to use `require()` in an ES module context or a browser environment without proper CommonJS polyfills/bundling.fixEnsure your Node.js project is configured to run as a CommonJS module (e.g., omit `"type": "module"` in `package.json` or rename files to `.cjs`). For browser usage, use a bundler like Webpack or Rollup configured for CommonJS.
Warnings
- gotcha Urkel compaction is currently inefficient and requires manual user intervention. It is not designed for frequent execution in consensus applications and will be optimized in a future C implementation.
- breaking Only the 'radix' variant of the Urkel Tree is currently maintained. Older 'trie' and 'optimized' backends were removed in version 1.0.0. Using or referencing these deprecated variants will result in errors.
- gotcha Urkel Tree requires uniformly distributed keys for optimal performance and correctness. The documentation explicitly states it 'should only be used with uniformly distributed keys (i.e. hashed)'.
Install
-
npm install urkel -
yarn add urkel -
pnpm add urkel
Imports
- Tree
import { Tree } from 'urkel';const { Tree } = require('urkel'); - Proof
import { Proof } from 'urkel';const { Proof } = require('urkel'); - BLAKE2b
import { BLAKE2b } from 'bcrypto';const { BLAKE2b } = require('bcrypto');
Quickstart
const bcrypto = require('bcrypto');
const urkel = require('urkel');
const {BLAKE2b, randomBytes} = bcrypto;
const {Tree, Proof} = urkel;
async function runUrkelExample() {
// Create a tree using blake2b-256 with a 256-bit key size.
// Specify a unique prefix path for the database files.
const tree = new Tree({
hash: BLAKE2b,
bits: 256,
prefix: './urkel-db-example'
});
await tree.open();
let keyToProve;
// Transactions allow atomic operations on the tree.
const txn = tree.transaction();
// Insert 500 random key-value pairs.
for (let i = 0; i < 500; i++) {
const k = randomBytes(32); // 32 bytes for 256 bits
const v = randomBytes(300);
await txn.insert(k, v);
if (i === 250) keyToProve = k; // Save one key for proof
}
// Commit the transaction and retrieve the new root hash.
const root = await txn.commit();
const snapshot = tree.snapshot(root);
// Generate a cryptographic proof for the saved key.
const proof = await snapshot.prove(keyToProve);
// Verify the generated proof against the root, key, hash, and bits.
const [code, value] = proof.verify(root, keyToProve, BLAKE2b, 256);
if (code !== 0) {
console.log('Could not verify proof: %s.', Proof.code(code));
} else if (value) {
console.log('Valid proof for %s, value: %s', keyToProve.toString('hex'), value.toString('hex'));
} else {
console.log('Absence proof for %s.', keyToProve.toString('hex'));
}
// Close the tree when done to release file handles.
await tree.close();
}
runUrkelExample().catch(console.error);