Node.js bindings for Merk, a fast Merkle tree library built on RocksDB.
npm install merk
let { Merk, verifyProof, restore } = require('merk')
let keys = [Buffer.from('key1'), Buffer.from('key2')]
let values = [Buffer.from('value1'), Buffer.from('value2')]
// create or load store
let db = Merk('./state.db')
// write some values
db.batch()
.put(keys[0], values[0])
.put(keys[1], values[1])
.commitSync()
// get a value
let value = db.getSync(keys[0])
// get the Merkle root
let hash = db.rootHash()
// create a merkle proof
let proof = db.proveSync(keys)
// verify a merkle proof
let proofResult = verifyProof(proof, keys, hash)
// close Merk and the underlying RocksDB instance
db.close()let { Merk, verifyProof, restore } = require('merk')Create or open an existing Merk by file path. path is a string.
Synchronously fetch a value from the db by its key. key is a buffer.
value is a buffer containing the value. This method throws if the the key isn't
found in the database.
Computes the Merkle root of the underlying tree. hash is a 20-byte buffer.
Returns the number of chunks required to prove the current tree.
Returns the chunk proof for the given index.
index is a number and must be less than db.numChunks(). chunk is a buffer.
Forces RocksDB to flush data to disk. You probably shouldn't need to do this manually.
Gracefully shut down Merk and RocksDB before a process exit, say.
Closes the database and removes all stored data from disk. Useful for cleaning up checkpoints.
Create a checkpoint at the desired path. A checkpoint is an immutable view of the current tree state.
Creating a checkpoint is a cheap operation because, under the hood, RocksDB is
just creating some symlinks. You may open the checkpoint at this path as a
full-blown Merk: let db2 = Merk(checkpointPath).
path is a string, and this method will throw if
anything already exists at this path.
Batches accumulate put and delete operations to be atomically committed to
the database.
Creates an empty batch, which can be used to build an atomic set of writes and deletes.
Adds a put operation to the batch. key and value are buffers. The write is
executed after the batch is committed.
Returns the batch instance, so you may chain multiple .put() or .delete() calls.
Adds a delete operation to the batch, removing the database entry for the key.
key is a buffer. Deletes happen when the batch is committed.
Synchronously execute all put and delete operations in this batch, writing
them to the database.
Generate a merkle proof for some keys. keys is an array of buffers.
The returned proof is a buffer, which can be verified against an expected root
hash to retrieve the proven values.
Verify a Merkle proof against an expected root hash. proof is a buffer, keys
is an array of buffers, and hash is a buffer.
The returned result is an array of buffers corresponding to values for each key,
in the same order as the provided keys.
Throws if any of the provided keys can't be proven.
The restore() function can be used to reconstruct a Merk from chunk proofs.
See the State Sync section below for more details on how to integrate this
with a state machine replication engine like Tendermint.
Create and returns a Restorer instance, which can sequentially process chunks to
reconstruct a Merk.
path is a string where you'd like to create the database. Throws if anything exists at this path already.
hash is the expected root hash of the tree you're restoring.
numChunks is a number, how many chunks you're expecting to process. Throws if
you process more than this many chunks.
Process a chunk proof, as returned from db.getChunkSync(). chunk is a
buffer.
Chunks must be processed in-order. Read more in the State Sync section below.
Throws if the chunk isn't valid.
Synchronously finalizes the restored Merk, which can then be opened safely with
the Merk constructor.
Must be called after all chunks have been processed.
If you're using Merk, there's a good chance you're also using Tendermint. This section will give you a quick overview on how the pieces fit together to accomplish state sync with Tendermint and Merk.
First you'll need to decide on a strategy for when to create snapshots (and their associated Merk checkpoints). Maybe each block, maybe every 100 blocks, etc.
To create a snapshot, you'll do something like this:
let snapshotMeta = {}
let height = 200
let checkpointPath = `./path/to/merk-${height}.db`
db.checkpoint(checkpointPath)
snapshotMeta[height] = {
checkpointPath,
hash: db.rootHash(),
chunks: db.numChunks(),
}You'll need to refer back to this snapshotMeta object in a few of the following ABCI messages.
When you receive the ListSnapshots ABCI message, send back an array of the
snapshotMeta objects from the above section, plus other data for the
Snapshot data
fields for metadata and format, which will depend on your application.
Tendermint is requesting a specific chunk from a snapshot you listed in your
ListSnapshots response.
Load the checkpoint and fetch the requested chunk like this:
let { Merk } = require('merk')
// `chunk` is the index of the requested chunk within the snapshot for this height
let { height, chunk } = loadSnapshotChunkRequest
let snapshot = snapshotMeta[height]
let db = Merk(snapshot.checkpointPath)
// Send this back to Tendermint:
let loadSnapshotChunkResponse = {
chunk: db.getChunkSync(chunk)
}If you're a newly-connected node, Tendermint will use this ABCI message to send you metadata about available snapshots. It's up to you to decide which of the recently-offered snapshots you'd like to accept.
When you accept a snapshot, save accepted snapshot metadata somewhere for use in the next ABCI message handler:
let { height, chunks } = offerSnapshotRequest.snapshot
let { appHash } = offerSnapshotRequest
let acceptedSnapshot = { height, chunks, appHash }No Merk interaction required here.
You're a syncing node, and you've accepted a snapshot. Here's where you receive a chunk and use it to partially-restore a Merk.
Tendermint applies chunks sequentially, so for the first chunk, you'll create a restorer. For all chunks, you'll process the chunk with this restorer. Then you'll finalize the restorer after the last chunk:
let { restore } = require('merk')
let { index, chunk } = applySnapshotChunkRequest
let { height, chunks, appHash } = acceptedSnapshot
let path = 'path/to/my.db'
// Just for the first chunk:
let restorer = restore(path, appHash, chunks)
// For all chunks:
restorer.processChunkSync(chunk)
// Just for the last chunk
if(index === chunks - 1) {
restorer.finalizeSync()
}
// Now the restored Merk may be safely opened and state sync is complete!
let db = Merk(path)