npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@aiken-lang/merkle-patricia-forestry

v1.2.0

Published

A library for working with Merkle Patricia Tries on Cardano.

Downloads

227

Readme

Merkle Patricia Forestry

This package provides a Node.js backend for working with an authenticated key/value store, a.k.a Merkle Patricia Forestry. It comes with an out-of-the-box on-disk storage solution based on level.js, but it also has an in-memory store available for debugging.

The library provides ways of constructing the trie incrementally (by inserting items into it) while keeping a low memory footprint. By default, only the topmost node is kept in memory, and children are only references to nodes stored on disk.

This package also allows producing succinct proofs for items, thus allowing the proof of membership, insertion, and deletion of items in the trie only from root hashes (and proofs!).

Installation

yarn add @aiken-lang/merkle-patricia-forestry

Overview

Constructing

new Trie(store?: Store): Trie

You can construct a new trie using new Trie, providing an (optional) store as a parameter. Using an on-disk store for non-trivial tries is strongly recommended, so only omit this argument for dummy scenarios.

import { Store, Trie } from '@aiken-lang/merkle-patricia-forestry';

// Construct a new trie with on-disk storage under the file path 'db'.
const trie = new Trie(new Store('db'));

Trie.fromList(items: Array<Item>, store?: Store): Promise<Trie>

With Item = { key: string|Buffer, value: string|Buffer }

Using Trie. fromList, you can rapidly bootstrap a new trie from many items. This method is convenient for small tries and rapid prototyping. The store is optional and defaults to in-memory when omitted.

const trie = await Trie.fromList([
  { key: 'apple', value: '🍎'},
  { key: 'blueberry', value: '🫐'},
  { key: 'cherries', value: '🍒'},
  { key: 'grapes', value: '🍇'},
  { key: 'tangerine', value: '🍊'},
  { key: 'tomato', value: '🍅'},
]);

trie.insert(key: string|Buffer, value: string|Buffer) -> Promise<Trie>

While Trie.fromList provides a convenient way to create small tries, mainly for testing purposes, the primary way of constructing tries is by repeatedly calling .insert for each item to insert. It returns the same reference to the trie as a Promise for convenience.

Note that you can only insert one item at a time, so make sure to await promises before attempting a new insert. Not doing so will trigger an exception. An exception is also raised when trying to insert an item at an already-known key. Delete first!

// Insert items one-by-one
await trie.insert('apple', '🍎');
await trie.insert('blueberry', '🫐');
await trie.insert('cherries', '🍒');
await trie.insert('grapes', '🍇');
await trie.insert('tangerine', '🍊');
await trie.insert('tomato', '🍅');

// Insert many items
const items = [
  { key: 'apple', value: '🍎'},
  { key: 'blueberry', value: '🫐'},
  { key: 'cherries', value: '🍒'},
  { key: 'grapes', value: '🍇'},
  { key: 'tangerine', value: '🍊'},
  { key: 'tomato', value: '🍅'},
];

await items.reduce(async (trie, { key, value }) => {
  return (await trie).insert(key, value);
}, trie);

[!TIP]

Both the key and value must be either strings or byte Buffer. When a string is provided, it is treated as a UTF-8 byte Buffer.

trie.delete(key: string|Buffer) -> Promise<Trie>

Similarly, the reverse operation delete is available to remove elements from the trie. It fails with an exception if the given key is not in the trie. For convenience, it returns the same reference to the trie as a Promise.

// Remove 'apple'
await trie.delete('apple');

// Throws an exception, apple is no longer in the trie.
await trie.delete('apple');

[!TIP]

The key must be either strings or byte Buffer. When a string is provided, it is treated as a UTF-8 byte Buffer.

Trie.load(store: Store): Promise<Trie>

Using Trie. load, you can load any previously constructed trie back from disk. In this case, the store argument must be provided.

import { Store, Trie } from '@aiken-lang/merkle-patricia-forestry';

// Construct a new trie with on-disk storage under the file path 'db'.
const trie = await Trie.load(new Store('db'));

Inspecting

You can inspect any trie using the util.inspect from Node.js or simply by calling console.log:

console.log(trie);
// ╔═══════════════════════════════════════════════════════════════════╗
// ║ #ee54d685370064b61cd8921f8476e54819990a67f6ebca402d1280ba1b03c75f ║
// ╚═══════════════════════════════════════════════════════════════════╝
//  ┌─ 0 #33af5a3bbf8f
//  ├─ 1 #a38f7e65ebf6
//  ├─ 3 #ac9d183ca637
//  └─ 9 #75eba4e4dae1

The output is a pretty-printed visualisation of the tree, with the entire root hash first in a box, followed by the rest of the trie. For each sub-trie, a summary of their hashes is shown using a pound sign (e.g. #eceea58af726) as well as the branch path and prefix, if any, that leads to that point.

For leaves, the remaining path (a.k.a suffix) is shown but truncated for brevity. The output also includes the key/value pertaining to that leaf.

Finally, the function is synchronous and thus doesn't have access to children beyond their hashes. You can, however, fetch more children using:

trie.fetchChildren(depth?: number = 0): Promise<()>

The depth parameter is optional and must be a positive integer when provided. It corresponds to the number of sub-levels to fetch.

await trie.fetchChildren(2);

console.log(trie);
// ╔═══════════════════════════════════════════════════════════════════╗
// ║ #ee54d685370064b61cd8921f8476e54819990a67f6ebca402d1280ba1b03c75f ║
// ╚═══════════════════════════════════════════════════════════════════╝
//  ┌─ 09ad7..[55 digits]..19d9 #33af5a3bbf8f { apple → 🍎 }
//  ├─ 1 #a38f7e65ebf6
//  │  ├─ b021f..[54 digits]..2290 #e5f9beffc856 { tomato → 🍅 }
//  │  └─ e7b4b..[54 digits]..0675 #b5e92076b81f { cherries → 🍒 }
//  ├─ 39cd4..[55 digits]..9e65 #ac9d183ca637 { blueberry → 🫐 }
//  └─ 9 #75eba4e4dae1
//     ├─ 702e3..[54 digits]..3a28 #c8b244fad188 { grapes → 🍇 }
//     └─ b19ae..[54 digits]..962c #830b96edc35b { tangerine → 🍊 }

[!TIP]

To retrieve the entire trie, use Number.MAX_SAFE_INTEGER. But be careful, for large tries may not fit in memory!

trie.save(): Promise<(Trie)>

To replace children with references again, use trie.save().

await trie.save();

console.log(trie);
// ╔═══════════════════════════════════════════════════════════════════╗
// ║ #ee54d685370064b61cd8921f8476e54819990a67f6ebca402d1280ba1b03c75f ║
// ╚═══════════════════════════════════════════════════════════════════╝
//  ┌─ 0 #33af5a3bbf8f
//  ├─ 1 #a38f7e65ebf6
//  ├─ 3 #ac9d183ca637
//  └─ 9 #75eba4e4dae1

[!NOTE]

There's, in principle, no need to manually save the trie otherwise. Operations on the trie such as insert or remove automatically modify the store.

Accessing

trie.get(key: string|Buffer): Promise<Buffer|undefined>

You can efficiently fetch any value from the trie from its key using the trie.get method.

await trie.get('cherries');
// 🍒

await trie.get('cherrie');
// undefined

trie.childAt(path: string): Promise<Trie|undefined>

You can retrieve any child from the trie at a given path. A path is a sequence of hexadecimal digits (a.k.a nibbles) given by the hash digest (blake2b-256) of the key, encoded in base16.

import blake2b from 'blake2b';

// Manually constructed, from a plain string prefix.
const cherries = await trie.childAt('1e');
// 7b4b..[54 digits]..0675 #b5e92076b81f { cherries → 🍒 }

const none = await trie.childAt('ffff');
// undefined

// Using the hash digest
const apple = await trie.childAt(
  blake2b(32).update(Buffer.from('apple')).digest('hex')
);
// 9ad7..[55 digits]..19d9 #33af5a3bbf8f { apple → 🍎 }

Proving

trie.prove(key: string|Buffer): Promise<Proof>

Let's get to the exciting part! The whole point of building a Merkle Patricia Forestry is to provide succinct proofs for items. A proof is portable and bound to both:

  1. A specific item
  2. A specific trie

Proofs are only valid for a precise trie root hash and state. So inserting (resp. removing) any item into (resp. from) the trie will invalidate any previously generated proof.

const proofTangerine = await trie.prove('tangerine');
// Proof {}

proof.verify(includingItem?: bool = true): Buffer

A proof can be verified by calling .verify on it. The verification is fully synchronous and yields a hash as a byte Buffer. If that hash matches the trie root hash, the proof is valid.

proofTangerine.verify().equals(trie.hash);
// true

Proofs can be computed in two manners. By default, they include the item in the proof, thus checking for inclusion in the trie. However, by setting includingItem to false, the proof will check for exclusion and yield the root hash of the trie without the item.

const previousHash = trie.hash;

await trie.insert('banana', '🍌');

const proofBanana = await trie.prove('banana');

proofBanana.verify(true).equals(trie.hash);
// true

proofBanana.verify(false).equals(previousHash);
// true

[!TIP]

Hence, you insert an element in the trie from just a proof and a current root hash (without the item), yielding a new root hash that includes just the item.

function insert(root, proof) {
  assert(proof.verify(false).equals(root));
  return proof.verify(true);
}

proof.toJSON(): object

Proofs are opaque but can be serialised into various formats, such as JSON. The proof format is explained in greater detail in the wiki.

proofTangerine.toJSON();
// [
//   {
//     type: 'branch',
//     skip: 0,
//     neighbors: '17a27bc4ce61078d26372800d331d6b8c4b00255080be66977c78b1554aabf8985c09af929492a871e4fae32d9d5c36e352471cd659bcdb61de08f17
// 22acc3b10eb923b0cbd24df54401d998531feead35a47a99f4deed205de4af81120f97610000000000000000000000000000000000000000000000000000000000000000
// '
//   },
//   {
//     type: 'leaf',
//     skip: 0,
//     neighbor: {
//       key: '9702e39845bfd6e0d0a5b6cb4a3a1c25262528c11bcff857867a50a0670e3a28',
//       value: 'b5898c51c32083e91b8c18c735d0ba74e08f964a20b1639c189d1e8704b78a09'
//     }
//   }
// ]

proof.toCBOR(): Buffer

JSON is cool, but proofs are ultimately meant to be passed on-chain as redeemer or datum. Thus, we provide a method .toCBOR to serialise a proof into a format compatible with the on-chain expectations.

proofTangerine.toCBOR().toString('hex');
// 9fd8799f005f58404be28f4839135e1f8f5372a90b54bb7bfaf997a5d13711bb4d7d93f9d4e04fbefa63eb4576001d8658219f928172eccb5448b4d7d62cd6d95228e13ebcbd53505840c1e96bcc431893eef34e03989814375d439faa592edf75c9e5dc10b3c30766700000000000000000000000000000000000000000000000000000000000000000ffffff

proof.toAiken(): string

For convenience, you can also generate Aiken code that works with the on-chain part of the library using .toAiken.

proofTangerine.toAiken();
// [
//   Branch { skip: 0, neighbors: #"17a27bc4ce61078d26372800d331d6b8c4b00255080be66977c78b1554aabf8985c09af929492a871e4fae32d9d5c36e352471c
// d659bcdb61de08f1722acc3b10eb923b0cbd24df54401d998531feead35a47a99f4deed205de4af81120f976100000000000000000000000000000000000000000000000
// 00000000000000000" },
//   Leaf { skip: 0, key: #"9702e39845bfd6e0d0a5b6cb4a3a1c25262528c11bcff857867a50a0670e3a28", value: #"b5898c51c32083e91b8c18c735d0ba74e08
// f964a20b1639c189d1e8704b78a09" },
// ]